@fungi.computer/oyster 0.1.0 → 0.3.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 +376 -72
- package/dist/agent-avatars.d.ts +103 -0
- package/dist/agent-avatars.js +66 -0
- package/dist/agents.d.ts +15 -4
- package/dist/agents.js +43 -4
- package/dist/app-builds.d.ts +81 -8
- package/dist/app-builds.js +210 -60
- package/dist/app-storage.d.ts +3 -3
- package/dist/approval-capability.d.ts +5 -0
- package/dist/approval-capability.js +46 -0
- package/dist/approval-home.d.ts +51 -0
- package/dist/approval-home.js +226 -0
- package/dist/apps/catalog.d.ts +335 -0
- package/dist/apps/catalog.js +46 -0
- package/dist/apps/gate.d.ts +26 -12
- package/dist/apps/gate.js +48 -22
- package/dist/apps/index.d.ts +3 -1
- package/dist/apps/index.js +3 -1
- package/dist/apps/installation.d.ts +147 -66
- package/dist/apps/installation.js +193 -86
- package/dist/apps/permissions.d.ts +247 -0
- package/dist/apps/permissions.js +255 -0
- package/dist/boot-capability-route.d.ts +15 -0
- package/dist/boot-capability-route.js +196 -0
- package/dist/boot-capability.d.ts +103 -0
- package/dist/boot-capability.js +141 -0
- package/dist/boot-construction.d.ts +12 -0
- package/dist/boot-construction.js +175 -0
- package/dist/boot-gate.d.ts +267 -0
- package/dist/boot-gate.js +236 -0
- package/dist/boot-host-script.d.ts +2 -0
- package/dist/boot-host-script.js +1 -0
- package/dist/boot-mount.d.ts +21 -0
- package/dist/boot-mount.js +65 -0
- package/dist/browser/boot.d.ts +73 -0
- package/dist/browser/boot.js +502 -0
- package/dist/browser/browser.d.ts +104 -0
- package/dist/browser/browser.js +374 -0
- package/dist/browser/capability-source.d.ts +76 -0
- package/dist/browser/capability-source.js +338 -0
- package/dist/browser/cdp-client.d.ts +56 -0
- package/dist/browser/cdp-client.js +184 -0
- package/dist/browser/errors.d.ts +79 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/index.d.ts +16 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/lease-registry.d.ts +61 -0
- package/dist/browser/lease-registry.js +101 -0
- package/dist/browser/operations.d.ts +225 -0
- package/dist/browser/operations.js +137 -0
- package/dist/browser/page.d.ts +25 -0
- package/dist/browser/page.js +172 -0
- package/dist/browser/quick-reads.d.ts +29 -0
- package/dist/browser/quick-reads.js +160 -0
- package/dist/browser/sessions.d.ts +52 -0
- package/dist/browser/sessions.js +118 -0
- package/dist/browser/url-policy.d.ts +18 -0
- package/dist/browser/url-policy.js +58 -0
- package/dist/build/app-imports.d.ts +12 -0
- package/dist/build/app-imports.js +105 -0
- package/dist/build/build-policy.d.ts +5 -3
- package/dist/build/build-policy.js +5 -3
- package/dist/build/candidate-builder.d.ts +1 -1
- package/dist/build/candidate-cli.mjs +8428 -337
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +28 -6
- package/dist/build/snapshot-package-store.d.ts +1 -1
- package/dist/build/snapshot-package-store.js +7 -2
- package/dist/build/workspace-build.d.ts +92 -5
- package/dist/build/workspace-build.js +93 -19
- package/dist/build-egress.js +4 -4
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- package/dist/catalog-frame.js +1 -1
- package/dist/cloudflare.d.ts +13 -3
- package/dist/cloudflare.js +106 -8
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.d.ts +1 -1
- package/dist/frame-binding-wire.js +5 -3
- package/dist/git-auth.d.ts +0 -2
- package/dist/git-auth.js +0 -2
- package/dist/git-journal.js +4 -4
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -2
- package/dist/host.d.ts +39 -13
- package/dist/host.js +268 -142
- package/dist/index.d.ts +26 -15
- package/dist/index.js +40 -16
- package/dist/install-release-cli.d.ts +2 -0
- package/dist/install-release-cli.js +46 -0
- package/dist/installation-client.d.ts +43 -0
- package/dist/installation-client.js +178 -0
- package/dist/installation-execution.d.ts +11 -0
- package/dist/installation-execution.js +184 -0
- package/dist/installation-operations.d.ts +1437 -0
- package/dist/installation-operations.js +664 -0
- package/dist/installation-store-generation.d.ts +3 -0
- package/dist/installation-store-generation.js +16 -0
- package/dist/instance-id.d.ts +10 -0
- package/dist/instance-id.js +10 -0
- package/dist/instance-refusal.d.ts +11 -0
- package/dist/instance-refusal.js +21 -0
- package/dist/internal/delivery.js +4 -2
- package/dist/internal/host-gate.js +7 -1
- package/dist/internal/registry-installations.js +4 -4
- package/dist/keys.d.ts +5 -1
- package/dist/keys.js +10 -2
- package/dist/machine-link/authority.d.ts +89 -0
- package/dist/machine-link/authority.js +37 -0
- package/dist/machine-link/client.d.ts +66 -0
- package/dist/machine-link/client.js +94 -0
- package/dist/machine-link/delivery.d.ts +18 -0
- package/dist/machine-link/delivery.js +55 -0
- package/dist/machine-link/enrollment.d.ts +111 -0
- package/dist/machine-link/enrollment.js +286 -0
- package/dist/machine-link/fences.d.ts +37 -0
- package/dist/machine-link/fences.js +166 -0
- package/dist/machine-link/home.d.ts +20 -0
- package/dist/machine-link/home.js +24 -0
- package/dist/machine-link/host.d.ts +43 -0
- package/dist/machine-link/host.js +189 -0
- package/dist/machine-link/index.d.ts +15 -0
- package/dist/machine-link/index.js +15 -0
- package/dist/machine-link/link.d.ts +48 -0
- package/dist/machine-link/link.js +223 -0
- package/dist/machine-link/presence.d.ts +10 -0
- package/dist/machine-link/presence.js +25 -0
- package/dist/machine-link/workers.d.ts +6 -0
- package/dist/machine-link/workers.js +6 -0
- package/dist/mcp-owner.d.ts +2 -2
- package/dist/mcp-owner.js +41 -33
- package/dist/mcp.d.ts +15 -3
- package/dist/mcp.js +146 -25
- package/dist/pack.d.ts +14 -3
- package/dist/pack.js +44 -19
- package/dist/page/app-access.d.ts +7 -0
- package/dist/page/app-access.js +75 -0
- package/dist/page/boot-host.js +57 -0
- package/dist/page/capabilities.d.ts +3 -3
- package/dist/page/capabilities.js +16 -12
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page/computer-target.d.ts +3 -0
- package/dist/page/computer-target.js +9 -0
- package/dist/page/mount.d.ts +23 -7
- package/dist/page/mount.js +152 -16
- package/dist/page-assets.d.ts +22 -0
- package/dist/page-assets.js +28 -0
- package/dist/page-script.d.ts +3 -0
- package/dist/page-script.js +5 -4
- package/dist/release.d.ts +13 -6
- package/dist/release.js +10 -7
- package/dist/root-data.d.ts +16 -6
- package/dist/root-data.js +2 -2
- package/dist/source/index.d.ts +1 -1
- package/dist/source/team-source.d.ts +20 -8
- package/dist/source/team-source.js +39 -10
- package/dist/source/template-seed.d.ts +2 -2
- package/dist/standalone/agent-mail.d.ts +24 -0
- package/dist/standalone/agent-mail.js +62 -0
- package/dist/standalone/agent-store.d.ts +9 -0
- package/dist/standalone/agent-store.js +30 -0
- package/dist/standalone/agents.d.ts +78 -12
- package/dist/standalone/agents.js +470 -59
- package/dist/standalone/app-builds-binding.d.ts +20 -2
- package/dist/standalone/app-builds-binding.js +151 -27
- package/dist/standalone/app-set.d.ts +5 -2
- package/dist/standalone/app-set.js +174 -143
- package/dist/standalone/browser.d.ts +11 -0
- package/dist/standalone/browser.js +72 -0
- package/dist/standalone/builds.d.ts +13 -4
- package/dist/standalone/builds.js +28 -44
- package/dist/standalone/computer-egress.d.ts +2 -0
- package/dist/standalone/computer-egress.js +13 -4
- package/dist/standalone/computer-fleet.d.ts +43 -0
- package/dist/standalone/computer-fleet.js +200 -0
- package/dist/standalone/computer.d.ts +44 -9
- package/dist/standalone/computer.js +132 -25
- package/dist/standalone/install-apps-cli.js +43 -6
- package/dist/standalone/instance.d.ts +2 -2
- package/dist/standalone/instance.js +8 -11
- package/dist/standalone/machine-link.d.ts +354 -0
- package/dist/standalone/machine-link.js +368 -0
- package/dist/standalone/media.d.ts +2 -2
- package/dist/standalone/media.js +19 -16
- package/dist/standalone/preview.d.ts +2 -2
- package/dist/standalone/preview.js +39 -12
- package/dist/standalone/registry.d.ts +149 -9
- package/dist/standalone/registry.js +299 -8
- package/dist/standalone/worker.d.ts +19 -5
- package/dist/standalone/worker.js +90 -54
- package/dist/standalone/workflow-binding.d.ts +28 -32
- package/dist/standalone/workflow-binding.js +20 -20
- package/dist/standalone/workflow-control.d.ts +4 -0
- package/dist/standalone/workflow-control.js +40 -0
- package/dist/standalone/workflow-events.d.ts +2 -2
- package/dist/standalone/workflow-events.js +9 -10
- package/dist/standalone/workflow-git-capture.js +4 -3
- package/dist/standalone/workflow-host.d.ts +177 -152
- package/dist/standalone/workflow-host.js +137 -30
- package/dist/standalone/workflow-node-capture.js +11 -4
- package/dist/standalone/workflow-schedules.d.ts +5 -2
- package/dist/standalone/workflow-schedules.js +9 -10
- package/dist/standalone/workflow-trigger-host.d.ts +88 -6
- package/dist/standalone/workflow-trigger-host.js +2 -2
- package/dist/standalone/workflow-webhooks.d.ts +22 -0
- package/dist/standalone/workflow-webhooks.js +39 -0
- package/dist/standalone/workflows.d.ts +203 -50
- package/dist/standalone/workflows.js +198 -39
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.js +14 -3
- package/dist/testing/installation.js +26 -2
- package/dist/testing/worker.js +1 -1
- package/dist/testing/workers-runtime.d.ts +8 -2
- package/dist/testing/workers-runtime.js +20 -2
- package/dist/webhook-abuse.d.ts +25 -0
- package/dist/webhook-abuse.js +54 -0
- package/dist/webhook-custody.d.ts +144 -0
- package/dist/webhook-custody.js +222 -0
- package/dist/webhook-ingress.d.ts +36 -0
- package/dist/webhook-ingress.js +146 -0
- package/dist/webhooks.d.ts +111 -0
- package/dist/webhooks.js +189 -0
- package/dist/workers-git.d.ts +12 -17
- package/dist/workers-git.js +60 -55
- package/dist/workers.d.ts +224 -13
- package/dist/workers.js +351 -51
- package/dist/workflow-agent-authority.d.ts +8 -0
- package/dist/workflow-agent-authority.js +42 -0
- package/dist/workflows.d.ts +66 -0
- package/dist/workflows.js +97 -0
- package/docs/api/README.md +63 -37
- package/docs/api/agents.md +17 -0
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +1394 -543
- package/docs/api/browser/boot.md +132 -0
- package/docs/api/browser.md +1440 -0
- package/docs/api/build-contract.md +38 -25
- package/docs/api/build.md +263 -84
- package/docs/api/cloudflare.md +93 -7
- package/docs/api/git.md +3 -13
- package/docs/api/host.md +944 -118
- package/docs/api/index.md +320 -18
- package/docs/api/keys.md +24 -1
- package/docs/api/mcp.md +59 -12
- package/docs/api/modules.md +33 -40
- package/docs/api/pack.md +57 -8
- package/docs/api/page/mount.md +92 -4
- package/docs/api/workers-git.md +103 -24
- package/docs/api/workers.md +1581 -244
- package/docs/site/agent-quickstart.mdx +28 -14
- package/examples/standalone/worker.ts +3 -1
- package/examples/standalone/wrangler.jsonc +2 -0
- package/package.json +73 -69
- package/dist/installation-consent.d.ts +0 -79
- package/dist/installation-consent.js +0 -82
- package/dist/shell/app-directory.d.ts +0 -542
- package/dist/shell/app-directory.js +0 -199
- package/dist/shell/browser-actions.d.ts +0 -7
- package/dist/shell/browser-actions.js +0 -56
- package/dist/shell/command-host.d.ts +0 -112
- package/dist/shell/command-host.js +0 -502
- package/dist/shell/commands.d.ts +0 -25
- package/dist/shell/commands.js +0 -78
- package/dist/shell/index.d.ts +0 -8
- package/dist/shell/index.js +0 -8
- package/dist/shell/keys.d.ts +0 -41
- package/dist/shell/keys.js +0 -134
- package/dist/shell/shell-module.d.ts +0 -55
- package/dist/shell/shell-module.js +0 -86
- package/dist/shell/window-manager.d.ts +0 -124
- package/dist/shell/window-manager.js +0 -711
- package/dist/shell/workbench-contract.d.ts +0 -68
- package/dist/shell/workbench-directory.d.ts +0 -332
- package/dist/shell/workbench-directory.js +0 -35
- package/dist/shell/workbench-host-types.d.ts +0 -96
- package/dist/shell/workbench-host-types.js +0 -1
- package/dist/shell/workbench-host.d.ts +0 -34
- package/dist/shell/workbench-host.js +0 -347
- package/dist/shell/workbench-model.d.ts +0 -192
- package/dist/shell/workbench-model.js +0 -53
- package/dist/shell/workbench.d.ts +0 -4
- package/dist/shell/workbench.js +0 -2
- package/docs/api/shell/app-directory.md +0 -599
- package/docs/api/shell/command-host.md +0 -224
- package/docs/api/shell/commands.md +0 -197
- package/docs/api/shell/keys.md +0 -183
- package/docs/api/shell/shell-module.md +0 -204
- package/docs/api/shell/window-manager.md +0 -267
- package/docs/api/shell/workbench-directory.md +0 -119
- package/docs/api/shell/workbench.md +0 -205
- package/docs/api/shell.md +0 -1915
- /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
package/dist/host.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { hostGateFacts, releaseCache } from "./internal/host-gate.js";
|
|
2
|
+
import { InstallationOperationError, installationCommandIds, installationRequestIdSchema, } from "./installation-operations.js";
|
|
3
|
+
import { completedInstallationResponse, observeInstallation, InstallationObservationError, } from "./installation-client.js";
|
|
4
|
+
import { instanceRefusalBody, instanceRefusalStatus, } from "./instance-refusal.js";
|
|
2
5
|
import { appSandbox } from "./app-sandbox.js";
|
|
3
|
-
import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
|
|
4
6
|
import { shellModuleAsset, shellModuleAssetPath, } from "@fungi.computer/puffball/assets";
|
|
5
|
-
import { installationConsentSchema, capabilityDescriptions, } from "./installation-consent.js";
|
|
6
7
|
import { gitRepositoryNameSchema } from "@fungi.computer/puffball/git-representation";
|
|
7
8
|
import { gillRequestSchema, decodeGillReply, decodeDeclaredGillResponse, } from "@fungi.computer/gills";
|
|
8
9
|
import { causeClass } from "./cause-class.js";
|
|
9
10
|
import { appStorageKeyNameSchema, appStorageWriteSchema, appStorageResetSchema, } from "@fungi.computer/agar/storage";
|
|
10
|
-
import {} from "@fungi.computer/agar/git";
|
|
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";
|
|
@@ -16,19 +16,21 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
16
16
|
import { match } from "path-to-regexp";
|
|
17
17
|
import * as Schema from "effect/Schema";
|
|
18
18
|
import * as Result from "effect/Result";
|
|
19
|
-
import {} from "@fungi.computer/puffball/background-replay";
|
|
20
19
|
import { emptyAppDeclaration } from "@fungi.computer/puffball/app";
|
|
21
|
-
import { applyInstallationCommand, installationIdSchema,
|
|
20
|
+
import { applyInstallationCommand, installationIdSchema, } from "./apps/installation.js";
|
|
22
21
|
import { admits, scopedBindings, grantsFor } from "./apps/gate.js";
|
|
22
|
+
import { appPermissionPageSchema, permissionSaveSchema, } from "./apps/permissions.js";
|
|
23
23
|
import { deriveDeclaredOperations, appCallOperation, } from "./apps/operations.js";
|
|
24
24
|
import { appBackendActorHeader, appBackendComputerHeader, appMethodRefusalStatus, } from "@fungi.computer/agar/backend";
|
|
25
25
|
import { appCallsDescriptor, } from "./capability-descriptor.js";
|
|
26
26
|
export { capabilityDescriptorSchema, capabilityMethodKindSchema, capabilityMethodSchema, } from "./capability-descriptor.js";
|
|
27
|
-
import {
|
|
28
|
-
import { parseRelease, readVerified,
|
|
27
|
+
import { selectionOf, } from "./index.js";
|
|
28
|
+
import { parseRelease, readVerified, sha256, } from "./release.js";
|
|
29
29
|
import { createGitDoor } from "./git.js";
|
|
30
|
-
import { buildGitPassPrefix, presentedGitKey
|
|
31
|
-
import {
|
|
30
|
+
import { buildGitPassPrefix, presentedGitKey } from "./git-auth.js";
|
|
31
|
+
import { workflowGitPassPrefix } from "@fungi.computer/braid/runtime";
|
|
32
|
+
import { loadPageAssets, PageAssetsLoadError, } from "./page-assets.js";
|
|
33
|
+
import { fungiFaviconHref } from "@fungi.computer/sigils/favicon";
|
|
32
34
|
import { createElement } from "react";
|
|
33
35
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
34
36
|
import { CenteredCard, CardHeader, CardTitle, CardContent, } from "@fungi.computer/caps/components/card";
|
|
@@ -92,14 +94,16 @@ function refusal(status, error, retryAfter) {
|
|
|
92
94
|
init.headers = { "Retry-After": String(retryAfter) };
|
|
93
95
|
return Response.json({ error }, init);
|
|
94
96
|
}
|
|
97
|
+
function installationOperationRefusal(cause) {
|
|
98
|
+
return cause.code === "stored_operation_invalid"
|
|
99
|
+
? Response.json({ error: cause.code, message: cause.message }, { status: 409 })
|
|
100
|
+
: refusal(cause.code === "unavailable" ? 503 : 409, cause.code);
|
|
101
|
+
}
|
|
102
|
+
/** A refusal no cache may keep: its answer depends on current authority. */
|
|
103
|
+
function noStoreRefusal(status, error) {
|
|
104
|
+
return Response.json({ error }, { status, headers: { "Cache-Control": "no-store" } });
|
|
105
|
+
}
|
|
95
106
|
function instanceRefusal(code, causeClass) {
|
|
96
|
-
const conflict = [
|
|
97
|
-
"conflicting_owner_state",
|
|
98
|
-
"stale_authority",
|
|
99
|
-
"instance_release_mismatch",
|
|
100
|
-
"release_no_longer_matches_instance",
|
|
101
|
-
"state_schema_migration_unsupported",
|
|
102
|
-
];
|
|
103
107
|
if (code === "failed") {
|
|
104
108
|
if (causeClass === undefined)
|
|
105
109
|
console.error("AppInstance failure", {
|
|
@@ -114,7 +118,9 @@ function instanceRefusal(code, causeClass) {
|
|
|
114
118
|
});
|
|
115
119
|
return refusal(502, "App execution failed");
|
|
116
120
|
}
|
|
117
|
-
return
|
|
121
|
+
return Response.json(instanceRefusalBody(code), {
|
|
122
|
+
status: instanceRefusalStatus(code),
|
|
123
|
+
});
|
|
118
124
|
}
|
|
119
125
|
/** An App's typed refusal keeps its code and caller-facing message. */
|
|
120
126
|
function methodResponse(reply) {
|
|
@@ -123,69 +129,69 @@ function methodResponse(reply) {
|
|
|
123
129
|
: Response.json({ error: reply.code, message: reply.message }, { status: appMethodRefusalStatus[reply.code] });
|
|
124
130
|
}
|
|
125
131
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
132
|
+
* Admit the retained command shared by POST and PUT selection. Only POST's
|
|
133
|
+
* explicit install-or-remint intent
|
|
134
|
+
* may choose a fresh id for a tombstone; ordinary selection keeps the retired
|
|
135
|
+
* id unavailable. App-mediated build selection stays refused until it has
|
|
136
|
+
* designated install authority.
|
|
131
137
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
async function admitInstallation(host, installationId, release, principal, intent, origin, requestId = installationCommandIds.request.generate()) {
|
|
139
|
+
// App-mediated installation needs installFree/designated-marketplace
|
|
140
|
+
// authority. A member's owner role is not that delegated App authority.
|
|
141
|
+
if (origin !== "owner-direct" && origin !== "workflow")
|
|
142
|
+
return refusal(403, "install_authority_unavailable");
|
|
143
|
+
if (!oysterPrincipalIsOwner(principal))
|
|
144
|
+
return refusal(403, "Owner only");
|
|
145
|
+
if (!installationIdSchema.safeParse(installationId).success)
|
|
146
|
+
return refusal(400, "Invalid installation id");
|
|
147
|
+
if (!installationRequestIdSchema.safeParse(requestId).success)
|
|
148
|
+
return refusal(400, "Invalid installation request identity");
|
|
149
|
+
const operations = host.installations.operations;
|
|
150
|
+
if (operations === undefined)
|
|
151
|
+
return refusal(503, "Installation commands unavailable");
|
|
134
152
|
try {
|
|
135
|
-
|
|
153
|
+
return await operations.admit({
|
|
154
|
+
requestId,
|
|
155
|
+
installationId,
|
|
156
|
+
release,
|
|
157
|
+
actor: principal.actor,
|
|
158
|
+
intent,
|
|
159
|
+
origin,
|
|
160
|
+
});
|
|
136
161
|
}
|
|
137
162
|
catch (cause) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
if (cause instanceof InstallationOperationError)
|
|
164
|
+
return installationOperationRefusal(cause);
|
|
165
|
+
throw cause;
|
|
141
166
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (current !== null && current.status !== "deleted") {
|
|
167
|
-
const update = await reduce(current, { kind: "select-release", release, releasePin: "pinned" }, current);
|
|
168
|
-
if (Result.isFailure(update))
|
|
169
|
-
return refusal(409, update.failure.code);
|
|
170
|
-
const next = update.success.next;
|
|
171
|
-
const refused = await apply(next);
|
|
172
|
-
if (refused !== null)
|
|
173
|
-
return refused;
|
|
174
|
-
await host.installations.put(next);
|
|
175
|
-
return Response.json(next);
|
|
167
|
+
}
|
|
168
|
+
/** Completed selection, without re-deriving or executing outside Watchdog. */
|
|
169
|
+
export async function installOrSelect(host, installationId, release, principal, intent, origin, requestId = installationCommandIds.request.generate(), signal = new AbortController().signal) {
|
|
170
|
+
const admitted = await admitInstallation(host, installationId, release, principal, intent, origin, requestId);
|
|
171
|
+
if (admitted instanceof Response)
|
|
172
|
+
return admitted;
|
|
173
|
+
const operations = host.installations.operations;
|
|
174
|
+
if (operations === undefined)
|
|
175
|
+
return refusal(503, "Installation commands unavailable");
|
|
176
|
+
try {
|
|
177
|
+
return completedInstallationResponse(await observeInstallation({
|
|
178
|
+
requestId,
|
|
179
|
+
initial: admitted,
|
|
180
|
+
read: () => operations.read(requestId, installationId),
|
|
181
|
+
signal,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
if (cause instanceof InstallationObservationError &&
|
|
186
|
+
cause.cause instanceof InstallationOperationError)
|
|
187
|
+
return installationOperationRefusal(cause.cause);
|
|
188
|
+
if (cause instanceof InstallationObservationError)
|
|
189
|
+
return Response.json({ error: "Installation outcome unconfirmed", requestId }, { status: 503 });
|
|
190
|
+
throw cause;
|
|
176
191
|
}
|
|
177
|
-
if (!installationIdSchema.safeParse(installationId).success)
|
|
178
|
-
return refusal(400, "Invalid installation id");
|
|
179
|
-
const update = await reduce(null, { kind: "install", release, releasePin: "pinned", grants: "declared" }, { installationId, instanceId: instanceIds.instance.generate() });
|
|
180
|
-
if (Result.isFailure(update))
|
|
181
|
-
return refusal(409, update.failure.code);
|
|
182
|
-
const installation = update.success.next;
|
|
183
|
-
const refused = await apply(installation);
|
|
184
|
-
if (refused !== null)
|
|
185
|
-
return refused;
|
|
186
|
-
await host.installations.put(installation);
|
|
187
|
-
return Response.json(installation, { status: 201 });
|
|
188
192
|
}
|
|
193
|
+
/** The tab icon of every Oyster page a person opens at the top level. */
|
|
194
|
+
const tabIcon = `<link rel="icon" type="image/svg+xml" href="${fungiFaviconHref}">`;
|
|
189
195
|
/**
|
|
190
196
|
* Create the Oyster App server: a Hono app that installs App Releases, serves
|
|
191
197
|
* their UI, forwards their backend requests and methods to their Instances,
|
|
@@ -198,13 +204,22 @@ export function createOyster(host) {
|
|
|
198
204
|
const releaseOf = releaseCache(host.artifacts);
|
|
199
205
|
const operationDescriptors = host.capabilities ?? [];
|
|
200
206
|
const gateFacts = (installation, release, principal) => hostGateFacts(host, installation, release, principal);
|
|
207
|
+
// Current approval for the home shell's code, read on every entry request.
|
|
208
|
+
const shellApproved = async (installation, release) => {
|
|
209
|
+
const declaration = release.declaration ?? emptyAppDeclaration;
|
|
210
|
+
return (grantsFor({
|
|
211
|
+
status: installation.status,
|
|
212
|
+
authority: installation.authority,
|
|
213
|
+
artifactDigest: installation.release.artifact.artifactDigest,
|
|
214
|
+
}, declaration, await deriveDeclaredOperations(installation.release, declaration, operationDescriptors), "team.workbenchHost") !== null);
|
|
215
|
+
};
|
|
201
216
|
const reduceInstallation = async (current, command, release, principal, identity) => {
|
|
202
217
|
const context = invocationCallerContext(principal, host.computerId ?? null);
|
|
203
218
|
const declaration = release.declaration ?? emptyAppDeclaration;
|
|
204
219
|
return applyInstallationCommand(current, command, {
|
|
205
220
|
by: context.actor,
|
|
206
221
|
role: context.role ?? null,
|
|
207
|
-
expectedVersion: current?.authorityVersion ?? null,
|
|
222
|
+
expectedVersion: current?.authority.authorityVersion ?? null,
|
|
208
223
|
declaration,
|
|
209
224
|
declaredOperations: await deriveDeclaredOperations(release.release, declaration, operationDescriptors),
|
|
210
225
|
now: Date.now(),
|
|
@@ -217,19 +232,55 @@ export function createOyster(host) {
|
|
|
217
232
|
contentType,
|
|
218
233
|
source,
|
|
219
234
|
});
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
grantConsent,
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
const prepareAssets = async () => {
|
|
236
|
+
const { pageScript, publicHomeScript, grantConsentScript, fontStyles, loginFontStyles, shellStyles, } = await (host.pageAssets ?? loadPageAssets)();
|
|
237
|
+
return Promise.all([
|
|
238
|
+
asset("page.js", "text/javascript; charset=utf-8", host.pageScript ?? pageScript),
|
|
239
|
+
asset("public-home.js", "text/javascript; charset=utf-8", publicHomeScript),
|
|
240
|
+
asset("grant-consent.js", "text/javascript; charset=utf-8", grantConsentScript),
|
|
241
|
+
asset("fonts.css", "text/css; charset=utf-8", fontStyles),
|
|
242
|
+
asset("login-fonts.css", "text/css; charset=utf-8", loginFontStyles),
|
|
243
|
+
asset("styles.css", "text/css; charset=utf-8", shellStyles),
|
|
244
|
+
]).then(([page, publicHome, grantConsent, fonts, loginFonts, styles]) => ({
|
|
245
|
+
page,
|
|
246
|
+
publicHome,
|
|
247
|
+
grantConsent,
|
|
248
|
+
fonts,
|
|
249
|
+
loginFonts,
|
|
250
|
+
styles,
|
|
251
|
+
}));
|
|
252
|
+
};
|
|
253
|
+
let preparedAssets;
|
|
254
|
+
const assets = () => (preparedAssets ??= prepareAssets().catch((cause) => {
|
|
255
|
+
preparedAssets = undefined;
|
|
256
|
+
throw cause instanceof PageAssetsLoadError
|
|
257
|
+
? cause
|
|
258
|
+
: new PageAssetsLoadError(cause);
|
|
232
259
|
}));
|
|
260
|
+
// The home page imports this host-owned entry, never the Release module's
|
|
261
|
+
// own URL: released Oyster 0.2.0 served that URL immutable, so a browser may
|
|
262
|
+
// hold a copy it would import without a request. The entry is never cached
|
|
263
|
+
// and rechecks current approval before it names the module.
|
|
264
|
+
app.get(`/ui-entry/:installationId/:releaseId${shellModuleAssetPath}`, async (context) => {
|
|
265
|
+
const installation = await host.installations.get(context.req.param("installationId"));
|
|
266
|
+
if (installation?.status !== "enabled" ||
|
|
267
|
+
installation.release.releaseId !== context.req.param("releaseId"))
|
|
268
|
+
return noStoreRefusal(404, "Not found");
|
|
269
|
+
const release = await releaseOf(installation.release);
|
|
270
|
+
if (release.ui?.kind !== "assets" ||
|
|
271
|
+
shellModuleAsset(release.ui.build) === undefined)
|
|
272
|
+
return noStoreRefusal(404, "Not found");
|
|
273
|
+
if (!(await shellApproved(installation, release)))
|
|
274
|
+
return noStoreRefusal(403, "not_approved");
|
|
275
|
+
const module = `/ui/${installation.installationId}/${installation.release.releaseId}${shellModuleAssetPath}`;
|
|
276
|
+
return new Response(`export * from ${JSON.stringify(module)};\n`, {
|
|
277
|
+
headers: {
|
|
278
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
279
|
+
"X-Content-Type-Options": "nosniff",
|
|
280
|
+
"Cache-Control": "no-store",
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
});
|
|
233
284
|
// App UI: digest-pinned, sandboxed, and readable without a session because
|
|
234
285
|
// the opaque-origin frame cannot carry one.
|
|
235
286
|
app.get("/ui/:installationId/:releaseId/*", async (context) => {
|
|
@@ -260,6 +311,11 @@ export function createOyster(host) {
|
|
|
260
311
|
const asset = release.ui.build.assets.find((item) => item.path === wanted);
|
|
261
312
|
if (asset === undefined)
|
|
262
313
|
return refusal(404, "Not found");
|
|
314
|
+
// The Release's shell module is read only through the host's entry, but
|
|
315
|
+
// keeps the same per-request check; other UI assets stay digest-cacheable.
|
|
316
|
+
const shellEntry = wanted === shellModuleAssetPath;
|
|
317
|
+
if (shellEntry && !(await shellApproved(installation, release)))
|
|
318
|
+
return noStoreRefusal(403, "not_approved");
|
|
263
319
|
const bytes = await readVerified(host.artifacts, asset.artifactRef, asset.digest);
|
|
264
320
|
const scope = `${origin}${prefix}/`;
|
|
265
321
|
return new Response(new Uint8Array(bytes), {
|
|
@@ -268,13 +324,15 @@ export function createOyster(host) {
|
|
|
268
324
|
"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}`,
|
|
269
325
|
"Access-Control-Allow-Origin": "*",
|
|
270
326
|
"X-Content-Type-Options": "nosniff",
|
|
271
|
-
"Cache-Control":
|
|
327
|
+
"Cache-Control": shellEntry
|
|
328
|
+
? "no-store"
|
|
329
|
+
: "public, max-age=31536000, immutable",
|
|
272
330
|
},
|
|
273
331
|
});
|
|
274
332
|
});
|
|
275
333
|
app.get("/_oyster/assets/*", async (context) => {
|
|
276
334
|
const wanted = new URL(context.req.url).pathname;
|
|
277
|
-
const found = Object.values(await assets).find(({ path }) => path === wanted);
|
|
335
|
+
const found = Object.values(await assets()).find(({ path }) => path === wanted);
|
|
278
336
|
if (found === undefined)
|
|
279
337
|
return refusal(404, "Not found");
|
|
280
338
|
return new Response(found.source, {
|
|
@@ -286,7 +344,7 @@ export function createOyster(host) {
|
|
|
286
344
|
});
|
|
287
345
|
});
|
|
288
346
|
app.get("/login", async () => {
|
|
289
|
-
const {
|
|
347
|
+
const { loginFonts, styles } = await assets();
|
|
290
348
|
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, {
|
|
291
349
|
label: "Owner key",
|
|
292
350
|
required: true,
|
|
@@ -297,7 +355,7 @@ export function createOyster(host) {
|
|
|
297
355
|
required: true,
|
|
298
356
|
}),
|
|
299
357
|
}), createElement(FormFooter, { layout: "stretch", divider: false }, createElement(Button, { type: "submit" }, "Sign in"))))));
|
|
300
|
-
return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Sign in · ${escapeHtml(title)}</title
|
|
358
|
+
return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Sign in · ${escapeHtml(title)}</title>${tabIcon}<link rel="stylesheet" href="${styles.path}"><link rel="stylesheet" href="${loginFonts.path}"></head><body class="px-lg">${form}</body></html>`, {
|
|
301
359
|
headers: {
|
|
302
360
|
"Content-Type": "text/html; charset=utf-8",
|
|
303
361
|
"Cache-Control": "no-store",
|
|
@@ -461,7 +519,7 @@ export function createOyster(host) {
|
|
|
461
519
|
});
|
|
462
520
|
}
|
|
463
521
|
const config = JSON.stringify({ title, apps }).replaceAll("<", "\\u003c");
|
|
464
|
-
return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title
|
|
522
|
+
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>`, {
|
|
465
523
|
headers: {
|
|
466
524
|
"Content-Type": "text/html; charset=utf-8",
|
|
467
525
|
"Cache-Control": "no-store",
|
|
@@ -473,7 +531,7 @@ export function createOyster(host) {
|
|
|
473
531
|
const items = installations
|
|
474
532
|
.map((installation) => `<li><a href="/apps/${escapeHtml(installation.installationId)}/">${escapeHtml(installation.installationId)}</a> <small>${escapeHtml(installation.release.releaseId)}</small>${oysterPrincipalIsOwner(context.get("principal")) ? ` <a href="/api/installations/${encodeURIComponent(installation.installationId)}/grants">App access</a>` : ""}</li>`)
|
|
475
533
|
.join("");
|
|
476
|
-
return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title
|
|
534
|
+
return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title>${tabIcon}<h1>${escapeHtml(title)}</h1><ul>${items || "<li>No Apps installed yet.</li><li>Install Apps with <code>npx oyster install-apps</code></li>"}</ul>`, {
|
|
477
535
|
headers: {
|
|
478
536
|
"Content-Type": "text/html; charset=utf-8",
|
|
479
537
|
"Cache-Control": "no-store",
|
|
@@ -488,6 +546,7 @@ export function createOyster(host) {
|
|
|
488
546
|
return undefined;
|
|
489
547
|
});
|
|
490
548
|
app.put("/api/installations/:installationId/visibility", async (context) => {
|
|
549
|
+
const generation = await host.installations.generation();
|
|
491
550
|
const current = await host.installations.get(context.req.param("installationId"));
|
|
492
551
|
if (current === null || current.status === "deleted")
|
|
493
552
|
return refusal(404, "Not found");
|
|
@@ -498,10 +557,16 @@ export function createOyster(host) {
|
|
|
498
557
|
if (Result.isFailure(update))
|
|
499
558
|
return refusal(409, update.failure.code);
|
|
500
559
|
const next = update.success.next;
|
|
501
|
-
const
|
|
560
|
+
const release = await releaseOf(current.release);
|
|
561
|
+
const refused = await prepare(next, release);
|
|
502
562
|
if (refused !== null)
|
|
503
563
|
return refused;
|
|
504
|
-
await host.installations.
|
|
564
|
+
const commit = await host.installations.compareAndPut(next, current.authority.authorityVersion, generation);
|
|
565
|
+
if (!commit.ok)
|
|
566
|
+
return refusal(409, commit.code);
|
|
567
|
+
const activationRefusal = await apply(next, release);
|
|
568
|
+
if (activationRefusal !== null)
|
|
569
|
+
return activationRefusal;
|
|
505
570
|
return Response.json(next);
|
|
506
571
|
});
|
|
507
572
|
if (host.ownerRoutes !== undefined) {
|
|
@@ -584,6 +649,14 @@ export function createOyster(host) {
|
|
|
584
649
|
await host.artifacts.put(artifactRef, bytes);
|
|
585
650
|
return new Response(null, { status: 204 });
|
|
586
651
|
});
|
|
652
|
+
const prepare = async (installation, verifiedRelease) => {
|
|
653
|
+
if (verifiedRelease.backend === null)
|
|
654
|
+
return null;
|
|
655
|
+
const result = await host.instances
|
|
656
|
+
.get(selectionOf(installation).instanceId, limits)
|
|
657
|
+
.prepare({ ...selectionOf(installation), status: installation.status }, installation.release);
|
|
658
|
+
return result.ok ? null : instanceRefusal(result.code, result.causeClass);
|
|
659
|
+
};
|
|
587
660
|
const apply = async (installation, verifiedRelease) => {
|
|
588
661
|
if (verifiedRelease.backend === null)
|
|
589
662
|
return null;
|
|
@@ -603,44 +676,67 @@ export function createOyster(host) {
|
|
|
603
676
|
return refusal(400, "Invalid install request");
|
|
604
677
|
}
|
|
605
678
|
const installationId = request.installationId ?? release.appId.replaceAll("_", "-");
|
|
606
|
-
|
|
679
|
+
if (!installationIdSchema.safeParse(installationId).success)
|
|
680
|
+
return refusal(400, "Invalid installation id");
|
|
681
|
+
const key = installationRequestIdSchema.safeParse(context.req.header("Idempotency-Key"));
|
|
682
|
+
if (!key.success)
|
|
683
|
+
return refusal(400, "Invalid installation request identity");
|
|
684
|
+
const admitted = await admitInstallation(host, installationId, release, context.get("principal"), "install-or-remint", context.req.raw.headers.has("X-Oyster-Installation")
|
|
685
|
+
? "app"
|
|
686
|
+
: "owner-direct", key.data);
|
|
687
|
+
return admitted instanceof Response
|
|
688
|
+
? admitted
|
|
689
|
+
: Response.json(admitted, { status: 202 });
|
|
690
|
+
});
|
|
691
|
+
app.get("/api/installations/:installationId/status", async (context) => {
|
|
692
|
+
if (!oysterPrincipalIsOwner(context.get("principal")) ||
|
|
693
|
+
context.req.raw.headers.has("X-Oyster-Installation"))
|
|
694
|
+
return refusal(403, "Owner only");
|
|
695
|
+
const key = installationRequestIdSchema.safeParse(context.req.query("requestId"));
|
|
696
|
+
if (!key.success)
|
|
697
|
+
return refusal(400, "Invalid installation request identity");
|
|
698
|
+
const operations = host.installations.operations;
|
|
699
|
+
if (operations === undefined)
|
|
700
|
+
return refusal(503, "Installation commands unavailable");
|
|
701
|
+
try {
|
|
702
|
+
const operation = await operations.read(key.data, context.req.param("installationId"));
|
|
703
|
+
return operation === null
|
|
704
|
+
? refusal(404, "Installation request not found")
|
|
705
|
+
: Response.json(operation);
|
|
706
|
+
}
|
|
707
|
+
catch (cause) {
|
|
708
|
+
if (cause instanceof InstallationOperationError)
|
|
709
|
+
return installationOperationRefusal(cause);
|
|
710
|
+
throw cause;
|
|
711
|
+
}
|
|
607
712
|
});
|
|
608
713
|
app.get("/api/installations/:installationId/grants", async (context) => {
|
|
609
714
|
if (context.req.raw.headers.has("X-Oyster-Installation"))
|
|
610
715
|
return refusal(403, "Owner direct only");
|
|
716
|
+
const generation = await host.installations.generation();
|
|
611
717
|
const current = await host.installations.get(context.req.param("installationId"));
|
|
612
718
|
if (current === null || current.status === "deleted")
|
|
613
719
|
return refusal(404, "Not found");
|
|
614
720
|
const verified = await releaseOf(current.release);
|
|
615
721
|
const declaration = verified.declaration ?? emptyAppDeclaration;
|
|
616
722
|
const declared = await deriveDeclaredOperations(current.release, declaration, operationDescriptors);
|
|
617
|
-
const config =
|
|
723
|
+
const config = appPermissionPageSchema.parse({
|
|
724
|
+
storeGeneration: generation,
|
|
618
725
|
installationId: current.installationId,
|
|
619
726
|
appId: current.appId,
|
|
620
727
|
appName: declaration.displayName ?? current.installationId,
|
|
621
|
-
authorityVersion: current.authorityVersion,
|
|
622
728
|
artifactDigest: current.release.artifact.artifactDigest,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
description: `Lets this App ${capabilityDescriptions[capability]}`,
|
|
628
|
-
scope: (existing?.resources ?? "tenant-scope") === "tenant-scope"
|
|
629
|
-
? "All of this Oyster"
|
|
630
|
-
: "Only the resources you already approved",
|
|
631
|
-
request: {
|
|
632
|
-
capability,
|
|
633
|
-
operations: existing?.operations ??
|
|
634
|
-
declared.capabilities.find((row) => row.capability === capability)
|
|
635
|
-
?.operations ??
|
|
636
|
-
[],
|
|
637
|
-
resources: existing?.resources ?? "tenant-scope",
|
|
638
|
-
},
|
|
639
|
-
};
|
|
640
|
-
}),
|
|
729
|
+
permissions: {
|
|
730
|
+
authority: current.authority,
|
|
731
|
+
declaredOperations: declared,
|
|
732
|
+
},
|
|
641
733
|
});
|
|
734
|
+
if (context.req.header("Accept")?.includes("application/json"))
|
|
735
|
+
return Response.json(config, {
|
|
736
|
+
headers: { "Cache-Control": "no-store" },
|
|
737
|
+
});
|
|
642
738
|
const encoded = JSON.stringify(config).replaceAll("<", "\\u003c");
|
|
643
|
-
return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(config.appName)} permissions</title><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)}</style><div id="oyster-consent"></div><script type="application/json" id="oyster-consent-config">${encoded}</script><script type="module" src="${(await assets).grantConsent.path}"></script>`, {
|
|
739
|
+
return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(config.appName)} permissions</title><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)}</style><div id="oyster-consent"></div><script type="application/json" id="oyster-consent-config">${encoded}</script><script type="module" src="${(await assets()).grantConsent.path}"></script>`, {
|
|
644
740
|
headers: {
|
|
645
741
|
"Content-Type": "text/html; charset=utf-8",
|
|
646
742
|
"Cache-Control": "no-store",
|
|
@@ -659,27 +755,33 @@ export function createOyster(host) {
|
|
|
659
755
|
const principal = context.get("principal");
|
|
660
756
|
if (!oysterPrincipalIsOwner(principal))
|
|
661
757
|
return refusal(403, "Owner only");
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
artifactDigest: sha256DigestSchema,
|
|
665
|
-
command: Schema.Unknown,
|
|
666
|
-
})), { onExcessProperty: "error" })(await context.req.text());
|
|
667
|
-
if (parsed._tag === "None")
|
|
758
|
+
const body = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))(await context.req.text());
|
|
759
|
+
if (body._tag === "None")
|
|
668
760
|
return refusal(400, "Invalid approval");
|
|
669
|
-
const
|
|
670
|
-
if (!
|
|
761
|
+
const parsed = permissionSaveSchema.safeParse(body.value);
|
|
762
|
+
if (!parsed.success || parsed.data.command.kind !== "change-grants")
|
|
671
763
|
return refusal(400, "Invalid approval");
|
|
764
|
+
const generation = await host.installations.generation();
|
|
765
|
+
if (generation !== parsed.data.storeGeneration)
|
|
766
|
+
return refusal(409, "stale_authority");
|
|
672
767
|
const current = await host.installations.get(context.req.param("installationId"));
|
|
673
|
-
if (current === null
|
|
768
|
+
if (current === null)
|
|
674
769
|
return refusal(404, "Not found");
|
|
675
|
-
|
|
676
|
-
|
|
770
|
+
// A retired installation's pins can never match a live record: the
|
|
771
|
+
// page already offers a reload, which then finds the App gone.
|
|
772
|
+
if (current.status === "deleted")
|
|
773
|
+
return refusal(409, "stale_authority");
|
|
774
|
+
if (current.authority.authorityVersion !== parsed.data.authorityVersion ||
|
|
775
|
+
current.release.artifact.artifactDigest !== parsed.data.artifactDigest)
|
|
677
776
|
return refusal(409, "stale_authority");
|
|
678
777
|
const release = await releaseOf(current.release);
|
|
679
|
-
const update = await reduceInstallation(current,
|
|
778
|
+
const update = await reduceInstallation(current, parsed.data.command, release, principal, current);
|
|
680
779
|
if (Result.isFailure(update))
|
|
681
780
|
return refusal(409, update.failure.code);
|
|
682
|
-
const
|
|
781
|
+
const preflightRefusal = await prepare(update.success.next, release);
|
|
782
|
+
if (preflightRefusal !== null)
|
|
783
|
+
return preflightRefusal;
|
|
784
|
+
const commit = await host.installations.compareAndPut(update.success.next, parsed.data.authorityVersion, parsed.data.storeGeneration);
|
|
683
785
|
if (!commit.ok)
|
|
684
786
|
return refusal(409, commit.code);
|
|
685
787
|
const refused = await apply(update.success.next, release);
|
|
@@ -695,9 +797,18 @@ export function createOyster(host) {
|
|
|
695
797
|
catch {
|
|
696
798
|
return refusal(400, "Invalid Release");
|
|
697
799
|
}
|
|
698
|
-
|
|
800
|
+
const key = installationRequestIdSchema.safeParse(context.req.header("Idempotency-Key") ??
|
|
801
|
+
installationCommandIds.request.generate());
|
|
802
|
+
if (!key.success)
|
|
803
|
+
return refusal(400, "Invalid installation request identity");
|
|
804
|
+
// PUT retains its completed-response meaning. Cancellation only stops this
|
|
805
|
+
// observer; Watchdog's retained command continues independently.
|
|
806
|
+
return installOrSelect(host, context.req.param("installationId"), release, context.get("principal"), "install-or-select", context.req.raw.headers.has("X-Oyster-Installation")
|
|
807
|
+
? "app"
|
|
808
|
+
: "owner-direct", key.data, context.req.raw.signal);
|
|
699
809
|
});
|
|
700
810
|
app.delete("/api/installations/:installationId", async (context) => {
|
|
811
|
+
const generation = await host.installations.generation();
|
|
701
812
|
const current = await host.installations.get(context.req.param("installationId"));
|
|
702
813
|
if (current === null || current.status === "deleted")
|
|
703
814
|
return refusal(404, "Not found");
|
|
@@ -705,10 +816,16 @@ export function createOyster(host) {
|
|
|
705
816
|
if (Result.isFailure(update))
|
|
706
817
|
return refusal(409, update.failure.code);
|
|
707
818
|
const next = update.success.next;
|
|
708
|
-
const
|
|
819
|
+
const release = await releaseOf(current.release);
|
|
820
|
+
const refused = await prepare(next, release);
|
|
709
821
|
if (refused !== null)
|
|
710
822
|
return refused;
|
|
711
|
-
await host.installations.
|
|
823
|
+
const commit = await host.installations.compareAndPut(next, current.authority.authorityVersion, generation);
|
|
824
|
+
if (!commit.ok)
|
|
825
|
+
return refusal(409, commit.code);
|
|
826
|
+
const activationRefusal = await apply(next, release);
|
|
827
|
+
if (activationRefusal !== null)
|
|
828
|
+
return activationRefusal;
|
|
712
829
|
return new Response(null, { status: 204 });
|
|
713
830
|
});
|
|
714
831
|
const enabled = async (context) => {
|
|
@@ -754,7 +871,8 @@ export function createOyster(host) {
|
|
|
754
871
|
const workbench = oysterPrincipalIsOwner(principal) &&
|
|
755
872
|
host.workbench !== undefined &&
|
|
756
873
|
installation.installationId === host.homeInstallationId &&
|
|
757
|
-
declaration?.capabilities.includes("team.workbenchHost") === true
|
|
874
|
+
declaration?.capabilities.includes("team.workbenchHost") === true &&
|
|
875
|
+
grantsFor(facts.installation, facts.declaration, facts.declaredOperations, "team.workbenchHost") !== null;
|
|
758
876
|
const uiUrl = `/ui/${installation.installationId}/${installation.release.releaseId}/`;
|
|
759
877
|
const config = {
|
|
760
878
|
workbench,
|
|
@@ -763,7 +881,7 @@ export function createOyster(host) {
|
|
|
763
881
|
shellModuleUrl: workbench &&
|
|
764
882
|
release.ui?.kind === "assets" &&
|
|
765
883
|
shellModuleAsset(release.ui.build) !== undefined
|
|
766
|
-
?
|
|
884
|
+
? `/ui-entry/${installation.installationId}/${installation.release.releaseId}${shellModuleAssetPath}`
|
|
767
885
|
: null,
|
|
768
886
|
storage: oysterPrincipalIsOwner(principal) &&
|
|
769
887
|
host.workbench !== undefined &&
|
|
@@ -771,7 +889,7 @@ export function createOyster(host) {
|
|
|
771
889
|
appsOpen: declaration?.capabilities.includes("apps.open") === true,
|
|
772
890
|
computerId: invocationCallerContext(principal, host.computerId ?? null)
|
|
773
891
|
.computerId,
|
|
774
|
-
authorityVersion: installation.authorityVersion,
|
|
892
|
+
authorityVersion: installation.authority.authorityVersion,
|
|
775
893
|
capabilities,
|
|
776
894
|
commands: declaration?.commands ?? [],
|
|
777
895
|
icon: declaration?.icon ?? null,
|
|
@@ -787,8 +905,8 @@ export function createOyster(host) {
|
|
|
787
905
|
return Response.json(config, {
|
|
788
906
|
headers: { "Cache-Control": "no-store", Vary: "Accept" },
|
|
789
907
|
});
|
|
790
|
-
const head = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(installation.installationId)} · ${escapeHtml(title)}</title
|
|
791
|
-
const tail = `<script type="application/json" id="oyster-config">${JSON.stringify(config).replaceAll("<", "\\u003c")}</script><script type="module" src="${(await assets).page.path}"></script>`;
|
|
908
|
+
const head = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(installation.installationId)} · ${escapeHtml(title)}</title>${tabIcon}<link rel="stylesheet" href="${(await assets()).fonts.path}">`;
|
|
909
|
+
const tail = `<script type="application/json" id="oyster-config">${JSON.stringify(config).replaceAll("<", "\\u003c")}</script><script type="module" src="${(await assets()).page.path}"></script>`;
|
|
792
910
|
// The home shell is trusted: the host's page script renders it in this
|
|
793
911
|
// top-level document, and it mounts each App's sandboxed frame inside
|
|
794
912
|
// that App's window. Every other App page is only its one framed App;
|
|
@@ -799,7 +917,9 @@ export function createOyster(host) {
|
|
|
799
917
|
headers: {
|
|
800
918
|
"Content-Type": "text/html; charset=utf-8",
|
|
801
919
|
"Cache-Control": "no-store",
|
|
802
|
-
|
|
920
|
+
// worker-src would otherwise fall back to script-src 'self': trusted
|
|
921
|
+
// shell code must not outlive revocation in a Worker or Service Worker.
|
|
922
|
+
"Content-Security-Policy": `default-src 'none'; script-src 'self'; worker-src 'none'; style-src 'self' 'unsafe-inline'; font-src data:; frame-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,
|
|
803
923
|
},
|
|
804
924
|
});
|
|
805
925
|
});
|
|
@@ -886,11 +1006,15 @@ export function createOyster(host) {
|
|
|
886
1006
|
return refusal(403, "Gill source is not admitted");
|
|
887
1007
|
const current = await gateFacts(installation, release, context.get("principal"));
|
|
888
1008
|
if (current.declaredOperations.digest !==
|
|
889
|
-
installation.declaredOperationsDigest ||
|
|
1009
|
+
installation.authority.declaredOperationsDigest ||
|
|
890
1010
|
current.declaredOperations.artifactDigest !==
|
|
891
1011
|
installation.release.artifact.artifactDigest)
|
|
892
1012
|
return refusal(403, "stale_authority");
|
|
893
|
-
if (method.requires.some((capability) => grantsFor(
|
|
1013
|
+
if (method.requires.some((capability) => grantsFor({
|
|
1014
|
+
status: installation.status,
|
|
1015
|
+
authority: installation.authority,
|
|
1016
|
+
artifactDigest: installation.release.artifact.artifactDigest,
|
|
1017
|
+
}, current.declaration, current.declaredOperations, capability) === null))
|
|
894
1018
|
return refusal(403, "requires_not_granted");
|
|
895
1019
|
if (selected?.kind === "computer")
|
|
896
1020
|
return Response.json({
|
|
@@ -1024,13 +1148,15 @@ export function createOyster(host) {
|
|
|
1024
1148
|
host.installations.get(target.installationId),
|
|
1025
1149
|
]);
|
|
1026
1150
|
if (currentCaller?.status !== "enabled" ||
|
|
1027
|
-
currentCaller.authorityVersion !==
|
|
1151
|
+
currentCaller.authority.authorityVersion !==
|
|
1152
|
+
caller.authority.authorityVersion ||
|
|
1028
1153
|
currentCaller.release.artifact.artifactDigest !==
|
|
1029
1154
|
caller.release.artifact.artifactDigest ||
|
|
1030
1155
|
!(await allows(currentCaller, fresh)))
|
|
1031
1156
|
return refusal(403, "app_call_not_allowed");
|
|
1032
1157
|
if (currentTarget?.status !== "enabled" ||
|
|
1033
|
-
currentTarget.authorityVersion !==
|
|
1158
|
+
currentTarget.authority.authorityVersion !==
|
|
1159
|
+
target.authority.authorityVersion ||
|
|
1034
1160
|
currentTarget.release.releaseId !== target.release.releaseId ||
|
|
1035
1161
|
currentTarget.release.artifact.artifactDigest !==
|
|
1036
1162
|
target.release.artifact.artifactDigest)
|