tangled
alpha
login
or
join now
mary.my.id
/
danaus
4
fork
atom
work-in-progress atproto PDS
typescript
atproto
pds
atcute
4
fork
atom
overview
issues
pulls
pipelines
feat: record validation
mary.my.id
1 month ago
fcfe01fe
2970cceb
verified
This commit was signed with the committer's
known signature
.
mary.my.id
SSH Key Fingerprint:
SHA256:ZlTP/auFSGpGnaoDg4mCTG1g9OZvXp62jWR4c6H4O3c=
+1509
-512
17 changed files
expand all
collapse all
unified
split
packages
danaus
drizzle
lexicon
20260121015822_dashing_orphan
migration.sql
snapshot.json
package.json
src
api
com.atproto
repo.applyWrites.ts
repo.createRecord.ts
repo.putRecord.ts
config.ts
context.ts
environment.ts
lexicon
cache.ts
db
index.ts
schema.ts
validate-writes.ts
logger.ts
test
test-pds.ts
tests
lexicon-validation.test.ts
pnpm-lock.yaml
+16
packages/danaus/drizzle/lexicon/20260121015822_dashing_orphan/migration.sql
···
1
1
+
CREATE TABLE `authority` (
2
2
+
`domain` text PRIMARY KEY,
3
3
+
`did` text,
4
4
+
`updated_at` integer NOT NULL
5
5
+
);
6
6
+
--> statement-breakpoint
7
7
+
CREATE TABLE `schema` (
8
8
+
`nsid` text PRIMARY KEY,
9
9
+
`authority_did` text NOT NULL,
10
10
+
`cid` text,
11
11
+
`doc` text,
12
12
+
`updated_at` integer NOT NULL
13
13
+
);
14
14
+
--> statement-breakpoint
15
15
+
CREATE INDEX `authority_updated_at_idx` ON `authority` (`updated_at`);--> statement-breakpoint
16
16
+
CREATE INDEX `schema_updated_at_idx` ON `schema` (`updated_at`);
+145
packages/danaus/drizzle/lexicon/20260121015822_dashing_orphan/snapshot.json
···
1
1
+
{
2
2
+
"version": "7",
3
3
+
"dialect": "sqlite",
4
4
+
"id": "7018e308-5b71-44e2-b9b8-862d395b9d97",
5
5
+
"prevIds": [
6
6
+
"00000000-0000-0000-0000-000000000000"
7
7
+
],
8
8
+
"ddl": [
9
9
+
{
10
10
+
"name": "authority",
11
11
+
"entityType": "tables"
12
12
+
},
13
13
+
{
14
14
+
"name": "schema",
15
15
+
"entityType": "tables"
16
16
+
},
17
17
+
{
18
18
+
"type": "text",
19
19
+
"notNull": false,
20
20
+
"autoincrement": false,
21
21
+
"default": null,
22
22
+
"generated": null,
23
23
+
"name": "domain",
24
24
+
"entityType": "columns",
25
25
+
"table": "authority"
26
26
+
},
27
27
+
{
28
28
+
"type": "text",
29
29
+
"notNull": false,
30
30
+
"autoincrement": false,
31
31
+
"default": null,
32
32
+
"generated": null,
33
33
+
"name": "did",
34
34
+
"entityType": "columns",
35
35
+
"table": "authority"
36
36
+
},
37
37
+
{
38
38
+
"type": "integer",
39
39
+
"notNull": true,
40
40
+
"autoincrement": false,
41
41
+
"default": null,
42
42
+
"generated": null,
43
43
+
"name": "updated_at",
44
44
+
"entityType": "columns",
45
45
+
"table": "authority"
46
46
+
},
47
47
+
{
48
48
+
"type": "text",
49
49
+
"notNull": false,
50
50
+
"autoincrement": false,
51
51
+
"default": null,
52
52
+
"generated": null,
53
53
+
"name": "nsid",
54
54
+
"entityType": "columns",
55
55
+
"table": "schema"
56
56
+
},
57
57
+
{
58
58
+
"type": "text",
59
59
+
"notNull": true,
60
60
+
"autoincrement": false,
61
61
+
"default": null,
62
62
+
"generated": null,
63
63
+
"name": "authority_did",
64
64
+
"entityType": "columns",
65
65
+
"table": "schema"
66
66
+
},
67
67
+
{
68
68
+
"type": "text",
69
69
+
"notNull": false,
70
70
+
"autoincrement": false,
71
71
+
"default": null,
72
72
+
"generated": null,
73
73
+
"name": "cid",
74
74
+
"entityType": "columns",
75
75
+
"table": "schema"
76
76
+
},
77
77
+
{
78
78
+
"type": "text",
79
79
+
"notNull": false,
80
80
+
"autoincrement": false,
81
81
+
"default": null,
82
82
+
"generated": null,
83
83
+
"name": "doc",
84
84
+
"entityType": "columns",
85
85
+
"table": "schema"
86
86
+
},
87
87
+
{
88
88
+
"type": "integer",
89
89
+
"notNull": true,
90
90
+
"autoincrement": false,
91
91
+
"default": null,
92
92
+
"generated": null,
93
93
+
"name": "updated_at",
94
94
+
"entityType": "columns",
95
95
+
"table": "schema"
96
96
+
},
97
97
+
{
98
98
+
"columns": [
99
99
+
"domain"
100
100
+
],
101
101
+
"nameExplicit": false,
102
102
+
"name": "authority_pk",
103
103
+
"table": "authority",
104
104
+
"entityType": "pks"
105
105
+
},
106
106
+
{
107
107
+
"columns": [
108
108
+
"nsid"
109
109
+
],
110
110
+
"nameExplicit": false,
111
111
+
"name": "schema_pk",
112
112
+
"table": "schema",
113
113
+
"entityType": "pks"
114
114
+
},
115
115
+
{
116
116
+
"columns": [
117
117
+
{
118
118
+
"value": "updated_at",
119
119
+
"isExpression": false
120
120
+
}
121
121
+
],
122
122
+
"isUnique": false,
123
123
+
"where": null,
124
124
+
"origin": "manual",
125
125
+
"name": "authority_updated_at_idx",
126
126
+
"entityType": "indexes",
127
127
+
"table": "authority"
128
128
+
},
129
129
+
{
130
130
+
"columns": [
131
131
+
{
132
132
+
"value": "updated_at",
133
133
+
"isExpression": false
134
134
+
}
135
135
+
],
136
136
+
"isUnique": false,
137
137
+
"where": null,
138
138
+
"origin": "manual",
139
139
+
"name": "schema_updated_at_idx",
140
140
+
"entityType": "indexes",
141
141
+
"table": "schema"
142
142
+
}
143
143
+
],
144
144
+
"renames": []
145
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
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
40
+
"@atcute/lexicon-doc": "^2.0.6",
41
41
+
"@atcute/lexicon-resolver": "^0.1.6",
42
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
2
-
import type { CanonicalResourceUri, Nsid, RecordKey } from '@atcute/lexicons';
2
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
7
-
8
8
-
type WriteInput = {
9
9
-
$type?: string;
10
10
-
collection: Nsid;
11
11
-
rkey?: RecordKey;
12
12
-
value?: unknown;
13
13
-
};
7
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
26
-
const { accountManager, actorManager, authVerifier } = context;
20
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
48
-
const writes = (input.writes as WriteInput[]).map((write): RepoWriteOp => {
42
42
+
const writes = input.writes.map((write): RepoWriteOp => {
49
43
switch (write.$type) {
50
50
-
case 'com.atproto.repo.applyWrites#create':
44
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
57
-
case 'com.atproto.repo.applyWrites#update':
51
51
+
}
52
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
64
-
case 'com.atproto.repo.applyWrites#delete':
59
59
+
}
60
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
70
-
}
71
71
-
72
72
-
if ('value' in write && write.value !== undefined) {
73
73
-
if (write.rkey === undefined) {
74
74
-
return {
75
75
-
action: 'create',
76
76
-
collection: write.collection,
77
77
-
rkey: write.rkey,
78
78
-
record: write.value,
79
79
-
};
80
66
}
81
81
-
82
82
-
throw new InvalidRequestError({
83
83
-
error: 'InvalidWrite',
84
84
-
description: `ambiguous write action without $type`,
85
85
-
});
86
67
}
68
68
+
});
87
69
88
88
-
return {
89
89
-
action: 'delete',
90
90
-
collection: write.collection,
91
91
-
rkey: write.rkey,
92
92
-
};
93
93
-
});
70
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
4
+
import type { RepoWriteOp } from '#app/actors/repo/types.ts';
4
5
import type { AppContext } from '#app/context.ts';
6
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
12
-
const { accountManager, actorManager, authVerifier } = context;
14
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
36
+
const writes: RepoWriteOp[] = [
37
37
+
{
38
38
+
action: 'create',
39
39
+
collection: input.collection,
40
40
+
rkey: input.rkey,
41
41
+
record: input.record,
42
42
+
},
43
43
+
];
44
44
+
45
45
+
await validateRecordWrites(lexiconCache, writes, input.validate);
46
46
+
34
47
const result = await actorManager.transact(account.did, (store) => {
35
35
-
return store.repo.applyWrites(
36
36
-
[
37
37
-
{
38
38
-
action: 'create',
39
39
-
collection: input.collection,
40
40
-
rkey: input.rkey,
41
41
-
record: input.record,
42
42
-
},
43
43
-
],
44
44
-
{
45
45
-
swapCommit: input.swapCommit ?? undefined,
46
46
-
validateBlobs: input.validate ?? true,
47
47
-
},
48
48
-
);
48
48
+
return store.repo.applyWrites(writes, {
49
49
+
swapCommit: input.swapCommit ?? undefined,
50
50
+
validateBlobs: input.validate ?? true,
51
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
2
+
import type { CanonicalResourceUri } from '@atcute/lexicons';
2
3
import { AuthRequiredError, InvalidRequestError, json, type XRPCRouter } from '@atcute/xrpc-server';
3
4
5
5
+
import type { RepoWriteOp } from '#app/actors/repo/types.ts';
4
6
import type { AppContext } from '#app/context.ts';
7
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
12
-
const { accountManager, actorManager, authVerifier } = context;
15
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
37
+
const uri = `at://${account.did}/${input.collection}/${input.rkey}` as CanonicalResourceUri;
38
38
+
39
39
+
// validate before transaction (validation doesn't depend on create vs update)
40
40
+
const writes: RepoWriteOp[] = [
41
41
+
{
42
42
+
action: 'create', // placeholder - actual action determined in transaction
43
43
+
collection: input.collection,
44
44
+
rkey: input.rkey,
45
45
+
swapRecord: input.swapRecord,
46
46
+
record: input.record,
47
47
+
},
48
48
+
];
49
49
+
50
50
+
await validateRecordWrites(lexiconCache, writes, input.validate);
51
51
+
52
52
+
// check if record exists and write in same transaction (upsert behavior)
34
53
const result = await actorManager.transact(account.did, (store) => {
54
54
+
const exists = store.record.getRecord(uri) !== null;
55
55
+
const action = exists ? 'update' : 'create';
56
56
+
35
57
return store.repo.applyWrites(
36
58
[
37
59
{
38
38
-
action: 'update',
60
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
86
+
export interface LexiconConfig {
87
87
+
enabled: boolean;
88
88
+
cacheDbLocation: string;
89
89
+
nameservers: string[] | null;
90
90
+
cacheStaleTtlMs: number;
91
91
+
cacheMaxTtlMs: number;
92
92
+
}
93
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
135
+
lexicon: LexiconConfig;
127
136
secrets: SecretsConfig;
128
137
subscription: SubscriptionConfig;
129
138
email: EmailConfig | null;
···
285
294
};
286
295
}
287
296
297
297
+
let lexicon: LexiconConfig;
298
298
+
{
299
299
+
lexicon = {
300
300
+
enabled: env.PDS_LEXICON_VALIDATION_ENABLED ?? true,
301
301
+
cacheDbLocation: env.PDS_LEXICON_CACHE_DB_LOCATION ?? locate('lexicon-cache.db'),
302
302
+
nameservers: env.PDS_LEXICON_NAMESERVERS ?? null,
303
303
+
cacheMaxTtlMs: env.PDS_LEXICON_CACHE_MAX_TTL ?? DAY,
304
304
+
cacheStaleTtlMs: env.PDS_LEXICON_CACHE_STALE_TTL ?? HOUR,
305
305
+
};
306
306
+
}
307
307
+
288
308
let secrets: SecretsConfig;
289
309
{
290
310
let jwtKey: KeyObject;
···
350
370
actorStore,
351
371
blobStore,
352
372
identity,
373
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
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
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
39
+
lexiconCache: LexiconCache;
37
40
38
41
handleResolver: HandleResolver;
39
42
didDocumentResolver: DidDocumentResolver<'plc' | 'web'>;
···
89
92
resolver: baseDidDocumentResolver,
90
93
});
91
94
95
95
+
const lexiconAuthorityResolver = new NodeDnsLexiconAuthorityResolver({
96
96
+
nameservers: config.lexicon.nameservers ?? undefined,
97
97
+
});
98
98
+
99
99
+
const lexiconCache = new LexiconCache({
100
100
+
location: config.lexicon.cacheDbLocation,
101
101
+
walAutoCheckpointDisabled: config.database.walAutoCheckpointDisabled,
102
102
+
backgroundQueue: backgroundQueue,
103
103
+
authorityResolver: lexiconAuthorityResolver,
104
104
+
didDocumentResolver: didDocumentResolver,
105
105
+
staleTtl: config.lexicon.cacheStaleTtlMs,
106
106
+
maxTtl: config.lexicon.cacheMaxTtlMs,
107
107
+
enabled: config.lexicon.enabled,
108
108
+
});
109
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
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
73
+
PDS_LEXICON_VALIDATION_ENABLED: v.optional(strbool),
74
74
+
PDS_LEXICON_CACHE_DB_LOCATION: v.optional(str),
75
75
+
PDS_LEXICON_NAMESERVERS: v.optional(strlist),
76
76
+
PDS_LEXICON_CACHE_STALE_TTL: v.optional(strint),
77
77
+
PDS_LEXICON_CACHE_MAX_TTL: v.optional(strint),
78
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
1
+
import { DocumentNotFoundError, type DidDocumentResolver } from '@atcute/identity-resolver';
2
2
+
import { findExternalReferences, type LexiconDoc } from '@atcute/lexicon-doc';
3
3
+
import { RecordValidator } from '@atcute/lexicon-doc/validations';
4
4
+
import {
5
5
+
AuthorityNotFoundError,
6
6
+
LexiconSchemaResolver,
7
7
+
type LexiconAuthorityResolver,
8
8
+
} from '@atcute/lexicon-resolver';
9
9
+
import type { AtprotoDid, Nsid } from '@atcute/lexicons/syntax';
10
10
+
11
11
+
import { eq, lt } from 'drizzle-orm';
12
12
+
import PQueue from 'p-queue';
13
13
+
14
14
+
import type { BackgroundQueue } from '#app/background.ts';
15
15
+
import { lexiconCacheLogger } from '#app/logger.ts';
16
16
+
import { HOUR } from '#app/utils/times.ts';
17
17
+
18
18
+
import { getLexiconCacheDb, t, type LexiconCacheDb } from './db/index.ts';
19
19
+
20
20
+
const DEFAULT_STALE_TTL = HOUR;
21
21
+
const DEFAULT_MAX_TTL = 24 * HOUR;
22
22
+
const DEFAULT_PRUNE_INTERVAL = HOUR;
23
23
+
24
24
+
/** maximum depth when crawling lexicon references */
25
25
+
const DEFAULT_MAX_CRAWL_DEPTH = 10;
26
26
+
/** maximum number of schemas to load when resolving dependencies */
27
27
+
const DEFAULT_MAX_CRAWL_SCHEMAS = 50;
28
28
+
29
29
+
export interface LexiconCacheOptions {
30
30
+
location: string;
31
31
+
walAutoCheckpointDisabled: boolean;
32
32
+
backgroundQueue: BackgroundQueue;
33
33
+
authorityResolver: LexiconAuthorityResolver;
34
34
+
didDocumentResolver: DidDocumentResolver;
35
35
+
/** time before an entry is considered stale (default: 1 hour) */
36
36
+
staleTtl?: number;
37
37
+
/** time before an entry expires completely (default: 24 hours) */
38
38
+
maxTtl?: number;
39
39
+
/** interval between pruning runs (default: 1 hour) */
40
40
+
pruneInterval?: number;
41
41
+
/** whether lexicon resolution is enabled (default: true) */
42
42
+
enabled?: boolean;
43
43
+
}
44
44
+
45
45
+
interface CachedAuthority {
46
46
+
/** authority DID, or null if authority confirmed not to exist */
47
47
+
did: AtprotoDid | null;
48
48
+
updatedAt: number;
49
49
+
stale: boolean;
50
50
+
expired: boolean;
51
51
+
}
52
52
+
53
53
+
interface CachedSchema {
54
54
+
/** schema document, or null for negative cache entry */
55
55
+
doc: LexiconDoc | null;
56
56
+
authorityDid: AtprotoDid;
57
57
+
/** schema CID, or null for negative cache entry */
58
58
+
cid: string | null;
59
59
+
updatedAt: number;
60
60
+
stale: boolean;
61
61
+
expired: boolean;
62
62
+
}
63
63
+
64
64
+
/**
65
65
+
* SQLite-backed lexicon cache for authority resolutions and schema documents.
66
66
+
* supports stale-while-revalidate pattern with background refresh.
67
67
+
*/
68
68
+
export class LexiconCache implements Disposable {
69
69
+
readonly #db: LexiconCacheDb;
70
70
+
readonly #backgroundQueue: BackgroundQueue;
71
71
+
readonly #authorityResolver: LexiconAuthorityResolver;
72
72
+
readonly #schemaResolver: LexiconSchemaResolver;
73
73
+
readonly #staleTtl: number;
74
74
+
readonly #maxTtl: number;
75
75
+
readonly #pruneInterval: Timer;
76
76
+
readonly #enabled: boolean;
77
77
+
78
78
+
/** p-queue for limiting concurrent network fetches */
79
79
+
readonly #fetchQueue = new PQueue({ concurrency: 4 });
80
80
+
81
81
+
/** in-flight authority resolution promises for request coalescing */
82
82
+
readonly #inflightAuthority = new Map<string, Promise<AtprotoDid | null>>();
83
83
+
84
84
+
/** in-flight schema resolution promises for request coalescing */
85
85
+
readonly #inflightSchema = new Map<Nsid, Promise<LexiconDoc | null>>();
86
86
+
87
87
+
constructor(options: LexiconCacheOptions) {
88
88
+
this.#db = getLexiconCacheDb(options.location, options.walAutoCheckpointDisabled);
89
89
+
this.#backgroundQueue = options.backgroundQueue;
90
90
+
this.#authorityResolver = options.authorityResolver;
91
91
+
this.#schemaResolver = new LexiconSchemaResolver({
92
92
+
didDocumentResolver: options.didDocumentResolver,
93
93
+
});
94
94
+
this.#staleTtl = options.staleTtl ?? DEFAULT_STALE_TTL;
95
95
+
this.#maxTtl = options.maxTtl ?? DEFAULT_MAX_TTL;
96
96
+
this.#enabled = options.enabled ?? true;
97
97
+
98
98
+
const pruneIntervalMs = options.pruneInterval ?? DEFAULT_PRUNE_INTERVAL;
99
99
+
this.#pruneInterval = setInterval(() => {
100
100
+
this.#backgroundQueue.add(() => this.pruneExpired());
101
101
+
}, pruneIntervalMs);
102
102
+
}
103
103
+
104
104
+
/** whether lexicon resolution is enabled */
105
105
+
get enabled(): boolean {
106
106
+
return this.#enabled;
107
107
+
}
108
108
+
109
109
+
// #region authority resolution
110
110
+
111
111
+
/**
112
112
+
* get NSID domain from an NSID (e.g., "app.bsky.feed.post" -> "app.bsky").
113
113
+
*/
114
114
+
#getNsidDomain(nsid: Nsid): string {
115
115
+
// NSID format: domain segments in reverse order, then name segment(s)
116
116
+
// e.g., "app.bsky.feed.post" -> authority is "app.bsky"
117
117
+
// the authority is determined by the first two segments
118
118
+
const parts = nsid.split('.');
119
119
+
if (parts.length < 3) {
120
120
+
return nsid;
121
121
+
}
122
122
+
return parts.slice(0, 2).join('.');
123
123
+
}
124
124
+
125
125
+
/**
126
126
+
* get cached authority resolution for an NSID domain.
127
127
+
*/
128
128
+
#getCachedAuthority(domain: string): CachedAuthority | null {
129
129
+
const row = this.#db.select().from(t.authority).where(eq(t.authority.domain, domain)).get();
130
130
+
131
131
+
if (!row) {
132
132
+
return null;
133
133
+
}
134
134
+
135
135
+
const now = Date.now();
136
136
+
const updatedAt = row.updated_at.getTime();
137
137
+
return {
138
138
+
did: row.did,
139
139
+
updatedAt: updatedAt,
140
140
+
stale: now > updatedAt + this.#staleTtl,
141
141
+
expired: now > updatedAt + this.#maxTtl,
142
142
+
};
143
143
+
}
144
144
+
145
145
+
/**
146
146
+
* cache an authority resolution.
147
147
+
* @param domain NSID domain (e.g., "app.bsky")
148
148
+
* @param did authority DID, or null to cache a negative result
149
149
+
* @internal exposed as `_setAuthority` for test injection
150
150
+
*/
151
151
+
_setAuthority(domain: string, did: AtprotoDid | null): void {
152
152
+
const now = new Date();
153
153
+
this.#db
154
154
+
.insert(t.authority)
155
155
+
.values({ domain, did, updated_at: now })
156
156
+
.onConflictDoUpdate({
157
157
+
target: t.authority.domain,
158
158
+
set: { did, updated_at: now },
159
159
+
})
160
160
+
.run();
161
161
+
}
162
162
+
163
163
+
/**
164
164
+
* resolve the authority DID for an NSID.
165
165
+
* @param nsid NSID to resolve authority for
166
166
+
* @returns authority DID or null if resolution fails or authority doesn't exist
167
167
+
*/
168
168
+
async resolveAuthority(nsid: Nsid): Promise<AtprotoDid | null> {
169
169
+
if (!this.#enabled) {
170
170
+
return null;
171
171
+
}
172
172
+
173
173
+
const domain = this.#getNsidDomain(nsid);
174
174
+
175
175
+
// check cache first (includes negative cache entries)
176
176
+
const cached = this.#getCachedAuthority(domain);
177
177
+
if (cached && !cached.expired) {
178
178
+
if (cached.stale && cached.did !== null) {
179
179
+
// only refresh in background for positive results
180
180
+
this.#refreshAuthorityInBackground(nsid);
181
181
+
}
182
182
+
return cached.did;
183
183
+
}
184
184
+
185
185
+
// coalesce concurrent requests
186
186
+
const existing = this.#inflightAuthority.get(domain);
187
187
+
if (existing) {
188
188
+
return existing;
189
189
+
}
190
190
+
191
191
+
// queue the fetch
192
192
+
const promise = this.#fetchQueue.add(async () => {
193
193
+
try {
194
194
+
const did = await this.#authorityResolver.resolve(nsid);
195
195
+
this._setAuthority(domain, did);
196
196
+
return did;
197
197
+
} catch (err) {
198
198
+
// cache negative result for AuthorityNotFoundError (definitive "no authority")
199
199
+
if (err instanceof AuthorityNotFoundError) {
200
200
+
lexiconCacheLogger.debug('caching negative authority result', { nsid, domain });
201
201
+
this._setAuthority(domain, null);
202
202
+
return null;
203
203
+
}
204
204
+
205
205
+
// don't cache transient errors
206
206
+
lexiconCacheLogger.warn('failed to resolve lexicon authority', { nsid, err });
207
207
+
return null;
208
208
+
}
209
209
+
});
210
210
+
211
211
+
this.#inflightAuthority.set(domain, promise);
212
212
+
213
213
+
try {
214
214
+
return await promise;
215
215
+
} finally {
216
216
+
this.#inflightAuthority.delete(domain);
217
217
+
}
218
218
+
}
219
219
+
220
220
+
/**
221
221
+
* queue a background refresh for an authority resolution.
222
222
+
*/
223
223
+
#refreshAuthorityInBackground(nsid: Nsid): void {
224
224
+
const domain = this.#getNsidDomain(nsid);
225
225
+
226
226
+
this.#backgroundQueue.add(async () => {
227
227
+
try {
228
228
+
const did = await this.#authorityResolver.resolve(nsid);
229
229
+
this._setAuthority(domain, did);
230
230
+
} catch (err) {
231
231
+
lexiconCacheLogger.warn('background authority refresh failed', { nsid, err });
232
232
+
}
233
233
+
});
234
234
+
}
235
235
+
236
236
+
// #endregion
237
237
+
238
238
+
// #region schema resolution
239
239
+
240
240
+
/**
241
241
+
* get cached schema for an NSID.
242
242
+
*/
243
243
+
#getCachedSchema(nsid: Nsid): CachedSchema | null {
244
244
+
const row = this.#db.select().from(t.schema).where(eq(t.schema.nsid, nsid)).get();
245
245
+
246
246
+
if (!row) {
247
247
+
return null;
248
248
+
}
249
249
+
250
250
+
const now = Date.now();
251
251
+
const updatedAt = row.updated_at.getTime();
252
252
+
return {
253
253
+
doc: row.doc,
254
254
+
authorityDid: row.authority_did,
255
255
+
cid: row.cid,
256
256
+
updatedAt: updatedAt,
257
257
+
stale: now > updatedAt + this.#staleTtl,
258
258
+
expired: now > updatedAt + this.#maxTtl,
259
259
+
};
260
260
+
}
261
261
+
262
262
+
/**
263
263
+
* cache a schema document.
264
264
+
* @param nsid NSID of the schema
265
265
+
* @param authorityDid authority DID that was used
266
266
+
* @param cid schema CID, or null for negative cache entry
267
267
+
* @param doc schema document, or null for negative cache entry
268
268
+
* @internal exposed as `_setSchema` for test injection
269
269
+
*/
270
270
+
_setSchema(nsid: Nsid, authorityDid: AtprotoDid, cid: string | null, doc: LexiconDoc | null): void {
271
271
+
const now = new Date();
272
272
+
this.#db
273
273
+
.insert(t.schema)
274
274
+
.values({
275
275
+
nsid,
276
276
+
authority_did: authorityDid,
277
277
+
cid,
278
278
+
doc,
279
279
+
updated_at: now,
280
280
+
})
281
281
+
.onConflictDoUpdate({
282
282
+
target: t.schema.nsid,
283
283
+
set: {
284
284
+
authority_did: authorityDid,
285
285
+
cid,
286
286
+
doc,
287
287
+
updated_at: now,
288
288
+
},
289
289
+
})
290
290
+
.run();
291
291
+
}
292
292
+
293
293
+
/**
294
294
+
* get a lexicon schema document.
295
295
+
* @param nsid NSID to fetch schema for
296
296
+
* @returns lexicon document or null if not found/resolution fails
297
297
+
*/
298
298
+
async getSchema(nsid: Nsid): Promise<LexiconDoc | null> {
299
299
+
if (!this.#enabled) {
300
300
+
return null;
301
301
+
}
302
302
+
303
303
+
// resolve authority first to check for authority changes
304
304
+
const authorityDid = await this.resolveAuthority(nsid);
305
305
+
if (!authorityDid) {
306
306
+
return null;
307
307
+
}
308
308
+
309
309
+
// check cache - invalidate if authority has changed
310
310
+
const cached = this.#getCachedSchema(nsid);
311
311
+
if (cached && !cached.expired) {
312
312
+
// if authority changed, bust the cache and refetch
313
313
+
if (cached.authorityDid !== authorityDid) {
314
314
+
lexiconCacheLogger.debug('authority changed, invalidating schema cache', {
315
315
+
nsid,
316
316
+
oldAuthority: cached.authorityDid,
317
317
+
newAuthority: authorityDid,
318
318
+
});
319
319
+
// don't return cached, fall through to refetch
320
320
+
} else {
321
321
+
if (cached.stale && cached.doc !== null) {
322
322
+
// only refresh in background for positive results
323
323
+
this.#refreshSchemaInBackground(nsid, cached.authorityDid);
324
324
+
}
325
325
+
return cached.doc;
326
326
+
}
327
327
+
}
328
328
+
329
329
+
// coalesce concurrent requests
330
330
+
const existing = this.#inflightSchema.get(nsid);
331
331
+
if (existing) {
332
332
+
const result = await existing;
333
333
+
return result;
334
334
+
}
335
335
+
336
336
+
// queue the fetch
337
337
+
const promise = this.#fetchQueue.add(async () => {
338
338
+
try {
339
339
+
const resolved = await this.#schemaResolver.resolve(authorityDid, nsid);
340
340
+
this._setSchema(nsid, authorityDid, resolved.cid, resolved.schema);
341
341
+
return resolved.schema;
342
342
+
} catch (err) {
343
343
+
// cache negative result for definitive "schema doesn't exist" errors
344
344
+
if (err instanceof DocumentNotFoundError) {
345
345
+
lexiconCacheLogger.debug('caching negative schema result', { nsid, authorityDid });
346
346
+
this._setSchema(nsid, authorityDid, null, null);
347
347
+
return null;
348
348
+
}
349
349
+
350
350
+
// don't cache transient errors (network failures, etc.)
351
351
+
lexiconCacheLogger.warn('failed to resolve lexicon schema', { nsid, authorityDid, err });
352
352
+
return null;
353
353
+
}
354
354
+
});
355
355
+
356
356
+
this.#inflightSchema.set(nsid, promise);
357
357
+
358
358
+
try {
359
359
+
const result = await promise;
360
360
+
return result;
361
361
+
} finally {
362
362
+
this.#inflightSchema.delete(nsid);
363
363
+
}
364
364
+
}
365
365
+
366
366
+
/**
367
367
+
* queue a background refresh for a schema.
368
368
+
*/
369
369
+
#refreshSchemaInBackground(nsid: Nsid, authorityDid: AtprotoDid): void {
370
370
+
this.#backgroundQueue.add(async () => {
371
371
+
try {
372
372
+
const resolved = await this.#schemaResolver.resolve(authorityDid, nsid);
373
373
+
this._setSchema(nsid, authorityDid, resolved.cid, resolved.schema);
374
374
+
} catch (err) {
375
375
+
lexiconCacheLogger.warn('background schema refresh failed', { nsid, err });
376
376
+
}
377
377
+
});
378
378
+
}
379
379
+
380
380
+
// #endregion
381
381
+
382
382
+
// #region dependency resolution
383
383
+
384
384
+
/**
385
385
+
* get all schema documents needed for validating a record type.
386
386
+
* recursively resolves all external references with depth and total limits.
387
387
+
* @param nsid NSID of the record type
388
388
+
* @returns map of NSID -> LexiconDoc, or null if any required schema can't be resolved
389
389
+
*/
390
390
+
async getRecordDocs(nsid: Nsid): Promise<Record<string, LexiconDoc> | null> {
391
391
+
if (!this.#enabled) {
392
392
+
return null;
393
393
+
}
394
394
+
395
395
+
const docs: Record<string, LexiconDoc> = {};
396
396
+
const visited = new Set<string>();
397
397
+
let count = 0;
398
398
+
399
399
+
// start with the main reference
400
400
+
for await (const result of this.#crawlReferences(`${nsid}#main`, visited, 0)) {
401
401
+
// null indicates a required schema couldn't be resolved or limits exceeded
402
402
+
if (result === null) {
403
403
+
return null;
404
404
+
}
405
405
+
406
406
+
// check total schemas limit
407
407
+
if (count >= DEFAULT_MAX_CRAWL_SCHEMAS) {
408
408
+
return null;
409
409
+
}
410
410
+
411
411
+
docs[result.nsid] = result.schema;
412
412
+
count++;
413
413
+
}
414
414
+
415
415
+
return docs;
416
416
+
}
417
417
+
418
418
+
/**
419
419
+
* recursively crawl all transitive dependencies for a given reference.
420
420
+
* yields null if a required schema can't be resolved or limits are exceeded.
421
421
+
* @param ref reference to crawl (e.g., "app.bsky.feed.post#main")
422
422
+
* @param visited set of already-visited references (for cycle detection)
423
423
+
* @param depth current recursion depth
424
424
+
*/
425
425
+
async *#crawlReferences(
426
426
+
ref: string,
427
427
+
visited: Set<string>,
428
428
+
depth: number,
429
429
+
): AsyncGenerator<{ nsid: Nsid; schema: LexiconDoc } | null> {
430
430
+
// check depth limit
431
431
+
if (depth > DEFAULT_MAX_CRAWL_DEPTH) {
432
432
+
yield null;
433
433
+
return;
434
434
+
}
435
435
+
436
436
+
// normalize ref to include #defId
437
437
+
if (!ref.includes('#')) {
438
438
+
ref = `${ref}#main`;
439
439
+
}
440
440
+
441
441
+
// cycle detection
442
442
+
if (visited.has(ref)) {
443
443
+
return;
444
444
+
}
445
445
+
visited.add(ref);
446
446
+
447
447
+
// parse the reference
448
448
+
const hashIndex = ref.indexOf('#');
449
449
+
const nsid = ref.slice(0, hashIndex) as Nsid;
450
450
+
const defId = ref.slice(hashIndex + 1);
451
451
+
452
452
+
// try to load the schema
453
453
+
const schema = await this.getSchema(nsid);
454
454
+
if (schema === null) {
455
455
+
yield null;
456
456
+
return;
457
457
+
}
458
458
+
459
459
+
yield { nsid, schema };
460
460
+
461
461
+
// find external references in the specific definition
462
462
+
const externalRefs = findExternalReferences(schema, defId);
463
463
+
464
464
+
// recursively crawl each external reference
465
465
+
for (const externalRef of externalRefs) {
466
466
+
yield* this.#crawlReferences(externalRef, visited, depth + 1);
467
467
+
}
468
468
+
}
469
469
+
470
470
+
// #endregion
471
471
+
472
472
+
// #region validation
473
473
+
474
474
+
/**
475
475
+
* create a RecordValidator for a record type.
476
476
+
* @param nsid NSID of the record type
477
477
+
* @returns RecordValidator or null if schemas can't be resolved
478
478
+
*/
479
479
+
async getRecordValidator(nsid: Nsid): Promise<RecordValidator | null> {
480
480
+
if (!this.#enabled) {
481
481
+
return null;
482
482
+
}
483
483
+
484
484
+
const docs = await this.getRecordDocs(nsid);
485
485
+
if (!docs) {
486
486
+
return null;
487
487
+
}
488
488
+
489
489
+
return new RecordValidator(docs, nsid);
490
490
+
}
491
491
+
492
492
+
// #endregion
493
493
+
494
494
+
// #region maintenance
495
495
+
496
496
+
/**
497
497
+
* remove all expired entries from the cache.
498
498
+
*/
499
499
+
async pruneExpired(): Promise<void> {
500
500
+
const cutoff = new Date(Date.now() - this.#maxTtl);
501
501
+
this.#db.delete(t.authority).where(lt(t.authority.updated_at, cutoff)).run();
502
502
+
this.#db.delete(t.schema).where(lt(t.schema.updated_at, cutoff)).run();
503
503
+
}
504
504
+
505
505
+
/**
506
506
+
* clear all cached entries.
507
507
+
*/
508
508
+
clear(): void {
509
509
+
this.#db.delete(t.authority).run();
510
510
+
this.#db.delete(t.schema).run();
511
511
+
}
512
512
+
513
513
+
dispose(): void {
514
514
+
clearInterval(this.#pruneInterval);
515
515
+
this.#db.$client.close();
516
516
+
}
517
517
+
518
518
+
[Symbol.dispose](): void {
519
519
+
this.dispose();
520
520
+
}
521
521
+
522
522
+
// #endregion
523
523
+
}
+30
packages/danaus/src/lexicon/db/index.ts
···
1
1
+
import { Database } from 'bun:sqlite';
2
2
+
import path from 'node:path';
3
3
+
4
4
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
5
5
+
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
6
6
+
7
7
+
import * as schema from './schema.ts';
8
8
+
9
9
+
const MIGRATIONS_DIR = path.resolve(import.meta.dir, '../../../drizzle/lexicon');
10
10
+
11
11
+
export const getLexiconCacheDb = (location: string, walAutoCheckpointDisabled: boolean) => {
12
12
+
const sqliteDb = new Database(location);
13
13
+
sqliteDb.run(`PRAGMA journal_mode = WAL;`);
14
14
+
if (walAutoCheckpointDisabled) {
15
15
+
sqliteDb.run(`PRAGMA wal_autocheckpoint = 0;`);
16
16
+
}
17
17
+
18
18
+
const db = drizzle({
19
19
+
client: sqliteDb,
20
20
+
schema: schema,
21
21
+
});
22
22
+
23
23
+
migrate(db, { migrationsFolder: MIGRATIONS_DIR });
24
24
+
25
25
+
return db;
26
26
+
};
27
27
+
28
28
+
export type LexiconCacheDb = ReturnType<typeof getLexiconCacheDb>;
29
29
+
30
30
+
export { schema as t };
+37
packages/danaus/src/lexicon/db/schema.ts
···
1
1
+
import type { LexiconDoc } from '@atcute/lexicon-doc';
2
2
+
import type { AtprotoDid, Nsid } from '@atcute/lexicons/syntax';
3
3
+
4
4
+
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
5
5
+
6
6
+
/**
7
7
+
* cached lexicon authority resolutions (NSID domain → authority DID).
8
8
+
* `did` is null for negative cache entries (authority confirmed not to exist).
9
9
+
*/
10
10
+
export const authority = sqliteTable(
11
11
+
'authority',
12
12
+
{
13
13
+
domain: text().primaryKey(),
14
14
+
/** authority DID, or null if authority confirmed not to exist */
15
15
+
did: text().$type<AtprotoDid | null>(),
16
16
+
updated_at: integer({ mode: 'timestamp' }).notNull(),
17
17
+
},
18
18
+
(t) => [index('authority_updated_at_idx').on(t.updated_at)],
19
19
+
);
20
20
+
21
21
+
/**
22
22
+
* cached lexicon schemas.
23
23
+
* `cid` and `doc` are null for negative cache entries (schema confirmed not to exist).
24
24
+
*/
25
25
+
export const schema = sqliteTable(
26
26
+
'schema',
27
27
+
{
28
28
+
nsid: text().$type<Nsid>().primaryKey(),
29
29
+
authority_did: text().$type<AtprotoDid>().notNull(),
30
30
+
/** schema CID, or null if schema confirmed not to exist */
31
31
+
cid: text().$type<string | null>(),
32
32
+
/** schema document, or null if schema confirmed not to exist */
33
33
+
doc: text({ mode: 'json' }).$type<LexiconDoc | null>(),
34
34
+
updated_at: integer({ mode: 'timestamp' }).notNull(),
35
35
+
},
36
36
+
(t) => [index('schema_updated_at_idx').on(t.updated_at)],
37
37
+
);
+84
packages/danaus/src/lexicon/validate-writes.ts
···
1
1
+
import { ValidationError } from '@atcute/lexicons/validations';
2
2
+
import * as TID from '@atcute/tid';
3
3
+
import { InvalidRequestError } from '@atcute/xrpc-server';
4
4
+
5
5
+
import type { RepoWriteOp } from '#app/actors/repo/types.ts';
6
6
+
7
7
+
import type { LexiconCache } from './cache.ts';
8
8
+
9
9
+
/**
10
10
+
* validate record writes against lexicon schemas.
11
11
+
*
12
12
+
* @param lexiconCache lexicon cache for resolving schemas
13
13
+
* @param writes array of write operations
14
14
+
* @param validate validation mode:
15
15
+
* - `true`: require validation, fail if lexicon cannot be resolved
16
16
+
* - `false`: skip validation entirely
17
17
+
* - `undefined` (default): validate if lexicon is known, skip if not
18
18
+
* @throws InvalidRequestError with `InvalidRecord` if validation fails
19
19
+
* @throws InvalidRequestError with `UnresolvableLexicon` if validate=true and lexicon cannot be resolved
20
20
+
*/
21
21
+
export const validateRecordWrites = async (
22
22
+
lexiconCache: LexiconCache,
23
23
+
writes: RepoWriteOp[],
24
24
+
validate: boolean | undefined,
25
25
+
): Promise<void> => {
26
26
+
// skip if validation is disabled
27
27
+
if (validate === false) {
28
28
+
return;
29
29
+
}
30
30
+
31
31
+
// skip if lexicon resolution is disabled
32
32
+
if (!lexiconCache.enabled) {
33
33
+
if (validate === true) {
34
34
+
throw new InvalidRequestError({
35
35
+
error: 'UnresolvableLexicon',
36
36
+
description: `lexicon resolution is disabled`,
37
37
+
});
38
38
+
}
39
39
+
40
40
+
return;
41
41
+
}
42
42
+
43
43
+
const ops = writes.filter((write) => write.action === 'create' || write.action === 'update');
44
44
+
45
45
+
if (ops.length === 0) {
46
46
+
return;
47
47
+
}
48
48
+
49
49
+
// fallback TID for validating key format when rkey not provided
50
50
+
const tid = TID.now();
51
51
+
52
52
+
// validate each write
53
53
+
await Promise.all(
54
54
+
ops.map(async (write) => {
55
55
+
const validator = await lexiconCache.getRecordValidator(write.collection);
56
56
+
57
57
+
if (!validator) {
58
58
+
// lexicon not found
59
59
+
if (validate === true) {
60
60
+
throw new InvalidRequestError({
61
61
+
error: 'UnresolvableLexicon',
62
62
+
description: `could not resolve lexicon for ${write.collection}`,
63
63
+
});
64
64
+
}
65
65
+
66
66
+
// validate=undefined: skip if lexicon not known
67
67
+
return;
68
68
+
}
69
69
+
70
70
+
try {
71
71
+
validator.parse({ key: write.rkey ?? tid, object: write.record });
72
72
+
} catch (err) {
73
73
+
if (err instanceof ValidationError) {
74
74
+
throw new InvalidRequestError({
75
75
+
error: 'InvalidRecord',
76
76
+
description: `record failed validation: ${err.message}`,
77
77
+
});
78
78
+
}
79
79
+
80
80
+
throw err;
81
81
+
}
82
82
+
}),
83
83
+
);
84
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
71
+
/** lexicon cache operations */
72
72
+
export const lexiconCacheLogger = getLogger(['danaus', 'lexicon-cache']);
73
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
11
-
import type { AppConfig, ProxyConfig, ServiceConfig } from '#app/config.ts';
11
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
22
-
export interface TestPdsConfig extends Partial<AppConfig> {
22
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
29
+
/** lexicon config overrides */
30
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
130
+
},
131
131
+
lexicon: {
132
132
+
// disabled by default in tests to avoid network access
133
133
+
enabled: false,
134
134
+
cacheDbLocation: path.join(rootDir, 'lexicon-cache.db'),
135
135
+
nameservers: null,
136
136
+
cacheStaleTtlMs: HOUR,
137
137
+
cacheMaxTtlMs: DAY,
138
138
+
...cfg.lexicon,
128
139
},
129
140
secrets: {
130
141
adminPassword: ADMIN_PASSWORD,
+534
packages/danaus/tests/lexicon-validation.test.ts
···
1
1
+
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
2
2
+
3
3
+
import {
4
4
+
ComAtprotoRepoApplyWrites,
5
5
+
ComAtprotoRepoCreateRecord,
6
6
+
ComAtprotoRepoPutRecord,
7
7
+
} from '@atcute/atproto';
8
8
+
import { Client, ok, simpleFetchHandler } from '@atcute/client';
9
9
+
import type { LexiconDoc } from '@atcute/lexicon-doc';
10
10
+
import type { Did } from '@atcute/lexicons';
11
11
+
12
12
+
import { TestNetworkNoAppView, usersSeed, type SeedClient } from '#app/test/index.ts';
13
13
+
14
14
+
/**
15
15
+
* simple test lexicon with no external references.
16
16
+
* defines a record type with required `title` and `count` fields.
17
17
+
*/
18
18
+
const TEST_LEXICON: LexiconDoc = {
19
19
+
lexicon: 1,
20
20
+
id: 'com.example.simple',
21
21
+
defs: {
22
22
+
main: {
23
23
+
type: 'record',
24
24
+
key: 'any',
25
25
+
record: {
26
26
+
type: 'object',
27
27
+
required: ['title', 'count'],
28
28
+
properties: {
29
29
+
title: { type: 'string', maxLength: 100 },
30
30
+
count: { type: 'integer', minimum: 0 },
31
31
+
description: { type: 'string' },
32
32
+
},
33
33
+
},
34
34
+
},
35
35
+
},
36
36
+
};
37
37
+
38
38
+
/**
39
39
+
* test lexicon that requires a literal 'self' rkey (like app.bsky.actor.profile).
40
40
+
*/
41
41
+
const TEST_LEXICON_SELF_KEY: LexiconDoc = {
42
42
+
lexicon: 1,
43
43
+
id: 'com.example.selfkey',
44
44
+
defs: {
45
45
+
main: {
46
46
+
type: 'record',
47
47
+
key: 'literal:self',
48
48
+
record: {
49
49
+
type: 'object',
50
50
+
required: ['name'],
51
51
+
properties: {
52
52
+
name: { type: 'string' },
53
53
+
},
54
54
+
},
55
55
+
},
56
56
+
},
57
57
+
};
58
58
+
59
59
+
/**
60
60
+
* test lexicon authority DID (fake).
61
61
+
*/
62
62
+
const TEST_AUTHORITY_DID = 'did:plc:testauthority123';
63
63
+
64
64
+
describe('lexicon validation', () => {
65
65
+
let network: TestNetworkNoAppView;
66
66
+
let sc: SeedClient;
67
67
+
let client: Client;
68
68
+
let alice: Did;
69
69
+
70
70
+
beforeAll(async () => {
71
71
+
// create network with lexicon validation enabled
72
72
+
network = await TestNetworkNoAppView.create({
73
73
+
pds: {
74
74
+
lexicon: {
75
75
+
enabled: true,
76
76
+
},
77
77
+
},
78
78
+
});
79
79
+
80
80
+
sc = network.getSeedClient();
81
81
+
await usersSeed(sc);
82
82
+
alice = sc.dids.alice!;
83
83
+
client = new Client({ handler: simpleFetchHandler({ service: network.pds.url }) });
84
84
+
85
85
+
// inject test lexicons into cache
86
86
+
const lexiconCache = network.pds.ctx.lexiconCache;
87
87
+
lexiconCache._setAuthority('com.example', TEST_AUTHORITY_DID);
88
88
+
lexiconCache._setSchema('com.example.simple', TEST_AUTHORITY_DID, 'testcid123', TEST_LEXICON);
89
89
+
lexiconCache._setSchema('com.example.selfkey', TEST_AUTHORITY_DID, 'testcid456', TEST_LEXICON_SELF_KEY);
90
90
+
});
91
91
+
92
92
+
afterAll(async () => {
93
93
+
await network?.close();
94
94
+
});
95
95
+
96
96
+
const getHeaders = (did: Did) => sc.getHeaders(did);
97
97
+
98
98
+
/** helper to expect an XRPC error */
99
99
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100
100
+
const expectError = async (result: Promise<any>, expectedError: string): Promise<void> => {
101
101
+
const res = await result;
102
102
+
expect(res.ok).toBe(false);
103
103
+
expect(res.data?.error).toBe(expectedError);
104
104
+
};
105
105
+
106
106
+
describe('createRecord', () => {
107
107
+
it('accepts valid record when validate=true', async () => {
108
108
+
const result = await ok(
109
109
+
client.call(ComAtprotoRepoCreateRecord, {
110
110
+
input: {
111
111
+
repo: alice,
112
112
+
collection: 'com.example.simple',
113
113
+
validate: true,
114
114
+
record: {
115
115
+
$type: 'com.example.simple',
116
116
+
title: 'Test Title',
117
117
+
count: 42,
118
118
+
},
119
119
+
},
120
120
+
headers: getHeaders(alice),
121
121
+
}),
122
122
+
);
123
123
+
124
124
+
expect(result.uri).toContain('com.example.simple');
125
125
+
expect(result.cid).toBeDefined();
126
126
+
});
127
127
+
128
128
+
it('rejects invalid record when validate=true', async () => {
129
129
+
await expectError(
130
130
+
client.call(ComAtprotoRepoCreateRecord, {
131
131
+
input: {
132
132
+
repo: alice,
133
133
+
collection: 'com.example.simple',
134
134
+
validate: true,
135
135
+
record: {
136
136
+
$type: 'com.example.simple',
137
137
+
// missing required 'title' field
138
138
+
count: 42,
139
139
+
},
140
140
+
},
141
141
+
headers: getHeaders(alice),
142
142
+
}),
143
143
+
'InvalidRecord',
144
144
+
);
145
145
+
});
146
146
+
147
147
+
it('rejects record with wrong type when validate=true', async () => {
148
148
+
await expectError(
149
149
+
client.call(ComAtprotoRepoCreateRecord, {
150
150
+
input: {
151
151
+
repo: alice,
152
152
+
collection: 'com.example.simple',
153
153
+
validate: true,
154
154
+
record: {
155
155
+
$type: 'com.example.simple',
156
156
+
title: 'Test',
157
157
+
count: 'not a number', // should be integer
158
158
+
},
159
159
+
},
160
160
+
headers: getHeaders(alice),
161
161
+
}),
162
162
+
'InvalidRecord',
163
163
+
);
164
164
+
});
165
165
+
166
166
+
it('rejects unknown lexicon when validate=true', async () => {
167
167
+
await expectError(
168
168
+
client.call(ComAtprotoRepoCreateRecord, {
169
169
+
input: {
170
170
+
repo: alice,
171
171
+
collection: 'com.unknown.type',
172
172
+
validate: true,
173
173
+
record: {
174
174
+
$type: 'com.unknown.type',
175
175
+
anything: 'goes',
176
176
+
},
177
177
+
},
178
178
+
headers: getHeaders(alice),
179
179
+
}),
180
180
+
'UnresolvableLexicon',
181
181
+
);
182
182
+
});
183
183
+
184
184
+
it('allows unknown lexicon when validate=undefined (default)', async () => {
185
185
+
const result = await ok(
186
186
+
client.call(ComAtprotoRepoCreateRecord, {
187
187
+
input: {
188
188
+
repo: alice,
189
189
+
collection: 'com.unknown.type',
190
190
+
// validate not specified (undefined)
191
191
+
record: {
192
192
+
$type: 'com.unknown.type',
193
193
+
anything: 'goes',
194
194
+
},
195
195
+
},
196
196
+
headers: getHeaders(alice),
197
197
+
}),
198
198
+
);
199
199
+
200
200
+
expect(result.uri).toContain('com.unknown.type');
201
201
+
});
202
202
+
203
203
+
it('skips validation entirely when validate=false', async () => {
204
204
+
const result = await ok(
205
205
+
client.call(ComAtprotoRepoCreateRecord, {
206
206
+
input: {
207
207
+
repo: alice,
208
208
+
collection: 'com.example.simple',
209
209
+
validate: false,
210
210
+
record: {
211
211
+
$type: 'com.example.simple',
212
212
+
// completely invalid - missing required fields
213
213
+
invalidField: true,
214
214
+
},
215
215
+
},
216
216
+
headers: getHeaders(alice),
217
217
+
}),
218
218
+
);
219
219
+
220
220
+
expect(result.uri).toContain('com.example.simple');
221
221
+
});
222
222
+
});
223
223
+
224
224
+
describe('putRecord', () => {
225
225
+
it('creates record if not exists (upsert) when validate=true', async () => {
226
226
+
const result = await ok(
227
227
+
client.call(ComAtprotoRepoPutRecord, {
228
228
+
input: {
229
229
+
repo: alice,
230
230
+
collection: 'com.example.simple',
231
231
+
rkey: 'test-put-create',
232
232
+
validate: true,
233
233
+
record: {
234
234
+
$type: 'com.example.simple',
235
235
+
title: 'Put Create Test',
236
236
+
count: 10,
237
237
+
},
238
238
+
},
239
239
+
headers: getHeaders(alice),
240
240
+
}),
241
241
+
);
242
242
+
243
243
+
expect(result.uri).toContain('test-put-create');
244
244
+
});
245
245
+
246
246
+
it('updates record if exists (upsert) when validate=true', async () => {
247
247
+
// first create via putRecord
248
248
+
await ok(
249
249
+
client.call(ComAtprotoRepoPutRecord, {
250
250
+
input: {
251
251
+
repo: alice,
252
252
+
collection: 'com.example.simple',
253
253
+
rkey: 'test-put-update',
254
254
+
validate: true,
255
255
+
record: {
256
256
+
$type: 'com.example.simple',
257
257
+
title: 'Initial',
258
258
+
count: 0,
259
259
+
},
260
260
+
},
261
261
+
headers: getHeaders(alice),
262
262
+
}),
263
263
+
);
264
264
+
265
265
+
// then update via putRecord
266
266
+
const result = await ok(
267
267
+
client.call(ComAtprotoRepoPutRecord, {
268
268
+
input: {
269
269
+
repo: alice,
270
270
+
collection: 'com.example.simple',
271
271
+
rkey: 'test-put-update',
272
272
+
validate: true,
273
273
+
record: {
274
274
+
$type: 'com.example.simple',
275
275
+
title: 'Updated',
276
276
+
count: 99,
277
277
+
},
278
278
+
},
279
279
+
headers: getHeaders(alice),
280
280
+
}),
281
281
+
);
282
282
+
283
283
+
expect(result.uri).toContain('test-put-update');
284
284
+
});
285
285
+
286
286
+
it('rejects invalid record when validate=true', async () => {
287
287
+
await expectError(
288
288
+
client.call(ComAtprotoRepoPutRecord, {
289
289
+
input: {
290
290
+
repo: alice,
291
291
+
collection: 'com.example.simple',
292
292
+
rkey: 'test-put-invalid',
293
293
+
validate: true,
294
294
+
record: {
295
295
+
$type: 'com.example.simple',
296
296
+
title: 'Missing Count',
297
297
+
// missing required 'count' field
298
298
+
},
299
299
+
},
300
300
+
headers: getHeaders(alice),
301
301
+
}),
302
302
+
'InvalidRecord',
303
303
+
);
304
304
+
});
305
305
+
});
306
306
+
307
307
+
describe('applyWrites', () => {
308
308
+
it('accepts valid writes when validate=true', async () => {
309
309
+
const result = await ok(
310
310
+
client.call(ComAtprotoRepoApplyWrites, {
311
311
+
input: {
312
312
+
repo: alice,
313
313
+
validate: true,
314
314
+
writes: [
315
315
+
{
316
316
+
$type: 'com.atproto.repo.applyWrites#create',
317
317
+
collection: 'com.example.simple',
318
318
+
value: {
319
319
+
$type: 'com.example.simple',
320
320
+
title: 'Apply Write Test',
321
321
+
count: 99,
322
322
+
},
323
323
+
},
324
324
+
],
325
325
+
},
326
326
+
headers: getHeaders(alice),
327
327
+
}),
328
328
+
);
329
329
+
330
330
+
expect(result.results).toHaveLength(1);
331
331
+
expect(result.results![0]).toHaveProperty('uri');
332
332
+
});
333
333
+
334
334
+
it('rejects any invalid write in batch when validate=true', async () => {
335
335
+
await expectError(
336
336
+
client.call(ComAtprotoRepoApplyWrites, {
337
337
+
input: {
338
338
+
repo: alice,
339
339
+
validate: true,
340
340
+
writes: [
341
341
+
{
342
342
+
$type: 'com.atproto.repo.applyWrites#create',
343
343
+
collection: 'com.example.simple',
344
344
+
value: {
345
345
+
$type: 'com.example.simple',
346
346
+
title: 'Valid One',
347
347
+
count: 1,
348
348
+
},
349
349
+
},
350
350
+
{
351
351
+
$type: 'com.atproto.repo.applyWrites#create',
352
352
+
collection: 'com.example.simple',
353
353
+
value: {
354
354
+
$type: 'com.example.simple',
355
355
+
// invalid - missing required fields
356
356
+
},
357
357
+
},
358
358
+
],
359
359
+
},
360
360
+
headers: getHeaders(alice),
361
361
+
}),
362
362
+
'InvalidRecord',
363
363
+
);
364
364
+
});
365
365
+
366
366
+
it('validates updates but not deletes', async () => {
367
367
+
// first create a record to delete
368
368
+
const created = await ok(
369
369
+
client.call(ComAtprotoRepoCreateRecord, {
370
370
+
input: {
371
371
+
repo: alice,
372
372
+
collection: 'com.example.simple',
373
373
+
validate: false,
374
374
+
record: {
375
375
+
$type: 'com.example.simple',
376
376
+
title: 'To Delete',
377
377
+
count: 0,
378
378
+
},
379
379
+
},
380
380
+
headers: getHeaders(alice),
381
381
+
}),
382
382
+
);
383
383
+
384
384
+
const rkey = created.uri.split('/').pop()!;
385
385
+
386
386
+
// delete should work even with validate=true (no record to validate)
387
387
+
const result = await ok(
388
388
+
client.call(ComAtprotoRepoApplyWrites, {
389
389
+
input: {
390
390
+
repo: alice,
391
391
+
validate: true,
392
392
+
writes: [
393
393
+
{
394
394
+
$type: 'com.atproto.repo.applyWrites#delete',
395
395
+
collection: 'com.example.simple',
396
396
+
rkey,
397
397
+
},
398
398
+
],
399
399
+
},
400
400
+
headers: getHeaders(alice),
401
401
+
}),
402
402
+
);
403
403
+
404
404
+
expect(result.results).toHaveLength(1);
405
405
+
});
406
406
+
});
407
407
+
408
408
+
describe('validation constraints', () => {
409
409
+
it('enforces maxLength constraint', async () => {
410
410
+
await expectError(
411
411
+
client.call(ComAtprotoRepoCreateRecord, {
412
412
+
input: {
413
413
+
repo: alice,
414
414
+
collection: 'com.example.simple',
415
415
+
validate: true,
416
416
+
record: {
417
417
+
$type: 'com.example.simple',
418
418
+
title: 'x'.repeat(101), // exceeds maxLength: 100
419
419
+
count: 1,
420
420
+
},
421
421
+
},
422
422
+
headers: getHeaders(alice),
423
423
+
}),
424
424
+
'InvalidRecord',
425
425
+
);
426
426
+
});
427
427
+
428
428
+
it('enforces minimum constraint', async () => {
429
429
+
await expectError(
430
430
+
client.call(ComAtprotoRepoCreateRecord, {
431
431
+
input: {
432
432
+
repo: alice,
433
433
+
collection: 'com.example.simple',
434
434
+
validate: true,
435
435
+
record: {
436
436
+
$type: 'com.example.simple',
437
437
+
title: 'Test',
438
438
+
count: -1, // violates minimum: 0
439
439
+
},
440
440
+
},
441
441
+
headers: getHeaders(alice),
442
442
+
}),
443
443
+
'InvalidRecord',
444
444
+
);
445
445
+
});
446
446
+
447
447
+
it('accepts optional fields', async () => {
448
448
+
const result = await ok(
449
449
+
client.call(ComAtprotoRepoCreateRecord, {
450
450
+
input: {
451
451
+
repo: alice,
452
452
+
collection: 'com.example.simple',
453
453
+
validate: true,
454
454
+
record: {
455
455
+
$type: 'com.example.simple',
456
456
+
title: 'With Description',
457
457
+
count: 5,
458
458
+
description: 'This is optional',
459
459
+
},
460
460
+
},
461
461
+
headers: getHeaders(alice),
462
462
+
}),
463
463
+
);
464
464
+
465
465
+
expect(result.uri).toBeDefined();
466
466
+
});
467
467
+
468
468
+
it('rejects missing rkey when lexicon requires literal key', async () => {
469
469
+
// com.example.selfkey requires key: 'literal:self'
470
470
+
// not providing rkey should fail because auto-generated TID != 'self'
471
471
+
await expectError(
472
472
+
client.call(ComAtprotoRepoCreateRecord, {
473
473
+
input: {
474
474
+
repo: alice,
475
475
+
collection: 'com.example.selfkey',
476
476
+
validate: true,
477
477
+
// no rkey provided - will use fallback TID which doesn't match 'self'
478
478
+
record: {
479
479
+
$type: 'com.example.selfkey',
480
480
+
name: 'Test',
481
481
+
},
482
482
+
},
483
483
+
headers: getHeaders(alice),
484
484
+
}),
485
485
+
'InvalidRecord',
486
486
+
);
487
487
+
});
488
488
+
489
489
+
it('accepts correct literal rkey', async () => {
490
490
+
const result = await ok(
491
491
+
client.call(ComAtprotoRepoCreateRecord, {
492
492
+
input: {
493
493
+
repo: alice,
494
494
+
collection: 'com.example.selfkey',
495
495
+
rkey: 'self',
496
496
+
validate: true,
497
497
+
record: {
498
498
+
$type: 'com.example.selfkey',
499
499
+
name: 'Test',
500
500
+
},
501
501
+
},
502
502
+
headers: getHeaders(alice),
503
503
+
}),
504
504
+
);
505
505
+
506
506
+
expect(result.uri).toContain('/self');
507
507
+
});
508
508
+
});
509
509
+
510
510
+
describe('negative caching', () => {
511
511
+
it('caches authority not found and does not repeat lookups', async () => {
512
512
+
const lexiconCache = network.pds.ctx.lexiconCache;
513
513
+
514
514
+
// first lookup - should trigger DNS resolution and cache negative result
515
515
+
const result1 = await lexiconCache.resolveAuthority('com.notfound.test' as never);
516
516
+
expect(result1).toBe(null);
517
517
+
518
518
+
// second lookup - should hit cache, not trigger another DNS lookup
519
519
+
const result2 = await lexiconCache.resolveAuthority('com.notfound.other' as never);
520
520
+
expect(result2).toBe(null);
521
521
+
522
522
+
// verify it's cached by checking the internal state
523
523
+
// if the authority was cached, repeated calls should be instant
524
524
+
const start = performance.now();
525
525
+
for (let i = 0; i < 100; i++) {
526
526
+
await lexiconCache.resolveAuthority(`com.notfound.item${i}` as never);
527
527
+
}
528
528
+
const elapsed = performance.now() - start;
529
529
+
530
530
+
// should be fast since it's all hitting cache (no network)
531
531
+
expect(elapsed).toBeLessThan(100);
532
532
+
});
533
533
+
});
534
534
+
});
+21
-459
pnpm-lock.yaml
···
13
13
14
14
.:
15
15
devDependencies:
16
16
-
'@ianvs/prettier-plugin-sort-imports':
17
17
-
specifier: ^4.7.0
18
18
-
version: 4.7.0(@prettier/plugin-oxc@0.1.3)(@vue/compiler-sfc@3.5.26)(prettier@3.8.0)
19
19
-
'@prettier/plugin-oxc':
20
20
-
specifier: ^0.1.3
21
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
28
-
prettier:
29
29
-
specifier: ^3.8.0
30
30
-
version: 3.8.0
31
31
-
prettier-plugin-tailwindcss:
32
32
-
specifier: ^0.7.2
33
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
61
+
'@atcute/lexicon-doc':
62
62
+
specifier: ^2.0.6
63
63
+
version: 2.0.6
64
64
+
'@atcute/lexicon-resolver':
65
65
+
specifier: ^0.1.6
66
66
+
version: 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)
67
67
+
'@atcute/lexicon-resolver-node':
68
68
+
specifier: ^0.1.0
69
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
274
+
'@atcute/lexicon-resolver-node@0.1.0':
275
275
+
resolution: {integrity: sha512-rp6az2R3aBb4h2sx2L+SiI5OZ3KBUaQKoviwoIK9fN9nPyqqCOiLj+gEjeT1Ch03WWICWdgqmArmYZu4FGZhzQ==}
276
276
+
peerDependencies:
277
277
+
'@atcute/identity': ^1.0.0
278
278
+
'@atcute/lexicon-resolver': ^0.1.0
279
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
470
-
'@babel/code-frame@7.28.6':
471
471
-
resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
472
472
-
engines: {node: '>=6.9.0'}
473
473
-
474
474
-
'@babel/generator@7.28.6':
475
475
-
resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==}
476
476
-
engines: {node: '>=6.9.0'}
477
477
-
478
478
-
'@babel/helper-globals@7.28.0':
479
479
-
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
480
480
-
engines: {node: '>=6.9.0'}
481
481
-
482
482
-
'@babel/helper-string-parser@7.27.1':
483
483
-
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
484
484
-
engines: {node: '>=6.9.0'}
485
485
-
486
486
-
'@babel/helper-validator-identifier@7.28.5':
487
487
-
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
488
488
-
engines: {node: '>=6.9.0'}
489
489
-
490
490
-
'@babel/parser@7.28.6':
491
491
-
resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==}
492
492
-
engines: {node: '>=6.0.0'}
493
493
-
hasBin: true
494
494
-
495
495
-
'@babel/template@7.28.6':
496
496
-
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
497
497
-
engines: {node: '>=6.9.0'}
498
498
-
499
499
-
'@babel/traverse@7.28.6':
500
500
-
resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==}
501
501
-
engines: {node: '>=6.9.0'}
502
502
-
503
503
-
'@babel/types@7.28.6':
504
504
-
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
505
505
-
engines: {node: '>=6.9.0'}
506
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
739
-
'@ianvs/prettier-plugin-sort-imports@4.7.0':
740
740
-
resolution: {integrity: sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==}
741
741
-
peerDependencies:
742
742
-
'@prettier/plugin-oxc': ^0.0.4
743
743
-
'@vue/compiler-sfc': 2.7.x || 3.x
744
744
-
content-tag: ^4.0.0
745
745
-
prettier: 2 || 3 || ^4.0.0-0
746
746
-
prettier-plugin-ember-template-tag: ^2.1.0
747
747
-
peerDependenciesMeta:
748
748
-
'@prettier/plugin-oxc':
749
749
-
optional: true
750
750
-
'@vue/compiler-sfc':
751
751
-
optional: true
752
752
-
content-tag:
753
753
-
optional: true
754
754
-
prettier-plugin-ember-template-tag:
755
755
-
optional: true
756
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
936
-
'@oxc-parser/binding-android-arm64@0.99.0':
937
937
-
resolution: {integrity: sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==}
938
938
-
engines: {node: ^20.19.0 || >=22.12.0}
939
939
-
cpu: [arm64]
940
940
-
os: [android]
941
941
-
942
942
-
'@oxc-parser/binding-darwin-arm64@0.99.0':
943
943
-
resolution: {integrity: sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==}
944
944
-
engines: {node: ^20.19.0 || >=22.12.0}
945
945
-
cpu: [arm64]
946
946
-
os: [darwin]
947
947
-
948
948
-
'@oxc-parser/binding-darwin-x64@0.99.0':
949
949
-
resolution: {integrity: sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==}
950
950
-
engines: {node: ^20.19.0 || >=22.12.0}
951
951
-
cpu: [x64]
952
952
-
os: [darwin]
953
953
-
954
954
-
'@oxc-parser/binding-freebsd-x64@0.99.0':
955
955
-
resolution: {integrity: sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==}
956
956
-
engines: {node: ^20.19.0 || >=22.12.0}
957
957
-
cpu: [x64]
958
958
-
os: [freebsd]
959
959
-
960
960
-
'@oxc-parser/binding-linux-arm-gnueabihf@0.99.0':
961
961
-
resolution: {integrity: sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==}
962
962
-
engines: {node: ^20.19.0 || >=22.12.0}
963
963
-
cpu: [arm]
964
964
-
os: [linux]
965
965
-
966
966
-
'@oxc-parser/binding-linux-arm-musleabihf@0.99.0':
967
967
-
resolution: {integrity: sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==}
968
968
-
engines: {node: ^20.19.0 || >=22.12.0}
969
969
-
cpu: [arm]
970
970
-
os: [linux]
971
971
-
972
972
-
'@oxc-parser/binding-linux-arm64-gnu@0.99.0':
973
973
-
resolution: {integrity: sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==}
974
974
-
engines: {node: ^20.19.0 || >=22.12.0}
975
975
-
cpu: [arm64]
976
976
-
os: [linux]
977
977
-
978
978
-
'@oxc-parser/binding-linux-arm64-musl@0.99.0':
979
979
-
resolution: {integrity: sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==}
980
980
-
engines: {node: ^20.19.0 || >=22.12.0}
981
981
-
cpu: [arm64]
982
982
-
os: [linux]
983
983
-
984
984
-
'@oxc-parser/binding-linux-riscv64-gnu@0.99.0':
985
985
-
resolution: {integrity: sha512-zxn/xkf519f12FKkpL5XwJipsylfSSnm36h6c1zBDTz4fbIDMGyIhHfWfwM7uUmHo9Aqw1pLxFpY39Etv398+Q==}
986
986
-
engines: {node: ^20.19.0 || >=22.12.0}
987
987
-
cpu: [riscv64]
988
988
-
os: [linux]
989
989
-
990
990
-
'@oxc-parser/binding-linux-s390x-gnu@0.99.0':
991
991
-
resolution: {integrity: sha512-Y1eSDKDS5E4IVC7Oxw+NbYAKRmJPMJTIjW+9xOWwteDHkFqpocKe0USxog+Q1uhzalD9M0p9eXWEWdGQCMDBMQ==}
992
992
-
engines: {node: ^20.19.0 || >=22.12.0}
993
993
-
cpu: [s390x]
994
994
-
os: [linux]
995
995
-
996
996
-
'@oxc-parser/binding-linux-x64-gnu@0.99.0':
997
997
-
resolution: {integrity: sha512-YVJMfk5cFWB8i2/nIrbk6n15bFkMHqWnMIWkVx7r2KwpTxHyFMfu2IpeVKo1ITDSmt5nBrGdLHD36QRlu2nDLg==}
998
998
-
engines: {node: ^20.19.0 || >=22.12.0}
999
999
-
cpu: [x64]
1000
1000
-
os: [linux]
1001
1001
-
1002
1002
-
'@oxc-parser/binding-linux-x64-musl@0.99.0':
1003
1003
-
resolution: {integrity: sha512-2+SDPrie5f90A1b9EirtVggOgsqtsYU5raZwkDYKyS1uvJzjqHCDhG/f4TwQxHmIc5YkczdQfwvN91lwmjsKYQ==}
1004
1004
-
engines: {node: ^20.19.0 || >=22.12.0}
1005
1005
-
cpu: [x64]
1006
1006
-
os: [linux]
1007
1007
-
1008
1008
-
'@oxc-parser/binding-wasm32-wasi@0.99.0':
1009
1009
-
resolution: {integrity: sha512-DKA4j0QerUWSMADziLM5sAyM7V53Fj95CV9SjP77bPfEfT7MnvFKnneaRMqPK1cpzjAGiQF52OBUIKyk0dwOQA==}
1010
1010
-
engines: {node: '>=14.0.0'}
1011
1011
-
cpu: [wasm32]
1012
1012
-
1013
1013
-
'@oxc-parser/binding-win32-arm64-msvc@0.99.0':
1014
1014
-
resolution: {integrity: sha512-EaB3AvsxqdNUhh9FOoAxRZ2L4PCRwDlDb//QXItwyOJrX7XS+uGK9B1KEUV4FZ/7rDhHsWieLt5e07wl2Ti5AQ==}
1015
1015
-
engines: {node: ^20.19.0 || >=22.12.0}
1016
1016
-
cpu: [arm64]
1017
1017
-
os: [win32]
1018
1018
-
1019
1019
-
'@oxc-parser/binding-win32-x64-msvc@0.99.0':
1020
1020
-
resolution: {integrity: sha512-sJN1Q8h7ggFOyDn0zsHaXbP/MklAVUvhrbq0LA46Qum686P3SZQHjbATqJn9yaVEvaSKXCshgl0vQ1gWkGgpcQ==}
1021
1021
-
engines: {node: ^20.19.0 || >=22.12.0}
1022
1022
-
cpu: [x64]
1023
1023
-
os: [win32]
1024
1024
-
1025
884
'@oxc-project/types@0.108.0':
1026
885
resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==}
1027
1027
-
1028
1028
-
'@oxc-project/types@0.99.0':
1029
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
1333
-
1334
1334
-
'@prettier/plugin-oxc@0.1.3':
1335
1335
-
resolution: {integrity: sha512-aABz3zIRilpWMekbt1FL1JVBQrQLR8L4Td2SRctECrWSsXGTNn/G1BqNSKCdbvQS1LWstAXfqcXzDki7GAAJyg==}
1336
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
1762
-
'@vue/compiler-core@3.5.26':
1763
1763
-
resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
1764
1764
-
1765
1765
-
'@vue/compiler-dom@3.5.26':
1766
1766
-
resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==}
1767
1767
-
1768
1768
-
'@vue/compiler-sfc@3.5.26':
1769
1769
-
resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==}
1770
1770
-
1771
1771
-
'@vue/compiler-ssr@3.5.26':
1772
1772
-
resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==}
1773
1773
-
1774
1774
-
'@vue/shared@3.5.26':
1775
1775
-
resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==}
1776
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
2153
-
entities@7.0.0:
2154
2154
-
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
2155
2155
-
engines: {node: '>=0.12'}
2156
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
2192
-
2193
2193
-
estree-walker@2.0.2:
2194
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
2420
-
2421
2421
-
js-tokens@4.0.0:
2422
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
2427
-
jsesc@3.1.0:
2428
2428
-
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
2429
2429
-
engines: {node: '>=6'}
2430
2430
-
hasBin: true
2431
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
2626
-
nanoid@3.3.11:
2627
2627
-
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
2628
2628
-
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
2629
2629
-
hasBin: true
2630
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
2689
-
2690
2690
-
oxc-parser@0.99.0:
2691
2691
-
resolution: {integrity: sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ==}
2692
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
2821
-
postcss@8.5.6:
2822
2822
-
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
2823
2823
-
engines: {node: ^10 || ^12 || >=14}
2824
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
2841
-
prettier-plugin-tailwindcss@0.7.2:
2842
2842
-
resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
2843
2843
-
engines: {node: '>=20.19'}
2844
2844
-
peerDependencies:
2845
2845
-
'@ianvs/prettier-plugin-sort-imports': '*'
2846
2846
-
'@prettier/plugin-hermes': '*'
2847
2847
-
'@prettier/plugin-oxc': '*'
2848
2848
-
'@prettier/plugin-pug': '*'
2849
2849
-
'@shopify/prettier-plugin-liquid': '*'
2850
2850
-
'@trivago/prettier-plugin-sort-imports': '*'
2851
2851
-
'@zackad/prettier-plugin-twig': '*'
2852
2852
-
prettier: ^3.0
2853
2853
-
prettier-plugin-astro: '*'
2854
2854
-
prettier-plugin-css-order: '*'
2855
2855
-
prettier-plugin-jsdoc: '*'
2856
2856
-
prettier-plugin-marko: '*'
2857
2857
-
prettier-plugin-multiline-arrays: '*'
2858
2858
-
prettier-plugin-organize-attributes: '*'
2859
2859
-
prettier-plugin-organize-imports: '*'
2860
2860
-
prettier-plugin-sort-imports: '*'
2861
2861
-
prettier-plugin-svelte: '*'
2862
2862
-
peerDependenciesMeta:
2863
2863
-
'@ianvs/prettier-plugin-sort-imports':
2864
2864
-
optional: true
2865
2865
-
'@prettier/plugin-hermes':
2866
2866
-
optional: true
2867
2867
-
'@prettier/plugin-oxc':
2868
2868
-
optional: true
2869
2869
-
'@prettier/plugin-pug':
2870
2870
-
optional: true
2871
2871
-
'@shopify/prettier-plugin-liquid':
2872
2872
-
optional: true
2873
2873
-
'@trivago/prettier-plugin-sort-imports':
2874
2874
-
optional: true
2875
2875
-
'@zackad/prettier-plugin-twig':
2876
2876
-
optional: true
2877
2877
-
prettier-plugin-astro:
2878
2878
-
optional: true
2879
2879
-
prettier-plugin-css-order:
2880
2880
-
optional: true
2881
2881
-
prettier-plugin-jsdoc:
2882
2882
-
optional: true
2883
2883
-
prettier-plugin-marko:
2884
2884
-
optional: true
2885
2885
-
prettier-plugin-multiline-arrays:
2886
2886
-
optional: true
2887
2887
-
prettier-plugin-organize-attributes:
2888
2888
-
optional: true
2889
2889
-
prettier-plugin-organize-imports:
2890
2890
-
optional: true
2891
2891
-
prettier-plugin-sort-imports:
2892
2892
-
optional: true
2893
2893
-
prettier-plugin-svelte:
2894
2894
-
optional: true
2895
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
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
3152
+
dependencies:
3153
3153
+
'@atcute/identity': 1.1.3
3154
3154
+
'@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.3)
3155
3155
+
'@atcute/lexicons': 1.2.6
3156
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
3845
-
'@babel/code-frame@7.28.6':
3846
3846
-
dependencies:
3847
3847
-
'@babel/helper-validator-identifier': 7.28.5
3848
3848
-
js-tokens: 4.0.0
3849
3849
-
picocolors: 1.1.1
3850
3850
-
3851
3851
-
'@babel/generator@7.28.6':
3852
3852
-
dependencies:
3853
3853
-
'@babel/parser': 7.28.6
3854
3854
-
'@babel/types': 7.28.6
3855
3855
-
'@jridgewell/gen-mapping': 0.3.13
3856
3856
-
'@jridgewell/trace-mapping': 0.3.31
3857
3857
-
jsesc: 3.1.0
3858
3858
-
3859
3859
-
'@babel/helper-globals@7.28.0': {}
3860
3860
-
3861
3861
-
'@babel/helper-string-parser@7.27.1': {}
3862
3862
-
3863
3863
-
'@babel/helper-validator-identifier@7.28.5': {}
3864
3864
-
3865
3865
-
'@babel/parser@7.28.6':
3866
3866
-
dependencies:
3867
3867
-
'@babel/types': 7.28.6
3868
3868
-
3869
3869
-
'@babel/template@7.28.6':
3870
3870
-
dependencies:
3871
3871
-
'@babel/code-frame': 7.28.6
3872
3872
-
'@babel/parser': 7.28.6
3873
3873
-
'@babel/types': 7.28.6
3874
3874
-
3875
3875
-
'@babel/traverse@7.28.6':
3876
3876
-
dependencies:
3877
3877
-
'@babel/code-frame': 7.28.6
3878
3878
-
'@babel/generator': 7.28.6
3879
3879
-
'@babel/helper-globals': 7.28.0
3880
3880
-
'@babel/parser': 7.28.6
3881
3881
-
'@babel/template': 7.28.6
3882
3882
-
'@babel/types': 7.28.6
3883
3883
-
debug: 4.4.3
3884
3884
-
transitivePeerDependencies:
3885
3885
-
- supports-color
3886
3886
-
3887
3887
-
'@babel/types@7.28.6':
3888
3888
-
dependencies:
3889
3889
-
'@babel/helper-string-parser': 7.27.1
3890
3890
-
'@babel/helper-validator-identifier': 7.28.5
3891
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
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
4082
-
dependencies:
4083
4083
-
'@babel/generator': 7.28.6
4084
4084
-
'@babel/parser': 7.28.6
4085
4085
-
'@babel/traverse': 7.28.6
4086
4086
-
'@babel/types': 7.28.6
4087
4087
-
prettier: 3.8.0
4088
4088
-
semver: 7.7.3
4089
4089
-
optionalDependencies:
4090
4090
-
'@prettier/plugin-oxc': 0.1.3
4091
4091
-
'@vue/compiler-sfc': 3.5.26
4092
4092
-
transitivePeerDependencies:
4093
4093
-
- supports-color
4094
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
4245
-
'@oxc-parser/binding-android-arm64@0.99.0':
4246
4246
-
optional: true
4247
4247
-
4248
4248
-
'@oxc-parser/binding-darwin-arm64@0.99.0':
4249
4249
-
optional: true
4250
4250
-
4251
4251
-
'@oxc-parser/binding-darwin-x64@0.99.0':
4252
4252
-
optional: true
4253
4253
-
4254
4254
-
'@oxc-parser/binding-freebsd-x64@0.99.0':
4255
4255
-
optional: true
4256
4256
-
4257
4257
-
'@oxc-parser/binding-linux-arm-gnueabihf@0.99.0':
4258
4258
-
optional: true
4259
4259
-
4260
4260
-
'@oxc-parser/binding-linux-arm-musleabihf@0.99.0':
4261
4261
-
optional: true
4262
4262
-
4263
4263
-
'@oxc-parser/binding-linux-arm64-gnu@0.99.0':
4264
4264
-
optional: true
4265
4265
-
4266
4266
-
'@oxc-parser/binding-linux-arm64-musl@0.99.0':
4267
4267
-
optional: true
4268
4268
-
4269
4269
-
'@oxc-parser/binding-linux-riscv64-gnu@0.99.0':
4270
4270
-
optional: true
4271
4271
-
4272
4272
-
'@oxc-parser/binding-linux-s390x-gnu@0.99.0':
4273
4273
-
optional: true
4274
4274
-
4275
4275
-
'@oxc-parser/binding-linux-x64-gnu@0.99.0':
4276
4276
-
optional: true
4277
4277
-
4278
4278
-
'@oxc-parser/binding-linux-x64-musl@0.99.0':
4279
4279
-
optional: true
4280
4280
-
4281
4281
-
'@oxc-parser/binding-wasm32-wasi@0.99.0':
4282
4282
-
dependencies:
4283
4283
-
'@napi-rs/wasm-runtime': 1.1.1
4284
4284
-
optional: true
4285
4285
-
4286
4286
-
'@oxc-parser/binding-win32-arm64-msvc@0.99.0':
4287
4287
-
optional: true
4288
4288
-
4289
4289
-
'@oxc-parser/binding-win32-x64-msvc@0.99.0':
4290
4290
-
optional: true
4291
4291
-
4292
3944
'@oxc-project/types@0.108.0': {}
4293
4293
-
4294
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
4568
-
4569
4569
-
'@prettier/plugin-oxc@0.1.3':
4570
4570
-
dependencies:
4571
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
4908
-
'@vue/compiler-core@3.5.26':
4909
4909
-
dependencies:
4910
4910
-
'@babel/parser': 7.28.6
4911
4911
-
'@vue/shared': 3.5.26
4912
4912
-
entities: 7.0.0
4913
4913
-
estree-walker: 2.0.2
4914
4914
-
source-map-js: 1.2.1
4915
4915
-
optional: true
4916
4916
-
4917
4917
-
'@vue/compiler-dom@3.5.26':
4918
4918
-
dependencies:
4919
4919
-
'@vue/compiler-core': 3.5.26
4920
4920
-
'@vue/shared': 3.5.26
4921
4921
-
optional: true
4922
4922
-
4923
4923
-
'@vue/compiler-sfc@3.5.26':
4924
4924
-
dependencies:
4925
4925
-
'@babel/parser': 7.28.6
4926
4926
-
'@vue/compiler-core': 3.5.26
4927
4927
-
'@vue/compiler-dom': 3.5.26
4928
4928
-
'@vue/compiler-ssr': 3.5.26
4929
4929
-
'@vue/shared': 3.5.26
4930
4930
-
estree-walker: 2.0.2
4931
4931
-
magic-string: 0.30.21
4932
4932
-
postcss: 8.5.6
4933
4933
-
source-map-js: 1.2.1
4934
4934
-
optional: true
4935
4935
-
4936
4936
-
'@vue/compiler-ssr@3.5.26':
4937
4937
-
dependencies:
4938
4938
-
'@vue/compiler-dom': 3.5.26
4939
4939
-
'@vue/shared': 3.5.26
4940
4940
-
optional: true
4941
4941
-
4942
4942
-
'@vue/shared@3.5.26':
4943
4943
-
optional: true
4944
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
5232
-
entities@7.0.0:
5233
5233
-
optional: true
5234
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
5291
-
5292
5292
-
estree-walker@2.0.2:
5293
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
5544
-
js-tokens@4.0.0: {}
5545
5545
-
5546
5147
jsbi@4.3.2: {}
5547
5547
-
5548
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
5712
-
nanoid@3.3.11:
5713
5713
-
optional: true
5714
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
5752
-
oxc-parser@0.99.0:
5753
5753
-
dependencies:
5754
5754
-
'@oxc-project/types': 0.99.0
5755
5755
-
optionalDependencies:
5756
5756
-
'@oxc-parser/binding-android-arm64': 0.99.0
5757
5757
-
'@oxc-parser/binding-darwin-arm64': 0.99.0
5758
5758
-
'@oxc-parser/binding-darwin-x64': 0.99.0
5759
5759
-
'@oxc-parser/binding-freebsd-x64': 0.99.0
5760
5760
-
'@oxc-parser/binding-linux-arm-gnueabihf': 0.99.0
5761
5761
-
'@oxc-parser/binding-linux-arm-musleabihf': 0.99.0
5762
5762
-
'@oxc-parser/binding-linux-arm64-gnu': 0.99.0
5763
5763
-
'@oxc-parser/binding-linux-arm64-musl': 0.99.0
5764
5764
-
'@oxc-parser/binding-linux-riscv64-gnu': 0.99.0
5765
5765
-
'@oxc-parser/binding-linux-s390x-gnu': 0.99.0
5766
5766
-
'@oxc-parser/binding-linux-x64-gnu': 0.99.0
5767
5767
-
'@oxc-parser/binding-linux-x64-musl': 0.99.0
5768
5768
-
'@oxc-parser/binding-wasm32-wasi': 0.99.0
5769
5769
-
'@oxc-parser/binding-win32-arm64-msvc': 0.99.0
5770
5770
-
'@oxc-parser/binding-win32-x64-msvc': 0.99.0
5771
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
5928
-
postcss@8.5.6:
5929
5929
-
dependencies:
5930
5930
-
nanoid: 3.3.11
5931
5931
-
picocolors: 1.1.1
5932
5932
-
source-map-js: 1.2.1
5933
5933
-
optional: true
5934
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
5944
-
5945
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
5946
-
dependencies:
5947
5947
-
prettier: 3.8.0
5948
5948
-
optionalDependencies:
5949
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
5950
-
'@prettier/plugin-oxc': 0.1.3
5951
5513
5952
5514
prettier@3.8.0: {}
5953
5515