@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
package/dist/host.js CHANGED
@@ -11,7 +11,7 @@ import { appStorageKeyNameSchema, appStorageWriteSchema, appStorageResetSchema,
11
11
  import { guestLimits } from "./guest-policy.js";
12
12
  import { APP_INVOCATION_CEILING_MS, admittedCallerFacts, admittedMethodCallerFacts, invocationAuthorityOf, invocationCallerContext, invocationFacts, } from "./invocation.js";
13
13
  import { appFacetResponse } from "./backend-request.js";
14
- import { Hono } from "hono";
14
+ import { endTime, startTime } from "hono/timing";
15
15
  import { bodyLimit } from "hono/body-limit";
16
16
  import { match } from "path-to-regexp";
17
17
  import * as Schema from "effect/Schema";
@@ -20,13 +20,13 @@ import { emptyAppDeclaration } from "@fungi.computer/puffball/app";
20
20
  import { applyInstallationCommand, installationIdSchema, } from "./apps/installation.js";
21
21
  import { admits, scopedBindings, grantsFor } from "./apps/gate.js";
22
22
  import { appPermissionPageSchema, permissionSaveSchema, } from "./apps/permissions.js";
23
- import { keyAppAccess } from "./keys.js";
23
+ import { app as appRoute, apps as appsRoute, door, member, oysterPrincipalIsOwner, owner, publicRoute, authorityOf, hostAuthorityPorts, reachesInstallation, routes, serveCapability, serveHost, } from "./authorization.js";
24
24
  import { deriveDeclaredOperations, appCallOperation, } from "./apps/operations.js";
25
25
  import { appBackendActorHeader, appBackendComputerHeader, appMethodRefusalStatus, } from "@fungi.computer/agar/backend";
26
26
  import { appCallsDescriptor, } from "./capability-descriptor.js";
27
27
  export { capabilityDescriptorSchema, capabilityMethodKindSchema, capabilityMethodSchema, } from "./capability-descriptor.js";
28
28
  import { selectionOf, } from "./index.js";
29
- import { parseRelease, readVerified, sha256, } from "./release.js";
29
+ import { parseRelease, readCachedVerified, sha256, } from "./release.js";
30
30
  import { createGitDoor } from "./git.js";
31
31
  import { buildGitPassPrefix, presentedGitKey } from "./git-auth.js";
32
32
  import { workflowGitPassPrefix } from "@fungi.computer/braid/runtime";
@@ -39,15 +39,8 @@ import { Form, Field, FormFooter } from "@fungi.computer/caps/components/field";
39
39
  import { Input } from "@fungi.computer/caps/components/input";
40
40
  import { Button } from "@fungi.computer/caps/components/button";
41
41
  import { parseArtifactRef } from "@fungi.computer/puffball";
42
+ import { isBuildJobLogRef } from "./build/job-log.js";
42
43
  export { ReleaseError, defaultAssetPolicy, parseRelease, resolveBackend, sha256, verifyRelease, } from "./release.js";
43
- /** Narrow an admitted principal to the anonymous public reader. */
44
- export function oysterPrincipalIsVisitor(principal) {
45
- return principal.actor.kind === "visitor";
46
- }
47
- /** True only for a member admitted with the owner role. */
48
- export function oysterPrincipalIsOwner(principal) {
49
- return "role" in principal && principal.role === "owner";
50
- }
51
44
  /** Stable actor identity for host storage and backend attribution. */
52
45
  export function oysterPrincipalId(principal) {
53
46
  switch (principal.actor.kind) {
@@ -86,9 +79,6 @@ function escapeHtml(value) {
86
79
  .replaceAll(">", ">")
87
80
  .replaceAll('"', """);
88
81
  }
89
- const visibilitySchema = Schema.Struct({
90
- visibility: Schema.Literals(["private", "public"]),
91
- });
92
82
  function refusal(status, error, retryAfter) {
93
83
  const init = { status };
94
84
  if (retryAfter !== undefined)
@@ -194,14 +184,32 @@ export async function installOrSelect(host, installationId, release, principal,
194
184
  /** The tab icon of every Oyster page a person opens at the top level. */
195
185
  const tabIcon = `<link rel="icon" type="image/svg+xml" href="${fungiFaviconHref}">`;
196
186
  /**
197
- * Create the Oyster App server: a Hono app that installs App Releases, serves
187
+ * Create the Oyster App server: a served route table that installs App Releases, serves
198
188
  * their UI, forwards their backend requests and methods to their Instances,
199
189
  * and mounts the host's MCP endpoint. The host's adapters decide where state
200
190
  * lives and who may sign in.
201
191
  */
202
192
  export function createOyster(host) {
193
+ return oysterRouteTable(host).serve();
194
+ }
195
+ /**
196
+ * The Oyster host's declared route table: every route names its requirement
197
+ * (`authorization.ts`), so an undeclared route is never served.
198
+ */
199
+ export function oysterRouteTable(host) {
203
200
  const limits = guestLimits(host.guestLimits);
204
- const app = new Hono();
201
+ const table = routes();
202
+ // One authentication per request, shared by its requirement and its door.
203
+ const authentications = new WeakMap();
204
+ const authenticate = (request) => {
205
+ let result = authentications.get(request);
206
+ if (result === undefined) {
207
+ result = host.auth.authenticate(request);
208
+ authentications.set(request, result);
209
+ }
210
+ return result;
211
+ };
212
+ const ports = hostAuthorityPorts(host);
205
213
  const releaseOf = releaseCache(host.artifacts);
206
214
  const operationDescriptors = host.capabilities ?? [];
207
215
  const gateFacts = (installation, release, principal) => hostGateFacts(host, installation, release, principal);
@@ -234,17 +242,15 @@ export function createOyster(host) {
234
242
  source,
235
243
  });
236
244
  const prepareAssets = async () => {
237
- const { pageScript, publicHomeScript, grantConsentScript, fontStyles, loginFontStyles, shellStyles, } = await (host.pageAssets ?? loadPageAssets)();
245
+ const { pageScript, grantConsentScript, fontStyles, loginFontStyles, shellStyles, } = await (host.pageAssets ?? loadPageAssets)();
238
246
  return Promise.all([
239
247
  asset("page.js", "text/javascript; charset=utf-8", host.pageScript ?? pageScript),
240
- asset("public-home.js", "text/javascript; charset=utf-8", publicHomeScript),
241
248
  asset("grant-consent.js", "text/javascript; charset=utf-8", grantConsentScript),
242
249
  asset("fonts.css", "text/css; charset=utf-8", fontStyles),
243
250
  asset("login-fonts.css", "text/css; charset=utf-8", loginFontStyles),
244
251
  asset("styles.css", "text/css; charset=utf-8", shellStyles),
245
- ]).then(([page, publicHome, grantConsent, fonts, loginFonts, styles]) => ({
252
+ ]).then(([page, grantConsent, fonts, loginFonts, styles]) => ({
246
253
  page,
247
- publicHome,
248
254
  grantConsent,
249
255
  fonts,
250
256
  loginFonts,
@@ -258,11 +264,23 @@ export function createOyster(host) {
258
264
  ? cause
259
265
  : new PageAssetsLoadError(cause);
260
266
  }));
267
+ // An Oyster is a private instance, never a search result: every response
268
+ // says so, and robots.txt is public so crawlers read the rule rather than a
269
+ // 401 (which Google treats as "crawl everything").
270
+ const noIndex = () => ({
271
+ "X-Robots-Tag": "noindex, nofollow",
272
+ });
273
+ table.get("/robots.txt", publicRoute("the crawl rule for a private instance"), () => new Response("User-agent: *\nDisallow: /\n", {
274
+ headers: {
275
+ "Content-Type": "text/plain; charset=utf-8",
276
+ "Cache-Control": "public, max-age=3600",
277
+ },
278
+ }));
261
279
  // The home page imports this host-owned entry, never the Release module's
262
280
  // own URL: released Oyster 0.2.0 served that URL immutable, so a browser may
263
281
  // hold a copy it would import without a request. The entry is never cached
264
282
  // and rechecks current approval before it names the module.
265
- app.get(`/ui-entry/:installationId/:releaseId${shellModuleAssetPath}`, async (context) => {
283
+ table.get(`/ui-entry/:installationId/:releaseId${shellModuleAssetPath}`, publicRoute("never cached; rechecks the shell's approval before naming it"), async (context) => {
266
284
  const installation = await host.installations.get(context.req.param("installationId"));
267
285
  if (installation?.status !== "enabled" ||
268
286
  installation.release.releaseId !== context.req.param("releaseId"))
@@ -284,8 +302,10 @@ export function createOyster(host) {
284
302
  });
285
303
  // App UI: digest-pinned, sandboxed, and readable without a session because
286
304
  // the opaque-origin frame cannot carry one.
287
- app.get("/ui/:installationId/:releaseId/*", async (context) => {
305
+ table.get("/ui/:installationId/:releaseId/*", publicRoute("digest-pinned App bytes for an opaque-origin frame"), async (context) => {
306
+ startTime(context, "registry");
288
307
  const installation = await host.installations.get(context.req.param("installationId"));
308
+ endTime(context, "registry");
289
309
  if (installation?.status !== "enabled" ||
290
310
  installation.release.releaseId !== context.req.param("releaseId"))
291
311
  return refusal(404, "Not found");
@@ -298,13 +318,15 @@ export function createOyster(host) {
298
318
  if (release.ui.kind === "html") {
299
319
  if (path !== "/")
300
320
  return refusal(404, "Not found");
301
- const bytes = await readVerified(host.artifacts, release.ui.artifactRef, release.ui.digest);
302
- return new Response(new Uint8Array(bytes), {
321
+ const body = await readCachedVerified(host.artifacts, host.uiCache, release.ui.artifactRef, release.ui.digest);
322
+ return new Response(body, {
303
323
  headers: {
304
324
  "Content-Type": "text/html; charset=utf-8",
305
325
  "Content-Security-Policy": `default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src blob: data:; media-src blob:; font-src data:; connect-src 'none'; frame-src 'none'; frame-ancestors ${origin}; base-uri 'none'; form-action 'none'; sandbox ${appSandbox}`,
306
326
  "X-Content-Type-Options": "nosniff",
307
- "Cache-Control": "no-store",
327
+ // Digest-pinned like an assets UI's index.html: the frame's
328
+ // authority is its host connection, never these bytes.
329
+ "Cache-Control": "public, max-age=31536000, immutable",
308
330
  },
309
331
  });
310
332
  }
@@ -317,9 +339,9 @@ export function createOyster(host) {
317
339
  const shellEntry = wanted === shellModuleAssetPath;
318
340
  if (shellEntry && !(await shellApproved(installation, release)))
319
341
  return noStoreRefusal(403, "not_approved");
320
- const bytes = await readVerified(host.artifacts, asset.artifactRef, asset.digest);
342
+ const body = await readCachedVerified(host.artifacts, host.uiCache, asset.artifactRef, asset.digest);
321
343
  const scope = `${origin}${prefix}/`;
322
- return new Response(new Uint8Array(bytes), {
344
+ return new Response(body, {
323
345
  headers: {
324
346
  "Content-Type": asset.contentType,
325
347
  "Content-Security-Policy": `default-src 'none'; script-src ${scope} 'wasm-unsafe-eval'; style-src ${scope} 'unsafe-inline'; img-src ${scope} blob: data:; media-src blob:; font-src ${scope} data:; connect-src data: ${scope}; worker-src blob:; frame-src 'none'; frame-ancestors ${origin}; base-uri 'none'; form-action 'none'; sandbox ${appSandbox}`,
@@ -331,7 +353,7 @@ export function createOyster(host) {
331
353
  },
332
354
  });
333
355
  });
334
- app.get("/_oyster/assets/*", async (context) => {
356
+ table.get("/_oyster/assets/*", publicRoute("content-addressed host page assets"), async (context) => {
335
357
  const wanted = new URL(context.req.url).pathname;
336
358
  const found = Object.values(await assets()).find(({ path }) => path === wanted);
337
359
  if (found === undefined)
@@ -344,7 +366,7 @@ export function createOyster(host) {
344
366
  },
345
367
  });
346
368
  });
347
- app.get("/login", async () => {
369
+ table.get("/login", publicRoute("the sign-in form"), async () => {
348
370
  const { loginFonts, styles } = await assets();
349
371
  const form = renderToStaticMarkup(createElement(CenteredCard, null, createElement(CardHeader, null, createElement(CardTitle, { level: 2 }, "Sign in")), createElement(CardContent, null, createElement(Form, { method: "post", action: "/login" }, createElement(Field, {
350
372
  label: "Owner key",
@@ -363,16 +385,16 @@ export function createOyster(host) {
363
385
  },
364
386
  });
365
387
  });
366
- app.post("/login", (context) => host.auth.login === undefined
388
+ table.post("/login", door("the host's own sign-in checks the posted key"), (context) => host.auth.login === undefined
367
389
  ? refusal(404, "Sign-in is not available")
368
390
  : host.auth.login(context.req.raw));
369
391
  if (host.mcp !== undefined) {
370
392
  const mcp = host.mcp;
371
- app.all("/mcp", (context) => mcp.handleRequest(context.req.raw));
393
+ table.all("/mcp", door("MCP authenticates every request; tools read this module's reach"), (context) => mcp.handleRequest(context.req.raw));
372
394
  }
373
395
  if (host.git !== undefined) {
374
396
  const git = host.git;
375
- app.route("/", createGitDoor({
397
+ const gitDoor = createGitDoor({
376
398
  authenticate: async (request, target) => {
377
399
  // A Workflow Git pass header is admitted only by the pass port,
378
400
  // never as a session or key.
@@ -388,69 +410,27 @@ export function createOyster(host) {
388
410
  return git.buildPass === undefined
389
411
  ? null
390
412
  : git.buildPass(presented.value, target);
391
- const result = await host.auth.authenticate(request);
392
- return result.kind === "admitted"
413
+ // A signed-in key reaches only the installations its grants
414
+ // name: clone needs App read, push App use. Otherwise the door
415
+ // refuses it.
416
+ const result = await authenticate(request);
417
+ if (result.kind !== "admitted")
418
+ return null;
419
+ const installation = await host.installations.get(target.installationId);
420
+ return installation !== null &&
421
+ (await reachesInstallation(await authorityOf(result.principal, ports), installation, target.scope === "write" ? "use" : "read", ports))
393
422
  ? git.writerOf(result.principal)
394
423
  : null;
395
424
  },
396
- isPublic: async (installationId) => (await host.installations.get(installationId))?.visibility ===
397
- "public",
398
425
  git: async (installationId) => {
399
426
  const installation = await host.installations.get(installationId);
400
427
  if (installation?.status !== "enabled")
401
428
  return null;
402
429
  return git.git(installation);
403
430
  },
404
- }));
431
+ });
432
+ table.all("/git/*", door("Git passes and keys; a key's reach is checked per installation"), (context) => gitDoor.fetch(context.req.raw));
405
433
  }
406
- // A public installation's page and read methods admit anonymous visitors.
407
- const visitor = { actor: { kind: "visitor" } };
408
- const visitorRoute = /^\/apps\/([^/]+)\/(?:|methods\/([A-Za-z0-9_.]+))$/u;
409
- const visitorMayReach = async (request) => {
410
- const match = visitorRoute.exec(new URL(request.url).pathname);
411
- if (match === null)
412
- return null;
413
- const [, installationId = "", method] = match;
414
- const page = method === undefined;
415
- if (page ? request.method !== "GET" : request.method !== "POST")
416
- return null;
417
- const installation = await host.installations.get(installationId);
418
- if (installation?.status !== "enabled" ||
419
- installation.visibility !== "public")
420
- return null;
421
- if (page)
422
- return installation;
423
- const release = await releaseOf(installation.release);
424
- return release.api?.methods[method]?.effect === "read"
425
- ? installation
426
- : null;
427
- };
428
- // The public home and public App reads admit visitors; other routes require authentication.
429
- app.use("*", async (context, next) => {
430
- const result = await host.auth.authenticate(context.req.raw);
431
- const installation = result.kind === "refused" ? await visitorMayReach(context.req.raw) : null;
432
- if (result.kind === "refused" &&
433
- (result.status === 401 || result.status === 403) &&
434
- ((context.req.method === "GET" && context.req.path === "/") ||
435
- installation !== null)) {
436
- if (installation !== null)
437
- context.set("installation", installation);
438
- context.set("principal", visitor);
439
- await next();
440
- return undefined;
441
- }
442
- if (result.kind === "refused") {
443
- const accept = context.req.header("Accept") ?? "";
444
- if ((result.status === 401 || result.status === 403) &&
445
- context.req.method === "GET" &&
446
- accept.includes("text/html"))
447
- return context.redirect("/login");
448
- return refusal(result.status, "Sign in to use this Oyster", result.retryAfter);
449
- }
450
- context.set("principal", result.principal);
451
- await next();
452
- return undefined;
453
- });
454
434
  const capabilityRoutes = (host.capabilities ?? []).flatMap((capability) => Object.entries(capability.methods).map(([name, method]) => ({
455
435
  capability,
456
436
  operation: `route:${capability.id}:${name}`,
@@ -461,17 +441,26 @@ export function createOyster(host) {
461
441
  sensitive: true,
462
442
  }),
463
443
  })));
464
- // Capability routes run under the principal stamped above; they receive the
465
- // admission as their env and never authenticate again.
466
- app.use("*", async (context, next) => {
444
+ // Each capability's own declared table, served once; it reads the
445
+ // dispatcher's admission from its env and never authenticates again.
446
+ const capabilityTables = new Map();
447
+ const capabilityTable = (capability) => {
448
+ let served = capabilityTables.get(capability.routes);
449
+ if (served === undefined) {
450
+ served = serveCapability(capability.routes, ports);
451
+ capabilityTables.set(capability.routes, served);
452
+ }
453
+ return served;
454
+ };
455
+ // Each declared capability path is a route: the App must hold that
456
+ // capability for the call, then the capability's own route decides the
457
+ // resource (an Agent, the Computer) under the same principal.
458
+ const dispatchCapability = async (context, principal) => {
467
459
  const url = new URL(context.req.url);
468
460
  const candidates = capabilityRoutes.filter((route) => (route.method === "ANY" || route.method === context.req.method) &&
469
461
  route.matches(url.pathname));
470
462
  if (candidates.length === 0)
471
- return next();
472
- const principal = context.get("principal");
473
- if (oysterPrincipalIsVisitor(principal))
474
- return refusal(401, "Sign in to use capabilities");
463
+ return refusal(404, "Not found");
475
464
  const browserTransport = context.req.header("Upgrade")?.toLowerCase() === "websocket" &&
476
465
  context.req.header("Origin") === url.origin &&
477
466
  candidates.some((route) => route.kind === "transport");
@@ -495,57 +484,45 @@ export function createOyster(host) {
495
484
  }).kind === "refused")
496
485
  continue;
497
486
  url.pathname = `/${url.pathname.slice(capability.base.length)}`;
498
- return capability.routes.fetch(new Request(url, context.req.raw), {
499
- principal,
500
- installation,
501
- });
487
+ return capabilityTable(capability).fetch(new Request(url, context.req.raw), { principal, installation });
502
488
  }
503
489
  return refusal(403, "Capability permission denied");
504
- });
505
- app.get("/", async (context) => {
506
- const actor = context.get("principal").actor;
507
- const publicHome = actor.kind === "visitor";
508
- const key = actor.kind === "team-key"
509
- ? ((await host.keys?.live(actor.keyId)) ?? null)
510
- : null;
511
- if (!publicHome && host.homeInstallationId !== undefined) {
490
+ };
491
+ const capabilityPaths = new Set((host.capabilities ?? []).flatMap((capability) => Object.values(capability.methods).flatMap((method) => {
492
+ const path = `${capability.base}${method.path}`;
493
+ return method.kind === "transport" ? [path, `${path}/*`] : [path];
494
+ })));
495
+ for (const path of capabilityPaths)
496
+ table.all(path, appRoute(), (context, admitted) => dispatchCapability(context, admitted.principal));
497
+ table.get("/", appsRoute(), async (context, admitted) => {
498
+ if (host.homeInstallationId !== undefined) {
512
499
  const home = await host.installations.get(host.homeInstallationId);
513
- if (home?.status === "enabled" &&
514
- (actor.kind !== "team-key" ||
515
- (key !== null && keyAppAccess(key, home.installationId) !== null)))
516
- return context.redirect(`/apps/${encodeURIComponent(home.installationId)}/`);
517
- }
518
- const installations = (await host.installations.list()).filter((installation) => installation.status === "enabled" &&
519
- (!publicHome || installation.visibility === "public") &&
520
- (actor.kind !== "team-key" ||
521
- (key !== null &&
522
- keyAppAccess(key, installation.installationId) !== null)));
523
- if (publicHome) {
524
- const apps = [];
525
- for (const installation of installations) {
526
- const release = await releaseOf(installation.release);
527
- if (release.ui === null)
528
- continue;
529
- apps.push({
530
- href: `/apps/${encodeURIComponent(installation.installationId)}/`,
531
- displayName: release.declaration?.displayName ?? "App",
532
- icon: release.declaration?.icon ?? null,
533
- });
500
+ if (home?.status === "enabled" && (await admitted.reaches(home))) {
501
+ const release = await releaseOf(home.release);
502
+ if (release.declaration?.capabilities.includes("team.workbenchHost") !==
503
+ true ||
504
+ (await shellApproved(home, release)))
505
+ return context.redirect(`/apps/${encodeURIComponent(home.installationId)}/`);
506
+ // Revocation must leave an owner a way back to consent, rather than
507
+ // redirecting into a shell that can no longer offer Settings.
534
508
  }
535
- const config = JSON.stringify({ title, apps }).replaceAll("<", "\\u003c");
536
- return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title>${tabIcon}<link rel="stylesheet" href="${(await assets()).fonts.path}"><style>html,body{margin:0;min-height:100%;font-family:var(--font-sans);background:var(--color-base-100);color:var(--color-base-content)}#oyster-public-home{min-height:100vh;display:flex;flex-direction:column}</style><div id="oyster-public-home"></div><script type="application/json" id="oyster-public-config">${config}</script><script type="module" src="${(await assets()).publicHome.path}"></script>`, {
537
- headers: {
538
- "Content-Type": "text/html; charset=utf-8",
539
- "Cache-Control": "no-store",
540
- "X-Content-Type-Options": "nosniff",
541
- "Content-Security-Policy": "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src data:; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
542
- },
543
- });
544
509
  }
510
+ const installations = [];
511
+ const stored = await host.installations.list();
512
+ for (const installation of stored)
513
+ if (installation.status === "enabled" &&
514
+ (await admitted.reaches(installation)))
515
+ installations.push(installation);
545
516
  const items = installations
546
- .map((installation) => `<li><a href="/apps/${escapeHtml(installation.installationId)}/">${escapeHtml(installation.installationId)}</a> <small>${escapeHtml(installation.release.releaseId)}</small>${oysterPrincipalIsOwner(context.get("principal")) ? ` <a href="/api/installations/${encodeURIComponent(installation.installationId)}/grants">App access</a>` : ""}</li>`)
517
+ .map((installation) => `<li><a href="/apps/${escapeHtml(installation.installationId)}/">${escapeHtml(installation.installationId)}</a> <small>${escapeHtml(installation.release.releaseId)}</small>${admitted.authority.kind === "owner" ? ` <a href="/api/installations/${encodeURIComponent(installation.installationId)}/grants">App access</a>` : ""}</li>`)
547
518
  .join("");
548
- return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title>${tabIcon}<h1>${escapeHtml(title)}</h1><ul>${items || "<li>No Apps installed yet.</li><li>Install Apps with <code>npx oyster install-apps</code></li>"}</ul>`, {
519
+ const resetAt = await host.installations.resetAt?.();
520
+ const resetNotice = resetAt !== undefined &&
521
+ resetAt !== null &&
522
+ !stored.some((installation) => installation.status === "enabled")
523
+ ? "<p>This update reset your installed Apps. Reinstall them with npx oyster install-apps.</p>"
524
+ : "";
525
+ return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title>${tabIcon}<h1>${escapeHtml(title)}</h1>${resetNotice}<ul>${items || "<li>No Apps installed yet.</li><li>Install Apps with <code>npx oyster install-apps</code></li>"}</ul>`, {
549
526
  headers: {
550
527
  "Content-Type": "text/html; charset=utf-8",
551
528
  "Cache-Control": "no-store",
@@ -553,54 +530,14 @@ export function createOyster(host) {
553
530
  },
554
531
  });
555
532
  });
556
- app.use("/api/*", async (context, next) => {
557
- if (!oysterPrincipalIsOwner(context.get("principal")))
558
- return refusal(403, "Only the owner manages this Oyster");
559
- await next();
560
- return undefined;
561
- });
562
- app.put("/api/installations/:installationId/visibility", async (context) => {
563
- const generation = await host.installations.generation();
564
- const current = await host.installations.get(context.req.param("installationId"));
565
- if (current === null || current.status === "deleted")
566
- return refusal(404, "Not found");
567
- const body = Schema.decodeUnknownOption(visibilitySchema)(await context.req.json());
568
- if (body._tag === "None")
569
- return refusal(400, "Invalid visibility");
570
- const update = await reduceInstallation(current, { kind: "set-visibility", visibility: body.value.visibility }, await releaseOf(current.release), context.get("principal"), current);
571
- if (Result.isFailure(update))
572
- return refusal(409, update.failure.code);
573
- const next = update.success.next;
574
- const release = await releaseOf(current.release);
575
- const refused = await prepare(next, release);
576
- if (refused !== null)
577
- return refused;
578
- const commit = await host.installations.compareAndPut(next, current.authority.authorityVersion, generation);
579
- if (!commit.ok)
580
- return refusal(409, commit.code);
581
- const activationRefusal = await apply(next, release);
582
- if (activationRefusal !== null)
583
- return activationRefusal;
584
- return Response.json(next);
585
- });
586
- if (host.ownerRoutes !== undefined) {
587
- const ownerRoutes = host.ownerRoutes;
588
- const owned = new Hono();
589
- owned.use("/api/*", async (context, next) => {
590
- if (!oysterPrincipalIsOwner(context.get("principal")))
591
- return refusal(403, "Owner only");
592
- await next();
593
- return undefined;
594
- });
595
- owned.route("/", ownerRoutes);
596
- app.route("/", owned);
597
- }
598
- app.get("/api/workbench/directory", async (context) => {
599
- if (!oysterPrincipalIsOwner(context.get("principal")))
600
- return refusal(403, "Owner only");
533
+ if (host.ownerRoutes !== undefined)
534
+ table.include(host.ownerRoutes);
535
+ table.get("/api/workbench/directory", owner("direct-or-app"), async (context) => {
601
536
  if (host.workbench === undefined)
602
537
  return refusal(404, "Not found");
538
+ startTime(context, "registry");
603
539
  let directory = await host.workbench.directory();
540
+ endTime(context, "registry");
604
541
  if (host.computerId !== undefined)
605
542
  directory = {
606
543
  ...directory,
@@ -610,10 +547,7 @@ export function createOyster(host) {
610
547
  headers: { "Cache-Control": "no-store" },
611
548
  });
612
549
  });
613
- app.all("/api/workbench/storage/:installationId/:operation", async (context) => {
614
- const principal = context.get("principal");
615
- if (!oysterPrincipalIsOwner(principal))
616
- return refusal(403, "Owner only");
550
+ table.all("/api/workbench/storage/:installationId/:operation", owner("direct-or-app"), async (context, { principal }) => {
617
551
  const workbench = host.workbench;
618
552
  if (workbench === undefined)
619
553
  return refusal(404, "Not found");
@@ -647,8 +581,8 @@ export function createOyster(host) {
647
581
  }
648
582
  return refusal(404, "Not found");
649
583
  });
650
- app.get("/api/installations", async () => Response.json(await host.installations.list()));
651
- app.put("/api/artifacts/:artifactRef", async (context) => {
584
+ table.get("/api/installations", owner("direct-or-app"), async () => Response.json(await host.installations.list()));
585
+ table.put("/api/artifacts/:artifactRef", owner("direct-or-app"), async (context) => {
652
586
  let artifactRef;
653
587
  try {
654
588
  artifactRef = parseArtifactRef(context.req.param("artifactRef"));
@@ -656,6 +590,8 @@ export function createOyster(host) {
656
590
  catch {
657
591
  return refusal(400, "Invalid artifact reference");
658
592
  }
593
+ if (isBuildJobLogRef(artifactRef))
594
+ return refusal(409, "build_job_log_ref_reserved");
659
595
  const expected = context.req.query("digest");
660
596
  const bytes = new Uint8Array(await context.req.arrayBuffer());
661
597
  if (expected === undefined || (await sha256(bytes)) !== expected)
@@ -679,7 +615,7 @@ export function createOyster(host) {
679
615
  .apply({ ...selectionOf(installation), status: installation.status }, installation.release);
680
616
  return result.ok ? null : instanceRefusal(result.code, result.causeClass);
681
617
  };
682
- app.post("/api/installations", async (context) => {
618
+ table.post("/api/installations", owner("direct-or-app"), async (context, { origin, principal }) => {
683
619
  let request;
684
620
  let release;
685
621
  try {
@@ -695,17 +631,12 @@ export function createOyster(host) {
695
631
  const key = installationRequestIdSchema.safeParse(context.req.header("Idempotency-Key"));
696
632
  if (!key.success)
697
633
  return refusal(400, "Invalid installation request identity");
698
- const admitted = await admitInstallation(host, installationId, release, context.get("principal"), "install-or-remint", context.req.raw.headers.has("X-Oyster-Installation")
699
- ? "app"
700
- : "owner-direct", key.data);
634
+ const admitted = await admitInstallation(host, installationId, release, principal, "install-or-remint", origin, key.data);
701
635
  return admitted instanceof Response
702
636
  ? admitted
703
637
  : Response.json(admitted, { status: 202 });
704
638
  });
705
- app.get("/api/installations/:installationId/status", async (context) => {
706
- if (!oysterPrincipalIsOwner(context.get("principal")) ||
707
- context.req.raw.headers.has("X-Oyster-Installation"))
708
- return refusal(403, "Owner only");
639
+ table.get("/api/installations/:installationId/status", owner("direct"), async (context) => {
709
640
  const key = installationRequestIdSchema.safeParse(context.req.query("requestId"));
710
641
  if (!key.success)
711
642
  return refusal(400, "Invalid installation request identity");
@@ -724,9 +655,7 @@ export function createOyster(host) {
724
655
  throw cause;
725
656
  }
726
657
  });
727
- app.get("/api/installations/:installationId/grants", async (context) => {
728
- if (context.req.raw.headers.has("X-Oyster-Installation"))
729
- return refusal(403, "Owner direct only");
658
+ table.get("/api/installations/:installationId/grants", owner("direct"), async (context, { principal }) => {
730
659
  const generation = await host.installations.generation();
731
660
  const current = await host.installations.get(context.req.param("installationId"));
732
661
  if (current === null || current.status === "deleted")
@@ -740,6 +669,18 @@ export function createOyster(host) {
740
669
  appId: current.appId,
741
670
  appName: declaration.displayName ?? current.installationId,
742
671
  artifactDigest: current.release.artifact.artifactDigest,
672
+ canEdit: oysterPrincipalIsOwner(principal),
673
+ resources: host.permissionResources === undefined
674
+ ? host.computerId === undefined
675
+ ? []
676
+ : [
677
+ {
678
+ kind: "computer",
679
+ id: host.computerId,
680
+ label: host.computerId,
681
+ },
682
+ ]
683
+ : await host.permissionResources(principal),
743
684
  permissions: {
744
685
  authority: current.authority,
745
686
  declaredOperations: declared,
@@ -755,20 +696,13 @@ export function createOyster(host) {
755
696
  "Content-Type": "text/html; charset=utf-8",
756
697
  "Cache-Control": "no-store",
757
698
  "X-Content-Type-Options": "nosniff",
758
- "Content-Security-Policy": "default-src 'none'; script-src 'self'; connect-src 'self'; style-src 'self' 'unsafe-inline'; font-src data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'",
699
+ "Content-Security-Policy": "default-src 'none'; script-src 'self'; connect-src 'self'; style-src 'self' 'unsafe-inline'; font-src data:; base-uri 'none'; form-action 'self'; frame-ancestors 'self'",
759
700
  },
760
701
  });
761
702
  });
762
- app.put("/api/installations/:installationId/grants", bodyLimit({
763
- maxSize: 64 * 1024,
764
- onError: () => refusal(413, "Approval request too large"),
765
- }), async (context) => {
766
- // Capability-origin requests cannot become direct owner consent.
767
- if (context.req.raw.headers.has("X-Oyster-Installation"))
768
- return refusal(403, "Owner direct only");
769
- const principal = context.get("principal");
770
- if (!oysterPrincipalIsOwner(principal))
771
- return refusal(403, "Owner only");
703
+ table.put("/api/installations/:installationId/grants",
704
+ // Capability-origin requests cannot become direct owner consent.
705
+ owner("direct"), async (context, { principal }) => {
772
706
  const body = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))(await context.req.text());
773
707
  if (body._tag === "None")
774
708
  return refusal(400, "Invalid approval");
@@ -802,8 +736,15 @@ export function createOyster(host) {
802
736
  if (refused !== null)
803
737
  return refused;
804
738
  return Response.json(update.success.next);
739
+ }, {
740
+ middleware: [
741
+ bodyLimit({
742
+ maxSize: 64 * 1024,
743
+ onError: () => refusal(413, "Approval request too large"),
744
+ }),
745
+ ],
805
746
  });
806
- app.put("/api/installations/:installationId/release", async (context) => {
747
+ table.put("/api/installations/:installationId/release", owner("direct-or-app"), async (context, admitted) => {
807
748
  let release;
808
749
  try {
809
750
  release = parseRelease(await context.req.json());
@@ -817,16 +758,14 @@ export function createOyster(host) {
817
758
  return refusal(400, "Invalid installation request identity");
818
759
  // PUT retains its completed-response meaning. Cancellation only stops this
819
760
  // observer; Watchdog's retained command continues independently.
820
- return installOrSelect(host, context.req.param("installationId"), release, context.get("principal"), "install-or-select", context.req.raw.headers.has("X-Oyster-Installation")
821
- ? "app"
822
- : "owner-direct", key.data, context.req.raw.signal);
761
+ return installOrSelect(host, context.req.param("installationId"), release, admitted.principal, "install-or-select", admitted.origin, key.data, context.req.raw.signal);
823
762
  });
824
- app.delete("/api/installations/:installationId", async (context) => {
763
+ table.delete("/api/installations/:installationId", owner("direct-or-app"), async (context, admitted) => {
825
764
  const generation = await host.installations.generation();
826
765
  const current = await host.installations.get(context.req.param("installationId"));
827
766
  if (current === null || current.status === "deleted")
828
767
  return refusal(404, "Not found");
829
- const update = await reduceInstallation(current, { kind: "set-status", status: "deleted" }, await releaseOf(current.release), context.get("principal"), current);
768
+ const update = await reduceInstallation(current, { kind: "set-status", status: "deleted" }, await releaseOf(current.release), admitted.principal, current);
830
769
  if (Result.isFailure(update))
831
770
  return refusal(409, update.failure.code);
832
771
  const next = update.success.next;
@@ -844,20 +783,21 @@ export function createOyster(host) {
844
783
  });
845
784
  const enabled = async (context) => {
846
785
  const installationId = context.req.param("installationId") ?? "";
847
- const resolved = context.get("installation");
848
- const installation = resolved?.installationId === installationId
849
- ? resolved
850
- : await host.installations.get(installationId);
786
+ startTime(context, "registry");
787
+ const installation = await host.installations.get(installationId);
788
+ endTime(context, "registry");
851
789
  return installation?.status === "enabled" ? installation : null;
852
790
  };
853
- app.get("/apps/:installationId/", async (context) => {
791
+ table.get("/apps/:installationId/", appRoute(), async (context, admitted) => {
854
792
  const installation = await enabled(context);
855
793
  if (installation === null)
856
794
  return refusal(404, "Not found");
857
795
  const release = await releaseOf(installation.release);
858
796
  const declaration = release.declaration;
859
- const principal = context.get("principal");
797
+ const principal = admitted.principal;
860
798
  const facts = await gateFacts(installation, release, principal);
799
+ if (principal.actor.kind === "team-key" && facts.access === null)
800
+ return refusal(403, "App access denied");
861
801
  const capabilities = [];
862
802
  for (const capability of host.capabilities ?? []) {
863
803
  const methods = Object.fromEntries(Object.entries(capability.methods).filter(([name, method]) => admits(facts, {
@@ -939,10 +879,7 @@ export function createOyster(host) {
939
879
  },
940
880
  });
941
881
  });
942
- app.put("/apps/:installationId/backend", async (context) => {
943
- const principal = context.get("principal");
944
- if (oysterPrincipalIsVisitor(principal))
945
- return refusal(403, "Sign in to use this App");
882
+ table.put("/apps/:installationId/backend", appRoute(), async (context, { principal }) => {
946
883
  const installation = await enabled(context);
947
884
  if (installation === null)
948
885
  return refusal(404, "Not found");
@@ -971,10 +908,8 @@ export function createOyster(host) {
971
908
  .fetch(admittedCallerFacts(installation, release, invocationCallerContext(principal, host.computerId ?? null), decision.grants), request);
972
909
  return appFacetResponse(response);
973
910
  });
974
- app.put("/apps/:installationId/gills", async (context) => {
975
- const caller = invocationCallerContext(context.get("principal"), host.computerId ?? null);
976
- if (caller.actor.kind !== "member" || caller.role !== "owner")
977
- return refusal(403, "Owner only");
911
+ table.put("/apps/:installationId/gills", owner("direct-or-app"), async (context, { principal }) => {
912
+ const caller = invocationCallerContext(principal, host.computerId ?? null);
978
913
  const installation = await enabled(context);
979
914
  if (installation === null)
980
915
  return refusal(404, "Not found");
@@ -987,7 +922,9 @@ export function createOyster(host) {
987
922
  const request = decoded.value;
988
923
  const release = await releaseOf(installation.release);
989
924
  const gill = release.declaration?.gills?.find((item) => item.id === request.viewId);
990
- if (gill === undefined || release.api === null || release.backend === null)
925
+ if (gill === undefined ||
926
+ release.api === null ||
927
+ release.backend === null)
991
928
  return refusal(404, "Gill is not declared");
992
929
  if (installation.instanceId === null)
993
930
  return instanceRefusal("invalid_invocation_facts");
@@ -1020,7 +957,7 @@ export function createOyster(host) {
1020
957
  method?.effect !== "read" ||
1021
958
  !method.requires.every((capability) => release.declaration?.capabilities.includes(capability)))
1022
959
  return refusal(403, "Gill source is not admitted");
1023
- const current = await gateFacts(installation, release, context.get("principal"));
960
+ const current = await gateFacts(installation, release, principal);
1024
961
  if (current.declaredOperations.digest !==
1025
962
  installation.authority.declaredOperationsDigest ||
1026
963
  current.declaredOperations.artifactDigest !==
@@ -1111,17 +1048,16 @@ export function createOyster(host) {
1111
1048
  return instanceRefusal(result.code, result.causeClass);
1112
1049
  return methodResponse(result.value);
1113
1050
  };
1114
- app.post("/apps/:installationId/methods/:name", async (context) => {
1051
+ table.post("/apps/:installationId/methods/:name", appRoute(), async (context, { principal }) => {
1115
1052
  const installation = await enabled(context);
1116
1053
  if (installation === null)
1117
1054
  return refusal(404, "Not found");
1118
- return executeMethod(installation, await releaseOf(installation.release), context.get("principal"), context.req.param("name"), context.req.raw);
1055
+ startTime(context, "method", "admission and Instance call");
1056
+ const response = await executeMethod(installation, await releaseOf(installation.release), principal, context.req.param("name"), context.req.raw);
1057
+ endTime(context, "method");
1058
+ return response;
1119
1059
  });
1120
- app.post("/apps/:caller/calls/:appId/:method", async (context) => {
1121
- const principal = context.get("principal");
1122
- if (oysterPrincipalIsVisitor(principal) ||
1123
- principal.actor.kind !== "member")
1124
- return refusal(403, "app_call_member_only");
1060
+ table.post("/apps/:caller/calls/:appId/:method", member(), async (context, { principal }) => {
1125
1061
  const caller = await host.installations.get(context.req.param("caller"));
1126
1062
  if (caller?.status !== "enabled")
1127
1063
  return refusal(404, "Not found");
@@ -1182,5 +1118,12 @@ export function createOyster(host) {
1182
1118
  };
1183
1119
  return executeMethod(target, release, principal, method, context.req.raw, recheck);
1184
1120
  });
1185
- return app;
1121
+ return {
1122
+ declarations: () => table.declarations(),
1123
+ serve: () => serveHost(table, authenticate, ports, {
1124
+ // Server-Timing names the Durable Object hops a request waited on.
1125
+ serverTiming: { totalDescription: "Oyster" },
1126
+ headers: [noIndex],
1127
+ }),
1128
+ };
1186
1129
  }