@powerhousedao/switchboard 6.2.3-dev.45 → 6.2.3-dev.47
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/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +22 -0
- package/package.json +15 -15
- package/test/workflow-read-gate.test.ts +125 -60
- package/test/workflow-runtime.test.ts +105 -169
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 6.2.3-dev.47 (2026-10-07)
|
|
2
|
+
|
|
3
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
4
|
+
|
|
5
|
+
## 6.2.3-dev.46 (2026-10-07)
|
|
6
|
+
|
|
7
|
+
### 🚀 Features
|
|
8
|
+
|
|
9
|
+
- **connect:** load local project models in the reactor worker ([6e2929ce60](https://github.com/powerhouse-inc/powerhouse/commit/6e2929ce60))
|
|
10
|
+
|
|
11
|
+
### 🩹 Fixes
|
|
12
|
+
|
|
13
|
+
- **builder-tools:** make the worker bundle survive consumer projects ([6361a66b6d](https://github.com/powerhouse-inc/powerhouse/commit/6361a66b6d))
|
|
14
|
+
- **builder-tools:** assert the vendor-dir mode only where modes exist ([f47c32a020](https://github.com/powerhouse-inc/powerhouse/commit/f47c32a020))
|
|
15
|
+
- **switchboard:** never let observability lower a fatal exit code ([0498342d22](https://github.com/powerhouse-inc/powerhouse/commit/0498342d22))
|
|
16
|
+
- **switchboard:** survive a logger that fails during fatal shutdown ([ecc8975282](https://github.com/powerhouse-inc/powerhouse/commit/ecc8975282))
|
|
17
|
+
|
|
18
|
+
### ❤️ Thank You
|
|
19
|
+
|
|
20
|
+
- Claude Fable 5
|
|
21
|
+
- Wouter Kampmann
|
|
22
|
+
|
|
1
23
|
## 6.2.3-dev.45 (2026-10-06)
|
|
2
24
|
|
|
3
25
|
### 🚀 Features
|
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.47",
|
|
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.47",
|
|
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-privacy": "6.2.3-dev.
|
|
67
|
-
"@powerhousedao/reactor-workflow": "6.2.3-dev.
|
|
68
|
-
"@powerhousedao/workflow": "6.2.3-dev.
|
|
58
|
+
"@powerhousedao/opentelemetry-instrumentation-reactor": "6.2.3-dev.47",
|
|
59
|
+
"@powerhousedao/reactor": "6.2.3-dev.47",
|
|
60
|
+
"@powerhousedao/shared": "6.2.3-dev.47",
|
|
61
|
+
"@powerhousedao/vetra": "6.2.3-dev.47",
|
|
62
|
+
"@powerhousedao/reactor-api": "6.2.3-dev.47",
|
|
63
|
+
"@powerhousedao/reactor-attachments": "6.2.3-dev.47",
|
|
64
|
+
"@powerhousedao/reactor-drive": "6.2.3-dev.47",
|
|
65
|
+
"@powerhousedao/reactor-group": "6.2.3-dev.47",
|
|
66
|
+
"@powerhousedao/reactor-privacy": "6.2.3-dev.47",
|
|
67
|
+
"@powerhousedao/reactor-workflow": "6.2.3-dev.47",
|
|
68
|
+
"@powerhousedao/workflow": "6.2.3-dev.47",
|
|
69
69
|
"@pyroscope/nodejs": "^0.4.5",
|
|
70
|
-
"@renown/sdk": "6.2.3-dev.
|
|
70
|
+
"@renown/sdk": "6.2.3-dev.47",
|
|
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.47",
|
|
74
74
|
"dotenv": "^16.4.7",
|
|
75
75
|
"express": "^4.22.1",
|
|
76
76
|
"graphql": "^16",
|
|
@@ -1,21 +1,41 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
1
2
|
import {
|
|
2
3
|
ReactorBuilder,
|
|
3
4
|
ReactorClientBuilder,
|
|
4
5
|
type InProcessReactorClientModule,
|
|
5
6
|
} from "@powerhousedao/reactor";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AuthorizationPolicy,
|
|
9
|
+
type Context,
|
|
10
|
+
type IAuthorizationService,
|
|
11
|
+
} from "@powerhousedao/reactor-api";
|
|
12
|
+
import * as engine from "@powerhousedao/reactor-workflow";
|
|
13
|
+
import type { WorkflowRuntimeService } from "@powerhousedao/reactor-workflow";
|
|
7
14
|
import {
|
|
8
15
|
initializeAuth,
|
|
9
16
|
withSignaturePolicy,
|
|
10
17
|
type DocumentModelModule,
|
|
11
18
|
} from "@powerhousedao/shared/document-model";
|
|
19
|
+
import {
|
|
20
|
+
createRelationalDb,
|
|
21
|
+
type IRelationalDb,
|
|
22
|
+
} from "@powerhousedao/shared/processors";
|
|
23
|
+
import { Connection } from "@powerhousedao/workflow/document-models/connection";
|
|
12
24
|
import type { ILogger } from "document-model";
|
|
13
25
|
import { documentModelDocumentModelModule } from "document-model";
|
|
26
|
+
import { Kysely } from "kysely";
|
|
27
|
+
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
14
28
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
15
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
composeWorkflowRuntime,
|
|
31
|
+
type ComposedWorkflowRuntime,
|
|
32
|
+
} from "../src/workflow-runtime.mjs";
|
|
16
33
|
|
|
17
34
|
const READER = "0xreader";
|
|
35
|
+
const WRITER = "0xwriter";
|
|
18
36
|
const OUTSIDER = "0xoutsider";
|
|
37
|
+
const DOCUMENT = "wf-policed";
|
|
38
|
+
const CONNECTION = "conn-policed";
|
|
19
39
|
|
|
20
40
|
// The host's legacy layer under OPEN admits everyone, anonymous included.
|
|
21
41
|
const openAuthorization = {
|
|
@@ -29,6 +49,19 @@ const openAuthorization = {
|
|
|
29
49
|
canWrite: () => Promise.resolve(true),
|
|
30
50
|
};
|
|
31
51
|
|
|
52
|
+
// Document permissions: everyone reads, only the writer writes.
|
|
53
|
+
const writerOnlyAuthorization = {
|
|
54
|
+
config: {
|
|
55
|
+
admins: [],
|
|
56
|
+
defaultProtection: false,
|
|
57
|
+
policy: AuthorizationPolicy.DOCUMENT_PERMISSIONS,
|
|
58
|
+
},
|
|
59
|
+
isSupremeAdmin: () => false,
|
|
60
|
+
canRead: () => Promise.resolve(true),
|
|
61
|
+
canWrite: (_id: string, address?: string) =>
|
|
62
|
+
Promise.resolve(address === WRITER),
|
|
63
|
+
};
|
|
64
|
+
|
|
32
65
|
const logger = {
|
|
33
66
|
verbose: vi.fn(),
|
|
34
67
|
debug: vi.fn(),
|
|
@@ -41,20 +74,56 @@ const logger = {
|
|
|
41
74
|
const contextFor = (address?: string) =>
|
|
42
75
|
({ headers: {}, db: {}, user: address ? { address } : undefined }) as Context;
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
const readerOnly = (id: string) =>
|
|
78
|
+
initializeAuth({
|
|
79
|
+
version: 1,
|
|
80
|
+
grants: [
|
|
81
|
+
{
|
|
82
|
+
id: `${id}-read`,
|
|
83
|
+
description: "the reader reads the domain",
|
|
84
|
+
effect: "allow",
|
|
85
|
+
principal: { address: READER },
|
|
86
|
+
capability: { can: "read", scope: "global" },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: `${id}-admin`,
|
|
90
|
+
description: "administration stays reachable",
|
|
91
|
+
effect: "allow",
|
|
92
|
+
principal: { anyone: true },
|
|
93
|
+
capability: { can: "execute", scope: "auth" },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("the workflow runtime's access checks", () => {
|
|
45
99
|
let module: InProcessReactorClientModule | undefined;
|
|
100
|
+
let composed: ComposedWorkflowRuntime | undefined;
|
|
101
|
+
let database: Kysely<unknown> | undefined;
|
|
46
102
|
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
|
|
103
|
+
afterEach(async () => {
|
|
104
|
+
await composed?.stop();
|
|
105
|
+
await module?.reactor.kill().completed;
|
|
106
|
+
await database?.destroy();
|
|
107
|
+
composed = undefined;
|
|
49
108
|
module = undefined;
|
|
109
|
+
database = undefined;
|
|
50
110
|
});
|
|
51
111
|
|
|
52
|
-
|
|
112
|
+
// The real engine, composed as the host composes it; the runtime it builds
|
|
113
|
+
// is kept so the tests can call it as the subgraph would.
|
|
114
|
+
async function hostRuntime(
|
|
115
|
+
authorizationService: Pick<
|
|
116
|
+
IAuthorizationService,
|
|
117
|
+
"config" | "isSupremeAdmin" | "canRead" | "canWrite"
|
|
118
|
+
>,
|
|
119
|
+
options: { policed: boolean },
|
|
120
|
+
): Promise<WorkflowRuntimeService> {
|
|
53
121
|
module = await new ReactorClientBuilder()
|
|
54
122
|
.withReactorBuilder(
|
|
55
123
|
new ReactorBuilder()
|
|
56
124
|
.withDocumentModelSources([
|
|
57
125
|
documentModelDocumentModelModule as unknown as DocumentModelModule,
|
|
126
|
+
Connection as unknown as DocumentModelModule,
|
|
58
127
|
])
|
|
59
128
|
.withExecutorConfig({
|
|
60
129
|
featureFlags: { documentDecisions: true, authEnforcement: true },
|
|
@@ -62,84 +131,80 @@ describe("the workflow runtime's read check", () => {
|
|
|
62
131
|
)
|
|
63
132
|
.buildModule();
|
|
64
133
|
const client = module.client;
|
|
65
|
-
const id = "wf-policed";
|
|
66
134
|
await client.create(
|
|
67
135
|
withSignaturePolicy(
|
|
68
136
|
documentModelDocumentModelModule.utils.createDocument(),
|
|
69
137
|
"legacy",
|
|
70
|
-
{ id },
|
|
138
|
+
{ id: DOCUMENT },
|
|
71
139
|
),
|
|
72
140
|
);
|
|
73
|
-
await client.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
grants: [
|
|
77
|
-
{
|
|
78
|
-
id: "g-read",
|
|
79
|
-
description: "the reader reads the domain",
|
|
80
|
-
effect: "allow",
|
|
81
|
-
principal: { address: READER },
|
|
82
|
-
capability: { can: "read", scope: "global" },
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "g-admin",
|
|
86
|
-
description: "administration stays reachable",
|
|
87
|
-
effect: "allow",
|
|
88
|
-
principal: { anyone: true },
|
|
89
|
-
capability: { can: "execute", scope: "auth" },
|
|
90
|
-
},
|
|
91
|
-
],
|
|
141
|
+
await client.create(
|
|
142
|
+
withSignaturePolicy(Connection.utils.createDocument(), "legacy", {
|
|
143
|
+
id: CONNECTION,
|
|
92
144
|
}),
|
|
93
|
-
|
|
145
|
+
);
|
|
146
|
+
if (options.policed) {
|
|
147
|
+
await client.execute(DOCUMENT, "main", [readerOnly(DOCUMENT)]);
|
|
148
|
+
await client.execute(CONNECTION, "main", [readerOnly(CONNECTION)]);
|
|
149
|
+
}
|
|
94
150
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
98
|
-
|
|
151
|
+
database = new Kysely<unknown>({
|
|
152
|
+
dialect: new PGliteDialect(new PGlite()),
|
|
153
|
+
});
|
|
154
|
+
let runtime: WorkflowRuntimeService | undefined;
|
|
155
|
+
composed = await composeWorkflowRuntime({
|
|
99
156
|
reactorClient: client,
|
|
100
|
-
relationalDb:
|
|
157
|
+
relationalDb: createRelationalDb(database) as IRelationalDb,
|
|
101
158
|
attachments: {} as never,
|
|
102
|
-
authorizationService:
|
|
159
|
+
authorizationService: authorizationService as never,
|
|
103
160
|
logger,
|
|
104
161
|
load: () =>
|
|
105
162
|
Promise.resolve({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
163
|
+
...engine,
|
|
164
|
+
createWorkflowRuntime: (deps) =>
|
|
165
|
+
(runtime = engine.createWorkflowRuntime(deps)),
|
|
166
|
+
}),
|
|
110
167
|
});
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
id,
|
|
114
|
-
deps: deps as {
|
|
115
|
-
assertCanRead: (id: string, caller: object) => Promise<unknown>;
|
|
116
|
-
subjectOf: (caller: object) => unknown;
|
|
117
|
-
},
|
|
118
|
-
};
|
|
168
|
+
return runtime!;
|
|
119
169
|
}
|
|
120
170
|
|
|
121
171
|
it("refuses under OPEN a document the caller is not served", async () => {
|
|
122
|
-
const
|
|
172
|
+
const runtime = await hostRuntime(openAuthorization, { policed: true });
|
|
123
173
|
|
|
124
|
-
await expect(deps.assertCanRead(id, contextFor())).rejects.toThrow();
|
|
125
174
|
await expect(
|
|
126
|
-
|
|
175
|
+
runtime.cancelTriggerTestFor(DOCUMENT, contextFor()),
|
|
176
|
+
).rejects.toThrow();
|
|
177
|
+
await expect(
|
|
178
|
+
runtime.cancelTriggerTestFor(DOCUMENT, contextFor(OUTSIDER)),
|
|
127
179
|
).rejects.toThrow();
|
|
128
180
|
await expect(
|
|
129
|
-
|
|
130
|
-
).resolves.
|
|
181
|
+
runtime.cancelTriggerTestFor(DOCUMENT, contextFor(READER)),
|
|
182
|
+
).resolves.toBe(false);
|
|
131
183
|
});
|
|
132
184
|
|
|
133
|
-
it("
|
|
134
|
-
const
|
|
185
|
+
it("lists a connection only to the callers it is served to", async () => {
|
|
186
|
+
const runtime = await hostRuntime(openAuthorization, { policed: true });
|
|
187
|
+
const listed = async (address?: string) =>
|
|
188
|
+
(await runtime.connections(contextFor(address))).map(({ id }) => id);
|
|
135
189
|
|
|
136
|
-
expect(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
expect(await listed(READER)).toEqual([CONNECTION]);
|
|
191
|
+
expect(await listed(OUTSIDER)).toEqual([]);
|
|
192
|
+
expect(await listed()).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("refuses a write the host's permissions deny", async () => {
|
|
196
|
+
const runtime = await hostRuntime(writerOnlyAuthorization, {
|
|
197
|
+
policed: false,
|
|
143
198
|
});
|
|
199
|
+
const startOAuth = (address: string) =>
|
|
200
|
+
runtime.startOAuth(CONNECTION, contextFor(address), {
|
|
201
|
+
redirectUri: "https://host/oauth/callback",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await expect(startOAuth(READER)).rejects.toThrow("to write this document");
|
|
205
|
+
// Past the gate: refused for what the connection is, not who asked.
|
|
206
|
+
await expect(startOAuth(WRITER)).rejects.toThrow(
|
|
207
|
+
"does not sign in with OAuth2",
|
|
208
|
+
);
|
|
144
209
|
});
|
|
145
210
|
});
|