@powerhousedao/switchboard 6.2.3-dev.24 → 6.2.3-dev.26

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.
Files changed (44) hide show
  1. package/.tsbuild/test/attachments/byte-grants.test.d.ts +2 -0
  2. package/.tsbuild/test/attachments/byte-grants.test.d.ts.map +1 -0
  3. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts +2 -0
  4. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts.map +1 -0
  5. package/.tsbuild/test/attachments/url-signer.test.d.ts +2 -0
  6. package/.tsbuild/test/attachments/url-signer.test.d.ts.map +1 -0
  7. package/.tsbuild/test/workflow-read-gate.test.d.ts +2 -0
  8. package/.tsbuild/test/workflow-read-gate.test.d.ts.map +1 -0
  9. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  10. package/.tsbuild/tsdown.config.d.ts.map +1 -1
  11. package/.tsbuild/vitest.config.d.ts.map +1 -1
  12. package/Auth.md +55 -2
  13. package/CHANGELOG.md +41 -0
  14. package/dist/index.d.mts +2 -1
  15. package/dist/index.mjs +4 -5
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/install-packages.d.mts +2 -1
  18. package/dist/install-packages.mjs +2 -2
  19. package/dist/install-packages.mjs.map +1 -1
  20. package/dist/migrate.d.mts +2 -1
  21. package/dist/migrate.mjs +4 -3
  22. package/dist/migrate.mjs.map +1 -1
  23. package/dist/{server-BFbus-at.mjs → server-DQCIIJzK.mjs} +236 -39
  24. package/dist/server-DQCIIJzK.mjs.map +1 -0
  25. package/dist/server.d.mts +38 -24
  26. package/dist/server.d.mts.map +1 -1
  27. package/dist/server.mjs +3 -4
  28. package/dist/{utils-BBHt2kdo.mjs → utils-uaBlzyjS.mjs} +3 -3
  29. package/dist/{utils-BBHt2kdo.mjs.map → utils-uaBlzyjS.mjs.map} +1 -1
  30. package/dist/utils.d.mts +4 -5
  31. package/dist/utils.d.mts.map +1 -1
  32. package/dist/utils.mjs +3 -3
  33. package/package.json +18 -18
  34. package/test/attachments/byte-grants.test.ts +287 -0
  35. package/test/attachments/byte-routes-read-gate.integration.test.ts +314 -0
  36. package/test/attachments/download-target.test.ts +194 -31
  37. package/test/attachments/routes-integration.test.ts +20 -5
  38. package/test/attachments/routes.test.ts +43 -25
  39. package/test/attachments/service-config.test.ts +13 -3
  40. package/test/attachments/url-signer.test.ts +105 -0
  41. package/test/workflow/harness.ts +1 -1
  42. package/test/workflow-read-gate.test.ts +145 -0
  43. package/tsdown.config.ts +3 -1
  44. package/dist/server-BFbus-at.mjs.map +0 -1
package/dist/server.d.mts CHANGED
@@ -5,8 +5,9 @@ import { IAttachmentService } from "@powerhousedao/reactor-attachments";
5
5
  import { IRenown } from "@renown/sdk/node";
6
6
  import { DriveInput } from "@powerhousedao/shared/document-drive";
7
7
  import { DocumentModelModule, SignaturePolicy, UpgradeManifest } from "@powerhousedao/shared/document-model";
8
+ import "@powerhousedao/reactor-workflow";
9
+ import "@powerhousedao/shared/processors";
8
10
  import { IRenown as IRenown$1 } from "@renown/sdk";
9
-
10
11
  //#region src/workflow-runtime.d.mts
11
12
  /** Whether the operation intake is indexing. Same shape as the attachment
12
13
  * reference projection, because it is the same limitation. */
