diff --git a/lexicons/README.md b/lexicons/README.md index 5ac51b62..98fcf10b 100644 --- a/lexicons/README.md +++ b/lexicons/README.md @@ -15,6 +15,13 @@ checked against the `com.atproto.lexicon.schema` records published at `dev.roe.ama` instead of home-grown equivalents. - `community.lexicon.location.address` — the body of `dev.roe.location`. +The files under `site/standard/` are copies of the +[standard.site](https://standard.site) schemas published at +`did:plc:re3ebnp5v7ffagz6rb6xfei4`. `shared/standard-site.ts` turns +`site.standard.publication` and `site.standard.document` into collections, which +the build-time sync writes to. `com/atproto/label/defs.json` is here because +both of them reference `#selfLabels`. + Refresh them from upstream when the shared defs change. - `pnpm lex:gen` regenerates the TypeScript in `shared/lex/` from these files. diff --git a/lexicons/com/atproto/label/defs.json b/lexicons/com/atproto/label/defs.json new file mode 100644 index 00000000..26b6a3f9 --- /dev/null +++ b/lexicons/com/atproto/label/defs.json @@ -0,0 +1,153 @@ +{ + "lexicon": 1, + "id": "com.atproto.label.defs", + "defs": { + "label": { + "type": "object", + "description": "Metadata tag on an atproto resource (eg, repo or record).", + "required": ["src", "uri", "val", "cts"], + "properties": { + "ver": { + "type": "integer", + "description": "The AT Protocol version of the label object." + }, + "src": { + "type": "string", + "format": "did", + "description": "DID of the actor who created this label." + }, + "uri": { + "type": "string", + "format": "uri", + "description": "AT URI of the record, repository (account), or other resource that this label applies to." + }, + "cid": { + "type": "string", + "format": "cid", + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." + }, + "val": { + "type": "string", + "maxLength": 128, + "description": "The short string name of the value or type of this label." + }, + "neg": { + "type": "boolean", + "description": "If true, this is a negation label, overwriting a previous label." + }, + "cts": { + "type": "string", + "format": "datetime", + "description": "Timestamp when this label was created." + }, + "exp": { + "type": "string", + "format": "datetime", + "description": "Timestamp at which this label expires (no longer applies)." + }, + "sig": { + "type": "bytes", + "description": "Signature of dag-cbor encoded label." + } + } + }, + "selfLabels": { + "type": "object", + "description": "Metadata tags on an atproto record, published by the author within the record.", + "required": ["values"], + "properties": { + "values": { + "type": "array", + "items": { "type": "ref", "ref": "#selfLabel" }, + "maxLength": 10 + } + } + }, + "selfLabel": { + "type": "object", + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.", + "required": ["val"], + "properties": { + "val": { + "type": "string", + "maxLength": 128, + "description": "The short string name of the value or type of this label." + } + } + }, + "labelValueDefinition": { + "type": "object", + "description": "Declares a label value and its expected interpretations and behaviors.", + "required": ["identifier", "severity", "blurs", "locales"], + "properties": { + "identifier": { + "type": "string", + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", + "maxLength": 100, + "maxGraphemes": 100 + }, + "severity": { + "type": "string", + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", + "knownValues": ["inform", "alert", "none"] + }, + "blurs": { + "type": "string", + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", + "knownValues": ["content", "media", "none"] + }, + "defaultSetting": { + "type": "string", + "description": "The default setting for this label.", + "knownValues": ["ignore", "warn", "hide"], + "default": "warn" + }, + "adultOnly": { + "type": "boolean", + "description": "Does the user need to have adult content enabled in order to configure this label?" + }, + "locales": { + "type": "array", + "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" } + } + } + }, + "labelValueDefinitionStrings": { + "type": "object", + "description": "Strings which describe the label in the UI, localized into a specific language.", + "required": ["lang", "name", "description"], + "properties": { + "lang": { + "type": "string", + "description": "The code of the language these strings are written in.", + "format": "language" + }, + "name": { + "type": "string", + "description": "A short human-readable name for the label.", + "maxGraphemes": 64, + "maxLength": 640 + }, + "description": { + "type": "string", + "description": "A longer description of what the label means and why it might be applied.", + "maxGraphemes": 10000, + "maxLength": 100000 + } + } + }, + "labelValue": { + "type": "string", + "knownValues": [ + "!hide", + "!warn", + "!no-unauthenticated", + "porn", + "sexual", + "nudity", + "graphic-media", + "bot" + ] + } + } +} diff --git a/lexicons/site/standard/document.json b/lexicons/site/standard/document.json new file mode 100644 index 00000000..e0095c10 --- /dev/null +++ b/lexicons/site/standard/document.json @@ -0,0 +1,125 @@ +{ + "lexicon": 1, + "id": "site.standard.document", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "site", + "title", + "publishedAt" + ], + "properties": { + "path": { + "type": "string", + "description": "Combine with site or publication url to construct a canonical URL to the document. Prepend with a leading slash." + }, + "site": { + "type": "string", + "format": "uri", + "description": "Points to a publication record (at://) or a publication url (https://) for loose documents. Avoid trailing slashes." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 1280, + "maxGraphemes": 128 + }, + "description": "Array of strings used to tag or categorize the document. Avoid prepending tags with hashtags." + }, + "links": { + "refs": [], + "type": "union", + "description": "Array of values describing relationships between this document and external resources" + }, + "title": { + "type": "string", + "maxLength": 5000, + "description": "Title of the document.", + "maxGraphemes": 500 + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this post. Effectively content warnings." + }, + "content": { + "refs": [], + "type": "union", + "closed": false, + "description": "Open union used to define the record's content. Each entry must specify a $type and may be extended with other lexicons to support additional content formats." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Timestamp of the documents last edit." + }, + "coverImage": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Image to used for thumbnail or cover image. Less than 1MB is size." + }, + "bskyPostRef": { + "ref": "com.atproto.repo.strongRef", + "type": "ref", + "description": "Strong reference to a Bluesky post. Useful to keep track of comments off-platform." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "A brief description or excerpt from the document.", + "maxGraphemes": 3000 + }, + "publishedAt": { + "type": "string", + "format": "datetime", + "description": "Timestamp of the documents publish time." + }, + "textContent": { + "type": "string", + "description": "Plaintext representation of the documents contents. Should not contain markdown or other formatting." + }, + "contributors": { + "type": "array", + "items": { + "ref": "#contributor", + "type": "ref" + } + } + } + }, + "description": "A document record representing a published article, blog post, or other content. Documents can belong to a publication or exist independently." + }, + "contributor": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "role": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 100 + }, + "displayName": { + "type": "string", + "maxLength": 1000, + "maxGraphemes": 100 + } + } + } + } +} diff --git a/lexicons/site/standard/publication.json b/lexicons/site/standard/publication.json new file mode 100644 index 00000000..5e5f7b6e --- /dev/null +++ b/lexicons/site/standard/publication.json @@ -0,0 +1,72 @@ +{ + "lexicon": 1, + "id": "site.standard.publication", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "url", + "name" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this publication. Effectively content warnings." + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } + }, + "description": "A publication record representing a blog, website, or content platform. Publications serve as containers for documents and define the overall branding and settings." + }, + "preferences": { + "type": "object", + "properties": { + "showInDiscover": { + "type": "boolean", + "default": true, + "description": "Boolean which decides whether the publication should appear in discovery feeds." + } + } + } + } +} diff --git a/lexicons/site/standard/theme/basic.json b/lexicons/site/standard/theme/basic.json new file mode 100644 index 00000000..d452dccd --- /dev/null +++ b/lexicons/site/standard/theme/basic.json @@ -0,0 +1,50 @@ +{ + "lexicon": 1, + "id": "site.standard.theme.basic", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "background", + "foreground", + "accent", + "accentForeground" + ], + "properties": { + "accent": { + "refs": [ + "site.standard.theme.color#rgb" + ], + "type": "union", + "description": "Color used for links and button backgrounds." + }, + "background": { + "refs": [ + "site.standard.theme.color#rgb" + ], + "type": "union", + "description": "Color used for content background." + }, + "foreground": { + "refs": [ + "site.standard.theme.color#rgb" + ], + "type": "union", + "description": "Color used for content text." + }, + "accentForeground": { + "refs": [ + "site.standard.theme.color#rgb" + ], + "type": "union", + "description": "Color used for button text." + } + } + }, + "description": "A simplified theme definition for publications, providing basic color customization for content display across different platforms and applications." + } + } +} diff --git a/lexicons/site/standard/theme/color.json b/lexicons/site/standard/theme/color.json new file mode 100644 index 00000000..14bccab3 --- /dev/null +++ b/lexicons/site/standard/theme/color.json @@ -0,0 +1,62 @@ +{ + "lexicon": 1, + "id": "site.standard.theme.color", + "defs": { + "rgb": { + "type": "object", + "required": [ + "r", + "g", + "b" + ], + "properties": { + "b": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "g": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "r": { + "type": "integer", + "maximum": 255, + "minimum": 0 + } + } + }, + "rgba": { + "type": "object", + "required": [ + "r", + "g", + "b", + "a" + ], + "properties": { + "a": { + "type": "integer", + "maximum": 100, + "minimum": 0 + }, + "b": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "g": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "r": { + "type": "integer", + "maximum": 255, + "minimum": 0 + } + } + } + } +} diff --git a/modules/shared/airspace.ts b/modules/shared/airspace.ts index be5a0035..4d411236 100644 --- a/modules/shared/airspace.ts +++ b/modules/shared/airspace.ts @@ -5,7 +5,7 @@ * Identity comes from `runtimeConfig.atproto`, populated by `modules/atproto`, */ import { createAirspace, passwordSession } from 'airspace' -import type { Identity } from 'airspace' +import type { AnyCollection, Identity } from 'airspace' import { useRuntimeConfig } from 'nuxt/kit' import { collections } from '../../shared/collections.ts' @@ -33,7 +33,9 @@ export function useBuildAirspace () { * A client that can write, for the sync providers. Needs * `NUXT_ATPROTO_PASSWORD`; the PDS and DID are already resolved. */ -export async function useBuildAirspaceWithSession () { +export async function useBuildAirspaceWithSession> (used: C): Promise>> +export async function useBuildAirspaceWithSession (): Promise>> +export async function useBuildAirspaceWithSession (used: Record = collections) { const { handle, password } = useRuntimeConfig().atproto const { service } = identity() if (!handle || !password) { @@ -41,7 +43,7 @@ export async function useBuildAirspaceWithSession () { } return createAirspace({ identity: identity(), - collections, + collections: used, session: await passwordSession({ service, identifier: handle, password }), }) } diff --git a/modules/sync/providers/standard-site.ts b/modules/sync/providers/standard-site.ts index 47a3be4b..e67d9b1c 100644 --- a/modules/sync/providers/standard-site.ts +++ b/modules/sync/providers/standard-site.ts @@ -1,14 +1,18 @@ import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' -import { Client } from '@atproto/lex' -import type { LexMap } from '@atproto/lex' -import { PasswordSession } from '@atproto/lex-password-session' -import { useRuntimeConfig } from 'nuxt/kit' +import type { RecordOf } from 'airspace' +import type { l } from '@atproto/lex-schema' +import type { site } from '../../../shared/lex/index.ts' import type { SyncItem, SyncOptions, SyncProvider } from './index' +import { useBuildAirspaceWithSession } from '../../shared/airspace' +import { standardSiteCollections } from '../../../shared/standard-site' import { publicationRkey, tidFromDate } from '../../shared/tid' +type Airspace = Awaited>> +type Document = RecordOf['value'] + export class StandardSiteProvider implements SyncProvider { name = 'standard-site' @@ -26,47 +30,23 @@ export class StandardSiteProvider implements SyncProvider { return } - const cfg = useRuntimeConfig() - const pdsUrl = cfg.public.atproto.service - const { handle, password } = cfg.atproto - if (!pdsUrl || !handle || !password) { - const missing = !password ? 'NUXT_ATPROTO_PASSWORD' : 'social.networks.bluesky.identifier' - throw new Error(`atproto identity / credentials not configured (missing ${missing}).`) - } - - const session = await PasswordSession.login({ service: pdsUrl, identifier: handle, password }) - const client = new Client(session) - - const did = client.assertDid - - const publication: LexMap = { - $type: 'site.standard.publication', - url: 'https://roe.dev', - name: 'Daniel Roe', - description: 'The personal website of Daniel Roe', - preferences: { showInDiscover: true }, - basicTheme, - } - - const icon = await resolvePublicationIcon(client) - if (icon) publication.icon = icon + const airspace = await useBuildAirspaceWithSession(standardSiteCollections) + const { did } = await airspace.identity() try { - await client.putRecord(publication as LexMap & { $type: 'site.standard.publication' }, publicationRkey) + await airspace.publication.put(publicationRkey, { + url: 'https://roe.dev', + name: 'Daniel Roe', + description: 'The personal website of Daniel Roe', + preferences: { showInDiscover: true }, + basicTheme, + icon: await resolvePublicationIcon(airspace), + }) } catch (error) { console.warn('[sync:standard-site] Failed to update publication record:', error instanceof Error ? error.message : error) } - // Delete legacy 'self' rkey publication record if it exists - try { - await client.deleteRecord('site.standard.publication', 'self') - console.info('[sync:standard-site] Deleted legacy publication record with rkey: self') - } - catch { - // Record may not exist, that's fine - } - // Build set of expected rkeys from current blog posts const expectedRkeys = new Set( blogItems @@ -75,17 +55,15 @@ export class StandardSiteProvider implements SyncProvider { ) // Delete any existing records that don't match a current blog post's TID - const previous = new Map() + const previous = new Map() try { - const existing = await client.listRecords('site.standard.document', { repo: did, limit: 100 }) - for (const record of existing.body.records) { - const rkey = record.uri.split('/').pop()! - if (!expectedRkeys.has(rkey)) { - console.info(`[sync:standard-site] Deleting stale record with rkey: ${rkey}`) - await client.deleteRecord('site.standard.document', rkey) + for (const record of await airspace.documents.list()) { + if (!expectedRkeys.has(record.rkey)) { + console.info(`[sync:standard-site] Deleting stale record with rkey: ${record.rkey}`) + await airspace.documents.delete(record.rkey) continue } - previous.set(rkey, record.value as LexMap) + previous.set(record.rkey, record.value) } } catch (error) { @@ -99,29 +77,20 @@ export class StandardSiteProvider implements SyncProvider { if (!slug || !item.date) continue const rkey = tidFromDate(item.date) + const existing = previous.get(rkey) - const record: LexMap = { - $type: 'site.standard.document', + await airspace.documents.put(rkey, { site: `at://${did}/site.standard.publication/${publicationRkey}`, path: `/blog/${slug}`, title: item.title, - publishedAt: new Date(item.date).toISOString(), - updatedAt: new Date(now + updated * 1000).toISOString(), - } - - if (item.description) record.description = item.description - if (item.tags?.length) record.tags = item.tags - if (item.text_content) record.textContent = item.text_content - - const existing = previous.get(rkey) - - const bskyPostRef = await resolveBskyPostRef(client, did, item.bluesky, existing) - if (bskyPostRef) record.bskyPostRef = bskyPostRef - - const coverImage = await resolveCoverImage(client, item.canonical_url, existing) - if (coverImage) record.coverImage = coverImage - - await client.putRecord(record as LexMap & { $type: 'site.standard.document' }, rkey) + publishedAt: new Date(item.date).toISOString() as l.DatetimeString, + updatedAt: new Date(now + updated * 1000).toISOString() as l.DatetimeString, + description: item.description || undefined, + tags: item.tags?.length ? item.tags : undefined, + textContent: item.text_content, + bskyPostRef: await resolveBskyPostRef(airspace, did, item.bluesky, existing), + coverImage: await resolveCoverImage(airspace, item.canonical_url, existing), + }) updated++ } @@ -130,16 +99,14 @@ export class StandardSiteProvider implements SyncProvider { } } -const rgb = (r: number, g: number, b: number) => ({ $type: 'site.standard.theme.color#rgb', r, g, b }) - /** Light-mode palette from `app/assets/main.css`. */ const basicTheme = { $type: 'site.standard.theme.basic', - background: rgb(229, 231, 235), - foreground: rgb(31, 41, 55), - accent: rgb(31, 41, 55), - accentForeground: rgb(243, 244, 246), -} + background: { $type: 'site.standard.theme.color#rgb', r: 229, g: 231, b: 235 }, + foreground: { $type: 'site.standard.theme.color#rgb', r: 31, g: 41, b: 55 }, + accent: { $type: 'site.standard.theme.color#rgb', r: 31, g: 41, b: 55 }, + accentForeground: { $type: 'site.standard.theme.color#rgb', r: 243, g: 244, b: 246 }, +} satisfies site.standard.theme.basic.Main const iconPath = fileURLToPath(new URL('../../../public/android-chrome-512x512.png', import.meta.url)) @@ -147,20 +114,13 @@ const iconPath = fileURLToPath(new URL('../../../public/android-chrome-512x512.p * Reuses the blob already referenced by the publication record where possible, * as re-uploading produces a new CID and invalidates consumers' cached icons. */ -async function resolvePublicationIcon (client: Client) { +async function resolvePublicationIcon (airspace: Airspace) { try { - const existing = await client.getRecord('site.standard.publication', publicationRkey) - const icon = (existing.body.value as LexMap).icon - if (icon) return icon - } - catch { - // no publication record yet - } + const existing = await airspace.publication.get(publicationRkey) + if (existing?.value.icon) return existing.value.icon - try { - const data = await readFile(iconPath) - const upload = await client.uploadBlob(new Uint8Array(data), { encoding: 'image/png' }) - return upload.body.blob + const upload = await airspace.blobs.upload(await readFile(iconPath), { mimeType: 'image/png' }) + return upload.blob } catch (error) { console.warn('[sync:standard-site] Failed to upload publication icon:', error instanceof Error ? error.message : error) @@ -174,19 +134,15 @@ const MAX_COVER_BYTES = 1_000_000 * A strong ref needs the post's CID as well as its URI, so the announcement * post is read back from the repo unless the stored ref already points at it. */ -async function resolveBskyPostRef (client: Client, did: string, uri: string | undefined, existing: LexMap | undefined) { +async function resolveBskyPostRef (airspace: Airspace, did: string, uri: string | undefined, existing: Document | undefined) { if (!uri) return - const stored = existing?.bskyPostRef as { uri?: string } | undefined - if (stored?.uri === uri) return stored - - const [, , repo, collection, rkey] = uri.split('/') - if (repo !== did || collection !== 'app.bsky.feed.post' || !rkey) return + if (existing?.bskyPostRef?.uri === uri) return existing.bskyPostRef + if (!uri.startsWith(`at://${did}/app.bsky.feed.post/`)) return try { - const post = await client.getRecord(collection, rkey) - if (!post.body.cid) return - return { uri: post.body.uri, cid: post.body.cid } + const post = await airspace.resolve(uri) + if (post) return { uri: post.uri, cid: post.cid } } catch (error) { console.warn(`[sync:standard-site] Failed to resolve Bluesky post ${uri}:`, error instanceof Error ? error.message : error) @@ -194,14 +150,13 @@ async function resolveBskyPostRef (client: Client, did: string, uri: string | un } /** - * Uses the page's own Open Graph image as the cover. It is read from the - * deployed site rather than generated here, as og images are rendered during - * prerender and do not exist on disk while this runs; a post published in this - * deploy therefore picks its cover up on the next one. + * Uses the page's own Open Graph image as the cover. Blog pages are rendered + * on demand rather than prerendered, and the image URL is signed, so the + * deployed page is the only place the URL can be read from; a post published + * in this deploy picks its cover up on the next one. */ -async function resolveCoverImage (client: Client, canonicalURL: string, existing: LexMap | undefined) { - const stored = existing?.coverImage - if (stored) return stored +async function resolveCoverImage (airspace: Airspace, canonicalURL: string, existing: Document | undefined) { + if (existing?.coverImage) return existing.coverImage try { const html = await fetch(canonicalURL).then(r => r.ok ? r.text() : null) @@ -209,15 +164,14 @@ async function resolveCoverImage (client: Client, canonicalURL: string, existing if (!url) return const response = await fetch(url) - if (!response.ok) return + const mimeType = response.headers.get('content-type') + if (!response.ok || !mimeType?.startsWith('image/')) return - const type = response.headers.get('content-type') - const encoding = type?.startsWith('image/') ? type as `image/${string}` : 'image/png' const data = new Uint8Array(await response.arrayBuffer()) if (data.byteLength > MAX_COVER_BYTES) return - const upload = await client.uploadBlob(data, { encoding }) - return upload.body.blob + const upload = await airspace.blobs.upload(data, { mimeType }) + return upload.blob } catch (error) { console.warn(`[sync:standard-site] Failed to upload cover image for ${canonicalURL}:`, error instanceof Error ? error.message : error) diff --git a/shared/lex/com/atproto.ts b/shared/lex/com/atproto.ts index b71218e1..96a5033c 100644 --- a/shared/lex/com/atproto.ts +++ b/shared/lex/com/atproto.ts @@ -2,4 +2,5 @@ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT. */ +export * as label from './atproto/label.ts' export * as repo from './atproto/repo.ts' diff --git a/shared/lex/com/atproto/label.ts b/shared/lex/com/atproto/label.ts new file mode 100644 index 00000000..0c6a4326 --- /dev/null +++ b/shared/lex/com/atproto/label.ts @@ -0,0 +1,5 @@ +/* + * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT. + */ + +export * as defs from './label/defs.ts' diff --git a/shared/lex/com/atproto/label/defs.defs.ts b/shared/lex/com/atproto/label/defs.defs.ts new file mode 100644 index 00000000..f53b1575 --- /dev/null +++ b/shared/lex/com/atproto/label/defs.defs.ts @@ -0,0 +1,256 @@ +/* + * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT. + */ + +import { l } from '@atproto/lex-schema' + +const $nsid = 'com.atproto.label.defs' + +type $nsid = typeof $nsid + +export { $nsid } + +/** Metadata tag on an atproto resource (eg, repo or record). */ +type Label = { + $type?: 'com.atproto.label.defs#label' + + /** + * The AT Protocol version of the label object. + */ + ver?: number + + /** + * DID of the actor who created this label. + */ + src: l.DidString + + /** + * AT URI of the record, repository (account), or other resource that this label applies to. + */ + uri: l.UriString + + /** + * Optionally, CID specifying the specific version of 'uri' resource this label applies to. + */ + cid?: l.CidString + + /** + * The short string name of the value or type of this label. + */ + val: string + + /** + * If true, this is a negation label, overwriting a previous label. + */ + neg?: boolean + + /** + * Timestamp when this label was created. + */ + cts: l.DatetimeString + + /** + * Timestamp at which this label expires (no longer applies). + */ + exp?: l.DatetimeString + + /** + * Signature of dag-cbor encoded label. + */ + sig?: Uint8Array +} + +export type { Label } + +/** Metadata tag on an atproto resource (eg, repo or record). */ +const label = /*#__PURE__*/ l.typedObject