···1010import type { ContrailConfig } from "@atmo-dev/contrail";
11111212/** Return the sorted list of XRPC method NSIDs (queries + procedures) in a
1313- * generated lexicon set — the same list that ends up in `<ns>.permissionSet`'s
1313+ * generated lexicon set — the same list that ends up in `<ns>.authFull`'s
1414 * `lxm`. Use when the permission set isn't published yet and you need the
1515 * scoped method list inline (e.g. in an OAuth client config).
1616 *
···956956 methodNsids.sort();
957957958958 const psConfig = config.permissionSet ?? {};
959959+ const nsPrefix = `${ns}.`;
960960+961961+ // Auto-include any configured collections that live under the namespace.
962962+ // Cross-namespace collections (e.g. `app.bsky.*`) can't appear here —
963963+ // permission-set lexicons can only reference NSIDs in their own namespace —
964964+ // so callers must declare those as separate scopes in the OAuth client config.
965965+ const autoCollections = Object.values(config.collections)
966966+ .map((c) => c.collection)
967967+ .filter((nsid) => nsid === ns || nsid.startsWith(nsPrefix))
968968+ .sort();
959969960970 // Permission-set lexicons can only reference NSIDs under their own namespace.
961971 // Validate `additional` entries before we emit and produce an invalid schema.
962962- const nsPrefix = `${ns}.`;
963972 for (const [i, perm] of (psConfig.additional ?? []).entries()) {
964973 const p = perm as { resource?: string; lxm?: string[]; collection?: string[] };
965974 const offending: string[] = [];
···979988 }
980989 }
981990982982- writeLexicon(`${ns}.permissionSet`, {
991991+ writeLexicon(`${ns}.authFull`, {
983992 lexicon: 1,
984984- id: `${ns}.permissionSet`,
993993+ id: `${ns}.authFull`,
985994 defs: {
986995 main: {
987996 type: "permission-set",
988997 title: psConfig.title ?? ns,
989998 description:
990990- psConfig.description ?? `All XRPC methods exposed by the ${ns} service.`,
999999+ psConfig.description ?? `Full access to the ${ns} service`,
9911000 permissions: [
9921001 {
9931002 type: "permission",
···10001009 aud: "*",
10011010 lxm: methodNsids,
10021011 },
10121012+ ...(autoCollections.length > 0
10131013+ ? [{ type: "permission" as const, resource: "repo" as const, collection: autoCollections }]
10141014+ : []),
10031015 ...(psConfig.additional ?? []),
10041016 ],
10051017 },
+30-3
packages/lexicons/tests/generate.test.ts
···232232 expect(methods).toContain("test.app.post.getRecord");
233233 expect(methods).toContain("test.app.getProfile");
234234 // Does not include non-method defs (e.g. `defs`, records, permission-set).
235235- expect(methods).not.toContain("test.app.permissionSet");
235235+ expect(methods).not.toContain("test.app.authFull");
236236 });
237237238238- it("listXrpcMethods matches the permissionSet lxm list for the same config", () => {
238238+ it("listXrpcMethods matches the authFull lxm list for the same config", () => {
239239 const methods = listXrpcMethods(BASIC_CONFIG, { rootDir: ROOT_DIR, lexiconDirs: [] });
240240 const lexicons = generate(BASIC_CONFIG);
241241- const ps = (lexicons["test.app.permissionSet"] as any).defs.main.permissions[0];
241241+ const ps = (lexicons["test.app.authFull"] as any).defs.main.permissions[0];
242242 expect(ps.lxm).toEqual(methods);
243243+ });
244244+245245+ it("authFull auto-includes same-namespace collections in a repo permission", () => {
246246+ const config: ContrailConfig = {
247247+ namespace: "test.app",
248248+ collections: {
249249+ local: { collection: "test.app.thing" },
250250+ external: { collection: "xyz.other.thing" },
251251+ },
252252+ };
253253+ const lexicons = generate(config);
254254+ const perms = (lexicons["test.app.authFull"] as any).defs.main.permissions;
255255+ const repoEntry = perms.find((p: any) => p.resource === "repo");
256256+ expect(repoEntry).toBeDefined();
257257+ expect(repoEntry.collection).toEqual(["test.app.thing"]);
258258+ });
259259+260260+ it("authFull omits the repo permission entry when no collections match the namespace", () => {
261261+ const config: ContrailConfig = {
262262+ namespace: "test.app",
263263+ collections: {
264264+ external: { collection: "xyz.other.thing" },
265265+ },
266266+ };
267267+ const lexicons = generate(config);
268268+ const perms = (lexicons["test.app.authFull"] as any).defs.main.permissions;
269269+ expect(perms.find((p: any) => p.resource === "repo")).toBeUndefined();
243270 });
244271245272 it("includes realtime + community + spaces endpoints when those modules are enabled", () => {
+1-1
packages/contrail/src/core/types.ts
···166166 * and hydrates `record.labels` onto `listRecords` / `getRecord` / profile
167167 * responses gated by the caller's `atproto-accept-labelers` header. */
168168 labels?: import("./labels/types").LabelsConfig;
169169- /** Customize the auto-generated `<namespace>.permissionSet` lexicon. */
169169+ /** Customize the auto-generated `<namespace>.authFull` lexicon. */
170170 permissionSet?: PermissionSetConfig;
171171}
172172