@hatk/hatk 0.0.1-alpha.11 → 0.0.1-alpha.111
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 +36 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +153 -37
- package/dist/car.js +1 -1
- package/dist/classifiers.d.ts +228 -0
- package/dist/classifiers.d.ts.map +1 -0
- package/dist/classifiers.js +688 -0
- package/dist/cli.js +248 -1043
- package/dist/config.d.ts +179 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +78 -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/database/db.d.ts +290 -0
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +2073 -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/database/schema.d.ts +91 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +153 -54
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +131 -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 +55 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +194 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +627 -92
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +238 -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 +212 -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 +27 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +80 -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 +131 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +504 -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 +92 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +396 -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 +22 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +92 -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 +75 -0
- package/dist/server.d.ts +46 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1137 -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 +112 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +460 -0
- package/dist/spaces/credential.d.ts +82 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +220 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +563 -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 +93 -0
- package/dist/spaces/index.d.ts +56 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +134 -0
- package/dist/spaces/notify.d.ts +60 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +128 -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/signature.d.ts +36 -0
- package/dist/spaces/signature.d.ts.map +1 -0
- package/dist/spaces/signature.js +83 -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 +46 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +94 -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 +143 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +282 -55
- package/package.json +29 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +652 -61
- package/dist/db.d.ts +0 -135
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1365
- 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 +0 -51
- 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
package/dist/xrpc.d.ts
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
|
+
import { type SpaceCredential } from './spaces/credential.ts';
|
|
2
|
+
import type { BaseContext } from './hydrate.ts';
|
|
1
3
|
import type { Row, FlatRow } from './lex-types.ts';
|
|
4
|
+
import type { OAuthConfig, CdnConfig } from './config.ts';
|
|
5
|
+
import type { PdsXrpcOptions } from './pds-proxy.ts';
|
|
6
|
+
import { type ObtainedSession } from './oauth/server.ts';
|
|
2
7
|
export type { Row, FlatRow };
|
|
8
|
+
/** Set the OAuth config used for record write helpers. Called once during boot. */
|
|
9
|
+
export declare function configureOAuth(config: OAuthConfig | null): void;
|
|
10
|
+
/** Thrown from XRPC handlers to return a 400 response with an error message. */
|
|
3
11
|
export declare class InvalidRequestError extends Error {
|
|
4
12
|
status: number;
|
|
5
13
|
errorName?: string;
|
|
6
14
|
constructor(message: string, errorName?: string);
|
|
7
15
|
}
|
|
16
|
+
/** Thrown from XRPC handlers to return a 404 response. */
|
|
8
17
|
export declare class NotFoundError extends InvalidRequestError {
|
|
9
18
|
status: number;
|
|
10
19
|
constructor(message?: string);
|
|
11
20
|
}
|
|
12
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Context passed to every XRPC handler. Provides database access, pagination
|
|
23
|
+
* helpers, viewer auth, record resolution, full-text search, label queries,
|
|
24
|
+
* and blob URL generation.
|
|
25
|
+
*
|
|
26
|
+
* @typeParam P - Query parameter types (derived from lexicon)
|
|
27
|
+
* @typeParam Records - Map of collection NSID → record type (from generated types)
|
|
28
|
+
* @typeParam I - Input body type for procedure calls
|
|
29
|
+
*/
|
|
30
|
+
export interface XrpcContext<P = Record<string, string>, Records extends Record<string, any> = Record<string, any>, I = unknown> extends BaseContext {
|
|
13
31
|
db: {
|
|
14
|
-
query: (sql: string, params?:
|
|
15
|
-
run: (sql: string,
|
|
32
|
+
query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
33
|
+
run: (sql: string, params?: unknown[]) => Promise<void>;
|
|
34
|
+
unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
16
35
|
};
|
|
17
36
|
params: P;
|
|
18
37
|
input: I;
|
|
19
38
|
cursor?: string;
|
|
20
39
|
limit: number;
|
|
21
|
-
viewer: {
|
|
22
|
-
did: string;
|
|
23
|
-
} | null;
|
|
24
40
|
packCursor: (primary: string | number, cid: string) => string;
|
|
25
41
|
unpackCursor: (cursor: string) => {
|
|
26
42
|
primary: string;
|
|
@@ -37,17 +53,132 @@ export interface XrpcContext<P = Record<string, string>, Records extends Record<
|
|
|
37
53
|
cursor?: string;
|
|
38
54
|
}>;
|
|
39
55
|
resolve: <R = unknown>(uris: string[]) => Promise<Row<R>[]>;
|
|
40
|
-
lookup: <R = any>(collection: string, field: string, values: string[]) => Promise<Map<string, Row<R>>>;
|
|
41
|
-
count: (collection: string, field: string, values: string[]) => Promise<Map<string, number>>;
|
|
42
56
|
exists: (collection: string, filters: Record<string, string>) => Promise<boolean>;
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
createRecord: (collection: string, record: Record<string, unknown>, opts?: {
|
|
58
|
+
rkey?: string;
|
|
59
|
+
}) => Promise<{
|
|
60
|
+
uri?: string;
|
|
61
|
+
cid?: string;
|
|
62
|
+
}>;
|
|
63
|
+
putRecord: (collection: string, rkey: string, record: Record<string, unknown>) => Promise<{
|
|
64
|
+
uri?: string;
|
|
65
|
+
cid?: string;
|
|
66
|
+
}>;
|
|
67
|
+
deleteRecord: (collection: string, rkey: string) => Promise<void>;
|
|
68
|
+
applyWrites: (writes: Array<{
|
|
69
|
+
$type: string;
|
|
70
|
+
collection: string;
|
|
71
|
+
rkey?: string;
|
|
72
|
+
value?: Record<string, unknown>;
|
|
73
|
+
}>) => Promise<{
|
|
74
|
+
results?: Array<{
|
|
75
|
+
$type: string;
|
|
76
|
+
uri?: string;
|
|
77
|
+
cid?: string;
|
|
78
|
+
}>;
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* Call any XRPC method on the viewer's PDS with their session. The escape
|
|
82
|
+
* hatch beside the record helpers above: nothing is validated or indexed, and
|
|
83
|
+
* the granted scopes still apply. See {@link pdsXrpc}.
|
|
84
|
+
*/
|
|
85
|
+
pds: (nsid: string, options?: PdsXrpcOptions) => Promise<Record<string, unknown>>;
|
|
86
|
+
/**
|
|
87
|
+
* The viewer's own credential for a space, minted with their session and
|
|
88
|
+
* cached until shortly before it expires. Its `fetch` presents it to any
|
|
89
|
+
* host in the space, signed for the repo a request names (or the authority).
|
|
90
|
+
*
|
|
91
|
+
* For reads an app makes itself, as the viewer, that the index does not
|
|
92
|
+
* serve. Throws a `SpaceCredentialError` — `status` and `code` are the
|
|
93
|
+
* authority's — when the viewer may not read the space, and with status 401
|
|
94
|
+
* when there is no viewer. Pass `refresh` after a host refuses a credential
|
|
95
|
+
* that had not yet expired.
|
|
96
|
+
*/
|
|
97
|
+
spaceCredential: (space: string, opts?: {
|
|
98
|
+
refresh?: boolean;
|
|
99
|
+
}) => Promise<SpaceCredential>;
|
|
100
|
+
/**
|
|
101
|
+
* Drop the viewer's cached credential for a space, after something that may
|
|
102
|
+
* have changed their standing in it, such as leaving. Does nothing without a
|
|
103
|
+
* viewer.
|
|
104
|
+
*/
|
|
105
|
+
forgetSpaceCredential: (space: string) => void;
|
|
106
|
+
/**
|
|
107
|
+
* The record helpers and `pds` above, acting as another account this app
|
|
108
|
+
* holds a session for — one it obtained with {@link obtainSession}, say.
|
|
109
|
+
* Nothing here checks that the viewer may act for that account: that is the
|
|
110
|
+
* app's decision, made before calling.
|
|
111
|
+
*/
|
|
112
|
+
asAccount: (did: string) => AccountHelpers;
|
|
113
|
+
/**
|
|
114
|
+
* Obtain a session for another account straight from its authorization
|
|
115
|
+
* server, outside the redirect flow — a group host creating an account for
|
|
116
|
+
* this app, for instance. POSTs `body` to `url` as this app's OAuth client,
|
|
117
|
+
* with DPoP, and stores the session the response carries for its `sub`, so
|
|
118
|
+
* {@link asAccount} works for it afterwards. See `obtainSession` in
|
|
119
|
+
* `oauth/server.ts`.
|
|
120
|
+
*/
|
|
121
|
+
obtainSession: (url: string, body: Record<string, unknown>, opts?: {
|
|
122
|
+
headers?: Record<string, string>;
|
|
123
|
+
}) => Promise<ObtainedSession>;
|
|
45
124
|
}
|
|
125
|
+
/** The PDS helpers on {@link XrpcContext}, for one account. */
|
|
126
|
+
export type AccountHelpers = Pick<XrpcContext, 'createRecord' | 'putRecord' | 'deleteRecord' | 'applyWrites' | 'pds'>;
|
|
127
|
+
/** Set the relay URL used for blob URL generation. Called once during boot. */
|
|
128
|
+
/** True when the relay is a loopback URL — a dev PDS rather than the network. */
|
|
129
|
+
export declare function isLocalRelay(): boolean;
|
|
46
130
|
export declare function configureRelay(relay: string): void;
|
|
47
|
-
|
|
131
|
+
/** Set the CDN config for imgproxy URL signing. Called once during boot. */
|
|
132
|
+
export declare function configureCdn(cdn: CdnConfig | null): void;
|
|
133
|
+
/**
|
|
134
|
+
* Generate a CDN URL for a blob ref. Uses the PDS directly in local dev,
|
|
135
|
+
* a configured imgproxy CDN if available, or the Bluesky CDN as fallback.
|
|
136
|
+
*/
|
|
137
|
+
export declare function blobUrl(did: string, ref: unknown, preset?: string): string | undefined;
|
|
138
|
+
/** Build a full XrpcContext from request parameters. Reuses buildBaseContext for shared fields. */
|
|
139
|
+
export declare function buildXrpcContext(params: XrpcParams, cursor: string | undefined, limit: number, viewer: {
|
|
140
|
+
did: string;
|
|
141
|
+
handle?: string;
|
|
142
|
+
} | null, input?: unknown): XrpcContext;
|
|
143
|
+
/**
|
|
144
|
+
* Discover and load XRPC handler modules from the `xrpc/` directory.
|
|
145
|
+
* Directory nesting maps to NSID segments. Parameters are validated and
|
|
146
|
+
* coerced against the matching lexicon definition.
|
|
147
|
+
*/
|
|
48
148
|
export declare function initXrpc(xrpcDir: string): Promise<void>;
|
|
49
|
-
|
|
149
|
+
/** Register a single XRPC handler from a scanned server/ module. */
|
|
150
|
+
export declare function registerXrpcHandler(nsid: string, handlerModule: {
|
|
151
|
+
handler: (ctx: any) => Promise<any>;
|
|
152
|
+
}): void;
|
|
153
|
+
/**
|
|
154
|
+
* What the query string gives a handler: one value per key, or every value
|
|
155
|
+
* when the key was repeated (`?dids=a&dids=b`).
|
|
156
|
+
*/
|
|
157
|
+
export type XrpcParams = Record<string, any>;
|
|
158
|
+
/** Collect a query string into params, keeping every value of a repeated key. */
|
|
159
|
+
export declare function paramsFromSearch(search: URLSearchParams): XrpcParams;
|
|
160
|
+
/**
|
|
161
|
+
* Apply the lexicon's parameter schema before a handler runs: defaults,
|
|
162
|
+
* integer coercion, and arrays. A query string cannot say whether
|
|
163
|
+
* `dids=x` is one string or a one-element list, so the schema decides: a
|
|
164
|
+
* parameter declared `array` always reaches the handler as one, or a handler
|
|
165
|
+
* that spreads it would spread the characters of a DID instead.
|
|
166
|
+
*/
|
|
167
|
+
export declare function coerceParams(params: XrpcParams, properties: Record<string, any>, required: string[]): void;
|
|
168
|
+
/** Execute a registered XRPC handler by name. Returns null if no handler matches. */
|
|
169
|
+
export declare function executeXrpc(name: string, params: XrpcParams, cursor: string | undefined, limit: number, viewer?: {
|
|
50
170
|
did: string;
|
|
51
171
|
} | null, input?: unknown): Promise<any | null>;
|
|
172
|
+
/** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
|
|
173
|
+
export declare function callXrpc(nsid: string, params?: Record<string, any>, input?: unknown): Promise<any>;
|
|
174
|
+
/**
|
|
175
|
+
* Register a core XRPC handler directly (no XrpcContext wrapping).
|
|
176
|
+
* Used for built-in dev.hatk.* handlers that manage their own dependencies.
|
|
177
|
+
*/
|
|
178
|
+
export declare function registerCoreXrpcHandler(nsid: string, fn: (params: XrpcParams, cursor: string | undefined, limit: number, viewer: {
|
|
179
|
+
did: string;
|
|
180
|
+
handle?: string;
|
|
181
|
+
} | null, input?: unknown) => Promise<any>): void;
|
|
182
|
+
/** Return all registered XRPC method names. */
|
|
52
183
|
export declare function listXrpc(): string[];
|
|
53
184
|
//# sourceMappingURL=xrpc.d.ts.map
|
package/dist/xrpc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
|
package/dist/xrpc.js
CHANGED
|
@@ -6,12 +6,43 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* XRPC method handler system for serving AT Protocol endpoints.
|
|
11
|
+
*
|
|
12
|
+
* Place handler modules in the `xrpc/` directory, nested by NSID segments
|
|
13
|
+
* (e.g. `xrpc/app/bsky/feed/getAuthorFeed.ts` → `app.bsky.feed.getAuthorFeed`).
|
|
14
|
+
* Each module default-exports a `{ handler }` function that receives an
|
|
15
|
+
* {@link XrpcContext} with database access, query params, pagination, and
|
|
16
|
+
* viewer auth.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* // xrpc/xyz/statusphere/getStatuses.ts
|
|
21
|
+
* import { defineXrpc } from '../../hatk.generated.ts'
|
|
22
|
+
*
|
|
23
|
+
* export default defineXrpc('xyz.statusphere.getStatuses', async (ctx) => {
|
|
24
|
+
* const rows = await ctx.db.query('SELECT * FROM statusphere_status LIMIT ?', [ctx.limit])
|
|
25
|
+
* return { statuses: rows }
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
|
|
30
|
+
import { forgetReaderSpaceCredential, readerSpaceCredential, SpaceCredentialError, } from "./spaces/credential.js";
|
|
9
31
|
import { resolve, relative } from 'node:path';
|
|
10
32
|
import { readdirSync, statSync } from 'node:fs';
|
|
11
|
-
import { log } from "./logger.js";
|
|
12
|
-
import {
|
|
13
|
-
import { resolveRecords } from "./hydrate.js";
|
|
14
|
-
import { getLexicon } from "./schema.js";
|
|
33
|
+
import { log, emit, timer } from "./logger.js";
|
|
34
|
+
import { runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, } from "./database/db.js";
|
|
35
|
+
import { resolveRecords, buildBaseContext } from "./hydrate.js";
|
|
36
|
+
import { getLexicon } from "./database/schema.js";
|
|
37
|
+
import { createHmac } from 'node:crypto';
|
|
38
|
+
import { pdsCreateRecord, pdsPutRecord, pdsDeleteRecord, pdsApplyWrites, pdsXrpc } from "./pds-proxy.js";
|
|
39
|
+
import { obtainSession, ObtainSessionError } from "./oauth/server.js";
|
|
40
|
+
let _oauthConfig = null;
|
|
41
|
+
/** Set the OAuth config used for record write helpers. Called once during boot. */
|
|
42
|
+
export function configureOAuth(config) {
|
|
43
|
+
_oauthConfig = config;
|
|
44
|
+
}
|
|
45
|
+
/** Thrown from XRPC handlers to return a 400 response with an error message. */
|
|
15
46
|
export class InvalidRequestError extends Error {
|
|
16
47
|
status = 400;
|
|
17
48
|
errorName;
|
|
@@ -20,6 +51,7 @@ export class InvalidRequestError extends Error {
|
|
|
20
51
|
this.errorName = errorName;
|
|
21
52
|
}
|
|
22
53
|
}
|
|
54
|
+
/** Thrown from XRPC handlers to return a 404 response. */
|
|
23
55
|
export class NotFoundError extends InvalidRequestError {
|
|
24
56
|
status = 404;
|
|
25
57
|
constructor(message = 'Not found') {
|
|
@@ -27,21 +59,149 @@ export class NotFoundError extends InvalidRequestError {
|
|
|
27
59
|
}
|
|
28
60
|
}
|
|
29
61
|
let _relayUrl = '';
|
|
62
|
+
let _cdn = null;
|
|
63
|
+
/** Set the relay URL used for blob URL generation. Called once during boot. */
|
|
64
|
+
/** True when the relay is a loopback URL — a dev PDS rather than the network. */
|
|
65
|
+
export function isLocalRelay() {
|
|
66
|
+
try {
|
|
67
|
+
const host = new URL(_relayUrl).hostname;
|
|
68
|
+
return (host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1' || host.endsWith('.localhost'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
30
74
|
export function configureRelay(relay) {
|
|
31
75
|
_relayUrl = relay;
|
|
32
76
|
}
|
|
77
|
+
/** Set the CDN config for imgproxy URL signing. Called once during boot. */
|
|
78
|
+
export function configureCdn(cdn) {
|
|
79
|
+
if (cdn) {
|
|
80
|
+
_cdn = {
|
|
81
|
+
url: cdn.url.replace(/\/$/, ''),
|
|
82
|
+
key: Buffer.from(cdn.key, 'hex'),
|
|
83
|
+
salt: Buffer.from(cdn.salt, 'hex'),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
_cdn = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Sign an imgproxy path with HMAC-SHA256 (URL-safe base64). */
|
|
91
|
+
function signPath(path) {
|
|
92
|
+
const hmac = createHmac('sha256', _cdn.key);
|
|
93
|
+
hmac.update(_cdn.salt);
|
|
94
|
+
hmac.update(path);
|
|
95
|
+
const sig = hmac.digest('base64url');
|
|
96
|
+
return `/${sig}${path}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate a CDN URL for a blob ref. Uses the PDS directly in local dev,
|
|
100
|
+
* a configured imgproxy CDN if available, or the Bluesky CDN as fallback.
|
|
101
|
+
*/
|
|
33
102
|
export function blobUrl(did, ref, preset = 'avatar') {
|
|
34
103
|
if (!ref)
|
|
35
104
|
return undefined;
|
|
36
105
|
const p = typeof ref === 'string' ? JSON.parse(ref) : ref;
|
|
37
106
|
if (!p?.ref?.$link)
|
|
38
107
|
return undefined;
|
|
39
|
-
|
|
40
|
-
|
|
108
|
+
// A local network has no image CDN. Serve blobs through the dev proxy
|
|
109
|
+
// (see /blob/ in server.ts), which fetches from each repo's own PDS — on a
|
|
110
|
+
// multi-PDS dev stack the repos are not all behind the relay being tailed.
|
|
111
|
+
if (isLocalRelay()) {
|
|
112
|
+
return `/blob/${did}/${p.ref.$link}`;
|
|
113
|
+
}
|
|
114
|
+
if (_cdn) {
|
|
115
|
+
const path = `/${preset}/plain/${did}/${p.ref.$link}`;
|
|
116
|
+
return `${_cdn.url}${signPath(path)}`;
|
|
41
117
|
}
|
|
42
118
|
return `https://cdn.bsky.app/img/${preset}/plain/${did}/${p.ref.$link}@jpeg`;
|
|
43
119
|
}
|
|
120
|
+
/** Build a full XrpcContext from request parameters. Reuses buildBaseContext for shared fields. */
|
|
121
|
+
export function buildXrpcContext(params, cursor, limit, viewer, input) {
|
|
122
|
+
const base = buildBaseContext(viewer);
|
|
123
|
+
return {
|
|
124
|
+
...base,
|
|
125
|
+
db: { query: guardedQuerySQL, run: runSQL, unfiltered: unfilteredQuerySQL },
|
|
126
|
+
params,
|
|
127
|
+
input: input || {},
|
|
128
|
+
cursor,
|
|
129
|
+
limit,
|
|
130
|
+
packCursor,
|
|
131
|
+
unpackCursor,
|
|
132
|
+
isTakendown: isTakendownDid,
|
|
133
|
+
filterTakendownDids,
|
|
134
|
+
search: searchRecords,
|
|
135
|
+
resolve: resolveRecords,
|
|
136
|
+
exists: async (collection, filters) => {
|
|
137
|
+
const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
|
|
138
|
+
const uri = await findUriByFields(collection, conditions);
|
|
139
|
+
return uri !== null;
|
|
140
|
+
},
|
|
141
|
+
...accountHelpers(viewer),
|
|
142
|
+
spaceCredential: async (space, opts) => {
|
|
143
|
+
if (!_oauthConfig)
|
|
144
|
+
throw new Error('No OAuth config — cannot read a space');
|
|
145
|
+
if (!viewer)
|
|
146
|
+
throw new SpaceCredentialError(401, 'AuthRequired', 'Authentication required to read a space');
|
|
147
|
+
return readerSpaceCredential(_oauthConfig, viewer.did, space, opts);
|
|
148
|
+
},
|
|
149
|
+
forgetSpaceCredential: (space) => {
|
|
150
|
+
if (viewer)
|
|
151
|
+
forgetReaderSpaceCredential(viewer.did, space);
|
|
152
|
+
},
|
|
153
|
+
asAccount: (did) => accountHelpers({ did }),
|
|
154
|
+
obtainSession: async (url, body, opts) => {
|
|
155
|
+
if (!_oauthConfig)
|
|
156
|
+
throw new Error('No OAuth config — cannot obtain a session');
|
|
157
|
+
try {
|
|
158
|
+
return await obtainSession(_oauthConfig, url, body, opts);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (err instanceof ObtainSessionError)
|
|
162
|
+
throw new InvalidRequestError(err.message, err.error);
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The PDS helpers for one account, with the session this app holds for it: the
|
|
170
|
+
* viewer's, or another account's through {@link XrpcContext.asAccount}.
|
|
171
|
+
*/
|
|
172
|
+
function accountHelpers(account) {
|
|
173
|
+
const ready = (what) => {
|
|
174
|
+
if (!_oauthConfig)
|
|
175
|
+
throw new Error(`No OAuth config — cannot ${what}`);
|
|
176
|
+
if (!account)
|
|
177
|
+
throw new Error(`Authentication required to ${what}`);
|
|
178
|
+
return { config: _oauthConfig, account };
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
createRecord: async (collection, record, opts) => {
|
|
182
|
+
const { config, account } = ready('write records');
|
|
183
|
+
return pdsCreateRecord(config, account, { collection, record, rkey: opts?.rkey });
|
|
184
|
+
},
|
|
185
|
+
putRecord: async (collection, rkey, record) => {
|
|
186
|
+
const { config, account } = ready('write records');
|
|
187
|
+
return pdsPutRecord(config, account, { collection, rkey, record });
|
|
188
|
+
},
|
|
189
|
+
deleteRecord: async (collection, rkey) => {
|
|
190
|
+
const { config, account } = ready('write records');
|
|
191
|
+
await pdsDeleteRecord(config, account, { collection, rkey });
|
|
192
|
+
},
|
|
193
|
+
applyWrites: async (writes) => {
|
|
194
|
+
const { config, account } = ready('write records');
|
|
195
|
+
return pdsApplyWrites(config, account, { writes });
|
|
196
|
+
},
|
|
197
|
+
pds: async (nsid, options) => {
|
|
198
|
+
const { config, account } = ready('call the PDS');
|
|
199
|
+
return pdsXrpc(config, account, nsid, options);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
44
203
|
const handlers = new Map();
|
|
204
|
+
/** Recursively collect .ts/.js files in a directory, skipping files prefixed with `_`. */
|
|
45
205
|
function walkDir(dir) {
|
|
46
206
|
const results = [];
|
|
47
207
|
try {
|
|
@@ -58,6 +218,11 @@ function walkDir(dir) {
|
|
|
58
218
|
catch { }
|
|
59
219
|
return results.sort();
|
|
60
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Discover and load XRPC handler modules from the `xrpc/` directory.
|
|
223
|
+
* Directory nesting maps to NSID segments. Parameters are validated and
|
|
224
|
+
* coerced against the matching lexicon definition.
|
|
225
|
+
*/
|
|
61
226
|
export async function initXrpc(xrpcDir) {
|
|
62
227
|
const files = walkDir(xrpcDir);
|
|
63
228
|
if (files.length === 0)
|
|
@@ -65,7 +230,7 @@ export async function initXrpc(xrpcDir) {
|
|
|
65
230
|
for (const scriptPath of files) {
|
|
66
231
|
const rel = relative(xrpcDir, scriptPath).replace(/\.(ts|js)$/, '');
|
|
67
232
|
const name = rel.replace(/[\\/]/g, '.');
|
|
68
|
-
const mod = await import(__rewriteRelativeImportExtension(scriptPath));
|
|
233
|
+
const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
|
|
69
234
|
const handler = mod.default;
|
|
70
235
|
// Extract param schema from lexicon for validation and defaults
|
|
71
236
|
const lexicon = getLexicon(name);
|
|
@@ -75,65 +240,127 @@ export async function initXrpc(xrpcDir) {
|
|
|
75
240
|
handlers.set(name, {
|
|
76
241
|
name,
|
|
77
242
|
execute: async (params, cursor, limit, viewer, input) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (params[key] == null && def.default != null) {
|
|
81
|
-
params[key] = String(def.default);
|
|
82
|
-
}
|
|
83
|
-
if (params[key] != null && def.type === 'integer') {
|
|
84
|
-
params[key] = Number(params[key]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
for (const param of requiredParams) {
|
|
88
|
-
if (!params[param]) {
|
|
89
|
-
throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const ctx = {
|
|
93
|
-
db: { query: querySQL, run: runSQL },
|
|
94
|
-
params,
|
|
95
|
-
input: input || {},
|
|
96
|
-
cursor,
|
|
97
|
-
limit,
|
|
98
|
-
viewer,
|
|
99
|
-
packCursor,
|
|
100
|
-
unpackCursor,
|
|
101
|
-
isTakendown: isTakendownDid,
|
|
102
|
-
filterTakendownDids,
|
|
103
|
-
search: searchRecords,
|
|
104
|
-
resolve: resolveRecords,
|
|
105
|
-
lookup: async (collection, field, values) => {
|
|
106
|
-
if (values.length === 0)
|
|
107
|
-
return new Map();
|
|
108
|
-
const unique = [...new Set(values.filter(Boolean))];
|
|
109
|
-
return lookupByFieldBatch(collection, field, unique);
|
|
110
|
-
},
|
|
111
|
-
count: async (collection, field, values) => {
|
|
112
|
-
if (values.length === 0)
|
|
113
|
-
return new Map();
|
|
114
|
-
const unique = [...new Set(values.filter(Boolean))];
|
|
115
|
-
return countByFieldBatch(collection, field, unique);
|
|
116
|
-
},
|
|
117
|
-
exists: async (collection, filters) => {
|
|
118
|
-
const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
|
|
119
|
-
const uri = await findUriByFields(collection, conditions);
|
|
120
|
-
return uri !== null;
|
|
121
|
-
},
|
|
122
|
-
labels: queryLabelsForUris,
|
|
123
|
-
blobUrl,
|
|
124
|
-
};
|
|
243
|
+
coerceParams(params, paramProperties, requiredParams);
|
|
244
|
+
const ctx = buildXrpcContext(params, cursor, limit, viewer, input);
|
|
125
245
|
return handler.handler(ctx);
|
|
126
246
|
},
|
|
127
247
|
});
|
|
128
248
|
log(`[xrpc] discovered: ${name}`);
|
|
129
249
|
}
|
|
130
250
|
}
|
|
251
|
+
/** Register a single XRPC handler from a scanned server/ module. */
|
|
252
|
+
export function registerXrpcHandler(nsid, handlerModule) {
|
|
253
|
+
const lexicon = getLexicon(nsid);
|
|
254
|
+
const paramsDef = lexicon?.defs?.main?.parameters;
|
|
255
|
+
const requiredParams = paramsDef?.required || [];
|
|
256
|
+
const paramProperties = paramsDef?.properties || {};
|
|
257
|
+
handlers.set(nsid, {
|
|
258
|
+
name: nsid,
|
|
259
|
+
execute: async (params, cursor, limit, viewer, input) => {
|
|
260
|
+
coerceParams(params, paramProperties, requiredParams);
|
|
261
|
+
const ctx = buildXrpcContext(params, cursor, limit, viewer, input);
|
|
262
|
+
return handlerModule.handler(ctx);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** Collect a query string into params, keeping every value of a repeated key. */
|
|
267
|
+
export function paramsFromSearch(search) {
|
|
268
|
+
const params = {};
|
|
269
|
+
for (const [key, value] of search) {
|
|
270
|
+
const prior = params[key];
|
|
271
|
+
if (prior === undefined)
|
|
272
|
+
params[key] = value;
|
|
273
|
+
else if (Array.isArray(prior))
|
|
274
|
+
prior.push(value);
|
|
275
|
+
else
|
|
276
|
+
params[key] = [prior, value];
|
|
277
|
+
}
|
|
278
|
+
return params;
|
|
279
|
+
}
|
|
280
|
+
function coerceScalar(value, def) {
|
|
281
|
+
// Only integers, as before: a boolean still reaches the handler as the
|
|
282
|
+
// string it was sent as, and handlers written against that keep working.
|
|
283
|
+
return def?.type === 'integer' ? Number(value) : value;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Apply the lexicon's parameter schema before a handler runs: defaults,
|
|
287
|
+
* integer coercion, and arrays. A query string cannot say whether
|
|
288
|
+
* `dids=x` is one string or a one-element list, so the schema decides: a
|
|
289
|
+
* parameter declared `array` always reaches the handler as one, or a handler
|
|
290
|
+
* that spreads it would spread the characters of a DID instead.
|
|
291
|
+
*/
|
|
292
|
+
export function coerceParams(params, properties, required) {
|
|
293
|
+
for (const [key, def] of Object.entries(properties)) {
|
|
294
|
+
if (params[key] == null && def.default != null) {
|
|
295
|
+
params[key] = def.type === 'array' ? [...def.default] : def.type === 'string' ? String(def.default) : def.default;
|
|
296
|
+
}
|
|
297
|
+
if (params[key] == null)
|
|
298
|
+
continue;
|
|
299
|
+
if (def.type === 'array') {
|
|
300
|
+
const list = Array.isArray(params[key]) ? params[key] : [params[key]];
|
|
301
|
+
params[key] = list.map((v) => coerceScalar(v, def.items));
|
|
302
|
+
}
|
|
303
|
+
else if (Array.isArray(params[key])) {
|
|
304
|
+
// A scalar given twice: the last one wins, as a form submission would.
|
|
305
|
+
params[key] = coerceScalar(params[key][params[key].length - 1], def);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
params[key] = coerceScalar(params[key], def);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const param of required) {
|
|
312
|
+
const v = params[param];
|
|
313
|
+
if (v == null || v === '' || (Array.isArray(v) && v.length === 0)) {
|
|
314
|
+
throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** Execute a registered XRPC handler by name. Returns null if no handler matches. */
|
|
131
319
|
export async function executeXrpc(name, params, cursor, limit, viewer, input) {
|
|
132
320
|
const handler = handlers.get(name);
|
|
133
321
|
if (!handler)
|
|
134
322
|
return null;
|
|
135
|
-
|
|
323
|
+
const elapsed = timer();
|
|
324
|
+
try {
|
|
325
|
+
const result = await handler.execute(params, cursor, limit, viewer || null, input);
|
|
326
|
+
emit('xrpc', name, { duration_ms: elapsed(), params, cursor, limit, viewer: viewer?.did });
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
emit('xrpc', name, { duration_ms: elapsed(), params, cursor, limit, viewer: viewer?.did, error: err.message });
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
|
|
335
|
+
export async function callXrpc(nsid, params = {}, input) {
|
|
336
|
+
const viewer = globalThis.__hatk_viewer ?? null;
|
|
337
|
+
// In externalized module context (e.g. SSR), delegate to the runner's callXrpc via globalThis.
|
|
338
|
+
// The runner's module instance has all registered handlers; this (Node's) instance may not.
|
|
339
|
+
if (handlers.size === 0 && globalThis.__hatk_callXrpc) {
|
|
340
|
+
return globalThis.__hatk_callXrpc(nsid, params, input);
|
|
341
|
+
}
|
|
342
|
+
const stringParams = {};
|
|
343
|
+
for (const [k, v] of Object.entries(params)) {
|
|
344
|
+
if (v == null)
|
|
345
|
+
continue;
|
|
346
|
+
// A list stays a list, as the query string would have carried it.
|
|
347
|
+
stringParams[k] = Array.isArray(v) ? v.map(String) : String(v);
|
|
348
|
+
}
|
|
349
|
+
const limit = params.limit ? Number(params.limit) : 20;
|
|
350
|
+
const cursor = params.cursor ?? undefined;
|
|
351
|
+
const result = await executeXrpc(nsid, stringParams, cursor, limit, viewer, input);
|
|
352
|
+
if (result === null)
|
|
353
|
+
throw new Error(`No XRPC handler registered for ${nsid}`);
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Register a core XRPC handler directly (no XrpcContext wrapping).
|
|
358
|
+
* Used for built-in dev.hatk.* handlers that manage their own dependencies.
|
|
359
|
+
*/
|
|
360
|
+
export function registerCoreXrpcHandler(nsid, fn) {
|
|
361
|
+
handlers.set(nsid, { name: nsid, execute: fn });
|
|
136
362
|
}
|
|
363
|
+
/** Return all registered XRPC method names. */
|
|
137
364
|
export function listXrpc() {
|
|
138
365
|
return Array.from(handlers.keys());
|
|
139
366
|
}
|