@hatk/hatk 0.0.1-alpha.94 → 0.0.1-alpha.96
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/classifiers.d.ts +168 -0
- package/dist/classifiers.d.ts.map +1 -0
- package/dist/classifiers.js +370 -0
- package/dist/cli.js +1 -0
- package/dist/config.d.ts +14 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/database/db.d.ts +78 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +253 -1
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +1 -1
- package/dist/database/schema.d.ts +27 -0
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +38 -21
- package/dist/dev-entry.d.ts.map +1 -1
- package/dist/dev-entry.js +3 -4
- package/dist/hydrate.d.ts +8 -2
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/main.js +8 -5
- package/dist/oauth/server.d.ts +36 -0
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +78 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +95 -6
- package/dist/spaces/blob.d.ts +67 -6
- package/dist/spaces/blob.d.ts.map +1 -1
- package/dist/spaces/blob.js +371 -22
- package/dist/spaces/index.d.ts +1 -1
- package/dist/spaces/index.d.ts.map +1 -1
- package/dist/spaces/index.js +1 -1
- package/dist/xrpc.d.ts +21 -0
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +40 -25
- package/package.json +10 -2
- package/public/admin.html +467 -0
package/dist/hydrate.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SpaceBlobPreset } from './spaces/blob.ts';
|
|
1
2
|
import type { Row } from './lex-types.ts';
|
|
2
3
|
export type { Row };
|
|
3
4
|
export interface BaseContext {
|
|
@@ -53,8 +54,13 @@ export interface BaseContext {
|
|
|
53
54
|
records: <R = unknown>(collection: string, field: string, values: string[]) => Promise<Row<R>[]>;
|
|
54
55
|
/** Throw `NotAuthorized` unless the viewer may read this space. */
|
|
55
56
|
requireSpace: (space: string) => void;
|
|
56
|
-
/**
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The URL this appview serves a space blob at, for the viewer asking.
|
|
59
|
+
*
|
|
60
|
+
* The preset names the size it is drawn at, from the same list the public
|
|
61
|
+
* path uses. Left out, the original comes back.
|
|
62
|
+
*/
|
|
63
|
+
spaceBlobUrl: (space: string, repo: string, cid: string, preset?: SpaceBlobPreset) => string;
|
|
58
64
|
}
|
|
59
65
|
/** Fetch records for URIs, reshape them, and filter out taken-down DIDs. */
|
|
60
66
|
export declare function resolveRecords(uris: string[]): Promise<Row<unknown>[]>;
|
package/dist/hydrate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"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"}
|
package/dist/main.js
CHANGED
|
@@ -14,7 +14,7 @@ import { log } from "./logger.js";
|
|
|
14
14
|
import { loadConfig } from "./config.js";
|
|
15
15
|
import { validatableLexicons, loadLexicons, storeLexicons, discoverCollections, buildSchemas, } from "./database/schema.js";
|
|
16
16
|
import { discoverViews } from "./views.js";
|
|
17
|
-
import { initDatabase, getCursor, querySQL, getSqlDialect, getSchemaDump
|
|
17
|
+
import { initDatabase, getCursor, querySQL, getSqlDialect, getSchemaDump } from "./database/db.js";
|
|
18
18
|
import { createAdapter } from "./database/adapter-factory.js";
|
|
19
19
|
import { getDialect } from "./database/dialect.js";
|
|
20
20
|
import { setSearchPort } from "./database/fts.js";
|
|
@@ -22,6 +22,7 @@ import { initFeeds, listFeeds } from "./feeds.js";
|
|
|
22
22
|
import { initXrpc, listXrpc, configureRelay, configureCdn, configureOAuth, callXrpc } from "./xrpc.js";
|
|
23
23
|
import { initOpengraph } from "./opengraph.js";
|
|
24
24
|
import { initLabels, getLabelDefinitions } from "./labels.js";
|
|
25
|
+
import { initClassifiers, listClassifiers, configureSystemOne } from "./classifiers.js";
|
|
25
26
|
import { auxCursorKey, jetstreamCursorKey, startAuxIndexer, startIndexer, sweepReferences } from "./indexer.js";
|
|
26
27
|
import { startJetstreamIndexer } from "./jetstream.js";
|
|
27
28
|
import { rebuildAllIndexes } from "./database/fts.js";
|
|
@@ -67,7 +68,7 @@ log(`[main] Loaded config: ${collections.length} collections`);
|
|
|
67
68
|
// Discover view defs from lexicons
|
|
68
69
|
discoverViews();
|
|
69
70
|
const engineDialect = getDialect(config.databaseEngine);
|
|
70
|
-
const { schemas, ddlStatements } = buildSchemas(lexicons, collections, engineDialect);
|
|
71
|
+
const { schemas, ddlStatements, indexStatements } = buildSchemas(lexicons, collections, engineDialect);
|
|
71
72
|
for (const s of schemas) {
|
|
72
73
|
if (s.columns.length === 0) {
|
|
73
74
|
log(`[main] No lexicon found for ${s.collection}, using generic JSON storage`);
|
|
@@ -82,10 +83,9 @@ if (config.database !== ':memory:') {
|
|
|
82
83
|
}
|
|
83
84
|
const { adapter, searchPort } = await createAdapter(config.databaseEngine);
|
|
84
85
|
setSearchPort(searchPort);
|
|
85
|
-
|
|
86
|
+
// Migrates as part of opening, between creating the tables and indexing them.
|
|
87
|
+
const migrationChanges = await initDatabase(adapter, config.database, schemas, ddlStatements, indexStatements);
|
|
86
88
|
log(`[main] Database initialized (${config.databaseEngine}, ${config.database === ':memory:' ? 'in-memory' : config.database})`);
|
|
87
|
-
// Auto-migrate schema if lexicons changed
|
|
88
|
-
const migrationChanges = await migrateSchema(schemas);
|
|
89
89
|
if (migrationChanges.length > 0) {
|
|
90
90
|
log(`[main] Applied ${migrationChanges.length} schema migration(s)`);
|
|
91
91
|
}
|
|
@@ -109,8 +109,11 @@ else {
|
|
|
109
109
|
log(`[main] OpenGraph initialized`);
|
|
110
110
|
await initLabels(resolve(configDir, 'labels'));
|
|
111
111
|
log(`[main] Labels initialized: ${getLabelDefinitions().length} definitions`);
|
|
112
|
+
await initClassifiers(resolve(configDir, 'classifiers'));
|
|
113
|
+
log(`[main] Classifiers initialized: ${listClassifiers().join(', ') || 'none'}`);
|
|
112
114
|
}
|
|
113
115
|
// Register built-in dev.hatk.* handlers so callXrpc() can find them
|
|
116
|
+
configureSystemOne(config.typesafe);
|
|
114
117
|
setPrivateCollections(config.privateCollections);
|
|
115
118
|
registerCoreHandlers(collections, config.oauth);
|
|
116
119
|
configureOAuth(config.oauth);
|
package/dist/oauth/server.d.ts
CHANGED
|
@@ -160,6 +160,42 @@ export declare function handleCallback(config: OAuthConfig, code: string, state:
|
|
|
160
160
|
*/
|
|
161
161
|
export declare function handleCallbackError(config: OAuthConfig, error: string, errorDescription: string | null, state: string | null): Promise<string>;
|
|
162
162
|
export declare function handleToken(config: OAuthConfig, body: Record<string, string>, dpopHeader: string, requestUrl: string): Promise<any>;
|
|
163
|
+
/** A refusal from the endpoint {@link obtainSession} called, or an answer it cannot use. */
|
|
164
|
+
export declare class ObtainSessionError extends Error {
|
|
165
|
+
/** The endpoint's error code, e.g. an XRPC error name. */
|
|
166
|
+
readonly error: string;
|
|
167
|
+
readonly status: number;
|
|
168
|
+
constructor(
|
|
169
|
+
/** The endpoint's error code, e.g. an XRPC error name. */
|
|
170
|
+
error: string, message: string, status: number);
|
|
171
|
+
}
|
|
172
|
+
/** What {@link obtainSession} returns: the endpoint's whole response, and whose session it held. */
|
|
173
|
+
export interface ObtainedSession {
|
|
174
|
+
did: string;
|
|
175
|
+
response: Record<string, unknown>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Obtain a session for another account straight from its authorization server,
|
|
179
|
+
* outside the redirect flow: a group host creating an account for this app and
|
|
180
|
+
* handing it a session, say.
|
|
181
|
+
*
|
|
182
|
+
* `body` is POSTed as JSON to `url` as this app's OAuth client, the way the
|
|
183
|
+
* token endpoint is called: `client_id` and, for a confidential client, a client
|
|
184
|
+
* assertion addressed to the endpoint's origin, are added to it, and the request
|
|
185
|
+
* carries a DPoP proof from the app's key, answering a nonce challenge once.
|
|
186
|
+
* `headers` are sent too, for whatever else the endpoint wants to know (service
|
|
187
|
+
* auth from the viewer, typically).
|
|
188
|
+
*
|
|
189
|
+
* The response must hold an OAuth token response, at `session` or as the whole
|
|
190
|
+
* body. That session is stored for its `sub` exactly as a login's is, bound to
|
|
191
|
+
* the same DPoP key, so refresh and the PDS helpers work for that DID from here
|
|
192
|
+
* on. It is only accepted if the account's own authorization server is the one
|
|
193
|
+
* that answered: an endpoint cannot hand over a session for an account it does
|
|
194
|
+
* not serve.
|
|
195
|
+
*/
|
|
196
|
+
export declare function obtainSession(config: OAuthConfig, url: string, body: Record<string, unknown>, opts?: {
|
|
197
|
+
headers?: Record<string, string>;
|
|
198
|
+
}): Promise<ObtainedSession>;
|
|
163
199
|
export declare function refreshPdsSession(config: OAuthConfig, session: {
|
|
164
200
|
did: string;
|
|
165
201
|
pds_endpoint: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAgD/C;;;;;;;GAOG;AACH,qBAAa,UAAW,SAAQ,KAAK;aAEjB,IAAI,EAChB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,qBAAqB,GACrB,wBAAwB,GACxB,eAAe;aACH,WAAW,EAAE,MAAM;aACnB,MAAM,EAAE,MAAM;gBARd,IAAI,EAChB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,qBAAqB,GACrB,wBAAwB,GACxB,eAAe,EACH,WAAW,EAAE,MAAM,EACnB,MAAM,GAAE,MAAY;CAKvC;AA4FD,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,sFAAsF;AACtF,wBAAgB,aAAa;;;;;;;;;;;;;;;;;;;;;;EAW5B;AA0CD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;EAuBpE;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAwLtD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAYhF;AAID;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,MAAM,CAAC,CAuIjB;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAmIrG;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,gBAAgB,EAAE,MAAM,GAAG,IAAI,EAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA4JD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;CACjB,GACA,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoFpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAgD/C;;;;;;;GAOG;AACH,qBAAa,UAAW,SAAQ,KAAK;aAEjB,IAAI,EAChB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,qBAAqB,GACrB,wBAAwB,GACxB,eAAe;aACH,WAAW,EAAE,MAAM;aACnB,MAAM,EAAE,MAAM;gBARd,IAAI,EAChB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,qBAAqB,GACrB,wBAAwB,GACxB,eAAe,EACH,WAAW,EAAE,MAAM,EACnB,MAAM,GAAE,MAAY;CAKvC;AA4FD,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,sFAAsF;AACtF,wBAAgB,aAAa;;;;;;;;;;;;;;;;;;;;;;EAW5B;AA0CD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;EAuBpE;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAwLtD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAYhF;AAID;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,MAAM,CAAC,CAuIjB;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAmIrG;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,gBAAgB,EAAE,MAAM,GAAG,IAAI,EAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA4JD,4FAA4F;AAC5F,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,0DAA0D;aAC1C,KAAK,EAAE,MAAM;aAEb,MAAM,EAAE,MAAM;;IAH9B,0DAA0D;IAC1C,KAAK,EAAE,MAAM,EAC7B,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM;CAIjC;AAED,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,GAC9C,OAAO,CAAC,eAAe,CAAC,CAmD1B;AAID,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;CACjB,GACA,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoFpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
|
package/dist/oauth/server.js
CHANGED
|
@@ -859,6 +859,84 @@ async function handleRefreshTokenGrant(config, body, dpopHeader, requestUrl) {
|
|
|
859
859
|
handle,
|
|
860
860
|
};
|
|
861
861
|
}
|
|
862
|
+
// --- Sessions obtained outside the redirect flow ---
|
|
863
|
+
/** A refusal from the endpoint {@link obtainSession} called, or an answer it cannot use. */
|
|
864
|
+
export class ObtainSessionError extends Error {
|
|
865
|
+
error;
|
|
866
|
+
status;
|
|
867
|
+
constructor(
|
|
868
|
+
/** The endpoint's error code, e.g. an XRPC error name. */
|
|
869
|
+
error, message, status) {
|
|
870
|
+
super(message);
|
|
871
|
+
this.error = error;
|
|
872
|
+
this.status = status;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Obtain a session for another account straight from its authorization server,
|
|
877
|
+
* outside the redirect flow: a group host creating an account for this app and
|
|
878
|
+
* handing it a session, say.
|
|
879
|
+
*
|
|
880
|
+
* `body` is POSTed as JSON to `url` as this app's OAuth client, the way the
|
|
881
|
+
* token endpoint is called: `client_id` and, for a confidential client, a client
|
|
882
|
+
* assertion addressed to the endpoint's origin, are added to it, and the request
|
|
883
|
+
* carries a DPoP proof from the app's key, answering a nonce challenge once.
|
|
884
|
+
* `headers` are sent too, for whatever else the endpoint wants to know (service
|
|
885
|
+
* auth from the viewer, typically).
|
|
886
|
+
*
|
|
887
|
+
* The response must hold an OAuth token response, at `session` or as the whole
|
|
888
|
+
* body. That session is stored for its `sub` exactly as a login's is, bound to
|
|
889
|
+
* the same DPoP key, so refresh and the PDS helpers work for that DID from here
|
|
890
|
+
* on. It is only accepted if the account's own authorization server is the one
|
|
891
|
+
* that answered: an endpoint cannot hand over a session for an account it does
|
|
892
|
+
* not serve.
|
|
893
|
+
*/
|
|
894
|
+
export async function obtainSession(config, url, body, opts = {}) {
|
|
895
|
+
const origin = new URL(url).origin;
|
|
896
|
+
const payload = {
|
|
897
|
+
...body,
|
|
898
|
+
...(await withClientAuth({ client_id: pdsClientId(config.issuer, config) }, config.issuer, config, origin)),
|
|
899
|
+
};
|
|
900
|
+
const send = async (nonce) => fetch(url, {
|
|
901
|
+
method: 'POST',
|
|
902
|
+
headers: {
|
|
903
|
+
...opts.headers,
|
|
904
|
+
'Content-Type': 'application/json',
|
|
905
|
+
DPoP: await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', url, undefined, nonce),
|
|
906
|
+
},
|
|
907
|
+
body: JSON.stringify(payload),
|
|
908
|
+
});
|
|
909
|
+
let res = await send();
|
|
910
|
+
let out = (await res.json().catch(() => ({})));
|
|
911
|
+
const nonce = res.headers.get('DPoP-Nonce');
|
|
912
|
+
if (!res.ok && out.error === 'use_dpop_nonce' && nonce) {
|
|
913
|
+
res = await send(nonce);
|
|
914
|
+
out = (await res.json().catch(() => ({})));
|
|
915
|
+
}
|
|
916
|
+
if (!res.ok) {
|
|
917
|
+
throw new ObtainSessionError(typeof out.error === 'string' ? out.error : 'UpstreamFailure', String(out.message ?? out.error_description ?? `${url} failed (${res.status})`), res.status);
|
|
918
|
+
}
|
|
919
|
+
const token = (typeof out.session === 'object' && out.session ? out.session : out);
|
|
920
|
+
const did = token.sub;
|
|
921
|
+
if (typeof did !== 'string' || typeof token.access_token !== 'string') {
|
|
922
|
+
throw new ObtainSessionError('NoSession', `${url} returned no session`, 502);
|
|
923
|
+
}
|
|
924
|
+
const { pdsEndpoint, authServerEndpoint, authServerMetadata } = await discoverAuthServer(did, _plcUrl);
|
|
925
|
+
if (new URL(authServerEndpoint).origin !== origin) {
|
|
926
|
+
throw new ObtainSessionError('WrongAuthServer', `${origin} is not the authorization server for ${did}`, 502);
|
|
927
|
+
}
|
|
928
|
+
await storeSession(did, {
|
|
929
|
+
pdsEndpoint,
|
|
930
|
+
pdsAuthServer: authServerEndpoint,
|
|
931
|
+
pdsTokenEndpoint: authServerMetadata.token_endpoint,
|
|
932
|
+
accessToken: token.access_token,
|
|
933
|
+
refreshToken: typeof token.refresh_token === 'string' ? token.refresh_token : undefined,
|
|
934
|
+
dpopJkt: serverJkt,
|
|
935
|
+
tokenExpiresAt: typeof token.expires_in === 'number' ? Math.floor(Date.now() / 1000) + token.expires_in : undefined,
|
|
936
|
+
});
|
|
937
|
+
emit('oauth', 'session_obtained', { did, from: origin });
|
|
938
|
+
return { did, response: out };
|
|
939
|
+
}
|
|
862
940
|
// --- PDS Session Refresh ---
|
|
863
941
|
export async function refreshPdsSession(config, session) {
|
|
864
942
|
if (!session.refresh_token)
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA4EA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAuE9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CA2M3F;AAgDD,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,mFAAmF;IACnF,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmwC5F;AAGD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,GAAE,MAAM,EAAO,EACrB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,GACpB,OAAO,WAAW,EAAE,MAAM,CAG5B"}
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,8 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { join, extname } from 'node:path';
|
|
5
5
|
import { gzipSync } from 'node:zlib';
|
|
6
|
-
import { queryRecords, getRecordByUri, searchRecords, getSchema, reshapeRow, setRepoStatus, getRepoStatus, getRepoRetryInfo, queryLabelsForUris, insertLabels, searchAccounts, listReposPaginated, getCollectionCounts, getRepoStatusCounts, getDatabaseSize, deleteLabels, getRecentRecords, listActiveRepoDids, removeRepo, getRepoHandle, getPreferences, putPreference, insertReport, queryReports, resolveReport, getOpenReportCount, } from "./database/db.js";
|
|
6
|
+
import { queryRecords, getRecordByUri, searchRecords, getSchema, reshapeRow, setRepoStatus, getRepoStatus, getRepoRetryInfo, queryLabelsForUris, insertLabels, searchAccounts, listReposPaginated, getCollectionCounts, getRepoStatusCounts, getDatabaseSize, deleteLabels, getRecentRecords, listActiveRepoDids, removeRepo, getRepoHandle, getPreferences, putPreference, insertReport, queryReports, resolveReport, getOpenReportCount, queryClassifications, getClassificationStats, getRecordsByDid, getRecordCountsByDid, findOpenReportsForSubject, } from "./database/db.js";
|
|
7
|
+
import { runScan, getScanProgress, listClassifiers, isSystemOneConfigured } from "./classifiers.js";
|
|
7
8
|
import { executeFeed, listFeeds } from "./feeds.js";
|
|
8
9
|
import { executeXrpc, InvalidRequestError, NotFoundError, registerCoreXrpcHandler, isLocalRelay, paramsFromSearch } from "./xrpc.js";
|
|
9
10
|
import { pdsFor } from "./backfill.js";
|
|
@@ -825,6 +826,93 @@ export function createHandler(config) {
|
|
|
825
826
|
invalidateAdminStats();
|
|
826
827
|
return withCors(json({ ok: true }, 200, acceptEncoding));
|
|
827
828
|
}
|
|
829
|
+
// GET /admin/review — the classifier review queue
|
|
830
|
+
if (url.pathname === '/admin/review' && request.method === 'GET') {
|
|
831
|
+
const denied = requireAdmin(viewer, acceptEncoding);
|
|
832
|
+
if (denied)
|
|
833
|
+
return denied;
|
|
834
|
+
const result = await queryClassifications({
|
|
835
|
+
classifier: url.searchParams.get('classifier') || undefined,
|
|
836
|
+
signal: url.searchParams.get('signal') || undefined,
|
|
837
|
+
minScore: url.searchParams.has('minScore') ? Number(url.searchParams.get('minScore')) : undefined,
|
|
838
|
+
state: url.searchParams.get('state') || 'pending',
|
|
839
|
+
limit: parseInt(url.searchParams.get('limit') || '25'),
|
|
840
|
+
offset: parseInt(url.searchParams.get('offset') || '0'),
|
|
841
|
+
});
|
|
842
|
+
return withCors(json(result, 200, acceptEncoding));
|
|
843
|
+
}
|
|
844
|
+
// GET /admin/review/stats — rollups, classifier names, scan progress
|
|
845
|
+
if (url.pathname === '/admin/review/stats' && request.method === 'GET') {
|
|
846
|
+
const denied = requireAdmin(viewer, acceptEncoding);
|
|
847
|
+
if (denied)
|
|
848
|
+
return denied;
|
|
849
|
+
const stats = await getClassificationStats();
|
|
850
|
+
return withCors(json({ ...stats, classifiers: listClassifiers(), configured: isSystemOneConfigured(), scan: getScanProgress() }, 200, acceptEncoding));
|
|
851
|
+
}
|
|
852
|
+
// POST /admin/review/scan — start a scan. Returns as soon as it is under
|
|
853
|
+
// way; a scan of a whole network outlives any sensible request timeout, so
|
|
854
|
+
// progress is polled from /admin/review/stats rather than awaited here.
|
|
855
|
+
if (url.pathname === '/admin/review/scan' && request.method === 'POST') {
|
|
856
|
+
const denied = requireAdmin(viewer, acceptEncoding);
|
|
857
|
+
if (denied)
|
|
858
|
+
return denied;
|
|
859
|
+
if (!isSystemOneConfigured())
|
|
860
|
+
return withCors(jsonError(400, 'System One is not configured — set `typesafe` in hatk.config.ts', acceptEncoding));
|
|
861
|
+
const body = JSON.parse((await request.text()) || '{}');
|
|
862
|
+
const running = getScanProgress();
|
|
863
|
+
if (running?.running)
|
|
864
|
+
return withCors(jsonError(409, 'A scan is already running', acceptEncoding));
|
|
865
|
+
runScan({ classifier: body.classifier, force: body.force === true, limit: body.limit })
|
|
866
|
+
.then(() => invalidateAdminStats())
|
|
867
|
+
.catch(() => { });
|
|
868
|
+
return withCors(json({ ok: true, started: true }, 202, acceptEncoding));
|
|
869
|
+
}
|
|
870
|
+
// GET /admin/review/subject/:did — everything needed to judge one subject
|
|
871
|
+
if (url.pathname.startsWith('/admin/review/subject/')) {
|
|
872
|
+
const denied = requireAdmin(viewer, acceptEncoding);
|
|
873
|
+
if (denied)
|
|
874
|
+
return denied;
|
|
875
|
+
const did = decodeURIComponent(url.pathname.slice('/admin/review/subject/'.length));
|
|
876
|
+
if (!did.startsWith('did:'))
|
|
877
|
+
return withCors(jsonError(400, 'Expected a DID', acceptEncoding));
|
|
878
|
+
const [records, counts, handle, status, labels] = await Promise.all([
|
|
879
|
+
getRecordsByDid(did, collections, 6),
|
|
880
|
+
getRecordCountsByDid(did, collections),
|
|
881
|
+
getRepoHandle(did),
|
|
882
|
+
getRepoStatus(did),
|
|
883
|
+
queryLabelsForUris([did]),
|
|
884
|
+
]);
|
|
885
|
+
return withCors(json({ did, handle, status, records, counts, labels }, 200, acceptEncoding));
|
|
886
|
+
}
|
|
887
|
+
// POST /admin/review/act — resolve the report and act, in one step
|
|
888
|
+
if (url.pathname === '/admin/review/act' && request.method === 'POST') {
|
|
889
|
+
const denied = requireAdmin(viewer, acceptEncoding);
|
|
890
|
+
if (denied)
|
|
891
|
+
return denied;
|
|
892
|
+
const { did, uri, action, label } = JSON.parse(await request.text());
|
|
893
|
+
const subjectUri = uri || did;
|
|
894
|
+
if (!subjectUri || !action)
|
|
895
|
+
return withCors(jsonError(400, 'Missing subject or action', acceptEncoding));
|
|
896
|
+
if (!['takedown', 'label', 'dismiss'].includes(action))
|
|
897
|
+
return withCors(jsonError(400, 'Action must be takedown, label or dismiss', acceptEncoding));
|
|
898
|
+
if (action === 'takedown') {
|
|
899
|
+
if (!did)
|
|
900
|
+
return withCors(jsonError(400, 'Takedown needs a did', acceptEncoding));
|
|
901
|
+
await setRepoStatus(did, 'takendown');
|
|
902
|
+
}
|
|
903
|
+
else if (action === 'label') {
|
|
904
|
+
if (!label)
|
|
905
|
+
return withCors(jsonError(400, 'Label action needs a label', acceptEncoding));
|
|
906
|
+
await insertLabels([{ src: 'admin', uri: subjectUri, val: label }]);
|
|
907
|
+
}
|
|
908
|
+
// Close whatever the classifier filed, so the subject leaves the queue
|
|
909
|
+
// whichever way it was decided.
|
|
910
|
+
for (const id of await findOpenReportsForSubject(subjectUri)) {
|
|
911
|
+
await resolveReport(id, action === 'dismiss' ? 'dismissed' : 'resolved', viewer.did);
|
|
912
|
+
}
|
|
913
|
+
invalidateAdminStats();
|
|
914
|
+
return withCors(json({ ok: true }, 200, acceptEncoding));
|
|
915
|
+
}
|
|
828
916
|
// GET /admin/info/:did — repo status info
|
|
829
917
|
if (url.pathname.startsWith('/admin/info/did:')) {
|
|
830
918
|
const denied = requireAdmin(viewer, acceptEncoding);
|
|
@@ -1342,15 +1430,16 @@ export function createHandler(config) {
|
|
|
1342
1430
|
throw err;
|
|
1343
1431
|
}
|
|
1344
1432
|
}
|
|
1345
|
-
// GET /space-blob?space=&repo=&cid= — a blob inside a permissioned space
|
|
1346
|
-
//
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1433
|
+
// GET /space-blob?space=&repo=&cid= — a blob inside a permissioned space.
|
|
1434
|
+
// Unlike /blob/ below it is not dev-only, and every request is authorized
|
|
1435
|
+
// with the viewer's own credential for the space: a space blob has no
|
|
1436
|
+
// public URL by design. `if-none-match` is passed along because the
|
|
1437
|
+
// answer may be that the viewer already holds it.
|
|
1349
1438
|
if (url.pathname === '/space-blob') {
|
|
1350
1439
|
const parsed = parseSpaceBlobRequest(url.searchParams);
|
|
1351
1440
|
if (!parsed)
|
|
1352
1441
|
return withCors(jsonError(400, 'Expected space, repo and cid', acceptEncoding));
|
|
1353
|
-
return withCors(await serveSpaceBlob(oauth, viewer, parsed));
|
|
1442
|
+
return withCors(await serveSpaceBlob(oauth, viewer, parsed, request.headers.get('if-none-match')));
|
|
1354
1443
|
}
|
|
1355
1444
|
// GET /blob/:did/:cid — dev-only image proxy (see blobUrl in xrpc.ts).
|
|
1356
1445
|
// A PDS serves getBlob as an attachment with nosniff, which a browser
|
package/dist/spaces/blob.d.ts
CHANGED
|
@@ -8,19 +8,71 @@
|
|
|
8
8
|
* public URL would turn that URL into the capability the credential exists to
|
|
9
9
|
* replace.
|
|
10
10
|
*
|
|
11
|
-
* So the bytes come through here,
|
|
12
|
-
* viewer's
|
|
13
|
-
*
|
|
11
|
+
* So the bytes come through here, and every request is authorized before any
|
|
12
|
+
* of them move: the viewer's credential for the space, or the same 404 a
|
|
13
|
+
* missing blob gets. What that authorization guards is served two ways —
|
|
14
|
+
* `private` so the viewer's own browser may hold it and no shared cache may,
|
|
15
|
+
* and out of a bounded in-memory cache so the same photo is fetched from its
|
|
16
|
+
* repo once rather than once per member looking at it.
|
|
14
17
|
*/
|
|
15
18
|
import type { OAuthConfig } from '../config.ts';
|
|
19
|
+
/**
|
|
20
|
+
* The sizes a space blob is served at.
|
|
21
|
+
*
|
|
22
|
+
* The same words hatk's public path uses for imgproxy, with the same numbers,
|
|
23
|
+
* so an app naming a size says it once and means it either side of the line
|
|
24
|
+
* between a public blob and a permissioned one. What differs is who resizes:
|
|
25
|
+
* a space blob may not pass through an image CDN, so it happens here.
|
|
26
|
+
*
|
|
27
|
+
* A request naming none gets the original, which is what every caller written
|
|
28
|
+
* before presets existed asks for.
|
|
29
|
+
*/
|
|
30
|
+
declare const PRESETS: {
|
|
31
|
+
readonly avatar: {
|
|
32
|
+
readonly width: 1000;
|
|
33
|
+
readonly height: 1000;
|
|
34
|
+
readonly fit: "cover";
|
|
35
|
+
readonly quality: 85;
|
|
36
|
+
};
|
|
37
|
+
readonly avatar_thumbnail: {
|
|
38
|
+
readonly width: 128;
|
|
39
|
+
readonly height: 128;
|
|
40
|
+
readonly fit: "cover";
|
|
41
|
+
readonly quality: 70;
|
|
42
|
+
};
|
|
43
|
+
readonly feed_thumbnail: {
|
|
44
|
+
readonly width: 1000;
|
|
45
|
+
readonly fit: "inside";
|
|
46
|
+
readonly quality: 70;
|
|
47
|
+
};
|
|
48
|
+
readonly feed_fullsize: {
|
|
49
|
+
readonly quality: 90;
|
|
50
|
+
};
|
|
51
|
+
readonly banner: {
|
|
52
|
+
readonly width: 3000;
|
|
53
|
+
readonly height: 1000;
|
|
54
|
+
readonly fit: "cover";
|
|
55
|
+
readonly quality: 85;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export type SpaceBlobPreset = keyof typeof PRESETS;
|
|
59
|
+
/**
|
|
60
|
+
* Only a name from that list is a preset.
|
|
61
|
+
*
|
|
62
|
+
* Free-form dimensions would let anyone mint unlimited distinct cache keys and
|
|
63
|
+
* evict everything real, and would put the cost of an arbitrary resize behind
|
|
64
|
+
* a URL. An unrecognised name is treated as none rather than refused: a broken
|
|
65
|
+
* image is a worse answer than a large one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function asPreset(name: string | null | undefined): SpaceBlobPreset | undefined;
|
|
16
68
|
/**
|
|
17
69
|
* The URL this appview serves a space blob at, for whoever is asking.
|
|
18
70
|
*
|
|
19
71
|
* Built here rather than by the app, because the route is hatk's: a blob in a
|
|
20
72
|
* space has no public address by design, and this is the one address it does
|
|
21
|
-
* have — answered
|
|
73
|
+
* have — answered only to a viewer who may read the space it names.
|
|
22
74
|
*/
|
|
23
|
-
export declare function spaceBlobUrl(space: string, repo: string, cid: string): string;
|
|
75
|
+
export declare function spaceBlobUrl(space: string, repo: string, cid: string, preset?: SpaceBlobPreset): string;
|
|
24
76
|
/**
|
|
25
77
|
* The CID a blob reference names, in whichever spelling the read used.
|
|
26
78
|
*
|
|
@@ -35,8 +87,12 @@ export interface SpaceBlobRequest {
|
|
|
35
87
|
/** The repo holding the blob — the writer's, not the authority's. */
|
|
36
88
|
repo: string;
|
|
37
89
|
cid: string;
|
|
90
|
+
/** The size to serve it at. Absent means the original. */
|
|
91
|
+
preset?: SpaceBlobPreset;
|
|
38
92
|
}
|
|
39
93
|
export declare function parseSpaceBlobRequest(params: URLSearchParams): SpaceBlobRequest | null;
|
|
94
|
+
/** Drop everything held. For tests, and for an operator who wants it gone. */
|
|
95
|
+
export declare function resetSpaceBlobCache(): void;
|
|
40
96
|
/**
|
|
41
97
|
* Fetch a space blob as this viewer, or answer why not.
|
|
42
98
|
*
|
|
@@ -44,8 +100,13 @@ export declare function parseSpaceBlobRequest(params: URLSearchParams): SpaceBlo
|
|
|
44
100
|
* there. The distinction between "you may not" and "it does not exist" is
|
|
45
101
|
* itself information about a private space, and the space host draws the line
|
|
46
102
|
* the same way when it reports a non-member's repo as simply absent.
|
|
103
|
+
*
|
|
104
|
+
* The credential is checked on every request, including the ones answered from
|
|
105
|
+
* the cache and the ones answered with a 304. Nothing here is ever served to
|
|
106
|
+
* somebody the authority has not just said yes to.
|
|
47
107
|
*/
|
|
48
108
|
export declare function serveSpaceBlob(oauth: OAuthConfig | null, viewer: {
|
|
49
109
|
did: string;
|
|
50
|
-
} | null, request: SpaceBlobRequest): Promise<Response>;
|
|
110
|
+
} | null, request: SpaceBlobRequest, ifNoneMatch?: string | null): Promise<Response>;
|
|
111
|
+
export {};
|
|
51
112
|
//# sourceMappingURL=blob.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../src/spaces/blob.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../src/spaces/blob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA6C/C;;;;;;;;;;GAUG;AACH,QAAA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;CAMsG,CAAA;AAEnH,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,OAAO,CAAA;AAElD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAErF;AAKD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,CAIvG;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMzD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,0DAA0D;IAC1D,MAAM,CAAC,EAAE,eAAe,CAAA;CACzB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAQtF;AA0DD,8EAA8E;AAC9E,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAuDD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9B,OAAO,EAAE,gBAAgB,EACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAAC,QAAQ,CAAC,CA+BnB"}
|