work-in-progress atproto PDS
typescript atproto pds atcute

feat: record validation

mary.my.id fcfe01fe 2970cceb

verified
+1509 -512
+16
packages/danaus/drizzle/lexicon/20260121015822_dashing_orphan/migration.sql
··· 1 + CREATE TABLE `authority` ( 2 + `domain` text PRIMARY KEY, 3 + `did` text, 4 + `updated_at` integer NOT NULL 5 + ); 6 + --> statement-breakpoint 7 + CREATE TABLE `schema` ( 8 + `nsid` text PRIMARY KEY, 9 + `authority_did` text NOT NULL, 10 + `cid` text, 11 + `doc` text, 12 + `updated_at` integer NOT NULL 13 + ); 14 + --> statement-breakpoint 15 + CREATE INDEX `authority_updated_at_idx` ON `authority` (`updated_at`);--> statement-breakpoint 16 + CREATE INDEX `schema_updated_at_idx` ON `schema` (`updated_at`);
+145
packages/danaus/drizzle/lexicon/20260121015822_dashing_orphan/snapshot.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "sqlite", 4 + "id": "7018e308-5b71-44e2-b9b8-862d395b9d97", 5 + "prevIds": [ 6 + "00000000-0000-0000-0000-000000000000" 7 + ], 8 + "ddl": [ 9 + { 10 + "name": "authority", 11 + "entityType": "tables" 12 + }, 13 + { 14 + "name": "schema", 15 + "entityType": "tables" 16 + }, 17 + { 18 + "type": "text", 19 + "notNull": false, 20 + "autoincrement": false, 21 + "default": null, 22 + "generated": null, 23 + "name": "domain", 24 + "entityType": "columns", 25 + "table": "authority" 26 + }, 27 + { 28 + "type": "text", 29 + "notNull": false, 30 + "autoincrement": false, 31 + "default": null, 32 + "generated": null, 33 + "name": "did", 34 + "entityType": "columns", 35 + "table": "authority" 36 + }, 37 + { 38 + "type": "integer", 39 + "notNull": true, 40 + "autoincrement": false, 41 + "default": null, 42 + "generated": null, 43 + "name": "updated_at", 44 + "entityType": "columns", 45 + "table": "authority" 46 + }, 47 + { 48 + "type": "text", 49 + "notNull": false, 50 + "autoincrement": false, 51 + "default": null, 52 + "generated": null, 53 + "name": "nsid", 54 + "entityType": "columns", 55 + "table": "schema" 56 + }, 57 + { 58 + "type": "text", 59 + "notNull": true, 60 + "autoincrement": false, 61 + "default": null, 62 + "generated": null, 63 + "name": "authority_did", 64 + "entityType": "columns", 65 + "table": "schema" 66 + }, 67 + { 68 + "type": "text", 69 + "notNull": false, 70 + "autoincrement": false, 71 + "default": null, 72 + "generated": null, 73 + "name": "cid", 74 + "entityType": "columns", 75 + "table": "schema" 76 + }, 77 + { 78 + "type": "text", 79 + "notNull": false, 80 + "autoincrement": false, 81 + "default": null, 82 + "generated": null, 83 + "name": "doc", 84 + "entityType": "columns", 85 + "table": "schema" 86 + }, 87 + { 88 + "type": "integer", 89 + "notNull": true, 90 + "autoincrement": false, 91 + "default": null, 92 + "generated": null, 93 + "name": "updated_at", 94 + "entityType": "columns", 95 + "table": "schema" 96 + }, 97 + { 98 + "columns": [ 99 + "domain" 100 + ], 101 + "nameExplicit": false, 102 + "name": "authority_pk", 103 + "table": "authority", 104 + "entityType": "pks" 105 + }, 106 + { 107 + "columns": [ 108 + "nsid" 109 + ], 110 + "nameExplicit": false, 111 + "name": "schema_pk", 112 + "table": "schema", 113 + "entityType": "pks" 114 + }, 115 + { 116 + "columns": [ 117 + { 118 + "value": "updated_at", 119 + "isExpression": false 120 + } 121 + ], 122 + "isUnique": false, 123 + "where": null, 124 + "origin": "manual", 125 + "name": "authority_updated_at_idx", 126 + "entityType": "indexes", 127 + "table": "authority" 128 + }, 129 + { 130 + "columns": [ 131 + { 132 + "value": "updated_at", 133 + "isExpression": false 134 + } 135 + ], 136 + "isUnique": false, 137 + "where": null, 138 + "origin": "manual", 139 + "name": "schema_updated_at_idx", 140 + "entityType": "indexes", 141 + "table": "schema" 142 + } 143 + ], 144 + "renames": [] 145 + }
+4
packages/danaus/package.json
··· 22 22 "db:generate:account": "drizzle-kit generate --dialect=sqlite --schema=src/accounts/db/schema.ts --out=drizzle/accounts", 23 23 "db:generate:actor": "drizzle-kit generate --dialect=sqlite --schema=src/actors/db/schema.ts --out=drizzle/actors", 24 24 "db:generate:identity": "drizzle-kit generate --dialect=sqlite --schema=src/identity/db/schema.ts --out=drizzle/identity", 25 + "db:generate:lexicon": "drizzle-kit generate --dialect=sqlite --schema=src/lexicon/db/schema.ts --out=drizzle/lexicon", 25 26 "db:generate:sequencer": "drizzle-kit generate --dialect=sqlite --schema=src/sequencer/db/schema.ts --out=drizzle/sequencer" 26 27 }, 27 28 "dependencies": { ··· 36 37 "@atcute/identity": "^1.1.3", 37 38 "@atcute/identity-resolver": "^1.2.2", 38 39 "@atcute/identity-resolver-node": "^1.0.3", 40 + "@atcute/lexicon-doc": "^2.0.6", 41 + "@atcute/lexicon-resolver": "^0.1.6", 42 + "@atcute/lexicon-resolver-node": "^0.1.0", 39 43 "@atcute/lexicons": "^1.2.6", 40 44 "@atcute/mst": "^0.1.2", 41 45 "@atcute/multibase": "^1.1.6",
+11 -34
packages/danaus/src/api/com.atproto/repo.applyWrites.ts
··· 1 1 import { ComAtprotoRepoApplyWrites } from '@atcute/atproto'; 2 - import type { CanonicalResourceUri, Nsid, RecordKey } from '@atcute/lexicons'; 2 + import type { CanonicalResourceUri } from '@atcute/lexicons'; 3 3 import { AuthRequiredError, InvalidRequestError, json, type XRPCRouter } from '@atcute/xrpc-server'; 4 4 5 5 import type { RepoWriteOp } from '#app/actors/repo/types.ts'; 6 6 import type { AppContext } from '#app/context.ts'; 7 - 8 - type WriteInput = { 9 - $type?: string; 10 - collection: Nsid; 11 - rkey?: RecordKey; 12 - value?: unknown; 13 - }; 7 + import { validateRecordWrites } from '#app/lexicon/validate-writes.ts'; 14 8 15 9 type WriteResult = 16 10 | { $type: 'com.atproto.repo.applyWrites#deleteResult' } ··· 23 17 * @param context app context 24 18 */ 25 19 export const applyWrites = (router: XRPCRouter, context: AppContext) => { 26 - const { accountManager, actorManager, authVerifier } = context; 20 + const { accountManager, actorManager, authVerifier, lexiconCache } = context; 27 21 28 22 router.addProcedure(ComAtprotoRepoApplyWrites, { 29 23 async handler({ input, request }) { ··· 45 39 throw new AuthRequiredError({ error: 'InvalidToken', description: `invalid repository credentials` }); 46 40 } 47 41 48 - const writes = (input.writes as WriteInput[]).map((write): RepoWriteOp => { 42 + const writes = input.writes.map((write): RepoWriteOp => { 49 43 switch (write.$type) { 50 - case 'com.atproto.repo.applyWrites#create': 44 + case 'com.atproto.repo.applyWrites#create': { 51 45 return { 52 46 action: 'create', 53 47 collection: write.collection, 54 48 rkey: write.rkey, 55 49 record: write.value, 56 50 }; 57 - case 'com.atproto.repo.applyWrites#update': 51 + } 52 + case 'com.atproto.repo.applyWrites#update': { 58 53 return { 59 54 action: 'update', 60 55 collection: write.collection, 61 56 rkey: write.rkey, 62 57 record: write.value, 63 58 }; 64 - case 'com.atproto.repo.applyWrites#delete': 59 + } 60 + case 'com.atproto.repo.applyWrites#delete': { 65 61 return { 66 62 action: 'delete', 67 63 collection: write.collection, 68 64 rkey: write.rkey, 69 65 }; 70 - } 71 - 72 - if ('value' in write && write.value !== undefined) { 73 - if (write.rkey === undefined) { 74 - return { 75 - action: 'create', 76 - collection: write.collection, 77 - rkey: write.rkey, 78 - record: write.value, 79 - }; 80 66 } 81 - 82 - throw new InvalidRequestError({ 83 - error: 'InvalidWrite', 84 - description: `ambiguous write action without $type`, 85 - }); 86 67 } 68 + }); 87 69 88 - return { 89 - action: 'delete', 90 - collection: write.collection, 91 - rkey: write.rkey, 92 - }; 93 - }); 70 + await validateRecordWrites(lexiconCache, writes, input.validate); 94 71 95 72 const result = await actorManager.transact(account.did, (store) => { 96 73 return store.repo.applyWrites(writes, {
+18 -15
packages/danaus/src/api/com.atproto/repo.createRecord.ts
··· 1 1 import { ComAtprotoRepoCreateRecord } from '@atcute/atproto'; 2 2 import { AuthRequiredError, InvalidRequestError, json, type XRPCRouter } from '@atcute/xrpc-server'; 3 3 4 + import type { RepoWriteOp } from '#app/actors/repo/types.ts'; 4 5 import type { AppContext } from '#app/context.ts'; 6 + import { validateRecordWrites } from '#app/lexicon/validate-writes.ts'; 5 7 6 8 /** 7 9 * register the `com.atproto.repo.createRecord` endpoint. ··· 9 11 * @param context app context 10 12 */ 11 13 export const createRecord = (router: XRPCRouter, context: AppContext) => { 12 - const { accountManager, actorManager, authVerifier } = context; 14 + const { accountManager, actorManager, authVerifier, lexiconCache } = context; 13 15 14 16 router.addProcedure(ComAtprotoRepoCreateRecord, { 15 17 async handler({ input, request }) { ··· 31 33 throw new AuthRequiredError({ error: 'InvalidToken', description: `invalid repository credentials` }); 32 34 } 33 35 36 + const writes: RepoWriteOp[] = [ 37 + { 38 + action: 'create', 39 + collection: input.collection, 40 + rkey: input.rkey, 41 + record: input.record, 42 + }, 43 + ]; 44 + 45 + await validateRecordWrites(lexiconCache, writes, input.validate); 46 + 34 47 const result = await actorManager.transact(account.did, (store) => { 35 - return store.repo.applyWrites( 36 - [ 37 - { 38 - action: 'create', 39 - collection: input.collection, 40 - rkey: input.rkey, 41 - record: input.record, 42 - }, 43 - ], 44 - { 45 - swapCommit: input.swapCommit ?? undefined, 46 - validateBlobs: input.validate ?? true, 47 - }, 48 - ); 48 + return store.repo.applyWrites(writes, { 49 + swapCommit: input.swapCommit ?? undefined, 50 + validateBlobs: input.validate ?? true, 51 + }); 49 52 }); 50 53 51 54 const write = result.results[0];
+24 -2
packages/danaus/src/api/com.atproto/repo.putRecord.ts
··· 1 1 import { ComAtprotoRepoPutRecord } from '@atcute/atproto'; 2 + import type { CanonicalResourceUri } from '@atcute/lexicons'; 2 3 import { AuthRequiredError, InvalidRequestError, json, type XRPCRouter } from '@atcute/xrpc-server'; 3 4 5 + import type { RepoWriteOp } from '#app/actors/repo/types.ts'; 4 6 import type { AppContext } from '#app/context.ts'; 7 + import { validateRecordWrites } from '#app/lexicon/validate-writes.ts'; 5 8 6 9 /** 7 10 * register the `com.atproto.repo.putRecord` endpoint. ··· 9 12 * @param context app context 10 13 */ 11 14 export const putRecord = (router: XRPCRouter, context: AppContext) => { 12 - const { accountManager, actorManager, authVerifier } = context; 15 + const { accountManager, actorManager, authVerifier, lexiconCache } = context; 13 16 14 17 router.addProcedure(ComAtprotoRepoPutRecord, { 15 18 async handler({ input, request }) { ··· 31 34 throw new AuthRequiredError({ error: 'InvalidToken', description: `invalid repository credentials` }); 32 35 } 33 36 37 + const uri = `at://${account.did}/${input.collection}/${input.rkey}` as CanonicalResourceUri; 38 + 39 + // validate before transaction (validation doesn't depend on create vs update) 40 + const writes: RepoWriteOp[] = [ 41 + { 42 + action: 'create', // placeholder - actual action determined in transaction 43 + collection: input.collection, 44 + rkey: input.rkey, 45 + swapRecord: input.swapRecord, 46 + record: input.record, 47 + }, 48 + ]; 49 + 50 + await validateRecordWrites(lexiconCache, writes, input.validate); 51 + 52 + // check if record exists and write in same transaction (upsert behavior) 34 53 const result = await actorManager.transact(account.did, (store) => { 54 + const exists = store.record.getRecord(uri) !== null; 55 + const action = exists ? 'update' : 'create'; 56 + 35 57 return store.repo.applyWrites( 36 58 [ 37 59 { 38 - action: 'update', 60 + action, 39 61 collection: input.collection, 40 62 rkey: input.rkey, 41 63 swapRecord: input.swapRecord,
+21
packages/danaus/src/config.ts
··· 83 83 serviceHandleDomains: string[]; 84 84 } 85 85 86 + export interface LexiconConfig { 87 + enabled: boolean; 88 + cacheDbLocation: string; 89 + nameservers: string[] | null; 90 + cacheStaleTtlMs: number; 91 + cacheMaxTtlMs: number; 92 + } 93 + 86 94 export interface SecretsConfig { 87 95 adminPassword: string | null; 88 96 dpopSecret: string | null; ··· 124 132 actorStore: ActorStoreConfig; 125 133 blobStore: BlobStoreConfig; 126 134 identity: IdentityConfig; 135 + lexicon: LexiconConfig; 127 136 secrets: SecretsConfig; 128 137 subscription: SubscriptionConfig; 129 138 email: EmailConfig | null; ··· 285 294 }; 286 295 } 287 296 297 + let lexicon: LexiconConfig; 298 + { 299 + lexicon = { 300 + enabled: env.PDS_LEXICON_VALIDATION_ENABLED ?? true, 301 + cacheDbLocation: env.PDS_LEXICON_CACHE_DB_LOCATION ?? locate('lexicon-cache.db'), 302 + nameservers: env.PDS_LEXICON_NAMESERVERS ?? null, 303 + cacheMaxTtlMs: env.PDS_LEXICON_CACHE_MAX_TTL ?? DAY, 304 + cacheStaleTtlMs: env.PDS_LEXICON_CACHE_STALE_TTL ?? HOUR, 305 + }; 306 + } 307 + 288 308 let secrets: SecretsConfig; 289 309 { 290 310 let jwtKey: KeyObject; ··· 350 370 actorStore, 351 371 blobStore, 352 372 identity, 373 + lexicon, 353 374 secrets, 354 375 subscription, 355 376 email,
+19
packages/danaus/src/context.ts
··· 9 9 type HandleResolver, 10 10 } from '@atcute/identity-resolver'; 11 11 import { NodeDnsHandleResolver } from '@atcute/identity-resolver-node'; 12 + import { NodeDnsLexiconAuthorityResolver } from '@atcute/lexicon-resolver-node'; 12 13 13 14 import { getAccountDb, type AccountDb } from './accounts/db'; 14 15 import { InviteCodeManager } from './accounts/invite-codes'; ··· 26 27 import { CachedDidDocumentResolver } from './identity/cached-did-document-resolver'; 27 28 import { CachedHandleResolver } from './identity/cached-handle-resolver'; 28 29 import { IdentityCache } from './identity/manager'; 30 + import { LexiconCache } from './lexicon/cache'; 29 31 import { createServiceProxy, type ServiceProxy } from './proxy/index'; 30 32 import { Sequencer } from './sequencer/sequencer'; 31 33 ··· 34 36 35 37 backgroundQueue: BackgroundQueue; 36 38 identityCache: IdentityCache; 39 + lexiconCache: LexiconCache; 37 40 38 41 handleResolver: HandleResolver; 39 42 didDocumentResolver: DidDocumentResolver<'plc' | 'web'>; ··· 89 92 resolver: baseDidDocumentResolver, 90 93 }); 91 94 95 + const lexiconAuthorityResolver = new NodeDnsLexiconAuthorityResolver({ 96 + nameservers: config.lexicon.nameservers ?? undefined, 97 + }); 98 + 99 + const lexiconCache = new LexiconCache({ 100 + location: config.lexicon.cacheDbLocation, 101 + walAutoCheckpointDisabled: config.database.walAutoCheckpointDisabled, 102 + backgroundQueue: backgroundQueue, 103 + authorityResolver: lexiconAuthorityResolver, 104 + didDocumentResolver: didDocumentResolver, 105 + staleTtl: config.lexicon.cacheStaleTtlMs, 106 + maxTtl: config.lexicon.cacheMaxTtlMs, 107 + enabled: config.lexicon.enabled, 108 + }); 109 + 92 110 const plcClient = new PlcClient({ 93 111 serviceUrl: config.identity.plcDirectoryUrl, 94 112 }); ··· 162 180 163 181 backgroundQueue: backgroundQueue, 164 182 identityCache: identityCache, 183 + lexiconCache: lexiconCache, 165 184 166 185 handleResolver: handleResolver, 167 186 didDocumentResolver: didDocumentResolver,
+6
packages/danaus/src/environment.ts
··· 70 70 ), 71 71 ), 72 72 73 + PDS_LEXICON_VALIDATION_ENABLED: v.optional(strbool), 74 + PDS_LEXICON_CACHE_DB_LOCATION: v.optional(str), 75 + PDS_LEXICON_NAMESERVERS: v.optional(strlist), 76 + PDS_LEXICON_CACHE_STALE_TTL: v.optional(strint), 77 + PDS_LEXICON_CACHE_MAX_TTL: v.optional(strint), 78 + 73 79 PDS_SUBSCRIPTION_BUFFER_LIMIT: v.optional(strint), 74 80 PDS_REPO_BACKFILL_LIMIT_MS: v.optional(strint), 75 81
+523
packages/danaus/src/lexicon/cache.ts
··· 1 + import { DocumentNotFoundError, type DidDocumentResolver } from '@atcute/identity-resolver'; 2 + import { findExternalReferences, type LexiconDoc } from '@atcute/lexicon-doc'; 3 + import { RecordValidator } from '@atcute/lexicon-doc/validations'; 4 + import { 5 + AuthorityNotFoundError, 6 + LexiconSchemaResolver, 7 + type LexiconAuthorityResolver, 8 + } from '@atcute/lexicon-resolver'; 9 + import type { AtprotoDid, Nsid } from '@atcute/lexicons/syntax'; 10 + 11 + import { eq, lt } from 'drizzle-orm'; 12 + import PQueue from 'p-queue'; 13 + 14 + import type { BackgroundQueue } from '#app/background.ts'; 15 + import { lexiconCacheLogger } from '#app/logger.ts'; 16 + import { HOUR } from '#app/utils/times.ts'; 17 + 18 + import { getLexiconCacheDb, t, type LexiconCacheDb } from './db/index.ts'; 19 + 20 + const DEFAULT_STALE_TTL = HOUR; 21 + const DEFAULT_MAX_TTL = 24 * HOUR; 22 + const DEFAULT_PRUNE_INTERVAL = HOUR; 23 + 24 + /** maximum depth when crawling lexicon references */ 25 + const DEFAULT_MAX_CRAWL_DEPTH = 10; 26 + /** maximum number of schemas to load when resolving dependencies */ 27 + const DEFAULT_MAX_CRAWL_SCHEMAS = 50; 28 + 29 + export interface LexiconCacheOptions { 30 + location: string; 31 + walAutoCheckpointDisabled: boolean; 32 + backgroundQueue: BackgroundQueue; 33 + authorityResolver: LexiconAuthorityResolver; 34 + didDocumentResolver: DidDocumentResolver; 35 + /** time before an entry is considered stale (default: 1 hour) */ 36 + staleTtl?: number; 37 + /** time before an entry expires completely (default: 24 hours) */ 38 + maxTtl?: number; 39 + /** interval between pruning runs (default: 1 hour) */ 40 + pruneInterval?: number; 41 + /** whether lexicon resolution is enabled (default: true) */ 42 + enabled?: boolean; 43 + } 44 + 45 + interface CachedAuthority { 46 + /** authority DID, or null if authority confirmed not to exist */ 47 + did: AtprotoDid | null; 48 + updatedAt: number; 49 + stale: boolean; 50 + expired: boolean; 51 + } 52 + 53 + interface CachedSchema { 54 + /** schema document, or null for negative cache entry */ 55 + doc: LexiconDoc | null; 56 + authorityDid: AtprotoDid; 57 + /** schema CID, or null for negative cache entry */ 58 + cid: string | null; 59 + updatedAt: number; 60 + stale: boolean; 61 + expired: boolean; 62 + } 63 + 64 + /** 65 + * SQLite-backed lexicon cache for authority resolutions and schema documents. 66 + * supports stale-while-revalidate pattern with background refresh. 67 + */ 68 + export class LexiconCache implements Disposable { 69 + readonly #db: LexiconCacheDb; 70 + readonly #backgroundQueue: BackgroundQueue; 71 + readonly #authorityResolver: LexiconAuthorityResolver; 72 + readonly #schemaResolver: LexiconSchemaResolver; 73 + readonly #staleTtl: number; 74 + readonly #maxTtl: number; 75 + readonly #pruneInterval: Timer; 76 + readonly #enabled: boolean; 77 + 78 + /** p-queue for limiting concurrent network fetches */ 79 + readonly #fetchQueue = new PQueue({ concurrency: 4 }); 80 + 81 + /** in-flight authority resolution promises for request coalescing */ 82 + readonly #inflightAuthority = new Map<string, Promise<AtprotoDid | null>>(); 83 + 84 + /** in-flight schema resolution promises for request coalescing */ 85 + readonly #inflightSchema = new Map<Nsid, Promise<LexiconDoc | null>>(); 86 + 87 + constructor(options: LexiconCacheOptions) { 88 + this.#db = getLexiconCacheDb(options.location, options.walAutoCheckpointDisabled); 89 + this.#backgroundQueue = options.backgroundQueue; 90 + this.#authorityResolver = options.authorityResolver; 91 + this.#schemaResolver = new LexiconSchemaResolver({ 92 + didDocumentResolver: options.didDocumentResolver, 93 + }); 94 + this.#staleTtl = options.staleTtl ?? DEFAULT_STALE_TTL; 95 + this.#maxTtl = options.maxTtl ?? DEFAULT_MAX_TTL; 96 + this.#enabled = options.enabled ?? true; 97 + 98 + const pruneIntervalMs = options.pruneInterval ?? DEFAULT_PRUNE_INTERVAL; 99 + this.#pruneInterval = setInterval(() => { 100 + this.#backgroundQueue.add(() => this.pruneExpired()); 101 + }, pruneIntervalMs); 102 + } 103 + 104 + /** whether lexicon resolution is enabled */ 105 + get enabled(): boolean { 106 + return this.#enabled; 107 + } 108 + 109 + // #region authority resolution 110 + 111 + /** 112 + * get NSID domain from an NSID (e.g., "app.bsky.feed.post" -> "app.bsky"). 113 + */ 114 + #getNsidDomain(nsid: Nsid): string { 115 + // NSID format: domain segments in reverse order, then name segment(s) 116 + // e.g., "app.bsky.feed.post" -> authority is "app.bsky" 117 + // the authority is determined by the first two segments 118 + const parts = nsid.split('.'); 119 + if (parts.length < 3) { 120 + return nsid; 121 + } 122 + return parts.slice(0, 2).join('.'); 123 + } 124 + 125 + /** 126 + * get cached authority resolution for an NSID domain. 127 + */ 128 + #getCachedAuthority(domain: string): CachedAuthority | null { 129 + const row = this.#db.select().from(t.authority).where(eq(t.authority.domain, domain)).get(); 130 + 131 + if (!row) { 132 + return null; 133 + } 134 + 135 + const now = Date.now(); 136 + const updatedAt = row.updated_at.getTime(); 137 + return { 138 + did: row.did, 139 + updatedAt: updatedAt, 140 + stale: now > updatedAt + this.#staleTtl, 141 + expired: now > updatedAt + this.#maxTtl, 142 + }; 143 + } 144 + 145 + /** 146 + * cache an authority resolution. 147 + * @param domain NSID domain (e.g., "app.bsky") 148 + * @param did authority DID, or null to cache a negative result 149 + * @internal exposed as `_setAuthority` for test injection 150 + */ 151 + _setAuthority(domain: string, did: AtprotoDid | null): void { 152 + const now = new Date(); 153 + this.#db 154 + .insert(t.authority) 155 + .values({ domain, did, updated_at: now }) 156 + .onConflictDoUpdate({ 157 + target: t.authority.domain, 158 + set: { did, updated_at: now }, 159 + }) 160 + .run(); 161 + } 162 + 163 + /** 164 + * resolve the authority DID for an NSID. 165 + * @param nsid NSID to resolve authority for 166 + * @returns authority DID or null if resolution fails or authority doesn't exist 167 + */ 168 + async resolveAuthority(nsid: Nsid): Promise<AtprotoDid | null> { 169 + if (!this.#enabled) { 170 + return null; 171 + } 172 + 173 + const domain = this.#getNsidDomain(nsid); 174 + 175 + // check cache first (includes negative cache entries) 176 + const cached = this.#getCachedAuthority(domain); 177 + if (cached && !cached.expired) { 178 + if (cached.stale && cached.did !== null) { 179 + // only refresh in background for positive results 180 + this.#refreshAuthorityInBackground(nsid); 181 + } 182 + return cached.did; 183 + } 184 + 185 + // coalesce concurrent requests 186 + const existing = this.#inflightAuthority.get(domain); 187 + if (existing) { 188 + return existing; 189 + } 190 + 191 + // queue the fetch 192 + const promise = this.#fetchQueue.add(async () => { 193 + try { 194 + const did = await this.#authorityResolver.resolve(nsid); 195 + this._setAuthority(domain, did); 196 + return did; 197 + } catch (err) { 198 + // cache negative result for AuthorityNotFoundError (definitive "no authority") 199 + if (err instanceof AuthorityNotFoundError) { 200 + lexiconCacheLogger.debug('caching negative authority result', { nsid, domain }); 201 + this._setAuthority(domain, null); 202 + return null; 203 + } 204 + 205 + // don't cache transient errors 206 + lexiconCacheLogger.warn('failed to resolve lexicon authority', { nsid, err }); 207 + return null; 208 + } 209 + }); 210 + 211 + this.#inflightAuthority.set(domain, promise); 212 + 213 + try { 214 + return await promise; 215 + } finally { 216 + this.#inflightAuthority.delete(domain); 217 + } 218 + } 219 + 220 + /** 221 + * queue a background refresh for an authority resolution. 222 + */ 223 + #refreshAuthorityInBackground(nsid: Nsid): void { 224 + const domain = this.#getNsidDomain(nsid); 225 + 226 + this.#backgroundQueue.add(async () => { 227 + try { 228 + const did = await this.#authorityResolver.resolve(nsid); 229 + this._setAuthority(domain, did); 230 + } catch (err) { 231 + lexiconCacheLogger.warn('background authority refresh failed', { nsid, err }); 232 + } 233 + }); 234 + } 235 + 236 + // #endregion 237 + 238 + // #region schema resolution 239 + 240 + /** 241 + * get cached schema for an NSID. 242 + */ 243 + #getCachedSchema(nsid: Nsid): CachedSchema | null { 244 + const row = this.#db.select().from(t.schema).where(eq(t.schema.nsid, nsid)).get(); 245 + 246 + if (!row) { 247 + return null; 248 + } 249 + 250 + const now = Date.now(); 251 + const updatedAt = row.updated_at.getTime(); 252 + return { 253 + doc: row.doc, 254 + authorityDid: row.authority_did, 255 + cid: row.cid, 256 + updatedAt: updatedAt, 257 + stale: now > updatedAt + this.#staleTtl, 258 + expired: now > updatedAt + this.#maxTtl, 259 + }; 260 + } 261 + 262 + /** 263 + * cache a schema document. 264 + * @param nsid NSID of the schema 265 + * @param authorityDid authority DID that was used 266 + * @param cid schema CID, or null for negative cache entry 267 + * @param doc schema document, or null for negative cache entry 268 + * @internal exposed as `_setSchema` for test injection 269 + */ 270 + _setSchema(nsid: Nsid, authorityDid: AtprotoDid, cid: string | null, doc: LexiconDoc | null): void { 271 + const now = new Date(); 272 + this.#db 273 + .insert(t.schema) 274 + .values({ 275 + nsid, 276 + authority_did: authorityDid, 277 + cid, 278 + doc, 279 + updated_at: now, 280 + }) 281 + .onConflictDoUpdate({ 282 + target: t.schema.nsid, 283 + set: { 284 + authority_did: authorityDid, 285 + cid, 286 + doc, 287 + updated_at: now, 288 + }, 289 + }) 290 + .run(); 291 + } 292 + 293 + /** 294 + * get a lexicon schema document. 295 + * @param nsid NSID to fetch schema for 296 + * @returns lexicon document or null if not found/resolution fails 297 + */ 298 + async getSchema(nsid: Nsid): Promise<LexiconDoc | null> { 299 + if (!this.#enabled) { 300 + return null; 301 + } 302 + 303 + // resolve authority first to check for authority changes 304 + const authorityDid = await this.resolveAuthority(nsid); 305 + if (!authorityDid) { 306 + return null; 307 + } 308 + 309 + // check cache - invalidate if authority has changed 310 + const cached = this.#getCachedSchema(nsid); 311 + if (cached && !cached.expired) { 312 + // if authority changed, bust the cache and refetch 313 + if (cached.authorityDid !== authorityDid) { 314 + lexiconCacheLogger.debug('authority changed, invalidating schema cache', { 315 + nsid, 316 + oldAuthority: cached.authorityDid, 317 + newAuthority: authorityDid, 318 + }); 319 + // don't return cached, fall through to refetch 320 + } else { 321 + if (cached.stale && cached.doc !== null) { 322 + // only refresh in background for positive results 323 + this.#refreshSchemaInBackground(nsid, cached.authorityDid); 324 + } 325 + return cached.doc; 326 + } 327 + } 328 + 329 + // coalesce concurrent requests 330 + const existing = this.#inflightSchema.get(nsid); 331 + if (existing) { 332 + const result = await existing; 333 + return result; 334 + } 335 + 336 + // queue the fetch 337 + const promise = this.#fetchQueue.add(async () => { 338 + try { 339 + const resolved = await this.#schemaResolver.resolve(authorityDid, nsid); 340 + this._setSchema(nsid, authorityDid, resolved.cid, resolved.schema); 341 + return resolved.schema; 342 + } catch (err) { 343 + // cache negative result for definitive "schema doesn't exist" errors 344 + if (err instanceof DocumentNotFoundError) { 345 + lexiconCacheLogger.debug('caching negative schema result', { nsid, authorityDid }); 346 + this._setSchema(nsid, authorityDid, null, null); 347 + return null; 348 + } 349 + 350 + // don't cache transient errors (network failures, etc.) 351 + lexiconCacheLogger.warn('failed to resolve lexicon schema', { nsid, authorityDid, err }); 352 + return null; 353 + } 354 + }); 355 + 356 + this.#inflightSchema.set(nsid, promise); 357 + 358 + try { 359 + const result = await promise; 360 + return result; 361 + } finally { 362 + this.#inflightSchema.delete(nsid); 363 + } 364 + } 365 + 366 + /** 367 + * queue a background refresh for a schema. 368 + */ 369 + #refreshSchemaInBackground(nsid: Nsid, authorityDid: AtprotoDid): void { 370 + this.#backgroundQueue.add(async () => { 371 + try { 372 + const resolved = await this.#schemaResolver.resolve(authorityDid, nsid); 373 + this._setSchema(nsid, authorityDid, resolved.cid, resolved.schema); 374 + } catch (err) { 375 + lexiconCacheLogger.warn('background schema refresh failed', { nsid, err }); 376 + } 377 + }); 378 + } 379 + 380 + // #endregion 381 + 382 + // #region dependency resolution 383 + 384 + /** 385 + * get all schema documents needed for validating a record type. 386 + * recursively resolves all external references with depth and total limits. 387 + * @param nsid NSID of the record type 388 + * @returns map of NSID -> LexiconDoc, or null if any required schema can't be resolved 389 + */ 390 + async getRecordDocs(nsid: Nsid): Promise<Record<string, LexiconDoc> | null> { 391 + if (!this.#enabled) { 392 + return null; 393 + } 394 + 395 + const docs: Record<string, LexiconDoc> = {}; 396 + const visited = new Set<string>(); 397 + let count = 0; 398 + 399 + // start with the main reference 400 + for await (const result of this.#crawlReferences(`${nsid}#main`, visited, 0)) { 401 + // null indicates a required schema couldn't be resolved or limits exceeded 402 + if (result === null) { 403 + return null; 404 + } 405 + 406 + // check total schemas limit 407 + if (count >= DEFAULT_MAX_CRAWL_SCHEMAS) { 408 + return null; 409 + } 410 + 411 + docs[result.nsid] = result.schema; 412 + count++; 413 + } 414 + 415 + return docs; 416 + } 417 + 418 + /** 419 + * recursively crawl all transitive dependencies for a given reference. 420 + * yields null if a required schema can't be resolved or limits are exceeded. 421 + * @param ref reference to crawl (e.g., "app.bsky.feed.post#main") 422 + * @param visited set of already-visited references (for cycle detection) 423 + * @param depth current recursion depth 424 + */ 425 + async *#crawlReferences( 426 + ref: string, 427 + visited: Set<string>, 428 + depth: number, 429 + ): AsyncGenerator<{ nsid: Nsid; schema: LexiconDoc } | null> { 430 + // check depth limit 431 + if (depth > DEFAULT_MAX_CRAWL_DEPTH) { 432 + yield null; 433 + return; 434 + } 435 + 436 + // normalize ref to include #defId 437 + if (!ref.includes('#')) { 438 + ref = `${ref}#main`; 439 + } 440 + 441 + // cycle detection 442 + if (visited.has(ref)) { 443 + return; 444 + } 445 + visited.add(ref); 446 + 447 + // parse the reference 448 + const hashIndex = ref.indexOf('#'); 449 + const nsid = ref.slice(0, hashIndex) as Nsid; 450 + const defId = ref.slice(hashIndex + 1); 451 + 452 + // try to load the schema 453 + const schema = await this.getSchema(nsid); 454 + if (schema === null) { 455 + yield null; 456 + return; 457 + } 458 + 459 + yield { nsid, schema }; 460 + 461 + // find external references in the specific definition 462 + const externalRefs = findExternalReferences(schema, defId); 463 + 464 + // recursively crawl each external reference 465 + for (const externalRef of externalRefs) { 466 + yield* this.#crawlReferences(externalRef, visited, depth + 1); 467 + } 468 + } 469 + 470 + // #endregion 471 + 472 + // #region validation 473 + 474 + /** 475 + * create a RecordValidator for a record type. 476 + * @param nsid NSID of the record type 477 + * @returns RecordValidator or null if schemas can't be resolved 478 + */ 479 + async getRecordValidator(nsid: Nsid): Promise<RecordValidator | null> { 480 + if (!this.#enabled) { 481 + return null; 482 + } 483 + 484 + const docs = await this.getRecordDocs(nsid); 485 + if (!docs) { 486 + return null; 487 + } 488 + 489 + return new RecordValidator(docs, nsid); 490 + } 491 + 492 + // #endregion 493 + 494 + // #region maintenance 495 + 496 + /** 497 + * remove all expired entries from the cache. 498 + */ 499 + async pruneExpired(): Promise<void> { 500 + const cutoff = new Date(Date.now() - this.#maxTtl); 501 + this.#db.delete(t.authority).where(lt(t.authority.updated_at, cutoff)).run(); 502 + this.#db.delete(t.schema).where(lt(t.schema.updated_at, cutoff)).run(); 503 + } 504 + 505 + /** 506 + * clear all cached entries. 507 + */ 508 + clear(): void { 509 + this.#db.delete(t.authority).run(); 510 + this.#db.delete(t.schema).run(); 511 + } 512 + 513 + dispose(): void { 514 + clearInterval(this.#pruneInterval); 515 + this.#db.$client.close(); 516 + } 517 + 518 + [Symbol.dispose](): void { 519 + this.dispose(); 520 + } 521 + 522 + // #endregion 523 + }
+30
packages/danaus/src/lexicon/db/index.ts
··· 1 + import { Database } from 'bun:sqlite'; 2 + import path from 'node:path'; 3 + 4 + import { drizzle } from 'drizzle-orm/bun-sqlite'; 5 + import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; 6 + 7 + import * as schema from './schema.ts'; 8 + 9 + const MIGRATIONS_DIR = path.resolve(import.meta.dir, '../../../drizzle/lexicon'); 10 + 11 + export const getLexiconCacheDb = (location: string, walAutoCheckpointDisabled: boolean) => { 12 + const sqliteDb = new Database(location); 13 + sqliteDb.run(`PRAGMA journal_mode = WAL;`); 14 + if (walAutoCheckpointDisabled) { 15 + sqliteDb.run(`PRAGMA wal_autocheckpoint = 0;`); 16 + } 17 + 18 + const db = drizzle({ 19 + client: sqliteDb, 20 + schema: schema, 21 + }); 22 + 23 + migrate(db, { migrationsFolder: MIGRATIONS_DIR }); 24 + 25 + return db; 26 + }; 27 + 28 + export type LexiconCacheDb = ReturnType<typeof getLexiconCacheDb>; 29 + 30 + export { schema as t };
+37
packages/danaus/src/lexicon/db/schema.ts
··· 1 + import type { LexiconDoc } from '@atcute/lexicon-doc'; 2 + import type { AtprotoDid, Nsid } from '@atcute/lexicons/syntax'; 3 + 4 + import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 5 + 6 + /** 7 + * cached lexicon authority resolutions (NSID domain → authority DID). 8 + * `did` is null for negative cache entries (authority confirmed not to exist). 9 + */ 10 + export const authority = sqliteTable( 11 + 'authority', 12 + { 13 + domain: text().primaryKey(), 14 + /** authority DID, or null if authority confirmed not to exist */ 15 + did: text().$type<AtprotoDid | null>(), 16 + updated_at: integer({ mode: 'timestamp' }).notNull(), 17 + }, 18 + (t) => [index('authority_updated_at_idx').on(t.updated_at)], 19 + ); 20 + 21 + /** 22 + * cached lexicon schemas. 23 + * `cid` and `doc` are null for negative cache entries (schema confirmed not to exist). 24 + */ 25 + export const schema = sqliteTable( 26 + 'schema', 27 + { 28 + nsid: text().$type<Nsid>().primaryKey(), 29 + authority_did: text().$type<AtprotoDid>().notNull(), 30 + /** schema CID, or null if schema confirmed not to exist */ 31 + cid: text().$type<string | null>(), 32 + /** schema document, or null if schema confirmed not to exist */ 33 + doc: text({ mode: 'json' }).$type<LexiconDoc | null>(), 34 + updated_at: integer({ mode: 'timestamp' }).notNull(), 35 + }, 36 + (t) => [index('schema_updated_at_idx').on(t.updated_at)], 37 + );
+84
packages/danaus/src/lexicon/validate-writes.ts
··· 1 + import { ValidationError } from '@atcute/lexicons/validations'; 2 + import * as TID from '@atcute/tid'; 3 + import { InvalidRequestError } from '@atcute/xrpc-server'; 4 + 5 + import type { RepoWriteOp } from '#app/actors/repo/types.ts'; 6 + 7 + import type { LexiconCache } from './cache.ts'; 8 + 9 + /** 10 + * validate record writes against lexicon schemas. 11 + * 12 + * @param lexiconCache lexicon cache for resolving schemas 13 + * @param writes array of write operations 14 + * @param validate validation mode: 15 + * - `true`: require validation, fail if lexicon cannot be resolved 16 + * - `false`: skip validation entirely 17 + * - `undefined` (default): validate if lexicon is known, skip if not 18 + * @throws InvalidRequestError with `InvalidRecord` if validation fails 19 + * @throws InvalidRequestError with `UnresolvableLexicon` if validate=true and lexicon cannot be resolved 20 + */ 21 + export const validateRecordWrites = async ( 22 + lexiconCache: LexiconCache, 23 + writes: RepoWriteOp[], 24 + validate: boolean | undefined, 25 + ): Promise<void> => { 26 + // skip if validation is disabled 27 + if (validate === false) { 28 + return; 29 + } 30 + 31 + // skip if lexicon resolution is disabled 32 + if (!lexiconCache.enabled) { 33 + if (validate === true) { 34 + throw new InvalidRequestError({ 35 + error: 'UnresolvableLexicon', 36 + description: `lexicon resolution is disabled`, 37 + }); 38 + } 39 + 40 + return; 41 + } 42 + 43 + const ops = writes.filter((write) => write.action === 'create' || write.action === 'update'); 44 + 45 + if (ops.length === 0) { 46 + return; 47 + } 48 + 49 + // fallback TID for validating key format when rkey not provided 50 + const tid = TID.now(); 51 + 52 + // validate each write 53 + await Promise.all( 54 + ops.map(async (write) => { 55 + const validator = await lexiconCache.getRecordValidator(write.collection); 56 + 57 + if (!validator) { 58 + // lexicon not found 59 + if (validate === true) { 60 + throw new InvalidRequestError({ 61 + error: 'UnresolvableLexicon', 62 + description: `could not resolve lexicon for ${write.collection}`, 63 + }); 64 + } 65 + 66 + // validate=undefined: skip if lexicon not known 67 + return; 68 + } 69 + 70 + try { 71 + validator.parse({ key: write.rkey ?? tid, object: write.record }); 72 + } catch (err) { 73 + if (err instanceof ValidationError) { 74 + throw new InvalidRequestError({ 75 + error: 'InvalidRecord', 76 + description: `record failed validation: ${err.message}`, 77 + }); 78 + } 79 + 80 + throw err; 81 + } 82 + }), 83 + ); 84 + };
+3
packages/danaus/src/logger.ts
··· 68 68 /** DID/identity cache operations */ 69 69 export const didCacheLogger = getLogger(['danaus', 'did-cache']); 70 70 71 + /** lexicon cache operations */ 72 + export const lexiconCacheLogger = getLogger(['danaus', 'lexicon-cache']); 73 + 71 74 /** event sequencer */ 72 75 export const seqLogger = getLogger(['danaus', 'sequencer']); 73 76
+13 -2
packages/danaus/src/test/test-pds.ts
··· 8 8 9 9 import getPort from 'get-port'; 10 10 11 - import type { AppConfig, ProxyConfig, ServiceConfig } from '#app/config.ts'; 11 + import type { AppConfig, LexiconConfig, ProxyConfig, ServiceConfig } from '#app/config.ts'; 12 12 import { PdsServer } from '#app/pds-server.ts'; 13 13 14 14 import { ADMIN_PASSWORD, JWT_SECRET } from './const.ts'; ··· 19 19 const HOUR = 60 * 60 * 1000; 20 20 const DAY = 24 * HOUR; 21 21 22 - export interface TestPdsConfig extends Partial<AppConfig> { 22 + export interface TestPdsConfig extends Partial<Omit<AppConfig, 'lexicon'>> { 23 23 plcUrl: string; 24 24 port?: number; 25 25 /** persistent data directory; uses temp directory if not provided */ 26 26 dataDirectory?: string; 27 27 /** hex-encoded secp256k1 private key for PLC rotation */ 28 28 plcRotationKey?: string; 29 + /** lexicon config overrides */ 30 + lexicon?: Partial<LexiconConfig>; 29 31 } 30 32 31 33 /** ··· 125 127 plcRecoveryKey: null, 126 128 serviceHandleDomains: DEFAULT_HANDLE_DOMAINS, 127 129 ...cfg.identity, 130 + }, 131 + lexicon: { 132 + // disabled by default in tests to avoid network access 133 + enabled: false, 134 + cacheDbLocation: path.join(rootDir, 'lexicon-cache.db'), 135 + nameservers: null, 136 + cacheStaleTtlMs: HOUR, 137 + cacheMaxTtlMs: DAY, 138 + ...cfg.lexicon, 128 139 }, 129 140 secrets: { 130 141 adminPassword: ADMIN_PASSWORD,
+534
packages/danaus/tests/lexicon-validation.test.ts
··· 1 + import { afterAll, beforeAll, describe, expect, it } from 'bun:test'; 2 + 3 + import { 4 + ComAtprotoRepoApplyWrites, 5 + ComAtprotoRepoCreateRecord, 6 + ComAtprotoRepoPutRecord, 7 + } from '@atcute/atproto'; 8 + import { Client, ok, simpleFetchHandler } from '@atcute/client'; 9 + import type { LexiconDoc } from '@atcute/lexicon-doc'; 10 + import type { Did } from '@atcute/lexicons'; 11 + 12 + import { TestNetworkNoAppView, usersSeed, type SeedClient } from '#app/test/index.ts'; 13 + 14 + /** 15 + * simple test lexicon with no external references. 16 + * defines a record type with required `title` and `count` fields. 17 + */ 18 + const TEST_LEXICON: LexiconDoc = { 19 + lexicon: 1, 20 + id: 'com.example.simple', 21 + defs: { 22 + main: { 23 + type: 'record', 24 + key: 'any', 25 + record: { 26 + type: 'object', 27 + required: ['title', 'count'], 28 + properties: { 29 + title: { type: 'string', maxLength: 100 }, 30 + count: { type: 'integer', minimum: 0 }, 31 + description: { type: 'string' }, 32 + }, 33 + }, 34 + }, 35 + }, 36 + }; 37 + 38 + /** 39 + * test lexicon that requires a literal 'self' rkey (like app.bsky.actor.profile). 40 + */ 41 + const TEST_LEXICON_SELF_KEY: LexiconDoc = { 42 + lexicon: 1, 43 + id: 'com.example.selfkey', 44 + defs: { 45 + main: { 46 + type: 'record', 47 + key: 'literal:self', 48 + record: { 49 + type: 'object', 50 + required: ['name'], 51 + properties: { 52 + name: { type: 'string' }, 53 + }, 54 + }, 55 + }, 56 + }, 57 + }; 58 + 59 + /** 60 + * test lexicon authority DID (fake). 61 + */ 62 + const TEST_AUTHORITY_DID = 'did:plc:testauthority123'; 63 + 64 + describe('lexicon validation', () => { 65 + let network: TestNetworkNoAppView; 66 + let sc: SeedClient; 67 + let client: Client; 68 + let alice: Did; 69 + 70 + beforeAll(async () => { 71 + // create network with lexicon validation enabled 72 + network = await TestNetworkNoAppView.create({ 73 + pds: { 74 + lexicon: { 75 + enabled: true, 76 + }, 77 + }, 78 + }); 79 + 80 + sc = network.getSeedClient(); 81 + await usersSeed(sc); 82 + alice = sc.dids.alice!; 83 + client = new Client({ handler: simpleFetchHandler({ service: network.pds.url }) }); 84 + 85 + // inject test lexicons into cache 86 + const lexiconCache = network.pds.ctx.lexiconCache; 87 + lexiconCache._setAuthority('com.example', TEST_AUTHORITY_DID); 88 + lexiconCache._setSchema('com.example.simple', TEST_AUTHORITY_DID, 'testcid123', TEST_LEXICON); 89 + lexiconCache._setSchema('com.example.selfkey', TEST_AUTHORITY_DID, 'testcid456', TEST_LEXICON_SELF_KEY); 90 + }); 91 + 92 + afterAll(async () => { 93 + await network?.close(); 94 + }); 95 + 96 + const getHeaders = (did: Did) => sc.getHeaders(did); 97 + 98 + /** helper to expect an XRPC error */ 99 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 100 + const expectError = async (result: Promise<any>, expectedError: string): Promise<void> => { 101 + const res = await result; 102 + expect(res.ok).toBe(false); 103 + expect(res.data?.error).toBe(expectedError); 104 + }; 105 + 106 + describe('createRecord', () => { 107 + it('accepts valid record when validate=true', async () => { 108 + const result = await ok( 109 + client.call(ComAtprotoRepoCreateRecord, { 110 + input: { 111 + repo: alice, 112 + collection: 'com.example.simple', 113 + validate: true, 114 + record: { 115 + $type: 'com.example.simple', 116 + title: 'Test Title', 117 + count: 42, 118 + }, 119 + }, 120 + headers: getHeaders(alice), 121 + }), 122 + ); 123 + 124 + expect(result.uri).toContain('com.example.simple'); 125 + expect(result.cid).toBeDefined(); 126 + }); 127 + 128 + it('rejects invalid record when validate=true', async () => { 129 + await expectError( 130 + client.call(ComAtprotoRepoCreateRecord, { 131 + input: { 132 + repo: alice, 133 + collection: 'com.example.simple', 134 + validate: true, 135 + record: { 136 + $type: 'com.example.simple', 137 + // missing required 'title' field 138 + count: 42, 139 + }, 140 + }, 141 + headers: getHeaders(alice), 142 + }), 143 + 'InvalidRecord', 144 + ); 145 + }); 146 + 147 + it('rejects record with wrong type when validate=true', async () => { 148 + await expectError( 149 + client.call(ComAtprotoRepoCreateRecord, { 150 + input: { 151 + repo: alice, 152 + collection: 'com.example.simple', 153 + validate: true, 154 + record: { 155 + $type: 'com.example.simple', 156 + title: 'Test', 157 + count: 'not a number', // should be integer 158 + }, 159 + }, 160 + headers: getHeaders(alice), 161 + }), 162 + 'InvalidRecord', 163 + ); 164 + }); 165 + 166 + it('rejects unknown lexicon when validate=true', async () => { 167 + await expectError( 168 + client.call(ComAtprotoRepoCreateRecord, { 169 + input: { 170 + repo: alice, 171 + collection: 'com.unknown.type', 172 + validate: true, 173 + record: { 174 + $type: 'com.unknown.type', 175 + anything: 'goes', 176 + }, 177 + }, 178 + headers: getHeaders(alice), 179 + }), 180 + 'UnresolvableLexicon', 181 + ); 182 + }); 183 + 184 + it('allows unknown lexicon when validate=undefined (default)', async () => { 185 + const result = await ok( 186 + client.call(ComAtprotoRepoCreateRecord, { 187 + input: { 188 + repo: alice, 189 + collection: 'com.unknown.type', 190 + // validate not specified (undefined) 191 + record: { 192 + $type: 'com.unknown.type', 193 + anything: 'goes', 194 + }, 195 + }, 196 + headers: getHeaders(alice), 197 + }), 198 + ); 199 + 200 + expect(result.uri).toContain('com.unknown.type'); 201 + }); 202 + 203 + it('skips validation entirely when validate=false', async () => { 204 + const result = await ok( 205 + client.call(ComAtprotoRepoCreateRecord, { 206 + input: { 207 + repo: alice, 208 + collection: 'com.example.simple', 209 + validate: false, 210 + record: { 211 + $type: 'com.example.simple', 212 + // completely invalid - missing required fields 213 + invalidField: true, 214 + }, 215 + }, 216 + headers: getHeaders(alice), 217 + }), 218 + ); 219 + 220 + expect(result.uri).toContain('com.example.simple'); 221 + }); 222 + }); 223 + 224 + describe('putRecord', () => { 225 + it('creates record if not exists (upsert) when validate=true', async () => { 226 + const result = await ok( 227 + client.call(ComAtprotoRepoPutRecord, { 228 + input: { 229 + repo: alice, 230 + collection: 'com.example.simple', 231 + rkey: 'test-put-create', 232 + validate: true, 233 + record: { 234 + $type: 'com.example.simple', 235 + title: 'Put Create Test', 236 + count: 10, 237 + }, 238 + }, 239 + headers: getHeaders(alice), 240 + }), 241 + ); 242 + 243 + expect(result.uri).toContain('test-put-create'); 244 + }); 245 + 246 + it('updates record if exists (upsert) when validate=true', async () => { 247 + // first create via putRecord 248 + await ok( 249 + client.call(ComAtprotoRepoPutRecord, { 250 + input: { 251 + repo: alice, 252 + collection: 'com.example.simple', 253 + rkey: 'test-put-update', 254 + validate: true, 255 + record: { 256 + $type: 'com.example.simple', 257 + title: 'Initial', 258 + count: 0, 259 + }, 260 + }, 261 + headers: getHeaders(alice), 262 + }), 263 + ); 264 + 265 + // then update via putRecord 266 + const result = await ok( 267 + client.call(ComAtprotoRepoPutRecord, { 268 + input: { 269 + repo: alice, 270 + collection: 'com.example.simple', 271 + rkey: 'test-put-update', 272 + validate: true, 273 + record: { 274 + $type: 'com.example.simple', 275 + title: 'Updated', 276 + count: 99, 277 + }, 278 + }, 279 + headers: getHeaders(alice), 280 + }), 281 + ); 282 + 283 + expect(result.uri).toContain('test-put-update'); 284 + }); 285 + 286 + it('rejects invalid record when validate=true', async () => { 287 + await expectError( 288 + client.call(ComAtprotoRepoPutRecord, { 289 + input: { 290 + repo: alice, 291 + collection: 'com.example.simple', 292 + rkey: 'test-put-invalid', 293 + validate: true, 294 + record: { 295 + $type: 'com.example.simple', 296 + title: 'Missing Count', 297 + // missing required 'count' field 298 + }, 299 + }, 300 + headers: getHeaders(alice), 301 + }), 302 + 'InvalidRecord', 303 + ); 304 + }); 305 + }); 306 + 307 + describe('applyWrites', () => { 308 + it('accepts valid writes when validate=true', async () => { 309 + const result = await ok( 310 + client.call(ComAtprotoRepoApplyWrites, { 311 + input: { 312 + repo: alice, 313 + validate: true, 314 + writes: [ 315 + { 316 + $type: 'com.atproto.repo.applyWrites#create', 317 + collection: 'com.example.simple', 318 + value: { 319 + $type: 'com.example.simple', 320 + title: 'Apply Write Test', 321 + count: 99, 322 + }, 323 + }, 324 + ], 325 + }, 326 + headers: getHeaders(alice), 327 + }), 328 + ); 329 + 330 + expect(result.results).toHaveLength(1); 331 + expect(result.results![0]).toHaveProperty('uri'); 332 + }); 333 + 334 + it('rejects any invalid write in batch when validate=true', async () => { 335 + await expectError( 336 + client.call(ComAtprotoRepoApplyWrites, { 337 + input: { 338 + repo: alice, 339 + validate: true, 340 + writes: [ 341 + { 342 + $type: 'com.atproto.repo.applyWrites#create', 343 + collection: 'com.example.simple', 344 + value: { 345 + $type: 'com.example.simple', 346 + title: 'Valid One', 347 + count: 1, 348 + }, 349 + }, 350 + { 351 + $type: 'com.atproto.repo.applyWrites#create', 352 + collection: 'com.example.simple', 353 + value: { 354 + $type: 'com.example.simple', 355 + // invalid - missing required fields 356 + }, 357 + }, 358 + ], 359 + }, 360 + headers: getHeaders(alice), 361 + }), 362 + 'InvalidRecord', 363 + ); 364 + }); 365 + 366 + it('validates updates but not deletes', async () => { 367 + // first create a record to delete 368 + const created = await ok( 369 + client.call(ComAtprotoRepoCreateRecord, { 370 + input: { 371 + repo: alice, 372 + collection: 'com.example.simple', 373 + validate: false, 374 + record: { 375 + $type: 'com.example.simple', 376 + title: 'To Delete', 377 + count: 0, 378 + }, 379 + }, 380 + headers: getHeaders(alice), 381 + }), 382 + ); 383 + 384 + const rkey = created.uri.split('/').pop()!; 385 + 386 + // delete should work even with validate=true (no record to validate) 387 + const result = await ok( 388 + client.call(ComAtprotoRepoApplyWrites, { 389 + input: { 390 + repo: alice, 391 + validate: true, 392 + writes: [ 393 + { 394 + $type: 'com.atproto.repo.applyWrites#delete', 395 + collection: 'com.example.simple', 396 + rkey, 397 + }, 398 + ], 399 + }, 400 + headers: getHeaders(alice), 401 + }), 402 + ); 403 + 404 + expect(result.results).toHaveLength(1); 405 + }); 406 + }); 407 + 408 + describe('validation constraints', () => { 409 + it('enforces maxLength constraint', async () => { 410 + await expectError( 411 + client.call(ComAtprotoRepoCreateRecord, { 412 + input: { 413 + repo: alice, 414 + collection: 'com.example.simple', 415 + validate: true, 416 + record: { 417 + $type: 'com.example.simple', 418 + title: 'x'.repeat(101), // exceeds maxLength: 100 419 + count: 1, 420 + }, 421 + }, 422 + headers: getHeaders(alice), 423 + }), 424 + 'InvalidRecord', 425 + ); 426 + }); 427 + 428 + it('enforces minimum constraint', async () => { 429 + await expectError( 430 + client.call(ComAtprotoRepoCreateRecord, { 431 + input: { 432 + repo: alice, 433 + collection: 'com.example.simple', 434 + validate: true, 435 + record: { 436 + $type: 'com.example.simple', 437 + title: 'Test', 438 + count: -1, // violates minimum: 0 439 + }, 440 + }, 441 + headers: getHeaders(alice), 442 + }), 443 + 'InvalidRecord', 444 + ); 445 + }); 446 + 447 + it('accepts optional fields', async () => { 448 + const result = await ok( 449 + client.call(ComAtprotoRepoCreateRecord, { 450 + input: { 451 + repo: alice, 452 + collection: 'com.example.simple', 453 + validate: true, 454 + record: { 455 + $type: 'com.example.simple', 456 + title: 'With Description', 457 + count: 5, 458 + description: 'This is optional', 459 + }, 460 + }, 461 + headers: getHeaders(alice), 462 + }), 463 + ); 464 + 465 + expect(result.uri).toBeDefined(); 466 + }); 467 + 468 + it('rejects missing rkey when lexicon requires literal key', async () => { 469 + // com.example.selfkey requires key: 'literal:self' 470 + // not providing rkey should fail because auto-generated TID != 'self' 471 + await expectError( 472 + client.call(ComAtprotoRepoCreateRecord, { 473 + input: { 474 + repo: alice, 475 + collection: 'com.example.selfkey', 476 + validate: true, 477 + // no rkey provided - will use fallback TID which doesn't match 'self' 478 + record: { 479 + $type: 'com.example.selfkey', 480 + name: 'Test', 481 + }, 482 + }, 483 + headers: getHeaders(alice), 484 + }), 485 + 'InvalidRecord', 486 + ); 487 + }); 488 + 489 + it('accepts correct literal rkey', async () => { 490 + const result = await ok( 491 + client.call(ComAtprotoRepoCreateRecord, { 492 + input: { 493 + repo: alice, 494 + collection: 'com.example.selfkey', 495 + rkey: 'self', 496 + validate: true, 497 + record: { 498 + $type: 'com.example.selfkey', 499 + name: 'Test', 500 + }, 501 + }, 502 + headers: getHeaders(alice), 503 + }), 504 + ); 505 + 506 + expect(result.uri).toContain('/self'); 507 + }); 508 + }); 509 + 510 + describe('negative caching', () => { 511 + it('caches authority not found and does not repeat lookups', async () => { 512 + const lexiconCache = network.pds.ctx.lexiconCache; 513 + 514 + // first lookup - should trigger DNS resolution and cache negative result 515 + const result1 = await lexiconCache.resolveAuthority('com.notfound.test' as never); 516 + expect(result1).toBe(null); 517 + 518 + // second lookup - should hit cache, not trigger another DNS lookup 519 + const result2 = await lexiconCache.resolveAuthority('com.notfound.other' as never); 520 + expect(result2).toBe(null); 521 + 522 + // verify it's cached by checking the internal state 523 + // if the authority was cached, repeated calls should be instant 524 + const start = performance.now(); 525 + for (let i = 0; i < 100; i++) { 526 + await lexiconCache.resolveAuthority(`com.notfound.item${i}` as never); 527 + } 528 + const elapsed = performance.now() - start; 529 + 530 + // should be fast since it's all hitting cache (no network) 531 + expect(elapsed).toBeLessThan(100); 532 + }); 533 + }); 534 + });
+21 -459
pnpm-lock.yaml
··· 13 13 14 14 .: 15 15 devDependencies: 16 - '@ianvs/prettier-plugin-sort-imports': 17 - specifier: ^4.7.0 18 - version: 4.7.0(@prettier/plugin-oxc@0.1.3)(@vue/compiler-sfc@3.5.26)(prettier@3.8.0) 19 - '@prettier/plugin-oxc': 20 - specifier: ^0.1.3 21 - version: 0.1.3 22 16 oxfmt: 23 17 specifier: ^0.26.0 24 18 version: 0.26.0 25 19 oxlint: 26 20 specifier: ^1.41.0 27 21 version: 1.41.0 28 - prettier: 29 - specifier: ^3.8.0 30 - version: 3.8.0 31 - prettier-plugin-tailwindcss: 32 - specifier: ^0.7.2 33 - version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.0(@prettier/plugin-oxc@0.1.3)(@vue/compiler-sfc@3.5.26)(prettier@3.8.0))(@prettier/plugin-oxc@0.1.3)(prettier@3.8.0) 34 22 typescript: 35 23 specifier: ^5.9.3 36 24 version: 5.9.3 ··· 70 58 '@atcute/identity-resolver-node': 71 59 specifier: ^1.0.3 72 60 version: 1.0.3(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3) 61 + '@atcute/lexicon-doc': 62 + specifier: ^2.0.6 63 + version: 2.0.6 64 + '@atcute/lexicon-resolver': 65 + specifier: ^0.1.6 66 + version: 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3) 67 + '@atcute/lexicon-resolver-node': 68 + specifier: ^0.1.0 69 + version: 0.1.0(@atcute/identity@1.1.3)(@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)) 73 70 '@atcute/lexicons': 74 71 specifier: ^1.2.6 75 72 version: 1.2.6 ··· 274 271 '@atcute/lexicon-doc@2.0.6': 275 272 resolution: {integrity: sha512-iDYJkuom+tIw3zIvU1ggCEVFfReXKfOUtIhpY2kEg2kQeSfMB75F+8k1QOpeAQBetyWYmjsHqBuSUX9oQS6L1Q==} 276 273 274 + '@atcute/lexicon-resolver-node@0.1.0': 275 + resolution: {integrity: sha512-rp6az2R3aBb4h2sx2L+SiI5OZ3KBUaQKoviwoIK9fN9nPyqqCOiLj+gEjeT1Ch03WWICWdgqmArmYZu4FGZhzQ==} 276 + peerDependencies: 277 + '@atcute/identity': ^1.0.0 278 + '@atcute/lexicon-resolver': ^0.1.0 279 + 277 280 '@atcute/lexicon-resolver@0.1.6': 278 281 resolution: {integrity: sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==} 279 282 peerDependencies: ··· 467 470 resolution: {integrity: sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w==} 468 471 engines: {node: '>=16'} 469 472 470 - '@babel/code-frame@7.28.6': 471 - resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} 472 - engines: {node: '>=6.9.0'} 473 - 474 - '@babel/generator@7.28.6': 475 - resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} 476 - engines: {node: '>=6.9.0'} 477 - 478 - '@babel/helper-globals@7.28.0': 479 - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} 480 - engines: {node: '>=6.9.0'} 481 - 482 - '@babel/helper-string-parser@7.27.1': 483 - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 484 - engines: {node: '>=6.9.0'} 485 - 486 - '@babel/helper-validator-identifier@7.28.5': 487 - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} 488 - engines: {node: '>=6.9.0'} 489 - 490 - '@babel/parser@7.28.6': 491 - resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} 492 - engines: {node: '>=6.0.0'} 493 - hasBin: true 494 - 495 - '@babel/template@7.28.6': 496 - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} 497 - engines: {node: '>=6.9.0'} 498 - 499 - '@babel/traverse@7.28.6': 500 - resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} 501 - engines: {node: '>=6.9.0'} 502 - 503 - '@babel/types@7.28.6': 504 - resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} 505 - engines: {node: '>=6.9.0'} 506 - 507 473 '@badrap/valita@0.4.6': 508 474 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 509 475 engines: {node: '>= 18'} ··· 736 702 '@hexagon/base64@1.1.28': 737 703 resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} 738 704 739 - '@ianvs/prettier-plugin-sort-imports@4.7.0': 740 - resolution: {integrity: sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==} 741 - peerDependencies: 742 - '@prettier/plugin-oxc': ^0.0.4 743 - '@vue/compiler-sfc': 2.7.x || 3.x 744 - content-tag: ^4.0.0 745 - prettier: 2 || 3 || ^4.0.0-0 746 - prettier-plugin-ember-template-tag: ^2.1.0 747 - peerDependenciesMeta: 748 - '@prettier/plugin-oxc': 749 - optional: true 750 - '@vue/compiler-sfc': 751 - optional: true 752 - content-tag: 753 - optional: true 754 - prettier-plugin-ember-template-tag: 755 - optional: true 756 - 757 705 '@img/sharp-darwin-arm64@0.33.5': 758 706 resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 759 707 engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} ··· 933 881 resolution: {integrity: sha512-tsXBEygGSzNpFK2gjsRlXBn7FiScUeLFWIZNpoAZ8iG85Km0/3K9xgqlQAXoQ+uEZBe4XplnzyCDvmEgbyNT8w==} 934 882 engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} 935 883 936 - '@oxc-parser/binding-android-arm64@0.99.0': 937 - resolution: {integrity: sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==} 938 - engines: {node: ^20.19.0 || >=22.12.0} 939 - cpu: [arm64] 940 - os: [android] 941 - 942 - '@oxc-parser/binding-darwin-arm64@0.99.0': 943 - resolution: {integrity: sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==} 944 - engines: {node: ^20.19.0 || >=22.12.0} 945 - cpu: [arm64] 946 - os: [darwin] 947 - 948 - '@oxc-parser/binding-darwin-x64@0.99.0': 949 - resolution: {integrity: sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==} 950 - engines: {node: ^20.19.0 || >=22.12.0} 951 - cpu: [x64] 952 - os: [darwin] 953 - 954 - '@oxc-parser/binding-freebsd-x64@0.99.0': 955 - resolution: {integrity: sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==} 956 - engines: {node: ^20.19.0 || >=22.12.0} 957 - cpu: [x64] 958 - os: [freebsd] 959 - 960 - '@oxc-parser/binding-linux-arm-gnueabihf@0.99.0': 961 - resolution: {integrity: sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==} 962 - engines: {node: ^20.19.0 || >=22.12.0} 963 - cpu: [arm] 964 - os: [linux] 965 - 966 - '@oxc-parser/binding-linux-arm-musleabihf@0.99.0': 967 - resolution: {integrity: sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==} 968 - engines: {node: ^20.19.0 || >=22.12.0} 969 - cpu: [arm] 970 - os: [linux] 971 - 972 - '@oxc-parser/binding-linux-arm64-gnu@0.99.0': 973 - resolution: {integrity: sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==} 974 - engines: {node: ^20.19.0 || >=22.12.0} 975 - cpu: [arm64] 976 - os: [linux] 977 - 978 - '@oxc-parser/binding-linux-arm64-musl@0.99.0': 979 - resolution: {integrity: sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==} 980 - engines: {node: ^20.19.0 || >=22.12.0} 981 - cpu: [arm64] 982 - os: [linux] 983 - 984 - '@oxc-parser/binding-linux-riscv64-gnu@0.99.0': 985 - resolution: {integrity: sha512-zxn/xkf519f12FKkpL5XwJipsylfSSnm36h6c1zBDTz4fbIDMGyIhHfWfwM7uUmHo9Aqw1pLxFpY39Etv398+Q==} 986 - engines: {node: ^20.19.0 || >=22.12.0} 987 - cpu: [riscv64] 988 - os: [linux] 989 - 990 - '@oxc-parser/binding-linux-s390x-gnu@0.99.0': 991 - resolution: {integrity: sha512-Y1eSDKDS5E4IVC7Oxw+NbYAKRmJPMJTIjW+9xOWwteDHkFqpocKe0USxog+Q1uhzalD9M0p9eXWEWdGQCMDBMQ==} 992 - engines: {node: ^20.19.0 || >=22.12.0} 993 - cpu: [s390x] 994 - os: [linux] 995 - 996 - '@oxc-parser/binding-linux-x64-gnu@0.99.0': 997 - resolution: {integrity: sha512-YVJMfk5cFWB8i2/nIrbk6n15bFkMHqWnMIWkVx7r2KwpTxHyFMfu2IpeVKo1ITDSmt5nBrGdLHD36QRlu2nDLg==} 998 - engines: {node: ^20.19.0 || >=22.12.0} 999 - cpu: [x64] 1000 - os: [linux] 1001 - 1002 - '@oxc-parser/binding-linux-x64-musl@0.99.0': 1003 - resolution: {integrity: sha512-2+SDPrie5f90A1b9EirtVggOgsqtsYU5raZwkDYKyS1uvJzjqHCDhG/f4TwQxHmIc5YkczdQfwvN91lwmjsKYQ==} 1004 - engines: {node: ^20.19.0 || >=22.12.0} 1005 - cpu: [x64] 1006 - os: [linux] 1007 - 1008 - '@oxc-parser/binding-wasm32-wasi@0.99.0': 1009 - resolution: {integrity: sha512-DKA4j0QerUWSMADziLM5sAyM7V53Fj95CV9SjP77bPfEfT7MnvFKnneaRMqPK1cpzjAGiQF52OBUIKyk0dwOQA==} 1010 - engines: {node: '>=14.0.0'} 1011 - cpu: [wasm32] 1012 - 1013 - '@oxc-parser/binding-win32-arm64-msvc@0.99.0': 1014 - resolution: {integrity: sha512-EaB3AvsxqdNUhh9FOoAxRZ2L4PCRwDlDb//QXItwyOJrX7XS+uGK9B1KEUV4FZ/7rDhHsWieLt5e07wl2Ti5AQ==} 1015 - engines: {node: ^20.19.0 || >=22.12.0} 1016 - cpu: [arm64] 1017 - os: [win32] 1018 - 1019 - '@oxc-parser/binding-win32-x64-msvc@0.99.0': 1020 - resolution: {integrity: sha512-sJN1Q8h7ggFOyDn0zsHaXbP/MklAVUvhrbq0LA46Qum686P3SZQHjbATqJn9yaVEvaSKXCshgl0vQ1gWkGgpcQ==} 1021 - engines: {node: ^20.19.0 || >=22.12.0} 1022 - cpu: [x64] 1023 - os: [win32] 1024 - 1025 884 '@oxc-project/types@0.108.0': 1026 885 resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==} 1027 - 1028 - '@oxc-project/types@0.99.0': 1029 - resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==} 1030 886 1031 887 '@oxc-resolver/binding-android-arm-eabi@11.16.3': 1032 888 resolution: {integrity: sha512-CVyWHu6ACDqDcJxR4nmGiG8vDF4TISJHqRNzac5z/gPQycs/QrP/1pDsJBy0MD7jSw8nVq2E5WqeHQKabBG/Jg==} ··· 1330 1186 '@poppinss/ts-exec@1.4.1': 1331 1187 resolution: {integrity: sha512-KA1gjEeKoYVZSK+pmasrIfq6xpRCRujBfOmVRfCD7jv+vci/kb+5ymvVuR8XsvbP9Ar8NQexeaT3IDuELHY1Rw==} 1332 1188 engines: {node: '>=24.0.0'} 1333 - 1334 - '@prettier/plugin-oxc@0.1.3': 1335 - resolution: {integrity: sha512-aABz3zIRilpWMekbt1FL1JVBQrQLR8L4Td2SRctECrWSsXGTNn/G1BqNSKCdbvQS1LWstAXfqcXzDki7GAAJyg==} 1336 - engines: {node: '>=14'} 1337 1189 1338 1190 '@protobufjs/aspromise@1.1.2': 1339 1191 resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} ··· 1759 1611 resolution: {integrity: sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==} 1760 1612 engines: {node: '>=20.0.0'} 1761 1613 1762 - '@vue/compiler-core@3.5.26': 1763 - resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} 1764 - 1765 - '@vue/compiler-dom@3.5.26': 1766 - resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} 1767 - 1768 - '@vue/compiler-sfc@3.5.26': 1769 - resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} 1770 - 1771 - '@vue/compiler-ssr@3.5.26': 1772 - resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} 1773 - 1774 - '@vue/shared@3.5.26': 1775 - resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} 1776 - 1777 1614 abort-controller@3.0.0: 1778 1615 resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 1779 1616 engines: {node: '>=6.5'} ··· 2150 1987 resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} 2151 1988 engines: {node: '>=10.13.0'} 2152 1989 2153 - entities@7.0.0: 2154 - resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} 2155 - engines: {node: '>=0.12'} 2156 - 2157 1990 es-define-property@1.0.1: 2158 1991 resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 2159 1992 engines: {node: '>= 0.4'} ··· 2189 2022 2190 2023 esm-env@1.2.2: 2191 2024 resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 2192 - 2193 - estree-walker@2.0.2: 2194 - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 2195 2025 2196 2026 etag@1.8.1: 2197 2027 resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} ··· 2417 2247 2418 2248 js-md4@0.3.2: 2419 2249 resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} 2420 - 2421 - js-tokens@4.0.0: 2422 - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 2423 2250 2424 2251 jsbi@4.3.2: 2425 2252 resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} 2426 2253 2427 - jsesc@3.1.0: 2428 - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 2429 - engines: {node: '>=6'} 2430 - hasBin: true 2431 - 2432 2254 jsonwebtoken@9.0.3: 2433 2255 resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} 2434 2256 engines: {node: '>=12', npm: '>=6'} ··· 2623 2445 murmurhash@2.0.1: 2624 2446 resolution: {integrity: sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==} 2625 2447 2626 - nanoid@3.3.11: 2627 - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 2628 - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 2629 - hasBin: true 2630 - 2631 2448 nanoid@5.1.6: 2632 2449 resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 2633 2450 engines: {node: ^18 || >=20} ··· 2686 2503 open@10.2.0: 2687 2504 resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} 2688 2505 engines: {node: '>=18'} 2689 - 2690 - oxc-parser@0.99.0: 2691 - resolution: {integrity: sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ==} 2692 - engines: {node: ^20.19.0 || >=22.12.0} 2693 2506 2694 2507 oxc-resolver@11.16.3: 2695 2508 resolution: {integrity: sha512-goLOJH3x69VouGWGp5CgCIHyksmOZzXr36lsRmQz1APg3SPFORrvV2q7nsUHMzLVa6ZJgNwkgUSJFsbCpAWkCA==} ··· 2818 2631 resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} 2819 2632 engines: {node: '>=10.13.0'} 2820 2633 2821 - postcss@8.5.6: 2822 - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 2823 - engines: {node: ^10 || ^12 || >=14} 2824 - 2825 2634 postgres-array@2.0.0: 2826 2635 resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} 2827 2636 engines: {node: '>=4'} ··· 2838 2647 resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} 2839 2648 engines: {node: '>=0.10.0'} 2840 2649 2841 - prettier-plugin-tailwindcss@0.7.2: 2842 - resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} 2843 - engines: {node: '>=20.19'} 2844 - peerDependencies: 2845 - '@ianvs/prettier-plugin-sort-imports': '*' 2846 - '@prettier/plugin-hermes': '*' 2847 - '@prettier/plugin-oxc': '*' 2848 - '@prettier/plugin-pug': '*' 2849 - '@shopify/prettier-plugin-liquid': '*' 2850 - '@trivago/prettier-plugin-sort-imports': '*' 2851 - '@zackad/prettier-plugin-twig': '*' 2852 - prettier: ^3.0 2853 - prettier-plugin-astro: '*' 2854 - prettier-plugin-css-order: '*' 2855 - prettier-plugin-jsdoc: '*' 2856 - prettier-plugin-marko: '*' 2857 - prettier-plugin-multiline-arrays: '*' 2858 - prettier-plugin-organize-attributes: '*' 2859 - prettier-plugin-organize-imports: '*' 2860 - prettier-plugin-sort-imports: '*' 2861 - prettier-plugin-svelte: '*' 2862 - peerDependenciesMeta: 2863 - '@ianvs/prettier-plugin-sort-imports': 2864 - optional: true 2865 - '@prettier/plugin-hermes': 2866 - optional: true 2867 - '@prettier/plugin-oxc': 2868 - optional: true 2869 - '@prettier/plugin-pug': 2870 - optional: true 2871 - '@shopify/prettier-plugin-liquid': 2872 - optional: true 2873 - '@trivago/prettier-plugin-sort-imports': 2874 - optional: true 2875 - '@zackad/prettier-plugin-twig': 2876 - optional: true 2877 - prettier-plugin-astro: 2878 - optional: true 2879 - prettier-plugin-css-order: 2880 - optional: true 2881 - prettier-plugin-jsdoc: 2882 - optional: true 2883 - prettier-plugin-marko: 2884 - optional: true 2885 - prettier-plugin-multiline-arrays: 2886 - optional: true 2887 - prettier-plugin-organize-attributes: 2888 - optional: true 2889 - prettier-plugin-organize-imports: 2890 - optional: true 2891 - prettier-plugin-sort-imports: 2892 - optional: true 2893 - prettier-plugin-svelte: 2894 - optional: true 2895 - 2896 2650 prettier@3.8.0: 2897 2651 resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} 2898 2652 engines: {node: '>=14'} ··· 3394 3148 '@atcute/util-text': 0.0.1 3395 3149 '@badrap/valita': 0.4.6 3396 3150 3151 + '@atcute/lexicon-resolver-node@0.1.0(@atcute/identity@1.1.3)(@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3))': 3152 + dependencies: 3153 + '@atcute/identity': 1.1.3 3154 + '@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3) 3155 + '@atcute/lexicons': 1.2.6 3156 + 3397 3157 '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)': 3398 3158 dependencies: 3399 3159 '@atcute/crypto': 2.3.0 ··· 3842 3602 jsonwebtoken: 9.0.3 3843 3603 uuid: 8.3.2 3844 3604 3845 - '@babel/code-frame@7.28.6': 3846 - dependencies: 3847 - '@babel/helper-validator-identifier': 7.28.5 3848 - js-tokens: 4.0.0 3849 - picocolors: 1.1.1 3850 - 3851 - '@babel/generator@7.28.6': 3852 - dependencies: 3853 - '@babel/parser': 7.28.6 3854 - '@babel/types': 7.28.6 3855 - '@jridgewell/gen-mapping': 0.3.13 3856 - '@jridgewell/trace-mapping': 0.3.31 3857 - jsesc: 3.1.0 3858 - 3859 - '@babel/helper-globals@7.28.0': {} 3860 - 3861 - '@babel/helper-string-parser@7.27.1': {} 3862 - 3863 - '@babel/helper-validator-identifier@7.28.5': {} 3864 - 3865 - '@babel/parser@7.28.6': 3866 - dependencies: 3867 - '@babel/types': 7.28.6 3868 - 3869 - '@babel/template@7.28.6': 3870 - dependencies: 3871 - '@babel/code-frame': 7.28.6 3872 - '@babel/parser': 7.28.6 3873 - '@babel/types': 7.28.6 3874 - 3875 - '@babel/traverse@7.28.6': 3876 - dependencies: 3877 - '@babel/code-frame': 7.28.6 3878 - '@babel/generator': 7.28.6 3879 - '@babel/helper-globals': 7.28.0 3880 - '@babel/parser': 7.28.6 3881 - '@babel/template': 7.28.6 3882 - '@babel/types': 7.28.6 3883 - debug: 4.4.3 3884 - transitivePeerDependencies: 3885 - - supports-color 3886 - 3887 - '@babel/types@7.28.6': 3888 - dependencies: 3889 - '@babel/helper-string-parser': 7.27.1 3890 - '@babel/helper-validator-identifier': 7.28.5 3891 - 3892 3605 '@badrap/valita@0.4.6': {} 3893 3606 3894 3607 '@bufbuild/protobuf@1.10.1': {} ··· 4078 3791 4079 3792 '@hexagon/base64@1.1.28': {} 4080 3793 4081 - '@ianvs/prettier-plugin-sort-imports@4.7.0(@prettier/plugin-oxc@0.1.3)(@vue/compiler-sfc@3.5.26)(prettier@3.8.0)': 4082 - dependencies: 4083 - '@babel/generator': 7.28.6 4084 - '@babel/parser': 7.28.6 4085 - '@babel/traverse': 7.28.6 4086 - '@babel/types': 7.28.6 4087 - prettier: 3.8.0 4088 - semver: 7.7.3 4089 - optionalDependencies: 4090 - '@prettier/plugin-oxc': 0.1.3 4091 - '@vue/compiler-sfc': 3.5.26 4092 - transitivePeerDependencies: 4093 - - supports-color 4094 - 4095 3794 '@img/sharp-darwin-arm64@0.33.5': 4096 3795 optionalDependencies: 4097 3796 '@img/sharp-libvips-darwin-arm64': 1.0.4 ··· 4242 3941 dependencies: 4243 3942 '@optique/core': 0.6.11 4244 3943 4245 - '@oxc-parser/binding-android-arm64@0.99.0': 4246 - optional: true 4247 - 4248 - '@oxc-parser/binding-darwin-arm64@0.99.0': 4249 - optional: true 4250 - 4251 - '@oxc-parser/binding-darwin-x64@0.99.0': 4252 - optional: true 4253 - 4254 - '@oxc-parser/binding-freebsd-x64@0.99.0': 4255 - optional: true 4256 - 4257 - '@oxc-parser/binding-linux-arm-gnueabihf@0.99.0': 4258 - optional: true 4259 - 4260 - '@oxc-parser/binding-linux-arm-musleabihf@0.99.0': 4261 - optional: true 4262 - 4263 - '@oxc-parser/binding-linux-arm64-gnu@0.99.0': 4264 - optional: true 4265 - 4266 - '@oxc-parser/binding-linux-arm64-musl@0.99.0': 4267 - optional: true 4268 - 4269 - '@oxc-parser/binding-linux-riscv64-gnu@0.99.0': 4270 - optional: true 4271 - 4272 - '@oxc-parser/binding-linux-s390x-gnu@0.99.0': 4273 - optional: true 4274 - 4275 - '@oxc-parser/binding-linux-x64-gnu@0.99.0': 4276 - optional: true 4277 - 4278 - '@oxc-parser/binding-linux-x64-musl@0.99.0': 4279 - optional: true 4280 - 4281 - '@oxc-parser/binding-wasm32-wasi@0.99.0': 4282 - dependencies: 4283 - '@napi-rs/wasm-runtime': 1.1.1 4284 - optional: true 4285 - 4286 - '@oxc-parser/binding-win32-arm64-msvc@0.99.0': 4287 - optional: true 4288 - 4289 - '@oxc-parser/binding-win32-x64-msvc@0.99.0': 4290 - optional: true 4291 - 4292 3944 '@oxc-project/types@0.108.0': {} 4293 - 4294 - '@oxc-project/types@0.99.0': {} 4295 3945 4296 3946 '@oxc-resolver/binding-android-arm-eabi@11.16.3': 4297 3947 optional: true ··· 4565 4215 get-tsconfig: 4.13.0 4566 4216 transitivePeerDependencies: 4567 4217 - '@swc/helpers' 4568 - 4569 - '@prettier/plugin-oxc@0.1.3': 4570 - dependencies: 4571 - oxc-parser: 0.99.0 4572 4218 4573 4219 '@protobufjs/aspromise@1.1.2': {} 4574 4220 ··· 4905 4551 transitivePeerDependencies: 4906 4552 - supports-color 4907 4553 4908 - '@vue/compiler-core@3.5.26': 4909 - dependencies: 4910 - '@babel/parser': 7.28.6 4911 - '@vue/shared': 3.5.26 4912 - entities: 7.0.0 4913 - estree-walker: 2.0.2 4914 - source-map-js: 1.2.1 4915 - optional: true 4916 - 4917 - '@vue/compiler-dom@3.5.26': 4918 - dependencies: 4919 - '@vue/compiler-core': 3.5.26 4920 - '@vue/shared': 3.5.26 4921 - optional: true 4922 - 4923 - '@vue/compiler-sfc@3.5.26': 4924 - dependencies: 4925 - '@babel/parser': 7.28.6 4926 - '@vue/compiler-core': 3.5.26 4927 - '@vue/compiler-dom': 3.5.26 4928 - '@vue/compiler-ssr': 3.5.26 4929 - '@vue/shared': 3.5.26 4930 - estree-walker: 2.0.2 4931 - magic-string: 0.30.21 4932 - postcss: 8.5.6 4933 - source-map-js: 1.2.1 4934 - optional: true 4935 - 4936 - '@vue/compiler-ssr@3.5.26': 4937 - dependencies: 4938 - '@vue/compiler-dom': 3.5.26 4939 - '@vue/shared': 3.5.26 4940 - optional: true 4941 - 4942 - '@vue/shared@3.5.26': 4943 - optional: true 4944 - 4945 4554 abort-controller@3.0.0: 4946 4555 dependencies: 4947 4556 event-target-shim: 5.0.1 ··· 5229 4838 graceful-fs: 4.2.11 5230 4839 tapable: 2.3.0 5231 4840 5232 - entities@7.0.0: 5233 - optional: true 5234 - 5235 4841 es-define-property@1.0.1: {} 5236 4842 5237 4843 es-errors@1.3.0: {} ··· 5288 4894 escape-html@1.0.3: {} 5289 4895 5290 4896 esm-env@1.2.2: {} 5291 - 5292 - estree-walker@2.0.2: 5293 - optional: true 5294 4897 5295 4898 etag@1.8.1: {} 5296 4899 ··· 5541 5144 5542 5145 js-md4@0.3.2: {} 5543 5146 5544 - js-tokens@4.0.0: {} 5545 - 5546 5147 jsbi@4.3.2: {} 5547 - 5548 - jsesc@3.1.0: {} 5549 5148 5550 5149 jsonwebtoken@9.0.3: 5551 5150 dependencies: ··· 5709 5308 5710 5309 murmurhash@2.0.1: {} 5711 5310 5712 - nanoid@3.3.11: 5713 - optional: true 5714 - 5715 5311 nanoid@5.1.6: {} 5716 5312 5717 5313 native-duplexpair@1.0.0: {} ··· 5749 5345 is-inside-container: 1.0.0 5750 5346 wsl-utils: 0.1.0 5751 5347 5752 - oxc-parser@0.99.0: 5753 - dependencies: 5754 - '@oxc-project/types': 0.99.0 5755 - optionalDependencies: 5756 - '@oxc-parser/binding-android-arm64': 0.99.0 5757 - '@oxc-parser/binding-darwin-arm64': 0.99.0 5758 - '@oxc-parser/binding-darwin-x64': 0.99.0 5759 - '@oxc-parser/binding-freebsd-x64': 0.99.0 5760 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.99.0 5761 - '@oxc-parser/binding-linux-arm-musleabihf': 0.99.0 5762 - '@oxc-parser/binding-linux-arm64-gnu': 0.99.0 5763 - '@oxc-parser/binding-linux-arm64-musl': 0.99.0 5764 - '@oxc-parser/binding-linux-riscv64-gnu': 0.99.0 5765 - '@oxc-parser/binding-linux-s390x-gnu': 0.99.0 5766 - '@oxc-parser/binding-linux-x64-gnu': 0.99.0 5767 - '@oxc-parser/binding-linux-x64-musl': 0.99.0 5768 - '@oxc-parser/binding-wasm32-wasi': 0.99.0 5769 - '@oxc-parser/binding-win32-arm64-msvc': 0.99.0 5770 - '@oxc-parser/binding-win32-x64-msvc': 0.99.0 5771 - 5772 5348 oxc-resolver@11.16.3: 5773 5349 optionalDependencies: 5774 5350 '@oxc-resolver/binding-android-arm-eabi': 11.16.3 ··· 5925 5501 5926 5502 pngjs@5.0.0: {} 5927 5503 5928 - postcss@8.5.6: 5929 - dependencies: 5930 - nanoid: 3.3.11 5931 - picocolors: 1.1.1 5932 - source-map-js: 1.2.1 5933 - optional: true 5934 - 5935 5504 postgres-array@2.0.0: {} 5936 5505 5937 5506 postgres-bytea@1.0.1: {} ··· 5941 5510 postgres-interval@1.2.0: 5942 5511 dependencies: 5943 5512 xtend: 4.0.2 5944 - 5945 - prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.0(@prettier/plugin-oxc@0.1.3)(@vue/compiler-sfc@3.5.26)(prettier@3.8.0))(@prettier/plugin-oxc@0.1.3)(prettier@3.8.0): 5946 - dependencies: 5947 - prettier: 3.8.0 5948 - optionalDependencies: 5949 - '@ianvs/prettier-plugin-sort-imports': 4.7.0(@prettier/plugin-oxc@0.1.3)(@vue/compiler-sfc@3.5.26)(prettier@3.8.0) 5950 - '@prettier/plugin-oxc': 0.1.3 5951 5513 5952 5514 prettier@3.8.0: {} 5953 5515