@@ -24,26 +25,28 @@ type WorkflowTriggersCapability = {
24
25
  * `powerhouse/reactor-drive` is wired through `addDefaultReactorDrive` and
25
26
  * uses the relationship-based child graph.
26
27
  */
27
- type SwitchboardDriveDocumentType = "powerhouse/document-drive" | "powerhouse/reactor-drive";
28
+ export type SwitchboardDriveDocumentType = "powerhouse/document-drive" | "powerhouse/reactor-drive";
28
29
  /** A {@link DriveInput} extended with the optional drive-container type. */
29
- type SwitchboardDriveInput = DriveInput & {
30
+ export type SwitchboardDriveInput = DriveInput & {
30
31
  documentType?: SwitchboardDriveDocumentType;
31
32
  };
32
- type StorageOptions = {
33
+ export type StorageOptions = {
33
34
  type: "filesystem" | "memory" | "postgres" | "browser";
34
35
  filesystemPath?: string;
35
36
  postgresUrl?: string;
36
37
  };
37
- type IdentityOptions = {
38
- /** Path to the keypair file. Defaults to ~/.ph/keypair.json */keypairPath?: string;
38
+ export type IdentityOptions = {
39
+ /** Path to the keypair file. Defaults to ~/.ph/keypair.json */
40
+ keypairPath?: string;
39
41
  /**
40
42
  * If true, won't start without an existing keypair.
41
43
  * Use this to ensure the switchboard only runs with an authenticated identity.
42
44
  */
43
- requireExisting?: boolean; /** Base url of the Renown instance to use */
45
+ requireExisting?: boolean;
46
+ /** Base url of the Renown instance to use */
44
47
  baseUrl?: string;
45
48
  };
46
- type StartServerOptions = {
49
+ export type StartServerOptions = {
47
50
  /**
48
51
  * Pre-built ReactorClientModule (typically from
49
52
  * `@powerhousedao/reactor#ReactorClientBuilder.buildModule`). When set, the
@@ -102,7 +105,8 @@ type StartServerOptions = {
102
105
  * When configured, the switchboard will load the keypair from `ph login`
103
106
  * and can authenticate with remote services on behalf of the user.
104
107
  */
105
- identity?: IdentityOptions; /** Base URL for the attachment service; defaults to `PH_SWITCHBOARD_PUBLIC_URL` then `http(s)://localhost:${port}`. */
108
+ identity?: IdentityOptions;
109
+ /** Base URL for the attachment service; defaults to `PH_SWITCHBOARD_PUBLIC_URL` then `http(s)://localhost:${port}`. */
106
110
  attachmentServiceUrl?: string;
107
111
  mcp?: boolean;
108
112
  /** Powerhouse workflows: the runtime, its subgraph, its webhooks and its
@@ -167,14 +171,17 @@ type StartServerOptions = {
167
171
  dbPoolSize?: number;
168
172
  };
169
173
  };
170
- type SwitchboardReactor = {
174
+ export type SwitchboardReactor = {
171
175
  defaultDriveUrl: string | undefined;
172
- reactor: IReactorClient; /** Switchboard-backed remote attachment service over its own `/attachments/*` API, for downstream consumers. */
173
- attachmentService: IAttachmentService; /** Whether the authoritative attachment-reference projection is active. */
176
+ reactor: IReactorClient;
177
+ /** Switchboard-backed remote attachment service over its own `/attachments/*` API, for downstream consumers. */
178
+ attachmentService: IAttachmentService;
179
+ /** Whether the authoritative attachment-reference projection is active. */
174
180
  attachmentReferenceProjection: AttachmentReferenceProjectionCapability;
175
181
  /** Whether the workflow runtime's operation intake is indexing. Undefined
176
182
  * when workflows are off; unavailable means no document trigger fires. */
177
- workflowTriggers?: WorkflowTriggersCapability; /** The Renown instance if identity was initialized */
183
+ workflowTriggers?: WorkflowTriggersCapability;
184
+ /** The Renown instance if identity was initialized */
178
185
  renown: IRenown$1 | null;
179
186
  /**
180
187
  * Port the HTTP server actually bound to. May differ from the requested
@@ -198,20 +205,25 @@ type SwitchboardReactor = {
198
205
  //#endregion
199
206
  //#region src/builder-defaults.d.mts
200
207
  type SwitchboardReactorDefaultsOptions = {
201
- documentModels?: DocumentModelModule[]; /** Upgrade manifests for versioned models, one per document type. */
202
- upgradeManifests?: UpgradeManifest<readonly number[]>[]; /** Default true. */
208
+ documentModels?: DocumentModelModule[];
209
+ /** Upgrade manifests for versioned models, one per document type. */
210
+ upgradeManifests?: UpgradeManifest<readonly number[]>[];
211
+ /** Default true. */
203
212
  includeBaseModels?: boolean;
204
213
  /**
205
214
  * Channel scheme. Defaults to `ChannelScheme.SWITCHBOARD`, which populates
206
215
  * `reactorModule.syncModule.syncManager` — required by reactor-api. Set
207
216
  * to `false` only if the caller will configure a scheme themselves.
208
217
  */
209
- channelScheme?: ChannelScheme | false; /** Defaults to true. Set false when the caller owns SIGINT handling. */
210
- signalHandlers?: boolean; /** Executor tuning. Omit to use the reactor's own defaults. */
218
+ channelScheme?: ChannelScheme | false;
219
+ /** Defaults to true. Set false when the caller owns SIGINT handling. */
220
+ signalHandlers?: boolean;
221
+ /** Executor tuning. Omit to use the reactor's own defaults. */
211
222
  executorConfig?: {
212
223
  maxSkipThreshold?: number;
213
224
  featureFlags?: Partial<ReactorFeatureFlags>;
214
- }; /** Wire dynamic document-model loading via HTTP. */
225
+ };
226
+ /** Wire dynamic document-model loading via HTTP. */
215
227
  documentModelLoader?: IDocumentModelLoader;
216
228
  logger?: ILogger;
217
229
  /**
@@ -224,10 +236,11 @@ type SwitchboardReactorDefaultsOptions = {
224
236
  * Which keys may sign as which users (typically from
225
237
  * `getRenownTrustPolicyConfig`); wins over one carried by `signer`.
226
238
  */
227
- trustPolicy?: Pick<SignerConfig, "trustPolicy" | "workerTrustPolicy">; /** What the client creates new documents as; see `resolveCreateSignaturePolicy`. */
239
+ trustPolicy?: Pick<SignerConfig, "trustPolicy" | "workerTrustPolicy">;
240
+ /** What the client creates new documents as; see `resolveCreateSignaturePolicy`. */
228
241
  createSignaturePolicy?: SignaturePolicy;
229
242
  };
230
- declare function applySwitchboardReactorDefaults(reactorBuilder: ReactorBuilder, clientBuilder: ReactorClientBuilder, options?: SwitchboardReactorDefaultsOptions): void;
243
+ export declare function applySwitchboardReactorDefaults(reactorBuilder: ReactorBuilder, clientBuilder: ReactorClientBuilder, options?: SwitchboardReactorDefaultsOptions): void;
231
244
  //#endregion
232
245
  //#region src/server.d.mts
233
246
  /**
@@ -235,9 +248,9 @@ declare function applySwitchboardReactorDefaults(reactorBuilder: ReactorBuilder,
235
248
  * the port is free, false if the OS reports it in use. Any other error is
236
249
  * surfaced so we don't silently mask real issues (permissions, bad host, …).
237
250
  */
238
- declare function isPortAvailable(port: number): Promise<boolean>;
251
+ export declare function isPortAvailable(port: number): Promise<boolean>;
239
252
  /** Derive the remote attachment service config for switchboard's own `/attachments/*` API. */
240
- declare function deriveAttachmentServiceConfig(options: Pick<StartServerOptions, "attachmentServiceUrl" | "https">, serverPort: number, renown: IRenown | null): {
253
+ export declare function deriveAttachmentServiceConfig(options: Pick<StartServerOptions, "attachmentServiceUrl" | "https">, serverPort: number, renown: IRenown | null): {
241
254
  remoteUrl: string;
242
255
  jwtHandler: JwtHandler | undefined;
243
256
  };
@@ -258,7 +271,8 @@ declare function deriveAttachmentServiceConfig(options: Pick<StartServerOptions,
258
271
  * trigger the `withShutdownHook` chain that disposes the api. Programmatic
259
272
  * full teardown isn't currently exposed — wire it via SIGINT/SIGTERM.
260
273
  */
261
- declare const startSwitchboard: (options?: StartServerOptions) => Promise<SwitchboardReactor>;
274
+ export declare const startSwitchboard: (options?: StartServerOptions) => Promise<SwitchboardReactor>;
262
275
  //#endregion
263
- export { IdentityOptions, StartServerOptions, StorageOptions, SwitchboardDriveDocumentType, SwitchboardDriveInput, SwitchboardReactor, type SwitchboardReactorDefaultsOptions, type WorkflowTriggersCapability, applySwitchboardReactorDefaults, deriveAttachmentServiceConfig, isPortAvailable, startSwitchboard };
276
+ export type { SwitchboardReactorDefaultsOptions, WorkflowTriggersCapability };
277
+ export {};
264
278
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../src/workflow-runtime.mts","../src/types.ts","../src/builder-defaults.mts","../src/server.mts"],"mappings":";;;;;;;;;;AAmDA;;AAAA,KAAY,0BAAA;EACN,MAAA;AAAA;EAEA,MAAA;EACA,MAAA;AAAA;;;;;;;AAJN;;KCjCY,4BAAA;;KAKA,qBAAA,GAAwB,UAAA;EAClC,YAAA,GAAe,4BAAA;AAAA;AAAA,KAGL,cAAA;EACV,IAAA;EACA,cAAA;EACA,WAAA;AAAA;AAAA,KAGU,eAAA;EAf4B,+DAiBtC,WAAA;EAjBsC;;AAKxC;;EAiBE,eAAA,YAhB2C;EAmB3C,OAAA;AAAA;AAAA,KAGU,kBAAA;EAtBiC;;AAG7C;;;;;;;;;AAMA;;;;;;;;;EAkCE,OAAA,GAAU,4BAAA;EArBkB;;;;;;;EA6B5B,kBAAA;EAmDgB;;;;;;EA5ChB,WAAA;EACA,UAAA;EACA,IAAA;EAQA;;;;;EAFA,UAAA;EACA,GAAA;EACA,MAAA;EACA,KAAA,GAAQ,qBAAA;EACR,QAAA;EACA,YAAA;EACA,KAAA;IAEM,OAAA;IACA,QAAA;EAAA;EAkBY;;;;;EATlB,QAAA,GAAW,eAAA,EA4BX;EA1BA,oBAAA;EACA,GAAA;EA6CE;;EA1CF,SAAA;IACE,OAAA;EAAA;EAEF,eAAA,GAAkB,GAAA;EAClB,oBAAA;EACA,4BAAA;EAwDU;;;;;EAlDV,mBAAA;EACA,MAAA,GAAS,OAAA;EA4DD;;;;;;;;;EAlDR,aAAA;EA6C+B;;;;;;;;;EAnC/B,cAAA;;;;AC7HF;;;;;EDsIE,UAAA;IACE,UAAA;IACA,mBAAA;IACA,gBAAA;EAAA;EC3GO;;;;;;;;;EDsHT,gBAAA;IACE,OAAA;IACA,UAAA;EAAA;AAAA;AAAA,KAIQ,kBAAA;EACV,eAAA;EACA,OAAA,EAAS,cAAA,ECxIQ;ED0IjB,iBAAA,EAAmB,kBAAA,ECvInB;EDyIA,6BAAA,EAA+B,uCAAA;ECxI/B;;ED2IA,gBAAA,GAAmB,0BAAA,ECrIV;EDuIT,MAAA,EAAQ,SAAA;EClIM;;;;EDuId,IAAA;ECrIuC;AAsBzC;;;;;;;;;;;ED4HE,QAAA,QAAgB,OAAA;AAAA;;;KCvLN,iCAAA;EAIV,cAAA,GAAiB,mBAAA;EAEjB,gBAAA,GAAmB,eAAA;EAEnB,iBAAA;;;AFsBF;;;EEhBE,aAAA,GAAgB,aAAA,UFiBZ;EEfJ,cAAA,YFkBI;EEhBJ,cAAA;IACE,gBAAA;IACA,YAAA,GAAe,OAAA,CAAQ,mBAAA;EAAA;EAGzB,mBAAA,GAAsB,oBAAA;EACtB,MAAA,GAAS,OAAA;;;;ADtBX;;EC4BE,MAAA,GAAS,YAAA;ED3BkC;;;;ECgC3C,WAAA,GAAc,IAAA,CAAK,YAAA,wCDhCwB;ECkC3C,qBAAA,GAAwB,eAAA;AAAA;AAAA,iBAsBV,+BAAA,CACd,cAAA,EAAgB,cAAA,EAChB,aAAA,EAAe,oBAAA,EACf,OAAA,GAAS,iCAAA;;;;;;;;iBC4DK,eAAA,CAAgB,IAAA,WAAe,OAAA;;iBAoI/B,6BAAA,CACd,OAAA,EAAS,IAAA,CAAK,kBAAA,qCACd,UAAA,UACA,MAAA,EAAQ,OAAA;EACL,SAAA;EAAmB,UAAA,EAAY,UAAA;AAAA;;;;;;;;;;AFrQpC;;;;;AAKA;;;cE0oCa,gBAAA,GACX,OAAA,GAAS,kBAAA,KACR,OAAA,CAAQ,kBAAA"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../src/workflow-runtime.mts","../src/types.ts","../src/builder-defaults.mts","../src/server.mts"],"mappings":";;;;;;;;;;;;;KAoDY;EACN;;EAEA;EACA;;;;;;;;;;YCtCM;;YAKA,wBAAwB;EAClC,eAAe;;YAGL;EACV;EACA;EACA;;YAGU;;EAEV;;;;;EAKA;;EAGA;;YAGU;;;;;;;;;;;;;;;;;;;;;EAqBV,UAAU;;;;;;;;EAQV;;;;;;;EAOA;EACA;EACA;;;;;;EAMA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;IAEM;IACA;;;;;;;EASN,WAAW;;EAEX;EACA;;;EAGA;IACE;;EAEF,kBAAkB;EAClB;EACA;;;;;;EAMA;EACA,SAAS;;;;;;;;;;EAUT;;;;;;;;;;EAUA;;;;;;;;;EASA;IACE;IACA;IACA;;;;;;;;;;;EAWF;IACE;IACA;;;YAIQ;EACV;EACA,SAAS;;EAET,mBAAmB;;EAEnB,+BAA+B;;;EAG/B,mBAAmB;;EAEnB,QAAQ;;;;;EAKR;;;;;;;;;;;;;EAaA,gBAAgB;;;;KCvLN;EAIV,iBAAiB;;EAEjB,mBAAmB;;EAEnB;;;;;;EAMA,gBAAgB;;EAEhB;;EAEA;IACE;IACA,eAAe,QAAQ;;;EAGzB,sBAAsB;EACtB,SAAS;;;;;;EAMT,SAAS;;;;;EAKT,cAAc,KAAK;;EAEnB,wBAAwB;;wBAsBV,gCACd,gBAAgB,gBAChB,eAAe,sBACf,UAAS;;;;;;;;wBC6DK,gBAAgB,eAAe;;wBAoI/B,8BACd,SAAS,KAAK,uDACd,oBACA,QAAQ;EACL;EAAmB,YAAY;;;;;;;;;;;;;;;;;;;qBAm5BvB,mBAAgB,UAClB,uBACR,QAAQ"}
package/dist/server.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as applySwitchboardReactorDefaults, n as isPortAvailable, r as startSwitchboard, t as deriveAttachmentServiceConfig } from "./server-BFbus-at.mjs";
3
- import "./utils-BBHt2kdo.mjs";
2
+ import { i as applySwitchboardReactorDefaults, n as isPortAvailable, r as startSwitchboard, t as deriveAttachmentServiceConfig } from "./server-DQCIIJzK.mjs";
4
3
  export { applySwitchboardReactorDefaults, deriveAttachmentServiceConfig, isPortAvailable, startSwitchboard };
5
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="89634554-85dd-5e2b-99fe-485c1c36fe72")}catch(e){}}();
6
- //# debugId=89634554-85dd-5e2b-99fe-485c1c36fe72
4
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c9f85b61-fdcb-5fe1-9b94-cceba4f2a89d")}catch(e){}}();
5
+ //# debugId=c9f85b61-fdcb-5fe1-9b94-cceba4f2a89d
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a69847ec-becf-5c25-988b-cd47759b2c19")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5ece528d-aafb-5305-a04d-e0c45c8c30a3")}catch(e){}}();
3
3
  import { reactorDriveCreateDocument, reactorDriveCreateState } from "@powerhousedao/reactor-drive";
