@fungi.computer/oyster 0.2.0 → 0.3.1

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.
Files changed (291) hide show
  1. package/README.md +396 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +213 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/older-oyster-store-refusal.d.ts +4 -0
  130. package/dist/older-oyster-store-refusal.js +18 -0
  131. package/dist/older-oyster-store.d.ts +21 -0
  132. package/dist/older-oyster-store.js +43 -0
  133. package/dist/pack.d.ts +14 -3
  134. package/dist/pack.js +44 -19
  135. package/dist/page/app-access.d.ts +7 -0
  136. package/dist/page/app-access.js +75 -0
  137. package/dist/page/boot-host.js +57 -0
  138. package/dist/page/capabilities.d.ts +3 -3
  139. package/dist/page/capabilities.js +16 -12
  140. package/dist/page/computer-handles.d.ts +3 -3
  141. package/dist/page/computer-target.d.ts +3 -0
  142. package/dist/page/computer-target.js +9 -0
  143. package/dist/page/mount.d.ts +23 -3
  144. package/dist/page/mount.js +152 -16
  145. package/dist/page-assets.d.ts +22 -0
  146. package/dist/page-assets.js +28 -0
  147. package/dist/page-script.d.ts +3 -0
  148. package/dist/page-script.js +5 -4
  149. package/dist/release.d.ts +13 -6
  150. package/dist/release.js +10 -7
  151. package/dist/root-data.d.ts +16 -6
  152. package/dist/root-data.js +2 -2
  153. package/dist/source/index.d.ts +1 -1
  154. package/dist/source/team-source.d.ts +20 -8
  155. package/dist/source/team-source.js +39 -10
  156. package/dist/source/template-seed.d.ts +2 -2
  157. package/dist/standalone/agent-mail.d.ts +24 -0
  158. package/dist/standalone/agent-mail.js +62 -0
  159. package/dist/standalone/agent-store.d.ts +9 -0
  160. package/dist/standalone/agent-store.js +30 -0
  161. package/dist/standalone/agents.d.ts +68 -9
  162. package/dist/standalone/agents.js +428 -56
  163. package/dist/standalone/app-builds-binding.d.ts +20 -2
  164. package/dist/standalone/app-builds-binding.js +147 -23
  165. package/dist/standalone/app-set.d.ts +5 -2
  166. package/dist/standalone/app-set.js +186 -144
  167. package/dist/standalone/browser.d.ts +11 -0
  168. package/dist/standalone/browser.js +72 -0
  169. package/dist/standalone/builds.d.ts +13 -4
  170. package/dist/standalone/builds.js +28 -44
  171. package/dist/standalone/computer-egress.d.ts +2 -0
  172. package/dist/standalone/computer-egress.js +13 -4
  173. package/dist/standalone/computer-fleet.d.ts +43 -0
  174. package/dist/standalone/computer-fleet.js +200 -0
  175. package/dist/standalone/computer.d.ts +44 -9
  176. package/dist/standalone/computer.js +129 -22
  177. package/dist/standalone/install-apps-cli.js +43 -6
  178. package/dist/standalone/instance.d.ts +2 -2
  179. package/dist/standalone/instance.js +8 -11
  180. package/dist/standalone/machine-link.d.ts +354 -0
  181. package/dist/standalone/machine-link.js +368 -0
  182. package/dist/standalone/media.d.ts +2 -2
  183. package/dist/standalone/media.js +19 -16
  184. package/dist/standalone/preview.d.ts +2 -2
  185. package/dist/standalone/preview.js +37 -10
  186. package/dist/standalone/registry.d.ts +151 -9
  187. package/dist/standalone/registry.js +305 -8
  188. package/dist/standalone/worker.d.ts +19 -5
  189. package/dist/standalone/worker.js +90 -54
  190. package/dist/standalone/workflow-binding.d.ts +18 -22
  191. package/dist/standalone/workflow-binding.js +18 -18
  192. package/dist/standalone/workflow-control.d.ts +4 -0
  193. package/dist/standalone/workflow-control.js +40 -0
  194. package/dist/standalone/workflow-events.d.ts +2 -2
  195. package/dist/standalone/workflow-events.js +9 -10
  196. package/dist/standalone/workflow-git-capture.js +4 -3
  197. package/dist/standalone/workflow-host.d.ts +162 -161
  198. package/dist/standalone/workflow-host.js +101 -21
  199. package/dist/standalone/workflow-node-capture.js +11 -4
  200. package/dist/standalone/workflow-schedules.d.ts +5 -2
  201. package/dist/standalone/workflow-schedules.js +9 -10
  202. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  203. package/dist/standalone/workflow-trigger-host.js +2 -2
  204. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  205. package/dist/standalone/workflow-webhooks.js +39 -0
  206. package/dist/standalone/workflows.d.ts +140 -43
  207. package/dist/standalone/workflows.js +198 -39
  208. package/dist/storage-frame.d.ts +6 -6
  209. package/dist/testing/capabilities.js +14 -3
  210. package/dist/testing/installation.js +26 -2
  211. package/dist/testing/worker.js +1 -1
  212. package/dist/testing/workers-runtime.d.ts +8 -2
  213. package/dist/testing/workers-runtime.js +20 -2
  214. package/dist/webhook-abuse.d.ts +25 -0
  215. package/dist/webhook-abuse.js +54 -0
  216. package/dist/webhook-custody.d.ts +144 -0
  217. package/dist/webhook-custody.js +222 -0
  218. package/dist/webhook-ingress.d.ts +36 -0
  219. package/dist/webhook-ingress.js +146 -0
  220. package/dist/webhooks.d.ts +111 -0
  221. package/dist/webhooks.js +189 -0
  222. package/dist/workers-git.d.ts +12 -17
  223. package/dist/workers-git.js +64 -52
  224. package/dist/workers.d.ts +226 -13
  225. package/dist/workers.js +363 -51
  226. package/dist/workflow-agent-authority.d.ts +8 -0
  227. package/dist/workflow-agent-authority.js +42 -0
  228. package/dist/workflows.d.ts +66 -0
  229. package/dist/workflows.js +97 -0
  230. package/docs/api/README.md +63 -37
  231. package/docs/api/agents.md +17 -0
  232. package/docs/api/app-storage.md +1 -1
  233. package/docs/api/apps.md +1394 -543
  234. package/docs/api/browser/boot.md +132 -0
  235. package/docs/api/browser.md +1440 -0
  236. package/docs/api/build-contract.md +38 -25
  237. package/docs/api/build.md +263 -84
  238. package/docs/api/cloudflare.md +93 -7
  239. package/docs/api/git.md +3 -13
  240. package/docs/api/host.md +944 -118
  241. package/docs/api/index.md +320 -18
  242. package/docs/api/keys.md +24 -1
  243. package/docs/api/mcp.md +59 -12
  244. package/docs/api/modules.md +33 -40
  245. package/docs/api/pack.md +57 -8
  246. package/docs/api/page/mount.md +92 -4
  247. package/docs/api/workers-git.md +103 -24
  248. package/docs/api/workers.md +1594 -244
  249. package/docs/site/agent-quickstart.mdx +28 -14
  250. package/examples/standalone/worker.ts +3 -1
  251. package/examples/standalone/wrangler.jsonc +2 -0
  252. package/package.json +73 -71
  253. package/dist/installation-consent.d.ts +0 -81
  254. package/dist/installation-consent.js +0 -84
  255. package/dist/shell/app-directory.d.ts +0 -542
  256. package/dist/shell/app-directory.js +0 -196
  257. package/dist/shell/browser-actions.d.ts +0 -7
  258. package/dist/shell/browser-actions.js +0 -56
  259. package/dist/shell/command-host.d.ts +0 -112
  260. package/dist/shell/command-host.js +0 -502
  261. package/dist/shell/commands.d.ts +0 -25
  262. package/dist/shell/commands.js +0 -78
  263. package/dist/shell/index.d.ts +0 -8
  264. package/dist/shell/index.js +0 -8
  265. package/dist/shell/keys.d.ts +0 -41
  266. package/dist/shell/keys.js +0 -134
  267. package/dist/shell/shell-module.d.ts +0 -55
  268. package/dist/shell/shell-module.js +0 -86
  269. package/dist/shell/window-manager.d.ts +0 -128
  270. package/dist/shell/window-manager.js +0 -730
  271. package/dist/shell/workbench-contract.d.ts +0 -68
  272. package/dist/shell/workbench-directory.d.ts +0 -332
  273. package/dist/shell/workbench-directory.js +0 -35
  274. package/dist/shell/workbench-host-types.d.ts +0 -96
  275. package/dist/shell/workbench-host-types.js +0 -1
  276. package/dist/shell/workbench-host.d.ts +0 -34
  277. package/dist/shell/workbench-host.js +0 -352
  278. package/dist/shell/workbench-model.d.ts +0 -194
  279. package/dist/shell/workbench-model.js +0 -53
  280. package/dist/shell/workbench.d.ts +0 -4
  281. package/dist/shell/workbench.js +0 -2
  282. package/docs/api/shell/app-directory.md +0 -599
  283. package/docs/api/shell/command-host.md +0 -224
  284. package/docs/api/shell/commands.md +0 -197
  285. package/docs/api/shell/keys.md +0 -183
  286. package/docs/api/shell/shell-module.md +0 -204
  287. package/docs/api/shell/window-manager.md +0 -267
  288. package/docs/api/shell/workbench-directory.md +0 -119
  289. package/docs/api/shell/workbench.md +0 -205
  290. package/docs/api/shell.md +0 -1915
  291. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -0,0 +1,144 @@
