@powerhousedao/switchboard 6.2.3-dev.5 → 6.2.3-dev.51
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/byte-grants.test.d.ts +2 -0
- package/.tsbuild/test/attachments/byte-grants.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts +2 -0
- package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts +2 -0
- package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/url-signer.test.d.ts +2 -0
- package/.tsbuild/test/attachments/url-signer.test.d.ts.map +1 -0
- 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-mcp-enabled.test.d.ts +2 -0
- package/.tsbuild/test/boot-mcp-enabled.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-privacy.test.d.ts +2 -0
- package/.tsbuild/test/boot-privacy.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-unsupported-stored-documents.test.d.ts +2 -0
- package/.tsbuild/test/boot-unsupported-stored-documents.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/boot-workflow-singleton-conflict.test.d.ts +2 -0
- package/.tsbuild/test/boot-workflow-singleton-conflict.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-workflow-singleton-retry.test.d.ts +2 -0
- package/.tsbuild/test/boot-workflow-singleton-retry.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/durable-node-fs-pg16.test.d.ts +2 -0
- package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
- package/.tsbuild/test/fatal-shutdown.test.d.ts +2 -0
- package/.tsbuild/test/fatal-shutdown.test.d.ts.map +1 -0
- package/.tsbuild/test/mcp-flag.test.d.ts +2 -0
- package/.tsbuild/test/mcp-flag.test.d.ts.map +1 -0
- package/.tsbuild/test/observability-signals.test.d.ts +2 -0
- package/.tsbuild/test/observability-signals.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
- package/.tsbuild/test/pglite-preflight.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/test/snapshot-writer.d.ts +18 -0
- package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
- package/.tsbuild/test/unsupported-stored-documents.test.d.ts +2 -0
- package/.tsbuild/test/unsupported-stored-documents.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/harness.d.ts +56 -0
- package/.tsbuild/test/workflow/harness.d.ts.map +1 -0
- package/.tsbuild/test/workflow/oauth.test.d.ts +2 -0
- package/.tsbuild/test/workflow/oauth.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/resolvers.test.d.ts +2 -0
- package/.tsbuild/test/workflow/resolvers.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-signed.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-signed.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-trigger-test.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-trigger-test.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
- package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
- package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow-read-gate.test.d.ts +2 -0
- package/.tsbuild/test/workflow-read-gate.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow-runtime.test.d.ts +2 -0
- package/.tsbuild/test/workflow-runtime.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 +94 -0
- package/CHANGELOG.md +541 -0
- package/README.md +35 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +48 -49
- 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-J9fpd42v.mjs +3549 -0
- package/dist/server-J9fpd42v.mjs.map +1 -0
- package/dist/server.d.mts +155 -30
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +4 -5
- 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 +22 -18
- package/test/attachment-reference-read-model.test.ts +6 -1
- package/test/attachments/auth.test.ts +85 -0
- package/test/attachments/byte-grants.test.ts +290 -0
- package/test/attachments/byte-routes-read-gate.integration.test.ts +314 -0
- package/test/attachments/download-target.test.ts +201 -32
- package/test/attachments/index.test.ts +34 -3
- package/test/attachments/reserve-metadata-parity.test.ts +96 -0
- package/test/attachments/routes-integration.test.ts +60 -4
- package/test/attachments/routes.test.ts +166 -23
- package/test/attachments/service-config.test.ts +13 -3
- package/test/attachments/url-signer.test.ts +105 -0
- package/test/boot-create-signature-policy.test.ts +108 -0
- package/test/boot-mcp-enabled.test.ts +121 -0
- package/test/boot-privacy.test.ts +243 -0
- package/test/boot-unsupported-stored-documents.test.ts +213 -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/boot-workflow-singleton-conflict.test.ts +107 -0
- package/test/boot-workflow-singleton-retry.test.ts +133 -0
- package/test/builder-defaults.test.ts +80 -0
- package/test/create-signature-policy.test.ts +69 -0
- package/test/durable-node-fs-pg16.test.ts +80 -0
- package/test/fatal-shutdown.test.ts +165 -0
- package/test/mcp-flag.test.ts +56 -0
- package/test/observability-signals.test.ts +131 -0
- package/test/pglite-dialect.test.ts +217 -2
- package/test/pglite-preflight.test.ts +313 -0
- package/test/pglite-version.test.ts +9 -1
- package/test/renown-trust-policy.test.ts +315 -0
- package/test/snapshot-writer.ts +94 -0
- package/test/unsupported-stored-documents.test.ts +72 -0
- package/test/worker-pool.test.ts +27 -1
- package/test/workflow/harness.ts +289 -0
- package/test/workflow/oauth.test.ts +214 -0
- package/test/workflow/resolvers.test.ts +357 -0
- package/test/workflow/webhook-dedupe.test.ts +295 -0
- package/test/workflow/webhook-signed.test.ts +509 -0
- package/test/workflow/webhook-trigger-test.test.ts +82 -0
- package/test/workflow/webhook-unsigned.test.ts +345 -0
- package/test/workflow-reactor-access.test.ts +384 -0
- package/test/workflow-reactor-enforcement.test.ts +808 -0
- package/test/workflow-read-gate.test.ts +213 -0
- package/test/workflow-runtime.test.ts +1067 -0
- package/tsconfig.json +9 -3
- package/tsdown.config.ts +3 -1
- package/dist/server-CiKkVE6g.mjs +0 -1555
- package/dist/server-CiKkVE6g.mjs.map +0 -1
- package/dist/utils-Baw7rThP.mjs.map +0 -1
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
// Under enforcement, with the Switchboard's Renown trust policy and real
|
|
2
|
+
// signers: who a publish makes the run user, and what a run may read and write.
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
ReactorBuilder,
|
|
9
|
+
ReactorClientBuilder,
|
|
10
|
+
type IReactorClient,
|
|
11
|
+
type InProcessReactorClientModule,
|
|
12
|
+
type SignatureTrustPolicy,
|
|
13
|
+
} from "@powerhousedao/reactor";
|
|
14
|
+
import {
|
|
15
|
+
AuthorizationPolicy,
|
|
16
|
+
getDbClient,
|
|
17
|
+
type Context,
|
|
18
|
+
type IAuthorizationService,
|
|
19
|
+
} from "@powerhousedao/reactor-api";
|
|
20
|
+
import {
|
|
21
|
+
CORE_PIECE_NAME,
|
|
22
|
+
CORE_PIECE_VERSION,
|
|
23
|
+
publishRunUser,
|
|
24
|
+
} from "@powerhousedao/reactor-workflow";
|
|
25
|
+
import {
|
|
26
|
+
initializeAuth,
|
|
27
|
+
withSignaturePolicy,
|
|
28
|
+
type Action,
|
|
29
|
+
type DocumentModelModule,
|
|
30
|
+
type Grant,
|
|
31
|
+
type ISigner,
|
|
32
|
+
} from "@powerhousedao/shared/document-model";
|
|
33
|
+
import {
|
|
34
|
+
createRelationalDb,
|
|
35
|
+
type IRelationalDb,
|
|
36
|
+
} from "@powerhousedao/shared/processors";
|
|
37
|
+
import {
|
|
38
|
+
Connection,
|
|
39
|
+
REACTOR_CONNECTOR_ID,
|
|
40
|
+
actions as connectionActions,
|
|
41
|
+
} from "@powerhousedao/workflow/document-models/connection";
|
|
42
|
+
import {
|
|
43
|
+
Workflow,
|
|
44
|
+
actions as workflowActions,
|
|
45
|
+
} from "@powerhousedao/workflow/document-models/workflow";
|
|
46
|
+
import {
|
|
47
|
+
buildAndSignCredential,
|
|
48
|
+
MemoryKeyStorage,
|
|
49
|
+
RenownCryptoBuilder,
|
|
50
|
+
RenownCryptoSigner,
|
|
51
|
+
type IRenown,
|
|
52
|
+
type PowerhouseVerifiableCredential,
|
|
53
|
+
type SignCredentialTypedData,
|
|
54
|
+
} from "@renown/sdk/node";
|
|
55
|
+
import type { ILogger } from "document-model";
|
|
56
|
+
import { documentModelDocumentModelModule } from "document-model";
|
|
57
|
+
import type { Kysely } from "kysely";
|
|
58
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
59
|
+
import { getRenownTrustPolicyConfig } from "../src/renown.js";
|
|
60
|
+
import {
|
|
61
|
+
composeWorkflowRuntime,
|
|
62
|
+
hostIdentityOf,
|
|
63
|
+
reactorAccessOf,
|
|
64
|
+
type ComposedWorkflowRuntime,
|
|
65
|
+
} from "../src/workflow-runtime.mjs";
|
|
66
|
+
|
|
67
|
+
// viem is the SDK's own dependency; the switchboard does not declare it.
|
|
68
|
+
type Account = {
|
|
69
|
+
address: `0x${string}`;
|
|
70
|
+
signTypedData(args: unknown): Promise<`0x${string}`>;
|
|
71
|
+
};
|
|
72
|
+
const { privateKeyToAccount } = createRequire(
|
|
73
|
+
import.meta.resolve("@renown/sdk/node"),
|
|
74
|
+
)("viem/accounts") as {
|
|
75
|
+
privateKeyToAccount: (key: `0x${string}`) => Account;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Well-known Anvil dev keys; never used for anything real.
|
|
79
|
+
const PUBLISHER = privateKeyToAccount(
|
|
80
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
|
|
81
|
+
);
|
|
82
|
+
const ATTACKER = privateKeyToAccount(
|
|
83
|
+
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
|
|
84
|
+
);
|
|
85
|
+
const HOST = privateKeyToAccount(
|
|
86
|
+
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const PIECE = "@powerhousedao/piece-enforcement-ops";
|
|
90
|
+
const PIECE_VERSION = "1.0.0";
|
|
91
|
+
|
|
92
|
+
const SOURCE = `
|
|
93
|
+
const text = (name) => ({ displayName: name, type: "SHORT_TEXT", required: false });
|
|
94
|
+
const action = (name, run) => ({
|
|
95
|
+
name,
|
|
96
|
+
displayName: name,
|
|
97
|
+
description: name,
|
|
98
|
+
requireAuth: false,
|
|
99
|
+
requireReactor: "write",
|
|
100
|
+
props: { id: text("id") },
|
|
101
|
+
run,
|
|
102
|
+
});
|
|
103
|
+
const setName = (name) => ({
|
|
104
|
+
id: "set-name-" + Date.now(),
|
|
105
|
+
type: "SET_NAME",
|
|
106
|
+
scope: "global",
|
|
107
|
+
input: { name },
|
|
108
|
+
timestampUtcMs: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
module.exports = {
|
|
111
|
+
app: {
|
|
112
|
+
displayName: "Ops",
|
|
113
|
+
description: "Document operations",
|
|
114
|
+
logoUrl: "https://example.com/ops.png",
|
|
115
|
+
categories: [],
|
|
116
|
+
actions: {
|
|
117
|
+
rename: action("rename", async (ctx) =>
|
|
118
|
+
(await ctx.reactor.execute(ctx.propsValue.id, "main", [setName("Renamed")])).header.name),
|
|
119
|
+
read: action("read", async (ctx) =>
|
|
120
|
+
(await ctx.reactor.find({ ids: [ctx.propsValue.id] })).results.length),
|
|
121
|
+
delete: action("delete", async (ctx) => {
|
|
122
|
+
await ctx.reactor.deleteDocument(ctx.propsValue.id);
|
|
123
|
+
return "deleted";
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
triggers: {},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
const logger = {
|
|
132
|
+
verbose: vi.fn(),
|
|
133
|
+
debug: vi.fn(),
|
|
134
|
+
info: vi.fn(),
|
|
135
|
+
warn: vi.fn(),
|
|
136
|
+
error: vi.fn(),
|
|
137
|
+
child: () => logger,
|
|
138
|
+
} as unknown as ILogger;
|
|
139
|
+
|
|
140
|
+
const ctxOf = (address: string) =>
|
|
141
|
+
({ headers: {}, db: {}, user: { address } }) as unknown as Context;
|
|
142
|
+
|
|
143
|
+
const grant = (
|
|
144
|
+
id: string,
|
|
145
|
+
principal: Grant["principal"],
|
|
146
|
+
capability: Grant["capability"],
|
|
147
|
+
): Grant => ({ id, description: id, effect: "allow", principal, capability });
|
|
148
|
+
|
|
149
|
+
const user = (address: string) => ({
|
|
150
|
+
address,
|
|
151
|
+
networkId: "eip155",
|
|
152
|
+
chainId: 1,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
async function signerFor(address: string): Promise<RenownCryptoSigner> {
|
|
156
|
+
const crypto = await new RenownCryptoBuilder()
|
|
157
|
+
.withKeyPairStorage(new MemoryKeyStorage())
|
|
158
|
+
.build();
|
|
159
|
+
return new RenownCryptoSigner(crypto, "connect", user(address));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The renown read model's row for a credential.
|
|
163
|
+
function credentialRow(c: PowerhouseVerifiableCredential) {
|
|
164
|
+
return {
|
|
165
|
+
documentId: "doc-cred",
|
|
166
|
+
credentialId: c.id,
|
|
167
|
+
context: c["@context"],
|
|
168
|
+
type: c.type,
|
|
169
|
+
issuerId: c.issuer.id,
|
|
170
|
+
issuerEthereumAddress: c.issuer.ethereumAddress,
|
|
171
|
+
issuanceDate: c.issuanceDate,
|
|
172
|
+
expirationDate: c.expirationDate,
|
|
173
|
+
credentialSubjectId: c.credentialSubject.id,
|
|
174
|
+
credentialSubjectApp: c.credentialSubject.app,
|
|
175
|
+
credentialStatusId: null,
|
|
176
|
+
credentialStatusType: null,
|
|
177
|
+
credentialSchemaId: c.credentialSchema.id,
|
|
178
|
+
credentialSchemaType: c.credentialSchema.type,
|
|
179
|
+
proofVerificationMethod: c.proof.verificationMethod,
|
|
180
|
+
proofEthereumAddress: c.proof.ethereumAddress,
|
|
181
|
+
proofCreated: c.proof.created,
|
|
182
|
+
proofPurpose: c.proof.proofPurpose,
|
|
183
|
+
proofType: c.proof.type,
|
|
184
|
+
proofValue: c.proof.proofValue,
|
|
185
|
+
proofEip712Domain: JSON.stringify(c.proof.eip712.domain),
|
|
186
|
+
proofEip712PrimaryType: c.proof.eip712.primaryType,
|
|
187
|
+
revoked: false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The account delegates the signer's key, as Renown login does.
|
|
192
|
+
function credentialFor(account: Account, key: string) {
|
|
193
|
+
return buildAndSignCredential({
|
|
194
|
+
signTypedData: ((args) =>
|
|
195
|
+
account.signTypedData(args)) as SignCredentialTypedData,
|
|
196
|
+
address: account.address,
|
|
197
|
+
chainId: 1,
|
|
198
|
+
app: "connect",
|
|
199
|
+
appId: key,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const OPEN: IAuthorizationService = {
|
|
204
|
+
config: {
|
|
205
|
+
admins: [],
|
|
206
|
+
defaultProtection: false,
|
|
207
|
+
policy: AuthorizationPolicy.OPEN,
|
|
208
|
+
},
|
|
209
|
+
// The reactor gate, not IAuthorizationService, is under test.
|
|
210
|
+
isSupremeAdmin: () => true,
|
|
211
|
+
canCreate: () => true,
|
|
212
|
+
canRead: () => Promise.resolve(true),
|
|
213
|
+
canWrite: () => Promise.resolve(true),
|
|
214
|
+
canManage: () => Promise.resolve(true),
|
|
215
|
+
canMutate: () => Promise.resolve(true),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
type Resolver = (parent: unknown, args: unknown, ctx: Context) => unknown;
|
|
219
|
+
type StepResult = {
|
|
220
|
+
runId: string;
|
|
221
|
+
status: string;
|
|
222
|
+
output: unknown;
|
|
223
|
+
error?: string | null;
|
|
224
|
+
errorName?: string | null;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
let root = "";
|
|
228
|
+
let module: InProcessReactorClientModule;
|
|
229
|
+
let hostSigner: RenownCryptoSigner;
|
|
230
|
+
let publisherSigner: RenownCryptoSigner;
|
|
231
|
+
let attackerSigner: RenownCryptoSigner;
|
|
232
|
+
let publisher: IReactorClient;
|
|
233
|
+
let composed: ComposedWorkflowRuntime;
|
|
234
|
+
let queries: Record<string, Resolver>;
|
|
235
|
+
let mutations: Record<string, Resolver>;
|
|
236
|
+
let trustPolicy: SignatureTrustPolicy;
|
|
237
|
+
let pieceBundle = "";
|
|
238
|
+
|
|
239
|
+
function clientOver(
|
|
240
|
+
signer?: ISigner,
|
|
241
|
+
over: InProcessReactorClientModule = module,
|
|
242
|
+
): Promise<IReactorClient> {
|
|
243
|
+
const builder = new ReactorClientBuilder().withReactor(
|
|
244
|
+
over.reactor,
|
|
245
|
+
over.eventBus,
|
|
246
|
+
over.documentIndexer,
|
|
247
|
+
over.documentView,
|
|
248
|
+
);
|
|
249
|
+
return (signer ? builder.withSigner(signer) : builder).build();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function createDocument(
|
|
253
|
+
model: DocumentModelModule,
|
|
254
|
+
id: string,
|
|
255
|
+
grants: Grant[],
|
|
256
|
+
client: IReactorClient = publisher,
|
|
257
|
+
) {
|
|
258
|
+
await client.create(
|
|
259
|
+
withSignaturePolicy(model.utils.createDocument(), "legacy", { id }),
|
|
260
|
+
);
|
|
261
|
+
await client.execute(id, "main", [initializeAuth({ version: 1, grants })]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type Harness = {
|
|
265
|
+
module: InProcessReactorClientModule;
|
|
266
|
+
publisher: IReactorClient;
|
|
267
|
+
composed: ComposedWorkflowRuntime;
|
|
268
|
+
queries: Record<string, Resolver>;
|
|
269
|
+
mutations: Record<string, Resolver>;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// A reactor signing as HOST under the Renown trust policy, with the runtime.
|
|
273
|
+
async function harness(authEnforcement: boolean): Promise<Harness> {
|
|
274
|
+
const built = await new ReactorClientBuilder()
|
|
275
|
+
.withSigner(hostSigner)
|
|
276
|
+
.withCreateSignaturePolicy("legacy")
|
|
277
|
+
.withReactorBuilder(
|
|
278
|
+
new ReactorBuilder()
|
|
279
|
+
.withDocumentModelSources([
|
|
280
|
+
Workflow as unknown as DocumentModelModule,
|
|
281
|
+
Connection as unknown as DocumentModelModule,
|
|
282
|
+
documentModelDocumentModelModule as unknown as DocumentModelModule,
|
|
283
|
+
])
|
|
284
|
+
.withExecutorConfig({
|
|
285
|
+
featureFlags: authEnforcement
|
|
286
|
+
? { documentDecisions: true, authEnforcement: true }
|
|
287
|
+
: {},
|
|
288
|
+
})
|
|
289
|
+
.withTrustPolicy(trustPolicy),
|
|
290
|
+
)
|
|
291
|
+
.buildModule();
|
|
292
|
+
const client = await clientOver(publisherSigner, built);
|
|
293
|
+
|
|
294
|
+
await client.create(
|
|
295
|
+
withSignaturePolicy(Connection.utils.createDocument(), "legacy", {
|
|
296
|
+
id: "conn-open",
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
await client.execute("conn-open", "main", [
|
|
300
|
+
connectionActions.setConnector({
|
|
301
|
+
connectorId: REACTOR_CONNECTOR_ID,
|
|
302
|
+
authType: "REACTOR",
|
|
303
|
+
}),
|
|
304
|
+
connectionActions.setConfig({ config: { endpoint: "local" } }),
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const { db } = getDbClient();
|
|
308
|
+
const relationalDb = createRelationalDb(
|
|
309
|
+
db as unknown as Kysely<unknown>,
|
|
310
|
+
) as IRelationalDb;
|
|
311
|
+
const pieces = new Map([
|
|
312
|
+
[root, [{ name: PIECE, version: PIECE_VERSION, bundleDir: pieceBundle }]],
|
|
313
|
+
]);
|
|
314
|
+
const runtime = await composeWorkflowRuntime({
|
|
315
|
+
reactorClient: built.client,
|
|
316
|
+
clientModule: built,
|
|
317
|
+
relationalDb,
|
|
318
|
+
attachments: {} as never,
|
|
319
|
+
authorizationService: OPEN,
|
|
320
|
+
pieces: { getPieces: () => pieces, onPiecesChange: () => undefined },
|
|
321
|
+
logger,
|
|
322
|
+
// Two harnesses share one database on purpose; placement has its own
|
|
323
|
+
// suite.
|
|
324
|
+
singletonLease: false,
|
|
325
|
+
});
|
|
326
|
+
const subgraph = new runtime.subgraph({
|
|
327
|
+
reactorClient: built.client,
|
|
328
|
+
authorizationService: OPEN,
|
|
329
|
+
relationalDb,
|
|
330
|
+
} as never);
|
|
331
|
+
const resolvers = subgraph.resolvers as Record<
|
|
332
|
+
string,
|
|
333
|
+
Record<string, Resolver>
|
|
334
|
+
>;
|
|
335
|
+
return {
|
|
336
|
+
module: built,
|
|
337
|
+
publisher: client,
|
|
338
|
+
composed: runtime,
|
|
339
|
+
queries: resolvers.WorkflowRuntimeQueries,
|
|
340
|
+
mutations: resolvers.WorkflowRuntimeMutations,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const publish = () =>
|
|
345
|
+
workflowActions.publishWorkflow({ publishedAt: new Date().toISOString() });
|
|
346
|
+
|
|
347
|
+
beforeAll(async () => {
|
|
348
|
+
root = await mkdtemp(join(tmpdir(), "switchboard-enforcement-"));
|
|
349
|
+
pieceBundle = join(root, "dist", "node", "pieces", "ops");
|
|
350
|
+
await mkdir(pieceBundle, { recursive: true });
|
|
351
|
+
await writeFile(
|
|
352
|
+
join(pieceBundle, "package.json"),
|
|
353
|
+
JSON.stringify({ name: PIECE, version: PIECE_VERSION, main: "index.js" }),
|
|
354
|
+
);
|
|
355
|
+
await writeFile(join(pieceBundle, "index.js"), SOURCE);
|
|
356
|
+
|
|
357
|
+
hostSigner = await signerFor(HOST.address);
|
|
358
|
+
publisherSigner = await signerFor(PUBLISHER.address);
|
|
359
|
+
// A Renown user in their own right, whose key claims the publisher.
|
|
360
|
+
attackerSigner = await signerFor(PUBLISHER.address);
|
|
361
|
+
const rows = [
|
|
362
|
+
credentialRow(await credentialFor(PUBLISHER, publisherSigner.app.key)),
|
|
363
|
+
credentialRow(await credentialFor(ATTACKER, attackerSigner.app.key)),
|
|
364
|
+
];
|
|
365
|
+
const trust = await getRenownTrustPolicyConfig(
|
|
366
|
+
{
|
|
367
|
+
source: "self",
|
|
368
|
+
request: () => Promise.resolve({ renownCredentials: rows }),
|
|
369
|
+
},
|
|
370
|
+
{ signer: hostSigner } as unknown as IRenown,
|
|
371
|
+
);
|
|
372
|
+
trustPolicy = trust.trustPolicy!;
|
|
373
|
+
|
|
374
|
+
({ module, publisher, composed, queries, mutations } = await harness(true));
|
|
375
|
+
}, 60_000);
|
|
376
|
+
|
|
377
|
+
afterAll(async () => {
|
|
378
|
+
await composed.stop();
|
|
379
|
+
module.reactor.kill();
|
|
380
|
+
await rm(root, { recursive: true, force: true });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("signer trust", () => {
|
|
384
|
+
const WORKFLOW = "wf-trust";
|
|
385
|
+
|
|
386
|
+
beforeAll(async () => {
|
|
387
|
+
await publisher.create(
|
|
388
|
+
withSignaturePolicy(Workflow.utils.createDocument(), "legacy", {
|
|
389
|
+
id: WORKFLOW,
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
await publisher.execute(WORKFLOW, "main", [
|
|
393
|
+
initializeAuth({
|
|
394
|
+
version: 1,
|
|
395
|
+
grants: [
|
|
396
|
+
grant(
|
|
397
|
+
"publisher",
|
|
398
|
+
{ address: PUBLISHER.address },
|
|
399
|
+
{ can: "execute" },
|
|
400
|
+
),
|
|
401
|
+
// Anyone may publish, so only the signature decides the run user.
|
|
402
|
+
grant(
|
|
403
|
+
"anyone",
|
|
404
|
+
{ anyone: true },
|
|
405
|
+
{ can: "execute", scope: "global" },
|
|
406
|
+
),
|
|
407
|
+
],
|
|
408
|
+
}),
|
|
409
|
+
]);
|
|
410
|
+
await publisher.execute(WORKFLOW, "main", [publish()]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Straight to the reactor, so no client signs or rewrites the claim.
|
|
414
|
+
async function submitted(action: Action) {
|
|
415
|
+
const job = await module.reactor.execute(WORKFLOW, "main", [action]);
|
|
416
|
+
return module.client.waitForJob(job);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
it("makes a credentialed publisher the run user", async () => {
|
|
420
|
+
const runUser = await publishRunUser(module.client, WORKFLOW);
|
|
421
|
+
expect(runUser?.address).toBe(PUBLISHER.address);
|
|
422
|
+
expect(runUser?.subject.key).toBe(publisherSigner.app.key);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("refuses a key that claims the publisher without their credential", async () => {
|
|
426
|
+
const attacker = await clientOver(attackerSigner);
|
|
427
|
+
|
|
428
|
+
await expect(
|
|
429
|
+
attacker.execute(WORKFLOW, "main", [publish()]),
|
|
430
|
+
).rejects.toThrow(/may not sign as/);
|
|
431
|
+
const runUser = await publishRunUser(module.client, WORKFLOW);
|
|
432
|
+
expect(runUser?.subject.key).toBe(publisherSigner.app.key);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("refuses a signature moved under the publisher's key", async () => {
|
|
436
|
+
const action = publish();
|
|
437
|
+
const forged = await attackerSigner.signAction(action, {
|
|
438
|
+
documentId: WORKFLOW,
|
|
439
|
+
branch: "main",
|
|
440
|
+
} as never);
|
|
441
|
+
const [timestamp, , hash, signature, ...rest] = forged;
|
|
442
|
+
action.context = {
|
|
443
|
+
signer: {
|
|
444
|
+
user: user(PUBLISHER.address),
|
|
445
|
+
app: { name: "connect", key: publisherSigner.app.key },
|
|
446
|
+
signatures: [
|
|
447
|
+
[timestamp, publisherSigner.app.key, hash, signature, ...rest],
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
} as Action["context"];
|
|
451
|
+
|
|
452
|
+
await expect(submitted(action)).resolves.toMatchObject({
|
|
453
|
+
status: "FAILED",
|
|
454
|
+
});
|
|
455
|
+
const runUser = await publishRunUser(module.client, WORKFLOW);
|
|
456
|
+
expect(runUser?.subject.key).toBe(publisherSigner.app.key);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("refuses an unsigned publish that claims the publisher", async () => {
|
|
460
|
+
const action = publish();
|
|
461
|
+
action.context = {
|
|
462
|
+
signer: {
|
|
463
|
+
user: user(PUBLISHER.address),
|
|
464
|
+
app: { name: "connect", key: "" },
|
|
465
|
+
signatures: [],
|
|
466
|
+
},
|
|
467
|
+
} as Action["context"];
|
|
468
|
+
|
|
469
|
+
const job = await submitted(action);
|
|
470
|
+
expect(job.status).toBe("FAILED");
|
|
471
|
+
expect(JSON.stringify(job.error)).toMatch(/UNSIGNED_IDENTITY/);
|
|
472
|
+
const runUser = await publishRunUser(module.client, WORKFLOW);
|
|
473
|
+
expect(runUser?.subject.key).toBe(publisherSigner.app.key);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// mutateDocument has the host sign an action sent unsigned through GraphQL.
|
|
477
|
+
it("does not make the Switchboard the run user of a publish it signs", async () => {
|
|
478
|
+
await module.client.execute(WORKFLOW, "main", [publish()]);
|
|
479
|
+
const hostIdentity = hostIdentityOf(reactorAccessOf(module));
|
|
480
|
+
expect(hostIdentity).toEqual({
|
|
481
|
+
address: HOST.address,
|
|
482
|
+
key: hostSigner.app.key,
|
|
483
|
+
});
|
|
484
|
+
await expect(
|
|
485
|
+
publishRunUser(module.client, WORKFLOW, hostIdentity),
|
|
486
|
+
).resolves.toBeNull();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("a run, as its run user", () => {
|
|
491
|
+
const WORKFLOW = "wf-ops";
|
|
492
|
+
// What the editor grants the Switchboard: global-scope execute.
|
|
493
|
+
const hostGrant = grant(
|
|
494
|
+
"switchboard",
|
|
495
|
+
{ address: HOST.address },
|
|
496
|
+
{ can: "execute", scope: "global" },
|
|
497
|
+
);
|
|
498
|
+
// And the document-scope operations of a create in a drive and a delete.
|
|
499
|
+
const hostDocumentGrant = grant(
|
|
500
|
+
"switchboard:document",
|
|
501
|
+
{ address: HOST.address },
|
|
502
|
+
{
|
|
503
|
+
can: "execute",
|
|
504
|
+
scope: "document",
|
|
505
|
+
operation: ["DELETE_DOCUMENT", "ADD_RELATIONSHIP", "REMOVE_RELATIONSHIP"],
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
const runUserGrant = grant(
|
|
509
|
+
"run-user",
|
|
510
|
+
{ address: PUBLISHER.address },
|
|
511
|
+
{ can: "execute" },
|
|
512
|
+
);
|
|
513
|
+
let round = 0;
|
|
514
|
+
|
|
515
|
+
beforeAll(async () => {
|
|
516
|
+
await publisher.create(
|
|
517
|
+
withSignaturePolicy(Workflow.utils.createDocument(), "legacy", {
|
|
518
|
+
id: WORKFLOW,
|
|
519
|
+
}),
|
|
520
|
+
);
|
|
521
|
+
await publisher.execute(WORKFLOW, "main", [
|
|
522
|
+
...["rename", "read", "delete"].map((actionName) =>
|
|
523
|
+
workflowActions.addStep({
|
|
524
|
+
id: actionName,
|
|
525
|
+
key: actionName,
|
|
526
|
+
name: actionName,
|
|
527
|
+
pieceName: PIECE,
|
|
528
|
+
pieceVersion: PIECE_VERSION,
|
|
529
|
+
actionName,
|
|
530
|
+
config: {},
|
|
531
|
+
reactorConnectionId: "conn-open",
|
|
532
|
+
}),
|
|
533
|
+
),
|
|
534
|
+
publish(),
|
|
535
|
+
]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
async function target(grants: Grant[]) {
|
|
539
|
+
const id = `ops-${++round}`;
|
|
540
|
+
await createDocument(documentModelDocumentModelModule as never, id, [
|
|
541
|
+
...grants,
|
|
542
|
+
// Lets the publisher set the document up; no one else may read it.
|
|
543
|
+
grant("owner", { address: PUBLISHER.address }, { can: "execute" }),
|
|
544
|
+
]);
|
|
545
|
+
return id;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function step(stepId: string, id: string) {
|
|
549
|
+
await publisher.execute(WORKFLOW, "main", [
|
|
550
|
+
workflowActions.updateStep({ id: stepId, config: { id } }),
|
|
551
|
+
]);
|
|
552
|
+
return (await mutations.testStep(
|
|
553
|
+
{},
|
|
554
|
+
{ workflowId: WORKFLOW, stepId },
|
|
555
|
+
ctxOf(PUBLISHER.address),
|
|
556
|
+
)) as StepResult;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function unreadable() {
|
|
560
|
+
const id = `ops-${++round}`;
|
|
561
|
+
await createDocument(documentModelDocumentModelModule as never, id, [
|
|
562
|
+
grant("host-only", { address: HOST.address }, { can: "execute" }),
|
|
563
|
+
]);
|
|
564
|
+
return id;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
it("lands a write the run user and the Switchboard may both make", async () => {
|
|
568
|
+
const id = await target([runUserGrant, hostGrant]);
|
|
569
|
+
|
|
570
|
+
expect(await step("rename", id)).toMatchObject({
|
|
571
|
+
status: "SUCCEEDED",
|
|
572
|
+
output: "Renamed",
|
|
573
|
+
});
|
|
574
|
+
}, 60_000);
|
|
575
|
+
|
|
576
|
+
it("refuses a write the document's policy denies the run user", async () => {
|
|
577
|
+
const id = await unreadable();
|
|
578
|
+
|
|
579
|
+
const result = await step("rename", id);
|
|
580
|
+
|
|
581
|
+
expect(result).toMatchObject({
|
|
582
|
+
status: "FAILED",
|
|
583
|
+
errorName: "ReactorAccessDeniedError",
|
|
584
|
+
});
|
|
585
|
+
expect(result.error).toContain("The run user may not apply SET_NAME");
|
|
586
|
+
expect((await module.client.get(id)).header.name).toBe("");
|
|
587
|
+
}, 60_000);
|
|
588
|
+
|
|
589
|
+
it("names the Switchboard's missing grant when admission refuses it", async () => {
|
|
590
|
+
const id = await target([runUserGrant]);
|
|
591
|
+
|
|
592
|
+
const result = await step("rename", id);
|
|
593
|
+
|
|
594
|
+
expect(result).toMatchObject({
|
|
595
|
+
status: "FAILED",
|
|
596
|
+
errorName: "ReactorAccessDeniedError",
|
|
597
|
+
});
|
|
598
|
+
expect(result.error).toContain(
|
|
599
|
+
"The Switchboard has no grant for this operation:",
|
|
600
|
+
);
|
|
601
|
+
expect(result.error).toContain(`${HOST.address} may not execute SET_NAME`);
|
|
602
|
+
}, 60_000);
|
|
603
|
+
|
|
604
|
+
it("reads as the run user", async () => {
|
|
605
|
+
const readable = await target([runUserGrant]);
|
|
606
|
+
const hidden = await unreadable();
|
|
607
|
+
|
|
608
|
+
expect(await step("read", readable)).toMatchObject({
|
|
609
|
+
status: "SUCCEEDED",
|
|
610
|
+
output: 1,
|
|
611
|
+
});
|
|
612
|
+
expect(await step("read", hidden)).toMatchObject({
|
|
613
|
+
status: "SUCCEEDED",
|
|
614
|
+
output: 0,
|
|
615
|
+
});
|
|
616
|
+
}, 60_000);
|
|
617
|
+
|
|
618
|
+
it("deletes only what the run user may delete", async () => {
|
|
619
|
+
const allowed = await target([runUserGrant, hostGrant, hostDocumentGrant]);
|
|
620
|
+
const denied = await unreadable();
|
|
621
|
+
|
|
622
|
+
const removed = await step("delete", allowed);
|
|
623
|
+
expect(removed.error).toBeUndefined();
|
|
624
|
+
expect(removed).toMatchObject({
|
|
625
|
+
status: "SUCCEEDED",
|
|
626
|
+
});
|
|
627
|
+
const deleted = await module.client.get(allowed).then(
|
|
628
|
+
(document) => document.state.document.isDeleted === true,
|
|
629
|
+
() => true,
|
|
630
|
+
);
|
|
631
|
+
expect(deleted).toBe(true);
|
|
632
|
+
const refused = await step("delete", denied);
|
|
633
|
+
expect(refused).toMatchObject({
|
|
634
|
+
status: "FAILED",
|
|
635
|
+
errorName: "ReactorAccessDeniedError",
|
|
636
|
+
});
|
|
637
|
+
expect(refused.error).toContain("DELETE_DOCUMENT");
|
|
638
|
+
}, 60_000);
|
|
639
|
+
|
|
640
|
+
it("refuses a run whose run user may not read its reactor connection", async () => {
|
|
641
|
+
const connection = "conn-private";
|
|
642
|
+
const workflowId = "wf-private";
|
|
643
|
+
await createDocument(Connection as never, connection, [
|
|
644
|
+
grant("host-only", { address: HOST.address }, { can: "execute" }),
|
|
645
|
+
]);
|
|
646
|
+
await module.client.execute(connection, "main", [
|
|
647
|
+
connectionActions.setConnector({
|
|
648
|
+
connectorId: REACTOR_CONNECTOR_ID,
|
|
649
|
+
authType: "REACTOR",
|
|
650
|
+
}),
|
|
651
|
+
connectionActions.setConfig({ config: { endpoint: "local" } }),
|
|
652
|
+
]);
|
|
653
|
+
await createDocument(Workflow as never, workflowId, [
|
|
654
|
+
grant("publisher", { address: PUBLISHER.address }, { can: "execute" }),
|
|
655
|
+
hostGrant,
|
|
656
|
+
]);
|
|
657
|
+
await publisher.execute(workflowId, "main", [
|
|
658
|
+
workflowActions.setTrigger({
|
|
659
|
+
id: "t1",
|
|
660
|
+
pieceName: CORE_PIECE_NAME,
|
|
661
|
+
pieceVersion: CORE_PIECE_VERSION,
|
|
662
|
+
triggerName: "manual",
|
|
663
|
+
config: {},
|
|
664
|
+
}),
|
|
665
|
+
workflowActions.addStep({
|
|
666
|
+
id: "read",
|
|
667
|
+
key: "read",
|
|
668
|
+
name: "read",
|
|
669
|
+
pieceName: PIECE,
|
|
670
|
+
pieceVersion: PIECE_VERSION,
|
|
671
|
+
actionName: "read",
|
|
672
|
+
config: { id: connection },
|
|
673
|
+
reactorConnectionId: connection,
|
|
674
|
+
}),
|
|
675
|
+
workflowActions.addEdge({
|
|
676
|
+
id: "e1",
|
|
677
|
+
from: "t1",
|
|
678
|
+
to: "read",
|
|
679
|
+
port: "next",
|
|
680
|
+
}),
|
|
681
|
+
publish(),
|
|
682
|
+
workflowActions.setWorkflowStatus({ status: "ENABLED" }),
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
await expect(
|
|
686
|
+
mutations.fire({}, { workflowId }, ctxOf(PUBLISHER.address)),
|
|
687
|
+
).rejects.toThrow(
|
|
688
|
+
`${PUBLISHER.address} cannot read reactor connection "${connection}"`,
|
|
689
|
+
);
|
|
690
|
+
}, 60_000);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const HOST_SIGNED = /published unsigned or by the Switchboard itself/;
|
|
694
|
+
|
|
695
|
+
// Published by the publisher, then again through the host's own client, as
|
|
696
|
+
// mutateDocument signs an action sent unsigned.
|
|
697
|
+
async function hostSignedWorkflow(
|
|
698
|
+
over: Harness,
|
|
699
|
+
workflowId: string,
|
|
700
|
+
target: string,
|
|
701
|
+
) {
|
|
702
|
+
await createDocument(
|
|
703
|
+
Workflow as unknown as DocumentModelModule,
|
|
704
|
+
workflowId,
|
|
705
|
+
[
|
|
706
|
+
grant("publisher", { address: PUBLISHER.address }, { can: "execute" }),
|
|
707
|
+
grant(
|
|
708
|
+
"switchboard",
|
|
709
|
+
{ address: HOST.address },
|
|
710
|
+
{ can: "execute", scope: "global" },
|
|
711
|
+
),
|
|
712
|
+
],
|
|
713
|
+
over.publisher,
|
|
714
|
+
);
|
|
715
|
+
await over.publisher.execute(workflowId, "main", [
|
|
716
|
+
workflowActions.setTrigger({
|
|
717
|
+
id: "t1",
|
|
718
|
+
pieceName: CORE_PIECE_NAME,
|
|
719
|
+
pieceVersion: CORE_PIECE_VERSION,
|
|
720
|
+
triggerName: "manual",
|
|
721
|
+
config: {},
|
|
722
|
+
}),
|
|
723
|
+
workflowActions.addStep({
|
|
724
|
+
id: "rename",
|
|
725
|
+
key: "rename",
|
|
726
|
+
name: "rename",
|
|
727
|
+
pieceName: PIECE,
|
|
728
|
+
pieceVersion: PIECE_VERSION,
|
|
729
|
+
actionName: "rename",
|
|
730
|
+
config: { id: target },
|
|
731
|
+
reactorConnectionId: "conn-open",
|
|
732
|
+
}),
|
|
733
|
+
workflowActions.addEdge({
|
|
734
|
+
id: "e1",
|
|
735
|
+
from: "t1",
|
|
736
|
+
to: "rename",
|
|
737
|
+
port: "next",
|
|
738
|
+
}),
|
|
739
|
+
publish(),
|
|
740
|
+
workflowActions.setWorkflowStatus({ status: "ENABLED" }),
|
|
741
|
+
]);
|
|
742
|
+
await over.module.client.execute(workflowId, "main", [publish()]);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const denialOf = (over: Harness, workflowId: string) =>
|
|
746
|
+
over.queries.reactorAccessDenial(
|
|
747
|
+
{},
|
|
748
|
+
{ workflowId },
|
|
749
|
+
ctxOf(PUBLISHER.address),
|
|
750
|
+
) as Promise<string | null>;
|
|
751
|
+
|
|
752
|
+
describe("a publish the Switchboard signs, auth enforcement on", () => {
|
|
753
|
+
const WORKFLOW = "wf-host-signed-on";
|
|
754
|
+
|
|
755
|
+
it("gets no reactor access", async () => {
|
|
756
|
+
const on = { module, publisher, composed, queries, mutations };
|
|
757
|
+
await hostSignedWorkflow(on, WORKFLOW, "conn-open");
|
|
758
|
+
|
|
759
|
+
await vi.waitFor(async () =>
|
|
760
|
+
expect(await denialOf(on, WORKFLOW)).toMatch(HOST_SIGNED),
|
|
761
|
+
);
|
|
762
|
+
await expect(
|
|
763
|
+
mutations.fire({}, { workflowId: WORKFLOW }, ctxOf(PUBLISHER.address)),
|
|
764
|
+
).rejects.toThrow(HOST_SIGNED);
|
|
765
|
+
}, 60_000);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
describe("a publish the Switchboard signs, auth enforcement off", () => {
|
|
769
|
+
const WORKFLOW = "wf-host-signed-off";
|
|
770
|
+
const TARGET = "off-target";
|
|
771
|
+
let off: Harness;
|
|
772
|
+
|
|
773
|
+
beforeAll(async () => {
|
|
774
|
+
off = await harness(false);
|
|
775
|
+
}, 60_000);
|
|
776
|
+
|
|
777
|
+
afterAll(async () => {
|
|
778
|
+
await off.composed.stop();
|
|
779
|
+
off.module.reactor.kill();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("runs with no run user, reading and writing", async () => {
|
|
783
|
+
await createDocument(
|
|
784
|
+
documentModelDocumentModelModule as never,
|
|
785
|
+
TARGET,
|
|
786
|
+
[grant("switchboard", { address: HOST.address }, { can: "execute" })],
|
|
787
|
+
off.publisher,
|
|
788
|
+
);
|
|
789
|
+
await hostSignedWorkflow(off, WORKFLOW, TARGET);
|
|
790
|
+
|
|
791
|
+
await expect(
|
|
792
|
+
publishRunUser(
|
|
793
|
+
off.module.client,
|
|
794
|
+
WORKFLOW,
|
|
795
|
+
hostIdentityOf(reactorAccessOf(off.module)),
|
|
796
|
+
),
|
|
797
|
+
).resolves.toBeNull();
|
|
798
|
+
expect(await denialOf(off, WORKFLOW)).toBeNull();
|
|
799
|
+
const result = (await off.mutations.fire(
|
|
800
|
+
{},
|
|
801
|
+
{ workflowId: WORKFLOW },
|
|
802
|
+
ctxOf(PUBLISHER.address),
|
|
803
|
+
)) as { status: string; error?: string };
|
|
804
|
+
expect(result.error).toBeUndefined();
|
|
805
|
+
expect(result.status).toBe("SUCCEEDED");
|
|
806
|
+
expect((await off.module.client.get(TARGET)).header.name).toBe("Renamed");
|
|
807
|
+
}, 60_000);
|
|
808
|
+
});
|