4
4
  import { driveCreateDocument, driveCreateState } from "@powerhousedao/shared/document-drive";
5
5
  import { withSignaturePolicy } from "@powerhousedao/shared/document-model";
@@ -82,5 +82,5 @@ function isPostgresUrl(url) {
82
82
  //#endregion
83
83
  export { addDefaultReactorDrive as n, isPostgresUrl as r, addDefaultDrive as t };
84
84
 
85
- //# sourceMappingURL=utils-BBHt2kdo.mjs.map
86
- //# debugId=a69847ec-becf-5c25-988b-cd47759b2c19
85
+ //# sourceMappingURL=utils-uaBlzyjS.mjs.map
86
+ //# debugId=5ece528d-aafb-5305-a04d-e0c45c8c30a3
@@ -1 +1 @@
1
- {"version":3,"file":"utils-BBHt2kdo.mjs","sources":["../src/utils.mts"],"sourcesContent":["import type { IReactorClient } from \"@powerhousedao/reactor\";\nimport {\n reactorDriveCreateDocument,\n reactorDriveCreateState,\n} from \"@powerhousedao/reactor-drive\";\nimport {\n driveCreateDocument,\n driveCreateState,\n} from \"@powerhousedao/shared/document-drive\";\nimport type { DriveInput } from \"@powerhousedao/shared/document-drive\";\nimport {\n withSignaturePolicy,\n type PHDocument,\n} from \"@powerhousedao/shared/document-model\";\n\n/**\n * A configured id pins the drive, and only a legacy drive can take a chosen\n * id; otherwise the client's creation default applies.\n */\nasync function underCreatePolicy<TDocument extends PHDocument>(\n client: IReactorClient,\n document: TDocument,\n id: string | undefined,\n): Promise<TDocument> {\n return id\n ? withSignaturePolicy(document, \"legacy\", { id })\n : withSignaturePolicy(document, await client.getCreateSignaturePolicy());\n}\n\nexport async function addDefaultDrive(\n client: IReactorClient,\n drive: DriveInput,\n serverPort: number,\n) {\n let driveId = drive.id;\n if (!driveId || driveId.length === 0) {\n driveId = drive.slug;\n }\n\n if (!driveId || driveId.length === 0) {\n throw new Error(\"Invalid Drive Id\");\n }\n\n // check if the drive already exists\n let existingDrive;\n try {\n existingDrive = await client.get(driveId);\n } catch {\n //\n }\n\n // already exists, return the existing drive url\n if (existingDrive) {\n return `http://localhost:${serverPort}/d/${driveId}`;\n }\n\n const { global } = driveCreateState();\n const created = driveCreateDocument({\n global: {\n ...global,\n name: drive.global.name,\n icon: drive.global.icon ?? global.icon,\n },\n local: {\n availableOffline: drive.local?.availableOffline ?? false,\n sharingType: drive.local?.sharingType ?? \"public\",\n listeners: drive.local?.listeners ?? [],\n triggers: drive.local?.triggers ?? [],\n },\n });\n\n const document = await underCreatePolicy(client, created, drive.id);\n if (drive.slug && drive.slug.length > 0) {\n document.header.slug = drive.slug;\n }\n if (drive.global.name) {\n document.header.name = drive.global.name;\n }\n if (drive.preferredEditor) {\n document.header.meta = { preferredEditor: drive.preferredEditor };\n }\n\n try {\n await client.create(document);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : String(e);\n if (!errorMessage.includes(\"already exists\")) {\n throw e;\n }\n }\n\n return `http://localhost:${serverPort}/d/${driveId}`;\n}\n\nexport async function addDefaultReactorDrive(\n client: IReactorClient,\n drive: DriveInput,\n serverPort: number,\n) {\n let driveId = drive.id;\n if (!driveId || driveId.length === 0) {\n driveId = drive.slug;\n }\n\n if (!driveId || driveId.length === 0) {\n throw new Error(\"Invalid Drive Id\");\n }\n\n let existingDrive;\n try {\n existingDrive = await client.get(driveId);\n } catch {\n //\n }\n\n if (existingDrive) {\n return `http://localhost:${serverPort}/d/${driveId}`;\n }\n\n const { global, local } = reactorDriveCreateState();\n const created = reactorDriveCreateDocument({\n global: {\n ...global,\n name: drive.global.name,\n icon: drive.global.icon ?? global.icon,\n },\n local: {\n ...local,\n availableOffline: drive.local?.availableOffline ?? local.availableOffline,\n sharingType: drive.local?.sharingType ?? local.sharingType,\n },\n });\n\n const document = await underCreatePolicy(client, created, drive.id);\n if (drive.slug && drive.slug.length > 0) {\n document.header.slug = drive.slug;\n }\n if (drive.global.name) {\n document.header.name = drive.global.name;\n }\n if (drive.preferredEditor) {\n document.header.meta = { preferredEditor: drive.preferredEditor };\n }\n\n try {\n await client.create(document);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : String(e);\n if (!errorMessage.includes(\"already exists\")) {\n throw e;\n }\n }\n\n return `http://localhost:${serverPort}/d/${driveId}`;\n}\n\nexport function isPostgresUrl(url: string) {\n return url.startsWith(\"postgresql\") || url.startsWith(\"postgres\");\n}\n"],"names":[],"mappings":";;;;;;;;;;AAmBA,eAAe,kBACb,QACA,UACA,IACoB;AACpB,QAAO,KACH,oBAAoB,UAAU,UAAU,EAAE,IAAI,CAAC,GAC/C,oBAAoB,UAAU,MAAM,OAAO,0BAA0B,CAAC;;AAG5E,eAAsB,gBACpB,QACA,OACA,YACA;CACA,IAAI,UAAU,MAAM;AACpB,KAAI,CAAC,WAAW,QAAQ,WAAW,EACjC,WAAU,MAAM;AAGlB,KAAI,CAAC,WAAW,QAAQ,WAAW,EACjC,OAAM,IAAI,MAAM,mBAAmB;CAIrC,IAAI;AACJ,KAAI;AACF,kBAAgB,MAAM,OAAO,IAAI,QAAQ;SACnC;AAKR,KAAI,cACF,QAAO,oBAAoB,WAAW,KAAK;CAG7C,MAAM,EAAE,WAAW,kBAAkB;CAerC,MAAM,WAAW,MAAM,kBAAkB,QAdzB,oBAAoB;EAClC,QAAQ;GACN,GAAG;GACH,MAAM,MAAM,OAAO;GACnB,MAAM,MAAM,OAAO,QAAQ,OAAO;GACnC;EACD,OAAO;GACL,kBAAkB,MAAM,OAAO,oBAAoB;GACnD,aAAa,MAAM,OAAO,eAAe;GACzC,WAAW,MAAM,OAAO,aAAa,EAAE;GACvC,UAAU,MAAM,OAAO,YAAY,EAAE;GACtC;EACF,CAAC,EAEwD,MAAM,GAAG;AACnE,KAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EACpC,UAAS,OAAO,OAAO,MAAM;AAE/B,KAAI,MAAM,OAAO,KACf,UAAS,OAAO,OAAO,MAAM,OAAO;AAEtC,KAAI,MAAM,gBACR,UAAS,OAAO,OAAO,EAAE,iBAAiB,MAAM,iBAAiB;AAGnE,KAAI;AACF,QAAM,OAAO,OAAO,SAAS;UACtB,GAAG;AAEV,MAAI,EADiB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,EAC7C,SAAS,iBAAiB,CAC1C,OAAM;;AAIV,QAAO,oBAAoB,WAAW,KAAK;;AAG7C,eAAsB,uBACpB,QACA,OACA,YACA;CACA,IAAI,UAAU,MAAM;AACpB,KAAI,CAAC,WAAW,QAAQ,WAAW,EACjC,WAAU,MAAM;AAGlB,KAAI,CAAC,WAAW,QAAQ,WAAW,EACjC,OAAM,IAAI,MAAM,mBAAmB;CAGrC,IAAI;AACJ,KAAI;AACF,kBAAgB,MAAM,OAAO,IAAI,QAAQ;SACnC;AAIR,KAAI,cACF,QAAO,oBAAoB,WAAW,KAAK;CAG7C,MAAM,EAAE,QAAQ,UAAU,yBAAyB;CAcnD,MAAM,WAAW,MAAM,kBAAkB,QAbzB,2BAA2B;EACzC,QAAQ;GACN,GAAG;GACH,MAAM,MAAM,OAAO;GACnB,MAAM,MAAM,OAAO,QAAQ,OAAO;GACnC;EACD,OAAO;GACL,GAAG;GACH,kBAAkB,MAAM,OAAO,oBAAoB,MAAM;GACzD,aAAa,MAAM,OAAO,eAAe,MAAM;GAChD;EACF,CAAC,EAEwD,MAAM,GAAG;AACnE,KAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EACpC,UAAS,OAAO,OAAO,MAAM;AAE/B,KAAI,MAAM,OAAO,KACf,UAAS,OAAO,OAAO,MAAM,OAAO;AAEtC,KAAI,MAAM,gBACR,UAAS,OAAO,OAAO,EAAE,iBAAiB,MAAM,iBAAiB;AAGnE,KAAI;AACF,QAAM,OAAO,OAAO,SAAS;UACtB,GAAG;AAEV,MAAI,EADiB,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,EAC7C,SAAS,iBAAiB,CAC1C,OAAM;;AAIV,QAAO,oBAAoB,WAAW,KAAK;;AAG7C,SAAgB,cAAc,KAAa;AACzC,QAAO,IAAI,WAAW,aAAa,IAAI,IAAI,WAAW,WAAW","debug_id":"a69847ec-becf-5c25-988b-cd47759b2c19"}
1
+ {"version":3,"file":"utils-uaBlzyjS.mjs","sources":["../src/utils.mts"],"sourcesContent":["import type { IReactorClient } from \"@powerhousedao/reactor\";\nimport {\n reactorDriveCreateDocument,\n reactorDriveCreateState,\n} from \"@powerhousedao/reactor-drive\";\nimport {\n driveCreateDocument,\n driveCreateState,\n} from \"@powerhousedao/shared/document-drive\";\nimport type { DriveInput } from \"@powerhousedao/shared/document-drive\";\nimport {\n withSignaturePolicy,\n type PHDocument,\n} from \"@powerhousedao/shared/document-model\";\n\n/**\n * A configured id pins the drive, and only a legacy drive can take a chosen\n * id; otherwise the client's creation default applies.\n */\nasync function underCreatePolicy<TDocument extends PHDocument>(\n client: IReactorClient,\n document: TDocument,\n id: string | undefined,\n): Promise<TDocument> {\n return id\n ? withSignaturePolicy(document, \"legacy\", { id })\n : withSignaturePolicy(document, await client.getCreateSignaturePolicy());\n}\n\nexport async function addDefaultDrive(\n client: IReactorClient,\n drive: DriveInput,\n serverPort: number,\n) {\n let driveId = drive.id;\n if (!driveId || driveId.length === 0) {\n driveId = drive.slug;\n }\n\n if (!driveId || driveId.length === 0) {\n throw new Error(\"Invalid Drive Id\");\n }\n\n // check if the drive already exists\n let existingDrive;\n try {\n existingDrive = await client.get(driveId);\n } catch {\n //\n }\n\n // already exists, return the existing drive url\n if (existingDrive) {\n return `http://localhost:${serverPort}/d/${driveId}`;\n }\n\n const { global } = driveCreateState();\n const created = driveCreateDocument({\n global: {\n ...global,\n name: drive.global.name,\n icon: drive.global.icon ?? global.icon,\n },\n local: {\n availableOffline: drive.local?.availableOffline ?? false,\n sharingType: drive.local?.sharingType ?? \"public\",\n listeners: drive.local?.listeners ?? [],\n triggers: drive.local?.triggers ?? [],\n },\n });\n\n const document = await underCreatePolicy(client, created, drive.id);\n if (drive.slug && drive.slug.length > 0) {\n document.header.slug = drive.slug;\n }\n if (drive.global.name) {\n document.header.name = drive.global.name;\n }\n if (drive.preferredEditor) {\n document.header.meta = { preferredEditor: drive.preferredEditor };\n }\n\n try {\n await client.create(document);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : String(e);\n if (!errorMessage.includes(\"already exists\")) {\n throw e;\n }\n }\n\n return `http://localhost:${serverPort}/d/${driveId}`;\n}\n\nexport async function addDefaultReactorDrive(\n client: IReactorClient,\n drive: DriveInput,\n serverPort: number,\n) {\n let driveId = drive.id;\n if (!driveId || driveId.length === 0) {\n driveId = drive.slug;\n }\n\n if (!driveId || driveId.length === 0) {\n throw new Error(\"Invalid Drive Id\");\n }\n\n let existingDrive;\n try {\n existingDrive = await client.get(driveId);\n } catch {\n //\n }\n\n if (existingDrive) {\n return `http://localhost:${serverPort}/d/${driveId}`;\n }\n\n const { global, local } = reactorDriveCreateState();\n const created = reactorDriveCreateDocument({\n global: {\n ...global,\n name: drive.global.name,\n icon: drive.global.icon ?? global.icon,\n },\n local: {\n ...local,\n availableOffline: drive.local?.availableOffline ?? local.availableOffline,\n sharingType: drive.local?.sharingType ?? local.sharingType,\n },\n });\n\n const document = await underCreatePolicy(client, created, drive.id);\n if (drive.slug && drive.slug.length > 0) {\n document.header.slug = drive.slug;\n }\n if (drive.global.name) {\n document.header.name = drive.global.name;\n }\n if (drive.preferredEditor) {\n document.header.meta = { preferredEditor: drive.preferredEditor };\n }\n\n try {\n await client.create(document);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : String(e);\n if (!errorMessage.includes(\"already exists\")) {\n throw e;\n }\n }\n\n return `http://localhost:${serverPort}/d/${driveId}`;\n}\n\nexport function isPostgresUrl(url: string) {\n return url.startsWith(\"postgresql\") || url.startsWith(\"postgres\");\n}\n"],"names":[],"mappings":"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","debug_id":"5ece528d-aafb-5305-a04d-e0c45c8c30a3"}
package/dist/utils.d.mts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { IReactorClient } from "@powerhousedao/reactor";
2
2
  import { DriveInput } from "@powerhousedao/shared/document-drive";
3
-
4
3
  //#region src/utils.d.mts
5
- declare function addDefaultDrive(client: IReactorClient, drive: DriveInput, serverPort: number): Promise<string>;
6
- declare function addDefaultReactorDrive(client: IReactorClient, drive: DriveInput, serverPort: number): Promise<string>;
7
- declare function isPostgresUrl(url: string): boolean;
4
+ export declare function addDefaultDrive(client: IReactorClient, drive: DriveInput, serverPort: number): Promise<string>;
5
+ export declare function addDefaultReactorDrive(client: IReactorClient, drive: DriveInput, serverPort: number): Promise<string>;
6
+ export declare function isPostgresUrl(url: string): boolean;
8
7
  //#endregion
9
- export { addDefaultDrive, addDefaultReactorDrive, isPostgresUrl };
8
+ export {};
10
9
  //# sourceMappingURL=utils.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.mts","names":[],"sources":["../src/utils.mts"],"mappings":";;;;iBA6BsB,eAAA,CACpB,MAAA,EAAQ,cAAA,EACR,KAAA,EAAO,UAAA,EACP,UAAA,WAAkB,OAAA;AAAA,iBA8DE,sBAAA,CACpB,MAAA,EAAQ,cAAA,EACR,KAAA,EAAO,UAAA,EACP,UAAA,WAAkB,OAAA;AAAA,iBA2DJ,aAAA,CAAc,GAAA"}
1
+ {"version":3,"file":"utils.d.mts","names":[],"sources":["../src/utils.mts"],"mappings":";;;wBA6BsB,gBACpB,QAAQ,gBACR,OAAO,YACP,qBAAkB;wBA8DE,uBACpB,QAAQ,gBACR,OAAO,YACP,qBAAkB;wBA2DJ,cAAc"}
package/dist/utils.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-BBHt2kdo.mjs";
1
+ import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-uaBlzyjS.mjs";
2
2
  export { addDefaultDrive, addDefaultReactorDrive, isPostgresUrl };
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4dfe0eb9-58d9-5240-969e-d37feaef290c")}catch(e){}}();
4
- //# debugId=4dfe0eb9-58d9-5240-969e-d37feaef290c
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e030fa72-327e-52e2-9ac0-68feb4ef300a")}catch(e){}}();
4
+ //# debugId=e030fa72-327e-52e2-9ac0-68feb4ef300a
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@powerhousedao/switchboard",
3
3
  "type": "module",
4
- "version": "6.2.3-dev.24",
4
+ "version": "6.2.3-dev.26",
5
5
  "main": "dist/index.mjs",
6
6
  "exports": {
7
7
  ".": {
@@ -41,7 +41,7 @@
41
41
  "@openfeature/core": "1.9.1",
42
42
  "@openfeature/env-var-provider": "0.3.1",
43
43
  "@openfeature/server-sdk": "1.19.0",
44
- "@powerhousedao/config": "6.2.3-dev.24",
44
+ "@powerhousedao/config": "6.2.3-dev.26",
45
45
  "@opentelemetry/api": "^1.9.0",
46
46
  "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
47
47
  "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
@@ -55,36 +55,36 @@
55
55
  "@opentelemetry/sdk-node": "^0.218.0",
56
56
  "@opentelemetry/sdk-trace-base": "^2.7.1",
57
57
  "@opentelemetry/semantic-conventions": "^1.41.1",
58
- "@powerhousedao/opentelemetry-instrumentation-reactor": "6.2.3-dev.24",
59
- "@powerhousedao/reactor": "6.2.3-dev.24",
60
- "@powerhousedao/shared": "6.2.3-dev.24",
61
- "@powerhousedao/vetra": "6.2.3-dev.24",
62
- "@powerhousedao/reactor-api": "6.2.3-dev.24",
63
- "@powerhousedao/reactor-attachments": "6.2.3-dev.24",
64
- "@powerhousedao/reactor-drive": "6.2.3-dev.24",
65
- "@powerhousedao/reactor-group": "6.2.3-dev.24",
66
- "@powerhousedao/reactor-workflow": "6.2.3-dev.24",
67
- "@powerhousedao/workflow": "6.2.3-dev.24",
68
- "@powerhousedao/pglite-fs": "6.2.3-dev.24",
58
+ "@powerhousedao/opentelemetry-instrumentation-reactor": "6.2.3-dev.26",
59
+ "@powerhousedao/reactor": "6.2.3-dev.26",
60
+ "@powerhousedao/shared": "6.2.3-dev.26",
61
+ "@powerhousedao/vetra": "6.2.3-dev.26",
62
+ "@powerhousedao/reactor-api": "6.2.3-dev.26",
63
+ "@powerhousedao/reactor-attachments": "6.2.3-dev.26",
64
+ "@powerhousedao/reactor-drive": "6.2.3-dev.26",
65
+ "@powerhousedao/reactor-group": "6.2.3-dev.26",
66
+ "@powerhousedao/reactor-workflow": "6.2.3-dev.26",
67
+ "@powerhousedao/workflow": "6.2.3-dev.26",
68
+ "@powerhousedao/pglite-fs": "6.2.3-dev.26",
69
69
  "@pyroscope/nodejs": "^0.4.5",
70
- "@renown/sdk": "6.2.3-dev.24",
70
+ "@renown/sdk": "6.2.3-dev.26",
71
71
  "@sentry/node": "^10.53.1",
72
72
  "@sentry/opentelemetry": "^10.53.1",
73
- "document-model": "6.2.3-dev.24",
73
+ "document-model": "6.2.3-dev.26",
74
74
  "dotenv": "^16.4.7",
75
- "express": "^4.21.2",
75
+ "express": "^4.22.1",
76
76
  "graphql": "^16",
77
77
  "graphql-tag": "^2",
78
78
  "kysely": "0.28.16",
79
79
  "kysely-pglite-dialect": "1.2.0",
80
80
  "pg": "8.18.0",
81
- "vite": "8.0.10"
81
+ "vite": "8.3.0"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@types/express": "^4.17.25",
85
85
  "@types/node": "25.2.3",
86
86
  "@types/pg": "8.16.0",
87
- "tsdown": "0.21.1",
87
+ "tsdown": "0.23.0",
88
88
  "concurrently": "9.2.1",
89
89
  "nodemon": "3.1.11",
90
90
  "react": "19.2.6",
@@ -0,0 +1,287 @@
1
+ import type { AttachmentHash } from "@powerhousedao/reactor";
2
+ import type { AttachmentBuildResult } from "@powerhousedao/reactor-attachments";
3
+ import type {
4
+ AttachmentAccessResult,
5
+ IAttachmentAccessService,
6
+ } from "@powerhousedao/reactor-api";
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+ import { Writable } from "node:stream";
9
+ import { describe, expect, it, vi } from "vitest";
10
+ import type { AttachmentActorContext } from "../../src/attachments/auth.js";
11
+ import {
12
+ makeDownloadHandler,
13
+ makeStatHandler,
14
+ } from "../../src/attachments/routes.js";
15
+ import { AttachmentUrlSigner } from "../../src/attachments/url-signer.js";
16
+
17
+ const HASH = "c".repeat(64) as AttachmentHash;
18
+ const REF = `attachment://v1:${HASH}`;
19
+ const NOW_MS = Date.parse("2026-09-24T00:00:00.000Z");
20
+ const SIGNER = new AttachmentUrlSigner("s".repeat(32), () => NOW_MS);
21
+
22
+ const ACTOR: AttachmentActorContext = {
23
+ user: {
24
+ address: "0xreader",
25
+ chainId: 1,
26
+ networkId: "mainnet",
27
+ appKey: "did:key:zApp",
28
+ },
29
+ authEnabled: true,
30
+ };
31
+
32
+ const HEADER = {
33
+ hash: HASH,
34
+ status: "available",
35
+ mimeType: "text/plain",
36
+ fileName: "a.txt",
37
+ sizeBytes: 0,
38
+ extension: "txt",
39
+ createdAtUtc: "2026-09-01T00:00:00.000Z",
40
+ lastAccessedAtUtc: "2026-09-01T00:00:00.000Z",
41
+ expiresAtUtc: null,
42
+ };
43
+
44
+ function makeReq(query: string): IncomingMessage {
45
+ return {
46
+ method: "GET",
47
+ url: `/attachments/${HASH}${query}`,
48
+ headers: {},
49
+ params: { hash: HASH },
50
+ once: () => undefined,
51
+ } as unknown as IncomingMessage;
52
+ }
53
+
54
+ function makeRes() {
55
+ const headers: Record<string, string> = {};
56
+ const chunks: Buffer[] = [];
57
+ const res = new Writable({
58
+ write(chunk: Buffer, _encoding, callback) {
59
+ chunks.push(Buffer.from(chunk));
60
+ callback();
61
+ },
62
+ });
63
+ Object.assign(res, {
64
+ statusCode: 200,
65
+ setHeader(name: string, value: string | number | readonly string[]) {
66
+ headers[name.toLowerCase()] = String(value);
67
+ },
68
+ });
69
+ Object.defineProperty(res, "_headers", { get: () => headers });
70
+ Object.defineProperty(res, "_body", {
71
+ get: () => Buffer.concat(chunks).toString("utf8"),
72
+ });
73
+ return res as unknown as ServerResponse & {
74
+ readonly _headers: Record<string, string>;
75
+ readonly _body: string;
76
+ };
77
+ }
78
+
79
+ function makeAttachments() {
80
+ const stat = vi.fn().mockResolvedValue(HEADER);
81
+ const get = vi.fn().mockResolvedValue({
82
+ header: HEADER,
83
+ body: new ReadableStream<Uint8Array>({ start: (c) => c.close() }),
84
+ });
85
+ return {
86
+ attachments: { store: { stat, get } } as unknown as AttachmentBuildResult,
87
+ stat,
88
+ get,
89
+ };
90
+ }
91
+
92
+ function makeAccess(result: AttachmentAccessResult | Error) {
93
+ const canReadAttachment = vi.fn(() =>
94
+ result instanceof Error ? Promise.reject(result) : Promise.resolve(result),
95
+ );
96
+ return {
97
+ access: { canReadAttachment } as IAttachmentAccessService,
98
+ canReadAttachment,
99
+ };
100
+ }
101
+
102
+ const ALLOWED: AttachmentAccessResult = {
103
+ kind: "allowed",
104
+ documentId: "canonical-doc" as never,
105
+ ref: REF as never,
106
+ };
107
+
108
+ function signedQuery(documentId = "doc-1", ttl = 300): string {
109
+ return `?${SIGNER.sign(HASH, documentId, ttl).query}`;
110
+ }
111
+
112
+ describe.each([
113
+ ["HEAD", makeStatHandler, "stat"],
114
+ ["GET", makeDownloadHandler, "get"],
115
+ ] as const)("%s /attachments/:hash grants", (_, makeHandler, storeCall) => {
116
+ async function run(
117
+ query: string,
118
+ access: IAttachmentAccessService,
119
+ actor: AttachmentActorContext | undefined = ACTOR,
120
+ signer: AttachmentUrlSigner | null = SIGNER,
121
+ ) {
122
+ const store = makeAttachments();
123
+ const res = makeRes();
124
+ await makeHandler(store.attachments, access, signer)(
125
+ makeReq(query),
126
+ res,
127
+ undefined,
128
+ actor,
129
+ );
130
+ return { res, store: store[storeCall] };
131
+ }
132
+
133
+ it("refuses a hash-only request with 404 without consulting access or storage", async () => {
134
+ const { access, canReadAttachment } = makeAccess(ALLOWED);
135
+ const { res, store } = await run("", access);
136
+
137
+ expect(res.statusCode).toBe(404);
138
+ expect(JSON.parse(res._body)).toEqual({ error: "Attachment not found" });
139
+ expect(canReadAttachment).not.toHaveBeenCalled();
140
+ expect(store).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it.each([
144
+ ["duplicated", "?documentId=a&documentId=b"],
145
+ ["blank", "?documentId=%20"],
146
+ ])("refuses a %s documentId with 404", async (_, query) => {
147
+ const { access, canReadAttachment } = makeAccess(ALLOWED);
148
+ const { res } = await run(query, access);
149
+
150
+ expect(res.statusCode).toBe(404);
151
+ expect(canReadAttachment).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("authorizes a documentId through the access service as the caller", async () => {
155
+ const { access, canReadAttachment } = makeAccess(ALLOWED);
156
+ const { res, store } = await run("?documentId=doc-1", access);
157
+
158
+ expect(res.statusCode).toBe(200);
159
+ expect(res._headers["cache-control"]).toBe("private");
160
+ expect(canReadAttachment).toHaveBeenCalledWith({
161
+ documentId: "doc-1",
162
+ attachmentRef: REF,
163
+ userAddress: "0xreader",
164
+ appKey: "did:key:zApp",
165
+ });
166
+ expect(store).toHaveBeenCalledOnce();
167
+ });
168
+
169
+ it("passes the anonymous caller as an empty subject", async () => {
170
+ const { access, canReadAttachment } = makeAccess(ALLOWED);
171
+ await run("?documentId=doc-1", access, {
172
+ user: undefined,
173
+ authEnabled: true,
174
+ });
175
+
176
+ expect(canReadAttachment).toHaveBeenCalledWith(
177
+ expect.objectContaining({ userAddress: undefined, appKey: undefined }),
178
+ );
179
+ });
180
+
181
+ it("answers a denied decision with the same 404 as an unknown hash", async () => {
182
+ const { access } = makeAccess({ kind: "denied" });
183
+ const { res, store } = await run("?documentId=doc-1", access);
184
+
185
+ expect(res.statusCode).toBe(404);
186
+ expect(JSON.parse(res._body)).toEqual({ error: "Attachment not found" });
187
+ expect(store).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it("answers projection-unavailable with 503", async () => {
191
+ const { access } = makeAccess({ kind: "projection-unavailable" });
192
+ const { res, store } = await run("?documentId=doc-1", access);
193
+
194
+ expect(res.statusCode).toBe(503);
195
+ expect(store).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("answers an access failure with an opaque 500", async () => {
199
+ const { access } = makeAccess(new Error("db down"));
200
+ const { res, store } = await run("?documentId=doc-1", access);
201
+
202
+ expect(res.statusCode).toBe(500);
203
+ expect(res._body).not.toContain("db down");
204
+ expect(store).not.toHaveBeenCalled();
205
+ });
206
+
207
+ it("serves a valid signed URL without re-running the gate, even anonymously", async () => {
208
+ const { access, canReadAttachment } = makeAccess({ kind: "denied" });
209
+ const { res, store } = await run(signedQuery(), access, {
210
+ user: undefined,
211
+ authEnabled: true,
212
+ });
213
+
214
+ expect(res.statusCode).toBe(200);
215
+ expect(canReadAttachment).not.toHaveBeenCalled();
216
+ expect(store).toHaveBeenCalledOnce();
217
+ });
218
+
219
+ it.each([
220
+ [
221
+ "tampered documentId",
222
+ () => signedQuery().replace("documentId=doc-1", "documentId=doc-2"),
223
+ ],
224
+ [
225
+ "tampered signature",
226
+ () =>
227
+ signedQuery().replace(
228
+ /signature=(.)/,
229
+ (_, c: string) => `signature=${c === "A" ? "B" : "A"}`,
230
+ ),
231
+ ],
232
+ [
233
+ "extended expiry",
234
+ () => signedQuery().replace(/expires=\d+/, "expires=9999999999"),
235
+ ],
236
+ ["missing signature", () => signedQuery().replace(/&signature=[^&]+/, "")],
237
+ ["missing expiry", () => signedQuery().replace(/&expires=\d+/, "")],
238
+ ])(
239
+ "refuses a %s with 404 and never falls back to the gate",
240
+ async (_, query) => {
241
+ const { access, canReadAttachment } = makeAccess(ALLOWED);
242
+ const { res, store } = await run(query(), access);
243
+
244
+ expect(res.statusCode).toBe(404);
245
+ expect(canReadAttachment).not.toHaveBeenCalled();
246
+ expect(store).not.toHaveBeenCalled();
247
+ },
248
+ );
249
+
250
+ it("refuses an expired signed URL with 404", async () => {
251
+ const { access } = makeAccess(ALLOWED);
252
+ const expired = new AttachmentUrlSigner(
253
+ "s".repeat(32),
254
+ () => NOW_MS + 301_000,
255
+ );
256
+ const { res } = await run(signedQuery(), access, ACTOR, expired);
257
+
258
+ expect(res.statusCode).toBe(404);
259
+ });
260
+
261
+ it("refuses a signed URL when this server has no signer", async () => {
262
+ const { access } = makeAccess(ALLOWED);
263
+ const { res } = await run(signedQuery(), access, ACTOR, null);
264
+
265
+ expect(res.statusCode).toBe(404);
266
+ });
267
+ });
268
+
269
+ describe("GET /attachments/:hash re-fetch anchor", () => {
270
+ it("passes the granted canonical documentId to the store", async () => {
271
+ const { attachments, get } = makeAttachments();
272
+ const { access } = makeAccess(ALLOWED);
273
+
274
+ await makeDownloadHandler(attachments, access, SIGNER)(
275
+ makeReq("?documentId=doc-1"),
276
+ makeRes(),
277
+ undefined,
278
+ ACTOR,
279
+ );
280
+
281
+ expect(get).toHaveBeenCalledWith(
282
+ HASH,
283
+ expect.any(AbortSignal),
284
+ "canonical-doc",
285
+ );
286
+ });
287
+ });