@hatk/hatk 0.0.1-alpha.8 → 0.0.1-alpha.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +110 -0
- package/dist/backfill.d.ts +21 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +102 -37
- package/dist/car.d.ts +42 -10
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +154 -14
- package/dist/cli.js +243 -1043
- package/dist/config.d.ts +74 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -9
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +23 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +74 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/{db.d.ts → database/db.d.ts} +57 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/{db.js → database/db.js} +755 -552
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +116 -32
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/{schema.d.ts → database/schema.d.ts} +14 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +81 -41
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +119 -0
- package/dist/feeds.d.ts +12 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +51 -6
- package/dist/hooks.d.ts +85 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +163 -0
- package/dist/hydrate.d.ts +7 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +4 -16
- package/dist/indexer.d.ts +109 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +338 -66
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +225 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +161 -72
- package/dist/mst.d.ts +18 -1
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +19 -8
- package/dist/oauth/db.d.ts +3 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +56 -19
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +13 -1
- package/dist/oauth/server.d.ts +92 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +352 -37
- package/dist/oauth/service-describe.d.ts +31 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +61 -0
- package/dist/oauth/session.d.ts +32 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +131 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +80 -40
- package/dist/pds-proxy.d.ts +82 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +356 -0
- package/dist/private-collections.d.ts +8 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +22 -0
- package/dist/push.d.ts +54 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +367 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +88 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +62 -0
- package/dist/server.d.ts +26 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +832 -638
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/templates/feed.tpl +14 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +41 -0
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +41 -32
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +82 -11
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +196 -39
- package/package.json +17 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jetstream v2 live tail — an alternative stream source to the relay firehose.
|
|
3
|
+
*
|
|
4
|
+
* Where `com.atproto.sync.subscribeRepos` ships every commit on the network as
|
|
5
|
+
* DAG-CBOR frames wrapping a CAR block store, Jetstream filters server-side and
|
|
6
|
+
* delivers records as already-decoded JSON. For an AppView tracking a handful
|
|
7
|
+
* of collections that is the difference between decoding the whole network and
|
|
8
|
+
* decoding only what it indexes.
|
|
9
|
+
*
|
|
10
|
+
* What this trades away: the CAR/MST proofs. hatk never verified them — it
|
|
11
|
+
* already trusts the relay — so nothing is lost that was being used. The
|
|
12
|
+
* `getRepo` backfill path in backfill.ts still speaks CAR and is unaffected.
|
|
13
|
+
*
|
|
14
|
+
* @see https://bsky.network/docs/jetstream
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { applyCommit, configureIndexer, getLastSeq, handleIdentityEvent, isIndexableCollection, noteSeq, resumeCursor, setCursorKey, } from "./indexer.js";
|
|
18
|
+
import { log, emit } from "./logger.js";
|
|
19
|
+
/** Server-side filter caps, rejected before the WebSocket upgrade. */
|
|
20
|
+
export const MAX_COLLECTIONS = 100;
|
|
21
|
+
export const MAX_DIDS = 10_000;
|
|
22
|
+
/** Event kinds hatk consumes. `commit` carries records; `identity` drives handle renames. */
|
|
23
|
+
const KINDS = ['commit', 'identity'];
|
|
24
|
+
const RECONNECT_DELAY_MS = 3000;
|
|
25
|
+
/**
|
|
26
|
+
* Refused handshakes before the next attempt drops its cursor and probes live.
|
|
27
|
+
*
|
|
28
|
+
* Jetstream retains roughly a day of seqs. A cursor older than that is refused
|
|
29
|
+
* at the handshake — a bare close 1006, no `open`, no message. Nothing arrives,
|
|
30
|
+
* so {@link getLastSeq} stays null and a plain resume offers the same dead
|
|
31
|
+
* cursor forever. Restarting does not clear it either: the boot cursor is read
|
|
32
|
+
* back from the row this stream never got to advance. The stream wedges for
|
|
33
|
+
* good, while records written through the AppView's own path keep indexing, so
|
|
34
|
+
* nothing looks broken from inside the app.
|
|
35
|
+
*
|
|
36
|
+
* A cursorless probe tells a dead cursor from a dead instance without guessing:
|
|
37
|
+
* a reachable instance always accepts a cursorless subscribe, so a probe that
|
|
38
|
+
* opens means the cursor was the problem, and a probe that is refused too means
|
|
39
|
+
* the instance is down and the cursor is still worth keeping. Probing every Nth
|
|
40
|
+
* attempt rather than once keeps that true across an outage of any length.
|
|
41
|
+
*/
|
|
42
|
+
export const CURSOR_PROBE_EVERY = 3;
|
|
43
|
+
/**
|
|
44
|
+
* Build the subscribe URL.
|
|
45
|
+
*
|
|
46
|
+
* Collections are sent explicitly rather than as an `ns.*` wildcard: a wildcard
|
|
47
|
+
* would also match sibling NSIDs the app has no lexicon for (and, for private
|
|
48
|
+
* collections, ones it must never accept from the network).
|
|
49
|
+
*
|
|
50
|
+
* Exported for tests.
|
|
51
|
+
*/
|
|
52
|
+
export function buildSubscribeUrl(jetstreamUrl, collections, pinnedRepos, cursor) {
|
|
53
|
+
const params = new URLSearchParams();
|
|
54
|
+
for (const collection of collections)
|
|
55
|
+
params.append('collections', collection);
|
|
56
|
+
for (const kind of KINDS)
|
|
57
|
+
params.append('kinds', kind);
|
|
58
|
+
if (pinnedRepos) {
|
|
59
|
+
for (const did of pinnedRepos)
|
|
60
|
+
params.append('dids', did);
|
|
61
|
+
}
|
|
62
|
+
if (cursor)
|
|
63
|
+
params.append('cursor', cursor);
|
|
64
|
+
return `${jetstreamUrl}/xrpc/network.bsky.jetstream.subscribeEvents?${params}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reject filters the server would reject at the handshake, where the message
|
|
68
|
+
* can name the actual limit instead of surfacing as a failed connection.
|
|
69
|
+
* Exported for tests.
|
|
70
|
+
*/
|
|
71
|
+
export function assertFilterLimits(collections, pinnedRepos) {
|
|
72
|
+
if (collections.size > MAX_COLLECTIONS) {
|
|
73
|
+
throw new Error(`Jetstream accepts at most ${MAX_COLLECTIONS} collections, got ${collections.size}. ` +
|
|
74
|
+
`Narrow the indexed collections or use the relay firehose instead.`);
|
|
75
|
+
}
|
|
76
|
+
if (pinnedRepos && pinnedRepos.size > MAX_DIDS) {
|
|
77
|
+
throw new Error(`Jetstream accepts at most ${MAX_DIDS} dids, got ${pinnedRepos.size}. ` +
|
|
78
|
+
`Unpin some repos or use the relay firehose instead.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Translate one Jetstream commit payload into hatk's wire-agnostic op shape.
|
|
83
|
+
*
|
|
84
|
+
* Deletes carry no `record` or `cid` — only the collection and rkey that
|
|
85
|
+
* identify what went away. Returns `null` when the payload is unusable or
|
|
86
|
+
* names a collection this AppView must not accept from the network.
|
|
87
|
+
*
|
|
88
|
+
* Exported for tests.
|
|
89
|
+
*/
|
|
90
|
+
export function commitToOp(commit, collections) {
|
|
91
|
+
const { collection, rkey, operation } = commit;
|
|
92
|
+
if (!collection || !rkey || !operation)
|
|
93
|
+
return null;
|
|
94
|
+
if (!isIndexableCollection(collection, collections))
|
|
95
|
+
return null;
|
|
96
|
+
if (operation === 'delete') {
|
|
97
|
+
return { action: 'delete', collection, rkey };
|
|
98
|
+
}
|
|
99
|
+
if (operation !== 'create' && operation !== 'update')
|
|
100
|
+
return null;
|
|
101
|
+
if (!commit.cid || !commit.record)
|
|
102
|
+
return null;
|
|
103
|
+
return { action: operation, collection, rkey, cid: commit.cid, record: commit.record };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Handle one decoded frame. Kept synchronous so the socket's event queue drains
|
|
107
|
+
* without backpressure — identity resolution is fire-and-forget, matching the
|
|
108
|
+
* relay path.
|
|
109
|
+
*
|
|
110
|
+
* Exported for tests.
|
|
111
|
+
*/
|
|
112
|
+
export function processEvent(payload, collections) {
|
|
113
|
+
const kind = typeof payload?.$type === 'string' ? payload.$type.split('#')[1] : undefined;
|
|
114
|
+
if (kind === 'identity') {
|
|
115
|
+
const did = typeof payload.did === 'string' ? payload.did : undefined;
|
|
116
|
+
const handle = typeof payload.handle === 'string' ? payload.handle : undefined;
|
|
117
|
+
if (did)
|
|
118
|
+
handleIdentityEvent(did, handle);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (kind !== 'commit')
|
|
122
|
+
return;
|
|
123
|
+
// Track sequence for the cursor before filtering: a stream of events for
|
|
124
|
+
// collections we don't index still advances the position we must resume from.
|
|
125
|
+
if (typeof payload.seq === 'number')
|
|
126
|
+
noteSeq(payload.seq);
|
|
127
|
+
const did = typeof payload.did === 'string' ? payload.did : undefined;
|
|
128
|
+
if (!did)
|
|
129
|
+
return;
|
|
130
|
+
const op = commitToOp(payload, collections);
|
|
131
|
+
if (!op)
|
|
132
|
+
return;
|
|
133
|
+
applyCommit(did, [op]);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The cursor a connection attempt should offer, given how many attempts in a
|
|
137
|
+
* row were refused before it.
|
|
138
|
+
*
|
|
139
|
+
* `refusals` counts consecutive closes that never reached `open`; it resets the
|
|
140
|
+
* moment one does. Every {@link CURSOR_PROBE_EVERY}th refusal answers null so
|
|
141
|
+
* the attempt subscribes to the live tip instead — see the constant for why
|
|
142
|
+
* that is the discriminator.
|
|
143
|
+
*
|
|
144
|
+
* Exported for tests.
|
|
145
|
+
*/
|
|
146
|
+
export function reconnectCursor(refusals, liveSeq, bootCursor) {
|
|
147
|
+
const resume = resumeCursor(liveSeq, bootCursor);
|
|
148
|
+
// Nothing to abandon — an attempt with no cursor is already a live tail.
|
|
149
|
+
if (!resume)
|
|
150
|
+
return resume;
|
|
151
|
+
return refusals > 0 && refusals % CURSOR_PROBE_EVERY === 0 ? null : resume;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Connect to a Jetstream v2 instance and begin indexing.
|
|
155
|
+
*
|
|
156
|
+
* Reconnects on disconnect after {@link RECONNECT_DELAY_MS}, resuming from the
|
|
157
|
+
* highest seq seen rather than the boot-time cursor. Jetstream's cursor is
|
|
158
|
+
* inclusive and delivery is at-least-once, so the event at the resume point
|
|
159
|
+
* arrives again — harmless, since writes upsert on the record's `at://` URI.
|
|
160
|
+
*
|
|
161
|
+
* `opts.cursor` is the boot-time cursor and stays fixed across reconnects;
|
|
162
|
+
* what each attempt actually offers comes from {@link reconnectCursor}, so a
|
|
163
|
+
* live probe can drop the cursor for one attempt without losing it.
|
|
164
|
+
*
|
|
165
|
+
* @param refusals Consecutive refused handshakes so far. Internal — reconnects
|
|
166
|
+
* pass their own count; callers start at 0.
|
|
167
|
+
* @returns The WebSocket connection (for shutdown coordination)
|
|
168
|
+
*/
|
|
169
|
+
export async function startJetstreamIndexer(opts, refusals = 0) {
|
|
170
|
+
const { jetstreamUrl, collections } = opts;
|
|
171
|
+
const pinnedRepos = opts.pinnedRepos || null;
|
|
172
|
+
assertFilterLimits(collections, pinnedRepos);
|
|
173
|
+
setCursorKey('jetstream');
|
|
174
|
+
await configureIndexer(opts);
|
|
175
|
+
const cursor = reconnectCursor(refusals, getLastSeq(), opts.cursor);
|
|
176
|
+
const wsUrl = buildSubscribeUrl(jetstreamUrl, collections, pinnedRepos, cursor);
|
|
177
|
+
if (cursor)
|
|
178
|
+
log(`[jetstream] Resuming from cursor ${cursor}`);
|
|
179
|
+
else if (refusals > 0)
|
|
180
|
+
log('[jetstream] Probing the live tip — the resume cursor keeps being refused');
|
|
181
|
+
log(`[jetstream] Connecting to ${jetstreamUrl} (${collections.size} collections)...`);
|
|
182
|
+
// The lexicon default is identical framing, so an empty subprotocol echo is
|
|
183
|
+
// fine; we offer it so the server can pick the JSON arm explicitly.
|
|
184
|
+
const ws = new WebSocket(wsUrl, ['xrpc.v1.json']);
|
|
185
|
+
ws.addEventListener('message', (event) => {
|
|
186
|
+
try {
|
|
187
|
+
if (typeof event.data !== 'string')
|
|
188
|
+
return;
|
|
189
|
+
const frame = JSON.parse(event.data);
|
|
190
|
+
// Events arrive wrapped in an envelope with the event under `payload`.
|
|
191
|
+
if (frame?.payload)
|
|
192
|
+
processEvent(frame.payload, collections);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
emit('jetstream', 'decode_error', { error: err instanceof Error ? err.message : String(err) });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
let opened = false;
|
|
199
|
+
ws.addEventListener('open', () => {
|
|
200
|
+
opened = true;
|
|
201
|
+
log('[jetstream] Connected');
|
|
202
|
+
});
|
|
203
|
+
ws.addEventListener('close', (event) => {
|
|
204
|
+
// A close that never reached `open` is a refused handshake, not a dropped
|
|
205
|
+
// stream, and the two want opposite things from the cursor.
|
|
206
|
+
const nextRefusals = opened ? 0 : refusals + 1;
|
|
207
|
+
if (opened) {
|
|
208
|
+
log(`[jetstream] Disconnected (${event.code}), reconnecting in ${RECONNECT_DELAY_MS / 1000}s...`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
log(`[jetstream] Handshake refused (${event.code}${event.reason ? `: ${event.reason}` : ''}) ` +
|
|
212
|
+
`x${nextRefusals}, retrying in ${RECONNECT_DELAY_MS / 1000}s...`);
|
|
213
|
+
emit('jetstream', 'handshake_refused', {
|
|
214
|
+
code: event.code,
|
|
215
|
+
reason: event.reason || undefined,
|
|
216
|
+
cursor: cursor ?? null,
|
|
217
|
+
refusals: nextRefusals,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// `opts` goes back unchanged so the boot cursor survives a probe; the seq
|
|
221
|
+
// is read at reconnect time, not here, so a resume uses everything seen.
|
|
222
|
+
setTimeout(() => startJetstreamIndexer(opts, nextRefusals), RECONNECT_DELAY_MS);
|
|
223
|
+
});
|
|
224
|
+
return ws;
|
|
225
|
+
}
|
package/dist/labels.d.ts
CHANGED
|
@@ -13,7 +13,36 @@ export interface LabelRuleContext {
|
|
|
13
13
|
value: Record<string, any>;
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
export interface LabelModule {
|
|
17
|
+
definition?: LabelDefinition;
|
|
18
|
+
evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
|
|
19
|
+
}
|
|
20
|
+
export declare function defineLabel(module: LabelModule): {
|
|
21
|
+
definition?: LabelDefinition;
|
|
22
|
+
evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
|
|
23
|
+
__type: "labels";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Discover and load label rule modules from the `labels/` directory.
|
|
27
|
+
*
|
|
28
|
+
* Each module should default-export an object with an optional `definition`
|
|
29
|
+
* (label metadata like severity and blur behavior) and an optional `evaluate`
|
|
30
|
+
* function that returns label values to apply to a record.
|
|
31
|
+
*
|
|
32
|
+
* @param labelsDir - Absolute path to the `labels/` directory
|
|
33
|
+
*/
|
|
16
34
|
export declare function initLabels(labelsDir: string): Promise<void>;
|
|
35
|
+
/** Clear all registered label definitions and rules (for hot-reload). */
|
|
36
|
+
export declare function clearLabels(): void;
|
|
37
|
+
/** Register a single label module from a scanned server/ module. */
|
|
38
|
+
export declare function registerLabelModule(name: string, labelMod: {
|
|
39
|
+
definition?: LabelDefinition;
|
|
40
|
+
evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
|
|
41
|
+
}): void;
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate all loaded label rules against a record and persist any resulting labels.
|
|
44
|
+
* Called after each record is indexed. Rule errors are logged but never block indexing.
|
|
45
|
+
*/
|
|
17
46
|
export declare function runLabelRules(record: {
|
|
18
47
|
uri: string;
|
|
19
48
|
cid: string;
|
|
@@ -21,9 +50,16 @@ export declare function runLabelRules(record: {
|
|
|
21
50
|
collection: string;
|
|
22
51
|
value: Record<string, any>;
|
|
23
52
|
}): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Re-evaluate all label rules against every existing record in the given collections.
|
|
55
|
+
* Used by `/admin/rescan-labels` to apply new or updated rules retroactively.
|
|
56
|
+
*
|
|
57
|
+
* @returns Count of records scanned and new labels applied
|
|
58
|
+
*/
|
|
24
59
|
export declare function rescanLabels(collections: string[]): Promise<{
|
|
25
60
|
scanned: number;
|
|
26
61
|
labeled: number;
|
|
27
62
|
}>;
|
|
63
|
+
/** Return all label definitions discovered during {@link initLabels}. */
|
|
28
64
|
export declare function getLabelDefinitions(): LabelDefinition[];
|
|
29
65
|
//# sourceMappingURL=labels.d.ts.map
|
package/dist/labels.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIlD,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACtD,CAAA;IACD,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACxD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;iBAJhC,eAAe;eACjB,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;;EAKxD;AAYD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCjE;AAED,yEAAyE;AACzE,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE;IAAE,UAAU,CAAC,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GAClG,IAAI,CAON;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuCvG;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,IAAI,eAAe,EAAE,CAEvD"}
|
package/dist/labels.js
CHANGED
|
@@ -6,13 +6,53 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Label system for applying moderation labels to records as they are indexed.
|
|
11
|
+
*
|
|
12
|
+
* Place label modules in the `labels/` directory. Each module default-exports
|
|
13
|
+
* an object with a `definition` (label metadata) and/or an `evaluate` function
|
|
14
|
+
* (rule that returns label values for a given record).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // labels/nsfw.ts
|
|
19
|
+
* import type { LabelRuleContext } from '@hatk/hatk/labels'
|
|
20
|
+
*
|
|
21
|
+
* export default {
|
|
22
|
+
* definition: {
|
|
23
|
+
* identifier: 'nsfw',
|
|
24
|
+
* severity: 'alert',
|
|
25
|
+
* blurs: 'media',
|
|
26
|
+
* defaultSetting: 'warn',
|
|
27
|
+
* locales: [{ lang: 'en', name: 'NSFW', description: 'Not safe for work' }],
|
|
28
|
+
* },
|
|
29
|
+
*
|
|
30
|
+
* async evaluate(ctx: LabelRuleContext): Promise<string[]> {
|
|
31
|
+
* if (ctx.record.value.nsfw === true) return ['nsfw']
|
|
32
|
+
* return []
|
|
33
|
+
* },
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
9
37
|
import { resolve } from 'node:path';
|
|
10
38
|
import { readdirSync } from 'node:fs';
|
|
11
|
-
import { querySQL, runSQL, insertLabels, getSchema } from "./db.js";
|
|
39
|
+
import { querySQL, runSQL, insertLabels, getSchema } from "./database/db.js";
|
|
12
40
|
import { log, emit } from "./logger.js";
|
|
41
|
+
export function defineLabel(module) {
|
|
42
|
+
return { __type: 'labels', ...module };
|
|
43
|
+
}
|
|
13
44
|
const rules = [];
|
|
14
45
|
let labelDefs = [];
|
|
15
46
|
let labelSrc = 'self';
|
|
47
|
+
/**
|
|
48
|
+
* Discover and load label rule modules from the `labels/` directory.
|
|
49
|
+
*
|
|
50
|
+
* Each module should default-export an object with an optional `definition`
|
|
51
|
+
* (label metadata like severity and blur behavior) and an optional `evaluate`
|
|
52
|
+
* function that returns label values to apply to a record.
|
|
53
|
+
*
|
|
54
|
+
* @param labelsDir - Absolute path to the `labels/` directory
|
|
55
|
+
*/
|
|
16
56
|
export async function initLabels(labelsDir) {
|
|
17
57
|
let files;
|
|
18
58
|
try {
|
|
@@ -26,7 +66,7 @@ export async function initLabels(labelsDir) {
|
|
|
26
66
|
for (const file of files) {
|
|
27
67
|
const name = file.replace(/\.(ts|js)$/, '');
|
|
28
68
|
const scriptPath = resolve(labelsDir, file);
|
|
29
|
-
const mod = await import(__rewriteRelativeImportExtension(scriptPath));
|
|
69
|
+
const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
|
|
30
70
|
const handler = mod.default;
|
|
31
71
|
if (handler.definition) {
|
|
32
72
|
labelDefs.push(handler.definition);
|
|
@@ -45,6 +85,24 @@ export async function initLabels(labelsDir) {
|
|
|
45
85
|
log(`[labels] ${labelDefs.length} label definitions loaded`);
|
|
46
86
|
}
|
|
47
87
|
}
|
|
88
|
+
/** Clear all registered label definitions and rules (for hot-reload). */
|
|
89
|
+
export function clearLabels() {
|
|
90
|
+
labelDefs.length = 0;
|
|
91
|
+
rules.length = 0;
|
|
92
|
+
}
|
|
93
|
+
/** Register a single label module from a scanned server/ module. */
|
|
94
|
+
export function registerLabelModule(name, labelMod) {
|
|
95
|
+
if (labelMod.definition) {
|
|
96
|
+
labelDefs.push(labelMod.definition);
|
|
97
|
+
}
|
|
98
|
+
if (labelMod.evaluate) {
|
|
99
|
+
rules.push({ name, evaluate: labelMod.evaluate });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Evaluate all loaded label rules against a record and persist any resulting labels.
|
|
104
|
+
* Called after each record is indexed. Rule errors are logged but never block indexing.
|
|
105
|
+
*/
|
|
48
106
|
export async function runLabelRules(record) {
|
|
49
107
|
if (rules.length === 0)
|
|
50
108
|
return;
|
|
@@ -69,15 +127,21 @@ export async function runLabelRules(record) {
|
|
|
69
127
|
emit('labels', 'applied', { count: allLabels.length, uri: record.uri, vals: allLabels.map((l) => l.val) });
|
|
70
128
|
}
|
|
71
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Re-evaluate all label rules against every existing record in the given collections.
|
|
132
|
+
* Used by `/admin/rescan-labels` to apply new or updated rules retroactively.
|
|
133
|
+
*
|
|
134
|
+
* @returns Count of records scanned and new labels applied
|
|
135
|
+
*/
|
|
72
136
|
export async function rescanLabels(collections) {
|
|
73
|
-
const beforeRows = await querySQL(`SELECT COUNT(*) as count FROM _labels`);
|
|
137
|
+
const beforeRows = (await querySQL(`SELECT COUNT(*) as count FROM _labels`));
|
|
74
138
|
const beforeCount = Number(beforeRows[0]?.count || 0);
|
|
75
139
|
let scanned = 0;
|
|
76
140
|
for (const collection of collections) {
|
|
77
141
|
const schema = getSchema(collection);
|
|
78
142
|
if (!schema)
|
|
79
143
|
continue;
|
|
80
|
-
const rows = await querySQL(`SELECT * FROM ${schema.tableName}`);
|
|
144
|
+
const rows = (await querySQL(`SELECT * FROM ${schema.tableName}`));
|
|
81
145
|
for (const row of rows) {
|
|
82
146
|
scanned++;
|
|
83
147
|
const value = {};
|
|
@@ -85,7 +149,7 @@ export async function rescanLabels(collections) {
|
|
|
85
149
|
let v = row[col.name];
|
|
86
150
|
if (v === null || v === undefined)
|
|
87
151
|
continue;
|
|
88
|
-
if (col.
|
|
152
|
+
if (col.isJson && typeof v === 'string') {
|
|
89
153
|
try {
|
|
90
154
|
v = JSON.parse(v);
|
|
91
155
|
}
|
|
@@ -102,10 +166,11 @@ export async function rescanLabels(collections) {
|
|
|
102
166
|
});
|
|
103
167
|
}
|
|
104
168
|
}
|
|
105
|
-
const afterRows = await querySQL(`SELECT COUNT(*) as count FROM _labels`);
|
|
169
|
+
const afterRows = (await querySQL(`SELECT COUNT(*) as count FROM _labels`));
|
|
106
170
|
const afterCount = Number(afterRows[0]?.count || 0);
|
|
107
171
|
return { scanned, labeled: afterCount - beforeCount };
|
|
108
172
|
}
|
|
173
|
+
/** Return all label definitions discovered during {@link initLabels}. */
|
|
109
174
|
export function getLabelDefinitions() {
|
|
110
175
|
return labelDefs;
|
|
111
176
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexicon-resolve.d.ts","sourceRoot":"","sources":["../src/lexicon-resolve.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lexicon-resolve.d.ts","sourceRoot":"","sources":["../src/lexicon-resolve.ts"],"names":[],"mappings":"AAoFA,UAAU,OAAO;IACf,OAAO,EAAE,MAAM,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AA2FD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgChF"}
|
package/dist/lexicon-resolve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Lexicon resolver — fetches lexicons from the AT Protocol registry via DNS → DID → PDS chain
|
|
2
2
|
// and recursively resolves all $ref dependencies.
|
|
3
3
|
import { isValidDid } from '@bigmoves/lexicon';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
6
|
// --- Authority ---
|
|
5
7
|
function nsidToDomain(nsid) {
|
|
6
8
|
const parts = nsid.split('.');
|
|
@@ -71,118 +73,31 @@ async function resolveDid(did, opts = {}) {
|
|
|
71
73
|
return null;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
|
-
// --- Built-in core schemas (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
uri: { type: 'string', format: 'uri' },
|
|
100
|
-
cid: { type: 'string', format: 'cid' },
|
|
101
|
-
val: { type: 'string', maxLength: 128 },
|
|
102
|
-
neg: { type: 'boolean' },
|
|
103
|
-
cts: { type: 'string', format: 'datetime' },
|
|
104
|
-
exp: { type: 'string', format: 'datetime' },
|
|
105
|
-
sig: { type: 'bytes' },
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
selfLabels: {
|
|
109
|
-
type: 'object',
|
|
110
|
-
description: 'Metadata tags on an atproto record, published by the author within the record.',
|
|
111
|
-
required: ['values'],
|
|
112
|
-
properties: { values: { type: 'array', items: { type: 'ref', ref: '#selfLabel' }, maxLength: 10 } },
|
|
113
|
-
},
|
|
114
|
-
selfLabel: { type: 'object', required: ['val'], properties: { val: { type: 'string', maxLength: 128 } } },
|
|
115
|
-
labelValueDefinition: {
|
|
116
|
-
type: 'object',
|
|
117
|
-
description: 'Declares a label value and its expected interpretations and behaviors.',
|
|
118
|
-
required: ['identifier', 'severity', 'blurs', 'locales'],
|
|
119
|
-
properties: {
|
|
120
|
-
identifier: { type: 'string', maxLength: 100 },
|
|
121
|
-
severity: { type: 'string', knownValues: ['inform', 'alert', 'none'] },
|
|
122
|
-
blurs: { type: 'string', knownValues: ['content', 'media', 'none'] },
|
|
123
|
-
defaultSetting: { type: 'string', knownValues: ['ignore', 'warn', 'hide'] },
|
|
124
|
-
adultOnly: { type: 'boolean' },
|
|
125
|
-
locales: { type: 'array', items: { type: 'ref', ref: '#labelValueDefinitionStrings' } },
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
labelValueDefinitionStrings: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
required: ['lang', 'name', 'description'],
|
|
131
|
-
properties: {
|
|
132
|
-
lang: { type: 'string', format: 'language' },
|
|
133
|
-
name: { type: 'string', maxLength: 640 },
|
|
134
|
-
description: { type: 'string', maxLength: 100000 },
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
labelValue: {
|
|
138
|
-
type: 'string',
|
|
139
|
-
knownValues: [
|
|
140
|
-
'!hide',
|
|
141
|
-
'!no-promote',
|
|
142
|
-
'!warn',
|
|
143
|
-
'!no-unauthenticated',
|
|
144
|
-
'dmca-violation',
|
|
145
|
-
'doxxing',
|
|
146
|
-
'porn',
|
|
147
|
-
'sexual',
|
|
148
|
-
'nudity',
|
|
149
|
-
'nsfl',
|
|
150
|
-
'gore',
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
'com.atproto.moderation.defs': {
|
|
156
|
-
lexicon: 1,
|
|
157
|
-
id: 'com.atproto.moderation.defs',
|
|
158
|
-
defs: {
|
|
159
|
-
reasonType: {
|
|
160
|
-
type: 'string',
|
|
161
|
-
knownValues: [
|
|
162
|
-
'com.atproto.moderation.defs#reasonSpam',
|
|
163
|
-
'com.atproto.moderation.defs#reasonViolation',
|
|
164
|
-
'com.atproto.moderation.defs#reasonMisleading',
|
|
165
|
-
'com.atproto.moderation.defs#reasonSexual',
|
|
166
|
-
'com.atproto.moderation.defs#reasonRude',
|
|
167
|
-
'com.atproto.moderation.defs#reasonOther',
|
|
168
|
-
'com.atproto.moderation.defs#reasonAppeal',
|
|
169
|
-
],
|
|
170
|
-
},
|
|
171
|
-
reasonSpam: { type: 'token', description: 'Spam: frequent unwanted promotion, replies, mentions.' },
|
|
172
|
-
reasonViolation: { type: 'token', description: 'Direct violation of server rules, laws, terms of service.' },
|
|
173
|
-
reasonMisleading: { type: 'token', description: 'Misleading identity, affiliation, or content.' },
|
|
174
|
-
reasonSexual: { type: 'token', description: 'Unwanted or mislabeled sexual content.' },
|
|
175
|
-
reasonRude: { type: 'token', description: 'Rude, harassing, explicit, or otherwise unwelcoming behavior.' },
|
|
176
|
-
reasonOther: { type: 'token', description: 'Reports not falling under another report category.' },
|
|
177
|
-
reasonAppeal: { type: 'token', description: 'Appeal a previously taken moderation action.' },
|
|
178
|
-
subjectType: {
|
|
179
|
-
type: 'string',
|
|
180
|
-
description: 'Tag describing a type of subject that might be reported.',
|
|
181
|
-
knownValues: ['account', 'record', 'chat'],
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
};
|
|
76
|
+
// --- Built-in core schemas (loaded from src/lexicons/) ---
|
|
77
|
+
function loadCoreSchemas() {
|
|
78
|
+
const schemas = {};
|
|
79
|
+
const lexDir = join(import.meta.dirname, 'lexicons');
|
|
80
|
+
function walk(dir) {
|
|
81
|
+
for (const entry of readdirSync(dir)) {
|
|
82
|
+
const full = join(dir, entry);
|
|
83
|
+
if (statSync(full).isDirectory()) {
|
|
84
|
+
walk(full);
|
|
85
|
+
}
|
|
86
|
+
else if (entry.endsWith('.json')) {
|
|
87
|
+
const lexicon = JSON.parse(readFileSync(full, 'utf-8'));
|
|
88
|
+
if (lexicon.id)
|
|
89
|
+
schemas[lexicon.id] = lexicon;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
walk(lexDir);
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
return schemas;
|
|
98
|
+
}
|
|
99
|
+
const coreSchemas = loadCoreSchemas();
|
|
100
|
+
// --- Resolver ---
|
|
186
101
|
function refToNsid(ref) {
|
|
187
102
|
let nsid = ref.startsWith('lex:') ? ref.slice(4) : ref;
|
|
188
103
|
const hashIndex = nsid.indexOf('#');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lexicon": 1,
|
|
3
|
+
"id": "com.atproto.label.defs",
|
|
4
|
+
"defs": {
|
|
5
|
+
"label": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"description": "Metadata tag on an atproto resource (eg, repo or record).",
|
|
8
|
+
"required": ["src", "uri", "val", "cts"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"ver": { "type": "integer" },
|
|
11
|
+
"src": { "type": "string", "format": "did" },
|
|
12
|
+
"uri": { "type": "string", "format": "uri" },
|
|
13
|
+
"cid": { "type": "string", "format": "cid" },
|
|
14
|
+
"val": { "type": "string", "maxLength": 128 },
|
|
15
|
+
"neg": { "type": "boolean" },
|
|
16
|
+
"cts": { "type": "string", "format": "datetime" },
|
|
17
|
+
"exp": { "type": "string", "format": "datetime" },
|
|
18
|
+
"sig": { "type": "bytes" }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"selfLabels": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"description": "Metadata tags on an atproto record, published by the author within the record.",
|
|
24
|
+
"required": ["values"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"values": { "type": "array", "items": { "type": "ref", "ref": "#selfLabel" }, "maxLength": 10 }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"selfLabel": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["val"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"val": { "type": "string", "maxLength": 128 }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"labelValueDefinition": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"description": "Declares a label value and its expected interpretations and behaviors.",
|
|
39
|
+
"required": ["identifier", "severity", "blurs", "locales"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"identifier": { "type": "string", "maxLength": 100 },
|
|
42
|
+
"severity": { "type": "string", "knownValues": ["inform", "alert", "none"] },
|
|
43
|
+
"blurs": { "type": "string", "knownValues": ["content", "media", "none"] },
|
|
44
|
+
"defaultSetting": { "type": "string", "knownValues": ["ignore", "warn", "hide"] },
|
|
45
|
+
"adultOnly": { "type": "boolean" },
|
|
46
|
+
"locales": { "type": "array", "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" } }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"labelValueDefinitionStrings": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["lang", "name", "description"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"lang": { "type": "string", "format": "language" },
|
|
54
|
+
"name": { "type": "string", "maxLength": 640 },
|
|
55
|
+
"description": { "type": "string", "maxLength": 100000 }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"labelValue": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"knownValues": [
|
|
61
|
+
"!hide",
|
|
62
|
+
"!no-promote",
|
|
63
|
+
"!warn",
|
|
64
|
+
"!no-unauthenticated",
|
|
65
|
+
"dmca-violation",
|
|
66
|
+
"doxxing",
|
|
67
|
+
"porn",
|
|
68
|
+
"sexual",
|
|
69
|
+
"nudity",
|
|
70
|
+
"nsfl",
|
|
71
|
+
"gore"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|