@fungi.computer/oyster 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +1134 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +210 -0
- package/dist/app-builds.js +499 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +31 -0
- package/dist/app-sources.js +370 -0
- package/dist/app-storage.d.ts +141 -0
- package/dist/app-storage.js +281 -0
- package/dist/apps/gate.d.ts +105 -0
- package/dist/apps/gate.js +273 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/installation.d.ts +248 -0
- package/dist/apps/installation.js +307 -0
- package/dist/apps/operations.d.ts +8 -0
- package/dist/apps/operations.js +98 -0
- package/dist/apps/tools.d.ts +33 -0
- package/dist/apps/tools.js +17 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +93 -0
- package/dist/backend-request.d.ts +3 -0
- package/dist/backend-request.js +53 -0
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +203 -0
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +37 -0
- package/dist/build/build-step.js +28 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28170 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +249 -0
- package/dist/build/effect.js +436 -0
- package/dist/build/git-environment.d.ts +9 -0
- package/dist/build/git-environment.js +60 -0
- package/dist/build/html-recipe.d.ts +20 -0
- package/dist/build/html-recipe.js +51 -0
- package/dist/build/local-build.d.ts +67 -0
- package/dist/build/local-build.js +1128 -0
- package/dist/build/snapshot-package-store.d.ts +12 -0
- package/dist/build/snapshot-package-store.js +36 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +828 -0
- package/dist/build/submission-contract.js +180 -0
- package/dist/build/workspace-build.d.ts +55 -0
- package/dist/build/workspace-build.js +200 -0
- package/dist/build-contract.d.ts +7 -0
- package/dist/build-contract.js +7 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +12 -0
- package/dist/build.js +14 -0
- package/dist/capability-descriptor.d.ts +46 -0
- package/dist/capability-descriptor.js +51 -0
- package/dist/catalog-frame.d.ts +39 -0
- package/dist/catalog-frame.js +72 -0
- package/dist/cause-class.d.ts +19 -0
- package/dist/cause-class.js +54 -0
- package/dist/cloudflare.d.ts +123 -0
- package/dist/cloudflare.js +391 -0
- package/dist/computer-terminal.d.ts +38 -0
- package/dist/computer-terminal.js +410 -0
- package/dist/computer.d.ts +60 -0
- package/dist/computer.js +33 -0
- package/dist/event-recipients.d.ts +57 -0
- package/dist/event-recipients.js +144 -0
- package/dist/facet-invocation.d.ts +4 -0
- package/dist/facet-invocation.js +45 -0
- package/dist/files.d.ts +42 -0
- package/dist/files.js +123 -0
- package/dist/frame-binding-wire-script.d.ts +2 -0
- package/dist/frame-binding-wire-script.js +1 -0
- package/dist/frame-binding-wire.d.ts +28 -0
- package/dist/frame-binding-wire.js +54 -0
- package/dist/git-admission.d.ts +52 -0
- package/dist/git-admission.js +188 -0
- package/dist/git-auth.d.ts +11 -0
- package/dist/git-auth.js +27 -0
- package/dist/git-door.d.ts +35 -0
- package/dist/git-door.js +108 -0
- package/dist/git-journal.d.ts +31 -0
- package/dist/git-journal.js +188 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +64 -0
- package/dist/guest-policy.d.ts +9 -0
- package/dist/guest-policy.js +13 -0
- package/dist/host.d.ts +244 -0
- package/dist/host.js +1043 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +320 -0
- package/dist/installation-consent.d.ts +79 -0
- package/dist/installation-consent.js +82 -0
- package/dist/internal/background-origins.d.ts +3 -0
- package/dist/internal/background-origins.js +8 -0
- package/dist/internal/delivery.d.ts +12 -0
- package/dist/internal/delivery.js +167 -0
- package/dist/internal/host-gate.d.ts +14 -0
- package/dist/internal/host-gate.js +62 -0
- package/dist/internal/registry-installations.d.ts +6 -0
- package/dist/internal/registry-installations.js +12 -0
- package/dist/invocation.d.ts +251 -0
- package/dist/invocation.js +377 -0
- package/dist/keys.d.ts +48 -0
- package/dist/keys.js +52 -0
- package/dist/mcp-owner.d.ts +27 -0
- package/dist/mcp-owner.js +155 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +429 -0
- package/dist/media.d.ts +23 -0
- package/dist/media.js +12 -0
- package/dist/pack.d.ts +79 -0
- package/dist/pack.js +151 -0
- package/dist/page/capabilities.d.ts +23 -0
- package/dist/page/capabilities.js +256 -0
- package/dist/page/computer-handles.d.ts +79 -0
- package/dist/page/computer-handles.js +44 -0
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/lifetime.d.ts +6 -0
- package/dist/page/lifetime.js +19 -0
- package/dist/page/mount.d.ts +163 -0
- package/dist/page/mount.js +390 -0
- package/dist/page/storage.d.ts +3 -0
- package/dist/page/storage.js +82 -0
- package/dist/page/terminal.d.ts +5 -0
- package/dist/page/terminal.js +7 -0
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.d.ts +14 -0
- package/dist/page-script.js +5 -0
- package/dist/r2-files.d.ts +3 -0
- package/dist/r2-files.js +79 -0
- package/dist/release.d.ts +83 -0
- package/dist/release.js +167 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +542 -0
- package/dist/shell/app-directory.js +199 -0
- package/dist/shell/browser-actions.d.ts +7 -0
- package/dist/shell/browser-actions.js +56 -0
- package/dist/shell/command-host.d.ts +112 -0
- package/dist/shell/command-host.js +502 -0
- package/dist/shell/commands.d.ts +25 -0
- package/dist/shell/commands.js +78 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/keys.d.ts +41 -0
- package/dist/shell/keys.js +134 -0
- package/dist/shell/shell-module.d.ts +55 -0
- package/dist/shell/shell-module.js +86 -0
- package/dist/shell/window-manager.d.ts +124 -0
- package/dist/shell/window-manager.js +711 -0
- package/dist/shell/workbench-contract.d.ts +68 -0
- package/dist/shell/workbench-contract.js +1 -0
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +96 -0
- package/dist/shell/workbench-host-types.js +1 -0
- package/dist/shell/workbench-host.d.ts +34 -0
- package/dist/shell/workbench-host.js +347 -0
- package/dist/shell/workbench-model.d.ts +192 -0
- package/dist/shell/workbench-model.js +53 -0
- package/dist/shell/workbench.d.ts +4 -0
- package/dist/shell/workbench.js +2 -0
- package/dist/source/fork.d.ts +27 -0
- package/dist/source/fork.js +46 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.js +10 -0
- package/dist/source/seed-receipt.d.ts +53 -0
- package/dist/source/seed-receipt.js +18 -0
- package/dist/source/team-source.d.ts +32 -0
- package/dist/source/team-source.js +42 -0
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +4 -0
- package/dist/source/template-repository.js +21 -0
- package/dist/source/template-seed.d.ts +68 -0
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +220 -0
- package/dist/standalone/agents.js +795 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +931 -0
- package/dist/standalone/workflow-host.js +751 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1694 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +657 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/tenant.d.ts +81 -0
- package/dist/tenant.js +71 -0
- package/dist/testing/capabilities.d.ts +29 -0
- package/dist/testing/capabilities.js +130 -0
- package/dist/testing/catalog.d.ts +3 -0
- package/dist/testing/catalog.js +53 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/installation.d.ts +11 -0
- package/dist/testing/installation.js +25 -0
- package/dist/testing/native-worker.d.ts +21 -0
- package/dist/testing/native-worker.js +106 -0
- package/dist/testing/worker.d.ts +18 -0
- package/dist/testing/worker.js +67 -0
- package/dist/testing/workers-runtime.d.ts +24 -0
- package/dist/testing/workers-runtime.js +33 -0
- package/dist/webhook-delivery.d.ts +14 -0
- package/dist/webhook-delivery.js +17 -0
- package/dist/workers-git.d.ts +285 -0
- package/dist/workers-git.js +1174 -0
- package/dist/workers.d.ts +352 -0
- package/dist/workers.js +868 -0
- package/docs/api/README.md +82 -0
- package/docs/api/agents.md +332 -0
- package/docs/api/app-storage.md +209 -0
- package/docs/api/apps.md +1375 -0
- package/docs/api/auth.md +63 -0
- package/docs/api/build-contract.md +1304 -0
- package/docs/api/build.md +1712 -0
- package/docs/api/cloudflare.md +421 -0
- package/docs/api/computer.md +203 -0
- package/docs/api/files.md +117 -0
- package/docs/api/git.md +470 -0
- package/docs/api/host.md +1328 -0
- package/docs/api/index.md +430 -0
- package/docs/api/invocation.md +738 -0
- package/docs/api/keys.md +175 -0
- package/docs/api/mcp.md +301 -0
- package/docs/api/media.md +40 -0
- package/docs/api/modules.md +50 -0
- package/docs/api/pack.md +141 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +599 -0
- package/docs/api/shell/command-host.md +224 -0
- package/docs/api/shell/commands.md +197 -0
- package/docs/api/shell/keys.md +183 -0
- package/docs/api/shell/shell-module.md +204 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +205 -0
- package/docs/api/shell.md +1915 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/tenant.md +194 -0
- package/docs/api/workers-git.md +1646 -0
- package/docs/api/workers.md +1691 -0
- package/docs/site/agent-quickstart.mdx +346 -0
- package/examples/standalone/worker.ts +7 -0
- package/examples/standalone/wrangler.jsonc +82 -0
- package/package.json +300 -3
package/dist/workers.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { installationSchema } from "./apps/installation.js";
|
|
2
|
+
import { registryInstallations } from "./internal/registry-installations.js";
|
|
3
|
+
import { approvedBindingGrants, scopedBindings } from "./apps/gate.js";
|
|
4
|
+
import { appMethodReplySchema } from "@fungi.computer/agar/backend";
|
|
5
|
+
import { nativeGitFrameMethods, nativeGitMethodNameSchema, } from "@fungi.computer/agar/git-rpc";
|
|
6
|
+
import { backgroundOriginKey, actorLane, mayDefer, } from "@fungi.computer/puffball/background-replay";
|
|
7
|
+
import { emptyAppDeclaration, appBackendBindingKinds, } from "@fungi.computer/puffball/app";
|
|
8
|
+
import { defaultTenant, tenantIdSchema, tenantScope, } from "./tenant.js";
|
|
9
|
+
import { decodeGillReply, } from "@fungi.computer/gills";
|
|
10
|
+
import { causeClass, failureDiagnostic } from "./cause-class.js";
|
|
11
|
+
import { ownerKeyAuth } from "./auth.js";
|
|
12
|
+
import { createOysterMcpOwner, remoteOysterMcp, } from "./mcp-owner.js";
|
|
13
|
+
export { createOysterMcpOwner, OysterMcpOwner, } from "./mcp-owner.js";
|
|
14
|
+
import { guestLimits } from "./guest-policy.js";
|
|
15
|
+
import { r2FileSource } from "./r2-files.js";
|
|
16
|
+
import { fileCapabilities } from "./files.js";
|
|
17
|
+
import { DurableObject } from "cloudflare:workers";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as Schema from "effect/Schema";
|
|
20
|
+
import * as Stream from "effect/Stream";
|
|
21
|
+
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
22
|
+
import { AppInstanceError, } from "./index.js";
|
|
23
|
+
import { createCloudflareAppInstance, createCloudflareInvocationGate, decodeFrameBindingProps, captureFrameBindingReply, frameBindingEntrypoint, backgroundFrameMethods, } from "./cloudflare.js";
|
|
24
|
+
import { invocationFactsSchema, assertBackgroundBinding, admitBackgroundFrame, } from "./invocation.js";
|
|
25
|
+
import { deliverBackground, deliverGitPush } from "./internal/delivery.js";
|
|
26
|
+
import { createOyster, verifyRelease, defaultAssetPolicy, } from "./host.js";
|
|
27
|
+
import { createGitPushJournal } from "./git-journal.js";
|
|
28
|
+
import { AppStorageError, } from "@fungi.computer/agar/storage";
|
|
29
|
+
import { storageFrameMethods } from "./storage-frame.js";
|
|
30
|
+
import { catalogFrameMethods, oysterAppCatalog } from "./catalog-frame.js";
|
|
31
|
+
import { appStorageKeyNameSchema, appStorageWriteSchema, appStorageResetSchema, } from "@fungi.computer/agar/storage";
|
|
32
|
+
import { createOysterRootData } from "./root-data.js";
|
|
33
|
+
import { backendCache, resolveBackend, } from "./release.js";
|
|
34
|
+
import { headContentObject } from "./build/content-objects.js";
|
|
35
|
+
import { ensureAppSourceTables, createOysterAppSourceReceipts, createOysterAppRegistrations, createOysterArtifactsRepositoryMetadataStore, } from "./app-sources.js";
|
|
36
|
+
export { r2FileSource, ownerKeyAuth };
|
|
37
|
+
/** Store Release artifacts in an R2 bucket. */
|
|
38
|
+
export function r2ArtifactStore(bucket) {
|
|
39
|
+
return {
|
|
40
|
+
head: (artifactRef) => headContentObject(bucket, artifactRef),
|
|
41
|
+
async get(artifactRef) {
|
|
42
|
+
const object = await bucket.get(artifactRef);
|
|
43
|
+
return object === null
|
|
44
|
+
? null
|
|
45
|
+
: new Uint8Array(await object.arrayBuffer());
|
|
46
|
+
},
|
|
47
|
+
async put(artifactRef, bytes) {
|
|
48
|
+
await bucket.put(artifactRef, bytes);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const recordRow = Schema.Struct({
|
|
53
|
+
installation_id: Schema.String,
|
|
54
|
+
record: Schema.String,
|
|
55
|
+
});
|
|
56
|
+
/** The installation registry: one SQLite-backed Durable Object per Oyster. */
|
|
57
|
+
export class OysterRegistry extends DurableObject {
|
|
58
|
+
#rootData;
|
|
59
|
+
/** Append-only App source seed receipts; a Git-capable subclass forks over these. */
|
|
60
|
+
appSourceReceipts;
|
|
61
|
+
/** Team-owned fork registrations, one App publisher record per App. */
|
|
62
|
+
appRegistrations;
|
|
63
|
+
/** Durable Artifacts repository provenance for this Oyster's own fork compositions. */
|
|
64
|
+
artifactsRepositories;
|
|
65
|
+
constructor(ctx, env) {
|
|
66
|
+
super(ctx, env);
|
|
67
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS installations (installation_id TEXT PRIMARY KEY, record TEXT NOT NULL) STRICT");
|
|
68
|
+
ensureAppSourceTables(ctx.storage.sql);
|
|
69
|
+
this.appSourceReceipts = createOysterAppSourceReceipts(ctx.storage.sql);
|
|
70
|
+
this.appRegistrations = createOysterAppRegistrations(ctx.storage.sql);
|
|
71
|
+
this.artifactsRepositories = createOysterArtifactsRepositoryMetadataStore(ctx.storage.sql);
|
|
72
|
+
this.#rootData = createOysterRootData({
|
|
73
|
+
sql: ctx.storage.sql,
|
|
74
|
+
commit: (body) => Effect.try({
|
|
75
|
+
try: () => ctx.storage.transactionSync(body),
|
|
76
|
+
catch: (cause) => cause instanceof Error
|
|
77
|
+
? cause
|
|
78
|
+
: new Error("Registry commit failed", { cause }),
|
|
79
|
+
}),
|
|
80
|
+
installations: {
|
|
81
|
+
list: () => Promise.resolve(this.list()),
|
|
82
|
+
},
|
|
83
|
+
resolveRelease: (release) => verifyRelease(r2ArtifactStore(env.RELEASES), release, defaultAssetPolicy, false),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Current desktop metadata; roles are the ADR's derived default, never Hub's stored choice. */
|
|
87
|
+
directory() {
|
|
88
|
+
return this.#rootData.directory();
|
|
89
|
+
}
|
|
90
|
+
/** Read one key for the authenticated root owner, under fresh App admission. */
|
|
91
|
+
storageRead(installationId, ownerId, key) {
|
|
92
|
+
return this.storageResult(this.storageView(installationId, () => Promise.resolve(ownerId)).read(key));
|
|
93
|
+
}
|
|
94
|
+
/** Write one declared key for the authenticated root owner. */
|
|
95
|
+
storageWrite(installationId, ownerId, input) {
|
|
96
|
+
return this.storageResult(this.storageView(installationId, () => Promise.resolve(ownerId)).write(input));
|
|
97
|
+
}
|
|
98
|
+
/** Reset one declared layer for the authenticated root owner. */
|
|
99
|
+
storageReset(installationId, ownerId, input) {
|
|
100
|
+
return this.storageResult(this.storageView(installationId, () => Promise.resolve(ownerId)).reset(input));
|
|
101
|
+
}
|
|
102
|
+
/** Storage for a live owner method frame; no guest-selected installation or member. */
|
|
103
|
+
storageBackend(raw, method, args, tenantId) {
|
|
104
|
+
const facts = Schema.decodeUnknownSync(invocationFactsSchema, {
|
|
105
|
+
onExcessProperty: "error",
|
|
106
|
+
})(raw);
|
|
107
|
+
const tenant = Schema.decodeUnknownSync(tenantIdSchema)(tenantId);
|
|
108
|
+
const view = this.storageView(facts.selection.owner.id, () => this.storageOwner(facts, tenant), facts);
|
|
109
|
+
switch (method) {
|
|
110
|
+
case "read":
|
|
111
|
+
return this.storageResult(view.read(Schema.decodeUnknownSync(Schema.Tuple([appStorageKeyNameSchema]), {
|
|
112
|
+
onExcessProperty: "error",
|
|
113
|
+
})(args)[0]));
|
|
114
|
+
case "write":
|
|
115
|
+
return this.storageResult(view.write(Schema.decodeUnknownSync(Schema.Tuple([appStorageWriteSchema]), {
|
|
116
|
+
onExcessProperty: "error",
|
|
117
|
+
})(args)[0]));
|
|
118
|
+
case "reset":
|
|
119
|
+
return this.storageResult(view.reset(Schema.decodeUnknownSync(Schema.Tuple([appStorageResetSchema]), {
|
|
120
|
+
onExcessProperty: "error",
|
|
121
|
+
})(args)[0]));
|
|
122
|
+
default:
|
|
123
|
+
return Promise.resolve({ kind: "rejected", code: "invalid" });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Open the existing App-storage invalidation stream under fresh owner admission. */
|
|
127
|
+
async storageEvents(installationId, ownerId, key) {
|
|
128
|
+
const view = this.storageView(installationId, () => Promise.resolve(ownerId));
|
|
129
|
+
const admitted = await this.storageResult(view.read(key));
|
|
130
|
+
if (admitted.kind === "rejected")
|
|
131
|
+
return Response.json(admitted, { status: 403 });
|
|
132
|
+
return new Response(Stream.toReadableStream(view
|
|
133
|
+
.events(key)
|
|
134
|
+
.pipe(Stream.map((event) => new TextEncoder().encode(event === null
|
|
135
|
+
? "event: ready\ndata: {}\n\n"
|
|
136
|
+
: `event: changed\ndata: ${JSON.stringify(event)}\n\n`)))), {
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "text/event-stream",
|
|
139
|
+
"Cache-Control": "no-store",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
storageResult(program) {
|
|
144
|
+
return Effect.runPromise(Effect.match(program, {
|
|
145
|
+
onSuccess: (value) => ({ kind: "accepted", value }),
|
|
146
|
+
onFailure: (error) => ({ kind: "rejected", code: error.code }),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
async storageOwner(facts, tenantId) {
|
|
150
|
+
const subject = facts.subject;
|
|
151
|
+
if (subject.kind !== "call")
|
|
152
|
+
return null;
|
|
153
|
+
const { actor, role } = subject.context;
|
|
154
|
+
if (actor.kind === "member")
|
|
155
|
+
return role === "owner" ? actor.memberId : null;
|
|
156
|
+
if (actor.kind !== "agent" ||
|
|
157
|
+
tenantId !== defaultTenant ||
|
|
158
|
+
this.env.AGENT_DIRECTORY === undefined)
|
|
159
|
+
return null;
|
|
160
|
+
const creator = await this.env.AGENT_DIRECTORY.getByName("agents").readCreator(actor.agentId);
|
|
161
|
+
return creator === null ? null : this.currentOwner(creator, tenantId);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolve a stored creator against the live owner. The single-owner root's
|
|
165
|
+
* owner is the identity its owner key admits; tenant roots override this
|
|
166
|
+
* with their canonical membership.
|
|
167
|
+
*/
|
|
168
|
+
currentOwner(memberId, tenantId) {
|
|
169
|
+
return Promise.resolve(tenantId === defaultTenant &&
|
|
170
|
+
memberId === (this.env.OYSTER_OWNER_NAME ?? "owner")
|
|
171
|
+
? memberId
|
|
172
|
+
: null);
|
|
173
|
+
}
|
|
174
|
+
storageView(installationId, owner, expected) {
|
|
175
|
+
return this.#rootData.storage.open(() => Effect.tryPromise({
|
|
176
|
+
try: async () => {
|
|
177
|
+
const installation = this.get(installationId);
|
|
178
|
+
if (installation?.status !== "enabled" ||
|
|
179
|
+
(expected !== undefined &&
|
|
180
|
+
(expected.selection.owner.kind !== "installation" ||
|
|
181
|
+
installation.instanceId !== expected.instanceId ||
|
|
182
|
+
installation.authorityVersion !==
|
|
183
|
+
expected.selection.authorityVersion ||
|
|
184
|
+
installation.release.releaseId !==
|
|
185
|
+
expected.selection.releaseId ||
|
|
186
|
+
installation.release.appId !== expected.selection.appId)))
|
|
187
|
+
throw new AppStorageError("unavailable");
|
|
188
|
+
const resolved = await verifyRelease(r2ArtifactStore(this.env.RELEASES), installation.release, defaultAssetPolicy, false);
|
|
189
|
+
if (expected !== undefined &&
|
|
190
|
+
resolved.backend?.digest !== expected.artifactDigest)
|
|
191
|
+
throw new AppStorageError("unavailable");
|
|
192
|
+
const ownerId = await owner();
|
|
193
|
+
if (ownerId === null)
|
|
194
|
+
throw new AppStorageError("owner_required");
|
|
195
|
+
const assertCurrent = () => {
|
|
196
|
+
if (expected !== undefined && Date.now() >= expected.deadlineAt)
|
|
197
|
+
throw new AppStorageError("unavailable");
|
|
198
|
+
const current = this.get(installationId);
|
|
199
|
+
if (current?.status !== "enabled" ||
|
|
200
|
+
current.authorityVersion !== installation.authorityVersion ||
|
|
201
|
+
current.release.releaseId !== installation.release.releaseId ||
|
|
202
|
+
current.release.artifact.artifactDigest !==
|
|
203
|
+
installation.release.artifact.artifactDigest)
|
|
204
|
+
throw new AppStorageError("unavailable");
|
|
205
|
+
};
|
|
206
|
+
assertCurrent();
|
|
207
|
+
return {
|
|
208
|
+
address: `installation:${installationId}`,
|
|
209
|
+
appId: installation.release.appId,
|
|
210
|
+
memberId: ownerId,
|
|
211
|
+
membershipVersion: 1,
|
|
212
|
+
isOwner: true,
|
|
213
|
+
keys: resolved.declaration?.storage ?? [],
|
|
214
|
+
assertCurrent,
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
catch: (cause) => cause instanceof AppStorageError
|
|
218
|
+
? cause
|
|
219
|
+
: new AppStorageError("unavailable", { cause }),
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
/** Every installation record. */
|
|
223
|
+
list() {
|
|
224
|
+
return this.ctx.storage.sql
|
|
225
|
+
.exec("SELECT installation_id, record FROM installations ORDER BY installation_id")
|
|
226
|
+
.toArray()
|
|
227
|
+
.map((row) => this.decode(row));
|
|
228
|
+
}
|
|
229
|
+
/** One installation record, or `null`. */
|
|
230
|
+
get(installationId) {
|
|
231
|
+
const row = this.ctx.storage.sql
|
|
232
|
+
.exec("SELECT installation_id, record FROM installations WHERE installation_id=?", installationId)
|
|
233
|
+
.toArray()[0];
|
|
234
|
+
return row === undefined ? null : this.decode(row);
|
|
235
|
+
}
|
|
236
|
+
/** Create or replace one installation record. */
|
|
237
|
+
async put(installation, tenantId = defaultTenant) {
|
|
238
|
+
await this.writeInstallation(installation, tenantId);
|
|
239
|
+
}
|
|
240
|
+
/** Native RPC returns a closed result; callers never classify a transported Error. */
|
|
241
|
+
async compareAndPut(installation, expectedAuthorityVersion, tenantId = defaultTenant) {
|
|
242
|
+
try {
|
|
243
|
+
await this.writeInstallation(installation, tenantId, expectedAuthorityVersion);
|
|
244
|
+
return { ok: true };
|
|
245
|
+
}
|
|
246
|
+
catch (cause) {
|
|
247
|
+
if (cause instanceof AppInstanceError && cause.code === "stale_authority")
|
|
248
|
+
return { ok: false, code: "stale_authority" };
|
|
249
|
+
throw cause;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async writeInstallation(installation, tenantId, expectedAuthorityVersion) {
|
|
253
|
+
const previous = this.get(installation.installationId);
|
|
254
|
+
if (expectedAuthorityVersion !== undefined &&
|
|
255
|
+
previous?.authorityVersion !== expectedAuthorityVersion)
|
|
256
|
+
throw new AppInstanceError("stale_authority");
|
|
257
|
+
const encode = (value) => JSON.stringify(installationSchema.parse(value));
|
|
258
|
+
const record = encode(installation);
|
|
259
|
+
const previousRecord = previous === null ? null : encode(previous);
|
|
260
|
+
if (previous !== null &&
|
|
261
|
+
previous.instanceId !== null &&
|
|
262
|
+
(previous.instanceId !== installation.instanceId ||
|
|
263
|
+
previous.authorityVersion !== installation.authorityVersion ||
|
|
264
|
+
previous.status !== installation.status ||
|
|
265
|
+
previous.release.artifact.artifactDigest !==
|
|
266
|
+
installation.release.artifact.artifactDigest))
|
|
267
|
+
if ((await resolveBackend(r2ArtifactStore(this.env.RELEASES), previous.release)) !== null) {
|
|
268
|
+
if (expectedAuthorityVersion !== undefined &&
|
|
269
|
+
this.get(installation.installationId)?.authorityVersion !==
|
|
270
|
+
expectedAuthorityVersion)
|
|
271
|
+
throw new AppInstanceError("stale_authority");
|
|
272
|
+
await this.env.INSTANCES.getByName(previous.instanceId).revoke(previous.installationId, undefined, tenantId);
|
|
273
|
+
}
|
|
274
|
+
this.ctx.storage.transactionSync(() => {
|
|
275
|
+
const current = this.get(installation.installationId);
|
|
276
|
+
if (expectedAuthorityVersion !== undefined &&
|
|
277
|
+
current?.authorityVersion !== expectedAuthorityVersion)
|
|
278
|
+
throw new AppInstanceError("stale_authority");
|
|
279
|
+
if ((current === null ? null : encode(current)) !== previousRecord)
|
|
280
|
+
throw new AppInstanceError("stale_authority");
|
|
281
|
+
this.ctx.storage.sql.exec("INSERT INTO installations(installation_id, record) VALUES(?,?) ON CONFLICT(installation_id) DO UPDATE SET record=excluded.record", installation.installationId, record);
|
|
282
|
+
if (installation.status === "deleted")
|
|
283
|
+
this.#rootData.storage.purgeInstallation(`installation:${installation.installationId}`);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
decode(row) {
|
|
287
|
+
const { record } = Schema.decodeUnknownSync(recordRow)(row);
|
|
288
|
+
return installationSchema.parse(JSON.parse(record));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const releaseRow = Schema.Struct({ release: Schema.String });
|
|
292
|
+
/**
|
|
293
|
+
* Settle one Instance operation for its host. An unexpected failure is logged
|
|
294
|
+
* here, beside the error object, because only its closed class crosses RPC.
|
|
295
|
+
*/
|
|
296
|
+
function result(run) {
|
|
297
|
+
return Effect.runPromise(Effect.match(run, {
|
|
298
|
+
onSuccess: (value) => ({ ok: true, value }),
|
|
299
|
+
onFailure: (error) => {
|
|
300
|
+
if (error instanceof AppInstanceError)
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
causeClass: causeClass(error),
|
|
304
|
+
code: error.code,
|
|
305
|
+
};
|
|
306
|
+
const diagnostic = failureDiagnostic(error);
|
|
307
|
+
console.error("AppInstance failure", {
|
|
308
|
+
stage: "instance_operation",
|
|
309
|
+
code: "failed",
|
|
310
|
+
...diagnostic,
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
causeClass: diagnostic.causeClass,
|
|
315
|
+
code: "failed",
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The one guest binding entrypoint over `table`. Every binding in a frame is
|
|
322
|
+
* this export with its own props; each packet returns to its originating
|
|
323
|
+
* Instance, which serves it from the same table.
|
|
324
|
+
*/
|
|
325
|
+
export function oysterBindingEntrypoint(table) {
|
|
326
|
+
return frameBindingEntrypoint(Object.fromEntries(Object.values(appBackendBindingKinds).flatMap(({ name }) => {
|
|
327
|
+
const row = table[name];
|
|
328
|
+
return row === undefined ? [] : [[name, row.methods]];
|
|
329
|
+
})), (env, props, method, args) => env.INSTANCES.getByName(props.instanceId).bindingCall(props, method, args));
|
|
330
|
+
}
|
|
331
|
+
const registryOf = (context) => context.env.REGISTRY.getByName(context.scope().durableName("registry"));
|
|
332
|
+
/** The plain Oyster host's bindings: App storage, the read-only catalog and replay admission. */
|
|
333
|
+
export const oysterBindings = {
|
|
334
|
+
APP_STORAGE: {
|
|
335
|
+
methods: storageFrameMethods.APP_STORAGE,
|
|
336
|
+
dispatch: (context, frame, _props, method, args) => registryOf(context).storageBackend(frame.facts, method, args, context.scope().tenantId),
|
|
337
|
+
},
|
|
338
|
+
APPS: {
|
|
339
|
+
methods: catalogFrameMethods.APPS,
|
|
340
|
+
async dispatch(context, frame) {
|
|
341
|
+
if (frame.host.declaration?.capabilities.includes("team.workbenchHost") !==
|
|
342
|
+
true)
|
|
343
|
+
throw new AppInstanceError("revoked");
|
|
344
|
+
const catalog = oysterAppCatalog(await registryOf(context).directory());
|
|
345
|
+
frame.signal.throwIfAborted();
|
|
346
|
+
return catalog;
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
BACKGROUND: {
|
|
350
|
+
methods: backgroundFrameMethods.BACKGROUND,
|
|
351
|
+
async dispatch(context, frame, _props, _method, [origin]) {
|
|
352
|
+
const host = await context.backgroundHost();
|
|
353
|
+
frame.signal.throwIfAborted();
|
|
354
|
+
if (host?.backgroundOriginKey === undefined)
|
|
355
|
+
return { kind: "refused", code: "access_changed" };
|
|
356
|
+
const key = await host.backgroundOriginKey();
|
|
357
|
+
frame.signal.throwIfAborted();
|
|
358
|
+
const decision = await admitBackgroundFrame(key, frame, origin);
|
|
359
|
+
frame.signal.throwIfAborted();
|
|
360
|
+
return decision;
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
/** The plain Oyster Worker's guest binding entrypoint. */
|
|
365
|
+
export class OysterBinding extends oysterBindingEntrypoint(oysterBindings) {
|
|
366
|
+
}
|
|
367
|
+
/** One host-facing handle over an Instance, pinned to `scope` and `budget`. */
|
|
368
|
+
function instanceHandle(target, scope, budget) {
|
|
369
|
+
const { tenantId, origin } = scope;
|
|
370
|
+
return {
|
|
371
|
+
apply: (state, release) => target.apply(state, release, budget, { tenantId, origin }),
|
|
372
|
+
fetch: (facts, request) => target.backend(facts, request, budget, tenantId),
|
|
373
|
+
call: async (facts, api, name, input) => {
|
|
374
|
+
const reply = await target.call(facts, api, name, input, budget, tenantId);
|
|
375
|
+
return reply.ok
|
|
376
|
+
? { ok: true, value: appMethodReplySchema.parse(reply.value) }
|
|
377
|
+
: reply;
|
|
378
|
+
},
|
|
379
|
+
gill: async (facts, api, source, input) => {
|
|
380
|
+
const reply = await target.gill(facts, api, source, input, budget, tenantId);
|
|
381
|
+
return reply.ok
|
|
382
|
+
? { ok: true, value: decodeGillReply(reply.value) }
|
|
383
|
+
: reply;
|
|
384
|
+
},
|
|
385
|
+
authorizeGitPush: (facts, question) => target.authorizeGitPush(facts, question, budget, tenantId),
|
|
386
|
+
onGitPush: (facts, fact) => target.onGitPush(facts, fact, budget, tenantId),
|
|
387
|
+
syncRefused: (facts, refusal) => target.syncRefused(facts, refusal, budget, tenantId),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* One App Instance: Oyster's lifecycle supervisor in this Durable Object's
|
|
392
|
+
* SQLite and the App backend in its `app` facet.
|
|
393
|
+
*/
|
|
394
|
+
export class OysterInstance extends DurableObject {
|
|
395
|
+
/** The bindings this Instance serves; a root's Instance names its own table. */
|
|
396
|
+
bindings = oysterBindings;
|
|
397
|
+
/** Stable per Instance; binding rows read the env, pin and root host through it. */
|
|
398
|
+
#bindingContext;
|
|
399
|
+
runtime;
|
|
400
|
+
limits = guestLimits();
|
|
401
|
+
frames;
|
|
402
|
+
/** Content-addressed backends are verified once per Instance, not per call. */
|
|
403
|
+
backends;
|
|
404
|
+
gitJournal;
|
|
405
|
+
constructor(ctx, env) {
|
|
406
|
+
super(ctx, env);
|
|
407
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_releases (release_id TEXT PRIMARY KEY, release TEXT NOT NULL) STRICT");
|
|
408
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_owner (id INTEGER PRIMARY KEY CHECK (id = 1), installation_id TEXT NOT NULL) STRICT");
|
|
409
|
+
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS oyster_tenant (id INTEGER PRIMARY KEY CHECK (id = 1), tenant_id TEXT NOT NULL, origin TEXT NOT NULL) STRICT");
|
|
410
|
+
this.#bindingContext = {
|
|
411
|
+
env,
|
|
412
|
+
scope: () => this.scope(),
|
|
413
|
+
backgroundHost: () => this.backgroundHost(),
|
|
414
|
+
};
|
|
415
|
+
this.backends = backendCache(r2ArtifactStore(env.RELEASES));
|
|
416
|
+
this.runtime = this.createRuntime(this.limits);
|
|
417
|
+
this.frames = createCloudflareInvocationGate(ctx, (frame) => this.closeBackground(frame));
|
|
418
|
+
// Local delivery avoids calling this Instance's own RPC stub from its alarm.
|
|
419
|
+
const instances = {
|
|
420
|
+
get: (instanceId) => this.localInstance(instanceId),
|
|
421
|
+
};
|
|
422
|
+
this.gitJournal = ctx.blockConcurrencyWhile(() => createGitPushJournal({
|
|
423
|
+
owner: ctx.storage,
|
|
424
|
+
deliver: (installationId, fact) => deliverGitPush({
|
|
425
|
+
artifacts: r2ArtifactStore(env.RELEASES),
|
|
426
|
+
installations: registryInstallations(env, this.scope()),
|
|
427
|
+
guestLimits: this.limits,
|
|
428
|
+
instances,
|
|
429
|
+
}, installationId, fact),
|
|
430
|
+
deliverSync: async (wake) => {
|
|
431
|
+
const host = await this.backgroundHost();
|
|
432
|
+
if (host === null)
|
|
433
|
+
return "refused";
|
|
434
|
+
const result = await deliverBackground({ ...host, instances }, wake);
|
|
435
|
+
if (result.more) {
|
|
436
|
+
const { installationId, actor, lane, originMethod, observed } = wake;
|
|
437
|
+
await (await this.gitJournal).armSync({
|
|
438
|
+
installationId,
|
|
439
|
+
actor,
|
|
440
|
+
lane,
|
|
441
|
+
originMethod,
|
|
442
|
+
observed,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
status: result.status,
|
|
447
|
+
receipt: { lane: wake.lane, epoch: wake.epoch },
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
scheduleWake: (at) => ctx.storage.transaction(async (transaction) => {
|
|
451
|
+
const current = await transaction.getAlarm();
|
|
452
|
+
if (current === null || at < current)
|
|
453
|
+
await transaction.setAlarm(at);
|
|
454
|
+
}),
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
/** The deployable root's host for one pinned tenant; this plain root is the single owner key. */
|
|
458
|
+
hostFor(scope) {
|
|
459
|
+
return ownerKeyRootHost(this.env, { scope });
|
|
460
|
+
}
|
|
461
|
+
/** Reconstruct current host admission for each wake; non-default tenants get no wakes. */
|
|
462
|
+
backgroundHost() {
|
|
463
|
+
if (!Schema.is(Schema.NonEmptyString)(this.env.OYSTER_OWNER_KEY))
|
|
464
|
+
return Promise.resolve(null);
|
|
465
|
+
const scope = this.scope();
|
|
466
|
+
if (scope.tenantId !== defaultTenant)
|
|
467
|
+
return Promise.resolve(null);
|
|
468
|
+
return Promise.resolve(this.hostFor(scope));
|
|
469
|
+
}
|
|
470
|
+
localInstance(instanceId) {
|
|
471
|
+
if (instanceId !== this.ctx.id.name)
|
|
472
|
+
throw new AppInstanceError("instance_identity_mismatch");
|
|
473
|
+
return instanceHandle(this, this.scope(), undefined);
|
|
474
|
+
}
|
|
475
|
+
async closeBackground(frame) {
|
|
476
|
+
const { subject } = frame.facts;
|
|
477
|
+
if (subject.kind !== "call" ||
|
|
478
|
+
subject.replay !== undefined ||
|
|
479
|
+
subject.method?.effect !== "write" ||
|
|
480
|
+
subject.context.backgroundLane === undefined ||
|
|
481
|
+
!mayDefer(subject.context, frame.facts.ancestry) ||
|
|
482
|
+
frame.host.declaration?.backend?.sync === undefined)
|
|
483
|
+
return;
|
|
484
|
+
if (subject.context.actor.kind !== "member" ||
|
|
485
|
+
subject.context.backgroundLane !== actorLane(subject.context.actor))
|
|
486
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
487
|
+
await (await this.gitJournal).armSync({
|
|
488
|
+
installationId: frame.facts.selection.owner.id,
|
|
489
|
+
actor: subject.context.actor,
|
|
490
|
+
lane: subject.context.backgroundLane,
|
|
491
|
+
originMethod: subject.method.name,
|
|
492
|
+
observed: {
|
|
493
|
+
authorityVersion: frame.facts.selection.authorityVersion,
|
|
494
|
+
artifactDigest: frame.facts.artifactDigest,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
createRuntime(limits) {
|
|
499
|
+
return createCloudflareAppInstance(this.ctx, this.env.LOADER, (releaseId, scope) => Effect.tryPromise({
|
|
500
|
+
try: async () => {
|
|
501
|
+
if (scope.kind === "invocation")
|
|
502
|
+
return scope.frame.host.artifact;
|
|
503
|
+
const release = this.release(releaseId);
|
|
504
|
+
const resolved = await this.backends(release);
|
|
505
|
+
if (resolved === null)
|
|
506
|
+
throw new Error("Release has no backend");
|
|
507
|
+
return { release, ...resolved };
|
|
508
|
+
},
|
|
509
|
+
catch: (cause) => cause instanceof Error ? cause : new Error("Release unavailable"),
|
|
510
|
+
}), (frame) => this.guestEnv(frame), limits);
|
|
511
|
+
}
|
|
512
|
+
runtimeFor(input) {
|
|
513
|
+
const limits = guestLimits(input);
|
|
514
|
+
if (limits.cpuMs !== this.limits.cpuMs ||
|
|
515
|
+
limits.subRequests !== this.limits.subRequests) {
|
|
516
|
+
// Supervisor/frame admission owns the permit here. The previous frame's
|
|
517
|
+
// facet is already aborted before a new budget can select its runtime.
|
|
518
|
+
this.runtime = this.createRuntime(limits);
|
|
519
|
+
this.limits = limits;
|
|
520
|
+
}
|
|
521
|
+
return this.runtime;
|
|
522
|
+
}
|
|
523
|
+
/** The tenant this Instance's host pinned at its first apply; never its guest frame. */
|
|
524
|
+
scope() {
|
|
525
|
+
const row = this.ctx.storage.sql
|
|
526
|
+
.exec("SELECT tenant_id, origin FROM oyster_tenant WHERE id = 1")
|
|
527
|
+
.toArray()[0];
|
|
528
|
+
if (row === undefined)
|
|
529
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
530
|
+
const pin = Schema.decodeUnknownSync(Schema.Struct({ tenant_id: tenantIdSchema, origin: Schema.String }))(row);
|
|
531
|
+
return tenantScope(pin.tenant_id, pin.origin);
|
|
532
|
+
}
|
|
533
|
+
requireTenant(tenantId) {
|
|
534
|
+
if (this.scope().tenantId !== tenantId)
|
|
535
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
536
|
+
}
|
|
537
|
+
/** The installation this Instance serves, once an owner state named it. */
|
|
538
|
+
installationId() {
|
|
539
|
+
const row = this.ctx.storage.sql
|
|
540
|
+
.exec("SELECT installation_id FROM oyster_owner WHERE id = 1")
|
|
541
|
+
.toArray()[0];
|
|
542
|
+
return row?.installation_id ?? null;
|
|
543
|
+
}
|
|
544
|
+
/** Every binding name is the one entrypoint, routed by props; authority is checked per packet. */
|
|
545
|
+
guestEnv(frame) {
|
|
546
|
+
if (frame.facts.instanceId !== this.ctx.id.name)
|
|
547
|
+
throw new AppInstanceError("instance_identity_mismatch");
|
|
548
|
+
return Object.fromEntries(Object.values(appBackendBindingKinds).map(({ name: binding }) => [
|
|
549
|
+
binding,
|
|
550
|
+
this.ctx.exports.OysterBinding({
|
|
551
|
+
props: {
|
|
552
|
+
instanceId: frame.facts.instanceId,
|
|
553
|
+
binding,
|
|
554
|
+
frameId: frame.frameId,
|
|
555
|
+
},
|
|
556
|
+
}),
|
|
557
|
+
]));
|
|
558
|
+
}
|
|
559
|
+
/** Resolve each guest packet against its immutable frame before any host effect. */
|
|
560
|
+
async bindingCall(raw, method, input) {
|
|
561
|
+
return captureFrameBindingReply(() => this.executeBindingCall(raw, method, input));
|
|
562
|
+
}
|
|
563
|
+
async executeBindingCall(raw, method, input) {
|
|
564
|
+
const props = decodeFrameBindingProps(raw);
|
|
565
|
+
const frame = this.frames.active();
|
|
566
|
+
if (props.instanceId !== frame.facts.instanceId ||
|
|
567
|
+
props.frameId !== frame.frameId)
|
|
568
|
+
throw new AppInstanceError("revoked");
|
|
569
|
+
assertBackgroundBinding(frame, props.binding);
|
|
570
|
+
// A guest packet is its own entry: exactly one registry read re-admits it.
|
|
571
|
+
await this.assertCurrent(frame);
|
|
572
|
+
const row = this.bindings[props.binding];
|
|
573
|
+
const descriptor = row?.methods[method];
|
|
574
|
+
// Operations declared by the verified Release; absent bindings grant nothing.
|
|
575
|
+
const granted = frame.facts.grants.find((grant) => grant.binding === props.binding)
|
|
576
|
+
?.operations ?? [];
|
|
577
|
+
if (granted.length === 0)
|
|
578
|
+
throw new AppInstanceError("binding_not_granted");
|
|
579
|
+
if (row === undefined || descriptor === undefined) {
|
|
580
|
+
const nativeGitMethod = props.binding === "GIT"
|
|
581
|
+
? nativeGitMethodNameSchema.safeParse(method)
|
|
582
|
+
: null;
|
|
583
|
+
const required = nativeGitMethod?.success
|
|
584
|
+
? nativeGitFrameMethods[nativeGitMethod.data].requiredOperations(input)
|
|
585
|
+
: [method];
|
|
586
|
+
if (!required.every((operation) => granted.includes(operation)))
|
|
587
|
+
throw new AppInstanceError("binding_not_granted");
|
|
588
|
+
throw new AppInstanceError("binding_unavailable");
|
|
589
|
+
}
|
|
590
|
+
const args = descriptor.decodeArgs(input);
|
|
591
|
+
if (!descriptor
|
|
592
|
+
.requiredOperations(args)
|
|
593
|
+
.every((operation) => granted.includes(operation)))
|
|
594
|
+
throw new AppInstanceError("binding_not_granted");
|
|
595
|
+
const result = await row.dispatch(this.#bindingContext, frame, props, method, args);
|
|
596
|
+
if (this.frames.active() !== frame)
|
|
597
|
+
throw new AppInstanceError("revoked");
|
|
598
|
+
assertBackgroundBinding(frame, props.binding);
|
|
599
|
+
return descriptor.decodeResult(result);
|
|
600
|
+
}
|
|
601
|
+
/** Last provider-accepted ref recorded by this installation's journal. */
|
|
602
|
+
async gitTip(installationId, repository, ref, tenantId = defaultTenant) {
|
|
603
|
+
this.requireTenant(tenantId);
|
|
604
|
+
if (installationId !== this.installationId())
|
|
605
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
606
|
+
return (await this.gitJournal).tip(installationId, repository, ref);
|
|
607
|
+
}
|
|
608
|
+
/** Join accepted fact identity and Carrier delivery in one local transaction. */
|
|
609
|
+
async recordGitPush(installationId, fact, tenantId = defaultTenant) {
|
|
610
|
+
this.requireTenant(tenantId);
|
|
611
|
+
if (installationId !== this.installationId())
|
|
612
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
613
|
+
return (await this.gitJournal).accept(installationId, fact);
|
|
614
|
+
}
|
|
615
|
+
/** Carrier owns retries, dead letters and retained delivery receipts. */
|
|
616
|
+
async alarm() {
|
|
617
|
+
await (await this.gitJournal).alarm();
|
|
618
|
+
}
|
|
619
|
+
/** Installation changes end guest work; a Workflow cancel targets only its run. */
|
|
620
|
+
async revoke(installationId, runId, tenantId = defaultTenant) {
|
|
621
|
+
this.requireTenant(tenantId);
|
|
622
|
+
if (installationId !== this.installationId())
|
|
623
|
+
throw new AppInstanceError("instance_owner_mismatch");
|
|
624
|
+
await this.frames.revoke(new AppInstanceError("revoked"), runId === undefined ? undefined : { runId });
|
|
625
|
+
}
|
|
626
|
+
/** One registry read re-admits a guest binding packet against its frame. */
|
|
627
|
+
async assertCurrent(frame) {
|
|
628
|
+
frame.signal.throwIfAborted();
|
|
629
|
+
const selected = frame.facts.selection;
|
|
630
|
+
const current = await this.env.REGISTRY.getByName(this.scope().durableName("registry")).get(selected.owner.id);
|
|
631
|
+
frame.signal.throwIfAborted();
|
|
632
|
+
if (current === null ||
|
|
633
|
+
"kind" in current ||
|
|
634
|
+
current.status !== "enabled" ||
|
|
635
|
+
current.instanceId !== selected.instanceId ||
|
|
636
|
+
current.authorityVersion !== selected.authorityVersion ||
|
|
637
|
+
current.release.releaseId !== selected.releaseId ||
|
|
638
|
+
current.release.artifact.artifactDigest !==
|
|
639
|
+
frame.host.artifact.release.artifact.artifactDigest)
|
|
640
|
+
throw new AppInstanceError("revoked");
|
|
641
|
+
const approved = approvedBindingGrants(current, frame.host.declaration ?? emptyAppDeclaration);
|
|
642
|
+
if (frame.facts.grants.some((grant) => grant.binding !== "BACKGROUND" &&
|
|
643
|
+
grant.operations.some((operation) => approved
|
|
644
|
+
.find((currentGrant) => currentGrant.binding === grant.binding)
|
|
645
|
+
?.operations.includes(operation) !== true)))
|
|
646
|
+
throw new AppInstanceError("revoked");
|
|
647
|
+
}
|
|
648
|
+
async withFrame(raw, limits, work, tenantId = defaultTenant, methodName) {
|
|
649
|
+
this.requireTenant(tenantId);
|
|
650
|
+
const facts = Schema.decodeUnknownSync(invocationFactsSchema, {
|
|
651
|
+
onExcessProperty: "error",
|
|
652
|
+
})(raw);
|
|
653
|
+
if (facts.instanceId !== this.ctx.id.name ||
|
|
654
|
+
facts.selection.owner.kind !== "installation" ||
|
|
655
|
+
facts.selection.owner.id !== this.installationId())
|
|
656
|
+
throw new AppInstanceError("instance_identity_mismatch");
|
|
657
|
+
const release = this.release(facts.selection.releaseId);
|
|
658
|
+
const resolved = await this.backends(release);
|
|
659
|
+
if (resolved?.digest !== facts.artifactDigest)
|
|
660
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
661
|
+
if (facts.subject.kind === "call" &&
|
|
662
|
+
facts.subject.method !== undefined &&
|
|
663
|
+
resolved.declaration?.api?.methods[facts.subject.method.name]?.effect !==
|
|
664
|
+
facts.subject.method.effect)
|
|
665
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
666
|
+
const bindings = resolved.declaration?.backend?.bindings ?? [];
|
|
667
|
+
if (facts.grants.some((grant) => {
|
|
668
|
+
if (grant.binding === "BACKGROUND")
|
|
669
|
+
return false;
|
|
670
|
+
const declared = bindings.find((binding) => binding.name === grant.binding);
|
|
671
|
+
return (declared === undefined ||
|
|
672
|
+
grant.operations.some((operation) => !declared.operations.includes(operation)));
|
|
673
|
+
}))
|
|
674
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
675
|
+
let scoped = facts;
|
|
676
|
+
if (methodName !== undefined) {
|
|
677
|
+
const api = resolved.declaration?.api;
|
|
678
|
+
if (facts.subject.kind !== "call" ||
|
|
679
|
+
!api ||
|
|
680
|
+
!Object.hasOwn(api.methods, methodName))
|
|
681
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
682
|
+
const method = api.methods[methodName];
|
|
683
|
+
if (method === undefined)
|
|
684
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
685
|
+
scoped = {
|
|
686
|
+
...facts,
|
|
687
|
+
grants: [
|
|
688
|
+
...scopedBindings(facts.grants.filter((grant) => grant.binding !== "BACKGROUND"), method),
|
|
689
|
+
...facts.grants.filter((grant) => grant.binding === "BACKGROUND"),
|
|
690
|
+
],
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
let host = {
|
|
694
|
+
artifact: { release, ...resolved },
|
|
695
|
+
declaration: resolved.declaration,
|
|
696
|
+
origin: "unchecked",
|
|
697
|
+
};
|
|
698
|
+
if (methodName !== undefined)
|
|
699
|
+
host = { ...host, method: Object.freeze({ name: methodName }) };
|
|
700
|
+
// Facts carry the authority admitted at the request's top. The Instance
|
|
701
|
+
// checks them against the owner state the host applied to it, never by
|
|
702
|
+
// calling back up to the registry.
|
|
703
|
+
return this.frames.run(scoped, host, (frame) => work(this.runtimeFor(limits), frame));
|
|
704
|
+
}
|
|
705
|
+
release(releaseId) {
|
|
706
|
+
const row = this.ctx.storage.sql
|
|
707
|
+
.exec("SELECT release FROM oyster_releases WHERE release_id=?", releaseId)
|
|
708
|
+
.toArray()[0];
|
|
709
|
+
if (row === undefined)
|
|
710
|
+
throw new Error("Release unknown to this Instance");
|
|
711
|
+
return Schema.decodeUnknownSync(Schema.fromJsonString(releaseSchema))(Schema.decodeUnknownSync(releaseRow)(row).release);
|
|
712
|
+
}
|
|
713
|
+
/** Remember `release`, then apply the owner state under the host's tenant pin. */
|
|
714
|
+
async apply(state, release, limits, tenant) {
|
|
715
|
+
const selected = tenantScope(Schema.decodeUnknownSync(tenantIdSchema)(tenant.tenantId), tenant.origin);
|
|
716
|
+
const checkTenant = () => {
|
|
717
|
+
const stored = this.ctx.storage.sql
|
|
718
|
+
.exec("SELECT tenant_id, origin FROM oyster_tenant WHERE id = 1")
|
|
719
|
+
.toArray()[0];
|
|
720
|
+
if (stored === undefined) {
|
|
721
|
+
if (this.installationId() !== null)
|
|
722
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const pin = this.scope();
|
|
726
|
+
if (pin.tenantId !== selected.tenantId || pin.origin !== selected.origin)
|
|
727
|
+
throw new AppInstanceError("installation_tenant_mismatch");
|
|
728
|
+
};
|
|
729
|
+
checkTenant();
|
|
730
|
+
await this.frames.revoke();
|
|
731
|
+
return this.frames.exclusive(async () => {
|
|
732
|
+
checkTenant();
|
|
733
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_releases(release_id, release) VALUES(?,?) ON CONFLICT(release_id) DO NOTHING", release.releaseId, Schema.encodeUnknownSync(Schema.fromJsonString(releaseSchema))(release));
|
|
734
|
+
const applied = await result(this.runtimeFor(limits).apply(state));
|
|
735
|
+
if (applied.ok) {
|
|
736
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_tenant (id, tenant_id, origin) VALUES (1, ?, ?) ON CONFLICT(id) DO NOTHING", selected.tenantId, selected.origin);
|
|
737
|
+
this.ctx.storage.sql.exec("INSERT INTO oyster_owner (id, installation_id) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET installation_id = excluded.installation_id", state.owner.id);
|
|
738
|
+
}
|
|
739
|
+
return applied;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/** Forward an admitted HTTP request to the App backend. */
|
|
743
|
+
async backend(facts, request, limits, tenantId = defaultTenant) {
|
|
744
|
+
try {
|
|
745
|
+
return await this.withFrame(facts, limits, (runtime, frame) => Effect.runPromise(runtime.fetch(frame, request)), tenantId);
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
return Response.json({ error: error instanceof AppInstanceError ? error.code : "failed" }, { status: error instanceof AppInstanceError ? 403 : 502 });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** Deliver a Git push through the same selected-Instance admission as methods. */
|
|
752
|
+
onGitPush(facts, fact, limits, tenantId = defaultTenant) {
|
|
753
|
+
return this.withFrame(facts, limits, (runtime, frame) => Effect.runPromise(runtime.onGitPush(frame, fact)), tenantId);
|
|
754
|
+
}
|
|
755
|
+
/** Ask the App's declared authorizer through the same frame-bound connection. */
|
|
756
|
+
authorizeGitPush(facts, question, limits, tenantId = defaultTenant) {
|
|
757
|
+
return this.withFrame(facts, limits, (runtime, frame) => Effect.runPromise(runtime.authorizeGitPush(frame, question)), tenantId);
|
|
758
|
+
}
|
|
759
|
+
/** An available lane settles a refusal under an active frame with no binding grants. */
|
|
760
|
+
syncRefused(facts, refusal, limits, tenantId = defaultTenant) {
|
|
761
|
+
return this.withFrame(facts, limits, (runtime, frame) => Effect.runPromise(runtime.syncRefused(frame, refusal)), tenantId);
|
|
762
|
+
}
|
|
763
|
+
/** Invoke a Release-declared Gill in a fresh immutable frame. */
|
|
764
|
+
gill(facts, _api, source, input, limits, tenantId = defaultTenant) {
|
|
765
|
+
return result(Effect.tryPromise({
|
|
766
|
+
try: () => this.withFrame(facts, limits, (runtime, frame) => {
|
|
767
|
+
const api = frame.host.declaration?.api;
|
|
768
|
+
if (!api || api.methods[source]?.effect !== "read")
|
|
769
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
770
|
+
return Effect.runPromise(runtime.gill(frame, api, source, input));
|
|
771
|
+
}, tenantId, source),
|
|
772
|
+
catch: (cause) => cause instanceof Error
|
|
773
|
+
? cause
|
|
774
|
+
: new Error("Gill invocation failed", { cause }),
|
|
775
|
+
}));
|
|
776
|
+
}
|
|
777
|
+
/** Invoke one declared App method; the value is its AppMethodReply wire,
|
|
778
|
+
* which the host handle decodes. */
|
|
779
|
+
call(facts, _api, name, input, limits, tenantId = defaultTenant) {
|
|
780
|
+
return result(Effect.tryPromise({
|
|
781
|
+
try: () => this.withFrame(facts, limits, (runtime, frame) => {
|
|
782
|
+
const api = frame.host.declaration?.api;
|
|
783
|
+
if (!api)
|
|
784
|
+
throw new AppInstanceError("invalid_invocation_facts");
|
|
785
|
+
return Effect.runPromise(runtime.call(frame, api, name, input));
|
|
786
|
+
}, tenantId, name),
|
|
787
|
+
catch: (cause) => cause instanceof Error
|
|
788
|
+
? cause
|
|
789
|
+
: new Error("App invocation failed", { cause }),
|
|
790
|
+
}));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/** Host adapters for a standalone Oyster Worker over its own bindings. */
|
|
794
|
+
export function oysterWorkerHost(env, auth, options = {}) {
|
|
795
|
+
const scope = options.scope ?? tenantScope(defaultTenant, env.OYSTER_ORIGIN);
|
|
796
|
+
const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
797
|
+
const installations = registryInstallations(env, scope);
|
|
798
|
+
const originSecret = Schema.decodeUnknownOption(Schema.NonEmptyString)(env.OYSTER_OWNER_KEY);
|
|
799
|
+
let host = {
|
|
800
|
+
auth,
|
|
801
|
+
workbench: {
|
|
802
|
+
directory: async () => {
|
|
803
|
+
await installations.list();
|
|
804
|
+
return registry().directory();
|
|
805
|
+
},
|
|
806
|
+
storage: (installationId, ownerId) => ({
|
|
807
|
+
read: (key) => registry().storageRead(installationId, ownerId, key),
|
|
808
|
+
write: (input) => registry().storageWrite(installationId, ownerId, input),
|
|
809
|
+
reset: (input) => registry().storageReset(installationId, ownerId, input),
|
|
810
|
+
events: (key) => registry().storageEvents(installationId, ownerId, key),
|
|
811
|
+
}),
|
|
812
|
+
},
|
|
813
|
+
guestLimits: guestLimits(options.guestLimits),
|
|
814
|
+
artifacts: r2ArtifactStore(env.RELEASES),
|
|
815
|
+
// File-only hosts may keep workspace files beside their Release artifacts.
|
|
816
|
+
capabilities: fileCapabilities(options.workspace?.files ??
|
|
817
|
+
r2FileSource(env.RELEASES, scope.workspacePrefix)),
|
|
818
|
+
installations,
|
|
819
|
+
instances: {
|
|
820
|
+
get: (instanceId, limits) => instanceHandle(env.INSTANCES.getByName(instanceId), scope, guestLimits(limits)),
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
if (originSecret._tag === "Some")
|
|
824
|
+
host = {
|
|
825
|
+
...host,
|
|
826
|
+
backgroundOriginKey: () => backgroundOriginKey(originSecret.value),
|
|
827
|
+
};
|
|
828
|
+
if (options.workspace !== undefined)
|
|
829
|
+
host = { ...host, computerId: options.workspace.computerId };
|
|
830
|
+
if (options.workspace?.computer !== undefined)
|
|
831
|
+
host = { ...host, computer: options.workspace.computer };
|
|
832
|
+
if (options.appToolPolicy !== undefined)
|
|
833
|
+
host = { ...host, appToolPolicy: options.appToolPolicy };
|
|
834
|
+
return {
|
|
835
|
+
...host,
|
|
836
|
+
mcp: {
|
|
837
|
+
handleRequest: (request) => remoteOysterMcp(env.MCP, scope).handleRequest(request),
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* The non-git standalone root: the single owner key over the default tenant's
|
|
843
|
+
* stores. Its HTTP Worker, MCP owner and background wakes all compose here.
|
|
844
|
+
*/
|
|
845
|
+
function ownerKeyRootHost(env, options) {
|
|
846
|
+
return oysterWorkerHost(env, ownerKeyAuth(env.OYSTER_OWNER_KEY), options);
|
|
847
|
+
}
|
|
848
|
+
/** The non-git standalone root's MCP owner. */
|
|
849
|
+
export class OysterMcp extends createOysterMcpOwner((env, scope) => ownerKeyRootHost(env, { scope })) {
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* The standalone Worker's `fetch` handler; frontend hosts are cached per env.
|
|
853
|
+
* MCP transports belong to the tenant Durable Object, independent of that cache.
|
|
854
|
+
*/
|
|
855
|
+
export function createOysterWorker(options = {}) {
|
|
856
|
+
const limits = guestLimits(options.guestLimits);
|
|
857
|
+
const apps = new WeakMap();
|
|
858
|
+
return {
|
|
859
|
+
fetch(request, env) {
|
|
860
|
+
let app = apps.get(env);
|
|
861
|
+
if (app === undefined) {
|
|
862
|
+
app = createOyster(ownerKeyRootHost(env, { guestLimits: limits }));
|
|
863
|
+
apps.set(env, app);
|
|
864
|
+
}
|
|
865
|
+
return app.fetch(request);
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
}
|