@hatk/hatk 0.0.1-alpha.9 → 0.0.1-alpha.92
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 +113 -0
- package/dist/backfill.d.ts +29 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +142 -51
- package/dist/car.d.ts +20 -0
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +108 -0
- package/dist/cli.js +247 -1043
- package/dist/config.d.ts +145 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -8
- 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 +16 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +76 -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} +68 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +1775 -0
- 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} +122 -34
- 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} +16 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +126 -44
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +127 -0
- package/dist/feeds.d.ts +26 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +72 -16
- package/dist/hooks.d.ts +102 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +186 -0
- package/dist/hydrate.d.ts +49 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +167 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +556 -94
- 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/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- 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 +198 -75
- package/dist/mst.d.ts +15 -0
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +13 -0
- package/dist/oauth/db.d.ts +16 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +74 -19
- package/dist/oauth/discovery.d.ts +10 -0
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +54 -2
- package/dist/oauth/server.d.ts +93 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +416 -39
- package/dist/oauth/service-describe.d.ts +42 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +108 -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 +94 -42
- 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 +13 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +27 -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 +34 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1032 -649
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/spaces/blob.d.ts +51 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +111 -0
- package/dist/spaces/credential.d.ts +62 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +184 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +559 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +88 -0
- package/dist/spaces/index.d.ts +55 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +133 -0
- package/dist/spaces/notify.d.ts +51 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +121 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +39 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +87 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +18 -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 +53 -0
- package/dist/test.d.ts +3 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +60 -36
- 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 +101 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +255 -55
- package/package.json +21 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1361
- 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,356 @@
|
|
|
1
|
+
// Shared PDS proxy functions — used by both HTTP route handlers and XRPC handlers.
|
|
2
|
+
import { getSession, getServerKey, deleteSession } from "./oauth/db.js";
|
|
3
|
+
import { createDpopProof } from "./oauth/dpop.js";
|
|
4
|
+
import { refreshPdsSession } from "./oauth/server.js";
|
|
5
|
+
import { validateRecord } from '@bigmoves/lexicon';
|
|
6
|
+
import { getLexiconArray } from "./database/schema.js";
|
|
7
|
+
import { insertRecord, deleteRecord as dbDeleteRecord } from "./database/db.js";
|
|
8
|
+
import { emit } from "./logger.js";
|
|
9
|
+
import { runLabelRules } from "./labels.js";
|
|
10
|
+
export class ProxyError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(status, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class ScopeMissingProxyError extends ProxyError {
|
|
18
|
+
constructor() {
|
|
19
|
+
super(401, 'ScopeMissingError');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Shared retry logic: DPoP nonce handling + token refresh. */
|
|
23
|
+
async function withDpopRetry(oauthConfig, session, doFetch) {
|
|
24
|
+
let accessToken = session.access_token;
|
|
25
|
+
let result = await doFetch(accessToken);
|
|
26
|
+
if (result.ok)
|
|
27
|
+
return result;
|
|
28
|
+
let nonce;
|
|
29
|
+
// Step 1: handle DPoP nonce requirement
|
|
30
|
+
if (isNonceChallenge(result)) {
|
|
31
|
+
nonce = result.headers.get('DPoP-Nonce') || undefined;
|
|
32
|
+
if (nonce) {
|
|
33
|
+
result = await doFetch(accessToken, nonce);
|
|
34
|
+
if (result.ok)
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Step 2: handle insufficient scope — clear session so user re-authenticates with updated scopes
|
|
39
|
+
if (result.body.error === 'ScopeMissingError') {
|
|
40
|
+
await deleteSession(session.did);
|
|
41
|
+
throw new ScopeMissingProxyError();
|
|
42
|
+
}
|
|
43
|
+
// Step 3: handle expired PDS token — refresh and retry
|
|
44
|
+
if (isTokenError(result.body.error)) {
|
|
45
|
+
const refreshed = await refreshPdsSession(oauthConfig, session);
|
|
46
|
+
if (refreshed) {
|
|
47
|
+
accessToken = refreshed.accessToken;
|
|
48
|
+
result = await doFetch(accessToken, nonce);
|
|
49
|
+
if (result.ok)
|
|
50
|
+
return result;
|
|
51
|
+
if (isNonceChallenge(result)) {
|
|
52
|
+
nonce = result.headers.get('DPoP-Nonce') || undefined;
|
|
53
|
+
if (nonce)
|
|
54
|
+
result = await doFetch(accessToken, nonce);
|
|
55
|
+
}
|
|
56
|
+
// A token minted seconds ago cannot be expired, so a second refusal is
|
|
57
|
+
// about the grant rather than the token: the session was authorized
|
|
58
|
+
// before the app asked for the scope this call needs, and no number of
|
|
59
|
+
// refreshes will add it. Read as a scope problem it becomes the prompt to
|
|
60
|
+
// authorize again; read as expiry it was an opaque 500.
|
|
61
|
+
//
|
|
62
|
+
// Not every PDS names this case. The reference answers ScopeMissingError,
|
|
63
|
+
// handled above; pds.js answers InvalidToken for a scope its permissioned
|
|
64
|
+
// space endpoints refuse, which is indistinguishable from expiry until
|
|
65
|
+
// the refresh rules expiry out.
|
|
66
|
+
if (!result.ok && isTokenError(result.body.error)) {
|
|
67
|
+
await deleteSession(session.did);
|
|
68
|
+
throw new ScopeMissingProxyError();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Is this refusal a DPoP nonce challenge — a request to retry, not a dead token?
|
|
76
|
+
*
|
|
77
|
+
* A nonce challenge is the one 401 that costs nothing to satisfy: the server
|
|
78
|
+
* hands back the nonce it wants in the same response. Miss it and the failure
|
|
79
|
+
* falls through to the token-error branch below, which refreshes, replays the
|
|
80
|
+
* identical proof, is refused identically, and drops the user's session — a
|
|
81
|
+
* re-authorization to fix a header.
|
|
82
|
+
*
|
|
83
|
+
* So it is read the way RFC 9449 §8 writes it, from
|
|
84
|
+
* `WWW-Authenticate: DPoP error="use_dpop_nonce"`, and only then from the body.
|
|
85
|
+
* The body is not always available to read: zds sends that header but an
|
|
86
|
+
* `InvalidToken` body, because its XRPC error handler overwrites the
|
|
87
|
+
* `UseDpopNonce` one on the way out.
|
|
88
|
+
*
|
|
89
|
+
* The `DPoP-Nonce` header alone is not the signal. A server may attach one to
|
|
90
|
+
* every 401 it sends, including 401s about a genuinely invalid token, and
|
|
91
|
+
* treating those as challenges would replay every dead token once for nothing.
|
|
92
|
+
*/
|
|
93
|
+
function isNonceChallenge(result) {
|
|
94
|
+
const error = result.body.error;
|
|
95
|
+
if (error === 'use_dpop_nonce' || error === 'UseDpopNonce')
|
|
96
|
+
return true;
|
|
97
|
+
return result.status === 401 && (result.headers.get('WWW-Authenticate') ?? '').includes('use_dpop_nonce');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The PDS returns 'InvalidToken' or 'ExpiredToken' (AT Proto PascalCase
|
|
101
|
+
* convention) while the OAuth spec uses 'invalid_token' (RFC 6750 snake_case).
|
|
102
|
+
*/
|
|
103
|
+
function isTokenError(error) {
|
|
104
|
+
return error === 'invalid_token' || error === 'InvalidToken' || error === 'ExpiredToken';
|
|
105
|
+
}
|
|
106
|
+
async function proxyToPds(oauthConfig, session, method, pdsUrl, body) {
|
|
107
|
+
const serverKey = await getServerKey('appview-oauth-key');
|
|
108
|
+
const privateJwk = JSON.parse(serverKey.privateKey);
|
|
109
|
+
const publicJwk = JSON.parse(serverKey.publicKey);
|
|
110
|
+
return withDpopRetry(oauthConfig, session, async (token, nonce) => {
|
|
111
|
+
const proof = await createDpopProof(privateJwk, publicJwk, method, pdsUrl, token, nonce);
|
|
112
|
+
// A GET carries no body, and sending a JSON content-type without one makes
|
|
113
|
+
// some servers try to parse it. Queries reach here through pdsXrpc.
|
|
114
|
+
const hasBody = method !== 'GET' && body !== undefined;
|
|
115
|
+
const res = await fetch(pdsUrl, {
|
|
116
|
+
method,
|
|
117
|
+
headers: {
|
|
118
|
+
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
|
119
|
+
Authorization: `DPoP ${token}`,
|
|
120
|
+
DPoP: proof,
|
|
121
|
+
},
|
|
122
|
+
body: hasBody ? JSON.stringify(body) : undefined,
|
|
123
|
+
});
|
|
124
|
+
const resBody = await res.json().catch(() => ({}));
|
|
125
|
+
return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Proxy a raw binary request to the user's PDS with DPoP + nonce retry + token refresh. */
|
|
129
|
+
async function proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType) {
|
|
130
|
+
const serverKey = await getServerKey('appview-oauth-key');
|
|
131
|
+
const privateJwk = JSON.parse(serverKey.privateKey);
|
|
132
|
+
const publicJwk = JSON.parse(serverKey.publicKey);
|
|
133
|
+
return withDpopRetry(oauthConfig, session, async (token, nonce) => {
|
|
134
|
+
const proof = await createDpopProof(privateJwk, publicJwk, 'POST', pdsUrl, token, nonce);
|
|
135
|
+
const res = await fetch(pdsUrl, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': contentType,
|
|
139
|
+
'Content-Length': String(body.length),
|
|
140
|
+
Authorization: `DPoP ${token}`,
|
|
141
|
+
DPoP: proof,
|
|
142
|
+
},
|
|
143
|
+
body: Buffer.from(body),
|
|
144
|
+
});
|
|
145
|
+
const resBody = await res.json().catch(() => ({}));
|
|
146
|
+
return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Call any XRPC method on the user's PDS with their session.
|
|
151
|
+
*
|
|
152
|
+
* The escape hatch beside the typed record helpers above, for methods hatk has
|
|
153
|
+
* no opinion about — `com.atproto.space.*` and the rest of proposal 0016 being
|
|
154
|
+
* the case it was added for. Nothing is validated or indexed: the response is
|
|
155
|
+
* whatever the PDS sent, and it is the caller's job to know its shape.
|
|
156
|
+
*
|
|
157
|
+
* The session's granted scopes still apply. A method the user never authorized
|
|
158
|
+
* comes back as a 403 from the PDS, not from here.
|
|
159
|
+
*/
|
|
160
|
+
export async function pdsXrpc(oauthConfig, viewer, nsid, options = {}) {
|
|
161
|
+
const session = await getSession(viewer.did);
|
|
162
|
+
if (!session)
|
|
163
|
+
throw new ProxyError(401, 'No PDS session for user');
|
|
164
|
+
const method = options.method ?? 'GET';
|
|
165
|
+
const url = new URL(`${session.pds_endpoint}/xrpc/${nsid}`);
|
|
166
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
167
|
+
if (value === undefined)
|
|
168
|
+
continue;
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
for (const v of value)
|
|
171
|
+
url.searchParams.append(key, String(v));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
url.searchParams.append(key, String(value));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const res = await proxyToPds(oauthConfig, session, method, url.toString(), options.body);
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
throw new ProxyError(res.status, String(res.body.error || `${nsid} failed`));
|
|
180
|
+
}
|
|
181
|
+
return res.body;
|
|
182
|
+
}
|
|
183
|
+
export async function pdsCreateRecord(oauthConfig, viewer, input) {
|
|
184
|
+
const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
|
|
185
|
+
if (validationError) {
|
|
186
|
+
throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
|
|
187
|
+
}
|
|
188
|
+
const session = await getSession(viewer.did);
|
|
189
|
+
if (!session)
|
|
190
|
+
throw new ProxyError(401, 'No PDS session for user');
|
|
191
|
+
const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.createRecord`;
|
|
192
|
+
const pdsBody = {
|
|
193
|
+
repo: viewer.did,
|
|
194
|
+
collection: input.collection,
|
|
195
|
+
rkey: input.rkey,
|
|
196
|
+
record: input.record,
|
|
197
|
+
};
|
|
198
|
+
const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
|
|
199
|
+
if (!pdsRes.ok)
|
|
200
|
+
throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
|
|
201
|
+
try {
|
|
202
|
+
await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
|
|
203
|
+
await runLabelRules({
|
|
204
|
+
uri: String(pdsRes.body.uri),
|
|
205
|
+
cid: String(pdsRes.body.cid),
|
|
206
|
+
did: viewer.did,
|
|
207
|
+
collection: input.collection,
|
|
208
|
+
value: input.record,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
emit('pds-proxy', 'local_index_error', {
|
|
213
|
+
op: 'createRecord',
|
|
214
|
+
error: err instanceof Error ? err.message : String(err),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return pdsRes.body;
|
|
218
|
+
}
|
|
219
|
+
export async function pdsDeleteRecord(oauthConfig, viewer, input) {
|
|
220
|
+
const session = await getSession(viewer.did);
|
|
221
|
+
if (!session)
|
|
222
|
+
throw new ProxyError(401, 'No PDS session for user');
|
|
223
|
+
const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.deleteRecord`;
|
|
224
|
+
const pdsBody = {
|
|
225
|
+
repo: viewer.did,
|
|
226
|
+
collection: input.collection,
|
|
227
|
+
rkey: input.rkey,
|
|
228
|
+
};
|
|
229
|
+
const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
|
|
230
|
+
if (!pdsRes.ok)
|
|
231
|
+
throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS delete failed'));
|
|
232
|
+
try {
|
|
233
|
+
const uri = `at://${viewer.did}/${input.collection}/${input.rkey}`;
|
|
234
|
+
await dbDeleteRecord(input.collection, uri);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
emit('pds-proxy', 'local_index_error', {
|
|
238
|
+
op: 'deleteRecord',
|
|
239
|
+
error: err instanceof Error ? err.message : String(err),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return pdsRes.body;
|
|
243
|
+
}
|
|
244
|
+
export async function pdsPutRecord(oauthConfig, viewer, input) {
|
|
245
|
+
const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
|
|
246
|
+
if (validationError) {
|
|
247
|
+
throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
|
|
248
|
+
}
|
|
249
|
+
const session = await getSession(viewer.did);
|
|
250
|
+
if (!session)
|
|
251
|
+
throw new ProxyError(401, 'No PDS session for user');
|
|
252
|
+
const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.putRecord`;
|
|
253
|
+
const pdsBody = {
|
|
254
|
+
repo: viewer.did,
|
|
255
|
+
collection: input.collection,
|
|
256
|
+
rkey: input.rkey,
|
|
257
|
+
record: input.record,
|
|
258
|
+
};
|
|
259
|
+
const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
|
|
260
|
+
if (!pdsRes.ok)
|
|
261
|
+
throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
|
|
262
|
+
try {
|
|
263
|
+
await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
|
|
264
|
+
await runLabelRules({
|
|
265
|
+
uri: String(pdsRes.body.uri),
|
|
266
|
+
cid: String(pdsRes.body.cid),
|
|
267
|
+
did: viewer.did,
|
|
268
|
+
collection: input.collection,
|
|
269
|
+
value: input.record,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
emit('pds-proxy', 'local_index_error', { op: 'putRecord', error: err instanceof Error ? err.message : String(err) });
|
|
274
|
+
}
|
|
275
|
+
return pdsRes.body;
|
|
276
|
+
}
|
|
277
|
+
/** Map dev.hatk.applyWrites#* types to com.atproto.repo.applyWrites#* for PDS. */
|
|
278
|
+
function toPdsWriteType($type) {
|
|
279
|
+
return $type.replace('dev.hatk.applyWrites#', 'com.atproto.repo.applyWrites#');
|
|
280
|
+
}
|
|
281
|
+
function isCreateOrUpdate($type) {
|
|
282
|
+
const mapped = toPdsWriteType($type);
|
|
283
|
+
return mapped === 'com.atproto.repo.applyWrites#create' || mapped === 'com.atproto.repo.applyWrites#update';
|
|
284
|
+
}
|
|
285
|
+
export async function pdsApplyWrites(oauthConfig, viewer, input) {
|
|
286
|
+
// Validate all create/update records before sending
|
|
287
|
+
for (const write of input.writes) {
|
|
288
|
+
if (isCreateOrUpdate(write.$type) && write.value) {
|
|
289
|
+
const validationError = validateRecord(getLexiconArray(), write.collection, write.value);
|
|
290
|
+
if (validationError) {
|
|
291
|
+
throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const session = await getSession(viewer.did);
|
|
296
|
+
if (!session)
|
|
297
|
+
throw new ProxyError(401, 'No PDS session for user');
|
|
298
|
+
const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.applyWrites`;
|
|
299
|
+
const pdsBody = {
|
|
300
|
+
repo: viewer.did,
|
|
301
|
+
writes: input.writes.map((w) => ({ ...w, $type: toPdsWriteType(w.$type) })),
|
|
302
|
+
};
|
|
303
|
+
const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
|
|
304
|
+
if (!pdsRes.ok)
|
|
305
|
+
throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS applyWrites failed'));
|
|
306
|
+
// Index results locally
|
|
307
|
+
const results = pdsRes.body.results ?? [];
|
|
308
|
+
for (let i = 0; i < input.writes.length; i++) {
|
|
309
|
+
const write = input.writes[i];
|
|
310
|
+
const result = results[i];
|
|
311
|
+
try {
|
|
312
|
+
const mapped = toPdsWriteType(write.$type);
|
|
313
|
+
if (mapped === 'com.atproto.repo.applyWrites#create' && result?.uri && result?.cid && write.value) {
|
|
314
|
+
await insertRecord(write.collection, result.uri, result.cid, viewer.did, write.value);
|
|
315
|
+
await runLabelRules({
|
|
316
|
+
uri: result.uri,
|
|
317
|
+
cid: result.cid,
|
|
318
|
+
did: viewer.did,
|
|
319
|
+
collection: write.collection,
|
|
320
|
+
value: write.value,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else if (mapped === 'com.atproto.repo.applyWrites#update' && result?.uri && result?.cid && write.value) {
|
|
324
|
+
await insertRecord(write.collection, result.uri, result.cid, viewer.did, write.value);
|
|
325
|
+
await runLabelRules({
|
|
326
|
+
uri: result.uri,
|
|
327
|
+
cid: result.cid,
|
|
328
|
+
did: viewer.did,
|
|
329
|
+
collection: write.collection,
|
|
330
|
+
value: write.value,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else if (mapped === 'com.atproto.repo.applyWrites#delete' && write.rkey) {
|
|
334
|
+
const uri = `at://${viewer.did}/${write.collection}/${write.rkey}`;
|
|
335
|
+
await dbDeleteRecord(write.collection, uri);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
emit('pds-proxy', 'local_index_error', {
|
|
340
|
+
op: 'applyWrites',
|
|
341
|
+
error: err instanceof Error ? err.message : String(err),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return pdsRes.body;
|
|
346
|
+
}
|
|
347
|
+
export async function pdsUploadBlob(oauthConfig, viewer, body, contentType) {
|
|
348
|
+
const session = await getSession(viewer.did);
|
|
349
|
+
if (!session)
|
|
350
|
+
throw new ProxyError(401, 'No PDS session for user');
|
|
351
|
+
const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.uploadBlob`;
|
|
352
|
+
const pdsRes = await proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType);
|
|
353
|
+
if (!pdsRes.ok)
|
|
354
|
+
throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS upload failed'));
|
|
355
|
+
return pdsRes.body;
|
|
356
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function setPrivateCollections(list: string[]): void;
|
|
2
|
+
export declare function isPrivateCollection(nsid: string | null | undefined): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The collection segment of a record AT-URI, in either shape.
|
|
5
|
+
*
|
|
6
|
+
* A repo record is `at://{did}/{collection}/{rkey}`; a space record is
|
|
7
|
+
* `at://{authority}/space/{type}/{skey}/{writer}/{collection}/{rkey}`, where
|
|
8
|
+
* the position a collection used to occupy holds the literal 'space'. Reading
|
|
9
|
+
* it positionally without that distinction reports every space record as
|
|
10
|
+
* belonging to a collection named 'space', which no guard would match.
|
|
11
|
+
*/
|
|
12
|
+
export declare function collectionFromUri(uri: string): string | undefined;
|
|
13
|
+
//# sourceMappingURL=private-collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-collections.d.ts","sourceRoot":"","sources":["../src/private-collections.ts"],"names":[],"mappings":"AAWA,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { collectionFromRecordUri } from "./spaces/uri.js";
|
|
2
|
+
/**
|
|
3
|
+
* Collections that are indexed and queryable in-process but must never be
|
|
4
|
+
* served by the built-in dev.hatk.* record endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Held module-level, mirroring the schema registry, so the guard can be applied
|
|
7
|
+
* inside handlers without threading config through every signature.
|
|
8
|
+
*/
|
|
9
|
+
let privateCollections = new Set();
|
|
10
|
+
export function setPrivateCollections(list) {
|
|
11
|
+
privateCollections = new Set(list);
|
|
12
|
+
}
|
|
13
|
+
export function isPrivateCollection(nsid) {
|
|
14
|
+
return nsid != null && privateCollections.has(nsid);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The collection segment of a record AT-URI, in either shape.
|
|
18
|
+
*
|
|
19
|
+
* A repo record is `at://{did}/{collection}/{rkey}`; a space record is
|
|
20
|
+
* `at://{authority}/space/{type}/{skey}/{writer}/{collection}/{rkey}`, where
|
|
21
|
+
* the position a collection used to occupy holds the literal 'space'. Reading
|
|
22
|
+
* it positionally without that distinction reports every space record as
|
|
23
|
+
* belonging to a collection named 'space', which no guard would match.
|
|
24
|
+
*/
|
|
25
|
+
export function collectionFromUri(uri) {
|
|
26
|
+
return collectionFromRecordUri(uri);
|
|
27
|
+
}
|
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface ApnsConfig {
|
|
2
|
+
keyFile: string;
|
|
3
|
+
keyId: string;
|
|
4
|
+
teamId: string;
|
|
5
|
+
bundleId: string;
|
|
6
|
+
production?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Firebase Cloud Messaging, for Android devices.
|
|
10
|
+
*
|
|
11
|
+
* `keyFile` is a Google service-account JSON with the Firebase Messaging role,
|
|
12
|
+
* resolved like the APNs key: relative to the config file. The project id is
|
|
13
|
+
* read from that file unless overridden here.
|
|
14
|
+
*/
|
|
15
|
+
export interface FcmConfig {
|
|
16
|
+
keyFile: string;
|
|
17
|
+
projectId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PushConfig {
|
|
20
|
+
apns?: ApnsConfig;
|
|
21
|
+
fcm?: FcmConfig;
|
|
22
|
+
}
|
|
23
|
+
export interface PushPayload {
|
|
24
|
+
did: string;
|
|
25
|
+
title: string;
|
|
26
|
+
body: string;
|
|
27
|
+
data?: Record<string, string>;
|
|
28
|
+
collapseId?: string;
|
|
29
|
+
badge?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface PushInterface {
|
|
32
|
+
send: (payload: PushPayload) => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Initialize push with config. Must be called before send().
|
|
36
|
+
*
|
|
37
|
+
* Each transport is loaded on its own: a missing or malformed credential
|
|
38
|
+
* disables that platform and leaves the other one sending, rather than taking
|
|
39
|
+
* push down entirely.
|
|
40
|
+
*/
|
|
41
|
+
export declare function initPush(config: PushConfig, configDir: string): void;
|
|
42
|
+
/** Check if push is configured and at least one transport is usable. */
|
|
43
|
+
export declare function isPushEnabled(): boolean;
|
|
44
|
+
/** Which platforms this process can actually deliver to, for startup logging. */
|
|
45
|
+
export declare function enabledPushTransports(): string[];
|
|
46
|
+
/** Build the push interface injected into hook contexts. */
|
|
47
|
+
export declare function buildPushInterface(): PushInterface;
|
|
48
|
+
/** Register a push token for a DID. Upserts on conflict. */
|
|
49
|
+
export declare function registerToken(did: string, token: string, platform: string): Promise<void>;
|
|
50
|
+
/** Remove a push token. */
|
|
51
|
+
export declare function removeToken(token: string): Promise<void>;
|
|
52
|
+
/** Unregister a specific token for a DID. */
|
|
53
|
+
export declare function unregisterToken(did: string, token: string): Promise<void>;
|
|
54
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../src/push.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,GAAG,CAAC,EAAE,SAAS,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9C;AAsBD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CA4CpE;AAED,wEAAwE;AACxE,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAKhD;AAED,4DAA4D;AAC5D,wBAAgB,kBAAkB,IAAI,aAAa,CAElD;AAmSD,4DAA4D;AAC5D,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/F;AAED,2BAA2B;AAC3B,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED,6CAA6C;AAC7C,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/E"}
|