Shared lexicon schemas for long-form publishing on AT Protocol. Uses typescript to json via prototypey.
44
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'refs/heads/3-29-26-changes'

+129 -154
+15 -5
README.md
··· 26 26 / 27 27 ├── src/ 28 28 │ └── lexicons/ # TypeScript lexicon definitions (source) 29 + │ ├── site.standard.authFull.ts 30 + │ ├── site.standard.authSocial.ts 29 31 │ ├── site.standard.document.ts 32 + │ ├── site.standard.graph.recommend.ts 30 33 │ ├── site.standard.graph.subscription.ts 31 34 │ ├── site.standard.publication.ts 32 35 │ ├── site.standard.theme.basic.ts 33 36 │ └── site.standard.theme.color.ts 34 37 └── out/ # Generated JSON schemas 35 - ├── site.standard.document.json 36 - ├── site.standard.graph.subscription.json 37 - ├── site.standard.publication.json 38 - ├── site.standard.theme.basic.json 39 - └── site.standard.theme.color.json 38 + └── site/ 39 + └── standard/ 40 + ├── authFull.json 41 + ├── authSocial.json 42 + ├── document.json 43 + ├── publication.json 44 + ├── graph/ 45 + │ ├── recommend.json 46 + │ └── subscription.json 47 + └── theme/ 48 + ├── basic.json 49 + └── color.json 40 50 ``` 41 51 42 52 ## Resources
+15 -7
bun.lock
··· 5 5 "": { 6 6 "name": "lexicons", 7 7 "dependencies": { 8 - "@atproto/api": "^0.15.5", 9 - "prototypey": "^0.3.7", 8 + "@atproto/api": "^0.15.27", 9 + "prototypey": "^0.7.0", 10 10 }, 11 11 "devDependencies": { 12 12 "@types/bun": "latest", 13 13 }, 14 14 "peerDependencies": { 15 - "typescript": "^5", 15 + "typescript": "^5.9.3", 16 16 }, 17 17 }, 18 18 }, ··· 47 47 48 48 "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 49 49 50 - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 50 + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], 51 51 52 - "prototypey": ["prototypey@0.3.8", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "sade": "^1.8.1", "tinyglobby": "^0.2.15" }, "bin": { "prototypey": "lib/cli/main.js" } }, "sha512-xSSOWfVVr1boe+O5R19bFy9Gacvwj1PiyAk/3UUzvbCvPWM6JUycNH6BigXicOorZYUugT/frP+TX2Wj1stI7g=="], 52 + "prototypey": ["prototypey@0.7.0", "", { "dependencies": { "@atproto/lexicon": "^0.6.2", "sade": "^1.8.1", "tinyglobby": "^0.2.16" }, "bin": { "prototypey": "lib/cli/main.js" } }, "sha512-/Gbzq3kFM0jP97ehBf3/tlqCeykkMf5eCvbgPIoEtefLxQhaIYFwmce7zdAHiUlBpZU5RAnUTx+t6QJNy04lWA=="], 53 53 54 54 "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], 55 55 56 - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 56 + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], 57 57 58 58 "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], 59 59 ··· 71 71 72 72 "@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.6.0", "", { "dependencies": { "@atproto/common-web": "^0.4.7", "@atproto/syntax": "^0.4.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ=="], 73 73 74 - "prototypey/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 74 + "prototypey/@atproto/lexicon": ["@atproto/lexicon@0.6.2", "", { "dependencies": { "@atproto/common-web": "^0.4.18", "@atproto/syntax": "^0.5.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw=="], 75 + 76 + "prototypey/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.21", "", { "dependencies": { "@atproto/lex-data": "^0.0.15", "@atproto/lex-json": "^0.0.16", "@atproto/syntax": "^0.5.4", "zod": "^3.23.8" } }, "sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw=="], 77 + 78 + "prototypey/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.5.4", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw=="], 79 + 80 + "prototypey/@atproto/lexicon/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.15", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw=="], 81 + 82 + "prototypey/@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.16", "", { "dependencies": { "@atproto/lex-data": "^0.0.15", "tslib": "^2.8.1" } }, "sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg=="], 75 83 } 76 84 }
+2 -3
package.json
··· 4 4 "type": "module", 5 5 "private": true, 6 6 "scripts": { 7 - "lexicon:emit": "bunx prototypey gen-emit ./out ./src/lexicons/**/*.ts && bun run scripts/lint.ts", 7 + "lexicon:emit": "bunx prototypey gen-emit ./out ./src/lexicons/**/*.ts", 8 8 "lexicon:import": "bunx prototypey gen-from-json ./src/lexicons ./out/**/*.json", 9 - "lexicon:lint": "bun run scripts/lint.ts", 10 9 "lexicon:publish": "bun run scripts/publish.ts" 11 10 }, 12 11 "devDependencies": { ··· 17 16 }, 18 17 "dependencies": { 19 18 "@atproto/api": "^0.15.27", 20 - "prototypey": "^0.3.8" 19 + "prototypey": "^0.7.0" 21 20 } 22 21 }
-136
scripts/lint.ts
··· 1 - import * as fs from 'fs' 2 - import * as path from 'path' 3 - import { glob } from 'tinyglobby' 4 - 5 - export type LexiconPatches = Record<string, Record<string, unknown>> 6 - 7 - /** 8 - * Get a nested value from an object using a dot-separated path. 9 - */ 10 - function getPath(obj: Record<string, unknown>, pathStr: string): unknown { 11 - return pathStr.split('.').reduce((acc: unknown, key) => { 12 - if (acc && typeof acc === 'object') { 13 - return (acc as Record<string, unknown>)[key] 14 - } 15 - return undefined 16 - }, obj) 17 - } 18 - 19 - /** 20 - * Load patches from lexicon source files. 21 - */ 22 - async function loadPatches(): Promise<Record<string, LexiconPatches>> { 23 - const srcDir = path.join(process.cwd(), 'src/lexicons') 24 - const files = await glob('**/*.ts', { cwd: srcDir, absolute: true }) 25 - 26 - const allPatches: Record<string, LexiconPatches> = {} 27 - 28 - for (const file of files) { 29 - try { 30 - const module = await import(file) 31 - if (!module.patches) continue 32 - 33 - const lexiconId = Object.values(module) 34 - .find((v): v is { json: { id: string } } => 35 - v !== null && typeof v === 'object' && 'json' in v && typeof (v as { 36 - json?: { id?: string } 37 - }).json?.id === 'string' 38 - )?.json.id 39 - 40 - if (!lexiconId) continue 41 - 42 - allPatches[lexiconId] = module.patches 43 - } catch { 44 - // Skip files that can't be imported 45 - } 46 - } 47 - 48 - return allPatches 49 - } 50 - 51 - /** 52 - * Apply patches to a lexicon object. 53 - */ 54 - function applyPatches(lexicon: Record<string, unknown>, patches: Record<string, LexiconPatches>): boolean { 55 - const id = lexicon.id as string 56 - const lexiconPatches = patches[id] 57 - if (!lexiconPatches) return false 58 - 59 - let applied = false 60 - for (const [pathStr, fields] of Object.entries(lexiconPatches)) { 61 - const target = getPath(lexicon, pathStr) as Record<string, unknown> | undefined 62 - if (!target || typeof target !== 'object') continue 63 - 64 - for (const [field, value] of Object.entries(fields)) { 65 - if (target[field] === value) continue 66 - 67 - target[field] = value 68 - applied = true 69 - } 70 - } 71 - return applied 72 - } 73 - 74 - /** 75 - * Recursively removes `"required": true` (boolean) from an object, 76 - * while preserving `"required": [...]` (arrays). 77 - */ 78 - function removeRequiredBooleans(obj: unknown): unknown { 79 - if (Array.isArray(obj)) { 80 - return obj.map(removeRequiredBooleans) 81 - } 82 - 83 - if (obj !== null && typeof obj === 'object') { 84 - const result: Record<string, unknown> = {} 85 - 86 - for (const [key, value] of Object.entries(obj)) { 87 - // Skip "required" if it's a boolean 88 - if (key === 'required' && typeof value === 'boolean') { 89 - continue 90 - } 91 - result[key] = removeRequiredBooleans(value) 92 - } 93 - 94 - return result 95 - } 96 - 97 - return obj 98 - } 99 - 100 - /** 101 - * Lint all JSON files in the out directory. 102 - */ 103 - async function lintLexicons() { 104 - const outDir = path.join(process.cwd(), 'out') 105 - 106 - const files = fs.readdirSync(outDir).filter((f) => f.endsWith('.json')) 107 - const patches = await loadPatches() 108 - 109 - let totalFixed = 0 110 - 111 - for (const file of files) { 112 - const filePath = path.join(outDir, file) 113 - const content = fs.readFileSync(filePath, 'utf8') 114 - const original = JSON.parse(content) 115 - const cleaned = removeRequiredBooleans(original) as Record<string, unknown> 116 - 117 - const originalStr = JSON.stringify(original) 118 - 119 - // Apply patches for features prototypey doesn't support 120 - applyPatches(cleaned, patches) 121 - 122 - const cleanedStr = JSON.stringify(cleaned, null, '\t') 123 - 124 - if (originalStr !== JSON.stringify(cleaned)) { 125 - fs.writeFileSync(filePath, cleanedStr + '\n') 126 - console.log(`Fixed: ${file}`) 127 - totalFixed++ 128 - } else { 129 - console.log(`OK: ${file}`) 130 - } 131 - } 132 - 133 - console.log(`\nLinted ${files.length} files, fixed ${totalFixed}`) 134 - } 135 - 136 - lintLexicons()
+25
src/lexicons/site.standard.authFull.ts
··· 1 + import { fromJSON } from 'prototypey' 2 + 3 + export const siteStandardAuthFull = fromJSON({ 4 + lexicon: 1, 5 + id: "site.standard.authFull", 6 + defs: { 7 + main: { 8 + type: "permission-set" as "record", 9 + title: "Standard.site", 10 + detail: "Manage your publications, documents, subscriptions, and recommends.", 11 + permissions: [ 12 + { 13 + type: "permission", 14 + resource: "repo", 15 + collection: [ 16 + "site.standard.publication", 17 + "site.standard.document", 18 + "site.standard.graph.subscription", 19 + "site.standard.graph.recommend" 20 + ] 21 + } 22 + ] 23 + } 24 + } 25 + })
+23
src/lexicons/site.standard.authSocial.ts
··· 1 + import { fromJSON } from 'prototypey' 2 + 3 + export const siteStandardAuthSocial = fromJSON({ 4 + lexicon: 1, 5 + id: "site.standard.authSocial", 6 + defs: { 7 + main: { 8 + type: "permission-set" as "record", 9 + title: "Standard.site", 10 + detail: "Manage your publication subscriptions and document recommendations.", 11 + permissions: [ 12 + { 13 + type: "permission", 14 + resource: "repo", 15 + collection: [ 16 + "site.standard.graph.subscription", 17 + "site.standard.graph.recommend" 18 + ] 19 + } 20 + ] 21 + } 22 + } 23 + })
+20
src/lexicons/site.standard.document.ts
··· 47 47 }, { 48 48 description: 'Array of strings used to tag or categorize the document. Avoid prepending tags with hashtags.' 49 49 }), 50 + labels: lx.union(["com.atproto.label.defs#selfLabels"], { 51 + description: "Self-label values for this post. Effectively content warnings.", 52 + }), 53 + links: lx.union([], { 54 + description: "Array of values describing relationships between this document and external resources" 55 + }), 56 + contributors: lx.array({ 57 + type:"ref", ref:"#contributor" 58 + }), 50 59 publishedAt: lx.string({ 51 60 required: true, 52 61 format: 'datetime', ··· 58 67 }) 59 68 }), 60 69 description: 'A document record representing a published article, blog post, or other content. Documents can belong to a publication or exist independently.' 70 + }), 71 + contributor: lx.object({ 72 + did: lx.string({format:"did", required: true}), 73 + role: lx.string({ 74 + maxLength: 1000, 75 + maxGraphemes: 100, 76 + }), 77 + displayName: lx.string({ 78 + maxLength: 1000, 79 + maxGraphemes: 100, 80 + }) 61 81 }) 62 82 })
+21
src/lexicons/site.standard.graph.recommend.ts
··· 1 + import { lx } from "prototypey"; 2 + 3 + export const siteStandardGraphRecommend = lx.lexicon( 4 + "site.standard.graph.recommend", 5 + { 6 + main: lx.record({ 7 + key: "tid", 8 + type: "record", 9 + record: lx.object({ 10 + document: lx.string({ 11 + required: true, 12 + format: "at-uri", 13 + description: 14 + "AT-URI reference to the document record being recommended (ex: at://did:plc:abc123/site.standard.document/xyz789).", 15 + }), 16 + createdAt: lx.string({ format: "datetime", required: true }), 17 + }), 18 + description: "Record declaring a recommendation of a document.", 19 + }), 20 + }, 21 + );
+3
src/lexicons/site.standard.graph.subscription.ts
··· 9 9 required: true, 10 10 format: 'at-uri', 11 11 description: 'AT-URI reference to the publication record being subscribed to (ex: at://did:plc:abc123/site.standard.publication/xyz789).' 12 + }), 13 + createdAt: lx.string({ 14 + format: "datetime" 12 15 }) 13 16 }), 14 17 description: 'Record declaring a subscription to a publication.'
+5 -3
src/lexicons/site.standard.publication.ts
··· 33 33 }), 34 34 preferences: lx.ref('#preferences', { 35 35 description: 'Object containing platform specific preferences (with a few shared properties).' 36 + }), 37 + labels: lx.union(["com.atproto.label.defs#selfLabels"], { 38 + description: "Self-label values for this publication. Effectively content warnings.", 36 39 }) 37 40 }), 38 41 description: 'A publication record representing a blog, website, or content platform. Publications serve as containers for documents and define the overall branding and settings.' ··· 42 45 default: true, 43 46 description: 'Boolean which decides whether the publication should appear in discovery feeds.' 44 47 }), 45 - }, { 46 - description: 'Platform-specific preferences for the publication, including discovery and visibility settings.' 47 - }) 48 + }), 49 + 48 50 })