@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,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 {};