git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
82 lines
3.7 KiB
JavaScript
82 lines
3.7 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { BusinessError } from "../src/errors.mjs";
|
|
import * as access from "../src/access.mjs";
|
|
|
|
// Row 193a (#1691): the viewer level. R1 (federation design r4.5, section 7) owns access.mjs and its
|
|
// tests; this file pins only what row 193 adds: a grant's level is collaborator or viewer, and covers()
|
|
// tells the two apart with a required least level. Every call into R1 goes through check() below, so a change to R1's loader
|
|
// interface edits one place.
|
|
const CONTEXT = {
|
|
providers: ["authentik"],
|
|
peers: ["factory"],
|
|
businesses: { "creative-hub": { human: "melanie", instances: ["researcher", "writer"] } },
|
|
ssoSubjects: [],
|
|
};
|
|
const check = (doc) => access.checkAccess(doc, CONTEXT);
|
|
|
|
const PROJECT = "0b6f4a52-6f1e-4c7a-9a0e-3f1d2c4b5a69";
|
|
const NOW = new Date("2026-10-20T19:30:00Z");
|
|
|
|
function file(level) {
|
|
const grant = { id: "g-1", user: "jason", business: "creative-hub", projects: ["*"], instances: ["researcher"] };
|
|
if (level !== undefined) grant.level = level;
|
|
return {
|
|
accessVersion: 1,
|
|
revision: 1,
|
|
users: [{ id: "jason", home: "local", subjects: [{ provider: "authentik", sub: "s-1" }] }],
|
|
grants: [grant],
|
|
};
|
|
}
|
|
|
|
function refuses(fn, pattern) {
|
|
assert.throws(fn, (error) => {
|
|
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
|
|
assert.equal(error.exitCode, 2);
|
|
assert.match(error.message, pattern);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
test("a grant's level is collaborator or viewer", () => {
|
|
for (const level of ["collaborator", "viewer"]) {
|
|
assert.doesNotThrow(() => check(file(level)), level);
|
|
}
|
|
});
|
|
|
|
test("any other level refuses with the key path named", () => {
|
|
for (const level of ["owner", "admin", "member", "Viewer", "viewer ", "", null, 1, ["viewer"], undefined]) {
|
|
refuses(() => check(file(level)), /grants\[0\]\.level/);
|
|
}
|
|
});
|
|
|
|
test("covers tells a viewer grant from a collaborator grant", () => {
|
|
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
|
|
const viewer = file("viewer").grants[0];
|
|
const collaborator = file("collaborator").grants[0];
|
|
// The level is the least one the action needs: share reads ask viewer; create, open, send and share ask
|
|
// collaborator (design 9).
|
|
assert.equal(access.covers(viewer, target, NOW, { level: "viewer" }), true);
|
|
assert.equal(access.covers(viewer, target, NOW, { level: "collaborator" }), false);
|
|
assert.equal(access.covers(collaborator, target, NOW, { level: "viewer" }), true);
|
|
assert.equal(access.covers(collaborator, target, NOW, { level: "collaborator" }), true);
|
|
});
|
|
|
|
test("covers refuses a call with no level, so a viewer grant can't slip past a collaborator check", () => {
|
|
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
|
|
const viewer = file("viewer").grants[0];
|
|
assert.throws(() => access.covers(viewer, target, NOW), TypeError);
|
|
assert.throws(() => access.covers(viewer, target, NOW, {}), TypeError);
|
|
});
|
|
|
|
test("a viewer grant still needs its business, project, instance and expiry to cover", () => {
|
|
const viewer = { ...file("viewer").grants[0], projects: [PROJECT], expires: "2026-10-21T00:00:00Z" };
|
|
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
|
|
const read = { level: "viewer" };
|
|
assert.equal(access.covers(viewer, target, NOW, read), true);
|
|
assert.equal(access.covers(viewer, { ...target, business: "other" }, NOW, read), false);
|
|
assert.equal(access.covers(viewer, { ...target, project: "7c1e2d3f-0a9b-4c8d-8e7f-6a5b4c3d2e1f" }, NOW, read), false);
|
|
assert.equal(access.covers(viewer, { ...target, instance: "writer" }, NOW, read), false);
|
|
assert.equal(access.covers(viewer, target, new Date("2026-10-21T00:00:01Z"), read), false);
|
|
});
|