@hatk/hatk 0.0.1-alpha.7 → 0.0.1-alpha.71
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 +108 -0
- package/dist/backfill.d.ts +21 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +116 -43
- 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 +46 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +42 -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 +82 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +161 -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 +38 -0
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +228 -31
- package/dist/oauth/session.d.ts +11 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +65 -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 +60 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +277 -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 +34 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +186 -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 +729 -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 +75 -11
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +189 -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,161 @@
|
|
|
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
|
+
* Build the subscribe URL.
|
|
27
|
+
*
|
|
28
|
+
* Collections are sent explicitly rather than as an `ns.*` wildcard: a wildcard
|
|
29
|
+
* would also match sibling NSIDs the app has no lexicon for (and, for private
|
|
30
|
+
* collections, ones it must never accept from the network).
|
|
31
|
+
*
|
|
32
|
+
* Exported for tests.
|
|
33
|
+
*/
|
|
34
|
+
export function buildSubscribeUrl(jetstreamUrl, collections, pinnedRepos, cursor) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
for (const collection of collections)
|
|
37
|
+
params.append('collections', collection);
|
|
38
|
+
for (const kind of KINDS)
|
|
39
|
+
params.append('kinds', kind);
|
|
40
|
+
if (pinnedRepos) {
|
|
41
|
+
for (const did of pinnedRepos)
|
|
42
|
+
params.append('dids', did);
|
|
43
|
+
}
|
|
44
|
+
if (cursor)
|
|
45
|
+
params.append('cursor', cursor);
|
|
46
|
+
return `${jetstreamUrl}/xrpc/network.bsky.jetstream.subscribeEvents?${params}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reject filters the server would reject at the handshake, where the message
|
|
50
|
+
* can name the actual limit instead of surfacing as a failed connection.
|
|
51
|
+
* Exported for tests.
|
|
52
|
+
*/
|
|
53
|
+
export function assertFilterLimits(collections, pinnedRepos) {
|
|
54
|
+
if (collections.size > MAX_COLLECTIONS) {
|
|
55
|
+
throw new Error(`Jetstream accepts at most ${MAX_COLLECTIONS} collections, got ${collections.size}. ` +
|
|
56
|
+
`Narrow the indexed collections or use the relay firehose instead.`);
|
|
57
|
+
}
|
|
58
|
+
if (pinnedRepos && pinnedRepos.size > MAX_DIDS) {
|
|
59
|
+
throw new Error(`Jetstream accepts at most ${MAX_DIDS} dids, got ${pinnedRepos.size}. ` +
|
|
60
|
+
`Unpin some repos or use the relay firehose instead.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Translate one Jetstream commit payload into hatk's wire-agnostic op shape.
|
|
65
|
+
*
|
|
66
|
+
* Deletes carry no `record` or `cid` — only the collection and rkey that
|
|
67
|
+
* identify what went away. Returns `null` when the payload is unusable or
|
|
68
|
+
* names a collection this AppView must not accept from the network.
|
|
69
|
+
*
|
|
70
|
+
* Exported for tests.
|
|
71
|
+
*/
|
|
72
|
+
export function commitToOp(commit, collections) {
|
|
73
|
+
const { collection, rkey, operation } = commit;
|
|
74
|
+
if (!collection || !rkey || !operation)
|
|
75
|
+
return null;
|
|
76
|
+
if (!isIndexableCollection(collection, collections))
|
|
77
|
+
return null;
|
|
78
|
+
if (operation === 'delete') {
|
|
79
|
+
return { action: 'delete', collection, rkey };
|
|
80
|
+
}
|
|
81
|
+
if (operation !== 'create' && operation !== 'update')
|
|
82
|
+
return null;
|
|
83
|
+
if (!commit.cid || !commit.record)
|
|
84
|
+
return null;
|
|
85
|
+
return { action: operation, collection, rkey, cid: commit.cid, record: commit.record };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handle one decoded frame. Kept synchronous so the socket's event queue drains
|
|
89
|
+
* without backpressure — identity resolution is fire-and-forget, matching the
|
|
90
|
+
* relay path.
|
|
91
|
+
*
|
|
92
|
+
* Exported for tests.
|
|
93
|
+
*/
|
|
94
|
+
export function processEvent(payload, collections) {
|
|
95
|
+
const kind = typeof payload?.$type === 'string' ? payload.$type.split('#')[1] : undefined;
|
|
96
|
+
if (kind === 'identity') {
|
|
97
|
+
const did = typeof payload.did === 'string' ? payload.did : undefined;
|
|
98
|
+
const handle = typeof payload.handle === 'string' ? payload.handle : undefined;
|
|
99
|
+
if (did)
|
|
100
|
+
handleIdentityEvent(did, handle);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (kind !== 'commit')
|
|
104
|
+
return;
|
|
105
|
+
// Track sequence for the cursor before filtering: a stream of events for
|
|
106
|
+
// collections we don't index still advances the position we must resume from.
|
|
107
|
+
if (typeof payload.seq === 'number')
|
|
108
|
+
noteSeq(payload.seq);
|
|
109
|
+
const did = typeof payload.did === 'string' ? payload.did : undefined;
|
|
110
|
+
if (!did)
|
|
111
|
+
return;
|
|
112
|
+
const op = commitToOp(payload, collections);
|
|
113
|
+
if (!op)
|
|
114
|
+
return;
|
|
115
|
+
applyCommit(did, [op]);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Connect to a Jetstream v2 instance and begin indexing.
|
|
119
|
+
*
|
|
120
|
+
* Reconnects on disconnect after {@link RECONNECT_DELAY_MS}, resuming from the
|
|
121
|
+
* highest seq seen rather than the boot-time cursor. Jetstream's cursor is
|
|
122
|
+
* inclusive and delivery is at-least-once, so the event at the resume point
|
|
123
|
+
* arrives again — harmless, since writes upsert on the record's `at://` URI.
|
|
124
|
+
*
|
|
125
|
+
* @returns The WebSocket connection (for shutdown coordination)
|
|
126
|
+
*/
|
|
127
|
+
export async function startJetstreamIndexer(opts) {
|
|
128
|
+
const { jetstreamUrl, collections, cursor } = opts;
|
|
129
|
+
const pinnedRepos = opts.pinnedRepos || null;
|
|
130
|
+
assertFilterLimits(collections, pinnedRepos);
|
|
131
|
+
setCursorKey('jetstream');
|
|
132
|
+
await configureIndexer(opts);
|
|
133
|
+
const wsUrl = buildSubscribeUrl(jetstreamUrl, collections, pinnedRepos, cursor);
|
|
134
|
+
if (cursor)
|
|
135
|
+
log(`[jetstream] Resuming from cursor ${cursor}`);
|
|
136
|
+
log(`[jetstream] Connecting to ${jetstreamUrl} (${collections.size} collections)...`);
|
|
137
|
+
// The lexicon default is identical framing, so an empty subprotocol echo is
|
|
138
|
+
// fine; we offer it so the server can pick the JSON arm explicitly.
|
|
139
|
+
const ws = new WebSocket(wsUrl, ['xrpc.v1.json']);
|
|
140
|
+
ws.addEventListener('message', (event) => {
|
|
141
|
+
try {
|
|
142
|
+
if (typeof event.data !== 'string')
|
|
143
|
+
return;
|
|
144
|
+
const frame = JSON.parse(event.data);
|
|
145
|
+
// Events arrive wrapped in an envelope with the event under `payload`.
|
|
146
|
+
if (frame?.payload)
|
|
147
|
+
processEvent(frame.payload, collections);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
emit('jetstream', 'decode_error', { error: err instanceof Error ? err.message : String(err) });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
ws.addEventListener('open', () => log('[jetstream] Connected'));
|
|
154
|
+
ws.addEventListener('close', () => {
|
|
155
|
+
log(`[jetstream] Disconnected, reconnecting in ${RECONNECT_DELAY_MS / 1000}s...`);
|
|
156
|
+
// Read the seq at close time, not connect time — reusing the boot cursor on
|
|
157
|
+
// every reconnect would replay everything received since the process started.
|
|
158
|
+
setTimeout(() => startJetstreamIndexer({ ...opts, cursor: resumeCursor(getLastSeq(), opts.cursor) }), RECONNECT_DELAY_MS);
|
|
159
|
+
});
|
|
160
|
+
return ws;
|
|
161
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lexicon": 1,
|
|
3
|
+
"id": "com.atproto.moderation.defs",
|
|
4
|
+
"defs": {
|
|
5
|
+
"reasonType": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"knownValues": [
|
|
8
|
+
"com.atproto.moderation.defs#reasonSpam",
|
|
9
|
+
"com.atproto.moderation.defs#reasonViolation",
|
|
10
|
+
"com.atproto.moderation.defs#reasonMisleading",
|
|
11
|
+
"com.atproto.moderation.defs#reasonSexual",
|
|
12
|
+
"com.atproto.moderation.defs#reasonRude",
|
|
13
|
+
"com.atproto.moderation.defs#reasonOther",
|
|
14
|
+
"com.atproto.moderation.defs#reasonAppeal"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"reasonSpam": { "type": "token", "description": "Spam: frequent unwanted promotion, replies, mentions." },
|
|
18
|
+
"reasonViolation": { "type": "token", "description": "Direct violation of server rules, laws, terms of service." },
|
|
19
|
+
"reasonMisleading": { "type": "token", "description": "Misleading identity, affiliation, or content." },
|
|
20
|
+
"reasonSexual": { "type": "token", "description": "Unwanted or mislabeled sexual content." },
|
|
21
|
+
"reasonRude": { "type": "token", "description": "Rude, harassing, explicit, or otherwise unwelcoming behavior." },
|
|
22
|
+
"reasonOther": { "type": "token", "description": "Reports not falling under another report category." },
|
|
23
|
+
"reasonAppeal": { "type": "token", "description": "Appeal a previously taken moderation action." },
|
|
24
|
+
"subjectType": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Tag describing a type of subject that might be reported.",
|
|
27
|
+
"knownValues": ["account", "record", "chat"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lexicon": 1,
|
|
3
|
+
"id": "com.atproto.repo.strongRef",
|
|
4
|
+
"description": "A URI with a content-hash fingerprint.",
|
|
5
|
+
"defs": {
|
|
6
|
+
"main": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["uri", "cid"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"uri": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"format": "at-uri"
|
|
13
|
+
},
|
|
14
|
+
"cid": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "cid"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|