@fungi.computer/oyster 0.5.0 → 0.7.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 (185) hide show
  1. package/README.md +155 -63
  2. package/dist/agent-archive-projection.d.ts +8 -0
  3. package/dist/agent-archive-projection.js +49 -0
  4. package/dist/agent-schedules.d.ts +50 -0
  5. package/dist/agent-schedules.js +17 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +97 -144
  8. package/dist/app-builds.d.ts +7 -2
  9. package/dist/app-builds.js +19 -2
  10. package/dist/approval-capability.d.ts +1 -1
  11. package/dist/approval-capability.js +11 -16
  12. package/dist/approval-home.d.ts +2 -1
  13. package/dist/approval-home.js +5 -3
  14. package/dist/apps/catalog.js +1 -1
  15. package/dist/apps/gate.d.ts +4 -7
  16. package/dist/apps/gate.js +14 -29
  17. package/dist/apps/index.d.ts +1 -1
  18. package/dist/apps/installation.d.ts +4 -42
  19. package/dist/apps/installation.js +7 -75
  20. package/dist/apps/operations.js +0 -10
  21. package/dist/apps/permissions.d.ts +18 -8
  22. package/dist/apps/permissions.js +7 -1
  23. package/dist/auth.js +9 -1
  24. package/dist/authorization.d.ts +307 -0
  25. package/dist/authorization.js +542 -0
  26. package/dist/boot-host-script.js +1 -1
  27. package/dist/build/build-jobs.d.ts +2 -2
  28. package/dist/build/candidate-builder.d.ts +2 -0
  29. package/dist/build/candidate-builder.js +5 -1
  30. package/dist/build/candidate-cli.js +3 -0
  31. package/dist/build/candidate-cli.mjs +67 -22
  32. package/dist/build/job-log.d.ts +23 -0
  33. package/dist/build/job-log.js +88 -0
  34. package/dist/build/local-build.d.ts +2 -0
  35. package/dist/build/local-build.js +11 -9
  36. package/dist/channels/binding.d.ts +8 -1
  37. package/dist/channels/binding.js +12 -0
  38. package/dist/channels/conversations.d.ts +16 -5
  39. package/dist/channels/visitor-channel.d.ts +53 -0
  40. package/dist/channels/visitor-channel.js +124 -0
  41. package/dist/channels/visitor-edge.d.ts +115 -0
  42. package/dist/channels/visitor-edge.js +582 -0
  43. package/dist/channels/visitor-request.d.ts +7 -0
  44. package/dist/channels/visitor-request.js +17 -0
  45. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  46. package/dist/channels/visitor-stream-slots.js +33 -0
  47. package/dist/channels/visitors.d.ts +47 -0
  48. package/dist/channels/visitors.js +181 -0
  49. package/dist/cloudflare.d.ts +11 -2
  50. package/dist/cloudflare.js +27 -1
  51. package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
  52. package/dist/{page/computer-target.js → computer-target.js} +3 -1
  53. package/dist/files.js +9 -8
  54. package/dist/git-admission.d.ts +3 -3
  55. package/dist/git-admission.js +4 -6
  56. package/dist/git-door.d.ts +8 -7
  57. package/dist/git-door.js +18 -17
  58. package/dist/git.d.ts +1 -1
  59. package/dist/host.d.ts +38 -42
  60. package/dist/host.js +182 -239
  61. package/dist/installation-execution.js +10 -5
  62. package/dist/installation-operations.d.ts +4 -180
  63. package/dist/installation-operations.js +1 -5
  64. package/dist/installation-store-generation.d.ts +4 -1
  65. package/dist/installation-store-generation.js +59 -6
  66. package/dist/internal/delivery.js +0 -1
  67. package/dist/internal/host-gate.d.ts +9 -6
  68. package/dist/internal/host-gate.js +19 -25
  69. package/dist/internal/invocation-timing.d.ts +16 -0
  70. package/dist/internal/invocation-timing.js +23 -0
  71. package/dist/internal/registry-installations.js +1 -0
  72. package/dist/invocation.d.ts +19 -11
  73. package/dist/invocation.js +171 -96
  74. package/dist/keys.d.ts +2 -19
  75. package/dist/keys.js +1 -37
  76. package/dist/mcp.d.ts +15 -27
  77. package/dist/mcp.js +105 -155
  78. package/dist/page/agent-browser.d.ts +15 -0
  79. package/dist/page/agent-browser.js +162 -0
  80. package/dist/page/app-permissions-port.d.ts +3 -0
  81. package/dist/page/app-permissions-port.js +43 -0
  82. package/dist/page/app-permissions.d.ts +11 -0
  83. package/dist/page/app-permissions.js +296 -0
  84. package/dist/page/capabilities.js +1 -1
  85. package/dist/page/child-selection.d.ts +27 -0
  86. package/dist/page/child-selection.js +38 -0
  87. package/dist/page/isolated-page-theme.d.ts +2 -0
  88. package/dist/page/isolated-page-theme.js +11 -0
  89. package/dist/page/mount.d.ts +2 -13
  90. package/dist/page/mount.js +8 -94
  91. package/dist/page-script.d.ts +0 -3
  92. package/dist/page-script.js +3 -4
  93. package/dist/release.d.ts +13 -0
  94. package/dist/release.js +10 -0
  95. package/dist/request-origin.d.ts +4 -0
  96. package/dist/request-origin.js +10 -0
  97. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  98. package/dist/standalone/agent-archive-owner.js +116 -0
  99. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  100. package/dist/standalone/agent-archive-schedules.js +76 -0
  101. package/dist/standalone/agent-archive.d.ts +31 -0
  102. package/dist/standalone/agent-archive.js +67 -0
  103. package/dist/standalone/agent-computers.d.ts +7 -6
  104. package/dist/standalone/agent-computers.js +4 -7
  105. package/dist/standalone/agent-mail.d.ts +2 -2
  106. package/dist/standalone/agents.d.ts +157 -20
  107. package/dist/standalone/agents.js +242 -38
  108. package/dist/standalone/app-builds-binding.js +1 -1
  109. package/dist/standalone/app-set.js +2 -54
  110. package/dist/standalone/app-source.d.ts +3 -4
  111. package/dist/standalone/app-source.js +12 -9
  112. package/dist/standalone/builds.d.ts +2 -14
  113. package/dist/standalone/builds.js +15 -13
  114. package/dist/standalone/computer-fleet.d.ts +4 -4
  115. package/dist/standalone/computer-fleet.js +20 -43
  116. package/dist/standalone/computer.d.ts +4 -3
  117. package/dist/standalone/computer.js +52 -53
  118. package/dist/standalone/dashboard-approval.d.ts +10 -0
  119. package/dist/standalone/dashboard-approval.js +66 -0
  120. package/dist/standalone/machine-link.d.ts +4 -221
  121. package/dist/standalone/machine-link.js +21 -15
  122. package/dist/standalone/media.d.ts +1 -0
  123. package/dist/standalone/media.js +20 -14
  124. package/dist/standalone/owner-routes.d.ts +16 -0
  125. package/dist/standalone/owner-routes.js +21 -0
  126. package/dist/standalone/permission-resources.d.ts +5 -0
  127. package/dist/standalone/permission-resources.js +31 -0
  128. package/dist/standalone/registry.d.ts +7 -0
  129. package/dist/standalone/registry.js +3 -0
  130. package/dist/standalone/tenancy.js +3 -1
  131. package/dist/standalone/worker-routes.d.ts +19 -0
  132. package/dist/standalone/worker-routes.js +9 -0
  133. package/dist/standalone/worker.d.ts +1 -1
  134. package/dist/standalone/worker.js +27 -35
  135. package/dist/standalone/workflow-binding.d.ts +8 -3
  136. package/dist/standalone/workflow-binding.js +14 -5
  137. package/dist/standalone/workflow-git-capture.js +2 -1
  138. package/dist/standalone/workflow-host.d.ts +8 -103
  139. package/dist/standalone/workflow-host.js +1 -1
  140. package/dist/standalone/workflow-node-capture.js +2 -2
  141. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  142. package/dist/standalone/workflow-root-authority.js +1 -1
  143. package/dist/standalone/workflow-schedules.d.ts +14 -5
  144. package/dist/standalone/workflow-schedules.js +11 -5
  145. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  146. package/dist/standalone/workflow-webhooks.js +5 -5
  147. package/dist/standalone/workflows.d.ts +36 -28
  148. package/dist/standalone/workflows.js +6 -3
  149. package/dist/storage-frame.d.ts +6 -6
  150. package/dist/testing/capabilities.d.ts +7 -6
  151. package/dist/testing/capabilities.js +3 -6
  152. package/dist/testing/installation.d.ts +0 -1
  153. package/dist/testing/installation.js +2 -5
  154. package/dist/turnstile.d.ts +27 -0
  155. package/dist/turnstile.js +51 -0
  156. package/dist/workers-git.d.ts +12 -4
  157. package/dist/workers-git.js +58 -36
  158. package/dist/workers.d.ts +9 -1
  159. package/dist/workers.js +59 -20
  160. package/dist/workflows.d.ts +2 -0
  161. package/dist/workflows.js +1 -0
  162. package/docs/api/README.md +7 -0
  163. package/docs/api/agent-schedules.md +175 -0
  164. package/docs/api/agents.md +34 -13
  165. package/docs/api/app-storage.md +1 -1
  166. package/docs/api/apps.md +323 -225
  167. package/docs/api/authorization.md +1300 -0
  168. package/docs/api/build-contract.md +2 -2
  169. package/docs/api/build.md +40 -37
  170. package/docs/api/cloudflare.md +27 -2
  171. package/docs/api/git.md +20 -11
  172. package/docs/api/host.md +80 -199
  173. package/docs/api/index.md +1 -1
  174. package/docs/api/invocation.md +15 -11
  175. package/docs/api/keys.md +1 -98
  176. package/docs/api/mcp.md +43 -40
  177. package/docs/api/modules.md +2 -0
  178. package/docs/api/page/mount.md +16 -19
  179. package/docs/api/workers-git.md +46 -12
  180. package/docs/api/workers.md +125 -235
  181. package/docs/site/agent-quickstart.mdx +6 -2
  182. package/examples/standalone/wrangler.celld.jsonc +66 -0
  183. package/package.json +64 -21
  184. package/dist/standalone/channel-session-door.d.ts +0 -4
  185. package/dist/standalone/channel-session-door.js +0 -42
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Oyster's one authorization owner. Every Oyster HTTP route is registered
3
+ * through {@link routes} with a typed {@link Requirement}; a table's `serve`
4
+ * is the only place those declarations become a Hono router, so an undeclared
5
+ * route cannot be served and a declared one cannot skip its decision.
6
+ *
7
+ * Who may reach what is decided here once, from one {@link Authority}: the
8
+ * owner reaches everything; a named key reaches exactly its Ear Tag grants
9
+ * (Apps, Agents, the Computer); an Agent reaches its App tool policy. An
10
+ * anonymous request reaches only the public routes: sign-in, robots.txt and
11
+ * digest-pinned assets. The App gate
12
+ * (`apps/gate.ts`) still owns App-declaration rules; it asks this module for a
13
+ * principal's App reach.
14
+ */
15
+ import { Hono } from "hono";
16
+ import { timing } from "hono/timing";
17
+ import * as Schema from "effect/Schema";
18
+ import { oysterKeyVocabulary } from "./keys.js";
19
+ /**
20
+ * One live key's access to one installation: `write` when its grants admit
21
+ * `apps.use` there, `read` when they admit `apps.read`, otherwise `null` --
22
+ * including for an installation added after the key was issued, unless the
23
+ * key holds "All Apps" (`targets: {kind: "all"}`). The same shape as an
24
+ * Agent's stored {@link AppToolPolicy}; its caller adds `bindingName` (which
25
+ * varies by caller: the plain installation id for the gate, the uppercased
26
+ * binding name for MCP) to reach the full `AppToolAccess`.
27
+ */
28
+ export function keyAppAccess(key, installationId) {
29
+ const level = oysterKeyVocabulary.grantsAllow(key.grants, "apps.use", installationId)
30
+ ? "write"
31
+ : oysterKeyVocabulary.grantsAllow(key.grants, "apps.read", installationId)
32
+ ? "read"
33
+ : null;
34
+ if (level === null)
35
+ return null;
36
+ return {
37
+ level,
38
+ revision: `key:${key.keyId}:${String(key.grantVersion)}`,
39
+ grantedBy: key.owner,
40
+ };
41
+ }
42
+ /** Agent access is scoped to the granted Agent ids, never inferred from App grants. */
43
+ export function keyAgentAccess(key, agentId) {
44
+ return oysterKeyVocabulary.grantsAllow(key.grants, "agents.use", agentId);
45
+ }
46
+ /** Whether a live key's own grants turn its Computer access on. Off by default. */
47
+ export function keyAllowsComputerUse(key) {
48
+ return oysterKeyVocabulary.grantsAllow(key.grants, "computer.use");
49
+ }
50
+ /** True only for a member admitted with the owner role. */
51
+ export function oysterPrincipalIsOwner(principal) {
52
+ return "role" in principal && principal.role === "owner";
53
+ }
54
+ /** A host's own ports: its key store's liveness and its Agents' tool policy. */
55
+ export function hostAuthorityPorts(host) {
56
+ return {
57
+ liveKey: async (keyId) => (await host.keys?.live(keyId)) ?? null,
58
+ appToolPolicy: async (actor) => (await host.appToolPolicy?.(actor)) ?? null,
59
+ };
60
+ }
61
+ /** Resolve one principal's authority through the live key port. */
62
+ export async function authorityOf(principal, ports) {
63
+ if (oysterPrincipalIsOwner(principal))
64
+ return { kind: "owner", principal };
65
+ if ("role" in principal)
66
+ return { kind: "member", principal };
67
+ const { actor } = principal;
68
+ switch (actor.kind) {
69
+ case "team-key":
70
+ return {
71
+ kind: "key",
72
+ principal,
73
+ key: await ports.liveKey(actor.keyId),
74
+ };
75
+ case "agent":
76
+ return { kind: "agent", principal, actor };
77
+ }
78
+ }
79
+ /** Whether this authority reaches one Agent and its Sessions. Only the owner and granted keys do. */
80
+ export function reachesAgent(authority, agentId) {
81
+ switch (authority.kind) {
82
+ case "owner":
83
+ return true;
84
+ case "key":
85
+ return authority.key !== null && keyAgentAccess(authority.key, agentId);
86
+ case "member":
87
+ case "agent":
88
+ return false;
89
+ }
90
+ }
91
+ /**
92
+ * Whether this authority reaches the host's Computer, its workspace files and
93
+ * the media written there. Members do; a key only with `computer.use`;
94
+ * Agents reach a Computer only through their own attachment owner.
95
+ */
96
+ export function reachesComputer(authority) {
97
+ switch (authority.kind) {
98
+ case "owner":
99
+ case "member":
100
+ return true;
101
+ case "key":
102
+ return authority.key !== null && keyAllowsComputerUse(authority.key);
103
+ case "agent":
104
+ return false;
105
+ }
106
+ }
107
+ /**
108
+ * A key's or Agent's tool access to one installation, in the App gate's
109
+ * policy shape; `null` when it reaches none. Members and the owner hold
110
+ * no tool policy: the App gate decides them by role.
111
+ */
112
+ export async function appToolAccess(authority, installationId, ports) {
113
+ switch (authority.kind) {
114
+ case "key":
115
+ return authority.key === null
116
+ ? null
117
+ : keyAppAccess(authority.key, installationId);
118
+ case "agent":
119
+ return ports.appToolPolicy(authority.actor);
120
+ case "owner":
121
+ case "member":
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * Whether this authority may see or open one installation at all (the home
127
+ * list, Git). A key needs `apps.read` there; `use` needs `apps.use`.
128
+ * Nothing anonymous reaches one.
129
+ */
130
+ export async function reachesInstallation(authority, installation, access, ports) {
131
+ switch (authority.kind) {
132
+ case "owner":
133
+ case "member":
134
+ return true;
135
+ case "key":
136
+ case "agent": {
137
+ const policy = await appToolAccess(authority, installation.installationId, ports);
138
+ return policy !== null && (access === "read" || policy.level === "write");
139
+ }
140
+ }
141
+ }
142
+ /** Whether a live key's grants permit a one-use device enrollment. */
143
+ export function allowsMachineEnrollment(key) {
144
+ return (key.grants.length === 1 &&
145
+ key.grants[0]?.capability === "machine.enroll" &&
146
+ oysterKeyVocabulary.grantsAllow(key.grants, "machine.enroll"));
147
+ }
148
+ /** A refusal this module returns before any handler runs. */
149
+ function refused(status, error) {
150
+ return Response.json({ error }, { status });
151
+ }
152
+ /** The token only this module holds; a requirement cannot be minted without it. */
153
+ const mint = Symbol("oyster.requirement.mint");
154
+ /** Module-private reader of a requirement's mint record; set once below. */
155
+ let mintRecordOf;
156
+ /**
157
+ * One route's declared requirement: an opaque, frozen value only this module
158
+ * mints. Its label and decision live in private fields that only this module
159
+ * reads, so a copy, a look-alike, an overridden method or a relabelled field
160
+ * changes nothing a route serves, and registering a non-minted value throws.
161
+ */
162
+ export class Requirement {
163
+ #declared;
164
+ #admit;
165
+ static {
166
+ mintRecordOf = (value) => #declared in value
167
+ ? { declared: value.#declared, admit: value.#admit }
168
+ : undefined;
169
+ }
170
+ /** Module-private: use the requirement constructors below. */
171
+ constructor(token, declared, admit) {
172
+ if (token !== mint)
173
+ throw new Error("Only authorization.ts mints route requirements");
174
+ this.#declared = Object.freeze({ ...declared });
175
+ this.#admit = admit;
176
+ Object.freeze(this);
177
+ }
178
+ }
179
+ function requirement(declared, admit) {
180
+ return new Requirement(mint, declared, admit);
181
+ }
182
+ /** Authenticate, or answer the table's own unauthenticated refusal. */
183
+ async function signedIn(facts) {
184
+ const result = await facts.principal();
185
+ return result.kind === "admitted"
186
+ ? result.principal
187
+ : unauthenticated(facts.request, result);
188
+ }
189
+ /** A browser page asks to sign in; every other refused request gets its status. */
190
+ export function unauthenticated(request, result) {
191
+ if ((result.status === 401 || result.status === 403) &&
192
+ request.method === "GET" &&
193
+ (request.headers.get("Accept") ?? "").includes("text/html"))
194
+ return new Response(null, { status: 302, headers: { Location: "/login" } });
195
+ const headers = new Headers();
196
+ if (result.retryAfter !== undefined)
197
+ headers.set("Retry-After", String(result.retryAfter));
198
+ return Response.json({ error: "Sign in to use this Oyster" }, { status: result.status, headers });
199
+ }
200
+ /** Served without any principal: sandboxed App bytes, page assets, sign-in. */
201
+ export function publicRoute(reason) {
202
+ return requirement({ kind: "public", reason }, () => Promise.resolve(null));
203
+ }
204
+ /**
205
+ * The route verifies its own credential (MCP, Git, signed webhook delivery,
206
+ * device enrollment); `reason` names that credential. A door that reads an
207
+ * Oyster principal decides its reach through this module's functions.
208
+ */
209
+ export function door(reason) {
210
+ return requirement({ kind: "door", reason }, () => Promise.resolve(null));
211
+ }
212
+ /**
213
+ * The owner only. `direct` also refuses an App-mediated request (one the
214
+ * page bridge sends with `X-Oyster-Installation`, and every request a
215
+ * capability table serves): consent and secrets never ride an App.
216
+ */
217
+ export function owner(via) {
218
+ return requirement({ kind: "owner", via }, async (facts) => {
219
+ const principal = await signedIn(facts);
220
+ if (principal instanceof Response)
221
+ return principal;
222
+ if (!oysterPrincipalIsOwner(principal))
223
+ return refused(403, "Owner only");
224
+ const origin = facts.mediated ? "app" : "owner-direct";
225
+ if (via === "direct" && origin === "app")
226
+ return refused(403, "Owner direct only");
227
+ return { principal, origin };
228
+ });
229
+ }
230
+ /**
231
+ * Any member, owner or not; never a key or Agent. `deny` shapes the
232
+ * refusal for a contract whose wire already names one.
233
+ */
234
+ export function member(deny = () => refused(403, "Members only")) {
235
+ return requirement({ kind: "member" }, async (facts) => {
236
+ const principal = await signedIn(facts);
237
+ if (principal instanceof Response)
238
+ return principal;
239
+ return "role" in principal ? { principal } : deny();
240
+ });
241
+ }
242
+ /**
243
+ * An App route: any signed-in principal, decided per call by the App gate,
244
+ * which reads this module's {@link appToolAccess}.
245
+ */
246
+ export function app() {
247
+ return requirement({ kind: "app" }, async (facts) => {
248
+ const principal = await signedIn(facts);
249
+ return principal instanceof Response ? principal : { principal };
250
+ });
251
+ }
252
+ /** The installation list (the home page): the owner sees all, keys their granted ones. */
253
+ export function apps() {
254
+ return requirement({ kind: "apps" }, async (facts) => {
255
+ const authority = await signedInAuthority(facts);
256
+ if (authority instanceof Response)
257
+ return authority;
258
+ return {
259
+ authority,
260
+ reaches: (installation) => reachesInstallation(authority, installation, "read", facts.ports),
261
+ };
262
+ });
263
+ }
264
+ /** Resolve a signed-in principal's authority, refusing an anonymous request. */
265
+ async function signedInAuthority(facts) {
266
+ const principal = await signedIn(facts);
267
+ if (principal instanceof Response)
268
+ return principal;
269
+ return authorityOf(principal, facts.ports);
270
+ }
271
+ /**
272
+ * One Agent, named by the route's `:agentId` (or by `id` from the request):
273
+ * the owner, or a key granted `agents.use` for that Agent id.
274
+ */
275
+ export function agent(id = (facts) => Promise.resolve(facts.param("agentId") ?? null)) {
276
+ return requirement({ kind: "agent" }, async (facts) => {
277
+ const authority = await signedInAuthority(facts);
278
+ if (authority instanceof Response)
279
+ return authority;
280
+ const agentId = await id(facts);
281
+ if (agentId === null)
282
+ return refused(400, "Name the Agent");
283
+ if (!reachesAgent(authority, agentId))
284
+ return refused(403, "Agent access denied");
285
+ return { authority, principal: authority.principal, agentId };
286
+ });
287
+ }
288
+ /** Agents (and their inbox rows): the owner sees all, a key its granted Agents. */
289
+ export function agents() {
290
+ return requirement({ kind: "agents" }, async (facts) => {
291
+ const authority = await signedInAuthority(facts);
292
+ if (authority instanceof Response)
293
+ return authority;
294
+ return {
295
+ authority,
296
+ reaches: (agentId) => Promise.resolve(reachesAgent(authority, agentId)),
297
+ };
298
+ });
299
+ }
300
+ /** The host's Computer, its workspace files and media: see {@link reachesComputer}. */
301
+ export function computer() {
302
+ return requirement({ kind: "computer" }, async (facts) => {
303
+ const authority = await signedInAuthority(facts);
304
+ if (authority instanceof Response)
305
+ return authority;
306
+ if (!reachesComputer(authority))
307
+ return refused(403, "Computer access requires an owner or key");
308
+ return { principal: authority.principal };
309
+ });
310
+ }
311
+ /** Module-private reader of a table's record; set once below. */
312
+ let tableRecordOf;
313
+ /** A table's record, or a refusal for anything `routes()` did not build. */
314
+ function recordOf(table) {
315
+ const record = tableRecordOf(table);
316
+ if (record === undefined)
317
+ throw new Error("routes() did not build this route table");
318
+ return record;
319
+ }
320
+ /**
321
+ * A declared route table: a frozen, opaque value only {@link routes} builds.
322
+ * Its routes live in a private record; including or serving reads only that
323
+ * record, never a method a copy could override. A table cannot serve itself:
324
+ * only {@link serveHost}, {@link serveDoors} and {@link serveCapability} do,
325
+ * each with the principal its composition root admits. There is no `use`: a
326
+ * path-wide middleware could serve a path no route declares.
327
+ */
328
+ export class Routes {
329
+ #entries = [];
330
+ #errorHandler = undefined;
331
+ static {
332
+ tableRecordOf = (table) => #entries in table
333
+ ? {
334
+ entries: table.#entries,
335
+ errorHandler: () => table.#errorHandler,
336
+ }
337
+ : undefined;
338
+ }
339
+ /** Module-private: use {@link routes}. */
340
+ constructor(token) {
341
+ if (token !== mint)
342
+ throw new Error("Only authorization.ts builds route tables");
343
+ Object.freeze(this);
344
+ }
345
+ /** Register one GET route. */
346
+ get(path, requirement, handler, options) {
347
+ return this.#register(["GET"], path, requirement, handler, options);
348
+ }
349
+ /** Register one POST route. */
350
+ post(path, requirement, handler, options) {
351
+ return this.#register(["POST"], path, requirement, handler, options);
352
+ }
353
+ /** Register one PUT route. */
354
+ put(path, requirement, handler, options) {
355
+ return this.#register(["PUT"], path, requirement, handler, options);
356
+ }
357
+ /** Register one DELETE route. */
358
+ delete(path, requirement, handler, options) {
359
+ return this.#register(["DELETE"], path, requirement, handler, options);
360
+ }
361
+ /** Register one route for every method. */
362
+ all(path, requirement, handler, options) {
363
+ return this.#register("ALL", path, requirement, handler, options);
364
+ }
365
+ /** Register one route for several methods. */
366
+ on(methods, path, requirement, handler, options) {
367
+ return this.#register(methods, path, requirement, handler, options);
368
+ }
369
+ /** Serve another table's routes at this position, keeping its own error handler. */
370
+ include(other) {
371
+ recordOf(other);
372
+ this.#entries.push({ kind: "table", table: other });
373
+ return this;
374
+ }
375
+ /** Answer an error thrown by this table's own handlers. */
376
+ onError(handler) {
377
+ this.#errorHandler = handler;
378
+ return this;
379
+ }
380
+ /** Every route this table and its included tables declare, in serving order. */
381
+ declarations() {
382
+ return declarationsOf(this);
383
+ }
384
+ #register(methods, path, requirement, handler, options = {}) {
385
+ // Only the mint record decides: a copy or look-alike has none, and a
386
+ // minted value's own properties are never read.
387
+ const record = mintRecordOf(requirement);
388
+ if (record === undefined)
389
+ throw new Error(`${path} names a requirement authorization.ts did not mint`);
390
+ this.#entries.push({
391
+ kind: "route",
392
+ declaration: { methods, path, requirement: record.declared },
393
+ mount: (hono, principal, ports, mediated) => {
394
+ // One admission per request, shared down the middleware chain.
395
+ const admittedFor = new WeakMap();
396
+ const gate = async (context, next) => {
397
+ const admitted = await record.admit({
398
+ request: context.req.raw,
399
+ param: (name) => context.req.param(name),
400
+ body: async (schema) => {
401
+ const decoded = Schema.decodeUnknownOption(Schema.fromJsonString(schema))(await context.req.text());
402
+ return decoded._tag === "Some" ? decoded.value : null;
403
+ },
404
+ principal: () => principal(context),
405
+ mediated: mediated(context.req.raw),
406
+ ports,
407
+ });
408
+ if (admitted instanceof Response)
409
+ return admitted;
410
+ admittedFor.set(context, { value: admitted });
411
+ await next();
412
+ return undefined;
413
+ };
414
+ const finish = async (context) => {
415
+ const admitted = admittedFor.get(context);
416
+ if (admitted === undefined)
417
+ throw new Error("A route handler ran without its admission");
418
+ return handler(context, admitted.value);
419
+ };
420
+ // Hono runs same-path handlers in registration order.
421
+ const chain = [gate, ...(options.middleware ?? [])];
422
+ for (const step of chain)
423
+ if (methods === "ALL")
424
+ hono.all(path, step);
425
+ else
426
+ hono.on([...methods], path, step);
427
+ if (methods === "ALL")
428
+ hono.all(path, (context) => finish(context, null));
429
+ else
430
+ hono.on([...methods], path, (context) => finish(context, null));
431
+ },
432
+ });
433
+ return this;
434
+ }
435
+ }
436
+ /** Start a declared route table. */
437
+ export function routes() {
438
+ return new Routes(mint);
439
+ }
440
+ /** A table's declarations, read from its record and its included records. */
441
+ function declarationsOf(table) {
442
+ return recordOf(table).entries.flatMap((entry) => entry.kind === "route" ? [entry.declaration] : declarationsOf(entry.table));
443
+ }
444
+ /** The page bridge names the installation an App call comes from. */
445
+ const bridged = (request) => request.headers.has("X-Oyster-Installation");
446
+ /**
447
+ * Serve one table: the only `Hono` the Oyster host builds. Each route runs
448
+ * its requirement first, then its middleware, then its handler. Nothing a
449
+ * caller supplies runs before a route's requirement: timing is Hono's own
450
+ * middleware, and header hooks run on the finished response.
451
+ */
452
+ function serveTable(table, principal, ports, rules) {
453
+ const { entries, errorHandler } = recordOf(table);
454
+ const hono = new Hono();
455
+ if (rules.serverTiming !== null) {
456
+ const { totalDescription } = rules.serverTiming;
457
+ hono.use(timing({
458
+ totalDescription,
459
+ enabled: (context) => context.res.status !== 101,
460
+ }));
461
+ }
462
+ const hooks = rules.headers;
463
+ hono.use(async (context, next) => {
464
+ await next();
465
+ const response = context.res;
466
+ // An upgraded socket keeps the runtime's own response.
467
+ if (response.status === 101)
468
+ return;
469
+ const headers = new Headers(response.headers);
470
+ for (const hook of hooks) {
471
+ const added = hook({
472
+ status: response.status,
473
+ headers: new Headers(response.headers),
474
+ });
475
+ if (added !== null)
476
+ for (const [name, value] of Object.entries(added))
477
+ headers.set(name, value);
478
+ }
479
+ // A fresh response with mutable headers, the route's status and body.
480
+ context.res = new Response(response.body, {
481
+ status: response.status,
482
+ statusText: response.statusText,
483
+ headers,
484
+ });
485
+ });
486
+ for (const entry of entries)
487
+ if (entry.kind === "table")
488
+ hono.route("/", serveTable(entry.table, principal, ports, {
489
+ headers: [],
490
+ serverTiming: null,
491
+ mediated: rules.mediated,
492
+ }));
493
+ else
494
+ entry.mount(hono, principal, ports, rules.mediated);
495
+ const handler = errorHandler();
496
+ if (handler !== undefined)
497
+ hono.onError(handler);
498
+ return hono;
499
+ }
500
+ function servedView(hono) {
501
+ return Object.freeze({
502
+ fetch: async (...request) => hono.fetch(...request),
503
+ request: async (...request) => hono.request(...request),
504
+ servedPaths: () => hono.routes.map((route) => route.path),
505
+ });
506
+ }
507
+ const hostServeOptionNames = new Set(["headers", "serverTiming"]);
508
+ /**
509
+ * Serve a tenant host's table: the composition root (`oysterRouteTable`)
510
+ * supplies how a request authenticates. An option this function does not
511
+ * know is refused rather than ignored.
512
+ */
513
+ export function serveHost(table, authenticate, ports, options = {}) {
514
+ for (const name of Reflect.ownKeys(options))
515
+ if (!hostServeOptionNames.has(name))
516
+ throw new Error(`serveHost has no option ${String(name)}`);
517
+ return servedView(serveTable(table, (context) => authenticate(context.req.raw), ports, {
518
+ headers: Object.hasOwn(options, "headers") ? (options.headers ?? []) : [],
519
+ serverTiming: Object.hasOwn(options, "serverTiming")
520
+ ? (options.serverTiming ?? null)
521
+ : null,
522
+ mediated: bridged,
523
+ }));
524
+ }
525
+ /**
526
+ * Serve a table of doors and public routes only (a Worker's top-level
527
+ * dispatch, before any tenant is selected): a route that needs an Oyster
528
+ * principal is refused, never admitted.
529
+ */
530
+ export function serveDoors(table) {
531
+ return servedView(serveTable(table, () => Promise.resolve({ kind: "refused", status: 401 }), { liveKey: () => Promise.resolve(null), appToolPolicy: () => null }, { headers: [], serverTiming: null, mediated: bridged }));
532
+ }
533
+ /**
534
+ * Serve one capability's declared table under the host dispatcher's
535
+ * admission, read from `c.env`: the App already holds the capability, and
536
+ * each route still decides its own resource. Every request here is
537
+ * App-mediated, however it was admitted (a header or a browser socket's
538
+ * query), so `owner("direct")` always refuses inside a capability.
539
+ */
540
+ export function serveCapability(table, ports) {
541
+ return servedView(serveTable(table, (context) => Promise.resolve({ kind: "admitted", principal: context.env.principal }), ports, { headers: [], serverTiming: null, mediated: () => true }));
542
+ }