@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.
Files changed (287) hide show
  1. package/README.md +376 -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 +210 -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/pack.d.ts +14 -3
  130. package/dist/pack.js +44 -19
  131. package/dist/page/app-access.d.ts +7 -0
  132. package/dist/page/app-access.js +75 -0
  133. package/dist/page/boot-host.js +57 -0
  134. package/dist/page/capabilities.d.ts +3 -3
  135. package/dist/page/capabilities.js +16 -12
  136. package/dist/page/computer-handles.d.ts +3 -3
  137. package/dist/page/computer-target.d.ts +3 -0
  138. package/dist/page/computer-target.js +9 -0
  139. package/dist/page/mount.d.ts +23 -3
  140. package/dist/page/mount.js +152 -16
  141. package/dist/page-assets.d.ts +22 -0
  142. package/dist/page-assets.js +28 -0
  143. package/dist/page-script.d.ts +3 -0
  144. package/dist/page-script.js +5 -4
  145. package/dist/release.d.ts +13 -6
  146. package/dist/release.js +10 -7
  147. package/dist/root-data.d.ts +16 -6
  148. package/dist/root-data.js +2 -2
  149. package/dist/source/index.d.ts +1 -1
  150. package/dist/source/team-source.d.ts +20 -8
  151. package/dist/source/team-source.js +39 -10
  152. package/dist/source/template-seed.d.ts +2 -2
  153. package/dist/standalone/agent-mail.d.ts +24 -0
  154. package/dist/standalone/agent-mail.js +62 -0
  155. package/dist/standalone/agent-store.d.ts +9 -0
  156. package/dist/standalone/agent-store.js +30 -0
  157. package/dist/standalone/agents.d.ts +68 -9
  158. package/dist/standalone/agents.js +428 -56
  159. package/dist/standalone/app-builds-binding.d.ts +20 -2
  160. package/dist/standalone/app-builds-binding.js +147 -23
  161. package/dist/standalone/app-set.d.ts +5 -2
  162. package/dist/standalone/app-set.js +174 -143
  163. package/dist/standalone/browser.d.ts +11 -0
  164. package/dist/standalone/browser.js +72 -0
  165. package/dist/standalone/builds.d.ts +13 -4
  166. package/dist/standalone/builds.js +28 -44
  167. package/dist/standalone/computer-egress.d.ts +2 -0
  168. package/dist/standalone/computer-egress.js +13 -4
  169. package/dist/standalone/computer-fleet.d.ts +43 -0
  170. package/dist/standalone/computer-fleet.js +200 -0
  171. package/dist/standalone/computer.d.ts +44 -9
  172. package/dist/standalone/computer.js +129 -22
  173. package/dist/standalone/install-apps-cli.js +43 -6
  174. package/dist/standalone/instance.d.ts +2 -2
  175. package/dist/standalone/instance.js +8 -11
  176. package/dist/standalone/machine-link.d.ts +354 -0
  177. package/dist/standalone/machine-link.js +368 -0
  178. package/dist/standalone/media.d.ts +2 -2
  179. package/dist/standalone/media.js +19 -16
  180. package/dist/standalone/preview.d.ts +2 -2
  181. package/dist/standalone/preview.js +37 -10
  182. package/dist/standalone/registry.d.ts +149 -9
  183. package/dist/standalone/registry.js +299 -8
  184. package/dist/standalone/worker.d.ts +19 -5
  185. package/dist/standalone/worker.js +90 -54
  186. package/dist/standalone/workflow-binding.d.ts +18 -22
  187. package/dist/standalone/workflow-binding.js +18 -18
  188. package/dist/standalone/workflow-control.d.ts +4 -0
  189. package/dist/standalone/workflow-control.js +40 -0
  190. package/dist/standalone/workflow-events.d.ts +2 -2
  191. package/dist/standalone/workflow-events.js +9 -10
  192. package/dist/standalone/workflow-git-capture.js +4 -3
  193. package/dist/standalone/workflow-host.d.ts +162 -161
  194. package/dist/standalone/workflow-host.js +101 -21
  195. package/dist/standalone/workflow-node-capture.js +11 -4
  196. package/dist/standalone/workflow-schedules.d.ts +5 -2
  197. package/dist/standalone/workflow-schedules.js +9 -10
  198. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  199. package/dist/standalone/workflow-trigger-host.js +2 -2
  200. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  201. package/dist/standalone/workflow-webhooks.js +39 -0
  202. package/dist/standalone/workflows.d.ts +140 -43
  203. package/dist/standalone/workflows.js +198 -39
  204. package/dist/storage-frame.d.ts +6 -6
  205. package/dist/testing/capabilities.js +14 -3
  206. package/dist/testing/installation.js +26 -2
  207. package/dist/testing/worker.js +1 -1
  208. package/dist/testing/workers-runtime.d.ts +8 -2
  209. package/dist/testing/workers-runtime.js +20 -2
  210. package/dist/webhook-abuse.d.ts +25 -0
  211. package/dist/webhook-abuse.js +54 -0
  212. package/dist/webhook-custody.d.ts +144 -0
  213. package/dist/webhook-custody.js +222 -0
  214. package/dist/webhook-ingress.d.ts +36 -0
  215. package/dist/webhook-ingress.js +146 -0
  216. package/dist/webhooks.d.ts +111 -0
  217. package/dist/webhooks.js +189 -0
  218. package/dist/workers-git.d.ts +12 -17
  219. package/dist/workers-git.js +60 -52
  220. package/dist/workers.d.ts +224 -13
  221. package/dist/workers.js +351 -51
  222. package/dist/workflow-agent-authority.d.ts +8 -0
  223. package/dist/workflow-agent-authority.js +42 -0
  224. package/dist/workflows.d.ts +66 -0
  225. package/dist/workflows.js +97 -0
  226. package/docs/api/README.md +63 -37
  227. package/docs/api/agents.md +17 -0
  228. package/docs/api/app-storage.md +1 -1
  229. package/docs/api/apps.md +1394 -543
  230. package/docs/api/browser/boot.md +132 -0
  231. package/docs/api/browser.md +1440 -0
  232. package/docs/api/build-contract.md +38 -25
  233. package/docs/api/build.md +263 -84
  234. package/docs/api/cloudflare.md +93 -7
  235. package/docs/api/git.md +3 -13
  236. package/docs/api/host.md +944 -118
  237. package/docs/api/index.md +320 -18
  238. package/docs/api/keys.md +24 -1
  239. package/docs/api/mcp.md +59 -12
  240. package/docs/api/modules.md +33 -40
  241. package/docs/api/pack.md +57 -8
  242. package/docs/api/page/mount.md +92 -4
  243. package/docs/api/workers-git.md +103 -24
  244. package/docs/api/workers.md +1581 -244
  245. package/docs/site/agent-quickstart.mdx +28 -14
  246. package/examples/standalone/worker.ts +3 -1
  247. package/examples/standalone/wrangler.jsonc +2 -0
  248. package/package.json +73 -71
  249. package/dist/installation-consent.d.ts +0 -81
  250. package/dist/installation-consent.js +0 -84
  251. package/dist/shell/app-directory.d.ts +0 -542
  252. package/dist/shell/app-directory.js +0 -196
  253. package/dist/shell/browser-actions.d.ts +0 -7
  254. package/dist/shell/browser-actions.js +0 -56
  255. package/dist/shell/command-host.d.ts +0 -112
  256. package/dist/shell/command-host.js +0 -502
  257. package/dist/shell/commands.d.ts +0 -25
  258. package/dist/shell/commands.js +0 -78
  259. package/dist/shell/index.d.ts +0 -8
  260. package/dist/shell/index.js +0 -8
  261. package/dist/shell/keys.d.ts +0 -41
  262. package/dist/shell/keys.js +0 -134
  263. package/dist/shell/shell-module.d.ts +0 -55
  264. package/dist/shell/shell-module.js +0 -86
  265. package/dist/shell/window-manager.d.ts +0 -128
  266. package/dist/shell/window-manager.js +0 -730
  267. package/dist/shell/workbench-contract.d.ts +0 -68
  268. package/dist/shell/workbench-directory.d.ts +0 -332
  269. package/dist/shell/workbench-directory.js +0 -35
  270. package/dist/shell/workbench-host-types.d.ts +0 -96
  271. package/dist/shell/workbench-host-types.js +0 -1
  272. package/dist/shell/workbench-host.d.ts +0 -34
  273. package/dist/shell/workbench-host.js +0 -352
  274. package/dist/shell/workbench-model.d.ts +0 -194
  275. package/dist/shell/workbench-model.js +0 -53
  276. package/dist/shell/workbench.d.ts +0 -4
  277. package/dist/shell/workbench.js +0 -2
  278. package/docs/api/shell/app-directory.md +0 -599
  279. package/docs/api/shell/command-host.md +0 -224
  280. package/docs/api/shell/commands.md +0 -197
  281. package/docs/api/shell/keys.md +0 -183
  282. package/docs/api/shell/shell-module.md +0 -204
  283. package/docs/api/shell/window-manager.md +0 -267
  284. package/docs/api/shell/workbench-directory.md +0 -119
  285. package/docs/api/shell/workbench.md +0 -205
  286. package/docs/api/shell.md +0 -1915
  287. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -0,0 +1,255 @@
