@fungi.computer/oyster 0.6.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 +132 -78
- 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 +93 -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.d.ts +1 -24
- package/dist/auth.js +9 -39
- 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 +59 -21
- 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/cloudflare.d.ts +4 -1
- package/dist/cloudflare.js +1 -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 +33 -40
- package/dist/host.js +153 -272
- 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 +3 -2
- package/dist/internal/host-gate.js +5 -18
- 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 +15 -14
- package/dist/invocation.js +58 -26
- 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/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 +7 -93
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- 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 +234 -30
- 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 +13 -42
- 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.d.ts +0 -3
- package/dist/standalone/tenancy.js +2 -5
- 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/workers-git.d.ts +12 -4
- package/dist/workers-git.js +38 -35
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +44 -8
- 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/auth.md +0 -55
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build.md +40 -37
- package/docs/api/cloudflare.md +3 -3
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +64 -192
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +14 -14
- 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 +124 -235
- package/package.json +39 -20
- package/dist/page/computer-target.d.ts +0 -3
- package/dist/page/computer-target.js +0 -9
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -72
package/dist/host.js
CHANGED
|
@@ -11,8 +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 {
|
|
15
|
-
import { endTime, startTime, timing } from "hono/timing";
|
|
14
|
+
import { endTime, startTime } from "hono/timing";
|
|
16
15
|
import { bodyLimit } from "hono/body-limit";
|
|
17
16
|
import { match } from "path-to-regexp";
|
|
18
17
|
import * as Schema from "effect/Schema";
|
|
@@ -21,7 +20,7 @@ import { emptyAppDeclaration } from "@fungi.computer/puffball/app";
|
|
|
21
20
|
import { applyInstallationCommand, installationIdSchema, } from "./apps/installation.js";
|
|
22
21
|
import { admits, scopedBindings, grantsFor } from "./apps/gate.js";
|
|
23
22
|
import { appPermissionPageSchema, permissionSaveSchema, } from "./apps/permissions.js";
|
|
24
|
-
import {
|
|
23
|
+
import { app as appRoute, apps as appsRoute, door, member, oysterPrincipalIsOwner, owner, publicRoute, authorityOf, hostAuthorityPorts, reachesInstallation, routes, serveCapability, serveHost, } from "./authorization.js";
|
|
25
24
|
import { deriveDeclaredOperations, appCallOperation, } from "./apps/operations.js";
|
|
26
25
|
import { appBackendActorHeader, appBackendComputerHeader, appMethodRefusalStatus, } from "@fungi.computer/agar/backend";
|
|
27
26
|
import { appCallsDescriptor, } from "./capability-descriptor.js";
|
|
@@ -40,15 +39,8 @@ import { Form, Field, FormFooter } from "@fungi.computer/caps/components/field";
|
|
|
40
39
|
import { Input } from "@fungi.computer/caps/components/input";
|
|
41
40
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
42
41
|
import { parseArtifactRef } from "@fungi.computer/puffball";
|
|
42
|
+
import { isBuildJobLogRef } from "./build/job-log.js";
|
|
43
43
|
export { ReleaseError, defaultAssetPolicy, parseRelease, resolveBackend, sha256, verifyRelease, } from "./release.js";
|
|
44
|
-
/** Narrow an admitted principal to the anonymous public reader. */
|
|
45
|
-
export function oysterPrincipalIsVisitor(principal) {
|
|
46
|
-
return principal.actor.kind === "visitor";
|
|
47
|
-
}
|
|
48
|
-
/** True only for a member admitted with the owner role. */
|
|
49
|
-
export function oysterPrincipalIsOwner(principal) {
|
|
50
|
-
return "role" in principal && principal.role === "owner";
|
|
51
|
-
}
|
|
52
44
|
/** Stable actor identity for host storage and backend attribution. */
|
|
53
45
|
export function oysterPrincipalId(principal) {
|
|
54
46
|
switch (principal.actor.kind) {
|
|
@@ -87,9 +79,6 @@ function escapeHtml(value) {
|
|
|
87
79
|
.replaceAll(">", ">")
|
|
88
80
|
.replaceAll('"', """);
|
|
89
81
|
}
|
|
90
|
-
const visibilitySchema = Schema.Struct({
|
|
91
|
-
visibility: Schema.Literals(["private", "public"]),
|
|
92
|
-
});
|
|
93
82
|
function refusal(status, error, retryAfter) {
|
|
94
83
|
const init = { status };
|
|
95
84
|
if (retryAfter !== undefined)
|
|
@@ -195,27 +184,32 @@ export async function installOrSelect(host, installationId, release, principal,
|
|
|
195
184
|
/** The tab icon of every Oyster page a person opens at the top level. */
|
|
196
185
|
const tabIcon = `<link rel="icon" type="image/svg+xml" href="${fungiFaviconHref}">`;
|
|
197
186
|
/**
|
|
198
|
-
* Create the Oyster App server: a
|
|
187
|
+
* Create the Oyster App server: a served route table that installs App Releases, serves
|
|
199
188
|
* their UI, forwards their backend requests and methods to their Instances,
|
|
200
189
|
* and mounts the host's MCP endpoint. The host's adapters decide where state
|
|
201
190
|
* lives and who may sign in.
|
|
202
191
|
*/
|
|
203
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) {
|
|
204
200
|
const limits = guestLimits(host.guestLimits);
|
|
205
|
-
const
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
context.res = new Response(context.res.body, context.res);
|
|
218
|
-
});
|
|
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);
|
|
219
213
|
const releaseOf = releaseCache(host.artifacts);
|
|
220
214
|
const operationDescriptors = host.capabilities ?? [];
|
|
221
215
|
const gateFacts = (installation, release, principal) => hostGateFacts(host, installation, release, principal);
|
|
@@ -248,17 +242,15 @@ export function createOyster(host) {
|
|
|
248
242
|
source,
|
|
249
243
|
});
|
|
250
244
|
const prepareAssets = async () => {
|
|
251
|
-
const { pageScript,
|
|
245
|
+
const { pageScript, grantConsentScript, fontStyles, loginFontStyles, shellStyles, } = await (host.pageAssets ?? loadPageAssets)();
|
|
252
246
|
return Promise.all([
|
|
253
247
|
asset("page.js", "text/javascript; charset=utf-8", host.pageScript ?? pageScript),
|
|
254
|
-
asset("public-home.js", "text/javascript; charset=utf-8", publicHomeScript),
|
|
255
248
|
asset("grant-consent.js", "text/javascript; charset=utf-8", grantConsentScript),
|
|
256
249
|
asset("fonts.css", "text/css; charset=utf-8", fontStyles),
|
|
257
250
|
asset("login-fonts.css", "text/css; charset=utf-8", loginFontStyles),
|
|
258
251
|
asset("styles.css", "text/css; charset=utf-8", shellStyles),
|
|
259
|
-
]).then(([page,
|
|
252
|
+
]).then(([page, grantConsent, fonts, loginFonts, styles]) => ({
|
|
260
253
|
page,
|
|
261
|
-
publicHome,
|
|
262
254
|
grantConsent,
|
|
263
255
|
fonts,
|
|
264
256
|
loginFonts,
|
|
@@ -275,22 +267,10 @@ export function createOyster(host) {
|
|
|
275
267
|
// An Oyster is a private instance, never a search result: every response
|
|
276
268
|
// says so, and robots.txt is public so crawlers read the rule rather than a
|
|
277
269
|
// 401 (which Google treats as "crawl everything").
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
context.res.headers.set("X-Robots-Tag", "noindex, nofollow");
|
|
282
|
-
}
|
|
283
|
-
catch (cause) {
|
|
284
|
-
// Fetched responses (proxied Apps, the git door) have immutable headers.
|
|
285
|
-
if (!(cause instanceof TypeError))
|
|
286
|
-
throw cause;
|
|
287
|
-
const { body, status, statusText, headers } = context.res;
|
|
288
|
-
const marked = new Headers(headers);
|
|
289
|
-
marked.set("X-Robots-Tag", "noindex, nofollow");
|
|
290
|
-
context.res = new Response(body, { status, statusText, headers: marked });
|
|
291
|
-
}
|
|
270
|
+
const noIndex = () => ({
|
|
271
|
+
"X-Robots-Tag": "noindex, nofollow",
|
|
292
272
|
});
|
|
293
|
-
|
|
273
|
+
table.get("/robots.txt", publicRoute("the crawl rule for a private instance"), () => new Response("User-agent: *\nDisallow: /\n", {
|
|
294
274
|
headers: {
|
|
295
275
|
"Content-Type": "text/plain; charset=utf-8",
|
|
296
276
|
"Cache-Control": "public, max-age=3600",
|
|
@@ -300,7 +280,7 @@ export function createOyster(host) {
|
|
|
300
280
|
// own URL: released Oyster 0.2.0 served that URL immutable, so a browser may
|
|
301
281
|
// hold a copy it would import without a request. The entry is never cached
|
|
302
282
|
// and rechecks current approval before it names the module.
|
|
303
|
-
|
|
283
|
+
table.get(`/ui-entry/:installationId/:releaseId${shellModuleAssetPath}`, publicRoute("never cached; rechecks the shell's approval before naming it"), async (context) => {
|
|
304
284
|
const installation = await host.installations.get(context.req.param("installationId"));
|
|
305
285
|
if (installation?.status !== "enabled" ||
|
|
306
286
|
installation.release.releaseId !== context.req.param("releaseId"))
|
|
@@ -322,7 +302,7 @@ export function createOyster(host) {
|
|
|
322
302
|
});
|
|
323
303
|
// App UI: digest-pinned, sandboxed, and readable without a session because
|
|
324
304
|
// the opaque-origin frame cannot carry one.
|
|
325
|
-
|
|
305
|
+
table.get("/ui/:installationId/:releaseId/*", publicRoute("digest-pinned App bytes for an opaque-origin frame"), async (context) => {
|
|
326
306
|
startTime(context, "registry");
|
|
327
307
|
const installation = await host.installations.get(context.req.param("installationId"));
|
|
328
308
|
endTime(context, "registry");
|
|
@@ -373,7 +353,7 @@ export function createOyster(host) {
|
|
|
373
353
|
},
|
|
374
354
|
});
|
|
375
355
|
});
|
|
376
|
-
|
|
356
|
+
table.get("/_oyster/assets/*", publicRoute("content-addressed host page assets"), async (context) => {
|
|
377
357
|
const wanted = new URL(context.req.url).pathname;
|
|
378
358
|
const found = Object.values(await assets()).find(({ path }) => path === wanted);
|
|
379
359
|
if (found === undefined)
|
|
@@ -386,7 +366,7 @@ export function createOyster(host) {
|
|
|
386
366
|
},
|
|
387
367
|
});
|
|
388
368
|
});
|
|
389
|
-
|
|
369
|
+
table.get("/login", publicRoute("the sign-in form"), async () => {
|
|
390
370
|
const { loginFonts, styles } = await assets();
|
|
391
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, {
|
|
392
372
|
label: "Owner key",
|
|
@@ -405,16 +385,16 @@ export function createOyster(host) {
|
|
|
405
385
|
},
|
|
406
386
|
});
|
|
407
387
|
});
|
|
408
|
-
|
|
388
|
+
table.post("/login", door("the host's own sign-in checks the posted key"), (context) => host.auth.login === undefined
|
|
409
389
|
? refusal(404, "Sign-in is not available")
|
|
410
390
|
: host.auth.login(context.req.raw));
|
|
411
391
|
if (host.mcp !== undefined) {
|
|
412
392
|
const mcp = host.mcp;
|
|
413
|
-
|
|
393
|
+
table.all("/mcp", door("MCP authenticates every request; tools read this module's reach"), (context) => mcp.handleRequest(context.req.raw));
|
|
414
394
|
}
|
|
415
395
|
if (host.git !== undefined) {
|
|
416
396
|
const git = host.git;
|
|
417
|
-
|
|
397
|
+
const gitDoor = createGitDoor({
|
|
418
398
|
authenticate: async (request, target) => {
|
|
419
399
|
// A Workflow Git pass header is admitted only by the pass port,
|
|
420
400
|
// never as a session or key.
|
|
@@ -430,80 +410,27 @@ export function createOyster(host) {
|
|
|
430
410
|
return git.buildPass === undefined
|
|
431
411
|
? null
|
|
432
412
|
: git.buildPass(presented.value, target);
|
|
433
|
-
|
|
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);
|
|
434
417
|
if (result.kind !== "admitted")
|
|
435
418
|
return null;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const key = (await host.keys?.live(actor.keyId)) ?? null;
|
|
442
|
-
const access = key === null ? null : keyAppAccess(key, target.installationId);
|
|
443
|
-
if (access === null ||
|
|
444
|
-
(target.scope === "write" && access.level !== "write"))
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
return git.writerOf(result.principal);
|
|
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))
|
|
422
|
+
? git.writerOf(result.principal)
|
|
423
|
+
: null;
|
|
448
424
|
},
|
|
449
|
-
isPublic: async (installationId) => (await host.installations.get(installationId))?.visibility ===
|
|
450
|
-
"public",
|
|
451
425
|
git: async (installationId) => {
|
|
452
426
|
const installation = await host.installations.get(installationId);
|
|
453
427
|
if (installation?.status !== "enabled")
|
|
454
428
|
return null;
|
|
455
429
|
return git.git(installation);
|
|
456
430
|
},
|
|
457
|
-
})
|
|
431
|
+
});
|
|
432
|
+
table.all("/git/*", door("Git passes and keys; a key's reach is checked per installation"), (context) => gitDoor.fetch(context.req.raw));
|
|
458
433
|
}
|
|
459
|
-
// A public installation's page and read methods admit anonymous visitors.
|
|
460
|
-
const visitor = { actor: { kind: "visitor" } };
|
|
461
|
-
const visitorRoute = /^\/apps\/([^/]+)\/(?:|methods\/([A-Za-z0-9_.]+))$/u;
|
|
462
|
-
const visitorMayReach = async (request) => {
|
|
463
|
-
const match = visitorRoute.exec(new URL(request.url).pathname);
|
|
464
|
-
if (match === null)
|
|
465
|
-
return null;
|
|
466
|
-
const [, installationId = "", method] = match;
|
|
467
|
-
const page = method === undefined;
|
|
468
|
-
if (page ? request.method !== "GET" : request.method !== "POST")
|
|
469
|
-
return null;
|
|
470
|
-
const installation = await host.installations.get(installationId);
|
|
471
|
-
if (installation?.status !== "enabled" ||
|
|
472
|
-
installation.visibility !== "public")
|
|
473
|
-
return null;
|
|
474
|
-
if (page)
|
|
475
|
-
return installation;
|
|
476
|
-
const release = await releaseOf(installation.release);
|
|
477
|
-
return release.api?.methods[method]?.effect === "read"
|
|
478
|
-
? installation
|
|
479
|
-
: null;
|
|
480
|
-
};
|
|
481
|
-
// The public home and public App reads admit visitors; other routes require authentication.
|
|
482
|
-
app.use("*", async (context, next) => {
|
|
483
|
-
const result = await host.auth.authenticate(context.req.raw);
|
|
484
|
-
const installation = result.kind === "refused" ? await visitorMayReach(context.req.raw) : null;
|
|
485
|
-
if (result.kind === "refused" &&
|
|
486
|
-
(result.status === 401 || result.status === 403) &&
|
|
487
|
-
((context.req.method === "GET" && context.req.path === "/") ||
|
|
488
|
-
installation !== null)) {
|
|
489
|
-
if (installation !== null)
|
|
490
|
-
context.set("installation", installation);
|
|
491
|
-
context.set("principal", visitor);
|
|
492
|
-
await next();
|
|
493
|
-
return undefined;
|
|
494
|
-
}
|
|
495
|
-
if (result.kind === "refused") {
|
|
496
|
-
const accept = context.req.header("Accept") ?? "";
|
|
497
|
-
if ((result.status === 401 || result.status === 403) &&
|
|
498
|
-
context.req.method === "GET" &&
|
|
499
|
-
accept.includes("text/html"))
|
|
500
|
-
return context.redirect("/login");
|
|
501
|
-
return refusal(result.status, "Sign in to use this Oyster", result.retryAfter);
|
|
502
|
-
}
|
|
503
|
-
context.set("principal", result.principal);
|
|
504
|
-
await next();
|
|
505
|
-
return undefined;
|
|
506
|
-
});
|
|
507
434
|
const capabilityRoutes = (host.capabilities ?? []).flatMap((capability) => Object.entries(capability.methods).map(([name, method]) => ({
|
|
508
435
|
capability,
|
|
509
436
|
operation: `route:${capability.id}:${name}`,
|
|
@@ -514,17 +441,26 @@ export function createOyster(host) {
|
|
|
514
441
|
sensitive: true,
|
|
515
442
|
}),
|
|
516
443
|
})));
|
|
517
|
-
//
|
|
518
|
-
// admission
|
|
519
|
-
|
|
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) => {
|
|
520
459
|
const url = new URL(context.req.url);
|
|
521
460
|
const candidates = capabilityRoutes.filter((route) => (route.method === "ANY" || route.method === context.req.method) &&
|
|
522
461
|
route.matches(url.pathname));
|
|
523
462
|
if (candidates.length === 0)
|
|
524
|
-
return
|
|
525
|
-
const principal = context.get("principal");
|
|
526
|
-
if (oysterPrincipalIsVisitor(principal))
|
|
527
|
-
return refusal(401, "Sign in to use capabilities");
|
|
463
|
+
return refusal(404, "Not found");
|
|
528
464
|
const browserTransport = context.req.header("Upgrade")?.toLowerCase() === "websocket" &&
|
|
529
465
|
context.req.header("Origin") === url.origin &&
|
|
530
466
|
candidates.some((route) => route.kind === "transport");
|
|
@@ -548,57 +484,45 @@ export function createOyster(host) {
|
|
|
548
484
|
}).kind === "refused")
|
|
549
485
|
continue;
|
|
550
486
|
url.pathname = `/${url.pathname.slice(capability.base.length)}`;
|
|
551
|
-
return capability.
|
|
552
|
-
principal,
|
|
553
|
-
installation,
|
|
554
|
-
});
|
|
487
|
+
return capabilityTable(capability).fetch(new Request(url, context.req.raw), { principal, installation });
|
|
555
488
|
}
|
|
556
489
|
return refusal(403, "Capability permission denied");
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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) {
|
|
565
499
|
const home = await host.installations.get(host.homeInstallationId);
|
|
566
|
-
if (home?.status === "enabled" &&
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
(key !== null &&
|
|
575
|
-
keyAppAccess(key, installation.installationId) !== null)));
|
|
576
|
-
if (publicHome) {
|
|
577
|
-
const apps = [];
|
|
578
|
-
for (const installation of installations) {
|
|
579
|
-
const release = await releaseOf(installation.release);
|
|
580
|
-
if (release.ui === null)
|
|
581
|
-
continue;
|
|
582
|
-
apps.push({
|
|
583
|
-
href: `/apps/${encodeURIComponent(installation.installationId)}/`,
|
|
584
|
-
displayName: release.declaration?.displayName ?? "App",
|
|
585
|
-
icon: release.declaration?.icon ?? null,
|
|
586
|
-
});
|
|
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.
|
|
587
508
|
}
|
|
588
|
-
const config = JSON.stringify({ title, apps }).replaceAll("<", "\\u003c");
|
|
589
|
-
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>`, {
|
|
590
|
-
headers: {
|
|
591
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
592
|
-
"Cache-Control": "no-store",
|
|
593
|
-
"X-Content-Type-Options": "nosniff",
|
|
594
|
-
"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'",
|
|
595
|
-
},
|
|
596
|
-
});
|
|
597
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);
|
|
598
516
|
const items = installations
|
|
599
|
-
.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>`)
|
|
600
518
|
.join("");
|
|
601
|
-
|
|
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>`, {
|
|
602
526
|
headers: {
|
|
603
527
|
"Content-Type": "text/html; charset=utf-8",
|
|
604
528
|
"Cache-Control": "no-store",
|
|
@@ -606,51 +530,9 @@ export function createOyster(host) {
|
|
|
606
530
|
},
|
|
607
531
|
});
|
|
608
532
|
});
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
await next();
|
|
613
|
-
return undefined;
|
|
614
|
-
});
|
|
615
|
-
app.put("/api/installations/:installationId/visibility", async (context) => {
|
|
616
|
-
const generation = await host.installations.generation();
|
|
617
|
-
const current = await host.installations.get(context.req.param("installationId"));
|
|
618
|
-
if (current === null || current.status === "deleted")
|
|
619
|
-
return refusal(404, "Not found");
|
|
620
|
-
const body = Schema.decodeUnknownOption(visibilitySchema)(await context.req.json());
|
|
621
|
-
if (body._tag === "None")
|
|
622
|
-
return refusal(400, "Invalid visibility");
|
|
623
|
-
const update = await reduceInstallation(current, { kind: "set-visibility", visibility: body.value.visibility }, await releaseOf(current.release), context.get("principal"), current);
|
|
624
|
-
if (Result.isFailure(update))
|
|
625
|
-
return refusal(409, update.failure.code);
|
|
626
|
-
const next = update.success.next;
|
|
627
|
-
const release = await releaseOf(current.release);
|
|
628
|
-
const refused = await prepare(next, release);
|
|
629
|
-
if (refused !== null)
|
|
630
|
-
return refused;
|
|
631
|
-
const commit = await host.installations.compareAndPut(next, current.authority.authorityVersion, generation);
|
|
632
|
-
if (!commit.ok)
|
|
633
|
-
return refusal(409, commit.code);
|
|
634
|
-
const activationRefusal = await apply(next, release);
|
|
635
|
-
if (activationRefusal !== null)
|
|
636
|
-
return activationRefusal;
|
|
637
|
-
return Response.json(next);
|
|
638
|
-
});
|
|
639
|
-
if (host.ownerRoutes !== undefined) {
|
|
640
|
-
const ownerRoutes = host.ownerRoutes;
|
|
641
|
-
const owned = new Hono();
|
|
642
|
-
owned.use("/api/*", async (context, next) => {
|
|
643
|
-
if (!oysterPrincipalIsOwner(context.get("principal")))
|
|
644
|
-
return refusal(403, "Owner only");
|
|
645
|
-
await next();
|
|
646
|
-
return undefined;
|
|
647
|
-
});
|
|
648
|
-
owned.route("/", ownerRoutes);
|
|
649
|
-
app.route("/", owned);
|
|
650
|
-
}
|
|
651
|
-
app.get("/api/workbench/directory", async (context) => {
|
|
652
|
-
if (!oysterPrincipalIsOwner(context.get("principal")))
|
|
653
|
-
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) => {
|
|
654
536
|
if (host.workbench === undefined)
|
|
655
537
|
return refusal(404, "Not found");
|
|
656
538
|
startTime(context, "registry");
|
|
@@ -665,10 +547,7 @@ export function createOyster(host) {
|
|
|
665
547
|
headers: { "Cache-Control": "no-store" },
|
|
666
548
|
});
|
|
667
549
|
});
|
|
668
|
-
|
|
669
|
-
const principal = context.get("principal");
|
|
670
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
671
|
-
return refusal(403, "Owner only");
|
|
550
|
+
table.all("/api/workbench/storage/:installationId/:operation", owner("direct-or-app"), async (context, { principal }) => {
|
|
672
551
|
const workbench = host.workbench;
|
|
673
552
|
if (workbench === undefined)
|
|
674
553
|
return refusal(404, "Not found");
|
|
@@ -702,8 +581,8 @@ export function createOyster(host) {
|
|
|
702
581
|
}
|
|
703
582
|
return refusal(404, "Not found");
|
|
704
583
|
});
|
|
705
|
-
|
|
706
|
-
|
|
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) => {
|
|
707
586
|
let artifactRef;
|
|
708
587
|
try {
|
|
709
588
|
artifactRef = parseArtifactRef(context.req.param("artifactRef"));
|
|
@@ -711,6 +590,8 @@ export function createOyster(host) {
|
|
|
711
590
|
catch {
|
|
712
591
|
return refusal(400, "Invalid artifact reference");
|
|
713
592
|
}
|
|
593
|
+
if (isBuildJobLogRef(artifactRef))
|
|
594
|
+
return refusal(409, "build_job_log_ref_reserved");
|
|
714
595
|
const expected = context.req.query("digest");
|
|
715
596
|
const bytes = new Uint8Array(await context.req.arrayBuffer());
|
|
716
597
|
if (expected === undefined || (await sha256(bytes)) !== expected)
|
|
@@ -734,7 +615,7 @@ export function createOyster(host) {
|
|
|
734
615
|
.apply({ ...selectionOf(installation), status: installation.status }, installation.release);
|
|
735
616
|
return result.ok ? null : instanceRefusal(result.code, result.causeClass);
|
|
736
617
|
};
|
|
737
|
-
|
|
618
|
+
table.post("/api/installations", owner("direct-or-app"), async (context, { origin, principal }) => {
|
|
738
619
|
let request;
|
|
739
620
|
let release;
|
|
740
621
|
try {
|
|
@@ -750,17 +631,12 @@ export function createOyster(host) {
|
|
|
750
631
|
const key = installationRequestIdSchema.safeParse(context.req.header("Idempotency-Key"));
|
|
751
632
|
if (!key.success)
|
|
752
633
|
return refusal(400, "Invalid installation request identity");
|
|
753
|
-
const admitted = await admitInstallation(host, installationId, release,
|
|
754
|
-
? "app"
|
|
755
|
-
: "owner-direct", key.data);
|
|
634
|
+
const admitted = await admitInstallation(host, installationId, release, principal, "install-or-remint", origin, key.data);
|
|
756
635
|
return admitted instanceof Response
|
|
757
636
|
? admitted
|
|
758
637
|
: Response.json(admitted, { status: 202 });
|
|
759
638
|
});
|
|
760
|
-
|
|
761
|
-
if (!oysterPrincipalIsOwner(context.get("principal")) ||
|
|
762
|
-
context.req.raw.headers.has("X-Oyster-Installation"))
|
|
763
|
-
return refusal(403, "Owner only");
|
|
639
|
+
table.get("/api/installations/:installationId/status", owner("direct"), async (context) => {
|
|
764
640
|
const key = installationRequestIdSchema.safeParse(context.req.query("requestId"));
|
|
765
641
|
if (!key.success)
|
|
766
642
|
return refusal(400, "Invalid installation request identity");
|
|
@@ -779,9 +655,7 @@ export function createOyster(host) {
|
|
|
779
655
|
throw cause;
|
|
780
656
|
}
|
|
781
657
|
});
|
|
782
|
-
|
|
783
|
-
if (context.req.raw.headers.has("X-Oyster-Installation"))
|
|
784
|
-
return refusal(403, "Owner direct only");
|
|
658
|
+
table.get("/api/installations/:installationId/grants", owner("direct"), async (context, { principal }) => {
|
|
785
659
|
const generation = await host.installations.generation();
|
|
786
660
|
const current = await host.installations.get(context.req.param("installationId"));
|
|
787
661
|
if (current === null || current.status === "deleted")
|
|
@@ -795,6 +669,18 @@ export function createOyster(host) {
|
|
|
795
669
|
appId: current.appId,
|
|
796
670
|
appName: declaration.displayName ?? current.installationId,
|
|
797
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),
|
|
798
684
|
permissions: {
|
|
799
685
|
authority: current.authority,
|
|
800
686
|
declaredOperations: declared,
|
|
@@ -810,20 +696,13 @@ export function createOyster(host) {
|
|
|
810
696
|
"Content-Type": "text/html; charset=utf-8",
|
|
811
697
|
"Cache-Control": "no-store",
|
|
812
698
|
"X-Content-Type-Options": "nosniff",
|
|
813
|
-
"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'",
|
|
814
700
|
},
|
|
815
701
|
});
|
|
816
702
|
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}), async (context) => {
|
|
821
|
-
// Capability-origin requests cannot become direct owner consent.
|
|
822
|
-
if (context.req.raw.headers.has("X-Oyster-Installation"))
|
|
823
|
-
return refusal(403, "Owner direct only");
|
|
824
|
-
const principal = context.get("principal");
|
|
825
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
826
|
-
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 }) => {
|
|
827
706
|
const body = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))(await context.req.text());
|
|
828
707
|
if (body._tag === "None")
|
|
829
708
|
return refusal(400, "Invalid approval");
|
|
@@ -857,8 +736,15 @@ export function createOyster(host) {
|
|
|
857
736
|
if (refused !== null)
|
|
858
737
|
return refused;
|
|
859
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
|
+
],
|
|
860
746
|
});
|
|
861
|
-
|
|
747
|
+
table.put("/api/installations/:installationId/release", owner("direct-or-app"), async (context, admitted) => {
|
|
862
748
|
let release;
|
|
863
749
|
try {
|
|
864
750
|
release = parseRelease(await context.req.json());
|
|
@@ -872,16 +758,14 @@ export function createOyster(host) {
|
|
|
872
758
|
return refusal(400, "Invalid installation request identity");
|
|
873
759
|
// PUT retains its completed-response meaning. Cancellation only stops this
|
|
874
760
|
// observer; Watchdog's retained command continues independently.
|
|
875
|
-
return installOrSelect(host, context.req.param("installationId"), release,
|
|
876
|
-
? "app"
|
|
877
|
-
: "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);
|
|
878
762
|
});
|
|
879
|
-
|
|
763
|
+
table.delete("/api/installations/:installationId", owner("direct-or-app"), async (context, admitted) => {
|
|
880
764
|
const generation = await host.installations.generation();
|
|
881
765
|
const current = await host.installations.get(context.req.param("installationId"));
|
|
882
766
|
if (current === null || current.status === "deleted")
|
|
883
767
|
return refusal(404, "Not found");
|
|
884
|
-
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);
|
|
885
769
|
if (Result.isFailure(update))
|
|
886
770
|
return refusal(409, update.failure.code);
|
|
887
771
|
const next = update.success.next;
|
|
@@ -899,21 +783,18 @@ export function createOyster(host) {
|
|
|
899
783
|
});
|
|
900
784
|
const enabled = async (context) => {
|
|
901
785
|
const installationId = context.req.param("installationId") ?? "";
|
|
902
|
-
const resolved = context.get("installation");
|
|
903
|
-
if (resolved?.installationId === installationId)
|
|
904
|
-
return resolved.status === "enabled" ? resolved : null;
|
|
905
786
|
startTime(context, "registry");
|
|
906
787
|
const installation = await host.installations.get(installationId);
|
|
907
788
|
endTime(context, "registry");
|
|
908
789
|
return installation?.status === "enabled" ? installation : null;
|
|
909
790
|
};
|
|
910
|
-
|
|
791
|
+
table.get("/apps/:installationId/", appRoute(), async (context, admitted) => {
|
|
911
792
|
const installation = await enabled(context);
|
|
912
793
|
if (installation === null)
|
|
913
794
|
return refusal(404, "Not found");
|
|
914
795
|
const release = await releaseOf(installation.release);
|
|
915
796
|
const declaration = release.declaration;
|
|
916
|
-
const principal =
|
|
797
|
+
const principal = admitted.principal;
|
|
917
798
|
const facts = await gateFacts(installation, release, principal);
|
|
918
799
|
if (principal.actor.kind === "team-key" && facts.access === null)
|
|
919
800
|
return refusal(403, "App access denied");
|
|
@@ -998,10 +879,7 @@ export function createOyster(host) {
|
|
|
998
879
|
},
|
|
999
880
|
});
|
|
1000
881
|
});
|
|
1001
|
-
|
|
1002
|
-
const principal = context.get("principal");
|
|
1003
|
-
if (oysterPrincipalIsVisitor(principal))
|
|
1004
|
-
return refusal(403, "Sign in to use this App");
|
|
882
|
+
table.put("/apps/:installationId/backend", appRoute(), async (context, { principal }) => {
|
|
1005
883
|
const installation = await enabled(context);
|
|
1006
884
|
if (installation === null)
|
|
1007
885
|
return refusal(404, "Not found");
|
|
@@ -1030,10 +908,8 @@ export function createOyster(host) {
|
|
|
1030
908
|
.fetch(admittedCallerFacts(installation, release, invocationCallerContext(principal, host.computerId ?? null), decision.grants), request);
|
|
1031
909
|
return appFacetResponse(response);
|
|
1032
910
|
});
|
|
1033
|
-
|
|
1034
|
-
const caller = invocationCallerContext(
|
|
1035
|
-
if (caller.actor.kind !== "member" || caller.role !== "owner")
|
|
1036
|
-
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);
|
|
1037
913
|
const installation = await enabled(context);
|
|
1038
914
|
if (installation === null)
|
|
1039
915
|
return refusal(404, "Not found");
|
|
@@ -1046,7 +922,9 @@ export function createOyster(host) {
|
|
|
1046
922
|
const request = decoded.value;
|
|
1047
923
|
const release = await releaseOf(installation.release);
|
|
1048
924
|
const gill = release.declaration?.gills?.find((item) => item.id === request.viewId);
|
|
1049
|
-
if (gill === undefined ||
|
|
925
|
+
if (gill === undefined ||
|
|
926
|
+
release.api === null ||
|
|
927
|
+
release.backend === null)
|
|
1050
928
|
return refusal(404, "Gill is not declared");
|
|
1051
929
|
if (installation.instanceId === null)
|
|
1052
930
|
return instanceRefusal("invalid_invocation_facts");
|
|
@@ -1079,7 +957,7 @@ export function createOyster(host) {
|
|
|
1079
957
|
method?.effect !== "read" ||
|
|
1080
958
|
!method.requires.every((capability) => release.declaration?.capabilities.includes(capability)))
|
|
1081
959
|
return refusal(403, "Gill source is not admitted");
|
|
1082
|
-
const current = await gateFacts(installation, release,
|
|
960
|
+
const current = await gateFacts(installation, release, principal);
|
|
1083
961
|
if (current.declaredOperations.digest !==
|
|
1084
962
|
installation.authority.declaredOperationsDigest ||
|
|
1085
963
|
current.declaredOperations.artifactDigest !==
|
|
@@ -1170,20 +1048,16 @@ export function createOyster(host) {
|
|
|
1170
1048
|
return instanceRefusal(result.code, result.causeClass);
|
|
1171
1049
|
return methodResponse(result.value);
|
|
1172
1050
|
};
|
|
1173
|
-
|
|
1051
|
+
table.post("/apps/:installationId/methods/:name", appRoute(), async (context, { principal }) => {
|
|
1174
1052
|
const installation = await enabled(context);
|
|
1175
1053
|
if (installation === null)
|
|
1176
1054
|
return refusal(404, "Not found");
|
|
1177
1055
|
startTime(context, "method", "admission and Instance call");
|
|
1178
|
-
const response = await executeMethod(installation, await releaseOf(installation.release),
|
|
1056
|
+
const response = await executeMethod(installation, await releaseOf(installation.release), principal, context.req.param("name"), context.req.raw);
|
|
1179
1057
|
endTime(context, "method");
|
|
1180
1058
|
return response;
|
|
1181
1059
|
});
|
|
1182
|
-
|
|
1183
|
-
const principal = context.get("principal");
|
|
1184
|
-
if (oysterPrincipalIsVisitor(principal) ||
|
|
1185
|
-
principal.actor.kind !== "member")
|
|
1186
|
-
return refusal(403, "app_call_member_only");
|
|
1060
|
+
table.post("/apps/:caller/calls/:appId/:method", member(), async (context, { principal }) => {
|
|
1187
1061
|
const caller = await host.installations.get(context.req.param("caller"));
|
|
1188
1062
|
if (caller?.status !== "enabled")
|
|
1189
1063
|
return refusal(404, "Not found");
|
|
@@ -1244,5 +1118,12 @@ export function createOyster(host) {
|
|
|
1244
1118
|
};
|
|
1245
1119
|
return executeMethod(target, release, principal, method, context.req.raw, recheck);
|
|
1246
1120
|
});
|
|
1247
|
-
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
|
+
};
|
|
1248
1129
|
}
|