diff --git a/lexicons/README.md b/lexicons/README.md new file mode 100644 index 00000000..567abd0c --- /dev/null +++ b/lexicons/README.md @@ -0,0 +1,22 @@ +# lexicons + +Schemas for the records my CMS writes to my PDS. `com/` holds vendored copies of +upstream schemas we reference; everything under `dev/roe/` is mine. + +- `pnpm lex:gen` regenerates the TypeScript types in `shared/lex/`. +- `pnpm lex:publish` writes each `dev.roe.*` schema to the PDS as a + `com.atproto.lexicon.schema` record, with the NSID as the record key. + +Third-party resolution also needs a DNS TXT record for the `dev.roe` authority +(reverse the NSID minus its final segment): + +``` +_lexicon.roe.dev TXT "did=" +``` + +`pnpm lex:publish` prints the exact value to use. + +Resolution is not hierarchical, so any future authority (e.g. `dev.roe.cms`) +needs its own `_lexicon.cms.roe.dev` record. + +See [Lexicon publication and resolution](https://atproto.com/specs/lexicon#lexicon-publication-and-resolution). diff --git a/lexicons/dev/roe/ama.json b/lexicons/dev/roe/ama.json index 1c0a18dc..4852d2d0 100644 --- a/lexicons/dev/roe/ama.json +++ b/lexicons/dev/roe/ama.json @@ -16,12 +16,13 @@ }, "encryptedQuestion": { "type": "string", - "description": "AES-256-GCM envelope holding the raw question text. Present iff status=unanswered." + "description": "AES-256-GCM envelope holding the raw question text. Present iff status=unanswered.", + "maxLength": 500000 }, "question": { "type": "string", "description": "Plaintext question. Present iff status=answered; the editor decrypts encryptedQuestion at publish time and writes it here so the record is self-contained going forward.", - "maxLength": 200000, + "maxLength": 500000, "maxGraphemes": 50000 }, "posts": { @@ -50,12 +51,12 @@ }, "imageDimensions": { "type": "ref", - "ref": "#imageDimensions", + "ref": "dev.roe.defs#aspectRatio", "description": "Pixel dimensions of `image`. Stored alongside because PDS blobs don't carry intrinsic dimensions and Bluesky's image embed needs `aspectRatio`." }, "backgroundStyle": { "type": "string", - "description": "ID of the background style used to render the image. See `shared/cms/backgrounds.ts`.", + "description": "ID of the background style used to render the image. The set of styles is internal to roe.dev.", "maxLength": 64 }, "createdAt": { @@ -77,12 +78,12 @@ "text": { "type": "string", "description": "Body text. Plain text with `@` placeholders for entity mentions (e.g. `Thanks @abc123def456 for the help`). The placeholder syntax is internal; the publisher swaps them for the right per-platform handle.", - "maxLength": 200000, + "maxLength": 500000, "maxGraphemes": 50000 }, "mentions": { "type": "array", - "description": "Strong-refs to dev.roe.entity records keyed by the placeholder token used in `text` (the entity rkey).", + "description": "Strong-refs to dev.roe.entity records. The rkey of each referenced record is the placeholder token used in `text`.", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" @@ -108,14 +109,7 @@ "linkedin": { "type": "string", "format": "uri" }, "youtubeShorts": { "type": "string", "format": "uri" } } - }, - "imageDimensions": { - "type": "object", - "required": ["width", "height"], - "properties": { - "width": { "type": "integer", "minimum": 1 }, - "height": { "type": "integer", "minimum": 1 } - } } } } + diff --git a/lexicons/dev/roe/defs.json b/lexicons/dev/roe/defs.json new file mode 100644 index 00000000..52d4969b --- /dev/null +++ b/lexicons/dev/roe/defs.json @@ -0,0 +1,15 @@ +{ + "lexicon": 1, + "id": "dev.roe.defs", + "defs": { + "aspectRatio": { + "type": "object", + "description": "Intrinsic width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`.", + "required": ["width", "height"], + "properties": { + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } + } + } + } +} diff --git a/lexicons/dev/roe/entity.json b/lexicons/dev/roe/entity.json index b0385088..66c59d99 100644 --- a/lexicons/dev/roe/entity.json +++ b/lexicons/dev/roe/entity.json @@ -12,7 +12,7 @@ "properties": { "name": { "type": "string", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "socialHandles": { diff --git a/lexicons/dev/roe/invite.json b/lexicons/dev/roe/invite.json index f6974116..b0ff68f8 100644 --- a/lexicons/dev/roe/invite.json +++ b/lexicons/dev/roe/invite.json @@ -12,7 +12,8 @@ "properties": { "encrypted": { "type": "string", - "description": "AES-256-GCM envelope (see server/utils/admin/encryption.ts) holding { slug, repo } as JSON. Opaque to anyone without the server key." + "description": "AES-256-GCM envelope holding `{ slug, repo }` as JSON. Opaque to anyone without the server key.", + "maxLength": 8192 }, "isActive": { "type": "boolean", diff --git a/lexicons/dev/roe/location.json b/lexicons/dev/roe/location.json index b3a8fe19..a3e3cbef 100644 --- a/lexicons/dev/roe/location.json +++ b/lexicons/dev/roe/location.json @@ -12,18 +12,18 @@ "properties": { "city": { "type": "string", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "region": { "type": "string", "description": "Region/state. Used to special-case Scotland and US/UK subdivisions on the public site.", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "country": { "type": "string", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "countryCode": { diff --git a/lexicons/dev/roe/talk.json b/lexicons/dev/roe/talk.json index 0945ae5e..7bb5ba6e 100644 --- a/lexicons/dev/roe/talk.json +++ b/lexicons/dev/roe/talk.json @@ -13,12 +13,12 @@ "title": { "type": "string", "description": "Optional for upcoming events with no announced title. Required for past talks unless the talk is part of a group.", - "maxLength": 2048, + "maxLength": 5120, "maxGraphemes": 512 }, "description": { "type": "string", - "maxLength": 20000, + "maxLength": 50000, "maxGraphemes": 5000 }, "date": { @@ -34,13 +34,13 @@ "source": { "type": "string", "description": "Conference name, meetup, podcast title, etc.", - "maxLength": 2048, + "maxLength": 5120, "maxGraphemes": 512 }, "location": { "type": "string", "description": "Free-form: city, country, or 'Online'.", - "maxLength": 2048, + "maxLength": 5120, "maxGraphemes": 512 }, "type": { @@ -51,7 +51,7 @@ "type": "array", "items": { "type": "string", - "maxLength": 256, + "maxLength": 640, "maxGraphemes": 64 }, "maxLength": 32 @@ -67,7 +67,8 @@ }, "slides": { "type": "string", - "description": "Identifier for slides; historically a GitHub release tag from danielroe/slides." + "description": "Identifier for slides; historically a GitHub release tag from danielroe/slides.", + "maxLength": 512 }, "demo": { "type": "string", @@ -89,8 +90,8 @@ }, "aspectRatio": { "type": "ref", - "ref": "#aspectRatio", - "description": "Intrinsic pixel dimensions of `image`. Mirrors `app.bsky.embed.defs#aspectRatio`." + "ref": "dev.roe.defs#aspectRatio", + "description": "Intrinsic pixel dimensions of `image`." }, "createdAt": { "type": "string", @@ -98,15 +99,6 @@ } } } - }, - "aspectRatio": { - "type": "object", - "description": "width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`.", - "required": ["width", "height"], - "properties": { - "width": { "type": "integer", "minimum": 1 }, - "height": { "type": "integer", "minimum": 1 } - } } } } diff --git a/lexicons/dev/roe/talkGroup.json b/lexicons/dev/roe/talkGroup.json index d227c940..acd5656e 100644 --- a/lexicons/dev/roe/talkGroup.json +++ b/lexicons/dev/roe/talkGroup.json @@ -12,12 +12,12 @@ "properties": { "title": { "type": "string", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "description": { "type": "string", - "maxLength": 10000, + "maxLength": 25000, "maxGraphemes": 2500 }, "createdAt": { diff --git a/lexicons/dev/roe/usesCategory.json b/lexicons/dev/roe/usesCategory.json index 5fd3c9ca..5e56fb20 100644 --- a/lexicons/dev/roe/usesCategory.json +++ b/lexicons/dev/roe/usesCategory.json @@ -12,7 +12,7 @@ "properties": { "title": { "type": "string", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "order": { diff --git a/lexicons/dev/roe/usesItem.json b/lexicons/dev/roe/usesItem.json index b22c8997..ced3cfa1 100644 --- a/lexicons/dev/roe/usesItem.json +++ b/lexicons/dev/roe/usesItem.json @@ -17,12 +17,12 @@ }, "name": { "type": "string", - "maxLength": 1024, + "maxLength": 2560, "maxGraphemes": 256 }, "description": { "type": "string", - "maxLength": 10000, + "maxLength": 25000, "maxGraphemes": 2500 }, "order": { @@ -37,8 +37,8 @@ }, "aspectRatio": { "type": "ref", - "ref": "#aspectRatio", - "description": "Intrinsic pixel dimensions of `image`. Mirrors `app.bsky.embed.defs#aspectRatio`." + "ref": "dev.roe.defs#aspectRatio", + "description": "Intrinsic pixel dimensions of `image`." }, "links": { "type": "array", @@ -65,19 +65,10 @@ }, "label": { "type": "string", - "maxLength": 256, + "maxLength": 640, "maxGraphemes": 64 } } - }, - "aspectRatio": { - "type": "object", - "description": "width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`.", - "required": ["width", "height"], - "properties": { - "width": { "type": "integer", "minimum": 1 }, - "height": { "type": "integer", "minimum": 1 } - } } } } diff --git a/package.json b/package.json index 23203c29..67c81606 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "auth:linkedin": "node --env-file=.env scripts/linkedin-auth.ts", "auth:youtube": "node --env-file=.env scripts/youtube-auth.ts", "auth:youtube:refresh": "node --env-file=.env scripts/youtube-auth.ts refresh", - "lex:gen": "node scripts/gen-lex.mjs" + "lex:gen": "node scripts/gen-lex.mjs", + "lex:publish": "node --env-file=.env scripts/publish-lex.ts" }, "nano-staged": { "*.{js,ts,mjs,vue}": [ diff --git a/scripts/publish-lex.ts b/scripts/publish-lex.ts new file mode 100644 index 00000000..bc558f47 --- /dev/null +++ b/scripts/publish-lex.ts @@ -0,0 +1,154 @@ +/** + * Publish the `dev.roe.*` lexicons in `lexicons/` to the PDS as + * `com.atproto.lexicon.schema` records, so that they can be resolved by third + * parties per https://atproto.com/specs/lexicon#lexicon-publication-and-resolution. + * + * Resolution also requires a DNS TXT record for each NSID authority. Every + * schema here lives under the `dev.roe` authority. + * + * _lexicon.roe.dev TXT "did=did:plc:jbeaa5kdaladzwq3r7f5xgwe" + * + * Usage: + * node --env-file=.env scripts/publish-lex.ts + * node --env-file=.env scripts/publish-lex.ts --dry-run + * + * Environment variables: + * NUXT_ATPROTO_HANDLE, NUXT_ATPROTO_PASSWORD, NUXT_PUBLIC_ATPROTO_SERVICE + */ +import { readFileSync, readdirSync } from 'node:fs' +import { join, resolve } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { AtpAgent } from '@atproto/api' +import { defineCommand, runMain } from 'citty' + +const root = resolve(fileURLToPath(new URL('.', import.meta.url)), '..') +const lexRoot = join(root, 'lexicons') + +/** Authorities we control, and so are allowed to publish schemas for. */ +const OWNED_AUTHORITIES = ['dev.roe'] + +interface LexiconFile { + path: string + nsid: string + doc: Record +} + +function collectLexicons (dir: string): LexiconFile[] { + const files: LexiconFile[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...collectLexicons(full)) + continue + } + if (!entry.isFile() || !entry.name.endsWith('.json')) continue + const doc = JSON.parse(readFileSync(full, 'utf8')) + if (typeof doc.id !== 'string') throw new Error(`${full} has no string \`id\``) + files.push({ path: full, nsid: doc.id, doc }) + } + return files +} + +function isOwned (nsid: string): boolean { + const authority = nsid.split('.').slice(0, -1).join('.') + return OWNED_AUTHORITIES.includes(authority) +} + +/** + * `com.atproto.lexicon.schema` records carry the same fields as the lexicon + * file itself, minus anything the meta-schema doesn't define. `defs` keys must + * not be `#`-prefixed, which our files already satisfy. + */ +function toSchemaRecord (doc: Record) { + return { + $type: 'com.atproto.lexicon.schema', + lexicon: doc.lexicon, + id: doc.id, + defs: doc.defs, + } +} + +function isEqual (a: unknown, b: unknown): boolean { + return JSON.stringify(a) === JSON.stringify(b) +} + +const main = defineCommand({ + meta: { + name: 'publish-lex', + description: 'Publish dev.roe lexicons to the PDS as com.atproto.lexicon.schema records', + }, + args: { + 'dry-run': { + type: 'boolean', + description: 'Print what would change without writing to the PDS', + default: false, + }, + }, + async run ({ args }) { + const service = process.env.NUXT_PUBLIC_ATPROTO_SERVICE || 'https://bsky.social' + const identifier = process.env.NUXT_ATPROTO_HANDLE + const password = process.env.NUXT_ATPROTO_PASSWORD + + if (!identifier || !password) { + console.error('Set NUXT_ATPROTO_HANDLE and NUXT_ATPROTO_PASSWORD (see .env.example).') + process.exitCode = 1 + return + } + + const lexicons = collectLexicons(lexRoot).filter(l => isOwned(l.nsid)) + if (!lexicons.length) { + console.error('No owned lexicons found under', lexRoot) + process.exitCode = 1 + return + } + + const agent = new AtpAgent({ service }) + await agent.login({ identifier, password }) + const did = agent.session?.did + if (!did) throw new Error('atproto login did not return a session') + + for (const { nsid, doc } of lexicons.sort((a, b) => a.nsid.localeCompare(b.nsid))) { + const record = toSchemaRecord(doc) + + let existing: unknown + try { + const res = await agent.com.atproto.repo.getRecord({ + repo: did, + collection: 'com.atproto.lexicon.schema', + rkey: nsid, + }) + existing = res.data.value + } + catch { + existing = undefined + } + + if (existing && isEqual(existing, record)) { + console.log(` unchanged ${nsid}`) + continue + } + + const verb = existing ? 'update' : 'create' + if (args['dry-run']) { + console.log(` ${verb} (dry run) ${nsid}`) + continue + } + + await agent.com.atproto.repo.putRecord({ + repo: did, + collection: 'com.atproto.lexicon.schema', + rkey: nsid, + record, + validate: false, + }) + console.log(`🟢 ${verb}d ${nsid}`) + } + + console.log(`\nat://${did}/com.atproto.lexicon.schema/`) + console.log('Resolution requires: _lexicon.roe.dev TXT "did=' + did + '"') + }, +}) + +runMain(main) diff --git a/shared/lex/index.ts b/shared/lex/index.ts index 3e4a5b85..4206143c 100644 --- a/shared/lex/index.ts +++ b/shared/lex/index.ts @@ -11,6 +11,7 @@ export type { $Typed, Un$Typed, OmitKey } from './util.ts' export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef.ts' export * as DevRoeAma from './types/dev/roe/ama.ts' +export * as DevRoeDefs from './types/dev/roe/defs.ts' export * as DevRoeEntity from './types/dev/roe/entity.ts' export * as DevRoeInvite from './types/dev/roe/invite.ts' export * as DevRoeLocation from './types/dev/roe/location.ts' diff --git a/shared/lex/lexicons.ts b/shared/lex/lexicons.ts index 8379003b..bfe444d9 100644 --- a/shared/lex/lexicons.ts +++ b/shared/lex/lexicons.ts @@ -52,12 +52,13 @@ export const schemaDict = { type: 'string', description: 'AES-256-GCM envelope holding the raw question text. Present iff status=unanswered.', + maxLength: 500000, }, question: { type: 'string', description: 'Plaintext question. Present iff status=answered; the editor decrypts encryptedQuestion at publish time and writes it here so the record is self-contained going forward.', - maxLength: 200000, + maxLength: 500000, maxGraphemes: 50000, }, posts: { @@ -88,14 +89,14 @@ export const schemaDict = { }, imageDimensions: { type: 'ref', - ref: 'lex:dev.roe.ama#imageDimensions', + ref: 'lex:dev.roe.defs#aspectRatio', description: "Pixel dimensions of `image`. Stored alongside because PDS blobs don't carry intrinsic dimensions and Bluesky's image embed needs `aspectRatio`.", }, backgroundStyle: { type: 'string', description: - 'ID of the background style used to render the image. See `shared/cms/backgrounds.ts`.', + 'ID of the background style used to render the image. The set of styles is internal to roe.dev.', maxLength: 64, }, createdAt: { @@ -119,13 +120,13 @@ export const schemaDict = { type: 'string', description: 'Body text. Plain text with `@` placeholders for entity mentions (e.g. `Thanks @abc123def456 for the help`). The placeholder syntax is internal; the publisher swaps them for the right per-platform handle.', - maxLength: 200000, + maxLength: 500000, maxGraphemes: 50000, }, mentions: { type: 'array', description: - 'Strong-refs to dev.roe.entity records keyed by the placeholder token used in `text` (the entity rkey).', + 'Strong-refs to dev.roe.entity records. The rkey of each referenced record is the placeholder token used in `text`.', items: { type: 'ref', ref: 'lex:com.atproto.repo.strongRef', @@ -176,8 +177,16 @@ export const schemaDict = { }, }, }, - imageDimensions: { + }, + }, + DevRoeDefs: { + lexicon: 1, + id: 'dev.roe.defs', + defs: { + aspectRatio: { type: 'object', + description: + 'Intrinsic width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`.', required: ['width', 'height'], properties: { width: { @@ -207,7 +216,7 @@ export const schemaDict = { properties: { name: { type: 'string', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, socialHandles: { @@ -264,7 +273,8 @@ export const schemaDict = { encrypted: { type: 'string', description: - 'AES-256-GCM envelope (see server/utils/admin/encryption.ts) holding { slug, repo } as JSON. Opaque to anyone without the server key.', + 'AES-256-GCM envelope holding `{ slug, repo }` as JSON. Opaque to anyone without the server key.', + maxLength: 8192, }, isActive: { type: 'boolean', @@ -294,19 +304,19 @@ export const schemaDict = { properties: { city: { type: 'string', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, region: { type: 'string', description: 'Region/state. Used to special-case Scotland and US/UK subdivisions on the public site.', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, country: { type: 'string', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, countryCode: { @@ -378,12 +388,12 @@ export const schemaDict = { type: 'string', description: 'Optional for upcoming events with no announced title. Required for past talks unless the talk is part of a group.', - maxLength: 2048, + maxLength: 5120, maxGraphemes: 512, }, description: { type: 'string', - maxLength: 20000, + maxLength: 50000, maxGraphemes: 5000, }, date: { @@ -400,13 +410,13 @@ export const schemaDict = { source: { type: 'string', description: 'Conference name, meetup, podcast title, etc.', - maxLength: 2048, + maxLength: 5120, maxGraphemes: 512, }, location: { type: 'string', description: "Free-form: city, country, or 'Online'.", - maxLength: 2048, + maxLength: 5120, maxGraphemes: 512, }, type: { @@ -424,7 +434,7 @@ export const schemaDict = { type: 'array', items: { type: 'string', - maxLength: 256, + maxLength: 640, maxGraphemes: 64, }, maxLength: 32, @@ -442,6 +452,7 @@ export const schemaDict = { type: 'string', description: 'Identifier for slides; historically a GitHub release tag from danielroe/slides.', + maxLength: 512, }, demo: { type: 'string', @@ -463,9 +474,8 @@ export const schemaDict = { }, aspectRatio: { type: 'ref', - ref: 'lex:dev.roe.talk#aspectRatio', - description: - 'Intrinsic pixel dimensions of `image`. Mirrors `app.bsky.embed.defs#aspectRatio`.', + ref: 'lex:dev.roe.defs#aspectRatio', + description: 'Intrinsic pixel dimensions of `image`.', }, createdAt: { type: 'string', @@ -474,22 +484,6 @@ export const schemaDict = { }, }, }, - aspectRatio: { - type: 'object', - description: - 'width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`.', - required: ['width', 'height'], - properties: { - width: { - type: 'integer', - minimum: 1, - }, - height: { - type: 'integer', - minimum: 1, - }, - }, - }, }, }, DevRoeTalkGroup: { @@ -507,12 +501,12 @@ export const schemaDict = { properties: { title: { type: 'string', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, description: { type: 'string', - maxLength: 10000, + maxLength: 25000, maxGraphemes: 2500, }, createdAt: { @@ -539,7 +533,7 @@ export const schemaDict = { properties: { title: { type: 'string', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, order: { @@ -582,12 +576,12 @@ export const schemaDict = { }, name: { type: 'string', - maxLength: 1024, + maxLength: 2560, maxGraphemes: 256, }, description: { type: 'string', - maxLength: 10000, + maxLength: 25000, maxGraphemes: 2500, }, order: { @@ -603,9 +597,8 @@ export const schemaDict = { }, aspectRatio: { type: 'ref', - ref: 'lex:dev.roe.usesItem#aspectRatio', - description: - 'Intrinsic pixel dimensions of `image`. Mirrors `app.bsky.embed.defs#aspectRatio`.', + ref: 'lex:dev.roe.defs#aspectRatio', + description: 'Intrinsic pixel dimensions of `image`.', }, links: { type: 'array', @@ -632,27 +625,11 @@ export const schemaDict = { }, label: { type: 'string', - maxLength: 256, + maxLength: 640, maxGraphemes: 64, }, }, }, - aspectRatio: { - type: 'object', - description: - 'width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`.', - required: ['width', 'height'], - properties: { - width: { - type: 'integer', - minimum: 1, - }, - height: { - type: 'integer', - minimum: 1, - }, - }, - }, }, }, } as const satisfies Record @@ -690,6 +667,7 @@ export function validate( export const ids = { ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', DevRoeAma: 'dev.roe.ama', + DevRoeDefs: 'dev.roe.defs', DevRoeEntity: 'dev.roe.entity', DevRoeInvite: 'dev.roe.invite', DevRoeLocation: 'dev.roe.location', diff --git a/shared/lex/types/dev/roe/ama.ts b/shared/lex/types/dev/roe/ama.ts index 5cbbb757..dc8e26cd 100644 --- a/shared/lex/types/dev/roe/ama.ts +++ b/shared/lex/types/dev/roe/ama.ts @@ -9,6 +9,7 @@ import { is$typed as _is$typed, type OmitKey, } from '../../../util.ts' +import type * as DevRoeDefs from './defs.ts' import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef.ts' const is$typed = _is$typed, @@ -27,8 +28,8 @@ export interface Main { platforms?: Platforms publishedLinks?: PublishedLinks image?: BlobRef - imageDimensions?: ImageDimensions - /** ID of the background style used to render the image. See `shared/cms/backgrounds.ts`. */ + imageDimensions?: DevRoeDefs.AspectRatio + /** ID of the background style used to render the image. The set of styles is internal to roe.dev. */ backgroundStyle?: string createdAt: string answeredAt?: string @@ -56,7 +57,7 @@ export interface Post { $type?: 'dev.roe.ama#post' /** Body text. Plain text with `@` placeholders for entity mentions (e.g. `Thanks @abc123def456 for the help`). The placeholder syntax is internal; the publisher swaps them for the right per-platform handle. */ text: string - /** Strong-refs to dev.roe.entity records keyed by the placeholder token used in `text` (the entity rkey). */ + /** Strong-refs to dev.roe.entity records. The rkey of each referenced record is the placeholder token used in `text`. */ mentions?: ComAtprotoRepoStrongRef.Main[] } @@ -105,19 +106,3 @@ export function isPublishedLinks(v: V) { export function validatePublishedLinks(v: V) { return validate(v, id, hashPublishedLinks) } - -export interface ImageDimensions { - $type?: 'dev.roe.ama#imageDimensions' - width: number - height: number -} - -const hashImageDimensions = 'imageDimensions' - -export function isImageDimensions(v: V) { - return is$typed(v, id, hashImageDimensions) -} - -export function validateImageDimensions(v: V) { - return validate(v, id, hashImageDimensions) -} diff --git a/shared/lex/types/dev/roe/defs.ts b/shared/lex/types/dev/roe/defs.ts new file mode 100644 index 00000000..573f252a --- /dev/null +++ b/shared/lex/types/dev/roe/defs.ts @@ -0,0 +1,32 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons.ts' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../util.ts' + +const is$typed = _is$typed, + validate = _validate +const id = 'dev.roe.defs' + +/** Intrinsic width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`. */ +export interface AspectRatio { + $type?: 'dev.roe.defs#aspectRatio' + width: number + height: number +} + +const hashAspectRatio = 'aspectRatio' + +export function isAspectRatio(v: V) { + return is$typed(v, id, hashAspectRatio) +} + +export function validateAspectRatio(v: V) { + return validate(v, id, hashAspectRatio) +} diff --git a/shared/lex/types/dev/roe/invite.ts b/shared/lex/types/dev/roe/invite.ts index a3bd070b..8d88cd84 100644 --- a/shared/lex/types/dev/roe/invite.ts +++ b/shared/lex/types/dev/roe/invite.ts @@ -16,7 +16,7 @@ const id = 'dev.roe.invite' export interface Main { $type: 'dev.roe.invite' - /** AES-256-GCM envelope (see server/utils/admin/encryption.ts) holding { slug, repo } as JSON. Opaque to anyone without the server key. */ + /** AES-256-GCM envelope holding `{ slug, repo }` as JSON. Opaque to anyone without the server key. */ encrypted: string /** Whether this invite is currently honoured. Inactive invites stay around for audit but don't get wired up in the route table. */ isActive: boolean diff --git a/shared/lex/types/dev/roe/talk.ts b/shared/lex/types/dev/roe/talk.ts index 3c698eee..5316d0c1 100644 --- a/shared/lex/types/dev/roe/talk.ts +++ b/shared/lex/types/dev/roe/talk.ts @@ -10,6 +10,7 @@ import { type OmitKey, } from '../../../util.ts' import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef.ts' +import type * as DevRoeDefs from './defs.ts' const is$typed = _is$typed, validate = _validate @@ -46,7 +47,7 @@ export interface Main { repo?: string group?: ComAtprotoRepoStrongRef.Main image?: BlobRef - aspectRatio?: AspectRatio + aspectRatio?: DevRoeDefs.AspectRatio createdAt: string [k: string]: unknown } @@ -66,20 +67,3 @@ export { isMain as isRecord, validateMain as validateRecord, } - -/** width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`. */ -export interface AspectRatio { - $type?: 'dev.roe.talk#aspectRatio' - width: number - height: number -} - -const hashAspectRatio = 'aspectRatio' - -export function isAspectRatio(v: V) { - return is$typed(v, id, hashAspectRatio) -} - -export function validateAspectRatio(v: V) { - return validate(v, id, hashAspectRatio) -} diff --git a/shared/lex/types/dev/roe/usesItem.ts b/shared/lex/types/dev/roe/usesItem.ts index 32c0d990..fad1782a 100644 --- a/shared/lex/types/dev/roe/usesItem.ts +++ b/shared/lex/types/dev/roe/usesItem.ts @@ -10,6 +10,7 @@ import { type OmitKey, } from '../../../util.ts' import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef.ts' +import type * as DevRoeDefs from './defs.ts' const is$typed = _is$typed, validate = _validate @@ -23,7 +24,7 @@ export interface Main { /** Lower values render first within the category. Defaults to 100 in the editor. */ order: number image?: BlobRef - aspectRatio?: AspectRatio + aspectRatio?: DevRoeDefs.AspectRatio links?: Link[] createdAt: string [k: string]: unknown @@ -60,20 +61,3 @@ export function isLink(v: V) { export function validateLink(v: V) { return validate(v, id, hashLink) } - -/** width:height of an image in pixels. Mirrors `app.bsky.embed.defs#aspectRatio`. */ -export interface AspectRatio { - $type?: 'dev.roe.usesItem#aspectRatio' - width: number - height: number -} - -const hashAspectRatio = 'aspectRatio' - -export function isAspectRatio(v: V) { - return is$typed(v, id, hashAspectRatio) -} - -export function validateAspectRatio(v: V) { - return validate(v, id, hashAspectRatio) -}