1
+ import * as z from "zod";
2
+ import { permissionCatalog } from "./catalog.js";
3
+ import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
4
+ import { appAuthoritySchema, declaredOperationsSchema, ownerCodec, grantCommandSchema, } from "./installation.js";
5
+ /** Includes newly declared operations and capabilities the owner has narrowed. */
6
+ export function pendingCapabilities(authority, declaration, declaredOperations) {
7
+ return declaration.capabilities.filter((capability) => {
8
+ if (permissionCatalog[capability].consent === "affordance")
9
+ return false;
10
+ const grant = authority.approvedGrants.find((row) => row.capability === capability);
11
+ return (grant === undefined ||
12
+ (grant.resources !== "tenant-scope" && grant.resources.length === 0) ||
13
+ declaredOperations.capabilities
14
+ .find((row) => row.capability === capability)
15
+ ?.operations.some((operation) => !grant.operations.includes(operation)) === true);
16
+ });
17
+ }
18
+ /** Bound the real SDK request; an Allow never silently widens selected resources. */
19
+ export function grantRequestFor(authority, facts, request, window) {
20
+ if (authority.declaredOperationsDigest !== facts.declaredOperations.digest)
21
+ return { kind: "refused", code: "stale_authority" };
22
+ const requested = [
23
+ ...new Set([...(request.agents ?? []), ...(request.computers ?? [])]),
24
+ ];
25
+ if (requested.some((capability) => !facts.declaration.capabilities.includes(capability)))
26
+ return { kind: "refused", code: "not_declared" };
27
+ const adds = [];
28
+ for (const capability of requested) {
29
+ const existing = authority.approvedGrants.find((row) => row.capability === capability);
30
+ const operations = facts.declaredOperations.capabilities.find((row) => row.capability === capability)?.operations ?? [];
31
+ if (existing !== undefined &&
32
+ existing.resources !== "tenant-scope" &&
33
+ existing.resources.length > 0) {
34
+ // approvedGrantSchema admits only capabilityApproved: true.
35
+ // A replay can use that approval, but cannot extend its finite scope.
36
+ if (permissionCatalog[capability].scope === "computer" &&
37
+ window.computerId !== null &&
38
+ existing.resources.some((resource) => resource.kind === "computer" && resource.id === window.computerId) &&
39
+ operations.every((operation) => existing.operations.includes(operation)))
40
+ continue;
41
+ return { kind: "customize" };
42
+ }
43
+ if (existing?.resources === "tenant-scope" &&
44
+ operations.every((operation) => existing.operations.includes(operation)))
45
+ continue;
46
+ let resources = "tenant-scope";
47
+ if (permissionCatalog[capability].scope === "computer") {
48
+ // Only the embedding host supplies this fact, never the App's request.
49
+ const computerId = window.computerId;
50
+ if (computerId == null)
51
+ return { kind: "refused", code: "window_scope_unavailable" };
52
+ resources = [{ kind: "computer", id: computerId }];
53
+ }
54
+ adds.push({ capability, grant: { operations, resources } });
55
+ }
56
+ if (adds.length === 0)
57
+ return { kind: "covered" };
58
+ return {
59
+ kind: "ask",
60
+ adds,
61
+ command: { kind: "change-grants", changes: adds },
62
+ };
63
+ }
64
+ /** The saved operation ceiling is the starting point, including empty operation rows. */
65
+ export function draftOf(authority) {
66
+ return new Map(authority.approvedGrants.map(({ capability, operations, resources }) => [
67
+ capability,
68
+ { operations, resources },
69
+ ]));
70
+ }
71
+ function sameGrant(a, b) {
72
+ if (a == null || b == null)
73
+ return a == null && b == null;
74
+ const sameValues = (left, right) => left.length === right.length &&
75
+ left.every((value) => right.includes(value));
76
+ if (!sameValues(a.operations, b.operations))
77
+ return false;
78
+ if (a.resources === "tenant-scope" || b.resources === "tenant-scope")
79
+ return a.resources === b.resources;
80
+ return (a.resources.length === b.resources.length &&
81
+ a.resources.every((resource) => b.resources !== "tenant-scope" &&
82
+ b.resources.some((other) => resource.kind === other.kind && resource.id === other.id)));
83
+ }
84
+ /** An edit changes one capability; unrelated saved operations and scopes are retained. */
85
+ export function editPermissions(draft, edit, declared) {
86
+ const row = declared.capabilities.find(({ capability }) => capability === edit.capability);
87
+ const policy = permissionCatalog[edit.capability];
88
+ if (row === undefined || policy.consent === "affordance")
89
+ return draft;
90
+ const next = new Map(draft);
91
+ const existing = draft.get(edit.capability);
92
+ const enabled = existing ?? {
93
+ operations: row.operations,
94
+ resources: "tenant-scope",
95
+ };
96
+ switch (edit.kind) {
97
+ case "switch":
98
+ next.set(edit.capability, edit.on ? enabled : null);
99
+ break;
100
+ case "all":
101
+ if (policy.scope === null)
102
+ return draft;
103
+ next.set(edit.capability, edit.on ? { ...enabled, resources: "tenant-scope" } : null);
104
+ break;
105
+ case "resource": {
106
+ if (policy.scope !== edit.resource.kind ||
107
+ existing?.resources === "tenant-scope")
108
+ return draft;
109
+ const resources = existing?.resources ?? [];
110
+ const kept = resources.filter((resource) => resource.kind !== edit.resource.kind ||
111
+ resource.id !== edit.resource.id);
112
+ const changed = edit.on ? [...kept, edit.resource] : kept;
113
+ next.set(edit.capability, changed.length === 0 ? null : { ...enabled, resources: changed });
114
+ break;
115
+ }
116
+ case "operation": {
117
+ if (edit.capability !== "apps.call" ||
118
+ !row.operations.includes(edit.operation))
119
+ return draft;
120
+ const kept = (existing?.operations ?? []).filter((operation) => operation !== edit.operation);
121
+ const operations = edit.on ? [...kept, edit.operation] : kept;
122
+ next.set(edit.capability, operations.length === 0 ? null : { ...enabled, operations });
123
+ break;
124
+ }
125
+ }
126
+ return next;
127
+ }
128
+ /** Only touched capability rows cross the save boundary. */
129
+ export function planGrantChange(baseline, draft) {
130
+ const saved = draftOf(baseline);
131
+ const capabilities = new Set([...saved.keys(), ...draft.keys()]);
132
+ return [...capabilities].flatMap((capability) => sameGrant(saved.get(capability), draft.get(capability))
133
+ ? []
134
+ : [{ capability, grant: draft.get(capability) ?? null }]);
135
+ }
136
+ /** A conflict preserves the person's edits while taking fresh untouched capabilities. */
137
+ export function rebaseDraft(draft, baseline, current) {
138
+ const rebased = new Map(draftOf(current));
139
+ for (const change of planGrantChange(baseline, draft))
140
+ rebased.set(change.capability, change.grant);
141
+ return rebased;
142
+ }
143
+ /** Serializable host facts; the browser derives presentation, never authority. */
144
+ export const appPermissionsWireSchema = z.strictObject({
145
+ authority: appAuthoritySchema,
146
+ declaredOperations: declaredOperationsSchema,
147
+ });
148
+ /** Every declared permission is visible, including removable saved resources no longer listed by the host. */
149
+ export function permissionScreen(input) {
150
+ const pending = pendingCapabilities(input.authority, {
151
+ capabilities: input.declaredOperations.capabilities.map((row) => row.capability),
152
+ }, input.declaredOperations);
153
+ const draft = draftOf(input.authority);
154
+ const grouped = new Map();
155
+ for (const { capability } of input.declaredOperations.capabilities) {
156
+ const group = permissionCatalog[capability].group;
157
+ grouped.set(group, [...(grouped.get(group) ?? []), capability]);
158
+ }
159
+ const column = (capability) => {
160
+ const { label, caption, description, sigil, warning } = permissionCatalog[capability];
161
+ return {
162
+ capability,
163
+ label,
164
+ caption,
165
+ description,
166
+ sigil,
167
+ warning,
168
+ pending: pending.includes(capability),
169
+ };
170
+ };
171
+ return {
172
+ canEdit: input.canEdit,
173
+ pending,
174
+ groups: [...grouped].map(([id, capabilities]) => {
175
+ const scoped = capabilities.filter((capability) => permissionCatalog[capability].scope !== null);
176
+ const scope = scoped.length === 0
177
+ ? null
178
+ : permissionCatalog[scoped[0] ?? "apps.open"].scope;
179
+ const resources = new Map(input.resources
180
+ .filter((resource) => resource.kind === scope)
181
+ .map((resource) => [resource.id, resource]));
182
+ for (const capability of scoped) {
183
+ const grant = draft.get(capability);
184
+ if (grant == null || grant.resources === "tenant-scope")
185
+ continue;
186
+ for (const resource of grant.resources) {
187
+ if (resource.kind === scope && !resources.has(resource.id))
188
+ resources.set(resource.id, { ...resource, label: resource.id });
189
+ }
190
+ }
191
+ const row = (resource, label) => ({
192
+ resource,
193
+ label,
194
+ cells: scoped.map((capability) => {
195
+ const grant = draft.get(capability);
196
+ const state = grant == null
197
+ ? "off"
198
+ : grant.resources === "tenant-scope"
199
+ ? resource === "all"
200
+ ? "on"
201
+ : "via-all"
202
+ : resource !== "all" &&
203
+ grant.resources.some((ref) => ref.kind === resource.kind && ref.id === resource.id)
204
+ ? "on"
205
+ : "off";
206
+ return { capability, state };
207
+ }),
208
+ });
209
+ return {
210
+ id,
211
+ scope,
212
+ columns: scoped.map(column),
213
+ rows: scope === null
214
+ ? []
215
+ : [
216
+ row("all", scope === "computer" ? "All Computers" : "All Agents"),
217
+ ...[...resources.values()].map((resource) => row(resource, resource.label)),
218
+ ],
219
+ switches: capabilities
220
+ .filter((capability) => permissionCatalog[capability].scope === null)
221
+ .map((capability) => ({
222
+ ...column(capability),
223
+ on: permissionCatalog[capability].consent === "affordance" ||
224
+ draft.get(capability) != null,
225
+ })),
226
+ operations: capabilities.includes("apps.call")
227
+ ? (input.declaredOperations.capabilities.find((capability) => capability.capability === "apps.call")?.operations ?? []).map((operation) => ({
228
+ capability: "apps.call",
229
+ operation,
230
+ on: draft.get("apps.call")?.operations.includes(operation) === true,
231
+ }))
232
+ : [],
233
+ };
234
+ }),
235
+ };
236
+ }
237
+ /** Owner-direct permission page and JSON projection. Copy stays in the catalog. */
238
+ export const permissionPinsSchema = z.strictObject({
239
+ storeGeneration: z.string().min(1),
240
+ authorityVersion: z.number().int().positive(),
241
+ artifactDigest: ownerCodec(sha256DigestSchema),
242
+ });
243
+ /** Permission edit command coupled to the version pins read by the owner. */
244
+ export const permissionSaveSchema = permissionPinsSchema.extend({
245
+ command: grantCommandSchema,
246
+ });
247
+ /** Owner permission-page identity, artifact and current permission facts. */
248
+ export const appPermissionPageSchema = z.strictObject({
249
+ storeGeneration: permissionPinsSchema.shape.storeGeneration,
250
+ installationId: z.string(),
251
+ appId: z.string(),
252
+ appName: z.string(),
253
+ artifactDigest: ownerCodec(sha256DigestSchema),
254
+ permissions: appPermissionsWireSchema,
255
+ });
@@ -0,0 +1,15 @@
1
+ import { type BootCapabilityStore } from "./boot-capability.js";
2
+ /** The artifact bucket read the route needs; an R2 bucket is one. */
3
+ export type BootAssetBucket = Readonly<{
4
+ get(key: string): Promise<Readonly<{
5
+ size: number;
6
+ body: ReadableStream<Uint8Array>;
7
+ }> | null>;
8
+ }>;
9
+ export type BootCapabilityRouteOptions = Readonly<{
10
+ configuredOrigin: string;
11
+ bucket: BootAssetBucket;
12
+ capabilities: BootCapabilityStore;
13
+ }>;
14
+ /** Worker-only HTTPS endpoint for a single digest-authorized boot lease. */
15
+ export declare function serveBootCapability(request: Request, options: BootCapabilityRouteOptions): Promise<Response | null>;
@@ -0,0 +1,196 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parseReleaseId } from "@fungi.computer/puffball";
3
+ import { z } from "zod";
4
+ import { BOOT_CAPABILITY_HEADER, } from "./boot-capability.js";
5
+ import {} from "./boot-gate.js";
6
+ import { defaultAssetPolicy } from "./release.js";
7
+ import { bootAssetUrl, bootHostScript, bootParentDocument, candidateAssetUrls, candidateSecurityHeaders, parentSecurityHeaders, } from "./boot-mount.js";
8
+ const tokenSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
9
+ const candidatePreflightPathSchema = z
10
+ .tuple([
11
+ z.literal("_boot"),
12
+ z.string(),
13
+ z.literal("candidate"),
14
+ z.string().min(1),
15
+ ])
16
+ .rest(z.string().min(1));
17
+ const capabilityPreflightSchema = z.object({
18
+ method: z.literal("OPTIONS"),
19
+ search: z.literal(""),
20
+ origin: z.literal("null"),
21
+ requestedMethod: z.literal("GET"),
22
+ requestedHeaders: z
23
+ .string()
24
+ .trim()
25
+ .toLowerCase()
26
+ .pipe(z.literal(BOOT_CAPABILITY_HEADER)),
27
+ path: candidatePreflightPathSchema,
28
+ });
29
+ /** Worker-only HTTPS endpoint for a single digest-authorized boot lease. */
30
+ export async function serveBootCapability(request, options) {
31
+ const url = new URL(request.url);
32
+ if (!url.pathname.startsWith("/_boot/"))
33
+ return null;
34
+ const configured = new URL(options.configuredOrigin);
35
+ // The same origins checkBootPage opens: https, or http on loopback only.
36
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(configured.hostname);
37
+ if ((configured.protocol !== "https:" &&
38
+ !(configured.protocol === "http:" && loopback)) ||
39
+ configured.pathname !== "/" ||
40
+ configured.search !== "" ||
41
+ configured.hash !== "" ||
42
+ configured.username !== "" ||
43
+ configured.password !== "" ||
44
+ url.origin !== configured.origin)
45
+ return refusal(404);
46
+ const segments = url.pathname.split("/");
47
+ if (isCapabilityPreflight(request, url, segments))
48
+ return capabilityPreflight();
49
+ const token = tokenSchema.safeParse(request.headers.get(BOOT_CAPABILITY_HEADER));
50
+ if (request.method !== "GET" ||
51
+ url.search !== "" ||
52
+ segments[2] === "" ||
53
+ !token.success ||
54
+ segments.length < 4 ||
55
+ segments[0] !== "" ||
56
+ segments[1] !== "_boot")
57
+ return refusal(404);
58
+ const tokenDigest = createHash("sha256")
59
+ .update(token.data, "utf8")
60
+ .digest("hex");
61
+ const lease = await options.capabilities.resolve(tokenDigest, Date.now());
62
+ if (lease === null)
63
+ return refusal(404);
64
+ // The capability store returns a lease already decoded at its persistence boundary.
65
+ const checkedLease = lease;
66
+ if (checkedLease.identity.releaseId !== segments[2])
67
+ return refusal(404);
68
+ const assets = checkedLease.assets.map(bootUiAsset);
69
+ const ui = {
70
+ identity: checkedLease.identity,
71
+ base: checkedLease.base,
72
+ assets,
73
+ };
74
+ const root = new URL(`/_boot/${checkedLease.identity.releaseId}/`, configured.origin);
75
+ const candidateBase = new URL("candidate/", root);
76
+ const urls = candidateAssetUrls(ui, candidateBase.href);
77
+ const entry = bootAssetUrl(candidateBase.href, checkedLease.base, "/index.html");
78
+ if (Date.now() >= checkedLease.expiresAt)
79
+ return refusal(404);
80
+ const parentPath = `${root.pathname}index.html`;
81
+ const bridgePath = `${root.pathname}bridge.js`;
82
+ if (url.pathname === parentPath) {
83
+ return new Response(bootParentDocument(checkedLease.identity, entry), {
84
+ status: 200,
85
+ headers: {
86
+ ...parentSecurityHeaders(candidateBase.href),
87
+ "content-type": "text/html; charset=utf-8",
88
+ },
89
+ });
90
+ }
91
+ if (url.pathname === bridgePath) {
92
+ return new Response(bootHostScript, {
93
+ status: 200,
94
+ headers: {
95
+ ...parentSecurityHeaders(candidateBase.href),
96
+ "content-type": "text/javascript; charset=utf-8",
97
+ },
98
+ });
99
+ }
100
+ const assetIndex = urls.findIndex((assetUrl) => {
101
+ const asset = new URL(assetUrl);
102
+ return asset.origin === url.origin && asset.pathname === url.pathname;
103
+ });
104
+ const asset = checkedLease.assets[assetIndex];
105
+ if (asset === undefined)
106
+ return refusal(404);
107
+ if (Date.now() >= checkedLease.expiresAt)
108
+ return refusal(404);
109
+ const object = await options.bucket.get(asset.artifactRef);
110
+ if (object?.size !== asset.byteLength)
111
+ return refusal(404);
112
+ const headers = new Headers({
113
+ ...candidateSecurityHeaders(candidateBase.href),
114
+ "content-type": asset.contentType,
115
+ "x-content-type-options": "nosniff",
116
+ });
117
+ if (request.headers.get("origin") === "null") {
118
+ headers.set("access-control-allow-origin", "null");
119
+ headers.set("vary", "Origin");
120
+ }
121
+ const hash = createHash("sha256");
122
+ let byteLength = 0;
123
+ const verified = object.body.pipeThrough(new TransformStream({
124
+ transform(chunk, controller) {
125
+ if (Date.now() >= checkedLease.expiresAt) {
126
+ controller.error(new Error("Boot asset lease expired"));
127
+ return;
128
+ }
129
+ byteLength += chunk.byteLength;
130
+ if (byteLength > defaultAssetPolicy.maxObjectBytes) {
131
+ controller.error(new Error("Boot asset exceeds its size limit"));
132
+ return;
133
+ }
134
+ hash.update(chunk);
135
+ controller.enqueue(chunk);
136
+ },
137
+ flush(controller) {
138
+ if (byteLength !== asset.byteLength ||
139
+ `sha256:${hash.digest("hex")}` !== asset.digest)
140
+ controller.error(new Error("Boot asset verification failed"));
141
+ },
142
+ }));
143
+ return new Response(verified, { status: 200, headers });
144
+ }
145
+ function isCapabilityPreflight(request, url, segments) {
146
+ const preflight = capabilityPreflightSchema.safeParse({
147
+ method: request.method,
148
+ search: url.search,
149
+ origin: request.headers.get("origin"),
150
+ requestedMethod: request.headers.get("access-control-request-method"),
151
+ requestedHeaders: request.headers.get("access-control-request-headers"),
152
+ path: segments.slice(1),
153
+ });
154
+ if (!preflight.success)
155
+ return false;
156
+ try {
157
+ parseReleaseId(preflight.data.path[1]);
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ function capabilityPreflight() {
165
+ return new Response(null, {
166
+ status: 204,
167
+ headers: {
168
+ "access-control-allow-origin": "null",
169
+ "access-control-allow-methods": "GET",
170
+ "access-control-allow-headers": BOOT_CAPABILITY_HEADER,
171
+ "cache-control": "no-store",
172
+ "referrer-policy": "no-referrer",
173
+ vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
174
+ "x-content-type-options": "nosniff",
175
+ },
176
+ });
177
+ }
178
+ function bootUiAsset(asset) {
179
+ return {
180
+ path: asset.path,
181
+ contentType: asset.contentType,
182
+ artifactRef: asset.artifactRef,
183
+ digest: asset.digest,
184
+ };
185
+ }
186
+ function refusal(status) {
187
+ return new Response("Boot resource unavailable", {
188
+ status,
189
+ headers: {
190
+ "cache-control": "no-store",
191
+ "content-type": "text/plain; charset=utf-8",
192
+ "referrer-policy": "no-referrer",
193
+ "x-content-type-options": "nosniff",
194
+ },
195
+ });
196
+ }
@@ -0,0 +1,103 @@
1
+ import { z } from "zod";
2
+ import { type BootUi, type UiBootCheckOutcome } from "./boot-gate.js";
3
+ import type { BootPageResult, BootPageTarget } from "./browser/boot.js";
4
+ export declare const BOOT_CAPABILITY_HEADER = "x-oyster-boot-capability";
5
+ declare const bootCapabilityAssetSchema: z.ZodObject<{
6
+ path: z.ZodString;
7
+ contentType: z.ZodString;
8
+ artifactRef: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").ArtifactRef, string>>;
9
+ digest: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, string>>;
10
+ byteLength: z.ZodNumber;
11
+ }, z.core.$strict>;
12
+ export declare const bootCapabilityLeaseSchema: z.ZodObject<{
13
+ id: z.ZodString;
14
+ identity: z.ZodObject<{
15
+ appId: z.ZodString;
16
+ releaseId: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").ReleaseId, string>>;
17
+ surfaceId: z.ZodLiteral<"host-boot">;
18
+ }, z.core.$strict>;
19
+ base: z.ZodString;
20
+ expiresAt: z.ZodNumber;
21
+ assets: z.ZodArray<z.ZodObject<{
22
+ path: z.ZodString;
23
+ contentType: z.ZodString;
24
+ artifactRef: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").ArtifactRef, string>>;
25
+ digest: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, string>>;
26
+ byteLength: z.ZodNumber;
27
+ }, z.core.$strict>>;
28
+ }, z.core.$strict>;
29
+ export declare const bootCapabilityGrantSchema: z.ZodObject<{
30
+ lease: z.ZodObject<{
31
+ id: z.ZodString;
32
+ identity: z.ZodObject<{
33
+ appId: z.ZodString;
34
+ releaseId: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").ReleaseId, string>>;
35
+ surfaceId: z.ZodLiteral<"host-boot">;
36
+ }, z.core.$strict>;
37
+ base: z.ZodString;
38
+ expiresAt: z.ZodNumber;
39
+ assets: z.ZodArray<z.ZodObject<{
40
+ path: z.ZodString;
41
+ contentType: z.ZodString;
42
+ artifactRef: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").ArtifactRef, string>>;
43
+ digest: z.ZodPipe<z.ZodString, z.ZodTransform<import("@fungi.computer/puffball").Sha256Digest, string>>;
44
+ byteLength: z.ZodNumber;
45
+ }, z.core.$strict>>;
46
+ }, z.core.$strict>;
47
+ tokenDigest: z.ZodString;
48
+ }, z.core.$strict>;
49
+ export type BootCapabilityAsset = z.output<typeof bootCapabilityAssetSchema>;
50
+ export type BootCapabilityLease = z.output<typeof bootCapabilityLeaseSchema>;
51
+ export type BootCapabilityGrant = z.output<typeof bootCapabilityGrantSchema>;
52
+ /** Host-owned bounded Durable Object port. No raw token crosses this seam. */
53
+ export type BootCapabilityStore = Readonly<{
54
+ issue(grant: BootCapabilityGrant): Promise<void>;
55
+ resolve(tokenDigest: string, now: number): Promise<BootCapabilityLease | null>;
56
+ revoke(id: string): Promise<void>;
57
+ }>;
58
+ /** Build metadata retained by the lease must exactly match the verified boot UI. */
59
+ export declare function capabilityAssets(assets: BootUi["assets"], byteLengths: readonly number[]): BootCapabilityAsset[];
60
+ export declare function validateCapabilityAssetSet(lease: BootCapabilityLease): void;
61
+ /** The artifact bucket's metadata read; asset bytes are never read here. */
62
+ export type BootCapabilityBucket = Readonly<{
63
+ head(key: string): Promise<Readonly<{
64
+ size: number;
65
+ }> | null>;
66
+ }>;
67
+ /**
68
+ * Mint one boot capability for every host: a fresh 256-bit token for the
69
+ * caller's request header, only its SHA-256 digest in the store, and a lease
70
+ * whose asset sizes come from the bucket's own metadata. An asset the bucket
71
+ * does not hold, or one beyond the asset policy, refuses before anything is
72
+ * stored.
73
+ */
74
+ export declare function issueBootCapability(input: Readonly<{
75
+ ui: BootUi;
76
+ bucket: BootCapabilityBucket;
77
+ capabilities: BootCapabilityStore;
78
+ expiresAt: number;
79
+ }>): Promise<Readonly<{
80
+ token: string;
81
+ leaseId: string;
82
+ }>>;
83
+ /** What one boot page report needs from its host. */
84
+ export type BootPageReportInput = Readonly<{
85
+ /** The host's configured origin for its `/_boot/` route, or null when unset. */
86
+ origin: string | null;
87
+ /** `checkBootPage` over the host's browser, or null without one. */
88
+ check: ((target: BootPageTarget) => Promise<BootPageResult>) | null;
89
+ /** The Release's UI as artifact references, or null when it has no UI. */
90
+ readUi: () => Promise<BootUi | null>;
91
+ bucket: BootCapabilityBucket;
92
+ capabilities: BootCapabilityStore;
93
+ now: () => number;
94
+ }>;
95
+ /**
96
+ * One UI page check for a Release, for every host, reading no UI bytes:
97
+ * issue a capability, open the pinned boot page, revoke the capability, and
98
+ * report in the boot gate's vocabulary. A page that did not mount fails,
99
+ * with diagnostics sanitized by the gate's schema; a timeout, unconfirmed
100
+ * cleanup, an unavailable runner, or no browser or origin is not checked.
101
+ */
102
+ export declare function reportBootPage(input: BootPageReportInput): Promise<UiBootCheckOutcome>;
103
+ export {};