chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The space type private repositories are has a lexicon, dev.knotless.space
docs/design/access.md+8−3
| @@ -62,9 +62,14 @@ The scope is built from the same constants the records and calls use, so a | ||
new record kind is asked for as soon as it is added. A session from before | ||
then asks again at its next login. | ||
| ||
The space scope names every space type. Naming `dev.knotless.space` alone | ||
would make the authorization server resolve that type's lexicon, which is | ||
not published. A PDS without spaces drops the scope it does not know. | ||
The space scope names every space type. An authorization server asked | ||
for a space type by name resolves that type's lexicon as the account | ||
approves, and refuses the login if it cannot. A private repository's | ||
type has one, `dev.knotless.space`, published with knotless's other | ||
lexicons, which lists the collections its space holds. An organization's | ||
space has none: its type, `dev.knotless.org`, is the NSID of the | ||
organization's record, whose lexicon is no space. A PDS without spaces | ||
drops the scope it does not know. | ||
| ||
## Private repositories | ||
| ||
docs/design/gateway.md+2−2
| @@ -62,8 +62,8 @@ record in public, its `orgMember` records in public and in its spaces, | ||
its own space for members, and | ||
making and changing the members of its spaces, never deleting one. Space | ||
types go unnamed | ||
(`space:*`): naming one has the PDS look up its lexicon, which is not | ||
published. | ||
(`space:*`): naming one has the PDS look up its lexicon, and an | ||
organization's space type has none ([Scopes](access.md#scopes)). | ||
`redirect` is left out, since a gateway never renames a repository. The | ||
scope is `atproto repo:dev.knotless.repo repo:dev.knotless.ref | ||
repo:dev.knotless.claim repo:dev.knotless.pack repo:dev.knotless.org | ||
harness/lexicons.mjs+33−1
| @@ -59,9 +59,20 @@ try { | ||
} | ||
linted = e.stdout | ||
} | ||
// goat 0.2.6 knows no space lexicon (`"type": "space"`, from the | ||
// permissioned-data alpha) and refuses the whole file, so those files are | ||
// checked below instead, against what the alpha's @atproto/lex-document | ||
// takes. Only that refusal is let go, and only for a file whose main is a | ||
// space: any other finding of it stands. | ||
const spaces = new Set( | ||
files(dir).filter((f) => JSON.parse(readFileSync(f, 'utf8')).defs?.main?.type === 'space').map((f) => relative(root, f)), | ||
) | ||
const unknownSpace = (found) => | ||
found['lint-name'] === 'schema-json-parse' && found.message === 'unexpected schema type: space' && spaces.has(found['file-path']) | ||
for (const line of linted.split('\n').filter(Boolean)) { | ||
const found = JSON.parse(line) | ||
if (!kept.has(`${found.nsid} ${found['lint-name']}`)) say(found.nsid, `${found['lint-name']}: ${found.message}`) | ||
if (unknownSpace(found)) continue | ||
if (!kept.has(`${found.nsid} ${found['lint-name']}`)) say(found.nsid ?? found['file-path'], `${found['lint-name']}: ${found.message}`) | ||
} | ||
| ||
/// Walks every schema in `def`, calling `visit(schema, path)`. | ||
| @@ -101,6 +112,22 @@ for (const file of files(dir)) { | ||
} | ||
} | ||
| ||
// A space lexicon, as the alpha's lexiconSpaceSchema takes it: a key, a | ||
// name of 1 to 64 characters for consent screens, and its collections, | ||
// each a record lexicon here. It is the file's only def, as goat, which | ||
// skips the file, lints none of it. | ||
const nsids = new Set([...now.values()].filter((d) => d.defs?.main?.type === 'record').map((d) => d.id)) | ||
for (const file of spaces) { | ||
const doc = now.get(file) | ||
const at = `${doc.id}#main` | ||
const def = doc.defs.main | ||
if (Object.keys(doc.defs).length > 1) say(doc.id, 'a space lexicon has its main def alone') | ||
if (!/^(any|nsid|tid|literal:.+)$/.test(def.key ?? '')) say(at, 'a space needs a key: any, nsid, tid or literal:<value>') | ||
if (typeof def.name !== 'string' || def.name.length < 1 || def.name.length > 64) say(at, 'a space needs a name of 1 to 64 characters') | ||
if (!Array.isArray(def.collections)) say(at, 'a space lists its collections') | ||
for (const c of def.collections ?? []) if (!nsids.has(c)) say(`${at}.collections`, `${c} is not a record lexicon here`) | ||
} | ||
| ||
// What each schema in `base` was, against what it is now. | ||
const shape = (s) => JSON.stringify({ type: s.type, format: s.format, ref: s.ref, refs: s.refs?.slice().sort() }) | ||
const loosened = (before, after, path) => { | ||
| @@ -164,6 +191,11 @@ if (base) { | ||
const at = `${before.id}#${name}` | ||
if (!after.defs?.[name]) say(at, `removed since ${base}`) | ||
else loosened(def, after.defs[name], at) | ||
// A scope naming a space type alone is granted the collections its | ||
// lexicon lists, so taking one out takes writes from those logins. | ||
if (def.type === 'space') | ||
for (const c of def.collections ?? []) | ||
if (!(after.defs?.[name]?.collections ?? []).includes(c)) say(`${at}.collections`, `${c} was taken out`) | ||
} | ||
} | ||
} | ||
lexicons/dev/knotless/space.jsonnew+31−0
| @@ -0,0 +1,31 @@ | ||
{ | ||
"lexicon": 1, | ||
"id": "dev.knotless.space", | ||
"defs": { | ||
"main": { | ||
"type": "space", | ||
"key": "any", | ||
"name": "Private repository", | ||
"description": "A private repository on knotless, keyed by its name, with its owner as its authority. Its members alone read it. The owner's records of the repository are in their repo in the space, as a public repository's are in their public repo; each member writes their pulls, issues, comments, reactions and labels in theirs, and a runner its checks, so that what is said of the code is as private as the code. An organization's gateway keeps a record there of each of its members, so the repository's readers know who of them pushes to it.", | ||
"collections": [ | ||
"dev.knotless.repo", | ||
"dev.knotless.ref", | ||
"dev.knotless.claim", | ||
"dev.knotless.pack", | ||
"dev.knotless.redirect", | ||
"dev.knotless.pull", | ||
"dev.knotless.issue", | ||
"dev.knotless.comment", | ||
"dev.knotless.state", | ||
"dev.knotless.hide", | ||
"dev.knotless.block", | ||
"dev.knotless.queue", | ||
"dev.knotless.label", | ||
"dev.knotless.reaction", | ||
"dev.knotless.check", | ||
"dev.knotless.rerun", | ||
"dev.knotless.orgMember" | ||
] | ||
} | ||
} | ||
} | ||
web/lexicons/README.md+5−0
| @@ -3,3 +3,8 @@ as published from bluesky-social/atproto's `lexicons/` at 1cb5b9b80, for | ||
`npm run lex` to make types of beside ours. Only these: `lex install` | ||
would follow every lexicon they name, `tools.ozone` and then `app.bsky` | ||
too. Ours are linted and held to main's by `just lex`; these are not. | ||
| ||
`npm run lex` leaves out `dev.knotless.space`, the space type private | ||
repositories are: @atproto/lex 0.4.4 refuses a lexicon of `"type": | ||
"space"`, which the permissioned-data alpha added, and the pages make no | ||
type of a space. | ||
web/package.json+1−1
| @@ -4,7 +4,7 @@ | ||
"type": "module", | ||
"description": "The web pages knotless-serve shows, drawn from its XRPC queries", | ||
"scripts": { | ||
"lex": "rm -rf .lexicons && mkdir .lexicons && cp -R ../lexicons/. lexicons/. .lexicons/ && lex build --lexicons .lexicons --out src/lib/lexicons --clear --import-ext .ts", | ||
"lex": "rm -rf .lexicons && mkdir .lexicons && cp -R ../lexicons/. lexicons/. .lexicons/ && rm .lexicons/dev/knotless/space.json && lex build --lexicons .lexicons --out src/lib/lexicons --clear --import-ext .ts", | ||
"dev": "npm run lex && vite dev", | ||
"build": "npm run lex && vite build", | ||
"prepare": "svelte-kit sync || echo ''", | ||