@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.
- package/README.md +155 -63
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +50 -0
- package/dist/agent-schedules.js +17 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +97 -144
- package/dist/app-builds.d.ts +7 -2
- package/dist/app-builds.js +19 -2
- package/dist/approval-capability.d.ts +1 -1
- package/dist/approval-capability.js +11 -16
- package/dist/approval-home.d.ts +2 -1
- package/dist/approval-home.js +5 -3
- package/dist/apps/catalog.js +1 -1
- package/dist/apps/gate.d.ts +4 -7
- package/dist/apps/gate.js +14 -29
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/installation.d.ts +4 -42
- package/dist/apps/installation.js +7 -75
- package/dist/apps/operations.js +0 -10
- package/dist/apps/permissions.d.ts +18 -8
- package/dist/apps/permissions.js +7 -1
- package/dist/auth.js +9 -1
- package/dist/authorization.d.ts +307 -0
- package/dist/authorization.js +542 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/build-jobs.d.ts +2 -2
- package/dist/build/candidate-builder.d.ts +2 -0
- package/dist/build/candidate-builder.js +5 -1
- package/dist/build/candidate-cli.js +3 -0
- package/dist/build/candidate-cli.mjs +67 -22
- package/dist/build/job-log.d.ts +23 -0
- package/dist/build/job-log.js +88 -0
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +11 -9
- package/dist/channels/binding.d.ts +8 -1
- package/dist/channels/binding.js +12 -0
- package/dist/channels/conversations.d.ts +16 -5
- package/dist/channels/visitor-channel.d.ts +53 -0
- package/dist/channels/visitor-channel.js +124 -0
- package/dist/channels/visitor-edge.d.ts +115 -0
- package/dist/channels/visitor-edge.js +582 -0
- package/dist/channels/visitor-request.d.ts +7 -0
- package/dist/channels/visitor-request.js +17 -0
- package/dist/channels/visitor-stream-slots.d.ts +2 -0
- package/dist/channels/visitor-stream-slots.js +33 -0
- package/dist/channels/visitors.d.ts +47 -0
- package/dist/channels/visitors.js +181 -0
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.js +27 -1
- package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
- package/dist/{page/computer-target.js → computer-target.js} +3 -1
- package/dist/files.js +9 -8
- package/dist/git-admission.d.ts +3 -3
- package/dist/git-admission.js +4 -6
- package/dist/git-door.d.ts +8 -7
- package/dist/git-door.js +18 -17
- package/dist/git.d.ts +1 -1
- package/dist/host.d.ts +38 -42
- package/dist/host.js +182 -239
- package/dist/installation-execution.js +10 -5
- package/dist/installation-operations.d.ts +4 -180
- package/dist/installation-operations.js +1 -5
- package/dist/installation-store-generation.d.ts +4 -1
- package/dist/installation-store-generation.js +59 -6
- package/dist/internal/delivery.js +0 -1
- package/dist/internal/host-gate.d.ts +9 -6
- package/dist/internal/host-gate.js +19 -25
- package/dist/internal/invocation-timing.d.ts +16 -0
- package/dist/internal/invocation-timing.js +23 -0
- package/dist/internal/registry-installations.js +1 -0
- package/dist/invocation.d.ts +19 -11
- package/dist/invocation.js +171 -96
- package/dist/keys.d.ts +2 -19
- package/dist/keys.js +1 -37
- package/dist/mcp.d.ts +15 -27
- package/dist/mcp.js +105 -155
- package/dist/page/agent-browser.d.ts +15 -0
- package/dist/page/agent-browser.js +162 -0
- package/dist/page/app-permissions-port.d.ts +3 -0
- package/dist/page/app-permissions-port.js +43 -0
- package/dist/page/app-permissions.d.ts +11 -0
- package/dist/page/app-permissions.js +296 -0
- package/dist/page/capabilities.js +1 -1
- package/dist/page/child-selection.d.ts +27 -0
- package/dist/page/child-selection.js +38 -0
- package/dist/page/isolated-page-theme.d.ts +2 -0
- package/dist/page/isolated-page-theme.js +11 -0
- package/dist/page/mount.d.ts +2 -13
- package/dist/page/mount.js +8 -94
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- package/dist/release.d.ts +13 -0
- package/dist/release.js +10 -0
- package/dist/request-origin.d.ts +4 -0
- package/dist/request-origin.js +10 -0
- package/dist/standalone/agent-archive-owner.d.ts +42 -0
- package/dist/standalone/agent-archive-owner.js +116 -0
- package/dist/standalone/agent-archive-schedules.d.ts +6 -0
- package/dist/standalone/agent-archive-schedules.js +76 -0
- package/dist/standalone/agent-archive.d.ts +31 -0
- package/dist/standalone/agent-archive.js +67 -0
- package/dist/standalone/agent-computers.d.ts +7 -6
- package/dist/standalone/agent-computers.js +4 -7
- package/dist/standalone/agent-mail.d.ts +2 -2
- package/dist/standalone/agents.d.ts +157 -20
- package/dist/standalone/agents.js +242 -38
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/app-set.js +2 -54
- package/dist/standalone/app-source.d.ts +3 -4
- package/dist/standalone/app-source.js +12 -9
- package/dist/standalone/builds.d.ts +2 -14
- package/dist/standalone/builds.js +15 -13
- package/dist/standalone/computer-fleet.d.ts +4 -4
- package/dist/standalone/computer-fleet.js +20 -43
- package/dist/standalone/computer.d.ts +4 -3
- package/dist/standalone/computer.js +52 -53
- package/dist/standalone/dashboard-approval.d.ts +10 -0
- package/dist/standalone/dashboard-approval.js +66 -0
- package/dist/standalone/machine-link.d.ts +4 -221
- package/dist/standalone/machine-link.js +21 -15
- package/dist/standalone/media.d.ts +1 -0
- package/dist/standalone/media.js +20 -14
- package/dist/standalone/owner-routes.d.ts +16 -0
- package/dist/standalone/owner-routes.js +21 -0
- package/dist/standalone/permission-resources.d.ts +5 -0
- package/dist/standalone/permission-resources.js +31 -0
- package/dist/standalone/registry.d.ts +7 -0
- package/dist/standalone/registry.js +3 -0
- package/dist/standalone/tenancy.js +3 -1
- package/dist/standalone/worker-routes.d.ts +19 -0
- package/dist/standalone/worker-routes.js +9 -0
- package/dist/standalone/worker.d.ts +1 -1
- package/dist/standalone/worker.js +27 -35
- package/dist/standalone/workflow-binding.d.ts +8 -3
- package/dist/standalone/workflow-binding.js +14 -5
- package/dist/standalone/workflow-git-capture.js +2 -1
- package/dist/standalone/workflow-host.d.ts +8 -103
- package/dist/standalone/workflow-host.js +1 -1
- package/dist/standalone/workflow-node-capture.js +2 -2
- package/dist/standalone/workflow-root-authority.d.ts +1 -1
- package/dist/standalone/workflow-root-authority.js +1 -1
- package/dist/standalone/workflow-schedules.d.ts +14 -5
- package/dist/standalone/workflow-schedules.js +11 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +36 -28
- package/dist/standalone/workflows.js +6 -3
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.d.ts +7 -6
- package/dist/testing/capabilities.js +3 -6
- package/dist/testing/installation.d.ts +0 -1
- package/dist/testing/installation.js +2 -5
- package/dist/turnstile.d.ts +27 -0
- package/dist/turnstile.js +51 -0
- package/dist/workers-git.d.ts +12 -4
- package/dist/workers-git.js +58 -36
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +59 -20
- package/dist/workflows.d.ts +2 -0
- package/dist/workflows.js +1 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +175 -0
- package/docs/api/agents.md +34 -13
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +323 -225
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build-contract.md +2 -2
- package/docs/api/build.md +40 -37
- package/docs/api/cloudflare.md +27 -2
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +80 -199
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +15 -11
- package/docs/api/keys.md +1 -98
- package/docs/api/mcp.md +43 -40
- package/docs/api/modules.md +2 -0
- package/docs/api/page/mount.md +16 -19
- package/docs/api/workers-git.md +46 -12
- package/docs/api/workers.md +125 -235
- package/docs/site/agent-quickstart.mdx +6 -2
- package/examples/standalone/wrangler.celld.jsonc +66 -0
- package/package.json +64 -21
- package/dist/standalone/channel-session-door.d.ts +0 -4
- 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 {
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
302
|
-
return new Response(
|
|
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
|
-
|
|
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
|
|
342
|
+
const body = await readCachedVerified(host.artifacts, host.uiCache, asset.artifactRef, asset.digest);
|
|
321
343
|
const scope = `${origin}${prefix}/`;
|
|
322
|
-
return new Response(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
-
//
|
|
465
|
-
// admission
|
|
466
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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>${
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
848
|
-
const installation =
|
|
849
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
975
|
-
const caller = invocationCallerContext(
|
|
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 ||
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|