Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ALTER TABLE coordinator_invites ADD COLUMN invite_kind TEXT;
ALTER TABLE coordinator_invites ADD COLUMN policy_team_id TEXT;
ALTER TABLE coordinator_invites ADD COLUMN target_identity_id TEXT;
ALTER TABLE coordinator_invites ADD COLUMN reviewed_preview_digest TEXT;

UPDATE coordinator_invites
SET invite_kind = CASE
WHEN operation_id IS NOT NULL THEN 'project_share'
ELSE 'legacy_enrollment'
END
WHERE invite_kind IS NULL;
6 changes: 5 additions & 1 deletion packages/cloudflare-coordinator-worker/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ CREATE TABLE IF NOT EXISTS coordinator_invites (
recipient_display_name TEXT,
recipient_device_display_name TEXT,
trust_state TEXT,
bootstrap_grant_id TEXT
bootstrap_grant_id TEXT,
invite_kind TEXT,
policy_team_id TEXT,
target_identity_id TEXT,
reviewed_preview_digest TEXT
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_coordinator_invites_operation_id
Expand Down
44 changes: 44 additions & 0 deletions packages/cloudflare-coordinator-worker/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,50 @@ describe("createCloudflareCoordinatorWorker", () => {
});
});

it("migration 0009 classifies existing invites and adds recipient invitation metadata", () => {
db.exec(`
DROP TABLE coordinator_invites;
CREATE TABLE coordinator_invites (
invite_id TEXT PRIMARY KEY, group_id TEXT NOT NULL, token TEXT NOT NULL UNIQUE,
policy TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_by TEXT, team_name_snapshot TEXT, revoked_at TEXT, operation_id TEXT
);
INSERT INTO coordinator_invites(invite_id, group_id, token, policy, expires_at, created_at, operation_id)
VALUES
('legacy-invite', 'g1', 'legacy-token', 'auto_admit', '2099-01-01T00:00:00Z',
'2026-03-28T00:00:00Z', NULL),
('project-invite', 'g1', 'project-token', 'auto_admit', '2099-01-01T00:00:00Z',
'2026-03-28T00:00:00Z', 'share_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
`);
const migration = readFileSync(
join(import.meta.dirname, "../migrations/0009_add_recipient_invite_kinds.sql"),
"utf8",
);
db.exec(migration);

expect(
db
.prepare(`SELECT invite_id, invite_kind, policy_team_id, target_identity_id,
reviewed_preview_digest FROM coordinator_invites ORDER BY invite_id`)
.all(),
).toEqual([
{
invite_id: "legacy-invite",
invite_kind: "legacy_enrollment",
policy_team_id: null,
target_identity_id: null,
reviewed_preview_digest: null,
},
{
invite_id: "project-invite",
invite_kind: "project_share",
policy_team_id: null,
target_identity_id: null,
reviewed_preview_digest: null,
},
]);
});

it("serves coordinator admin data through the worker entrypoint", async () => {
const store = new D1CoordinatorStore(d1db);
await store.createGroup("g1", "Team Alpha");
Expand Down
124 changes: 124 additions & 0 deletions packages/cloudflare-coordinator-worker/test/worker.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,4 +757,128 @@ function signHeaders(identity: TestIdentity, method: string, url: string, body:
);
expect(payload.items.some((item) => item.device_id === devices[3]?.deviceId)).toBe(false);
});

it("persists explicit Team and add-device invitation bindings without mutating coordinator memberships", async () => {
const device = createIdentity();
const otherDevice = createIdentity();
const adminHeaders = {
"content-type": "application/json",
"X-Codemem-Coordinator-Admin": "test-secret",
};
await env.COORDINATOR_DB.prepare(
"INSERT INTO groups (group_id, display_name, created_at) VALUES ('g1', 'Coordinator Team', ?)",
)
.bind("2026-07-21T00:00:00Z")
.run();
const createInvite = async (body: Record<string, unknown>) => {
const response = await exports.default.fetch("https://example.com/v1/admin/invites", {
method: "POST",
headers: adminHeaders,
body: JSON.stringify({
group_id: "g1",
policy: "auto_admit",
expires_at: "2099-01-01T00:00:00Z",
coordinator_url: "https://example.com",
...body,
}),
});
expect(response.status).toBe(200);
return (await response.json()) as { payload: InvitePayload };
};

const team = await createInvite({
invite_kind: "team_member",
policy_team_id: "policy-team-1",
reviewed_preview_digest: "a".repeat(64),
});
expect(team.payload).toMatchObject({
kind: "team_member",
policy_team_id: "policy-team-1",
reviewed_preview_digest: "a".repeat(64),
});
const inspect = await exports.default.fetch("https://example.com/v1/invites/inspect", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token: team.payload.token }),
});
expect(await inspect.json()).toMatchObject({
kind: "team_member",
policy_team_id: "policy-team-1",
bound: false,
});
const acceptBody = {
token: team.payload.token,
invite_kind: "team_member",
identity_id: "identity-brian",
device_id: device.deviceId,
public_key: device.publicKey,
fingerprint: device.fingerprint,
};
const accept = () =>
exports.default.fetch("https://example.com/v1/join", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(acceptBody),
});
expect(await (await accept()).json()).toMatchObject({ status: "accepted" });
expect(await (await accept()).json()).toMatchObject({ status: "existing" });
const changedDevice = await exports.default.fetch("https://example.com/v1/join", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
...acceptBody,
device_id: otherDevice.deviceId,
public_key: otherDevice.publicKey,
fingerprint: otherDevice.fingerprint,
}),
});
expect(changedDevice.status).toBe(409);
expect(await changedDevice.json()).toEqual({ error: "invite_already_bound" });

const addDevice = await createInvite({
invite_kind: "add_device",
target_identity_id: "identity-brian",
reviewed_preview_digest: "b".repeat(64),
});
const wrongIdentity = await exports.default.fetch("https://example.com/v1/join", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
token: addDevice.payload.token,
invite_kind: "add_device",
identity_id: "identity-other",
device_id: otherDevice.deviceId,
public_key: otherDevice.publicKey,
fingerprint: otherDevice.fingerprint,
}),
});
expect(wrongIdentity.status).toBe(409);
expect(await wrongIdentity.json()).toEqual({ error: "invite_identity_conflict" });

expect(
await env.COORDINATOR_DB.prepare("SELECT COUNT(*) AS count FROM enrolled_devices")
.first<{ count: number }>(),
).toEqual({ count: 0 });
expect(
await env.COORDINATOR_DB.prepare("SELECT COUNT(*) AS count FROM coordinator_scope_memberships")
.first<{ count: number }>(),
).toEqual({ count: 0 });

await env.COORDINATOR_DB.prepare(
"UPDATE coordinator_invites SET revoked_at = ? WHERE token_digest IS NOT NULL AND invite_kind = 'team_member'",
)
.bind("2026-07-21T01:00:00Z")
.run();
expect((await accept()).status).toBe(400);
await env.COORDINATOR_DB.prepare("UPDATE groups SET archived_at = ? WHERE group_id = 'g1'")
.bind("2026-07-21T01:00:00Z")
.run();
const archived = await exports.default.fetch("https://example.com/v1/invites/inspect", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token: addDevice.payload.token }),
});
expect(archived.status).toBe(409);
expect(await archived.json()).toEqual({ error: "group_archived" });
});
});
Loading