Skip to content

Commit 5462c64

Browse files
aspiersclaude
andcommitted
feat(permissions): implement org.hypercerts and app.certified CRUD permission sets
Add the two permission-set lexicons designed in docs/design/permission-sets.md: - org.hypercerts.permissions.crud — CRUD over all org.hypercerts.* records - app.certified.permissions.crud — CRUD over all app.certified.* records Each enumerates every type:"record" collection in its namespace (the spec forbids wildcards inside a permission set) with action create/update/delete. permission-set is a lexicon primitive that lex gen-api cannot codegen (it has no TypeScript shape), so these files are published as-is but excluded from the codegen globs (gen-api/gen-md/gen-ts) and from generated/exports.ts. The full `npm run check` (gen-api, lint, typecheck, build, test, style) passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d3aa70 commit 5462c64

5 files changed

Lines changed: 91 additions & 4 deletions

File tree

.changeset/permission-sets-crud.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@hypercerts-org/lexicon": minor
3+
---
4+
5+
Add two permission-set lexicons — `org.hypercerts.permissions.crud` and `app.certified.permissions.crud` — each granting create/update/delete over every record collection in its namespace.
6+
7+
A [permission set](https://atproto.com/specs/permission#permission-sets) lets any AT Protocol app request a whole bundle of `repo:` scopes with a single `include:<nsid>` OAuth scope, instead of enumerating each collection by hand. The user's PDS resolves and expands the set during the OAuth grant; the same published set can also be consumed by services (e.g. the Certified group service) when expanding API-key scopes.
8+
9+
Permission sets are published as-is (they are the source of truth for what gets published to AT Protocol) but have no TypeScript shape — `lex gen-api` cannot generate code for `permission-set` defs. They are therefore excluded from the codegen globs (`gen-api`/`gen-md`/`gen-ts`) and from `generated/exports.ts`, while still shipping as raw lexicon JSON.
10+
11+
Collection lists are enumerated explicitly because the spec forbids wildcards inside a permission set; they must be kept in sync as record types are added. See `docs/design/permission-sets.md`.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"lexicon": 1,
3+
"id": "app.certified.permissions.crud",
4+
"defs": {
5+
"main": {
6+
"type": "permission-set",
7+
"description": "Permission set granting create, update, and delete on every app.certified.* record collection.",
8+
"title": "Full CRUD on Certified record collections",
9+
"detail": "Create, update, and delete records in every app.certified.* record collection. Requested as a single include: scope instead of enumerating each repo: scope.",
10+
"permissions": [
11+
{
12+
"type": "permission",
13+
"resource": "repo",
14+
"collection": [
15+
"app.certified.actor.organization",
16+
"app.certified.actor.profile",
17+
"app.certified.badge.award",
18+
"app.certified.badge.definition",
19+
"app.certified.badge.response",
20+
"app.certified.graph.follow",
21+
"app.certified.link.evm",
22+
"app.certified.location",
23+
"app.certified.signature.proof"
24+
],
25+
"action": ["create", "update", "delete"]
26+
}
27+
]
28+
}
29+
}
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"lexicon": 1,
3+
"id": "org.hypercerts.permissions.crud",
4+
"defs": {
5+
"main": {
6+
"type": "permission-set",
7+
"description": "Permission set granting create, update, and delete on every org.hypercerts.* record collection.",
8+
"title": "Full CRUD on Hypercerts record collections",
9+
"detail": "Create, update, and delete records in every org.hypercerts.* record collection. Requested as a single include: scope instead of enumerating each repo: scope.",
10+
"permissions": [
11+
{
12+
"type": "permission",
13+
"resource": "repo",
14+
"collection": [
15+
"org.hypercerts.claim.activity",
16+
"org.hypercerts.claim.contribution",
17+
"org.hypercerts.claim.contributorInformation",
18+
"org.hypercerts.claim.rights",
19+
"org.hypercerts.collection",
20+
"org.hypercerts.context.acknowledgement",
21+
"org.hypercerts.context.attachment",
22+
"org.hypercerts.context.evaluation",
23+
"org.hypercerts.context.measurement",
24+
"org.hypercerts.funding.receipt",
25+
"org.hypercerts.workscope.tag"
26+
],
27+
"action": ["create", "update", "delete"]
28+
}
29+
]
30+
}
31+
}
32+
}

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@
4242
"check": "npm run gen-api && npm run lint && npm run typecheck && npm run build && npm run test",
4343
"build": "rollup -c && npm run build:types",
4444
"build:types": "tsc --project tsconfig.build.json",
45-
"gen-api": "find ./lexicons -name '*.json' | xargs lex gen-api --yes ./generated && npm run gen-index",
46-
"gen-md": "find ./lexicons -name '*.json' | xargs lex gen-md --yes ./lexicons.md",
45+
"//permissions": "permission-set lexicons (lexicons/**/permissions/*.json) are published as-is but have no TS shape; lex gen-* cannot codegen them, so they are excluded from the codegen globs below.",
46+
"gen-api": "find ./lexicons -name '*.json' -not -path '*/permissions/*' | xargs lex gen-api --yes ./generated && npm run gen-index",
47+
"gen-md": "find ./lexicons -name '*.json' -not -path '*/permissions/*' | xargs lex gen-md --yes ./lexicons.md",
4748
"gen-schemas-md": "node ./scripts/generate-schemas.js",
4849
"//gen-ts": "UNUSED - use gen-api instead",
49-
"gen-ts": "find ./lexicons -name '*.json' | xargs lex gen-ts-obj > generated/DO-NOT-USE-lexicons.ts",
50+
"gen-ts": "find ./lexicons -name '*.json' -not -path '*/permissions/*' | xargs lex gen-ts-obj > generated/DO-NOT-USE-lexicons.ts",
5051
"gen-index": "node ./scripts/generate-exports.js",
5152
"lex": "lex",
5253
"test": "vitest run",

scripts/generate-exports.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,24 @@ function readNsid(filePath) {
163163
return lexicon.lexicon === 1 ? lexicon.id : null;
164164
}
165165

166+
/**
167+
* Is this lexicon a permission-set? Permission sets are published as-is but
168+
* have no generated TypeScript type (lex gen-api cannot process them), so they
169+
* are excluded from the generated exports.
170+
*/
171+
function isPermissionSet(filePath) {
172+
const fullPath = join(lexiconsDir, filePath);
173+
const lexicon = JSON.parse(readFileSync(fullPath, "utf-8"));
174+
return lexicon?.defs?.main?.type === "permission-set";
175+
}
176+
166177
/**
167178
* Generate generated/exports.ts content
168179
*/
169180
function generateIndex() {
170-
const jsonFiles = findJsonFiles(lexiconsDir).sort();
181+
const jsonFiles = findJsonFiles(lexiconsDir)
182+
.filter((filePath) => !isPermissionSet(filePath))
183+
.sort();
171184

172185
const lexicons = jsonFiles.map((filePath) => ({
173186
path: filePath,

0 commit comments

Comments
 (0)