1
+ import { z } from "zod";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
+ import { type ArmedWorkflowTrigger } from "@fungi.computer/braid/runtime";
4
+ import type { WebhookDelivery } from "./webhook-ingress.js";
5
+ export declare const workflowWebhookVerifierSchema: z.ZodEnum<{
6
+ "fungi-v1": "fungi-v1";
7
+ github: "github";
8
+ "standard-webhooks": "standard-webhooks";
9
+ }>;
10
+ export type WorkflowWebhookVerifier = z.output<typeof workflowWebhookVerifierSchema>;
11
+ /**
12
+ * This owner lives INSIDE the Team DO / standalone store. Only verification is
13
+ * handed to ingress. No read-existing-secret method, master key or generation
14
+ * derivation exists. Host authentication supplies the principal; owner() must
15
+ * resolve CURRENT server-side ownership, not trust a caller-supplied role.
16
+ */
17
+ export declare function workflowWebhookCustody<Principal>(options: {
18
+ sql: WatchdogSqliteOwner["sql"];
19
+ transactionSync: WatchdogSqliteOwner["transactionSync"];
20
+ now(): number;
21
+ owner(principal: Principal): Promise<string | null>;
22
+ }): {
23
+ create(principal: Principal, bindingId: string, verifier: WorkflowWebhookVerifier, rawBinding: ArmedWorkflowTrigger): Promise<{
24
+ readonly kind: "refused";
25
+ } | {
26
+ kind: "created";
27
+ bindingId: string;
28
+ installationId: string;
29
+ secret: string | null;
30
+ }>;
31
+ rotate(principal: Principal, bindingId: string): Promise<{
32
+ readonly kind: "refused";
33
+ } | {
34
+ kind: "rotated";
35
+ secret: string | null;
36
+ }>;
37
+ retirePrevious(principal: Principal, bindingId: string): Promise<{
38
+ readonly kind: "refused";
39
+ } | {
40
+ kind: "retired";
41
+ }>;
42
+ revoke(principal: Principal, bindingId: string): Promise<{
43
+ readonly kind: "refused";
44
+ } | {
45
+ kind: "revoked";
46
+ }>;
47
+ list(principal: Principal, after?: string | null): Promise<{
48
+ bindings: {
49
+ bindingId: string;
50
+ verifier: "fungi-v1" | "github" | "standard-webhooks";
51
+ creator: string;
52
+ generation: number;
53
+ previousExpiresAt: number | null;
54
+ }[];
55
+ next: string | null;
56
+ }>;
57
+ verification: {
58
+ /** Final owner-store check for an already verified durable reservation. */
59
+ isActive(bindingId: string): boolean;
60
+ /** Always does six maintained verifier checks, including unknown IDs. */
61
+ verify(delivery: WebhookDelivery): {
62
+ status: "refused";
63
+ eventId?: undefined;
64
+ binding?: undefined;
65
+ } | {
66
+ status: "ok";
67
+ eventId: string;
68
+ binding: {
69
+ installationId: string;
70
+ workflowId: string;
71
+ commit: string;
72
+ digest: string & {
73
+ readonly appReleaseKind: "Sha256Digest";
74
+ };
75
+ triggerId: string;
76
+ bindings: Record<string, string | {
77
+ kind: "team-source";
78
+ }>;
79
+ armedBy: string;
80
+ armedVia: {
81
+ kind: "connector";
82
+ agentId: string;
83
+ attachmentId: string;
84
+ attachmentVersion: number;
85
+ } | {
86
+ kind: "policy";
87
+ agentId: string;
88
+ policyRevision: string;
89
+ grantorMemberId: string;
90
+ } | null;
91
+ grantorMembershipVersion: number;
92
+ installationAuthority: {
93
+ selection: {
94
+ instanceId: string;
95
+ owner: {
96
+ kind: "installation" | "deployment";
97
+ id: string;
98
+ };
99
+ appId: string & {
100
+ readonly appReleaseKind: "AppId";
101
+ };
102
+ releaseId: string & {
103
+ readonly appReleaseKind: "ReleaseId";
104
+ };
105
+ authorityVersion: number;
106
+ };
107
+ artifactDigest: string & {
108
+ readonly appReleaseKind: "Sha256Digest";
109
+ };
110
+ };
111
+ computerId: string;
112
+ delivery?: {
113
+ readonly agentId: string;
114
+ readonly sessionId: string;
115
+ } | undefined;
116
+ nodeGitAccess?: readonly {
117
+ readonly nodeId: string;
118
+ readonly gitAccess: {
119
+ repository: {
120
+ kind: "installation";
121
+ installationId: string;
122
+ repository: string;
123
+ } | {
124
+ kind: "team-source";
125
+ repository: string;
126
+ };
127
+ branches: string[];
128
+ } | {
129
+ repository: {
130
+ kind: "installation";
131
+ installationId: string;
132
+ repository: string;
133
+ } | {
134
+ kind: "team-source";
135
+ repository: string;
136
+ };
137
+ access: "read";
138
+ };
139
+ readonly graphId?: string;
140
+ }[] | undefined;
141
+ };
142
+ };
143
+ };
144
+ };
@@ -0,0 +1,222 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { Base64Url } from "effect/encoding";
3
+ import { z } from "zod";
4
+ import { armedWorkflowTriggerSchema, } from "@fungi.computer/braid/runtime";
5
+ import { verifyFungiV1, verifyGithub, verifyStandardWebhooks, standardWebhookSecret, } from "@fungi.computer/veil/event-verifiers";
6
+ export const workflowWebhookVerifierSchema = z.enum([
7
+ "fungi-v1",
8
+ "github",
9
+ "standard-webhooks",
10
+ ]);
11
+ const idSchema = z.string().min(1).max(256);
12
+ const keySchema = z.strictObject({
13
+ secret: z.string().regex(/^[A-Za-z0-9_-]{43}$/u),
14
+ generation: z.number().int().positive(),
15
+ });
16
+ const rowSchema = z.strictObject({
17
+ id: idSchema,
18
+ creator: idSchema,
19
+ binding: armedWorkflowTriggerSchema,
20
+ verifier: workflowWebhookVerifierSchema,
21
+ current: keySchema,
22
+ previous: keySchema
23
+ .extend({ expiresAt: z.number().int().nonnegative() })
24
+ .nullable(),
25
+ });
26
+ const refused = { kind: "refused" };
27
+ const DAY = 24 * 60 * 60 * 1000;
28
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
29
+ const verifiers = {
30
+ "fungi-v1": verifyFungiV1,
31
+ github: verifyGithub,
32
+ "standard-webhooks": verifyStandardWebhooks,
33
+ };
34
+ function matchingEvent(row, scheme, current, previous, now) {
35
+ if (row?.verifier !== scheme)
36
+ return null;
37
+ if (current.status === "ok")
38
+ return current.eventId;
39
+ if (previous.status === "ok" &&
40
+ row.previous !== null &&
41
+ row.previous.expiresAt > now())
42
+ return previous.eventId;
43
+ return null;
44
+ }
45
+ /** Evaluate both slots for EVERY scheme; never short-circuit on success. */
46
+ function verifiedEvent(row, request, dummy, now) {
47
+ let eventId = null;
48
+ for (const scheme of workflowWebhookVerifierSchema.options) {
49
+ const current = verifiers[scheme](row?.current.secret ?? dummy, request);
50
+ const previous = verifiers[scheme](row?.previous?.secret ?? dummy, request);
51
+ const match = matchingEvent(row, scheme, current, previous, now);
52
+ if (match !== null)
53
+ eventId = match;
54
+ }
55
+ return eventId;
56
+ }
57
+ function deliveryIdentity(verifier, eventId, body) {
58
+ // GitHub deliberately retains its unsigned ID plus body hash: accepted
59
+ // weaker freshness. Neither signatures nor key bytes enter the identity.
60
+ switch (verifier) {
61
+ case "github":
62
+ return hash(JSON.stringify([eventId, hash(body)]));
63
+ case "fungi-v1":
64
+ return hash(body);
65
+ case "standard-webhooks":
66
+ return hash(eventId);
67
+ }
68
+ }
69
+ /**
70
+ * This owner lives INSIDE the Team DO / standalone store. Only verification is
71
+ * handed to ingress. No read-existing-secret method, master key or generation
72
+ * derivation exists. Host authentication supplies the principal; owner() must
73
+ * resolve CURRENT server-side ownership, not trust a caller-supplied role.
74
+ */
75
+ export function workflowWebhookCustody(options) {
76
+ const { sql } = options;
77
+ sql.exec(`CREATE TABLE IF NOT EXISTS oyster_workflow_webhook_keys (
78
+ id TEXT PRIMARY KEY, record TEXT NOT NULL
79
+ ) STRICT`);
80
+ sql.exec(`CREATE TABLE IF NOT EXISTS oyster_workflow_webhook_revoked (
81
+ id TEXT PRIMARY KEY
82
+ ) STRICT`);
83
+ const read = (id) => {
84
+ const raw = sql
85
+ .exec("SELECT record FROM oyster_workflow_webhook_keys WHERE id=?", id)
86
+ .toArray()[0];
87
+ if (raw === undefined)
88
+ return null;
89
+ const envelope = z.object({ record: z.string() }).parse(raw);
90
+ return rowSchema.parse(JSON.parse(envelope.record));
91
+ };
92
+ const write = (row) => {
93
+ sql.exec("INSERT INTO oyster_workflow_webhook_keys(id,record) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET record=excluded.record", row.id, JSON.stringify(row));
94
+ };
95
+ const newKey = (generation) => ({
96
+ secret: Base64Url.encode(randomBytes(32)),
97
+ generation,
98
+ });
99
+ const dummy = Base64Url.encode(randomBytes(32));
100
+ const disclosure = (row, actor) => actor !== row.creator
101
+ ? null
102
+ : row.verifier === "standard-webhooks"
103
+ ? standardWebhookSecret(row.current.secret)
104
+ : row.current.secret;
105
+ const withOwnedBinding = async (principal, bindingId, update) => {
106
+ const actor = await options.owner(principal);
107
+ if (actor === null)
108
+ return refused;
109
+ return options.transactionSync(() => {
110
+ const row = read(idSchema.parse(bindingId));
111
+ return row === null ? refused : update(row, actor);
112
+ });
113
+ };
114
+ return {
115
+ async create(principal, bindingId, verifier, rawBinding) {
116
+ const creator = await options.owner(principal);
117
+ if (creator === null)
118
+ return refused;
119
+ const id = idSchema.parse(bindingId);
120
+ const scheme = workflowWebhookVerifierSchema.parse(verifier);
121
+ // Supplied only by the host's existing trigger-arm owner, never ingress.
122
+ const binding = armedWorkflowTriggerSchema.parse(rawBinding);
123
+ if (binding.armedBy !== creator || binding.armedVia !== null)
124
+ return refused;
125
+ return options.transactionSync(() => {
126
+ if (read(id) !== null ||
127
+ sql
128
+ .exec("SELECT id FROM oyster_workflow_webhook_revoked WHERE id=?", id)
129
+ .toArray().length > 0)
130
+ return refused;
131
+ const row = rowSchema.parse({
132
+ id,
133
+ creator,
134
+ binding,
135
+ verifier: scheme,
136
+ current: newKey(1),
137
+ previous: null,
138
+ });
139
+ write(row);
140
+ return {
141
+ kind: "created",
142
+ bindingId: row.id,
143
+ installationId: row.binding.installationId,
144
+ secret: disclosure(row, creator),
145
+ };
146
+ });
147
+ },
148
+ async rotate(principal, bindingId) {
149
+ return withOwnedBinding(principal, bindingId, (row, actor) => {
150
+ if (row.previous !== null && row.previous.expiresAt > options.now())
151
+ return refused;
152
+ row.previous = { ...row.current, expiresAt: options.now() + DAY };
153
+ row.current = newKey(row.current.generation + 1);
154
+ write(row);
155
+ return { kind: "rotated", secret: disclosure(row, actor) };
156
+ });
157
+ },
158
+ async retirePrevious(principal, bindingId) {
159
+ return withOwnedBinding(principal, bindingId, (row) => {
160
+ row.previous = null;
161
+ write(row);
162
+ return { kind: "retired" };
163
+ });
164
+ },
165
+ async revoke(principal, bindingId) {
166
+ return withOwnedBinding(principal, bindingId, (row) => {
167
+ // Delete ALL key bytes immediately; the tombstone prevents redisclosure.
168
+ sql.exec("DELETE FROM oyster_workflow_webhook_keys WHERE id=?", row.id);
169
+ sql.exec("INSERT INTO oyster_workflow_webhook_revoked(id) VALUES(?) ON CONFLICT DO NOTHING", row.id);
170
+ return { kind: "revoked" };
171
+ });
172
+ },
173
+ async list(principal, after = null) {
174
+ if ((await options.owner(principal)) === null)
175
+ return { bindings: [], next: null };
176
+ const cursor = after === null ? "" : idSchema.parse(after);
177
+ const rows = sql
178
+ .exec("SELECT record FROM oyster_workflow_webhook_keys WHERE id>? ORDER BY id LIMIT 101", cursor)
179
+ .toArray();
180
+ const bindings = rows.slice(0, 100).map((raw) => {
181
+ const row = rowSchema.parse(JSON.parse(z.object({ record: z.string() }).parse(raw).record));
182
+ return {
183
+ bindingId: row.id,
184
+ verifier: row.verifier,
185
+ creator: row.creator,
186
+ generation: row.current.generation,
187
+ previousExpiresAt: row.previous?.expiresAt ?? null,
188
+ };
189
+ });
190
+ return {
191
+ bindings,
192
+ next: rows.length > 100 ? (bindings.at(-1)?.bindingId ?? null) : null,
193
+ };
194
+ },
195
+ verification: {
196
+ /** Final owner-store check for an already verified durable reservation. */
197
+ isActive(bindingId) {
198
+ return read(idSchema.parse(bindingId)) !== null;
199
+ },
200
+ /** Always does six maintained verifier checks, including unknown IDs. */
201
+ verify(delivery) {
202
+ const row = idSchema.safeParse(delivery.bindingId).success
203
+ ? read(delivery.bindingId)
204
+ : null;
205
+ const request = {
206
+ headers: delivery.headers,
207
+ body: delivery.body,
208
+ urlToken: null,
209
+ nowSeconds: Math.floor(options.now() / 1000),
210
+ };
211
+ const eventId = verifiedEvent(row, request, dummy, () => options.now());
212
+ if (eventId === null || row === null)
213
+ return { status: "refused" };
214
+ return {
215
+ status: "ok",
216
+ eventId: deliveryIdentity(row.verifier, eventId, delivery.body),
217
+ binding: row.binding,
218
+ };
219
+ },
220
+ },
221
+ };
222
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ declare const limitsSchema: z.ZodObject<{
3
+ bodyBytes: z.ZodNumber;
4
+ headerBytes: z.ZodNumber;
5
+ headerCount: z.ZodNumber;
6
+ readMilliseconds: z.ZodNumber;
7
+ }, z.core.$strict>;
8
+ /** A private defensive ceiling, not a sender-controlled policy or spend cap. */
9
+ export type WebhookIngressLimits = z.output<typeof limitsSchema>;
10
+ export type WebhookDelivery = Readonly<{
11
+ bindingId: string;
12
+ headers: Headers;
13
+ body: Uint8Array;
14
+ }>;
15
+ export type WebhookAdmission = Readonly<{
16
+ status: "accepted";
17
+ deliveryId: string;
18
+ }> | Readonly<{
19
+ status: "refused";
20
+ }>;
21
+ export declare const workflowWebhookRefusal: () => Response;
22
+ /**
23
+ * The public representation boundary. Rate admission and streaming limits run
24
+ * before ANY binding access. The private owner verifies and durably admits;
25
+ * this port cannot read, disclose or choose a key, actor or Computer.
26
+ */
27
+ export declare function workflowWebhookIngress(options: {
28
+ limits: WebhookIngressLimits;
29
+ hostLease(): Promise<null | (() => void)>;
30
+ deliver(delivery: WebhookDelivery): Promise<WebhookAdmission>;
31
+ /** Count a fixed failure category; never log producer-controlled error text. */
32
+ reportReadFailure(kind: "aborted" | "read_failed"): void;
33
+ /** Fixed operational counter only; private owner errors may contain secrets. */
34
+ reportOwnerFailure(): void;
35
+ }): (bindingId: string, request: Request) => Promise<Response>;
36
+ export {};
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ const limitsSchema = z.strictObject({
3
+ bodyBytes: z
4
+ .number()
5
+ .int()
6
+ .positive()
7
+ .max(1024 * 1024),
8
+ headerBytes: z
9
+ .number()
10
+ .int()
11
+ .positive()
12
+ .max(32 * 1024),
13
+ headerCount: z.number().int().positive().max(128),
14
+ readMilliseconds: z.number().int().positive().max(30_000),
15
+ });
16
+ const failureResponse = (error) => Response.json({ error }, {
17
+ status: error === "refused" ? 403 : 503,
18
+ headers: { "cache-control": "no-store" },
19
+ });
20
+ export const workflowWebhookRefusal = () => failureResponse("refused");
21
+ const readFailureKind = (cause) => cause instanceof Error && cause.name === "AbortError"
22
+ ? "aborted"
23
+ : "read_failed";
24
+ function headersWithinLimits(headers, limits) {
25
+ let count = 0;
26
+ let bytes = 0;
27
+ const encoder = new TextEncoder();
28
+ headers.forEach((value, key) => {
29
+ count++;
30
+ bytes += encoder.encode(key).byteLength + encoder.encode(value).byteLength;
31
+ });
32
+ return count <= limits.headerCount && bytes <= limits.headerBytes;
33
+ }
34
+ /** null means the producer exhausted its byte or read-time allowance. */
35
+ async function readChunks(reader, expired, maximum) {
36
+ const chunks = [];
37
+ let length = 0;
38
+ for (;;) {
39
+ const part = await Promise.race([reader.read(), expired]);
40
+ if (part === null)
41
+ return null;
42
+ if (part.done)
43
+ return chunks;
44
+ const chunk = z.instanceof(Uint8Array).parse(part.value);
45
+ length += chunk.byteLength;
46
+ if (length > maximum)
47
+ return null;
48
+ chunks.push(chunk);
49
+ }
50
+ }
51
+ function joinChunks(chunks) {
52
+ const body = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.byteLength, 0));
53
+ let offset = 0;
54
+ for (const chunk of chunks) {
55
+ body.set(chunk, offset);
56
+ offset += chunk.byteLength;
57
+ }
58
+ return body;
59
+ }
60
+ async function readBoundedBody(request, limits, report) {
61
+ const reader = request.body?.getReader();
62
+ if (reader === undefined)
63
+ return new Uint8Array();
64
+ let timer;
65
+ const expired = new Promise((resolve) => {
66
+ timer = setTimeout(() => {
67
+ resolve(null);
68
+ }, limits.readMilliseconds);
69
+ });
70
+ let chunks;
71
+ try {
72
+ chunks = await readChunks(reader, expired, limits.bodyBytes);
73
+ if (chunks === null) {
74
+ // Cancellation is not awaited: an untrusted producer can stall it.
75
+ void reader.cancel().catch((cause) => {
76
+ report(readFailureKind(cause));
77
+ });
78
+ }
79
+ }
80
+ catch (cause) {
81
+ report(readFailureKind(cause));
82
+ return null;
83
+ }
84
+ finally {
85
+ clearTimeout(timer);
86
+ reader.releaseLock();
87
+ }
88
+ return chunks === null ? null : joinChunks(chunks);
89
+ }
90
+ /**
91
+ * The public representation boundary. Rate admission and streaming limits run
92
+ * before ANY binding access. The private owner verifies and durably admits;
93
+ * this port cannot read, disclose or choose a key, actor or Computer.
94
+ */
95
+ export function workflowWebhookIngress(options) {
96
+ const limits = limitsSchema.parse(options.limits);
97
+ const admitHost = async () => {
98
+ try {
99
+ const release = await options.hostLease();
100
+ return release === null
101
+ ? { status: "refused" }
102
+ : { status: "admitted", release };
103
+ }
104
+ catch {
105
+ options.reportOwnerFailure();
106
+ return { status: "unavailable" };
107
+ }
108
+ };
109
+ const deliver = async (delivery) => {
110
+ try {
111
+ return await options.deliver(delivery);
112
+ }
113
+ catch {
114
+ options.reportOwnerFailure();
115
+ return { status: "unavailable" };
116
+ }
117
+ };
118
+ return async (bindingId, request) => {
119
+ const admission = await admitHost();
120
+ if (admission.status !== "admitted")
121
+ return failureResponse(admission.status);
122
+ const { release } = admission;
123
+ try {
124
+ if (request.method !== "POST")
125
+ return workflowWebhookRefusal();
126
+ if (!headersWithinLimits(request.headers, limits))
127
+ return workflowWebhookRefusal();
128
+ const body = await readBoundedBody(request, limits, (kind) => {
129
+ options.reportReadFailure(kind);
130
+ });
131
+ if (body === null)
132
+ return workflowWebhookRefusal();
133
+ const outcome = await deliver({
134
+ bindingId,
135
+ headers: request.headers,
136
+ body,
137
+ });
138
+ if (outcome.status !== "accepted")
139
+ return failureResponse(outcome.status);
140
+ return Response.json({ accepted: true, deliveryId: outcome.deliveryId }, { status: 202, headers: { "cache-control": "no-store" } });
141
+ }
142
+ finally {
143
+ release();
144
+ }
145
+ };
146
+ }
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ import { type createTeamWorkflowRuns, type createTeamWorkflowTriggers, type WorkflowInstallationAuthority, type WorkflowTriggerTarget } from "@fungi.computer/braid/runtime";
3
+ import { workflowWebhookCustody, type WorkflowWebhookVerifier } from "./webhook-custody.js";
4
+ import { type WebhookIngressLimits } from "./webhook-ingress.js";
5
+ import { workflowWebhookAbuse } from "./webhook-abuse.js";
6
+ export declare const workflowWebhookCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
7
+ operation: z.ZodLiteral<"create">;
8
+ target: z.ZodObject<{
9
+ installationId: z.ZodString;
10
+ workflowId: z.ZodString;
11
+ commit: z.ZodString;
12
+ digest: z.ZodString & z.ZodType<string & {
13
+ readonly appReleaseKind: "Sha256Digest";
14
+ }, string, z.core.$ZodTypeInternals<string & {
15
+ readonly appReleaseKind: "Sha256Digest";
16
+ }, string>>;
17
+ triggerId: z.ZodString;
18
+ bindings: z.ZodRecord<z.ZodLazy<z.ZodString>, z.ZodUnion<readonly [z.ZodString, z.ZodLazy<z.ZodObject<{
19
+ kind: z.ZodLiteral<"team-source">;
20
+ }, z.core.$strict>>]>>;
21
+ delivery: z.ZodOptional<z.ZodCustom<{
22
+ readonly agentId: string;
23
+ readonly sessionId: string;
24
+ }, {
25
+ readonly agentId: string;
26
+ readonly sessionId: string;
27
+ }>>;
28
+ }, z.core.$strict>;
29
+ computerId: z.ZodString;
30
+ verifier: z.ZodEnum<{
31
+ "fungi-v1": "fungi-v1";
32
+ github: "github";
33
+ "standard-webhooks": "standard-webhooks";
34
+ }>;
35
+ }, z.core.$strict>, z.ZodObject<{
36
+ operation: z.ZodEnum<{
37
+ rotate: "rotate";
38
+ retirePrevious: "retirePrevious";
39
+ revoke: "revoke";
40
+ }>;
41
+ bindingId: z.ZodString;
42
+ }, z.core.$strict>, z.ZodObject<{
43
+ operation: z.ZodLiteral<"list">;
44
+ after: z.ZodOptional<z.ZodString>;
45
+ }, z.core.$strict>], "operation">;
46
+ /**
47
+ * Dispatch the hosts' decoded closed control contract. Hosts decode before
48
+ * resolving installation dependencies; only they supply the current pin and
49
+ * their captured target.
50
+ */
51
+ export declare function workflowWebhookControlResponse<Principal>(webhooks: ReturnType<typeof createWorkflowWebhooks<Principal>>, principal: Principal, command: z.output<typeof workflowWebhookCommandSchema>, authorize: (target: WorkflowTriggerTarget, computerId: string) => Promise<{
52
+ target: WorkflowTriggerTarget;
53
+ authority: WorkflowInstallationAuthority;
54
+ }>): Promise<Response>;
55
+ /**
56
+ * Host-owned composition: custody verifies, Braid reserves/maps/starts, and the
57
+ * host's ordinary idempotent launch ensures its recorded engine instance.
58
+ * No separate intake state machine or run/settlement store exists here.
59
+ */
60
+ export declare function createWorkflowWebhooks<Principal>(options: Parameters<typeof workflowWebhookCustody<Principal>>[0] & {
61
+ scope: Readonly<{
62
+ host: string;
63
+ tenant: string;
64
+ }>;
65
+ runs: Pick<ReturnType<typeof createTeamWorkflowRuns>, "reserveWebhookDelivery" | "pendingWebhookDeliveries" | "completeWebhookDelivery" | "nextWebhookWake" | "graph">;
66
+ triggers: Pick<ReturnType<typeof createTeamWorkflowTriggers>, "arm" | "fire">;
67
+ launch: Parameters<typeof createTeamWorkflowTriggers>[0]["launch"];
68
+ abuse: Omit<Parameters<typeof workflowWebhookAbuse>[0], "scope">;
69
+ limits: WebhookIngressLimits;
70
+ /** Recompute the native alarm from ALL host-owned wake sources. */
71
+ wake(): Promise<void>;
72
+ reportFailure(): void;
73
+ }): {
74
+ receive: (bindingId: string, request: Request) => Promise<Response>;
75
+ nextWake: () => number | null;
76
+ create(principal: Principal, target: WorkflowTriggerTarget, computerId: string, authority: WorkflowInstallationAuthority, verifier: WorkflowWebhookVerifier): Promise<{
77
+ readonly kind: "refused";
78
+ } | {
79
+ kind: "created";
80
+ bindingId: string;
81
+ installationId: string;
82
+ secret: string | null;
83
+ }>;
84
+ rotate: (principal: Principal, bindingId: string) => Promise<{
85
+ readonly kind: "refused";
86
+ } | {
87
+ kind: "rotated";
88
+ secret: string | null;
89
+ }>;
90
+ retirePrevious: (principal: Principal, bindingId: string) => Promise<{
91
+ readonly kind: "refused";
92
+ } | {
93
+ kind: "retired";
94
+ }>;
95
+ revoke: (principal: Principal, bindingId: string) => Promise<{
96
+ readonly kind: "refused";
97
+ } | {
98
+ kind: "revoked";
99
+ }>;
100
+ list: (principal: Principal, after?: string | null) => Promise<{
101
+ bindings: {
102
+ bindingId: string;
103
+ verifier: "fungi-v1" | "github" | "standard-webhooks";
104
+ creator: string;
105
+ generation: number;
106
+ previousExpiresAt: number | null;
107
+ }[];
108
+ next: string | null;
109
+ }>;
110
+ recover(): Promise<void>;
111
+ };