import assert from "node:assert/strict"; import { readdir, readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; // Use a checkout with the draft space/space-ref extensions, without vendoring // upstream schemas or depending on an incompatible published SDK release. const root = fileURLToPath(new URL("../", import.meta.url)); const atproto = resolve(process.argv[2] ?? resolve(root, "../atproto")); const moduleAt = (path) => import(pathToFileURL(resolve(atproto, path)).href); const { lexiconDoc, Lexicons } = await moduleAt( "packages/lexicon/dist/index.js", ); const { LexiconIterableIndexer, LexiconSchemaBuilder } = await moduleAt( "packages/lex/lex-document/dist/index.js", ); const { parseAtUriString } = await moduleAt("packages/syntax/dist/index.js"); const { IncludeScope, RpcPermission, SpacePermission } = await moduleAt( "packages/oauth/oauth-scopes/dist/index.js", ); const ns = "group.opensocial."; const directory = resolve(root, "lexicons/group/opensocial"); const files = (await readdir(directory)) .filter((file) => file.endsWith(".json")) .sort(); const docs = await Promise.all( files.map(async (file) => { const doc = JSON.parse(await readFile(resolve(directory, file), "utf8")); assert.equal(doc.id, ns + file.slice(0, -5), `NSID/path mismatch: ${file}`); // Also catch unsupported fields that the schema parser would silently strip. assert.deepEqual(lexiconDoc.parse(doc), doc, `Invalid schema: ${file}`); return doc; }), ); assert.equal(new Set(docs.map((doc) => doc.id)).size, docs.length); const lexicons = new Lexicons(structuredClone(docs)); function checkRefs(value, id) { if (!value || typeof value !== "object") return; if (value.type === "ref" || value.type === "union") { for (const ref of value.refs ?? [value.ref]) { assert.ok( lexicons.getDef(ref.startsWith("#") ? id + ref : ref), `Missing ref: ${ref}`, ); } } if (value.type === "space") { assert.equal(value.key, "literal:self"); assert.equal(new Set(value.collections).size, value.collections.length); for (const collection of value.collections) { assert.equal( lexicons.getDef(collection)?.type, "record", `Not a record: ${collection}`, ); } } for (const item of Object.values(value)) checkRefs(item, id); } for (const doc of docs) checkRefs(doc, doc.id); const schemas = await LexiconSchemaBuilder.buildAll( new LexiconIterableIndexer(docs), ); // Guard against accidentally leaving an entry from the proposal unimplemented. const proposal = await readFile(resolve(root, "proposal.md"), "utf8"); for (const match of proposal.matchAll( /^\| `group\.opensocial\.([A-Za-z]+)` \|/gm, )) { assert.ok(lexicons.get(ns + match[1]), `Missing proposal entry: ${match[1]}`); } for (const match of proposal .split("### Methods")[1] .matchAll(/^\| `([A-Za-z]+)` \|/gm)) { assert.ok( ["query", "procedure"].includes(lexicons.getDef(ns + match[1])?.type), `Missing method: ${match[1]}`, ); } assert.equal(lexicons.getDef(ns + "createReport")?.type, "procedure"); const examples = JSON.parse( await readFile(resolve(root, "examples/records.json"), "utf8"), ); const covered = new Set(); const authorityAuthored = new Set( [ "profile", "rule", "role", "permissions", "space", "membership", "access", "label", ].map((name) => ns + name), ); for (const { uri, value } of examples) { const parsed = parseAtUriString(uri); assert.ok(parsed.success, `Invalid example URI: ${uri}`); assert.equal(parsed.value.collection, value.$type); const schema = schemas.get(value.$type + "#main"); schema.parse(value); lexicons.assertValidRecord(value.$type, value); covered.add(value.$type); const author = parsed.value.author ?? parsed.value.authority; if (authorityAuthored.has(value.$type)) { assert.equal(author, parsed.value.authority, `Wrong record author: ${uri}`); } if (value.$type === ns + "membership") assert.equal(parsed.value.rkey, value.member); if (value.$type === ns + "acceptance") { assert.equal(parsed.value.rkey, "self"); // These fixtures demonstrate a bidirectional membership pair. This is not // a requirement that all acceptance records have a current membership. assert.ok( examples.some(({ uri: candidateUri, value: candidate }) => { if ( candidate.$type !== ns + "membership" || candidate.member !== author ) return false; const candidateParts = parseAtUriString(candidateUri); return ( candidateParts.success && candidateParts.value.authority === parsed.value.authority ); }), `Acceptance example has no matching membership: ${uri}`, ); } if (value.$type === ns + "invite") { assert.equal(author, value.group); assert.equal(parsed.value.authority, value.invitee); } } for (const doc of docs) { if (doc.defs.main?.type === "record") assert.ok(covered.has(doc.id), `No example: ${doc.id}`); } const group = "did:plc:abcdefghijklmnopqrstuvwx"; const member = "did:plc:zyxwvutsrqponmlkjihgfedcb"; const space = `at://${group}/space/${ns}members/self`; const recordUri = `${space}/${member}/${ns}acceptance/self`; const cid = "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"; const subject = { $type: ns + "defs#recordSubject", uri: recordUri, cid }; const input = (name, value) => lexicons.assertValidXrpcInput(ns + name, value); const output = (name, value) => lexicons.assertValidXrpcOutput(ns + name, value); const params = (name, value) => lexicons.assertValidXrpcParams(ns + name, value); const rejects = (fn) => assert.throws(fn); // Exercise permission-set expansion with the actual OAuth scope implementation: // valid Lexicon shape alone does not establish that permissions survive expansion. const permissionSets = new Map(); const authorizedMethods = new Set(); for (const doc of docs) { if (doc.defs.main?.type !== "permission-set") continue; const expanded = new IncludeScope(doc.id).toPermissions(doc.defs.main); assert.equal( expanded.length, doc.defs.main.permissions.length, `Dropped permission in ${doc.id}`, ); permissionSets.set(doc.id, expanded); for (const permission of expanded) { if (permission instanceof RpcPermission) { for (const method of permission.lxm) { assert.ok( ["query", "procedure"].includes(lexicons.getDef(method)?.type), `Unknown RPC: ${method}`, ); authorizedMethods.add(method); } } else { assert.ok( permission instanceof SpacePermission, `Unexpected resource in ${doc.id}`, ); assert.equal(lexicons.getDef(permission.type)?.type, "space"); } } } for (const doc of docs) { if (["query", "procedure"].includes(doc.defs.main?.type)) { assert.ok( authorizedMethods.has(doc.id), `No permission set covers ${doc.id}`, ); } } const allowsSpace = (set, target) => permissionSets .get(ns + set) .some( (permission) => permission instanceof SpacePermission && permission.withResolvedAuthority(member).matches(target), ); const allowsRpc = (set, method) => permissionSets .get(ns + set) .some( (permission) => permission instanceof RpcPermission && permission.matches({ aud: `${group}#atproto_space_host`, lxm: ns + method, }), ); const memberSpace = { type: ns + "members", authority: group, skey: "self" }; assert.ok(allowsSpace("authRead", { ...memberSpace, action: "read" })); assert.ok( !allowsSpace("authRead", { ...memberSpace, action: "create", collection: ns + "acceptance", }), ); assert.ok( allowsSpace("authParticipate", { ...memberSpace, action: "create", collection: ns + "acceptance", }), ); for (const collection of [ "membership", "permissions", "access", "role", "label", ]) { assert.ok( !allowsSpace("authParticipate", { ...memberSpace, action: "create", collection: ns + collection, }), ); } assert.ok( !allowsSpace("authParticipate", { ...memberSpace, manage: "delete" }), ); assert.ok( !allowsSpace("authRead", { ...memberSpace, type: "com.atmoboards.forum", action: "read", }), ); const inbox = { type: ns + "invites", authority: member, skey: "self", action: "read", }; assert.ok(allowsSpace("authParticipate", inbox)); assert.ok(!allowsSpace("authParticipate", { ...inbox, authority: group })); assert.ok(allowsRpc("authParticipate", "createReport")); assert.ok(!allowsRpc("authParticipate", "assignRoles")); assert.ok(!allowsRpc("authParticipate", "applyLabel")); assert.ok(allowsRpc("authManageMembers", "assignRoles")); assert.ok(!allowsRpc("authManageMembers", "putRole")); assert.ok(allowsRpc("authModerate", "applyLabel")); assert.ok(allowsRpc("authManageGroup", "putRole")); input("updateProfile", { group, profile: { displayName: "Garden", joinPolicy: "approval", url: "https://garden.example.com/", }, }); rejects(() => input("updateProfile", { group, profile: { displayName: "Garden", joinPolicy: "approval", url: "not a URL", }, }), ); // Permissioned URIs must round-trip, while space references reject record URIs. input("createReport", { group, subject, reasonType: "com.atproto.moderation.defs#reasonOther", }); rejects(() => input("createReport", { group, subject, reasonType: "" })); rejects(() => lexicons.assertValidXrpcOutput(ns + "getSubjectHistory", { events: [ { id: "report-1", createdAt: "2026-09-29T12:00:00.000Z", event: { $type: ns + "defs#reportEvent", reasonType: "" }, }, ], }), ); input("applyLabel", { group, subject, val: "!hide" }); output("applyLabel", { uri: recordUri, cid }); input("updateSpace", { group, space, access: { public: false, readRoles: ["member"], grants: [] }, }); rejects(() => input("updateSpace", { group, space: recordUri, access: { public: false, readRoles: [], grants: [] }, }), ); rejects(() => input("applyLabel", { group, subject: { $type: ns + "defs#accountSubject", did: "not-a-did" }, val: "!hide", }), ); rejects(() => input("applyLabel", { group, subject, val: "" })); // Replacement operations require the complete new permission-bearing payload. input("assignRoles", { group, member, roles: [] }); rejects(() => input("assignRoles", { group, member })); rejects(() => input("assignRoles", { group, member, roles: ["invalid/role"] })); input("putRole", { group, role: "helper", value: { displayName: "Helper" }, actions: ["invite"], assignable: [], }); rejects(() => input("putRole", { group, role: "helper", value: { displayName: "Helper" }, actions: ["invite"], }), ); rejects(() => input("updateSpace", { group, space, access: { public: false } }), ); // Member-authored acceptance needs no group role or permission payload. lexicons.assertValidRecord(ns + "acceptance", { $type: ns + "acceptance", createdAt: "2026-09-29T12:00:00.000Z", }); input("requestJoin", { group }); output("requestJoin", { status: "admitted", membership: { uri: recordUri, cid }, }); output("requestJoin", { status: "pending", request: { id: "request-1", member, createdAt: "2026-09-29T12:00:00.000Z" }, }); rejects(() => output("requestJoin", { status: "unknown" })); // Moderator transitions and query bounds are constrained at the schema layer. input("resolveSubject", { group, subject, status: "escalated", note: "Needs another moderator.", }); rejects(() => input("resolveSubject", { group, subject, status: "open" })); params("getSubjectHistory", { group, subject: recordUri, cid, limit: 100 }); params("getSubjectHistory", { group, subject: member }); params("uploadImage", { group }); rejects(() => params("uploadImage", {})); rejects(() => params("listSubjects", { group, limit: 101 })); rejects(() => params("listJoinRequests", { group, limit: 0 })); console.log( `Validated ${docs.length} documents, compiled ${schemas.size} definitions, checked ${examples.length} records, ${permissionSets.size} permission sets, and API boundary cases.`, );