@powerhousedao/switchboard 6.1.0-staging.0 → 6.1.0
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/attachments/auth.test.d.ts +2 -0
- package/.tsbuild/test/attachments/auth.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/index.test.d.ts +2 -0
- package/.tsbuild/test/attachments/index.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/routes-integration.test.d.ts +2 -0
- package/.tsbuild/test/attachments/routes-integration.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/routes.test.d.ts +2 -0
- package/.tsbuild/test/attachments/routes.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/service-config.test.d.ts +2 -0
- package/.tsbuild/test/attachments/service-config.test.d.ts.map +1 -0
- package/.tsbuild/test/metrics.test.d.ts +2 -0
- package/.tsbuild/test/metrics.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-dialect.test.d.ts +2 -0
- package/.tsbuild/test/pglite-dialect.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-version.test.d.ts +2 -0
- package/.tsbuild/test/pglite-version.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -0
- package/.tsbuild/tsdown.config.d.ts +3 -0
- package/.tsbuild/tsdown.config.d.ts.map +1 -0
- package/.tsbuild/vitest.config.d.ts +3 -0
- package/.tsbuild/vitest.config.d.ts.map +1 -0
- package/CHANGELOG.md +124 -1
- package/dist/index.mjs +7 -7
- package/dist/index.mjs.map +1 -1
- package/dist/{server-bMFA4VKj.mjs → server-CdgQ8Mhi.mjs} +19 -6
- package/dist/server-CdgQ8Mhi.mjs.map +1 -0
- package/dist/server.d.mts +15 -6
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +5 -5
- package/dist/{utils-BVNg1DRI.mjs → utils-Baw7rThP.mjs} +3 -4
- package/dist/utils-Baw7rThP.mjs.map +1 -0
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs +3 -3
- package/package.json +25 -25
- package/test/attachments/auth.test.ts +36 -25
- package/test/attachments/index.test.ts +14 -10
- package/test/attachments/service-config.test.ts +170 -0
- package/dist/server-bMFA4VKj.mjs.map +0 -1
- package/dist/utils-BVNg1DRI.mjs.map +0 -1
package/dist/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as
|
|
3
|
-
import "./utils-
|
|
4
|
-
export { applySwitchboardReactorDefaults, 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=
|
|
2
|
+
import { i as applySwitchboardReactorDefaults, n as isPortAvailable, r as startSwitchboard, t as deriveAttachmentServiceConfig } from "./server-CdgQ8Mhi.mjs";
|
|
3
|
+
import "./utils-Baw7rThP.mjs";
|
|
4
|
+
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]="4dadbdeb-65f4-5a9c-a028-770910fc26e8")}catch(e){}}();
|
|
6
|
+
//# debugId=4dadbdeb-65f4-5a9c-a028-770910fc26e8
|
|
@@ -1,8 +1,7 @@
|
|
|
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]="686dcce6-c280-5558-89dd-a18a7e514cdf")}catch(e){}}();
|
|
3
3
|
import { reactorDriveCreateDocument, reactorDriveCreateState } from "@powerhousedao/reactor-drive";
|
|
4
4
|
import { driveCreateDocument, driveCreateState } from "@powerhousedao/shared/document-drive";
|
|
5
|
-
import "@powerhousedao/shared/document-model";
|
|
6
5
|
//#region src/utils.mts
|
|
7
6
|
async function addDefaultDrive(client, drive, serverPort) {
|
|
8
7
|
let driveId = drive.id;
|
|
@@ -77,5 +76,5 @@ function isPostgresUrl(url) {
|
|
|
77
76
|
//#endregion
|
|
78
77
|
export { addDefaultReactorDrive as n, isPostgresUrl as r, addDefaultDrive as t };
|
|
79
78
|
|
|
80
|
-
//# sourceMappingURL=utils-
|
|
81
|
-
//# debugId=
|
|
79
|
+
//# sourceMappingURL=utils-Baw7rThP.mjs.map
|
|
80
|
+
//# debugId=686dcce6-c280-5558-89dd-a18a7e514cdf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-Baw7rThP.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\";\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 document = 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 if (drive.id && drive.id.length > 0) {\n document.header.id = drive.id;\n }\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 document = 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 if (drive.id && drive.id.length > 0) {\n document.header.id = drive.id;\n }\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":";;;;;AAWA,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;CACrC,MAAM,WAAW,oBAAoB;EACnC,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;AAEF,KAAI,MAAM,MAAM,MAAM,GAAG,SAAS,EAChC,UAAS,OAAO,KAAK,MAAM;AAE7B,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,2BAA2B;EAC1C,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;AAEF,KAAI,MAAM,MAAM,MAAM,GAAG,SAAS,EAChC,UAAS,OAAO,KAAK,MAAM;AAE7B,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":"686dcce6-c280-5558-89dd-a18a7e514cdf"}
|
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":";;;;iBAWsB,eAAA,CACpB,MAAA,EAAQ,cAAA,EACR,KAAA,EAAO,UAAA,EACP,UAAA,WAAkB,OAAA;AAAA,iBAgEE,sBAAA,CACpB,MAAA,EAAQ,cAAA,EACR,KAAA,EAAO,UAAA,EACP,UAAA,WAAkB,OAAA;AAAA,iBA6DJ,aAAA,CAAc,GAAA"}
|
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-Baw7rThP.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]="8227e60f-e412-513a-b0cb-502740b7f818")}catch(e){}}();
|
|
4
|
+
//# debugId=8227e60f-e412-513a-b0cb-502740b7f818
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/switchboard",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.1.0
|
|
4
|
+
"version": "6.1.0",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -42,37 +42,37 @@
|
|
|
42
42
|
"@openfeature/env-var-provider": "0.3.1",
|
|
43
43
|
"@openfeature/server-sdk": "1.19.0",
|
|
44
44
|
"@opentelemetry/api": "^1.9.0",
|
|
45
|
-
"@opentelemetry/exporter-metrics-otlp-http": "^0.
|
|
46
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
47
|
-
"@opentelemetry/instrumentation-express": "^0.
|
|
48
|
-
"@opentelemetry/instrumentation-graphql": "^0.
|
|
49
|
-
"@opentelemetry/instrumentation-http": "^0.
|
|
50
|
-
"@opentelemetry/instrumentation-pg": "^0.
|
|
51
|
-
"@opentelemetry/resources": "^
|
|
52
|
-
"@opentelemetry/sdk-metrics": "^
|
|
53
|
-
"@opentelemetry/sdk-node": "^0.
|
|
54
|
-
"@opentelemetry/sdk-trace-base": "^
|
|
55
|
-
"@opentelemetry/semantic-conventions": "^1.
|
|
45
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
|
|
46
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
|
|
47
|
+
"@opentelemetry/instrumentation-express": "^0.66.0",
|
|
48
|
+
"@opentelemetry/instrumentation-graphql": "^0.66.0",
|
|
49
|
+
"@opentelemetry/instrumentation-http": "^0.218.0",
|
|
50
|
+
"@opentelemetry/instrumentation-pg": "^0.70.0",
|
|
51
|
+
"@opentelemetry/resources": "^2.7.1",
|
|
52
|
+
"@opentelemetry/sdk-metrics": "^2.7.1",
|
|
53
|
+
"@opentelemetry/sdk-node": "^0.218.0",
|
|
54
|
+
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
55
|
+
"@opentelemetry/semantic-conventions": "^1.41.1",
|
|
56
56
|
"@pyroscope/nodejs": "^0.4.5",
|
|
57
|
-
"@sentry/node": "^
|
|
58
|
-
"@sentry/opentelemetry": "^
|
|
57
|
+
"@sentry/node": "^10.52.0",
|
|
58
|
+
"@sentry/opentelemetry": "^10.52.0",
|
|
59
59
|
"dotenv": "^16.4.7",
|
|
60
60
|
"express": "^4.21.2",
|
|
61
61
|
"kysely": "0.28.16",
|
|
62
62
|
"kysely-pglite-dialect": "1.2.0",
|
|
63
63
|
"pg": "8.18.0",
|
|
64
64
|
"vite": "8.0.8",
|
|
65
|
-
"@powerhousedao/config": "6.1.0
|
|
66
|
-
"@powerhousedao/
|
|
67
|
-
"@powerhousedao/reactor": "6.1.0
|
|
68
|
-
"@powerhousedao/vetra": "6.1.0
|
|
69
|
-
"@powerhousedao/shared": "6.1.0
|
|
70
|
-
"@powerhousedao/reactor-
|
|
71
|
-
"@powerhousedao/reactor-
|
|
72
|
-
"@
|
|
73
|
-
"@powerhousedao/
|
|
74
|
-
"
|
|
75
|
-
"
|
|
65
|
+
"@powerhousedao/config": "6.1.0",
|
|
66
|
+
"@powerhousedao/reactor": "6.1.0",
|
|
67
|
+
"@powerhousedao/opentelemetry-instrumentation-reactor": "6.1.0",
|
|
68
|
+
"@powerhousedao/vetra": "6.1.0",
|
|
69
|
+
"@powerhousedao/shared": "6.1.0",
|
|
70
|
+
"@powerhousedao/reactor-api": "6.1.0",
|
|
71
|
+
"@powerhousedao/reactor-attachments": "6.1.0",
|
|
72
|
+
"@renown/sdk": "6.1.0",
|
|
73
|
+
"@powerhousedao/reactor-drive": "6.1.0",
|
|
74
|
+
"document-model": "6.1.0",
|
|
75
|
+
"@powerhousedao/pglite-fs": "6.1.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/express": "^4.17.25",
|
|
@@ -60,7 +60,7 @@ function makeAuthService(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
describe("requireAuth", () => {
|
|
63
|
-
it("returns the original handler unchanged when authService is undefined",
|
|
63
|
+
it("returns the original handler unchanged when authService is undefined", () => {
|
|
64
64
|
const handler: NodeHandler = vi.fn();
|
|
65
65
|
const wrapped = requireAuth(undefined, handler);
|
|
66
66
|
expect(wrapped).toBe(handler);
|
|
@@ -77,11 +77,13 @@ describe("requireAuth", () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it("returns 401 with { error: 'Authentication required' } when Authorization header is missing", async () => {
|
|
80
|
-
const { service, spy } = makeAuthService(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
const { service, spy } = makeAuthService(() =>
|
|
81
|
+
Promise.resolve({
|
|
82
|
+
user: undefined,
|
|
83
|
+
admins: [],
|
|
84
|
+
auth_enabled: true,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
85
87
|
const handler = vi.fn<NodeHandler>();
|
|
86
88
|
const wrapped = requireAuth(service, handler);
|
|
87
89
|
|
|
@@ -96,12 +98,13 @@ describe("requireAuth", () => {
|
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
it("forwards a Response from AuthService (status, content-type, body) for an invalid bearer token", async () => {
|
|
99
|
-
const { service } = makeAuthService(
|
|
100
|
-
|
|
101
|
+
const { service } = makeAuthService(() =>
|
|
102
|
+
Promise.resolve(
|
|
101
103
|
new Response(JSON.stringify({ error: "Verification failed" }), {
|
|
102
104
|
status: 401,
|
|
103
105
|
headers: { "content-type": "application/json" },
|
|
104
106
|
}),
|
|
107
|
+
),
|
|
105
108
|
);
|
|
106
109
|
const handler = vi.fn<NodeHandler>();
|
|
107
110
|
const wrapped = requireAuth(service, handler);
|
|
@@ -119,12 +122,13 @@ describe("requireAuth", () => {
|
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
it("forwards a Response with a non-JSON content-type verbatim", async () => {
|
|
122
|
-
const { service } = makeAuthService(
|
|
123
|
-
|
|
125
|
+
const { service } = makeAuthService(() =>
|
|
126
|
+
Promise.resolve(
|
|
124
127
|
new Response("nope", {
|
|
125
128
|
status: 403,
|
|
126
129
|
headers: { "content-type": "text/plain" },
|
|
127
130
|
}),
|
|
131
|
+
),
|
|
128
132
|
);
|
|
129
133
|
const handler = vi.fn<NodeHandler>();
|
|
130
134
|
const wrapped = requireAuth(service, handler);
|
|
@@ -142,11 +146,13 @@ describe("requireAuth", () => {
|
|
|
142
146
|
});
|
|
143
147
|
|
|
144
148
|
it("invokes the handler and leaves the response untouched on a valid bearer token", async () => {
|
|
145
|
-
const { service } = makeAuthService(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
const { service } = makeAuthService(() =>
|
|
150
|
+
Promise.resolve({
|
|
151
|
+
user: { address: "0x123", chainId: 1, networkId: "mainnet" },
|
|
152
|
+
admins: [],
|
|
153
|
+
auth_enabled: true,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
150
156
|
const handler = vi.fn<NodeHandler>();
|
|
151
157
|
const wrapped = requireAuth(service, handler);
|
|
152
158
|
|
|
@@ -166,6 +172,7 @@ describe("requireAuth", () => {
|
|
|
166
172
|
|
|
167
173
|
it("returns 500 with a sanitized body when AuthService throws", async () => {
|
|
168
174
|
const { service } = makeAuthService(async () => {
|
|
175
|
+
await Promise.resolve();
|
|
169
176
|
throw new Error("transient Renown failure: secret-internal-detail");
|
|
170
177
|
});
|
|
171
178
|
const handler = vi.fn<NodeHandler>();
|
|
@@ -187,11 +194,13 @@ describe("requireAuth", () => {
|
|
|
187
194
|
});
|
|
188
195
|
|
|
189
196
|
it("calls authService.verifyBearer with the incoming authorization header", async () => {
|
|
190
|
-
const { service, spy } = makeAuthService(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
const { service, spy } = makeAuthService(() =>
|
|
198
|
+
Promise.resolve({
|
|
199
|
+
user: { address: "0x1", chainId: 1, networkId: "mainnet" },
|
|
200
|
+
admins: [],
|
|
201
|
+
auth_enabled: true,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
195
204
|
const wrapped = requireAuth(service, vi.fn());
|
|
196
205
|
|
|
197
206
|
await wrapped(
|
|
@@ -204,11 +213,13 @@ describe("requireAuth", () => {
|
|
|
204
213
|
});
|
|
205
214
|
|
|
206
215
|
it("calls verifyBearer with undefined when no authorization header is present", async () => {
|
|
207
|
-
const { service, spy } = makeAuthService(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
const { service, spy } = makeAuthService(() =>
|
|
217
|
+
Promise.resolve({
|
|
218
|
+
user: undefined,
|
|
219
|
+
admins: [],
|
|
220
|
+
auth_enabled: true,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
212
223
|
const wrapped = requireAuth(service, vi.fn());
|
|
213
224
|
|
|
214
225
|
await wrapped(makeReq({ headers: {} }), makeRes());
|
|
@@ -61,11 +61,13 @@ function makeRes() {
|
|
|
61
61
|
|
|
62
62
|
describe("mountAuthenticatedNodeRoute", () => {
|
|
63
63
|
it("wraps the handler with auth enforcement when authService is defined", async () => {
|
|
64
|
-
const verifyBearer = vi.fn(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const verifyBearer = vi.fn(() =>
|
|
65
|
+
Promise.resolve({
|
|
66
|
+
user: undefined,
|
|
67
|
+
admins: [],
|
|
68
|
+
auth_enabled: true,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
69
71
|
const authService = { verifyBearer } as unknown as AuthService;
|
|
70
72
|
const { api, captured } = makeFakeApi(authService);
|
|
71
73
|
const inner = vi.fn();
|
|
@@ -88,11 +90,13 @@ describe("mountAuthenticatedNodeRoute", () => {
|
|
|
88
90
|
});
|
|
89
91
|
|
|
90
92
|
it("invokes the inner handler when verifyBearer returns a valid user", async () => {
|
|
91
|
-
const verifyBearer = vi.fn(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const verifyBearer = vi.fn(() =>
|
|
94
|
+
Promise.resolve({
|
|
95
|
+
user: { address: "0x1", chainId: 1, networkId: "mainnet" },
|
|
96
|
+
admins: [],
|
|
97
|
+
auth_enabled: true,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
96
100
|
const authService = { verifyBearer } as unknown as AuthService;
|
|
97
101
|
const { api, captured } = makeFakeApi(authService);
|
|
98
102
|
const inner = vi.fn();
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import type { API } from "@powerhousedao/reactor-api";
|
|
3
|
+
import { createHttpAdapter } from "@powerhousedao/reactor-api";
|
|
4
|
+
import {
|
|
5
|
+
AttachmentBuilder,
|
|
6
|
+
type AttachmentBuildResult,
|
|
7
|
+
createRemoteAttachmentService,
|
|
8
|
+
} from "@powerhousedao/reactor-attachments";
|
|
9
|
+
import type { IRenown } from "@renown/sdk/node";
|
|
10
|
+
import { Kysely } from "kysely";
|
|
11
|
+
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import type { Server } from "node:http";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
17
|
+
import { registerAttachmentRoutes } from "../../src/attachments/index.js";
|
|
18
|
+
import { deriveAttachmentServiceConfig } from "../../src/server.mjs";
|
|
19
|
+
|
|
20
|
+
describe("deriveAttachmentServiceConfig", () => {
|
|
21
|
+
const ORIGINAL_PUBLIC_URL = process.env.PH_SWITCHBOARD_PUBLIC_URL;
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
if (ORIGINAL_PUBLIC_URL === undefined) {
|
|
25
|
+
delete process.env.PH_SWITCHBOARD_PUBLIC_URL;
|
|
26
|
+
} else {
|
|
27
|
+
process.env.PH_SWITCHBOARD_PUBLIC_URL = ORIGINAL_PUBLIC_URL;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("defaults to http://localhost:${port} with no auth when renown is null", () => {
|
|
32
|
+
delete process.env.PH_SWITCHBOARD_PUBLIC_URL;
|
|
33
|
+
const config = deriveAttachmentServiceConfig({}, 4001, null);
|
|
34
|
+
expect(config.remoteUrl).toBe("http://localhost:4001");
|
|
35
|
+
expect(config.jwtHandler).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("uses https when options.https is set", () => {
|
|
39
|
+
delete process.env.PH_SWITCHBOARD_PUBLIC_URL;
|
|
40
|
+
const config = deriveAttachmentServiceConfig({ https: true }, 4443, null);
|
|
41
|
+
expect(config.remoteUrl).toBe("https://localhost:4443");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("prefers PH_SWITCHBOARD_PUBLIC_URL over the localhost default", () => {
|
|
45
|
+
process.env.PH_SWITCHBOARD_PUBLIC_URL = "https://sb.example.com";
|
|
46
|
+
const config = deriveAttachmentServiceConfig({}, 4001, null);
|
|
47
|
+
expect(config.remoteUrl).toBe("https://sb.example.com");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("prefers the explicit attachmentServiceUrl option above all else", () => {
|
|
51
|
+
process.env.PH_SWITCHBOARD_PUBLIC_URL = "https://sb.example.com";
|
|
52
|
+
const config = deriveAttachmentServiceConfig(
|
|
53
|
+
{ attachmentServiceUrl: "https://override.example.com" },
|
|
54
|
+
4001,
|
|
55
|
+
null,
|
|
56
|
+
);
|
|
57
|
+
expect(config.remoteUrl).toBe("https://override.example.com");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("builds a jwtHandler from renown that scopes the token to the request url", async () => {
|
|
61
|
+
const calls: Array<{ expiresIn: number; aud: string }> = [];
|
|
62
|
+
const renown = {
|
|
63
|
+
user: { address: "0xabc" },
|
|
64
|
+
getBearerToken: (opts: { expiresIn: number; aud: string }) => {
|
|
65
|
+
calls.push(opts);
|
|
66
|
+
return Promise.resolve("tok-for-" + opts.aud);
|
|
67
|
+
},
|
|
68
|
+
} as unknown as IRenown;
|
|
69
|
+
|
|
70
|
+
const { jwtHandler } = deriveAttachmentServiceConfig({}, 4001, renown);
|
|
71
|
+
expect(jwtHandler).toBeDefined();
|
|
72
|
+
const token = await jwtHandler!("http://localhost:4001/attachments/x");
|
|
73
|
+
expect(token).toBe("tok-for-http://localhost:4001/attachments/x");
|
|
74
|
+
expect(calls[0]).toEqual({
|
|
75
|
+
expiresIn: 10,
|
|
76
|
+
aud: "http://localhost:4001/attachments/x",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns undefined token when renown has no user identity", async () => {
|
|
81
|
+
const renown = {
|
|
82
|
+
user: null,
|
|
83
|
+
getBearerToken: () => Promise.resolve("should-not-be-called"),
|
|
84
|
+
} as unknown as IRenown;
|
|
85
|
+
|
|
86
|
+
const { jwtHandler } = deriveAttachmentServiceConfig({}, 4001, renown);
|
|
87
|
+
expect(jwtHandler).toBeDefined();
|
|
88
|
+
await expect(
|
|
89
|
+
jwtHandler!("http://localhost:4001/x"),
|
|
90
|
+
).resolves.toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("attachment service built from deriveAttachmentServiceConfig round-trips", () => {
|
|
95
|
+
let attachments: AttachmentBuildResult;
|
|
96
|
+
let kysely: Kysely<unknown>;
|
|
97
|
+
let storagePath: string;
|
|
98
|
+
let server: Server;
|
|
99
|
+
let port: number;
|
|
100
|
+
|
|
101
|
+
beforeAll(async () => {
|
|
102
|
+
const pglite = new PGlite();
|
|
103
|
+
kysely = new Kysely<unknown>({ dialect: new PGliteDialect(pglite) });
|
|
104
|
+
storagePath = await mkdtemp(join(tmpdir(), "switchboard-attach-cfg-"));
|
|
105
|
+
attachments = await new AttachmentBuilder(kysely, storagePath).build();
|
|
106
|
+
|
|
107
|
+
const { adapter } = await createHttpAdapter("express");
|
|
108
|
+
adapter.setupMiddleware({});
|
|
109
|
+
registerAttachmentRoutes({
|
|
110
|
+
httpAdapter: adapter,
|
|
111
|
+
attachments,
|
|
112
|
+
authService: undefined,
|
|
113
|
+
} as unknown as API);
|
|
114
|
+
|
|
115
|
+
server = await adapter.listen(0);
|
|
116
|
+
const addr = server.address();
|
|
117
|
+
if (!addr || typeof addr === "string") throw new Error("no addr");
|
|
118
|
+
port = addr.port;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
123
|
+
await kysely.destroy();
|
|
124
|
+
await rm(storagePath, { recursive: true, force: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("reserve -> upload -> get works against the live routes", async () => {
|
|
128
|
+
const config = deriveAttachmentServiceConfig(
|
|
129
|
+
{ attachmentServiceUrl: `http://127.0.0.1:${port}` },
|
|
130
|
+
port,
|
|
131
|
+
null,
|
|
132
|
+
);
|
|
133
|
+
const service = createRemoteAttachmentService(config);
|
|
134
|
+
|
|
135
|
+
const upload = await service.reserve({
|
|
136
|
+
mimeType: "text/plain",
|
|
137
|
+
fileName: "hello.txt",
|
|
138
|
+
extension: "txt",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const payload = "switchboard attachment service";
|
|
142
|
+
const bytes = new TextEncoder().encode(payload);
|
|
143
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
144
|
+
start(controller) {
|
|
145
|
+
controller.enqueue(bytes);
|
|
146
|
+
controller.close();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const result = await upload.send(stream);
|
|
150
|
+
expect(result.header.sizeBytes).toBe(bytes.byteLength);
|
|
151
|
+
|
|
152
|
+
const got = await service.get(result.ref);
|
|
153
|
+
expect(got.header.mimeType).toBe("text/plain");
|
|
154
|
+
const reader = got.body.getReader();
|
|
155
|
+
const chunks: Uint8Array[] = [];
|
|
156
|
+
for (;;) {
|
|
157
|
+
const { done, value } = await reader.read();
|
|
158
|
+
if (done) break;
|
|
159
|
+
chunks.push(value);
|
|
160
|
+
}
|
|
161
|
+
const total = chunks.reduce((n, c) => n + c.byteLength, 0);
|
|
162
|
+
const merged = new Uint8Array(total);
|
|
163
|
+
let off = 0;
|
|
164
|
+
for (const c of chunks) {
|
|
165
|
+
merged.set(c, off);
|
|
166
|
+
off += c.byteLength;
|
|
167
|
+
}
|
|
168
|
+
expect(new TextDecoder().decode(merged)).toBe(payload);
|
|
169
|
+
});
|
|
170
|
+
});
|