@powerhousedao/switchboard 6.2.3-dev.23 → 6.2.3-dev.25
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/.tsbuild/test/boot-create-signature-policy.test.d.ts +2 -0
- package/.tsbuild/test/boot-create-signature-policy.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-without-renown.test.d.ts +2 -0
- package/.tsbuild/test/boot-without-renown.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-without-require-signatures.test.d.ts +2 -0
- package/.tsbuild/test/boot-without-require-signatures.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-worker-trust-policy.test.d.ts +2 -0
- package/.tsbuild/test/boot-worker-trust-policy.test.d.ts.map +1 -0
- package/.tsbuild/test/builder-defaults.test.d.ts +2 -0
- package/.tsbuild/test/builder-defaults.test.d.ts.map +1 -0
- package/.tsbuild/test/create-signature-policy.test.d.ts +2 -0
- package/.tsbuild/test/create-signature-policy.test.d.ts.map +1 -0
- package/.tsbuild/test/renown-trust-policy.test.d.ts +2 -0
- package/.tsbuild/test/renown-trust-policy.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/.tsbuild/tsdown.config.d.ts.map +1 -1
- package/.tsbuild/vitest.config.d.ts.map +1 -1
- package/Auth.md +92 -0
- package/CHANGELOG.md +38 -0
- package/README.md +14 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/install-packages.d.mts +2 -1
- package/dist/install-packages.mjs +2 -2
- package/dist/install-packages.mjs.map +1 -1
- package/dist/migrate.d.mts +2 -1
- package/dist/migrate.mjs +4 -3
- package/dist/migrate.mjs.map +1 -1
- package/dist/{server-DVotaGIp.mjs → server-C3WaP3kE.mjs} +229 -40
- package/dist/server-C3WaP3kE.mjs.map +1 -0
- package/dist/server.d.mts +48 -28
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -4
- package/dist/{utils-Baw7rThP.mjs → utils-uaBlzyjS.mjs} +15 -9
- package/dist/utils-uaBlzyjS.mjs.map +1 -0
- package/dist/utils.d.mts +4 -5
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs +3 -3
- package/package.json +17 -17
- package/test/attachments/auth.test.ts +85 -0
- package/test/attachments/download-target.test.ts +71 -8
- package/test/attachments/index.test.ts +34 -3
- package/test/boot-create-signature-policy.test.ts +108 -0
- package/test/boot-without-renown.test.ts +81 -0
- package/test/boot-without-require-signatures.test.ts +74 -0
- package/test/boot-worker-trust-policy.test.ts +90 -0
- package/test/builder-defaults.test.ts +80 -0
- package/test/create-signature-policy.test.ts +69 -0
- package/test/renown-trust-policy.test.ts +315 -0
- package/tsdown.config.ts +3 -1
- package/dist/server-DVotaGIp.mjs.map +0 -1
- package/dist/utils-Baw7rThP.mjs.map +0 -1
package/dist/server.d.mts
CHANGED
|
@@ -4,9 +4,10 @@ import { AttachmentReferenceProjectionCapability } from "@powerhousedao/reactor-
|
|
|
4
4
|
import { IAttachmentService } from "@powerhousedao/reactor-attachments";
|
|
5
5
|
import { IRenown } from "@renown/sdk/node";
|
|
6
6
|
import { DriveInput } from "@powerhousedao/shared/document-drive";
|
|
7
|
+
import { DocumentModelModule, SignaturePolicy, UpgradeManifest } from "@powerhousedao/shared/document-model";
|
|
8
|
+
import "@powerhousedao/reactor-workflow";
|
|
9
|
+
import "@powerhousedao/shared/processors";
|
|
7
10
|
import { IRenown as IRenown$1 } from "@renown/sdk";
|
|
8
|
-
import { DocumentModelModule, UpgradeManifest } from "@powerhousedao/shared/document-model";
|
|
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,27 +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 */
|
|
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;
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
requireExisting?: boolean;
|
|
46
|
+
/** Base url of the Renown instance to use */
|
|
47
|
+
baseUrl?: string;
|
|
46
48
|
};
|
|
47
|
-
type StartServerOptions = {
|
|
49
|
+
export type StartServerOptions = {
|
|
48
50
|
/**
|
|
49
51
|
* Pre-built ReactorClientModule (typically from
|
|
50
52
|
* `@powerhousedao/reactor#ReactorClientBuilder.buildModule`). When set, the
|
|
@@ -103,7 +105,8 @@ type StartServerOptions = {
|
|
|
103
105
|
* When configured, the switchboard will load the keypair from `ph login`
|
|
104
106
|
* and can authenticate with remote services on behalf of the user.
|
|
105
107
|
*/
|
|
106
|
-
identity?: IdentityOptions;
|
|
108
|
+
identity?: IdentityOptions;
|
|
109
|
+
/** Base URL for the attachment service; defaults to `PH_SWITCHBOARD_PUBLIC_URL` then `http(s)://localhost:${port}`. */
|
|
107
110
|
attachmentServiceUrl?: string;
|
|
108
111
|
mcp?: boolean;
|
|
109
112
|
/** Powerhouse workflows: the runtime, its subgraph, its webhooks and its
|
|
@@ -168,14 +171,17 @@ type StartServerOptions = {
|
|
|
168
171
|
dbPoolSize?: number;
|
|
169
172
|
};
|
|
170
173
|
};
|
|
171
|
-
type SwitchboardReactor = {
|
|
174
|
+
export type SwitchboardReactor = {
|
|
172
175
|
defaultDriveUrl: string | undefined;
|
|
173
|
-
reactor: IReactorClient;
|
|
174
|
-
|
|
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. */
|
|
175
180
|
attachmentReferenceProjection: AttachmentReferenceProjectionCapability;
|
|
176
181
|
/** Whether the workflow runtime's operation intake is indexing. Undefined
|
|
177
182
|
* when workflows are off; unavailable means no document trigger fires. */
|
|
178
|
-
workflowTriggers?: WorkflowTriggersCapability;
|
|
183
|
+
workflowTriggers?: WorkflowTriggersCapability;
|
|
184
|
+
/** The Renown instance if identity was initialized */
|
|
179
185
|
renown: IRenown$1 | null;
|
|
180
186
|
/**
|
|
181
187
|
* Port the HTTP server actually bound to. May differ from the requested
|
|
@@ -199,29 +205,42 @@ type SwitchboardReactor = {
|
|
|
199
205
|
//#endregion
|
|
200
206
|
//#region src/builder-defaults.d.mts
|
|
201
207
|
type SwitchboardReactorDefaultsOptions = {
|
|
202
|
-
documentModels?: DocumentModelModule[];
|
|
203
|
-
|
|
208
|
+
documentModels?: DocumentModelModule[];
|
|
209
|
+
/** Upgrade manifests for versioned models, one per document type. */
|
|
210
|
+
upgradeManifests?: UpgradeManifest<readonly number[]>[];
|
|
211
|
+
/** Default true. */
|
|
204
212
|
includeBaseModels?: boolean;
|
|
205
213
|
/**
|
|
206
214
|
* Channel scheme. Defaults to `ChannelScheme.SWITCHBOARD`, which populates
|
|
207
215
|
* `reactorModule.syncModule.syncManager` — required by reactor-api. Set
|
|
208
216
|
* to `false` only if the caller will configure a scheme themselves.
|
|
209
217
|
*/
|
|
210
|
-
channelScheme?: ChannelScheme | false;
|
|
211
|
-
|
|
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. */
|
|
212
222
|
executorConfig?: {
|
|
213
223
|
maxSkipThreshold?: number;
|
|
214
224
|
featureFlags?: Partial<ReactorFeatureFlags>;
|
|
215
|
-
};
|
|
225
|
+
};
|
|
226
|
+
/** Wire dynamic document-model loading via HTTP. */
|
|
216
227
|
documentModelLoader?: IDocumentModelLoader;
|
|
217
228
|
logger?: ILogger;
|
|
218
229
|
/**
|
|
219
|
-
* Identity signer (typically from `getRenownSignerConfig`).
|
|
220
|
-
*
|
|
230
|
+
* Identity signer (typically from `getRenownSignerConfig`). Signs client
|
|
231
|
+
* writes and the operations the executor synthesizes, in pooled workers
|
|
232
|
+
* through `workerSigner`; omit for unsigned operation.
|
|
221
233
|
*/
|
|
222
234
|
signer?: SignerConfig;
|
|
235
|
+
/**
|
|
236
|
+
* Which keys may sign as which users (typically from
|
|
237
|
+
* `getRenownTrustPolicyConfig`); wins over one carried by `signer`.
|
|
238
|
+
*/
|
|
239
|
+
trustPolicy?: Pick<SignerConfig, "trustPolicy" | "workerTrustPolicy">;
|
|
240
|
+
/** What the client creates new documents as; see `resolveCreateSignaturePolicy`. */
|
|
241
|
+
createSignaturePolicy?: SignaturePolicy;
|
|
223
242
|
};
|
|
224
|
-
declare function applySwitchboardReactorDefaults(reactorBuilder: ReactorBuilder, clientBuilder: ReactorClientBuilder, options?: SwitchboardReactorDefaultsOptions): void;
|
|
243
|
+
export declare function applySwitchboardReactorDefaults(reactorBuilder: ReactorBuilder, clientBuilder: ReactorClientBuilder, options?: SwitchboardReactorDefaultsOptions): void;
|
|
225
244
|
//#endregion
|
|
226
245
|
//#region src/server.d.mts
|
|
227
246
|
/**
|
|
@@ -229,9 +248,9 @@ declare function applySwitchboardReactorDefaults(reactorBuilder: ReactorBuilder,
|
|
|
229
248
|
* the port is free, false if the OS reports it in use. Any other error is
|
|
230
249
|
* surfaced so we don't silently mask real issues (permissions, bad host, …).
|
|
231
250
|
*/
|
|
232
|
-
declare function isPortAvailable(port: number): Promise<boolean>;
|
|
251
|
+
export declare function isPortAvailable(port: number): Promise<boolean>;
|
|
233
252
|
/** Derive the remote attachment service config for switchboard's own `/attachments/*` API. */
|
|
234
|
-
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): {
|
|
235
254
|
remoteUrl: string;
|
|
236
255
|
jwtHandler: JwtHandler | undefined;
|
|
237
256
|
};
|
|
@@ -252,7 +271,8 @@ declare function deriveAttachmentServiceConfig(options: Pick<StartServerOptions,
|
|
|
252
271
|
* trigger the `withShutdownHook` chain that disposes the api. Programmatic
|
|
253
272
|
* full teardown isn't currently exposed — wire it via SIGINT/SIGTERM.
|
|
254
273
|
*/
|
|
255
|
-
declare const startSwitchboard: (options?: StartServerOptions) => Promise<SwitchboardReactor>;
|
|
274
|
+
export declare const startSwitchboard: (options?: StartServerOptions) => Promise<SwitchboardReactor>;
|
|
256
275
|
//#endregion
|
|
257
|
-
export {
|
|
276
|
+
export type { SwitchboardReactorDefaultsOptions, WorkflowTriggersCapability };
|
|
277
|
+
export {};
|
|
258
278
|
//# sourceMappingURL=server.d.mts.map
|
package/dist/server.d.mts.map
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/workflow-runtime.mts","../src/types.ts","../src/builder-defaults.mts","../src/server.mts"],"mappings":";;;;;;;;;;;;;KAmDY;EACN;;EAEA;EACA;;;;;;;;;;YCrCM;;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;;;;;;;;wBC4DK,gBAAgB,eAAe;;wBAoI/B,8BACd,SAAS,KAAK,uDACd,oBACA,QAAQ;EACL;EAAmB,YAAY;;;;;;;;;;;;;;;;;;;qBA04BvB,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-
|
|
3
|
-
import "./utils-Baw7rThP.mjs";
|
|
2
|
+
import { i as applySwitchboardReactorDefaults, n as isPortAvailable, r as startSwitchboard, t as deriveAttachmentServiceConfig } from "./server-C3WaP3kE.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]="
|
|
6
|
-
//# debugId=
|
|
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]="1217c4fa-83a6-5775-a7ff-15c675d58ff3")}catch(e){}}();
|
|
5
|
+
//# debugId=1217c4fa-83a6-5775-a7ff-15c675d58ff3
|
|
@@ -1,8 +1,16 @@
|
|
|
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]="
|
|
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
|
+
import { withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
5
6
|
//#region src/utils.mts
|
|
7
|
+
/**
|
|
8
|
+
* A configured id pins the drive, and only a legacy drive can take a chosen
|
|
9
|
+
* id; otherwise the client's creation default applies.
|
|
10
|
+
*/
|
|
11
|
+
async function underCreatePolicy(client, document, id) {
|
|
12
|
+
return id ? withSignaturePolicy(document, "legacy", { id }) : withSignaturePolicy(document, await client.getCreateSignaturePolicy());
|
|
13
|
+
}
|
|
6
14
|
async function addDefaultDrive(client, drive, serverPort) {
|
|
7
15
|
let driveId = drive.id;
|
|
8
16
|
if (!driveId || driveId.length === 0) driveId = drive.slug;
|
|
@@ -13,7 +21,7 @@ async function addDefaultDrive(client, drive, serverPort) {
|
|
|
13
21
|
} catch {}
|
|
14
22
|
if (existingDrive) return `http://localhost:${serverPort}/d/${driveId}`;
|
|
15
23
|
const { global } = driveCreateState();
|
|
16
|
-
const document = driveCreateDocument({
|
|
24
|
+
const document = await underCreatePolicy(client, driveCreateDocument({
|
|
17
25
|
global: {
|
|
18
26
|
...global,
|
|
19
27
|
name: drive.global.name,
|
|
@@ -25,8 +33,7 @@ async function addDefaultDrive(client, drive, serverPort) {
|
|
|
25
33
|
listeners: drive.local?.listeners ?? [],
|
|
26
34
|
triggers: drive.local?.triggers ?? []
|
|
27
35
|
}
|
|
28
|
-
});
|
|
29
|
-
if (drive.id && drive.id.length > 0) document.header.id = drive.id;
|
|
36
|
+
}), drive.id);
|
|
30
37
|
if (drive.slug && drive.slug.length > 0) document.header.slug = drive.slug;
|
|
31
38
|
if (drive.global.name) document.header.name = drive.global.name;
|
|
32
39
|
if (drive.preferredEditor) document.header.meta = { preferredEditor: drive.preferredEditor };
|
|
@@ -47,7 +54,7 @@ async function addDefaultReactorDrive(client, drive, serverPort) {
|
|
|
47
54
|
} catch {}
|
|
48
55
|
if (existingDrive) return `http://localhost:${serverPort}/d/${driveId}`;
|
|
49
56
|
const { global, local } = reactorDriveCreateState();
|
|
50
|
-
const document = reactorDriveCreateDocument({
|
|
57
|
+
const document = await underCreatePolicy(client, reactorDriveCreateDocument({
|
|
51
58
|
global: {
|
|
52
59
|
...global,
|
|
53
60
|
name: drive.global.name,
|
|
@@ -58,8 +65,7 @@ async function addDefaultReactorDrive(client, drive, serverPort) {
|
|
|
58
65
|
availableOffline: drive.local?.availableOffline ?? local.availableOffline,
|
|
59
66
|
sharingType: drive.local?.sharingType ?? local.sharingType
|
|
60
67
|
}
|
|
61
|
-
});
|
|
62
|
-
if (drive.id && drive.id.length > 0) document.header.id = drive.id;
|
|
68
|
+
}), drive.id);
|
|
63
69
|
if (drive.slug && drive.slug.length > 0) document.header.slug = drive.slug;
|
|
64
70
|
if (drive.global.name) document.header.name = drive.global.name;
|
|
65
71
|
if (drive.preferredEditor) document.header.meta = { preferredEditor: drive.preferredEditor };
|
|
@@ -76,5 +82,5 @@ function isPostgresUrl(url) {
|
|
|
76
82
|
//#endregion
|
|
77
83
|
export { addDefaultReactorDrive as n, isPostgresUrl as r, addDefaultDrive as t };
|
|
78
84
|
|
|
79
|
-
//# sourceMappingURL=utils-
|
|
80
|
-
//# debugId=
|
|
85
|
+
//# sourceMappingURL=utils-uaBlzyjS.mjs.map
|
|
86
|
+
//# debugId=5ece528d-aafb-5305-a04d-e0c45c8c30a3
|
|
@@ -0,0 +1 @@
|
|
|
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 {
|
|
8
|
+
export {};
|
|
10
9
|
//# sourceMappingURL=utils.d.mts.map
|
package/dist/utils.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.mts","names":[],"sources":["../src/utils.mts"],"mappings":"
|
|
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-
|
|
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]="
|
|
4
|
-
//# debugId=
|
|
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.
|
|
4
|
+
"version": "6.2.3-dev.25",
|
|
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.
|
|
44
|
+
"@powerhousedao/config": "6.2.3-dev.25",
|
|
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,22 +55,22 @@
|
|
|
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.
|
|
59
|
-
"@powerhousedao/reactor": "6.2.3-dev.
|
|
60
|
-
"@powerhousedao/shared": "6.2.3-dev.
|
|
61
|
-
"@powerhousedao/vetra": "6.2.3-dev.
|
|
62
|
-
"@powerhousedao/reactor-api": "6.2.3-dev.
|
|
63
|
-
"@powerhousedao/reactor-attachments": "6.2.3-dev.
|
|
64
|
-
"@powerhousedao/reactor-drive": "6.2.3-dev.
|
|
65
|
-
"@powerhousedao/reactor-group": "6.2.3-dev.
|
|
66
|
-
"@powerhousedao/reactor-workflow": "6.2.3-dev.
|
|
67
|
-
"@powerhousedao/workflow": "6.2.3-dev.
|
|
68
|
-
"@powerhousedao/pglite-fs": "6.2.3-dev.
|
|
58
|
+
"@powerhousedao/opentelemetry-instrumentation-reactor": "6.2.3-dev.25",
|
|
59
|
+
"@powerhousedao/reactor": "6.2.3-dev.25",
|
|
60
|
+
"@powerhousedao/shared": "6.2.3-dev.25",
|
|
61
|
+
"@powerhousedao/vetra": "6.2.3-dev.25",
|
|
62
|
+
"@powerhousedao/reactor-api": "6.2.3-dev.25",
|
|
63
|
+
"@powerhousedao/reactor-attachments": "6.2.3-dev.25",
|
|
64
|
+
"@powerhousedao/reactor-drive": "6.2.3-dev.25",
|
|
65
|
+
"@powerhousedao/reactor-group": "6.2.3-dev.25",
|
|
66
|
+
"@powerhousedao/reactor-workflow": "6.2.3-dev.25",
|
|
67
|
+
"@powerhousedao/workflow": "6.2.3-dev.25",
|
|
68
|
+
"@powerhousedao/pglite-fs": "6.2.3-dev.25",
|
|
69
69
|
"@pyroscope/nodejs": "^0.4.5",
|
|
70
|
-
"@renown/sdk": "6.2.3-dev.
|
|
70
|
+
"@renown/sdk": "6.2.3-dev.25",
|
|
71
71
|
"@sentry/node": "^10.53.1",
|
|
72
72
|
"@sentry/opentelemetry": "^10.53.1",
|
|
73
|
-
"document-model": "6.2.3-dev.
|
|
73
|
+
"document-model": "6.2.3-dev.25",
|
|
74
74
|
"dotenv": "^16.4.7",
|
|
75
75
|
"express": "^4.21.2",
|
|
76
76
|
"graphql": "^16",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@types/express": "^4.17.25",
|
|
85
85
|
"@types/node": "25.2.3",
|
|
86
86
|
"@types/pg": "8.16.0",
|
|
87
|
-
"tsdown": "0.
|
|
87
|
+
"tsdown": "0.23.0",
|
|
88
88
|
"concurrently": "9.2.1",
|
|
89
89
|
"nodemon": "3.1.11",
|
|
90
90
|
"react": "19.2.6",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"scripts": {
|
|
97
97
|
"tsc": "tsc",
|
|
98
98
|
"test": "vitest run",
|
|
99
|
-
"lint": "
|
|
99
|
+
"lint": "cd ../.. && oxlint apps/switchboard",
|
|
100
100
|
"build": "tsdown && pnpm run install-packages",
|
|
101
101
|
"start": "node dist/index.mjs",
|
|
102
102
|
"start:profile": "mkdir -p .prof && node --cpu-prof --cpu-prof-dir=.prof dist/index.mjs",
|
|
@@ -342,6 +342,91 @@ describe("requireAuth", () => {
|
|
|
342
342
|
});
|
|
343
343
|
});
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* The deployment-wide floor. These routes never pass the GraphQL fetch chain,
|
|
347
|
+
* so `REQUIRE_AUTHENTICATED_CALLER` reaches them through this option instead
|
|
348
|
+
* — including on the routes that opt into `allowAnonymous`, which says the
|
|
349
|
+
* handler can decide without identity, not that the server will serve callers
|
|
350
|
+
* it cannot name.
|
|
351
|
+
*/
|
|
352
|
+
describe("requireAuthenticatedCaller", () => {
|
|
353
|
+
const anonymousUnderOpenPolicy = () =>
|
|
354
|
+
makeAuthService(() =>
|
|
355
|
+
Promise.resolve({ user: undefined, admins: [], auth_enabled: false }),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
it("refuses a missing bearer even with auth disabled, where today it passes", async () => {
|
|
359
|
+
const { service } = anonymousUnderOpenPolicy();
|
|
360
|
+
const handler = vi.fn<NodeHandler>();
|
|
361
|
+
const wrapped = requireAuth(service, handler, {
|
|
362
|
+
requireAuthenticatedCaller: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const res = makeRes();
|
|
366
|
+
await wrapped(makeReq({ headers: {} }), res);
|
|
367
|
+
|
|
368
|
+
expect(res.statusCode).toBe(401);
|
|
369
|
+
expect(JSON.parse(res._body)).toEqual({
|
|
370
|
+
error: "Authentication required",
|
|
371
|
+
});
|
|
372
|
+
expect(handler).not.toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("overrides allowAnonymous", async () => {
|
|
376
|
+
const { service } = anonymousUnderOpenPolicy();
|
|
377
|
+
const handler = vi.fn<NodeHandler>();
|
|
378
|
+
const wrapped = requireAuth(service, handler, {
|
|
379
|
+
allowAnonymous: true,
|
|
380
|
+
requireAuthenticatedCaller: true,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const res = makeRes();
|
|
384
|
+
await wrapped(makeReq({ headers: {} }), res);
|
|
385
|
+
|
|
386
|
+
expect(res.statusCode).toBe(401);
|
|
387
|
+
expect(handler).not.toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("still serves a verified caller", async () => {
|
|
391
|
+
const { service } = makeAuthService(() =>
|
|
392
|
+
Promise.resolve({
|
|
393
|
+
user: { address: "0x123", chainId: 1, networkId: "mainnet" },
|
|
394
|
+
admins: [],
|
|
395
|
+
auth_enabled: false,
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
const handler = vi.fn<NodeHandler>();
|
|
399
|
+
const wrapped = requireAuth(service, handler, {
|
|
400
|
+
allowAnonymous: true,
|
|
401
|
+
requireAuthenticatedCaller: true,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const req = makeReq({ headers: { authorization: "Bearer good-token" } });
|
|
405
|
+
const res = makeRes();
|
|
406
|
+
await wrapped(req, res);
|
|
407
|
+
|
|
408
|
+
expect(res.statusCode).toBe(200);
|
|
409
|
+
expect(handler).toHaveBeenCalledWith(req, res, undefined, {
|
|
410
|
+
user: { address: "0x123", chainId: 1, networkId: "mainnet" },
|
|
411
|
+
authEnabled: false,
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("leaves every route as it was when the deployment has not set it", async () => {
|
|
416
|
+
const { service } = anonymousUnderOpenPolicy();
|
|
417
|
+
const handler = vi.fn<NodeHandler>();
|
|
418
|
+
const wrapped = requireAuth(service, handler, {
|
|
419
|
+
requireAuthenticatedCaller: false,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const res = makeRes();
|
|
423
|
+
await wrapped(makeReq({ headers: {} }), res);
|
|
424
|
+
|
|
425
|
+
expect(res.statusCode).toBe(200);
|
|
426
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
345
430
|
it("calls verifyBearer with undefined when no authorization header is present", async () => {
|
|
346
431
|
const { service, spy } = makeAuthService(() =>
|
|
347
432
|
Promise.resolve({
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
IAttachmentAccessService,
|
|
10
10
|
} from "@powerhousedao/reactor-api";
|
|
11
11
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
12
|
-
import { describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
13
|
import type { AttachmentActorContext } from "../../src/attachments/auth.js";
|
|
14
14
|
import { registerAttachmentRoutes } from "../../src/attachments/index.js";
|
|
15
15
|
import { makeDownloadTargetHandler } from "../../src/attachments/routes.js";
|
|
@@ -126,6 +126,7 @@ describe("makeDownloadTargetHandler", () => {
|
|
|
126
126
|
documentId: DOC_ID,
|
|
127
127
|
attachmentRef: REF,
|
|
128
128
|
userAddress: "0xverified",
|
|
129
|
+
appKey: "did:key:zAppTest",
|
|
129
130
|
});
|
|
130
131
|
});
|
|
131
132
|
|
|
@@ -148,7 +149,7 @@ describe("makeDownloadTargetHandler", () => {
|
|
|
148
149
|
|
|
149
150
|
expect(res.statusCode).toBe(200);
|
|
150
151
|
expect(JSON.parse(res._body)).toMatchObject({ kind: "presigned-get" });
|
|
151
|
-
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH,
|
|
152
|
+
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH, 300);
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
it("passes a requested expiresIn through to the backend presigner", async () => {
|
|
@@ -168,7 +169,7 @@ describe("makeDownloadTargetHandler", () => {
|
|
|
168
169
|
|
|
169
170
|
await handler(
|
|
170
171
|
makeReq({
|
|
171
|
-
url: `/attachments/${HASH}/download-target?documentId=${DOC_ID}&expiresIn=
|
|
172
|
+
url: `/attachments/${HASH}/download-target?documentId=${DOC_ID}&expiresIn=60`,
|
|
172
173
|
}),
|
|
173
174
|
res,
|
|
174
175
|
undefined,
|
|
@@ -176,10 +177,10 @@ describe("makeDownloadTargetHandler", () => {
|
|
|
176
177
|
);
|
|
177
178
|
|
|
178
179
|
expect(res.statusCode).toBe(200);
|
|
179
|
-
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH,
|
|
180
|
+
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH, 60);
|
|
180
181
|
});
|
|
181
182
|
|
|
182
|
-
it("clamps expiresIn to the
|
|
183
|
+
it("clamps a requested expiresIn to the ceiling", async () => {
|
|
183
184
|
const { access } = makeAccess(ALLOWED);
|
|
184
185
|
const prepareDownloadTarget = vi.fn().mockResolvedValue({
|
|
185
186
|
kind: "presigned-get",
|
|
@@ -203,10 +204,10 @@ describe("makeDownloadTargetHandler", () => {
|
|
|
203
204
|
ACTOR,
|
|
204
205
|
);
|
|
205
206
|
|
|
206
|
-
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH,
|
|
207
|
+
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH, 300);
|
|
207
208
|
});
|
|
208
209
|
|
|
209
|
-
it("
|
|
210
|
+
it("applies the ceiling when expiresIn is absent, rather than the backend default", async () => {
|
|
210
211
|
const { access } = makeAccess(ALLOWED);
|
|
211
212
|
const prepareDownloadTarget = vi.fn().mockResolvedValue({
|
|
212
213
|
kind: "presigned-get",
|
|
@@ -222,7 +223,69 @@ describe("makeDownloadTargetHandler", () => {
|
|
|
222
223
|
|
|
223
224
|
await handler(makeReq({}), makeRes(), undefined, ACTOR);
|
|
224
225
|
|
|
225
|
-
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH,
|
|
226
|
+
expect(prepareDownloadTarget).toHaveBeenCalledWith(HASH, 300);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS", () => {
|
|
230
|
+
const ORIGINAL = process.env.ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS;
|
|
231
|
+
|
|
232
|
+
afterEach(() => {
|
|
233
|
+
if (ORIGINAL === undefined) {
|
|
234
|
+
delete process.env.ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS;
|
|
235
|
+
} else {
|
|
236
|
+
process.env.ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS = ORIGINAL;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
async function ttlFor(configured: string | undefined, query = "") {
|
|
241
|
+
if (configured === undefined) {
|
|
242
|
+
delete process.env.ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS;
|
|
243
|
+
} else {
|
|
244
|
+
process.env.ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS = configured;
|
|
245
|
+
}
|
|
246
|
+
const { access } = makeAccess(ALLOWED);
|
|
247
|
+
const prepareDownloadTarget = vi.fn().mockResolvedValue({
|
|
248
|
+
kind: "presigned-get",
|
|
249
|
+
method: "GET",
|
|
250
|
+
url: "https://bucket.example.com/attachments/aa?sig=1",
|
|
251
|
+
headers: {},
|
|
252
|
+
expiresAtUtc: EXPIRES,
|
|
253
|
+
});
|
|
254
|
+
const attachments = makeAttachments({
|
|
255
|
+
backend: { kind: "s3", prepareDownloadTarget } as never,
|
|
256
|
+
});
|
|
257
|
+
const handler = makeDownloadTargetHandler(attachments, access);
|
|
258
|
+
await handler(
|
|
259
|
+
makeReq({
|
|
260
|
+
url: `/attachments/${HASH}/download-target?documentId=${DOC_ID}${query}`,
|
|
261
|
+
}),
|
|
262
|
+
makeRes(),
|
|
263
|
+
undefined,
|
|
264
|
+
ACTOR,
|
|
265
|
+
);
|
|
266
|
+
return (prepareDownloadTarget.mock.calls[0] as unknown[])[1];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
it("raises the ceiling when a deployment configures one", async () => {
|
|
270
|
+
await expect(ttlFor("1800")).resolves.toBe(1800);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("never lets a configured ceiling exceed the signing maximum", async () => {
|
|
274
|
+
await expect(ttlFor("99999999")).resolves.toBe(604800);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it.each(["not-a-number", "0", "-5", "12.5"])(
|
|
278
|
+
"falls back to the default, not the maximum, on %s",
|
|
279
|
+
async (configured) => {
|
|
280
|
+
// A typo must narrow nothing and widen nothing: falling back to the
|
|
281
|
+
// signing ceiling would turn a misconfiguration into a week-long URL.
|
|
282
|
+
await expect(ttlFor(configured)).resolves.toBe(300);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
it("still clamps a caller's request to the configured ceiling", async () => {
|
|
287
|
+
await expect(ttlFor("1800", "&expiresIn=99999")).resolves.toBe(1800);
|
|
288
|
+
});
|
|
226
289
|
});
|
|
227
290
|
|
|
228
291
|
it.each([
|
|
@@ -13,8 +13,16 @@ type Captured = {
|
|
|
13
13
|
) => void | Promise<void>;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
type FakeApi = Pick<
|
|
17
|
+
API,
|
|
18
|
+
"httpAdapter" | "authService" | "requireAuthenticatedCaller"
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
function makeFakeApi(
|
|
22
|
+
authService: AuthService | undefined,
|
|
23
|
+
requireAuthenticatedCaller = false,
|
|
24
|
+
): {
|
|
25
|
+
api: FakeApi;
|
|
18
26
|
captured: Captured[];
|
|
19
27
|
} {
|
|
20
28
|
const captured: Captured[] = [];
|
|
@@ -29,7 +37,8 @@ function makeFakeApi(authService: AuthService | undefined): {
|
|
|
29
37
|
},
|
|
30
38
|
},
|
|
31
39
|
authService,
|
|
32
|
-
|
|
40
|
+
requireAuthenticatedCaller,
|
|
41
|
+
} as unknown as FakeApi;
|
|
33
42
|
return { api, captured };
|
|
34
43
|
}
|
|
35
44
|
|
|
@@ -138,6 +147,28 @@ describe("mountAuthenticatedNodeRoute", () => {
|
|
|
138
147
|
});
|
|
139
148
|
});
|
|
140
149
|
|
|
150
|
+
it("carries the deployment's authenticated-caller floor onto the route, over allowAnonymous", async () => {
|
|
151
|
+
// auth_enabled false is the shape a deployment running the document
|
|
152
|
+
// policies has: the boundary policy is OPEN, and this floor is what keeps
|
|
153
|
+
// an anonymous caller off the attachment routes.
|
|
154
|
+
const verifyBearer = vi.fn(() =>
|
|
155
|
+
Promise.resolve({ user: undefined, admins: [], auth_enabled: false }),
|
|
156
|
+
);
|
|
157
|
+
const authService = { verifyBearer } as unknown as AuthService;
|
|
158
|
+
const { api, captured } = makeFakeApi(authService, true);
|
|
159
|
+
const inner = vi.fn();
|
|
160
|
+
|
|
161
|
+
mountAuthenticatedNodeRoute(api, "GET", "/x", inner, {
|
|
162
|
+
allowAnonymous: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const res = makeRes();
|
|
166
|
+
await captured[0].handler(makeReq(), res);
|
|
167
|
+
|
|
168
|
+
expect(res.statusCode).toBe(401);
|
|
169
|
+
expect(inner).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
141
172
|
it("mounts a wrapper that forwards the anonymous actor when authService is undefined", async () => {
|
|
142
173
|
const { api, captured } = makeFakeApi(undefined);
|
|
143
174
|
const inner = vi.fn();
|