@hatk/hatk 0.0.1-alpha.93 → 0.0.1-alpha.95

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/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
- /** The URL this appview serves a space blob at, for the viewer asking. */
57
- spaceBlobUrl: (space: string, repo: string, cid: string) => string;
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>[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEzC,YAAY,EAAE,GAAG,EAAE,CAAA;AAInB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/C,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9D;;;;WAIG;QACH,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KACpE,CAAA;IACD,UAAU,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1G,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IAC3E;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACpG;;;OAGG;IACH,YAAY,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACnF,6FAA6F;IAC7F,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAChG,mEAAmE;IACnE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,0EAA0E;IAC1E,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CACnE;AAID,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAsC5E;AAID,yCAAyC;AACzC,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,WAAW,CAuB7F"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"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"}
package/dist/indexer.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare function getLastSeq(): number | null;
23
23
  */
24
24
  export declare function checkpointCursor(): Promise<void>;
25
25
  /** Point cursor persistence at a stream's own `_cursor` row. See {@link cursorKey}. */
26
- export declare function setCursorKey(key: 'relay' | 'jetstream'): void;
26
+ export declare function setCursorKey(key: string): void;
27
27
  /**
28
28
  * The cursor a reconnect should resume from: the latest seq this process has
29
29
  * seen, falling back to the boot-time cursor before any message has arrived.
@@ -123,6 +123,16 @@ export interface AuxIndexerOpts {
123
123
  }
124
124
  /** The `_cursor` row an auxiliary firehose persists its position to. */
125
125
  export declare function auxCursorKey(relayUrl: string): string;
126
+ /**
127
+ * The `_cursor` row a Jetstream instance persists its position to.
128
+ *
129
+ * Named for the instance, because a Jetstream sequence means nothing on
130
+ * another one. Pointed at a second instance — a region swap, an outage
131
+ * worked around — a shared row would hand over a number from somewhere else,
132
+ * and the stream would resume at whatever that happens to address there:
133
+ * no error, no refusal, just a silent jump past whatever fell in between.
134
+ */
135
+ export declare function jetstreamCursorKey(jetstreamUrl: string): string;
126
136
  /**
127
137
  * Tail a second `subscribeRepos` alongside the primary stream.
128
138
  *
@@ -1 +1 @@
1
- {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"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"}
package/dist/indexer.js CHANGED
@@ -22,8 +22,10 @@ let cursorCheckpointTimer = null;
22
22
  *
23
23
  * The relay's `subscribeRepos` seq and Jetstream's seq are different
24
24
  * coordinate systems — resuming one from the other's value would either skip
25
- * a swathe of the stream or replay from a nonsense offset. Each source owns
26
- * its own key so switching between them (or back) is safe.
25
+ * a swathe of the stream or replay from a nonsense offset. So is one Jetstream
26
+ * instance's seq against another's, which is why the key names the instance
27
+ * rather than the kind. Each source owns its own key so switching between them
28
+ * (or back) is safe.
27
29
  */
28
30
  let cursorKey = 'relay';
29
31
  const BATCH_SIZE = 100;
@@ -517,6 +519,18 @@ export async function startIndexer(opts) {
517
519
  export function auxCursorKey(relayUrl) {
518
520
  return `relay:${relayUrl}`;
519
521
  }
522
+ /**
523
+ * The `_cursor` row a Jetstream instance persists its position to.
524
+ *
525
+ * Named for the instance, because a Jetstream sequence means nothing on
526
+ * another one. Pointed at a second instance — a region swap, an outage
527
+ * worked around — a shared row would hand over a number from somewhere else,
528
+ * and the stream would resume at whatever that happens to address there:
529
+ * no error, no refusal, just a silent jump past whatever fell in between.
530
+ */
531
+ export function jetstreamCursorKey(jetstreamUrl) {
532
+ return `jetstream:${jetstreamUrl}`;
533
+ }
520
534
  /**
521
535
  * Tail a second `subscribeRepos` alongside the primary stream.
522
536
  *
@@ -1 +1 @@
1
- {"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EASL,KAAK,QAAQ,EACb,KAAK,eAAe,EACrB,MAAM,cAAc,CAAA;AAGrB,sEAAsE;AACtE,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,QAAQ,QAAS,CAAA;AAO9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,wDAAwD;AACxD,UAAU,eAAe;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAalG;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,QAAQ,GAAG,IAAI,CAY7F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAuBzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,GAAG,IAAI,GAAG,SAAS,CAK3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CA0DjG"}
1
+ {"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAUL,KAAK,QAAQ,EACb,KAAK,eAAe,EACrB,MAAM,cAAc,CAAA;AAGrB,sEAAsE;AACtE,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,QAAQ,QAAS,CAAA;AAO9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,wDAAwD;AACxD,UAAU,eAAe;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAalG;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,QAAQ,GAAG,IAAI,CAY7F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAuBzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,GAAG,IAAI,GAAG,SAAS,CAK3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CA0DjG"}
package/dist/jetstream.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * @see https://bsky.network/docs/jetstream
15
15
  * @module
16
16
  */
17
- import { applyCommit, configureIndexer, getLastSeq, handleIdentityEvent, isIndexableCollection, noteSeq, resumeCursor, setCursorKey, } from "./indexer.js";
17
+ import { applyCommit, configureIndexer, getLastSeq, handleIdentityEvent, isIndexableCollection, jetstreamCursorKey, noteSeq, resumeCursor, setCursorKey, } from "./indexer.js";
18
18
  import { log, emit } from "./logger.js";
19
19
  /** Server-side filter caps, rejected before the WebSocket upgrade. */
20
20
  export const MAX_COLLECTIONS = 100;
@@ -170,7 +170,7 @@ export async function startJetstreamIndexer(opts, refusals = 0) {
170
170
  const { jetstreamUrl, collections } = opts;
171
171
  const pinnedRepos = opts.pinnedRepos || null;
172
172
  assertFilterLimits(collections, pinnedRepos);
173
- setCursorKey('jetstream');
173
+ setCursorKey(jetstreamCursorKey(opts.jetstreamUrl));
174
174
  await configureIndexer(opts);
175
175
  const cursor = reconnectCursor(refusals, getLastSeq(), opts.cursor);
176
176
  const wsUrl = buildSubscribeUrl(jetstreamUrl, collections, pinnedRepos, cursor);
package/dist/main.js CHANGED
@@ -22,7 +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 { auxCursorKey, startAuxIndexer, startIndexer, sweepReferences } from "./indexer.js";
25
+ import { auxCursorKey, jetstreamCursorKey, startAuxIndexer, startIndexer, sweepReferences } from "./indexer.js";
26
26
  import { startJetstreamIndexer } from "./jetstream.js";
27
27
  import { rebuildAllIndexes } from "./database/fts.js";
28
28
  import { createHandler, registerCoreHandlers } from "./server.js";
@@ -237,7 +237,7 @@ const indexerCore = {
237
237
  ftsRebuildInterval: config.ftsRebuildInterval,
238
238
  };
239
239
  if (config.jetstream) {
240
- const cursor = ignoreSavedCursor ? null : await getCursor('jetstream');
240
+ const cursor = ignoreSavedCursor ? null : await getCursor(jetstreamCursorKey(config.jetstream.url));
241
241
  startJetstreamIndexer({ ...indexerCore, jetstreamUrl: config.jetstream.url, cursor });
242
242
  }
243
243
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAsEA,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,CAyqC5F;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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAsEA,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,CA0qC5F;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
@@ -1342,15 +1342,16 @@ export function createHandler(config) {
1342
1342
  throw err;
1343
1343
  }
1344
1344
  }
1345
- // GET /space-blob?space=&repo=&cid= — a blob inside a permissioned space,
1346
- // fetched with the viewer's own credential. Unlike /blob/ below it is not
1347
- // dev-only and not cacheable: a space blob has no public URL by design,
1348
- // and this response belongs to one viewer.
1345
+ // GET /space-blob?space=&repo=&cid= — a blob inside a permissioned space.
1346
+ // Unlike /blob/ below it is not dev-only, and every request is authorized
1347
+ // with the viewer's own credential for the space: a space blob has no
1348
+ // public URL by design. `if-none-match` is passed along because the
1349
+ // answer may be that the viewer already holds it.
1349
1350
  if (url.pathname === '/space-blob') {
1350
1351
  const parsed = parseSpaceBlobRequest(url.searchParams);
1351
1352
  if (!parsed)
1352
1353
  return withCors(jsonError(400, 'Expected space, repo and cid', acceptEncoding));
1353
- return withCors(await serveSpaceBlob(oauth, viewer, parsed));
1354
+ return withCors(await serveSpaceBlob(oauth, viewer, parsed, request.headers.get('if-none-match')));
1354
1355
  }
1355
1356
  // GET /blob/:did/:cid — dev-only image proxy (see blobUrl in xrpc.ts).
1356
1357
  // A PDS serves getBlob as an attachment with nosniff, which a browser
@@ -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, fetched on the viewer's own behalf with the
12
- * viewer's own credential, and go no further: `private, no-store` keeps them
13
- * out of every shared cache between this server and that browser.
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 per viewer, with that viewer's credential, uncacheable.
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;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAe/C;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG7E;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;CACZ;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAOtF;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAsCnB"}
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"}
@@ -8,9 +8,12 @@
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, fetched on the viewer's own behalf with the
12
- * viewer's own credential, and go no further: `private, no-store` keeps them
13
- * out of every shared cache between this server and that browser.
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 { emit } from "../logger.js";
16
19
  import { repoEndpoint } from "./identity.js";
@@ -24,15 +27,74 @@ import { viewerCredential } from "./viewer.js";
24
27
  * what a browser executes in this origin.
25
28
  */
26
29
  const SERVABLE = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif']);
30
+ /**
31
+ * How long a browser may reuse a blob it has before it asks again.
32
+ *
33
+ * Every request that does arrive is authorized, so this is the window in which
34
+ * a viewer who has just lost access still sees what they had. A minute is
35
+ * small against what the surrounding design already accepts: the readable set
36
+ * is cached for five, and the space host lets a minted credential outlive a
37
+ * revocation by up to two hours.
38
+ */
39
+ const MAX_AGE_S = 60;
40
+ /**
41
+ * Bytes held across every cached blob, and the largest single one worth
42
+ * holding. A blob past the entry limit streams straight through instead —
43
+ * caching it would evict a great many small ones to save one fetch.
44
+ */
45
+ const CACHE_BYTES = 64 * 1024 * 1024;
46
+ const MAX_ENTRY_BYTES = 4 * 1024 * 1024;
47
+ /**
48
+ * How much of an original we will read in order to make a smaller one.
49
+ *
50
+ * Larger than what we hold, and deliberately: a photograph straight off a
51
+ * camera is exactly the case where resizing pays, and refusing to read it
52
+ * would be refusing the request that needs answering most. What comes back out
53
+ * is a derivative small enough to keep.
54
+ */
55
+ const MAX_SOURCE_BYTES = 20 * 1024 * 1024;
56
+ /**
57
+ * The sizes a space blob is served at.
58
+ *
59
+ * The same words hatk's public path uses for imgproxy, with the same numbers,
60
+ * so an app naming a size says it once and means it either side of the line
61
+ * between a public blob and a permissioned one. What differs is who resizes:
62
+ * a space blob may not pass through an image CDN, so it happens here.
63
+ *
64
+ * A request naming none gets the original, which is what every caller written
65
+ * before presets existed asks for.
66
+ */
67
+ const PRESETS = {
68
+ avatar: { width: 1000, height: 1000, fit: 'cover', quality: 85 },
69
+ avatar_thumbnail: { width: 128, height: 128, fit: 'cover', quality: 70 },
70
+ feed_thumbnail: { width: 1000, fit: 'inside', quality: 70 },
71
+ feed_fullsize: { quality: 90 },
72
+ banner: { width: 3000, height: 1000, fit: 'cover', quality: 85 },
73
+ };
74
+ /**
75
+ * Only a name from that list is a preset.
76
+ *
77
+ * Free-form dimensions would let anyone mint unlimited distinct cache keys and
78
+ * evict everything real, and would put the cost of an arbitrary resize behind
79
+ * a URL. An unrecognised name is treated as none rather than refused: a broken
80
+ * image is a worse answer than a large one.
81
+ */
82
+ export function asPreset(name) {
83
+ return name && name in PRESETS ? name : undefined;
84
+ }
85
+ /** Resizing is skipped for these: one frame of an animation is not the animation. */
86
+ const ANIMATED = new Set(['image/gif']);
27
87
  /**
28
88
  * The URL this appview serves a space blob at, for whoever is asking.
29
89
  *
30
90
  * Built here rather than by the app, because the route is hatk's: a blob in a
31
91
  * space has no public address by design, and this is the one address it does
32
- * have — answered per viewer, with that viewer's credential, uncacheable.
92
+ * have — answered only to a viewer who may read the space it names.
33
93
  */
34
- export function spaceBlobUrl(space, repo, cid) {
94
+ export function spaceBlobUrl(space, repo, cid, preset) {
35
95
  const params = new URLSearchParams({ space, repo, cid });
96
+ if (preset)
97
+ params.set('preset', preset);
36
98
  return `/space-blob?${params}`;
37
99
  }
38
100
  /**
@@ -61,7 +123,114 @@ export function parseSpaceBlobRequest(params) {
61
123
  return null;
62
124
  if (!parseSpaceRef(space) || !repo.startsWith('did:'))
63
125
  return null;
64
- return { space, repo, cid };
126
+ const preset = asPreset(params.get('preset'));
127
+ return { space, repo, cid, ...(preset ? { preset } : {}) };
128
+ }
129
+ /**
130
+ * Blobs held in memory, least recently used first.
131
+ *
132
+ * In memory and not on disk on purpose. These are members' private photos, and
133
+ * a cache that survives a restart is a store of them: this one is bounded,
134
+ * forgotten when the process ends, and never written anywhere a backup would
135
+ * find it.
136
+ */
137
+ const cache = new Map();
138
+ let cachedBytes = 0;
139
+ /**
140
+ * Fetches in flight, so a page opening with the same image in it twenty times
141
+ * fetches it once. Keyed like the cache.
142
+ */
143
+ const inFlight = new Map();
144
+ /**
145
+ * The space is part of the key, and has to be.
146
+ *
147
+ * A CID names bytes, so caching by CID alone would hit across spaces — and the
148
+ * check that a blob is actually referenced from the space being named lives
149
+ * upstream, in `com.atproto.space.getBlob`. A viewer who may read one space
150
+ * could then name any CID they had heard of and be served it out of the cache
151
+ * without that check ever running.
152
+ */
153
+ const keyFor = (request) => `${request.space}\n${request.cid}\n${request.preset ?? ''}`;
154
+ function cacheGet(key) {
155
+ const entry = cache.get(key);
156
+ if (!entry)
157
+ return undefined;
158
+ // Re-inserted so iteration order is least-recently-used first.
159
+ cache.delete(key);
160
+ cache.set(key, entry);
161
+ return entry;
162
+ }
163
+ function cachePut(key, entry) {
164
+ if (entry.bytes.byteLength > MAX_ENTRY_BYTES)
165
+ return;
166
+ const existing = cache.get(key);
167
+ if (existing)
168
+ cachedBytes -= existing.bytes.byteLength;
169
+ cache.set(key, entry);
170
+ cachedBytes += entry.bytes.byteLength;
171
+ while (cachedBytes > CACHE_BYTES) {
172
+ const oldest = cache.keys().next();
173
+ if (oldest.done)
174
+ break;
175
+ cachedBytes -= cache.get(oldest.value).bytes.byteLength;
176
+ cache.delete(oldest.value);
177
+ }
178
+ }
179
+ /** Drop everything held. For tests, and for an operator who wants it gone. */
180
+ export function resetSpaceBlobCache() {
181
+ cache.clear();
182
+ inFlight.clear();
183
+ cachedBytes = 0;
184
+ }
185
+ /**
186
+ * A CID is a hash of the bytes it names, which is exactly what an ETag is for.
187
+ * Strong, free, and stable for as long as the blob exists. The preset is part
188
+ * of it because it is part of what comes back.
189
+ */
190
+ const etagFor = (request) => request.preset ? `"${request.cid}-${request.preset}"` : `"${request.cid}"`;
191
+ /** Whether the browser already holds this blob. `*` matches anything it has. */
192
+ function matches(ifNoneMatch, etag) {
193
+ if (!ifNoneMatch)
194
+ return false;
195
+ return ifNoneMatch
196
+ .split(',')
197
+ .map((candidate) => candidate.trim().replace(/^W\//, ''))
198
+ .some((candidate) => candidate === '*' || candidate === etag);
199
+ }
200
+ /**
201
+ * Headers every answer carries, whether it has a body or not.
202
+ *
203
+ * `private` is the whole of the caching posture: the viewer's own browser may
204
+ * keep what it was shown, and no cache between here and there may. `vary`
205
+ * keeps it honest on a browser where two people sign in — the response turns
206
+ * on who asked, and who asked is the cookie or the token.
207
+ */
208
+ function cacheHeaders(request) {
209
+ return {
210
+ etag: etagFor(request),
211
+ 'cache-control': `private, max-age=${MAX_AGE_S}, must-revalidate`,
212
+ vary: 'cookie, authorization',
213
+ };
214
+ }
215
+ function serve(entry, request) {
216
+ return new Response(entry.bytes, {
217
+ status: 200,
218
+ headers: {
219
+ 'content-type': entry.contentType,
220
+ ...cacheHeaders(request),
221
+ 'content-disposition': 'inline',
222
+ 'content-security-policy': "default-src 'none'; sandbox",
223
+ 'x-content-type-options': 'nosniff',
224
+ },
225
+ });
226
+ }
227
+ /** A refusal from the repo host, carried out through the in-flight promise. */
228
+ class UpstreamRefusal extends Error {
229
+ status;
230
+ constructor(status) {
231
+ super(`upstream refused with ${status}`);
232
+ this.status = status;
233
+ }
65
234
  }
66
235
  /**
67
236
  * Fetch a space blob as this viewer, or answer why not.
@@ -70,42 +239,222 @@ export function parseSpaceBlobRequest(params) {
70
239
  * there. The distinction between "you may not" and "it does not exist" is
71
240
  * itself information about a private space, and the space host draws the line
72
241
  * the same way when it reports a non-member's repo as simply absent.
242
+ *
243
+ * The credential is checked on every request, including the ones answered from
244
+ * the cache and the ones answered with a 304. Nothing here is ever served to
245
+ * somebody the authority has not just said yes to.
73
246
  */
74
- export async function serveSpaceBlob(oauth, viewer, request) {
247
+ export async function serveSpaceBlob(oauth, viewer, request, ifNoneMatch) {
75
248
  if (!viewer)
76
249
  return new Response('Unauthorized', { status: 401 });
77
250
  const credential = await viewerCredential(oauth, viewer, request.space);
78
251
  if (!credential)
79
252
  return new Response('Not found', { status: 404 });
253
+ // The browser has it already. Authorized above, so this is a viewer who may
254
+ // go on looking at what they hold — and it costs neither of us the bytes.
255
+ if (matches(ifNoneMatch, etagFor(request))) {
256
+ return new Response(null, { status: 304, headers: cacheHeaders(request) });
257
+ }
258
+ const key = keyFor(request);
259
+ const hit = cacheGet(key);
260
+ if (hit)
261
+ return serve(hit, request);
262
+ let entry;
263
+ try {
264
+ entry = await (inFlight.get(key) ?? fetchOnce(key, credential, request));
265
+ }
266
+ catch (err) {
267
+ if (err instanceof UpstreamRefusal)
268
+ return new Response('Not found', { status: err.status });
269
+ emit('spaces', 'blob_error', { space: request.space, repo: request.repo, error: err.message });
270
+ return new Response('Blob unavailable', { status: 502 });
271
+ }
272
+ // Too large to hold, which the read above discovered from its headers and
273
+ // then dropped. Asking again costs a round trip and no bytes, and buys a
274
+ // body that is streamed rather than gathered into memory to be served once.
275
+ if (!entry)
276
+ return streamThrough(credential, request);
277
+ return serve(entry, request);
278
+ }
279
+ function blobUrlFor(endpoint, request) {
280
+ const url = new URL(`${endpoint}/xrpc/com.atproto.space.getBlob`);
281
+ url.searchParams.set('space', request.space);
282
+ // `repo`, not `did`: a space read names the repo holding the record the
283
+ // same way everywhere. A server hosting one account falls back to it,
284
+ // which is what makes `did` ever appear to work.
285
+ url.searchParams.set('repo', request.repo);
286
+ url.searchParams.set('cid', request.cid);
287
+ return url;
288
+ }
289
+ /**
290
+ * The upstream read, shared by everyone who asked for this blob at once.
291
+ *
292
+ * Resolves to null for a blob too large to hold, which the caller streams
293
+ * instead. A failure rejects, and rejects for all of them: one refusal is
294
+ * every caller's refusal, because the credential each of them presented had
295
+ * already been accepted for this space.
296
+ */
297
+ function fetchOnce(key, credential, request) {
298
+ const task = (async () => {
299
+ const upstream = await credential.fetch(blobUrlFor(await repoEndpoint(request.repo), request));
300
+ if (!upstream.ok) {
301
+ // A credential the repo host rejects reads as absent, not as
302
+ // unauthorized: the viewer holds one, so what they are being told is
303
+ // that this blob is not theirs to see.
304
+ throw new UpstreamRefusal(upstream.status === 401 ? 404 : upstream.status);
305
+ }
306
+ const contentType = servableType(upstream);
307
+ const limit = request.preset ? MAX_SOURCE_BYTES : MAX_ENTRY_BYTES;
308
+ const declared = Number(upstream.headers.get('content-length') ?? NaN);
309
+ if (declared > limit) {
310
+ await upstream.body?.cancel();
311
+ return null;
312
+ }
313
+ // Read against the limit rather than trusting the length, which a repo host
314
+ // is free not to send and free to get wrong.
315
+ const source = await readBounded(upstream, limit);
316
+ if (!source)
317
+ return null;
318
+ // A resize that cannot happen is not a failure to serve. Sharp missing, an
319
+ // animation, or an image it cannot read all end here with the original,
320
+ // which is the answer this route gave before presets existed.
321
+ const bytes = request.preset ? ((await resize(source, contentType, request.preset)) ?? source) : source;
322
+ const entry = { bytes, contentType };
323
+ cachePut(key, entry);
324
+ return entry;
325
+ })().finally(() => {
326
+ if (inFlight.get(key) === task)
327
+ inFlight.delete(key);
328
+ });
329
+ inFlight.set(key, task);
330
+ return task;
331
+ }
332
+ /**
333
+ * The whole body, or null once it is plainly too big to hold.
334
+ *
335
+ * Bounded as it reads: a body that goes past the limit is abandoned there, so
336
+ * the most this ever holds is one blob's worth over the limit by one chunk.
337
+ */
338
+ async function readBounded(upstream, limit) {
339
+ if (!upstream.body)
340
+ return new Uint8Array(0);
341
+ const reader = upstream.body.getReader();
342
+ const chunks = [];
343
+ let total = 0;
344
+ try {
345
+ while (true) {
346
+ const { done, value } = await reader.read();
347
+ if (done)
348
+ break;
349
+ total += value.byteLength;
350
+ if (total > limit) {
351
+ await reader.cancel();
352
+ return null;
353
+ }
354
+ chunks.push(value);
355
+ }
356
+ }
357
+ finally {
358
+ reader.releaseLock();
359
+ }
360
+ const bytes = new Uint8Array(total);
361
+ let at = 0;
362
+ for (const chunk of chunks) {
363
+ bytes.set(chunk, at);
364
+ at += chunk.byteLength;
365
+ }
366
+ return bytes;
367
+ }
368
+ /**
369
+ * Sharp, if this deployment has it.
370
+ *
371
+ * An optional peer dependency: hatk is a library, and most of what installs it
372
+ * never serves a permissioned space at all. Resolved once — a missing module
373
+ * is answered from the same settled promise rather than re-attempted per
374
+ * image.
375
+ */
376
+ let sharpModule;
377
+ function loadSharp() {
378
+ sharpModule ??= import('sharp')
379
+ .then((m) => m.default)
380
+ .catch(() => {
381
+ emit('spaces', 'resize_unavailable', { reason: 'sharp is not installed' });
382
+ return null;
383
+ });
384
+ return sharpModule;
385
+ }
386
+ /**
387
+ * The blob at the size it is drawn, or null to serve the original instead.
388
+ *
389
+ * Orientation is applied before the resize and the metadata that carried it is
390
+ * dropped with everything else — which also means the GPS in a ride photo's
391
+ * EXIF stops being served to everyone the space admits.
392
+ *
393
+ * The format is kept rather than normalised to JPEG. Re-encoding a PNG that
394
+ * way silently flattens its transparency, and no size is worth that.
395
+ *
396
+ * Null when the original is the better answer — an animation, an image sharp
397
+ * cannot read, sharp absent, or a derivative that came out no smaller.
398
+ */
399
+ async function resize(source, contentType, preset) {
400
+ if (ANIMATED.has(contentType))
401
+ return null;
402
+ const sharp = await loadSharp();
403
+ if (!sharp)
404
+ return null;
405
+ const { width, height, fit, quality } = PRESETS[preset];
406
+ try {
407
+ let pipeline = sharp(source).rotate();
408
+ if (width)
409
+ pipeline = pipeline.resize({ width, height, fit, withoutEnlargement: true });
410
+ // Named explicitly rather than left to sharp, which keeps the input format
411
+ // but not the quality — without this a preset that only sets quality, like
412
+ // a full-size one, would re-encode at sharp's default and do nothing.
413
+ if (contentType === 'image/jpeg')
414
+ pipeline = pipeline.jpeg({ quality, mozjpeg: true });
415
+ else if (contentType === 'image/webp')
416
+ pipeline = pipeline.webp({ quality });
417
+ else if (contentType === 'image/avif')
418
+ pipeline = pipeline.avif({ quality });
419
+ else if (contentType === 'image/png')
420
+ pipeline = pipeline.png({ compressionLevel: 9 });
421
+ const out = await pipeline.toBuffer();
422
+ // A derivative bigger than what it came from is a worse answer than the
423
+ // original — small images re-encoded at a higher quality can land there.
424
+ if (out.byteLength >= source.byteLength)
425
+ return null;
426
+ return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
427
+ }
428
+ catch (err) {
429
+ emit('spaces', 'resize_failed', { preset, content_type: contentType, error: err?.message ?? String(err) });
430
+ return null;
431
+ }
432
+ }
433
+ /** A blob too large to cache, passed through as it arrives. */
434
+ async function streamThrough(credential, request) {
80
435
  let upstream;
81
436
  try {
82
- const url = new URL(`${await repoEndpoint(request.repo)}/xrpc/com.atproto.space.getBlob`);
83
- url.searchParams.set('space', request.space);
84
- // `repo`, not `did`: a space read names the repo holding the record the
85
- // same way everywhere. A server hosting one account falls back to it,
86
- // which is what makes `did` ever appear to work.
87
- url.searchParams.set('repo', request.repo);
88
- url.searchParams.set('cid', request.cid);
89
- upstream = await credential.fetch(url);
437
+ upstream = await credential.fetch(blobUrlFor(await repoEndpoint(request.repo), request));
90
438
  }
91
439
  catch (err) {
92
440
  emit('spaces', 'blob_error', { space: request.space, repo: request.repo, error: err.message });
93
441
  return new Response('Blob unavailable', { status: 502 });
94
442
  }
95
- if (!upstream.ok)
443
+ if (!upstream.ok) {
96
444
  return new Response('Not found', { status: upstream.status === 401 ? 404 : upstream.status });
97
- const claimed = (upstream.headers.get('content-type') ?? '').split(';')[0].trim();
98
- const contentType = SERVABLE.has(claimed) ? claimed : 'application/octet-stream';
445
+ }
99
446
  return new Response(upstream.body, {
100
447
  status: 200,
101
448
  headers: {
102
- 'content-type': contentType,
103
- // Not a public cache: this response is one viewer's, and the next
104
- // request for the same URL may be somebody with no right to it.
105
- 'cache-control': 'private, no-store',
449
+ 'content-type': servableType(upstream),
450
+ ...cacheHeaders(request),
106
451
  'content-disposition': 'inline',
107
452
  'content-security-policy': "default-src 'none'; sandbox",
108
453
  'x-content-type-options': 'nosniff',
109
454
  },
110
455
  });
111
456
  }
457
+ function servableType(upstream) {
458
+ const claimed = (upstream.headers.get('content-type') ?? '').split(';')[0].trim();
459
+ return SERVABLE.has(claimed) ? claimed : 'application/octet-stream';
460
+ }
@@ -22,7 +22,7 @@ export declare function serverDpopProof(method: string, url: string, accessToken
22
22
  export { handleWriteNotice } from './engine.ts';
23
23
  export { clearPendingNotices, NoticeError, parseWriteNotice, scheduleNoticeSync, verifyNotice, type WriteNotice, } from './notify.ts';
24
24
  export * from './uri.ts';
25
- export { blobCid, spaceBlobUrl } from './blob.ts';
25
+ export { asPreset, blobCid, resetSpaceBlobCache, type SpaceBlobPreset, spaceBlobUrl } from './blob.ts';
26
26
  export { records, requireSpace, spaceRecords } from './records.ts';
27
27
  export declare function spaceServiceId(): string | null;
28
28
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/spaces/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAG7D,OAAO,EAA+D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAOnH,OAAO,EACL,uBAAuB,EACvB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAErF;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAIjB;AACD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AACpB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAWlE,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzG;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,6CAA6C;IAC7C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAqD1D;AAED,wBAAgB,UAAU,IAAI,IAAI,CAMjC;AAED,+FAA+F;AAC/F,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,YAAY,IAAI,aAAa,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/spaces/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAG7D,OAAO,EAA+D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAOnH,OAAO,EACL,uBAAuB,EACvB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAErF;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAIjB;AACD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AACpB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACtG,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAWlE,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9C;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzG;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,6CAA6C;IAC7C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAqD1D;AAED,wBAAgB,UAAU,IAAI,IAAI,CAMjC;AAED,+FAA+F;AAC/F,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,YAAY,IAAI,aAAa,EAAE,CAAA"}
@@ -33,7 +33,7 @@ export async function serverDpopProof(method, url, accessToken, nonce) {
33
33
  export { handleWriteNotice } from "./engine.js";
34
34
  export { clearPendingNotices, NoticeError, parseWriteNotice, scheduleNoticeSync, verifyNotice, } from "./notify.js";
35
35
  export * from "./uri.js";
36
- export { blobCid, spaceBlobUrl } from "./blob.js";
36
+ export { asPreset, blobCid, resetSpaceBlobCache, spaceBlobUrl } from "./blob.js";
37
37
  export { records, requireSpace, spaceRecords } from "./records.js";
38
38
  let sweepTimer = null;
39
39
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.93",
3
+ "version": "0.0.1-alpha.95",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"
@@ -50,9 +50,16 @@
50
50
  "@types/better-sqlite3": "^7.6.13",
51
51
  "@types/react": "^19.2.14",
52
52
  "@vitest/coverage-v8": "^4.1.11",
53
+ "sharp": "^0.35.4",
53
54
  "vite": "^8.0.0"
54
55
  },
55
56
  "peerDependencies": {
57
+ "sharp": "^0.35.4",
56
58
  "vite": "^8.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "sharp": {
62
+ "optional": true
63
+ }
57
64
  }
58
65
  }