@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/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.45",
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.45",
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.45",
59
- "@powerhousedao/reactor": "6.2.3-dev.45",
60
- "@powerhousedao/shared": "6.2.3-dev.45",
61
- "@powerhousedao/vetra": "6.2.3-dev.45",
62
- "@powerhousedao/reactor-api": "6.2.3-dev.45",
63
- "@powerhousedao/reactor-attachments": "6.2.3-dev.45",
64
- "@powerhousedao/reactor-drive": "6.2.3-dev.45",
65
- "@powerhousedao/reactor-group": "6.2.3-dev.45",
66
- "@powerhousedao/reactor-privacy": "6.2.3-dev.45",
67
- "@powerhousedao/reactor-workflow": "6.2.3-dev.45",
68
- "@powerhousedao/workflow": "6.2.3-dev.45",
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.45",
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.45",
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 { AuthorizationPolicy, type Context } from "@powerhousedao/reactor-api";
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 { composeWorkflowRuntime } from "../src/workflow-runtime.mjs";
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
- describe("the workflow runtime's read check", () => {
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
- module?.reactor.kill();
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
- async function hostDeps() {
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.execute(id, "main", [
74
- initializeAuth({
75
- version: 1,
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
- const createWorkflowRuntime = vi.fn((_deps: Record<string, unknown>) => ({
96
- shutdown: vi.fn(),
97
- }));
98
- await composeWorkflowRuntime({
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: {} as never,
157
+ relationalDb: createRelationalDb(database) as IRelationalDb,
101
158
  attachments: {} as never,
102
- authorizationService: openAuthorization as never,
159
+ authorizationService: authorizationService as never,
103
160
  logger,
104
161
  load: () =>
105
162
  Promise.resolve({
106
- WORKFLOW_PACKAGE_NAME: "@powerhousedao/workflow",
107
- setPieceRegistryUrl: vi.fn(),
108
- createWorkflowRuntime,
109
- } as never),
163
+ ...engine,
164
+ createWorkflowRuntime: (deps) =>
165
+ (runtime = engine.createWorkflowRuntime(deps)),
166
+ }),
110
167
  });
111
- const [deps] = createWorkflowRuntime.mock.calls[0]!;
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 { id, deps } = await hostDeps();
172
+ const runtime = await hostRuntime(openAuthorization, { policed: true });
123
173
 
124
- await expect(deps.assertCanRead(id, contextFor())).rejects.toThrow();
125
174
  await expect(
126
- deps.assertCanRead(id, contextFor(OUTSIDER)),
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
- deps.assertCanRead(id, contextFor(READER)),
130
- ).resolves.toBeUndefined();
181
+ runtime.cancelTriggerTestFor(DOCUMENT, contextFor(READER)),
182
+ ).resolves.toBe(false);
131
183
  });
132
184
 
133
- it("reads a caller as its own subject, anonymous as the empty one", async () => {
134
- const { deps } = await hostDeps();
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(deps.subjectOf(contextFor(READER))).toEqual({
137
- address: READER,
138
- key: undefined,
139
- });
140
- expect(deps.subjectOf(contextFor())).toEqual({
141
- address: undefined,
142
- key: undefined,
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
  });