@fungi.computer/oyster 0.2.0 → 0.3.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/README.md +376 -72
- package/dist/agents.d.ts +3 -2
- package/dist/agents.js +4 -2
- package/dist/app-builds.d.ts +81 -8
- package/dist/app-builds.js +210 -60
- package/dist/app-storage.d.ts +3 -3
- package/dist/approval-capability.d.ts +5 -0
- package/dist/approval-capability.js +46 -0
- package/dist/approval-home.d.ts +51 -0
- package/dist/approval-home.js +226 -0
- package/dist/apps/catalog.d.ts +335 -0
- package/dist/apps/catalog.js +46 -0
- package/dist/apps/gate.d.ts +20 -6
- package/dist/apps/gate.js +42 -16
- package/dist/apps/index.d.ts +3 -1
- package/dist/apps/index.js +3 -1
- package/dist/apps/installation.d.ts +132 -51
- package/dist/apps/installation.js +176 -70
- package/dist/apps/permissions.d.ts +247 -0
- package/dist/apps/permissions.js +255 -0
- package/dist/boot-capability-route.d.ts +15 -0
- package/dist/boot-capability-route.js +196 -0
- package/dist/boot-capability.d.ts +103 -0
- package/dist/boot-capability.js +141 -0
- package/dist/boot-construction.d.ts +12 -0
- package/dist/boot-construction.js +175 -0
- package/dist/boot-gate.d.ts +267 -0
- package/dist/boot-gate.js +236 -0
- package/dist/boot-host-script.d.ts +2 -0
- package/dist/boot-host-script.js +1 -0
- package/dist/boot-mount.d.ts +21 -0
- package/dist/boot-mount.js +65 -0
- package/dist/browser/boot.d.ts +73 -0
- package/dist/browser/boot.js +502 -0
- package/dist/browser/browser.d.ts +104 -0
- package/dist/browser/browser.js +374 -0
- package/dist/browser/capability-source.d.ts +76 -0
- package/dist/browser/capability-source.js +338 -0
- package/dist/browser/cdp-client.d.ts +56 -0
- package/dist/browser/cdp-client.js +184 -0
- package/dist/browser/errors.d.ts +79 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/index.d.ts +16 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/lease-registry.d.ts +61 -0
- package/dist/browser/lease-registry.js +101 -0
- package/dist/browser/operations.d.ts +225 -0
- package/dist/browser/operations.js +137 -0
- package/dist/browser/page.d.ts +25 -0
- package/dist/browser/page.js +172 -0
- package/dist/browser/quick-reads.d.ts +29 -0
- package/dist/browser/quick-reads.js +160 -0
- package/dist/browser/sessions.d.ts +52 -0
- package/dist/browser/sessions.js +118 -0
- package/dist/browser/url-policy.d.ts +18 -0
- package/dist/browser/url-policy.js +58 -0
- package/dist/build/app-imports.d.ts +12 -0
- package/dist/build/app-imports.js +105 -0
- package/dist/build/build-policy.d.ts +5 -3
- package/dist/build/build-policy.js +5 -3
- package/dist/build/candidate-builder.d.ts +1 -1
- package/dist/build/candidate-cli.mjs +8375 -334
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +28 -6
- package/dist/build/snapshot-package-store.d.ts +1 -1
- package/dist/build/snapshot-package-store.js +7 -2
- package/dist/build/workspace-build.d.ts +92 -5
- package/dist/build/workspace-build.js +93 -19
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- package/dist/catalog-frame.js +1 -1
- package/dist/cloudflare.d.ts +13 -3
- package/dist/cloudflare.js +102 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.js +4 -2
- package/dist/git-auth.d.ts +0 -2
- package/dist/git-auth.js +0 -2
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -2
- package/dist/host.d.ts +39 -13
- package/dist/host.js +268 -142
- package/dist/index.d.ts +25 -15
- package/dist/index.js +39 -16
- package/dist/install-release-cli.d.ts +2 -0
- package/dist/install-release-cli.js +46 -0
- package/dist/installation-client.d.ts +43 -0
- package/dist/installation-client.js +178 -0
- package/dist/installation-execution.d.ts +11 -0
- package/dist/installation-execution.js +184 -0
- package/dist/installation-operations.d.ts +1437 -0
- package/dist/installation-operations.js +664 -0
- package/dist/installation-store-generation.d.ts +3 -0
- package/dist/installation-store-generation.js +16 -0
- package/dist/instance-id.d.ts +10 -0
- package/dist/instance-id.js +10 -0
- package/dist/instance-refusal.d.ts +11 -0
- package/dist/instance-refusal.js +21 -0
- package/dist/internal/delivery.js +4 -2
- package/dist/internal/host-gate.js +7 -1
- package/dist/internal/registry-installations.js +4 -4
- package/dist/keys.d.ts +5 -1
- package/dist/keys.js +10 -2
- package/dist/machine-link/authority.d.ts +89 -0
- package/dist/machine-link/authority.js +37 -0
- package/dist/machine-link/client.d.ts +66 -0
- package/dist/machine-link/client.js +94 -0
- package/dist/machine-link/delivery.d.ts +18 -0
- package/dist/machine-link/delivery.js +55 -0
- package/dist/machine-link/enrollment.d.ts +111 -0
- package/dist/machine-link/enrollment.js +286 -0
- package/dist/machine-link/fences.d.ts +37 -0
- package/dist/machine-link/fences.js +166 -0
- package/dist/machine-link/home.d.ts +20 -0
- package/dist/machine-link/home.js +24 -0
- package/dist/machine-link/host.d.ts +43 -0
- package/dist/machine-link/host.js +189 -0
- package/dist/machine-link/index.d.ts +15 -0
- package/dist/machine-link/index.js +15 -0
- package/dist/machine-link/link.d.ts +48 -0
- package/dist/machine-link/link.js +223 -0
- package/dist/machine-link/presence.d.ts +10 -0
- package/dist/machine-link/presence.js +25 -0
- package/dist/machine-link/workers.d.ts +6 -0
- package/dist/machine-link/workers.js +6 -0
- package/dist/mcp-owner.d.ts +2 -2
- package/dist/mcp-owner.js +41 -33
- package/dist/mcp.d.ts +15 -3
- package/dist/mcp.js +146 -25
- package/dist/pack.d.ts +14 -3
- package/dist/pack.js +44 -19
- package/dist/page/app-access.d.ts +7 -0
- package/dist/page/app-access.js +75 -0
- package/dist/page/boot-host.js +57 -0
- package/dist/page/capabilities.d.ts +3 -3
- package/dist/page/capabilities.js +16 -12
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page/computer-target.d.ts +3 -0
- package/dist/page/computer-target.js +9 -0
- package/dist/page/mount.d.ts +23 -3
- package/dist/page/mount.js +152 -16
- package/dist/page-assets.d.ts +22 -0
- package/dist/page-assets.js +28 -0
- package/dist/page-script.d.ts +3 -0
- package/dist/page-script.js +5 -4
- package/dist/release.d.ts +13 -6
- package/dist/release.js +10 -7
- package/dist/root-data.d.ts +16 -6
- package/dist/root-data.js +2 -2
- package/dist/source/index.d.ts +1 -1
- package/dist/source/team-source.d.ts +20 -8
- package/dist/source/team-source.js +39 -10
- package/dist/source/template-seed.d.ts +2 -2
- package/dist/standalone/agent-mail.d.ts +24 -0
- package/dist/standalone/agent-mail.js +62 -0
- package/dist/standalone/agent-store.d.ts +9 -0
- package/dist/standalone/agent-store.js +30 -0
- package/dist/standalone/agents.d.ts +68 -9
- package/dist/standalone/agents.js +428 -56
- package/dist/standalone/app-builds-binding.d.ts +20 -2
- package/dist/standalone/app-builds-binding.js +147 -23
- package/dist/standalone/app-set.d.ts +5 -2
- package/dist/standalone/app-set.js +174 -143
- package/dist/standalone/browser.d.ts +11 -0
- package/dist/standalone/browser.js +72 -0
- package/dist/standalone/builds.d.ts +13 -4
- package/dist/standalone/builds.js +28 -44
- package/dist/standalone/computer-egress.d.ts +2 -0
- package/dist/standalone/computer-egress.js +13 -4
- package/dist/standalone/computer-fleet.d.ts +43 -0
- package/dist/standalone/computer-fleet.js +200 -0
- package/dist/standalone/computer.d.ts +44 -9
- package/dist/standalone/computer.js +129 -22
- package/dist/standalone/install-apps-cli.js +43 -6
- package/dist/standalone/instance.d.ts +2 -2
- package/dist/standalone/instance.js +8 -11
- package/dist/standalone/machine-link.d.ts +354 -0
- package/dist/standalone/machine-link.js +368 -0
- package/dist/standalone/media.d.ts +2 -2
- package/dist/standalone/media.js +19 -16
- package/dist/standalone/preview.d.ts +2 -2
- package/dist/standalone/preview.js +37 -10
- package/dist/standalone/registry.d.ts +149 -9
- package/dist/standalone/registry.js +299 -8
- package/dist/standalone/worker.d.ts +19 -5
- package/dist/standalone/worker.js +90 -54
- package/dist/standalone/workflow-binding.d.ts +18 -22
- package/dist/standalone/workflow-binding.js +18 -18
- package/dist/standalone/workflow-control.d.ts +4 -0
- package/dist/standalone/workflow-control.js +40 -0
- package/dist/standalone/workflow-events.d.ts +2 -2
- package/dist/standalone/workflow-events.js +9 -10
- package/dist/standalone/workflow-git-capture.js +4 -3
- package/dist/standalone/workflow-host.d.ts +162 -161
- package/dist/standalone/workflow-host.js +101 -21
- package/dist/standalone/workflow-node-capture.js +11 -4
- package/dist/standalone/workflow-schedules.d.ts +5 -2
- package/dist/standalone/workflow-schedules.js +9 -10
- package/dist/standalone/workflow-trigger-host.d.ts +65 -5
- package/dist/standalone/workflow-trigger-host.js +2 -2
- package/dist/standalone/workflow-webhooks.d.ts +22 -0
- package/dist/standalone/workflow-webhooks.js +39 -0
- package/dist/standalone/workflows.d.ts +140 -43
- package/dist/standalone/workflows.js +198 -39
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.js +14 -3
- package/dist/testing/installation.js +26 -2
- package/dist/testing/worker.js +1 -1
- package/dist/testing/workers-runtime.d.ts +8 -2
- package/dist/testing/workers-runtime.js +20 -2
- package/dist/webhook-abuse.d.ts +25 -0
- package/dist/webhook-abuse.js +54 -0
- package/dist/webhook-custody.d.ts +144 -0
- package/dist/webhook-custody.js +222 -0
- package/dist/webhook-ingress.d.ts +36 -0
- package/dist/webhook-ingress.js +146 -0
- package/dist/webhooks.d.ts +111 -0
- package/dist/webhooks.js +189 -0
- package/dist/workers-git.d.ts +12 -17
- package/dist/workers-git.js +60 -52
- package/dist/workers.d.ts +224 -13
- package/dist/workers.js +351 -51
- package/dist/workflow-agent-authority.d.ts +8 -0
- package/dist/workflow-agent-authority.js +42 -0
- package/dist/workflows.d.ts +66 -0
- package/dist/workflows.js +97 -0
- package/docs/api/README.md +63 -37
- package/docs/api/agents.md +17 -0
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +1394 -543
- package/docs/api/browser/boot.md +132 -0
- package/docs/api/browser.md +1440 -0
- package/docs/api/build-contract.md +38 -25
- package/docs/api/build.md +263 -84
- package/docs/api/cloudflare.md +93 -7
- package/docs/api/git.md +3 -13
- package/docs/api/host.md +944 -118
- package/docs/api/index.md +320 -18
- package/docs/api/keys.md +24 -1
- package/docs/api/mcp.md +59 -12
- package/docs/api/modules.md +33 -40
- package/docs/api/pack.md +57 -8
- package/docs/api/page/mount.md +92 -4
- package/docs/api/workers-git.md +103 -24
- package/docs/api/workers.md +1581 -244
- package/docs/site/agent-quickstart.mdx +28 -14
- package/examples/standalone/worker.ts +3 -1
- package/examples/standalone/wrangler.jsonc +2 -0
- package/package.json +73 -71
- package/dist/installation-consent.d.ts +0 -81
- package/dist/installation-consent.js +0 -84
- package/dist/shell/app-directory.d.ts +0 -542
- package/dist/shell/app-directory.js +0 -196
- package/dist/shell/browser-actions.d.ts +0 -7
- package/dist/shell/browser-actions.js +0 -56
- package/dist/shell/command-host.d.ts +0 -112
- package/dist/shell/command-host.js +0 -502
- package/dist/shell/commands.d.ts +0 -25
- package/dist/shell/commands.js +0 -78
- package/dist/shell/index.d.ts +0 -8
- package/dist/shell/index.js +0 -8
- package/dist/shell/keys.d.ts +0 -41
- package/dist/shell/keys.js +0 -134
- package/dist/shell/shell-module.d.ts +0 -55
- package/dist/shell/shell-module.js +0 -86
- package/dist/shell/window-manager.d.ts +0 -128
- package/dist/shell/window-manager.js +0 -730
- package/dist/shell/workbench-contract.d.ts +0 -68
- package/dist/shell/workbench-directory.d.ts +0 -332
- package/dist/shell/workbench-directory.js +0 -35
- package/dist/shell/workbench-host-types.d.ts +0 -96
- package/dist/shell/workbench-host-types.js +0 -1
- package/dist/shell/workbench-host.d.ts +0 -34
- package/dist/shell/workbench-host.js +0 -352
- package/dist/shell/workbench-model.d.ts +0 -194
- package/dist/shell/workbench-model.js +0 -53
- package/dist/shell/workbench.d.ts +0 -4
- package/dist/shell/workbench.js +0 -2
- package/docs/api/shell/app-directory.md +0 -599
- package/docs/api/shell/command-host.md +0 -224
- package/docs/api/shell/commands.md +0 -197
- package/docs/api/shell/keys.md +0 -183
- package/docs/api/shell/shell-module.md +0 -204
- package/docs/api/shell/window-manager.md +0 -267
- package/docs/api/shell/workbench-directory.md +0 -119
- package/docs/api/shell/workbench.md +0 -205
- package/docs/api/shell.md +0 -1915
- /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
|
@@ -10,13 +10,16 @@ import { createStandaloneWorkflowSchedules, } from "./workflow-schedules.js";
|
|
|
10
10
|
import { createStandaloneWorkflowEvents, } from "./workflow-events.js";
|
|
11
11
|
import { createStandaloneWorkflowTriggerHost, standaloneTriggerErrors, } from "./workflow-trigger-host.js";
|
|
12
12
|
import { createWorkflowPublishedDefinitionPort, readPublishedWorkflow, } from "@fungi.computer/workflows-wire";
|
|
13
|
+
import { workflowAgentAuthority } from "../workflow-agent-authority.js";
|
|
13
14
|
import { defaultTenant, tenantScope } from "../tenant.js";
|
|
14
15
|
import { AppInstanceError } from "../index.js";
|
|
15
|
-
import { workflowGitPassPrefix } from "../git.js";
|
|
16
16
|
import { createWorkflowGitPasses, createWorkflowGitPassStore, workflowGitPassWriter, workflowRunReport, wireRunInitiator, } from "@fungi.computer/braid/runtime";
|
|
17
17
|
import { canonicalJson } from "@fungi.computer/braid";
|
|
18
18
|
import { runKey, reviewKey, workflowGitPassRequestSchema, } from "./workflow-binding.js";
|
|
19
19
|
import { agentDirectoryForScope } from "./agent-directory.js";
|
|
20
|
+
import { createWorkflowWebhooks, workflowWebhookCommandSchema, workflowWebhookControlResponse, } from "../webhooks.js";
|
|
21
|
+
import { workflowWebhookRefusal } from "../webhook-ingress.js";
|
|
22
|
+
import { Hono } from "hono";
|
|
20
23
|
/** SQLite and the native Workflow binding belong to this standalone host; its root supplies `hostFor`. */
|
|
21
24
|
export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
22
25
|
composed;
|
|
@@ -37,7 +40,7 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
37
40
|
) STRICT`);
|
|
38
41
|
this.composed = ctx.blockConcurrencyWhile(async () => {
|
|
39
42
|
const shared = await standaloneTenancy(env);
|
|
40
|
-
const host = this.hostFor(tenantScope(defaultTenant, env.OYSTER_ORIGIN));
|
|
43
|
+
const host = await this.hostFor(tenantScope(defaultTenant, env.OYSTER_ORIGIN));
|
|
41
44
|
const agentToolPolicy = async (agentId) => (await host.appToolPolicy?.({ kind: "agent", agentId })) ?? null;
|
|
42
45
|
// A key's App access is its own grants (Ear Tag), read through
|
|
43
46
|
// `host.keys.live`, never through `appToolPolicy`.
|
|
@@ -45,6 +48,15 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
45
48
|
// The one owner reaches every Agent; the report needs one that exists.
|
|
46
49
|
const admitDelivery = (_memberId, delivery) => agentDirectoryForScope(env, tenantScope(defaultTenant, env.OYSTER_ORIGIN)).has(delivery.agentId);
|
|
47
50
|
const owner = createStandaloneWorkflowHost(ctx.storage, host, {
|
|
51
|
+
async lifecycleMember() {
|
|
52
|
+
const member = await shared.owner();
|
|
53
|
+
return member === null
|
|
54
|
+
? null
|
|
55
|
+
: {
|
|
56
|
+
memberId: member.actor.memberId,
|
|
57
|
+
membershipVersion: member.membershipVersion,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
48
60
|
authority(libraryInstallationId) {
|
|
49
61
|
return createStandaloneWorkflowRootAuthority({
|
|
50
62
|
shared,
|
|
@@ -80,6 +92,7 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
80
92
|
sql: ctx.storage.sql,
|
|
81
93
|
transactionSync: (body) => ctx.storage.transactionSync(body),
|
|
82
94
|
teamId: () => libraryId,
|
|
95
|
+
now: () => this.scheduleClock.now(),
|
|
83
96
|
},
|
|
84
97
|
triggers: {
|
|
85
98
|
published: createWorkflowPublishedDefinitionPort((git) => ({
|
|
@@ -93,10 +106,20 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
93
106
|
memberId,
|
|
94
107
|
});
|
|
95
108
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
readAgentAccess: async (lookup) => {
|
|
110
|
+
if (lookup.kind !== "policy")
|
|
111
|
+
return null;
|
|
112
|
+
const policy = await agentToolPolicy(lookup.agentId);
|
|
113
|
+
return policy === null
|
|
114
|
+
? null
|
|
115
|
+
: {
|
|
116
|
+
kind: "policy",
|
|
117
|
+
agentId: lookup.agentId,
|
|
118
|
+
policyRevision: policy.revision,
|
|
119
|
+
grantorMemberId: policy.grantedBy,
|
|
120
|
+
level: policy.level,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
100
123
|
// The member's current Workflows Git policy; mints no pass itself.
|
|
101
124
|
admitGitAccess: async (memberId, repository, scope) => {
|
|
102
125
|
if (!(await gitAccess(libraryId)(memberId, repository, scope)))
|
|
@@ -122,20 +145,71 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
122
145
|
const admitted = await owner.admitInvocation(facts, operation);
|
|
123
146
|
if (admitted.libraryId !== libraryId)
|
|
124
147
|
throw new AppInstanceError("revoked");
|
|
125
|
-
return
|
|
148
|
+
return {
|
|
149
|
+
...admitted,
|
|
150
|
+
authority: await workflowAgentAuthority(facts, operation, agentToolPolicy),
|
|
151
|
+
};
|
|
126
152
|
},
|
|
127
153
|
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
let webhooks = null;
|
|
155
|
+
const wake = async () => {
|
|
156
|
+
const due = [
|
|
157
|
+
schedules.nextWake(),
|
|
158
|
+
webhooks?.nextWake() ?? null,
|
|
159
|
+
].filter((value) => value !== null);
|
|
160
|
+
if (due.length === 0)
|
|
161
|
+
await ctx.storage.deleteAlarm();
|
|
162
|
+
else
|
|
163
|
+
await ctx.storage.setAlarm(this.scheduleClock.alarmAt(Math.min(...due)));
|
|
164
|
+
};
|
|
165
|
+
const schedules = createStandaloneWorkflowSchedules({
|
|
166
|
+
...controls,
|
|
167
|
+
now: () => this.scheduleClock.now(),
|
|
168
|
+
wake,
|
|
169
|
+
reportFailure: ({ source, cause }) => {
|
|
170
|
+
console.error("Standalone Workflow schedule failed", source, cause);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (env.WORKFLOW_WEBHOOK_HOST_RATE !== undefined &&
|
|
174
|
+
env.WORKFLOW_WEBHOOK_BINDING_RATE !== undefined &&
|
|
175
|
+
env.WORKFLOW_WEBHOOK_POLICY !== undefined)
|
|
176
|
+
webhooks = createWorkflowWebhooks({
|
|
177
|
+
sql: ctx.storage.sql,
|
|
178
|
+
transactionSync: (body) => ctx.storage.transactionSync(body),
|
|
131
179
|
now: () => this.scheduleClock.now(),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
180
|
+
scope: { host: env.OYSTER_ORIGIN, tenant: libraryId },
|
|
181
|
+
owner: async (principal) => {
|
|
182
|
+
const current = await shared.owner();
|
|
183
|
+
return principal.actor.kind === "member" &&
|
|
184
|
+
current?.actor.memberId === principal.actor.memberId
|
|
185
|
+
? principal.actor.memberId
|
|
186
|
+
: null;
|
|
187
|
+
},
|
|
188
|
+
runs: triggers.runs,
|
|
189
|
+
triggers: triggers.triggers,
|
|
190
|
+
launch: async (run) => {
|
|
191
|
+
const stored = triggers.runs.graph.read(run.runId);
|
|
192
|
+
if (stored === null)
|
|
193
|
+
throw new Error("Reserved Workflow run is missing");
|
|
194
|
+
// Recover the existing run's dispatch context after a crash between
|
|
195
|
+
// durable admission and the standalone adapter's caller write.
|
|
196
|
+
owner.recordTriggerCaller(run.runId, stored.actorId, stored.computerId);
|
|
197
|
+
await this.launch(libraryId, run);
|
|
198
|
+
},
|
|
199
|
+
abuse: {
|
|
200
|
+
hostRate: env.WORKFLOW_WEBHOOK_HOST_RATE,
|
|
201
|
+
bindingRate: env.WORKFLOW_WEBHOOK_BINDING_RATE,
|
|
202
|
+
concurrency: env.WORKFLOW_WEBHOOK_POLICY.concurrency,
|
|
203
|
+
},
|
|
204
|
+
limits: env.WORKFLOW_WEBHOOK_POLICY.limits,
|
|
205
|
+
wake,
|
|
206
|
+
reportFailure: () => {
|
|
207
|
+
console.error("Standalone Workflow webhook failed");
|
|
137
208
|
},
|
|
138
|
-
})
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
schedules,
|
|
212
|
+
webhooks,
|
|
139
213
|
events: createStandaloneWorkflowEvents(controls),
|
|
140
214
|
};
|
|
141
215
|
};
|
|
@@ -151,13 +225,12 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
151
225
|
transactionSync: (body) => ctx.storage.transactionSync(body),
|
|
152
226
|
ownerId: () => this.libraryId(),
|
|
153
227
|
computerId: () => computerId,
|
|
154
|
-
|
|
155
|
-
remote: (_passId, repository) => {
|
|
228
|
+
remote: (repository) => {
|
|
156
229
|
const libraryId = this.libraryId();
|
|
157
230
|
if (repository.kind !== "installation" || libraryId === null)
|
|
158
231
|
throw new AppInstanceError("revoked");
|
|
159
|
-
// The tenant's ordinary smart-HTTP door; the
|
|
160
|
-
// URL and travels only as the bearer header.
|
|
232
|
+
// The tenant's ordinary smart-HTTP door; the credential never
|
|
233
|
+
// enters the URL and travels only as the bearer header.
|
|
161
234
|
return new URL(`/git/${repository.installationId}/${repository.repository}.git`, env.OYSTER_ORIGIN).href;
|
|
162
235
|
},
|
|
163
236
|
denied: () => new AppInstanceError("revoked"),
|
|
@@ -184,13 +257,6 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
184
257
|
if (!(await access(memberId, repository, scope)))
|
|
185
258
|
throw new AppInstanceError("revoked");
|
|
186
259
|
},
|
|
187
|
-
open: (_computer, grant) => {
|
|
188
|
-
const pass = passStore.openWorkflowPass(grant);
|
|
189
|
-
return Promise.resolve({
|
|
190
|
-
...pass,
|
|
191
|
-
bearer: `${workflowGitPassPrefix}${libraryId}.${pass.passId}`,
|
|
192
|
-
});
|
|
193
|
-
},
|
|
194
260
|
revoke: (_computer, input) => {
|
|
195
261
|
passStore.revokeWorkflowRunGitPasses(input);
|
|
196
262
|
return Promise.resolve();
|
|
@@ -200,8 +266,20 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
200
266
|
acknowledge: () => undefined,
|
|
201
267
|
});
|
|
202
268
|
};
|
|
203
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
owner,
|
|
271
|
+
libraryTriggers,
|
|
272
|
+
passStore,
|
|
273
|
+
gitAccess,
|
|
274
|
+
gitPasses,
|
|
275
|
+
computerId,
|
|
276
|
+
};
|
|
204
277
|
});
|
|
278
|
+
ctx.waitUntil(this.composed.then(async ({ libraryTriggers }) => {
|
|
279
|
+
const libraryId = this.libraryId();
|
|
280
|
+
if (libraryId !== null)
|
|
281
|
+
await libraryTriggers(libraryId).webhooks?.recover();
|
|
282
|
+
}));
|
|
205
283
|
this.owner = this.composed.then(({ owner }) => owner);
|
|
206
284
|
}
|
|
207
285
|
/** Record the library this object serves; another name is a revocation. */
|
|
@@ -213,11 +291,12 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
213
291
|
this.ctx.storage.sql.exec("INSERT INTO standalone_workflow_library(singleton,installation_id) VALUES(1,?)", libraryId);
|
|
214
292
|
}
|
|
215
293
|
/**
|
|
216
|
-
*
|
|
217
|
-
* Null when this library dispatched no such step; a step that
|
|
218
|
-
* the frame's attested Workflow, member and Computer is a
|
|
294
|
+
* Admit the Git step a node frame is executing: its public grant, never a
|
|
295
|
+
* credential. Null when this library dispatched no such step; a step that
|
|
296
|
+
* does not match the frame's attested Workflow, member and Computer is a
|
|
297
|
+
* revocation. Only NEW native admission then issues, through issueWorkflowGit.
|
|
219
298
|
*/
|
|
220
|
-
async
|
|
299
|
+
async admitWorkflowGit(raw) {
|
|
221
300
|
const request = workflowGitPassRequestSchema.parse(raw);
|
|
222
301
|
const { owner, gitPasses } = await this.composed;
|
|
223
302
|
const libraryId = this.libraryId();
|
|
@@ -234,18 +313,37 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
234
313
|
canonicalJson(node.context.armedBy) !==
|
|
235
314
|
canonicalJson(request.workflow.armedBy))
|
|
236
315
|
throw new AppInstanceError("revoked");
|
|
237
|
-
return gitPasses(libraryId).
|
|
316
|
+
return gitPasses(libraryId).admitWorkflowGit(node.context, {
|
|
238
317
|
installationId: node.installationId,
|
|
239
318
|
method: node.method,
|
|
240
319
|
});
|
|
241
320
|
}
|
|
242
|
-
/**
|
|
243
|
-
async
|
|
244
|
-
const {
|
|
321
|
+
/** One fresh credential for a NEW native invocation of an admitted step. */
|
|
322
|
+
async issueWorkflowGit(invocationId, grant) {
|
|
323
|
+
const { passStore } = await this.composed;
|
|
324
|
+
return await passStore.issue(invocationId, grant);
|
|
325
|
+
}
|
|
326
|
+
/** A live native replay extends its lease; it never issues another. */
|
|
327
|
+
async renewWorkflowGit(invocationId, grant) {
|
|
328
|
+
const { passStore } = await this.composed;
|
|
329
|
+
passStore.renew(invocationId, grant);
|
|
330
|
+
}
|
|
331
|
+
/** Close an invocation's credential, even before its issue arrives. */
|
|
332
|
+
async closeWorkflowGit(invocationId, grant) {
|
|
333
|
+
const { passStore } = await this.composed;
|
|
334
|
+
passStore.close(invocationId, grant);
|
|
335
|
+
}
|
|
336
|
+
/** The tenant Git door's per-request admission of a presented bearer. */
|
|
337
|
+
async admitWorkflowGitPass(bearer, repository, scope) {
|
|
338
|
+
const { owner, passStore, gitAccess, computerId } = await this.composed;
|
|
245
339
|
const libraryId = this.libraryId();
|
|
246
|
-
const
|
|
247
|
-
if (libraryId === null ||
|
|
248
|
-
|
|
340
|
+
const admitted = await passStore.admit({ bearer, repository, scope });
|
|
341
|
+
if (libraryId === null || admitted === null || computerId === undefined)
|
|
342
|
+
return null;
|
|
343
|
+
const { grant, invocationId } = admitted;
|
|
344
|
+
if (
|
|
345
|
+
// The selected Computer proves the credential's native command live.
|
|
346
|
+
!(await this.env.COMPUTER.getByName(computerId).executionIsLive(invocationId)) ||
|
|
249
347
|
owner.store.outcome(grant.runId) !== "active" ||
|
|
250
348
|
!(await gitAccess(libraryId)(grant.memberId, repository, scope)))
|
|
251
349
|
return null;
|
|
@@ -347,6 +445,42 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
347
445
|
if (libraryId === null)
|
|
348
446
|
return;
|
|
349
447
|
await libraryTriggers(libraryId).schedules.alarm();
|
|
448
|
+
await libraryTriggers(libraryId).webhooks?.recover();
|
|
449
|
+
}
|
|
450
|
+
/** Signed traffic uses the native fetch interface; no sender principal is accepted. */
|
|
451
|
+
async fetch(request) {
|
|
452
|
+
return new Hono()
|
|
453
|
+
.all("/webhooks/:bindingId", async (context) => {
|
|
454
|
+
const { libraryTriggers } = await this.composed;
|
|
455
|
+
const libraryId = this.libraryId();
|
|
456
|
+
const webhooks = libraryId === null ? null : libraryTriggers(libraryId).webhooks;
|
|
457
|
+
return webhooks === null
|
|
458
|
+
? workflowWebhookRefusal()
|
|
459
|
+
: webhooks.receive(context.req.param("bindingId"), context.req.raw);
|
|
460
|
+
})
|
|
461
|
+
.fetch(request);
|
|
462
|
+
}
|
|
463
|
+
async #isWebhookOwner(principal) {
|
|
464
|
+
const shared = await standaloneTenancy(this.env);
|
|
465
|
+
const current = await shared.owner();
|
|
466
|
+
return (principal.actor.kind === "member" &&
|
|
467
|
+
current?.actor.memberId === principal.actor.memberId);
|
|
468
|
+
}
|
|
469
|
+
/** Root-authenticated owner RPC; command data cannot nominate an actor or pin. */
|
|
470
|
+
async webhookControl(principal, installationId, raw) {
|
|
471
|
+
if (!(await this.#isWebhookOwner(principal)))
|
|
472
|
+
return workflowWebhookRefusal();
|
|
473
|
+
const parsed = workflowWebhookCommandSchema.safeParse(raw);
|
|
474
|
+
if (!parsed.success)
|
|
475
|
+
return workflowWebhookRefusal();
|
|
476
|
+
const { owner, libraryTriggers } = await this.composed;
|
|
477
|
+
const webhooks = libraryTriggers(installationId).webhooks;
|
|
478
|
+
if (webhooks === null)
|
|
479
|
+
return workflowWebhookRefusal();
|
|
480
|
+
return workflowWebhookControlResponse(webhooks, principal, parsed.data, async (target) => ({
|
|
481
|
+
target: { ...target, installationId },
|
|
482
|
+
authority: (await owner.triggerAuthority(installationId)).pin,
|
|
483
|
+
}));
|
|
350
484
|
}
|
|
351
485
|
/**
|
|
352
486
|
* The emitting App's Instance delivers each accepted event here. The
|
|
@@ -465,6 +599,13 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
465
599
|
const run = owner.store.read(runId);
|
|
466
600
|
if (run?.teamId !== installationId)
|
|
467
601
|
return { error: "not_found" };
|
|
602
|
+
return this.runStatus(runId);
|
|
603
|
+
}
|
|
604
|
+
async runStatus(runId) {
|
|
605
|
+
const owner = await this.owner;
|
|
606
|
+
const run = owner.store.read(runId);
|
|
607
|
+
if (run === null)
|
|
608
|
+
return null;
|
|
468
609
|
const outcome = owner.store.outcome(runId);
|
|
469
610
|
let status;
|
|
470
611
|
try {
|
|
@@ -499,6 +640,24 @@ export class StandaloneWorkflowRunsBase extends DurableObject {
|
|
|
499
640
|
const run = owner.store.read(runId);
|
|
500
641
|
if (run?.teamId !== installationId)
|
|
501
642
|
return { kind: "refused", reason: "denied" };
|
|
643
|
+
return this.cancelRun(runId);
|
|
644
|
+
}
|
|
645
|
+
/** Host-only MCP control reopens the configured owner's authority on every call. */
|
|
646
|
+
async control(principal, operation, runId) {
|
|
647
|
+
const shared = await standaloneTenancy(this.env);
|
|
648
|
+
const current = await shared.owner();
|
|
649
|
+
if (principal.actor.kind !== "member" ||
|
|
650
|
+
current?.actor.memberId !== principal.actor.memberId)
|
|
651
|
+
throw new AppInstanceError("revoked");
|
|
652
|
+
const owner = await this.owner;
|
|
653
|
+
if (owner.store.read(runId) === null)
|
|
654
|
+
return null;
|
|
655
|
+
return operation === "status"
|
|
656
|
+
? this.runStatus(runId)
|
|
657
|
+
: this.cancelRun(runId);
|
|
658
|
+
}
|
|
659
|
+
async cancelRun(runId) {
|
|
660
|
+
const owner = await this.owner;
|
|
502
661
|
if (owner.store.outcome(runId) !== "active")
|
|
503
662
|
return { kind: "terminal" };
|
|
504
663
|
const instance = await this.env.GRAPH_WORKFLOWS.get(runId);
|
package/dist/storage-frame.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare const storageFrameMethods: {
|
|
|
8
8
|
readonly kind: "accepted";
|
|
9
9
|
readonly value: {
|
|
10
10
|
readonly value: Schema.Json;
|
|
11
|
-
readonly source: "
|
|
11
|
+
readonly source: "member" | "team" | "none";
|
|
12
12
|
readonly team: {
|
|
13
13
|
readonly value: Schema.Json;
|
|
14
14
|
readonly revision: number;
|
|
@@ -21,7 +21,7 @@ export declare const storageFrameMethods: {
|
|
|
21
21
|
};
|
|
22
22
|
} | {
|
|
23
23
|
readonly kind: "rejected";
|
|
24
|
-
readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded"
|
|
24
|
+
readonly code: "unavailable" | "invalid" | "conflict" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded";
|
|
25
25
|
};
|
|
26
26
|
requiredOperations: () => string[];
|
|
27
27
|
};
|
|
@@ -29,7 +29,7 @@ export declare const storageFrameMethods: {
|
|
|
29
29
|
decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
|
|
30
30
|
readonly value: Schema.Json;
|
|
31
31
|
readonly key: string;
|
|
32
|
-
readonly scope: "
|
|
32
|
+
readonly scope: "member" | "team";
|
|
33
33
|
readonly basedOn: number | null;
|
|
34
34
|
}];
|
|
35
35
|
decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
@@ -39,14 +39,14 @@ export declare const storageFrameMethods: {
|
|
|
39
39
|
};
|
|
40
40
|
} | {
|
|
41
41
|
readonly kind: "rejected";
|
|
42
|
-
readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded"
|
|
42
|
+
readonly code: "unavailable" | "invalid" | "conflict" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded";
|
|
43
43
|
};
|
|
44
44
|
requiredOperations: () => string[];
|
|
45
45
|
};
|
|
46
46
|
reset: {
|
|
47
47
|
decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
|
|
48
48
|
readonly key: string;
|
|
49
|
-
readonly scope: "
|
|
49
|
+
readonly scope: "member" | "team";
|
|
50
50
|
readonly basedOn: number | null;
|
|
51
51
|
}];
|
|
52
52
|
decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
@@ -56,7 +56,7 @@ export declare const storageFrameMethods: {
|
|
|
56
56
|
};
|
|
57
57
|
} | {
|
|
58
58
|
readonly kind: "rejected";
|
|
59
|
-
readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded"
|
|
59
|
+
readonly code: "unavailable" | "invalid" | "conflict" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded";
|
|
60
60
|
};
|
|
61
61
|
requiredOperations: () => string[];
|
|
62
62
|
};
|
|
@@ -72,12 +72,20 @@ export async function capabilityFixture(options = {}) {
|
|
|
72
72
|
descriptors: capabilities,
|
|
73
73
|
});
|
|
74
74
|
if (options.admitted === false)
|
|
75
|
-
installation = {
|
|
75
|
+
installation = {
|
|
76
|
+
...installation,
|
|
77
|
+
authority: {
|
|
78
|
+
...installation.authority,
|
|
79
|
+
approvedGrants: [],
|
|
80
|
+
approval: null,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
76
83
|
// An anonymous reader reaches only a public App, as the host's visitor.
|
|
77
84
|
if (options.visitor)
|
|
78
85
|
installation = { ...installation, visibility: "public" };
|
|
79
86
|
const readOnly = () => Promise.reject(new Error("Fixture installations are read-only"));
|
|
80
87
|
const base = {
|
|
88
|
+
bootUi: () => Promise.resolve({ kind: "passed" }),
|
|
81
89
|
auth: {
|
|
82
90
|
resolve: () => Promise.reject(new Error("Capability fixture resolves no actor")),
|
|
83
91
|
authenticate: options.authenticate ??
|
|
@@ -96,9 +104,9 @@ export async function capabilityFixture(options = {}) {
|
|
|
96
104
|
put: () => Promise.reject(new Error("Fixture artifacts are read-only")),
|
|
97
105
|
},
|
|
98
106
|
installations: {
|
|
107
|
+
generation: () => Promise.resolve("fixture-generation"),
|
|
99
108
|
list: () => Promise.resolve([installation]),
|
|
100
109
|
get: (id) => Promise.resolve(id === installation.installationId ? installation : null),
|
|
101
|
-
put: readOnly,
|
|
102
110
|
compareAndPut: readOnly,
|
|
103
111
|
},
|
|
104
112
|
instances: {
|
|
@@ -123,7 +131,10 @@ export async function capabilityFixture(options = {}) {
|
|
|
123
131
|
installation = {
|
|
124
132
|
...installation,
|
|
125
133
|
release: await release("rel_undeclared", []),
|
|
126
|
-
|
|
134
|
+
authority: {
|
|
135
|
+
...installation.authority,
|
|
136
|
+
authorityVersion: installation.authority.authorityVersion + 1,
|
|
137
|
+
},
|
|
127
138
|
};
|
|
128
139
|
},
|
|
129
140
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { permissionCatalog } from "../apps/catalog.js";
|
|
1
2
|
import * as Result from "effect/Result";
|
|
2
3
|
import { applyInstallationCommand, deriveDeclaredOperations, } from "../apps/index.js";
|
|
3
4
|
/** Build fixture state through the same approval owner used by installation routes. */
|
|
@@ -6,7 +7,6 @@ export async function approvedInstallation(input) {
|
|
|
6
7
|
kind: "install",
|
|
7
8
|
release: input.release,
|
|
8
9
|
releasePin: "pinned",
|
|
9
|
-
grants: "declared",
|
|
10
10
|
}, {
|
|
11
11
|
by: { kind: "member", memberId: "fixture-owner" },
|
|
12
12
|
role: "owner",
|
|
@@ -21,5 +21,29 @@ export async function approvedInstallation(input) {
|
|
|
21
21
|
});
|
|
22
22
|
if (Result.isFailure(result))
|
|
23
23
|
throw new Error(result.failure.code);
|
|
24
|
-
|
|
24
|
+
const installed = result.success.next;
|
|
25
|
+
const declaredOperations = await deriveDeclaredOperations(input.release, input.declaration, input.descriptors ?? []);
|
|
26
|
+
const changes = declaredOperations.capabilities
|
|
27
|
+
.filter(({ capability }) => permissionCatalog[capability].consent === "ask")
|
|
28
|
+
.map(({ capability, operations }) => ({
|
|
29
|
+
capability,
|
|
30
|
+
grant: { operations, resources: "tenant-scope" },
|
|
31
|
+
}));
|
|
32
|
+
if (changes.length === 0)
|
|
33
|
+
return { ...installed, visibility: input.visibility ?? "private" };
|
|
34
|
+
const approved = applyInstallationCommand(installed, { kind: "change-grants", changes }, {
|
|
35
|
+
by: { kind: "member", memberId: "fixture-owner" },
|
|
36
|
+
role: "owner",
|
|
37
|
+
expectedVersion: installed.authority.authorityVersion,
|
|
38
|
+
declaration: input.declaration,
|
|
39
|
+
declaredOperations,
|
|
40
|
+
now: 1001,
|
|
41
|
+
identity: installed,
|
|
42
|
+
});
|
|
43
|
+
if (Result.isFailure(approved))
|
|
44
|
+
throw new Error(approved.failure.code);
|
|
45
|
+
return {
|
|
46
|
+
...approved.success.next,
|
|
47
|
+
visibility: input.visibility ?? "private",
|
|
48
|
+
};
|
|
25
49
|
}
|
package/dist/testing/worker.js
CHANGED
|
@@ -16,7 +16,7 @@ export class ConfiguredInstance extends DurableObject {
|
|
|
16
16
|
this.catalog = createFixtureCatalog(env.CATALOG_FIXTURE);
|
|
17
17
|
this.gate = createCloudflareInvocationGate(ctx, () => Promise.resolve());
|
|
18
18
|
void ctx.blockConcurrencyWhile(() => seedFixtureCatalog(env.CATALOG_FIXTURE, this.catalog));
|
|
19
|
-
this.runtime = createCloudflareAppInstance(ctx, env.LOADER, (id) => resolveDurableBackend(this.catalog, id).pipe(Effect.mapError((error) => new Error(String(error)))), () => ({}));
|
|
19
|
+
this.runtime = createCloudflareAppInstance(ctx, env.LOADER, (id) => resolveDurableBackend(this.catalog, id).pipe(Effect.mapError((error) => new Error(String(error)))), () => ({}), () => Promise.resolve(null));
|
|
20
20
|
}
|
|
21
21
|
async fetch(request) {
|
|
22
22
|
const url = new URL(request.url);
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node stand-in for `cloudflare:workers` and `cloudflare:workflows
|
|
3
|
-
*
|
|
2
|
+
* Node-only stand-in for `cloudflare:workers` and `cloudflare:workflows`. It
|
|
3
|
+
* supplies constructors, an explicit retained-work recorder for tests, and
|
|
4
|
+
* empty binding lookup; no Cloudflare service or lifetime semantics are
|
|
5
|
+
* supplied. Adapters and SQLite run unchanged.
|
|
4
6
|
* Resolve it with `import.meta.resolve("@fungi.computer/oyster/testing/workers-runtime")`
|
|
5
7
|
* from a module hook.
|
|
6
8
|
*/
|
|
9
|
+
export declare const env: {};
|
|
10
|
+
/** Node tests retain and explicitly drain the same platform-owned work. */
|
|
11
|
+
export declare function waitUntil(promise: Promise<unknown>): void;
|
|
12
|
+
export declare function drainWaitUntil(): Promise<void>;
|
|
7
13
|
export declare class DurableObject<Env = unknown> {
|
|
8
14
|
readonly ctx: DurableObjectState;
|
|
9
15
|
readonly env: Env;
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node stand-in for `cloudflare:workers` and `cloudflare:workflows
|
|
3
|
-
*
|
|
2
|
+
* Node-only stand-in for `cloudflare:workers` and `cloudflare:workflows`. It
|
|
3
|
+
* supplies constructors, an explicit retained-work recorder for tests, and
|
|
4
|
+
* empty binding lookup; no Cloudflare service or lifetime semantics are
|
|
5
|
+
* supplied. Adapters and SQLite run unchanged.
|
|
4
6
|
* Resolve it with `import.meta.resolve("@fungi.computer/oyster/testing/workers-runtime")`
|
|
5
7
|
* from a module hook.
|
|
6
8
|
*/
|
|
9
|
+
export const env = {};
|
|
10
|
+
const retainedWork = new Set();
|
|
11
|
+
/** Node tests retain and explicitly drain the same platform-owned work. */
|
|
12
|
+
export function waitUntil(promise) {
|
|
13
|
+
retainedWork.add(Promise.resolve(promise).then(() => ({ kind: "completed" }), (cause) => ({
|
|
14
|
+
kind: "failed",
|
|
15
|
+
causeKind: cause instanceof Error ? cause.name : "non-error rejection",
|
|
16
|
+
})));
|
|
17
|
+
}
|
|
18
|
+
export async function drainWaitUntil() {
|
|
19
|
+
const outcomes = await Promise.all(retainedWork);
|
|
20
|
+
retainedWork.clear();
|
|
21
|
+
const failure = outcomes.find((outcome) => outcome.kind === "failed");
|
|
22
|
+
if (failure?.kind === "failed")
|
|
23
|
+
throw new Error(`Retained Worker work failed: ${failure.causeKind}`);
|
|
24
|
+
}
|
|
7
25
|
export class DurableObject {
|
|
8
26
|
ctx;
|
|
9
27
|
env;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RateLimit } from "@cloudflare/workers-types";
|
|
3
|
+
declare const webhookConcurrencySchema: z.ZodObject<{
|
|
4
|
+
hostConcurrency: z.ZodNumber;
|
|
5
|
+
bindingConcurrency: z.ZodNumber;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
/** Native rate bindings own quotas; this adapter never implements a rate clock. */
|
|
8
|
+
export declare function workflowWebhookAbuse(options: {
|
|
9
|
+
scope: Readonly<{
|
|
10
|
+
host: string;
|
|
11
|
+
tenant: string;
|
|
12
|
+
}>;
|
|
13
|
+
hostRate: Pick<RateLimit, "limit">;
|
|
14
|
+
bindingRate: Pick<RateLimit, "limit">;
|
|
15
|
+
concurrency: z.input<typeof webhookConcurrencySchema>;
|
|
16
|
+
}): {
|
|
17
|
+
concurrency: {
|
|
18
|
+
hostConcurrency: number;
|
|
19
|
+
bindingConcurrency: number;
|
|
20
|
+
};
|
|
21
|
+
/** Per-instance streaming bound; durable run concurrency belongs to Braid. */
|
|
22
|
+
hostLease(): Promise<null | (() => void)>;
|
|
23
|
+
bindingAdmission(bindingId: string): Promise<boolean>;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
// The maintained edge rate-limit contract bounds keys to 256 characters.
|
|
4
|
+
// Hash the full scoped tuple, rather than truncating identities or depending
|
|
5
|
+
// on a provider to accept an oversized concatenation.
|
|
6
|
+
const rateKey = (parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
7
|
+
const webhookConcurrencySchema = z.strictObject({
|
|
8
|
+
hostConcurrency: z.number().int().positive(),
|
|
9
|
+
bindingConcurrency: z.number().int().positive(),
|
|
10
|
+
});
|
|
11
|
+
/** Native rate bindings own quotas; this adapter never implements a rate clock. */
|
|
12
|
+
export function workflowWebhookAbuse(options) {
|
|
13
|
+
const scope = z
|
|
14
|
+
.strictObject({
|
|
15
|
+
host: z.string().min(1).max(256),
|
|
16
|
+
tenant: z.string().min(1).max(256),
|
|
17
|
+
})
|
|
18
|
+
.parse(options.scope);
|
|
19
|
+
const concurrency = webhookConcurrencySchema.parse(options.concurrency);
|
|
20
|
+
const prefix = ["workflow-webhooks", scope.host, scope.tenant];
|
|
21
|
+
let active = 0;
|
|
22
|
+
return {
|
|
23
|
+
concurrency,
|
|
24
|
+
/** Per-instance streaming bound; durable run concurrency belongs to Braid. */
|
|
25
|
+
async hostLease() {
|
|
26
|
+
if (active >= concurrency.hostConcurrency)
|
|
27
|
+
return null;
|
|
28
|
+
const { success } = await options.hostRate.limit({
|
|
29
|
+
key: rateKey(["workflow-webhooks", scope.host, "host"]),
|
|
30
|
+
});
|
|
31
|
+
// Native admission awaits I/O; simultaneous successful replies still
|
|
32
|
+
// cannot overbook the local streaming ceiling.
|
|
33
|
+
if (!success || active >= concurrency.hostConcurrency)
|
|
34
|
+
return null;
|
|
35
|
+
active++;
|
|
36
|
+
let released = false;
|
|
37
|
+
return () => {
|
|
38
|
+
if (released)
|
|
39
|
+
return;
|
|
40
|
+
released = true;
|
|
41
|
+
active--;
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
async bindingAdmission(bindingId) {
|
|
45
|
+
const id = z.string().min(1).max(256).safeParse(bindingId);
|
|
46
|
+
if (!id.success)
|
|
47
|
+
return false;
|
|
48
|
+
const { success } = await options.bindingRate.limit({
|
|
49
|
+
key: rateKey([...prefix, "binding", id.data]),
|
|
50
|
+
});
|
|
51
|
+
return success;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|