@fungi.computer/oyster 0.0.1 → 0.2.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 +228 -176
- package/dist/agent-avatars.d.ts +103 -0
- package/dist/agent-avatars.js +66 -0
- package/dist/agents.d.ts +12 -2
- package/dist/agents.js +39 -2
- package/dist/app-builds.d.ts +76 -17
- package/dist/app-builds.js +207 -63
- package/dist/app-sources.d.ts +7 -22
- package/dist/app-sources.js +49 -14
- package/dist/apps/gate.d.ts +7 -7
- package/dist/apps/gate.js +6 -6
- package/dist/apps/installation.d.ts +35 -35
- package/dist/apps/installation.js +20 -19
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.js +7 -1
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +12 -1
- package/dist/build/build-step.js +9 -1
- 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 +28220 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/effect.d.ts +18 -0
- package/dist/build/effect.js +52 -12
- package/dist/build/git-environment.d.ts +3 -0
- package/dist/build/git-environment.js +21 -9
- package/dist/build/local-build.d.ts +13 -3
- package/dist/build/local-build.js +149 -99
- package/dist/build/snapshot-package-store.d.ts +10 -0
- package/dist/build/snapshot-package-store.js +15 -0
- package/dist/build/snapshot-program-scripts.js +1 -1
- 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 +461 -17
- package/dist/build/submission-contract.js +136 -26
- package/dist/build/workspace-build.d.ts +7 -2
- package/dist/build/workspace-build.js +55 -11
- package/dist/build-contract.d.ts +3 -0
- package/dist/build-contract.js +3 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +1 -0
- package/dist/cloudflare.js +4 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.d.ts +1 -1
- package/dist/frame-binding-wire.js +1 -1
- package/dist/git-journal.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/installation-consent.d.ts +3 -1
- package/dist/installation-consent.js +2 -0
- package/dist/mcp-owner.js +20 -8
- package/dist/pack.d.ts +6 -3
- package/dist/pack.js +28 -11
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/mount.d.ts +4 -8
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +11 -0
- package/dist/release.js +4 -1
- package/dist/shell/app-directory.d.ts +50 -45
- package/dist/shell/app-directory.js +19 -18
- package/dist/shell/shell-module.d.ts +26 -2
- package/dist/shell/shell-module.js +51 -2
- package/dist/shell/window-manager.d.ts +5 -1
- package/dist/shell/window-manager.js +21 -2
- 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 +1 -1
- package/dist/shell/workbench-host.js +5 -0
- package/dist/shell/workbench-model.d.ts +3 -1
- package/dist/source/index.d.ts +6 -7
- package/dist/source/index.js +6 -7
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +2 -0
- package/dist/source/template-repository.js +13 -0
- package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
- 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 +227 -0
- package/dist/standalone/agents.js +834 -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 +955 -0
- package/dist/standalone/workflow-host.js +778 -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 +1716 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +713 -0
- package/dist/standalone/workflows.js +554 -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/workers-git.js +1 -4
- package/dist/workers.d.ts +2 -2
- package/dist/workers.js +3 -1
- package/docs/api/build-contract.md +829 -32
- package/docs/api/build.md +253 -17
- package/docs/api/modules.md +40 -32
- package/docs/api/pack.md +6 -3
- package/docs/api/shell/app-directory.md +267 -0
- package/docs/api/shell/shell-module.md +64 -3
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +19 -0
- package/docs/api/shell.md +127 -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/workers-git.md +140 -21
- package/docs/api/workers.md +15 -2
- package/docs/site/agent-quickstart.mdx +52 -55
- package/examples/standalone/worker.ts +7 -14
- package/examples/standalone/wrangler.jsonc +67 -6
- package/package.json +78 -25
- package/dist/build/r2-artifact-writer.d.ts +0 -31
- package/dist/build/r2-artifact-writer.js +0 -179
- package/dist/source/template-write.js +0 -344
- package/docs/media/architecture.mmd +0 -32
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/examples/git/worker.ts +0 -39
- package/examples/git/wrangler.jsonc +0 -32
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { FrameBindingProps } from "../cloudflare.js";
|
|
3
|
+
import type { InvocationFrame } from "../invocation.js";
|
|
4
|
+
import type { OysterBindingContext, OysterBindingResult } from "../workers.js";
|
|
5
|
+
import type { TenantScope } from "../tenant.js";
|
|
6
|
+
import type { StandaloneComputerEnv } from "./computer.js";
|
|
7
|
+
/** The Instance gate and native guest entrypoint share these argument codecs. */
|
|
8
|
+
export declare const appsFrameMethods: {
|
|
9
|
+
APPS: {
|
|
10
|
+
build: {
|
|
11
|
+
decodeArgs: (raw: readonly unknown[]) => [{
|
|
12
|
+
appId: string;
|
|
13
|
+
sourceRevision: string;
|
|
14
|
+
}];
|
|
15
|
+
decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Schema.Json;
|
|
16
|
+
requiredOperations: () => string[];
|
|
17
|
+
};
|
|
18
|
+
buildStatus: {
|
|
19
|
+
decodeArgs: (raw: readonly unknown[]) => [{
|
|
20
|
+
jobId: string;
|
|
21
|
+
}];
|
|
22
|
+
decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Schema.Json;
|
|
23
|
+
requiredOperations: () => string[];
|
|
24
|
+
};
|
|
25
|
+
select: {
|
|
26
|
+
decodeArgs: (raw: readonly unknown[]) => [{
|
|
27
|
+
installationId: string;
|
|
28
|
+
release: unknown;
|
|
29
|
+
}];
|
|
30
|
+
decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Schema.Json;
|
|
31
|
+
requiredOperations: () => string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** Called only after the Instance resolves the captured frame and current grants. */
|
|
36
|
+
export declare function dispatchAppsBuildsBinding(env: StandaloneComputerEnv, scope: TenantScope, frame: InvocationFrame, props: FrameBindingProps, method: string, args: readonly unknown[], backgroundHost: OysterBindingContext<StandaloneComputerEnv>["backgroundHost"]): Promise<OysterBindingResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The APPS guest binding on the standalone Bonsai root: start a build job on
|
|
3
|
+
* this Oyster's one Computer, read a job's status, and install (or switch) a
|
|
4
|
+
* Release at an installation id. Every operation re-checks the caller is the
|
|
5
|
+
* tenant owner; the binding carries no key and no owner secret.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import { AppInstanceError } from "../index.js";
|
|
10
|
+
import { installOrSelect } from "../host.js";
|
|
11
|
+
import { installationIdSchema } from "../apps/index.js";
|
|
12
|
+
import { appIdSchema, releaseSchema } from "@fungi.computer/puffball/effect";
|
|
13
|
+
/** The calling tenant's registry owns its App builds. */
|
|
14
|
+
const buildRegistry = (env, scope) => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
15
|
+
const buildInput = z.lazy(() => z.strictObject({
|
|
16
|
+
appId: z.string().min(1).max(256),
|
|
17
|
+
sourceRevision: z.string().min(1).max(256),
|
|
18
|
+
}));
|
|
19
|
+
const buildStatusInput = z.lazy(() => z.strictObject({
|
|
20
|
+
jobId: z.string().min(1).max(256),
|
|
21
|
+
}));
|
|
22
|
+
const selectInput = z.lazy(() => z.strictObject({
|
|
23
|
+
installationId: z.string().min(1).max(256),
|
|
24
|
+
release: z.unknown(),
|
|
25
|
+
}));
|
|
26
|
+
/** The Instance gate and native guest entrypoint share these argument codecs. */
|
|
27
|
+
export const appsFrameMethods = {
|
|
28
|
+
APPS: {
|
|
29
|
+
build: {
|
|
30
|
+
decodeArgs: (raw) => z.tuple([buildInput]).parse(raw),
|
|
31
|
+
decodeResult: Schema.decodeUnknownSync(Schema.Json),
|
|
32
|
+
requiredOperations: () => ["build"],
|
|
33
|
+
},
|
|
34
|
+
buildStatus: {
|
|
35
|
+
decodeArgs: (raw) => z.tuple([buildStatusInput]).parse(raw),
|
|
36
|
+
decodeResult: Schema.decodeUnknownSync(Schema.Json),
|
|
37
|
+
requiredOperations: () => ["buildStatus"],
|
|
38
|
+
},
|
|
39
|
+
select: {
|
|
40
|
+
decodeArgs: (raw) => z.tuple([selectInput]).parse(raw),
|
|
41
|
+
decodeResult: Schema.decodeUnknownSync(Schema.Json),
|
|
42
|
+
requiredOperations: () => ["select"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/** Only a live owner call reaches build, status or select; a key or a
|
|
47
|
+
* member without the owner role is refused before any registry or host read. */
|
|
48
|
+
function callerOwner(frame) {
|
|
49
|
+
const subject = frame.facts.subject;
|
|
50
|
+
if (subject.kind !== "call" ||
|
|
51
|
+
subject.context.actor.kind !== "member" ||
|
|
52
|
+
subject.context.role !== "owner")
|
|
53
|
+
throw new AppInstanceError("revoked");
|
|
54
|
+
return { actor: subject.context.actor, role: "owner" };
|
|
55
|
+
}
|
|
56
|
+
async function responseResult(response) {
|
|
57
|
+
return { status: response.status, body: await response.json() };
|
|
58
|
+
}
|
|
59
|
+
/** Called only after the Instance resolves the captured frame and current grants. */
|
|
60
|
+
export async function dispatchAppsBuildsBinding(env, scope, frame, props, method, args, backgroundHost) {
|
|
61
|
+
frame.signal.throwIfAborted();
|
|
62
|
+
if (props.binding !== "APPS")
|
|
63
|
+
throw new AppInstanceError("revoked");
|
|
64
|
+
const principal = callerOwner(frame);
|
|
65
|
+
const requestedBy = principal.actor.kind === "member" ? principal.actor.memberId : "owner";
|
|
66
|
+
if (method === "build") {
|
|
67
|
+
const [input] = z.tuple([buildInput]).parse(args);
|
|
68
|
+
const appId = Schema.decodeUnknownSync(appIdSchema)(input.appId);
|
|
69
|
+
const registry = buildRegistry(env, scope);
|
|
70
|
+
let build;
|
|
71
|
+
try {
|
|
72
|
+
build = await registry.startBuild({
|
|
73
|
+
appId,
|
|
74
|
+
sourceRevision: input.sourceRevision,
|
|
75
|
+
requestedBy,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return Schema.decodeUnknownSync(Schema.Json)({
|
|
80
|
+
status: 409,
|
|
81
|
+
body: { error: "App has no build source" },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
frame.signal.throwIfAborted();
|
|
85
|
+
return Schema.decodeUnknownSync(Schema.Json)({ status: 202, body: build });
|
|
86
|
+
}
|
|
87
|
+
if (method === "buildStatus") {
|
|
88
|
+
const [input] = z.tuple([buildStatusInput]).parse(args);
|
|
89
|
+
const build = await buildRegistry(env, scope).readBuild(input.jobId);
|
|
90
|
+
frame.signal.throwIfAborted();
|
|
91
|
+
return Schema.decodeUnknownSync(Schema.Json)({
|
|
92
|
+
status: build === null ? 404 : 200,
|
|
93
|
+
body: build ?? { error: "Not found" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (method === "select") {
|
|
97
|
+
const [input] = z.tuple([selectInput]).parse(args);
|
|
98
|
+
const installationId = installationIdSchema.parse(input.installationId);
|
|
99
|
+
let release;
|
|
100
|
+
try {
|
|
101
|
+
release = Schema.decodeUnknownSync(releaseSchema)(input.release, {
|
|
102
|
+
onExcessProperty: "error",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return Schema.decodeUnknownSync(Schema.Json)({
|
|
107
|
+
status: 400,
|
|
108
|
+
body: { error: "Invalid Release" },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const host = await backgroundHost();
|
|
112
|
+
if (host === null)
|
|
113
|
+
throw new AppInstanceError("revoked");
|
|
114
|
+
const response = await installOrSelect(host, installationId, release, principal);
|
|
115
|
+
frame.signal.throwIfAborted();
|
|
116
|
+
return Schema.decodeUnknownSync(Schema.Json)(await responseResult(response));
|
|
117
|
+
}
|
|
118
|
+
throw new AppInstanceError("revoked");
|
|
119
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { type PackOptions } from "../pack.js";
|
|
3
|
+
/** One starter App: where its source lives and how the Computer builds it. */
|
|
4
|
+
export type StarterApp = Readonly<{
|
|
5
|
+
installationId: string;
|
|
6
|
+
appId: string;
|
|
7
|
+
/** The App's directory in a Botanical checkout; its recipe is rooted there. */
|
|
8
|
+
directory: string;
|
|
9
|
+
/** Its own public source repository's name, the App at that repository's root. */
|
|
10
|
+
repository: string;
|
|
11
|
+
recipe: PackOptions["recipe"];
|
|
12
|
+
}>;
|
|
13
|
+
export declare const standaloneAppSet: readonly StarterApp[];
|
|
14
|
+
/** Where the starter Apps' sources come from. */
|
|
15
|
+
export type StarterAppSource = Readonly<{
|
|
16
|
+
/** A Botanical checkout at one exact commit; each App is its directory. */
|
|
17
|
+
kind: "checkout";
|
|
18
|
+
directory: string;
|
|
19
|
+
revision: string;
|
|
20
|
+
}> | Readonly<{
|
|
21
|
+
/**
|
|
22
|
+
* Each App's own repository, `${base}/${app.repository}.git`, at `ref`
|
|
23
|
+
* (a branch or tag): the App at the repository's root.
|
|
24
|
+
*/
|
|
25
|
+
kind: "repositories";
|
|
26
|
+
base: string;
|
|
27
|
+
ref: string;
|
|
28
|
+
/** A scratch directory the installer clones into; it removes each clone. */
|
|
29
|
+
scratch: string;
|
|
30
|
+
}>;
|
|
31
|
+
/** Wall-clock milliseconds one App spent in each install phase. */
|
|
32
|
+
export type StarterAppTimings = Readonly<{
|
|
33
|
+
/** Cloning its repository on this machine; zero for a checkout. */
|
|
34
|
+
fetch: number;
|
|
35
|
+
/** Seeding its template on the Oyster by git push. */
|
|
36
|
+
publish: number;
|
|
37
|
+
/** Waiting for a build slot on the Computer. */
|
|
38
|
+
queue: number;
|
|
39
|
+
/** From starting its build to the build's terminal state. */
|
|
40
|
+
build: number;
|
|
41
|
+
/** Installing the built Release. */
|
|
42
|
+
install: number;
|
|
43
|
+
/** The Computer's own report of its build, when it gives one. */
|
|
44
|
+
computer: Readonly<{
|
|
45
|
+
clone: number;
|
|
46
|
+
install: number;
|
|
47
|
+
build: number;
|
|
48
|
+
upload: number;
|
|
49
|
+
}> | null;
|
|
50
|
+
}>;
|
|
51
|
+
/** One App the installer published, built and installed. */
|
|
52
|
+
export type InstalledStarterApp = Readonly<{
|
|
53
|
+
installationId: string;
|
|
54
|
+
published: "recorded" | "existing";
|
|
55
|
+
seedRevision: string;
|
|
56
|
+
sourceRevision: string;
|
|
57
|
+
jobId: string;
|
|
58
|
+
installed: "installed" | "replaced";
|
|
59
|
+
release: Schema.Json;
|
|
60
|
+
timings: StarterAppTimings;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Builds the installer runs at once by default. A build's compiler may take a
|
|
64
|
+
* 4 GB heap, so two fit the platform's 12 GiB Computer (standard-4) with room
|
|
65
|
+
* for pnpm and esbuild; a third is the most it will run.
|
|
66
|
+
*/
|
|
67
|
+
export declare const defaultStarterBuildConcurrency = 2;
|
|
68
|
+
/**
|
|
69
|
+
* Install the standalone App set, each App built on the Oyster's own
|
|
70
|
+
* Computer. Each App's editable source is published as its template; the host
|
|
71
|
+
* builds that seed revision and the installer installs the exact Release the
|
|
72
|
+
* build recorded. Builds run `concurrency` at a time on the one Computer,
|
|
73
|
+
* Tourtois first; each App installs as soon as its own build succeeds. No App
|
|
74
|
+
* is compiled on this machine and no installed App is needed to build.
|
|
75
|
+
*/
|
|
76
|
+
export declare function installStandaloneAppSet(options: Readonly<{
|
|
77
|
+
source: StarterAppSource;
|
|
78
|
+
url: string;
|
|
79
|
+
key: string;
|
|
80
|
+
report: (message: string) => void;
|
|
81
|
+
/** Builds at once on the Computer, 1 to 3; one by default. */
|
|
82
|
+
concurrency?: number;
|
|
83
|
+
/** Publishes one App's source; the real installer uses `publishSource`. */
|
|
84
|
+
publish?: (app: StarterApp, located: Readonly<{
|
|
85
|
+
repositoryPath: string;
|
|
86
|
+
sourceRevision: string;
|
|
87
|
+
recipe: PackOptions["recipe"];
|
|
88
|
+
}>) => Promise<Response>;
|
|
89
|
+
pollMs?: number;
|
|
90
|
+
waitMs?: number;
|
|
91
|
+
}>): Promise<readonly InstalledStarterApp[]>;
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import { publishSource } from "../pack.js";
|
|
7
|
+
import { parseSourceRevision } from "@fungi.computer/puffball";
|
|
8
|
+
// Existing closed source recipes, shared by the standalone install command and
|
|
9
|
+
// its real-root proof. The canonical Oyster builder owns every compilation, on
|
|
10
|
+
// the Oyster's own Computer.
|
|
11
|
+
const assets = {
|
|
12
|
+
maxManifestBytes: 1_048_576,
|
|
13
|
+
maxObjects: 1024,
|
|
14
|
+
maxObjectBytes: 16_777_216,
|
|
15
|
+
maxTotalBytes: 67_108_864,
|
|
16
|
+
};
|
|
17
|
+
const facet = { kind: "durable-facet", stateSchema: 1 };
|
|
18
|
+
export const standaloneAppSet = [
|
|
19
|
+
{
|
|
20
|
+
installationId: "tourtois",
|
|
21
|
+
directory: "apps/tourtois",
|
|
22
|
+
repository: "tourtois",
|
|
23
|
+
appId: "app_tourtois",
|
|
24
|
+
recipe: {
|
|
25
|
+
entry: "apps/tourtois/index.html",
|
|
26
|
+
assets,
|
|
27
|
+
assetBase: "./",
|
|
28
|
+
backend: {
|
|
29
|
+
entry: "apps/tourtois/backend/index.ts",
|
|
30
|
+
exportName: "Tourtois",
|
|
31
|
+
compatibilityDate: "2026-09-04",
|
|
32
|
+
runtime: facet,
|
|
33
|
+
},
|
|
34
|
+
maxOutputBytes: 67_108_864,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
installationId: "bonsai",
|
|
39
|
+
directory: "apps/bonsai",
|
|
40
|
+
repository: "bonsai",
|
|
41
|
+
appId: "app_bonsai",
|
|
42
|
+
recipe: {
|
|
43
|
+
entry: "apps/bonsai/web/index.html",
|
|
44
|
+
assets,
|
|
45
|
+
assetBase: "./",
|
|
46
|
+
maxOutputBytes: 67_108_864,
|
|
47
|
+
backend: {
|
|
48
|
+
entry: "apps/bonsai/backend/index.ts",
|
|
49
|
+
exportName: "BonsaiApp",
|
|
50
|
+
compatibilityDate: "2026-09-04",
|
|
51
|
+
runtime: facet,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
installationId: "shed",
|
|
57
|
+
directory: "apps/shed",
|
|
58
|
+
repository: "shed",
|
|
59
|
+
appId: "app_shed",
|
|
60
|
+
recipe: {
|
|
61
|
+
entry: "apps/shed/index.html",
|
|
62
|
+
assets: {
|
|
63
|
+
maxManifestBytes: 1_048_576,
|
|
64
|
+
maxObjects: 512,
|
|
65
|
+
maxObjectBytes: 4_194_304,
|
|
66
|
+
maxTotalBytes: 16_777_216,
|
|
67
|
+
},
|
|
68
|
+
assetBase: "./",
|
|
69
|
+
maxOutputBytes: 16_777_216,
|
|
70
|
+
backend: {
|
|
71
|
+
entry: "apps/shed/backend/index.ts",
|
|
72
|
+
exportName: "Shed",
|
|
73
|
+
compatibilityDate: "2026-09-25",
|
|
74
|
+
runtime: facet,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
installationId: "inkcap",
|
|
80
|
+
directory: "apps/inkcap",
|
|
81
|
+
repository: "inkcap",
|
|
82
|
+
appId: "app_inkcap",
|
|
83
|
+
recipe: {
|
|
84
|
+
entry: "apps/inkcap/index.html",
|
|
85
|
+
assets,
|
|
86
|
+
assetBase: "./",
|
|
87
|
+
maxOutputBytes: 16_777_216,
|
|
88
|
+
backend: {
|
|
89
|
+
entry: "apps/inkcap/backend/index.ts",
|
|
90
|
+
exportName: "Inkcap",
|
|
91
|
+
compatibilityDate: "2026-09-25",
|
|
92
|
+
runtime: facet,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
installationId: "trellis",
|
|
98
|
+
directory: "apps/trellis",
|
|
99
|
+
repository: "trellis",
|
|
100
|
+
appId: "app_trellis",
|
|
101
|
+
recipe: {
|
|
102
|
+
entry: "apps/trellis/index.html",
|
|
103
|
+
maxOutputBytes: 16_777_216,
|
|
104
|
+
backend: {
|
|
105
|
+
entry: "apps/trellis/backend/index.ts",
|
|
106
|
+
exportName: "Trellis",
|
|
107
|
+
compatibilityDate: "2026-09-04",
|
|
108
|
+
runtime: facet,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
installationId: "prints",
|
|
114
|
+
directory: "apps/prints",
|
|
115
|
+
repository: "prints",
|
|
116
|
+
appId: "app_prints",
|
|
117
|
+
recipe: {
|
|
118
|
+
entry: "apps/prints/index.html",
|
|
119
|
+
assets,
|
|
120
|
+
assetBase: "./",
|
|
121
|
+
maxOutputBytes: 16_777_216,
|
|
122
|
+
backend: {
|
|
123
|
+
entry: "apps/prints/backend/index.ts",
|
|
124
|
+
exportName: "Prints",
|
|
125
|
+
compatibilityDate: "2026-09-04",
|
|
126
|
+
runtime: facet,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
installationId: "grove",
|
|
132
|
+
directory: "apps/grove",
|
|
133
|
+
repository: "grove",
|
|
134
|
+
appId: "app_grove",
|
|
135
|
+
recipe: {
|
|
136
|
+
entry: "apps/grove/index.html",
|
|
137
|
+
assets,
|
|
138
|
+
assetBase: "./",
|
|
139
|
+
maxOutputBytes: 67_108_864,
|
|
140
|
+
backend: {
|
|
141
|
+
entry: "apps/grove/backend/index.ts",
|
|
142
|
+
exportName: "Grove",
|
|
143
|
+
compatibilityDate: "2026-09-04",
|
|
144
|
+
runtime: facet,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
const publishedSchema = Schema.Struct({
|
|
150
|
+
kind: Schema.Literals(["recorded", "existing"]),
|
|
151
|
+
receipt: Schema.Struct({
|
|
152
|
+
seedRevision: Schema.String,
|
|
153
|
+
/** The upstream commit this distribution was projected from. */
|
|
154
|
+
sourceRevision: Schema.String,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
const buildSchema = Schema.Struct({
|
|
158
|
+
jobId: Schema.String,
|
|
159
|
+
status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
|
|
160
|
+
detail: Schema.NullOr(Schema.String),
|
|
161
|
+
/** Installed exactly as the host encoded it. */
|
|
162
|
+
release: Schema.NullOr(Schema.Json),
|
|
163
|
+
});
|
|
164
|
+
/** The phases a Computer build reports in its success detail. */
|
|
165
|
+
const computerPhasesSchema = Schema.fromJsonString(Schema.Struct({
|
|
166
|
+
phasesMs: Schema.Struct({
|
|
167
|
+
clone: Schema.Number,
|
|
168
|
+
install: Schema.Number,
|
|
169
|
+
build: Schema.Number,
|
|
170
|
+
upload: Schema.Number,
|
|
171
|
+
}),
|
|
172
|
+
}));
|
|
173
|
+
/** How long one App's build may run before the installer gives up: the
|
|
174
|
+
* host's whole-build budget plus queueing on the one Computer. */
|
|
175
|
+
const appBuildWaitMs = 20 * 60_000;
|
|
176
|
+
/**
|
|
177
|
+
* Builds the installer runs at once by default. A build's compiler may take a
|
|
178
|
+
* 4 GB heap, so two fit the platform's 12 GiB Computer (standard-4) with room
|
|
179
|
+
* for pnpm and esbuild; a third is the most it will run.
|
|
180
|
+
*/
|
|
181
|
+
export const defaultStarterBuildConcurrency = 2;
|
|
182
|
+
/** The most builds at once on the 12 GiB Computer. */
|
|
183
|
+
const maxStarterBuildConcurrency = 3;
|
|
184
|
+
/** A recipe rooted at `from` (a checkout directory), rooted at `to` instead. */
|
|
185
|
+
function relocateRecipe(recipe, from, to) {
|
|
186
|
+
const move = (path) => {
|
|
187
|
+
if (!path.startsWith(`${from}/`))
|
|
188
|
+
throw new Error(`${path} is outside its App directory ${from}`);
|
|
189
|
+
const rest = path.slice(from.length + 1);
|
|
190
|
+
return to === "" ? rest : `${to}/${rest}`;
|
|
191
|
+
};
|
|
192
|
+
const backend = recipe.backend;
|
|
193
|
+
if (backend?.entry === undefined)
|
|
194
|
+
return { ...recipe, entry: move(recipe.entry) };
|
|
195
|
+
return {
|
|
196
|
+
...recipe,
|
|
197
|
+
entry: move(recipe.entry),
|
|
198
|
+
backend: { ...backend, entry: move(backend.entry) },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const run = (command, args, cwd) => new Promise((resolveRun, reject) => {
|
|
202
|
+
execFile(command, args, { cwd, encoding: "utf8", maxBuffer: 1_048_576 }, (error, stdout) => {
|
|
203
|
+
if (error)
|
|
204
|
+
reject(new Error(`${command} ${args[0] ?? ""} failed`, { cause: error }));
|
|
205
|
+
else
|
|
206
|
+
resolveRun(stdout);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Install the standalone App set, each App built on the Oyster's own
|
|
211
|
+
* Computer. Each App's editable source is published as its template; the host
|
|
212
|
+
* builds that seed revision and the installer installs the exact Release the
|
|
213
|
+
* build recorded. Builds run `concurrency` at a time on the one Computer,
|
|
214
|
+
* Tourtois first; each App installs as soon as its own build succeeds. No App
|
|
215
|
+
* is compiled on this machine and no installed App is needed to build.
|
|
216
|
+
*/
|
|
217
|
+
export async function installStandaloneAppSet(options) {
|
|
218
|
+
const concurrency = options.concurrency ?? defaultStarterBuildConcurrency;
|
|
219
|
+
if (!Number.isInteger(concurrency) ||
|
|
220
|
+
concurrency < 1 ||
|
|
221
|
+
concurrency > maxStarterBuildConcurrency)
|
|
222
|
+
throw new Error(`Starter builds run 1 to ${String(maxStarterBuildConcurrency)} at a time`);
|
|
223
|
+
const authorization = { Authorization: `Bearer ${options.key}` };
|
|
224
|
+
const owner = async (method, path, body, signal) => {
|
|
225
|
+
const init = body === undefined
|
|
226
|
+
? { method, headers: authorization }
|
|
227
|
+
: {
|
|
228
|
+
method,
|
|
229
|
+
headers: { ...authorization, "Content-Type": "application/json" },
|
|
230
|
+
body: JSON.stringify(body),
|
|
231
|
+
};
|
|
232
|
+
if (signal !== undefined)
|
|
233
|
+
init.signal = signal;
|
|
234
|
+
return fetch(new URL(path, options.url), init);
|
|
235
|
+
};
|
|
236
|
+
/** Start one build and wait for its terminal outcome within the build
|
|
237
|
+
* budget; the budget cancels every request and body read it covers. */
|
|
238
|
+
const buildOnComputer = async (name, appId, seedRevision) => {
|
|
239
|
+
const budget = AbortSignal.timeout(options.waitMs ?? appBuildWaitMs);
|
|
240
|
+
let jobId = null;
|
|
241
|
+
try {
|
|
242
|
+
const started = await owner("POST", "/api/builds", { appId, sourceRevision: seedRevision }, budget);
|
|
243
|
+
if (started.status !== 202)
|
|
244
|
+
throw new Error(`${name} build refused (${String(started.status)})`);
|
|
245
|
+
let build = Schema.decodeUnknownSync(buildSchema)(await started.json());
|
|
246
|
+
jobId = build.jobId;
|
|
247
|
+
options.report(`Building ${name} on the Computer (${build.jobId})`);
|
|
248
|
+
while (build.status === "running") {
|
|
249
|
+
await sleep(options.pollMs ?? 5_000, undefined, { signal: budget });
|
|
250
|
+
const read = await owner("GET", `/api/builds/${encodeURIComponent(build.jobId)}`, undefined, budget);
|
|
251
|
+
if (!read.ok)
|
|
252
|
+
throw new Error(`${name} build is missing (${String(read.status)})`);
|
|
253
|
+
build = Schema.decodeUnknownSync(buildSchema)(await read.json());
|
|
254
|
+
}
|
|
255
|
+
return build;
|
|
256
|
+
}
|
|
257
|
+
catch (cause) {
|
|
258
|
+
if (budget.aborted)
|
|
259
|
+
throw new Error(`${name} build did not finish (${jobId ?? "not started"})`, {
|
|
260
|
+
cause,
|
|
261
|
+
});
|
|
262
|
+
throw cause;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const publish = options.publish ??
|
|
266
|
+
((app, located) => publishSource({
|
|
267
|
+
appId: app.appId,
|
|
268
|
+
entry: located.recipe.entry,
|
|
269
|
+
repositoryPath: located.repositoryPath,
|
|
270
|
+
sourceRevision: located.sourceRevision,
|
|
271
|
+
recipe: located.recipe,
|
|
272
|
+
url: options.url,
|
|
273
|
+
key: options.key,
|
|
274
|
+
}));
|
|
275
|
+
/** Locate one App's source at an exact commit, cloning it when it has its
|
|
276
|
+
* own repository; `release` removes a clone once its template is seeded. */
|
|
277
|
+
const locate = async (app) => {
|
|
278
|
+
const source = options.source;
|
|
279
|
+
if (source.kind === "checkout")
|
|
280
|
+
return {
|
|
281
|
+
repositoryPath: source.directory,
|
|
282
|
+
sourceRevision: parseSourceRevision(source.revision),
|
|
283
|
+
recipe: app.recipe,
|
|
284
|
+
release: () => Promise.resolve(),
|
|
285
|
+
};
|
|
286
|
+
const clone = await mkdtemp(join(source.scratch, `${app.repository}-`));
|
|
287
|
+
try {
|
|
288
|
+
await run("git", [
|
|
289
|
+
"clone",
|
|
290
|
+
"--quiet",
|
|
291
|
+
"--depth=1",
|
|
292
|
+
`--branch=${source.ref}`,
|
|
293
|
+
"--",
|
|
294
|
+
`${source.base}/${app.repository}.git`,
|
|
295
|
+
clone,
|
|
296
|
+
]);
|
|
297
|
+
const head = (await run("git", ["rev-parse", "HEAD"], clone)).trim();
|
|
298
|
+
return {
|
|
299
|
+
repositoryPath: clone,
|
|
300
|
+
sourceRevision: parseSourceRevision(`git:${head}`),
|
|
301
|
+
recipe: relocateRecipe(app.recipe, app.directory, ""),
|
|
302
|
+
release: () => rm(clone, { recursive: true, force: true }),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
catch (cause) {
|
|
306
|
+
await rm(clone, { recursive: true, force: true });
|
|
307
|
+
throw cause;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
// Seed every template first: a git push each, quick beside a build.
|
|
311
|
+
const seeded = [];
|
|
312
|
+
for (const app of standaloneAppSet) {
|
|
313
|
+
const name = app.installationId;
|
|
314
|
+
const fetchStarted = performance.now();
|
|
315
|
+
const located = await locate(app);
|
|
316
|
+
const publishStarted = performance.now();
|
|
317
|
+
try {
|
|
318
|
+
const published = await publish(app, located);
|
|
319
|
+
if (!published.ok)
|
|
320
|
+
throw new Error(`${name} source publish failed (${String(published.status)})`);
|
|
321
|
+
const { kind, receipt } = Schema.decodeUnknownSync(publishedSchema)(await published.json());
|
|
322
|
+
options.report(`${name} source ${kind} at ${receipt.seedRevision}`);
|
|
323
|
+
const ready = performance.now();
|
|
324
|
+
seeded.push({
|
|
325
|
+
app,
|
|
326
|
+
kind,
|
|
327
|
+
seedRevision: receipt.seedRevision,
|
|
328
|
+
sourceRevision: receipt.sourceRevision,
|
|
329
|
+
fetch: publishStarted - fetchStarted,
|
|
330
|
+
publish: ready - publishStarted,
|
|
331
|
+
ready,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await located.release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Then build `concurrency` at a time, in set order, installing each as its
|
|
339
|
+
// build succeeds. The first failure rejects the install and starts no
|
|
340
|
+
// further build; builds already running finish and install.
|
|
341
|
+
const installed = new Map();
|
|
342
|
+
const pending = [...seeded];
|
|
343
|
+
/** Set by the first failure, so no further build starts. */
|
|
344
|
+
const stop = { requested: false };
|
|
345
|
+
const slot = async () => {
|
|
346
|
+
for (let next = stop.requested ? undefined : pending.shift(); next !== undefined; next = stop.requested ? undefined : pending.shift()) {
|
|
347
|
+
const name = next.app.installationId;
|
|
348
|
+
try {
|
|
349
|
+
const buildStarted = performance.now();
|
|
350
|
+
const build = await buildOnComputer(name, next.app.appId, next.seedRevision);
|
|
351
|
+
if (build.status !== "succeeded" || build.release === null)
|
|
352
|
+
throw new Error(`${name} build ${build.status}: ${build.detail ?? "no Release"}`);
|
|
353
|
+
const installStarted = performance.now();
|
|
354
|
+
const installation = await owner("POST", "/api/installations", {
|
|
355
|
+
release: build.release,
|
|
356
|
+
installationId: name,
|
|
357
|
+
});
|
|
358
|
+
if (!installation.ok)
|
|
359
|
+
throw new Error(`${name} installation failed (${String(installation.status)})`);
|
|
360
|
+
options.report(`${installation.status === 201 ? "Installed" : "Replaced"} ${name} from its build ${build.jobId}`);
|
|
361
|
+
const phases = build.detail === null
|
|
362
|
+
? undefined
|
|
363
|
+
: Schema.decodeUnknownOption(computerPhasesSchema)(build.detail);
|
|
364
|
+
installed.set(name, {
|
|
365
|
+
installationId: name,
|
|
366
|
+
published: next.kind,
|
|
367
|
+
seedRevision: next.seedRevision,
|
|
368
|
+
sourceRevision: next.sourceRevision,
|
|
369
|
+
jobId: build.jobId,
|
|
370
|
+
installed: installation.status === 201 ? "installed" : "replaced",
|
|
371
|
+
release: build.release,
|
|
372
|
+
timings: {
|
|
373
|
+
fetch: next.fetch,
|
|
374
|
+
publish: next.publish,
|
|
375
|
+
queue: buildStarted - next.ready,
|
|
376
|
+
build: installStarted - buildStarted,
|
|
377
|
+
install: performance.now() - installStarted,
|
|
378
|
+
computer: phases === undefined || phases._tag === "None"
|
|
379
|
+
? null
|
|
380
|
+
: phases.value.phasesMs,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
catch (cause) {
|
|
385
|
+
stop.requested = true;
|
|
386
|
+
throw cause;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
await Promise.all(Array.from({ length: concurrency }, slot));
|
|
391
|
+
return standaloneAppSet.flatMap((app) => {
|
|
392
|
+
const result = installed.get(app.installationId);
|
|
393
|
+
return result === undefined ? [] : [result];
|
|
394
|
+
});
|
|
395
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { TenantScope } from "../tenant.js";
|
|
3
|
+
import type { OysterBindingResult } from "../workers.js";
|
|
4
|
+
import type { OysterEnv } from "../host.js";
|
|
5
|
+
import type { InvocationFrame } from "../invocation.js";
|
|
6
|
+
import type { StandaloneRegistry } from "./registry.js";
|
|
7
|
+
/** The standalone App source binding needs the Git-capable registry, by
|
|
8
|
+
* identity: `worker.ts` exports `StandaloneRegistry` as the `REGISTRY`
|
|
9
|
+
* binding's class, so this Instance's env sees that same object. */
|
|
10
|
+
export type AppSourceBindingEnv = Readonly<{
|
|
11
|
+
REGISTRY: DurableObjectNamespace<StandaloneRegistry>;
|
|
12
|
+
}>;
|
|
13
|
+
/** `catalogFrameMethods.APPS`'s descriptors already decode to
|
|
14
|
+
* `OysterBindingResult` at their owner (`catalog-frame.ts`); no re-typing
|
|
15
|
+
* is needed here. */
|
|
16
|
+
export declare const appSourceFrameMethods: {
|
|
17
|
+
APPS: {
|
|
18
|
+
catalog: {
|
|
19
|
+
decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{}];
|
|
20
|
+
decodeResult: (raw: unknown) => OysterBindingResult;
|
|
21
|
+
requiredOperations: () => string[];
|
|
22
|
+
};
|
|
23
|
+
fork: {
|
|
24
|
+
decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
|
|
25
|
+
readonly platformAppId: import("@fungi.computer/puffball").AppId;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
}];
|
|
28
|
+
decodeResult: (raw: unknown) => OysterBindingResult;
|
|
29
|
+
requiredOperations: () => string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Serve `APPS.catalog`/`APPS.fork`; `fork` is admitted only for the tenant owner,
|
|
34
|
+
* re-checked here (never trusted from a stale captured frame). */
|
|
35
|
+
export declare function dispatchAppSourceBinding(env: AppSourceBindingEnv, scope: TenantScope, frame: InvocationFrame, method: string, args: readonly unknown[]): Promise<OysterBindingResult>;
|
|
36
|
+
/** Operator-only: seed one App's editable source template (`install-apps.mjs`).
|
|
37
|
+
* Mounted under `host.ownerRoutes`, already owner-key-gated for every `/api/*`
|
|
38
|
+
* path before this handler runs (`host.ts`'s `oysterPrincipalIsOwner` check).
|
|
39
|
+
* The operator `git push`es the projected source to
|
|
40
|
+
* `/api/apps/:appId/source.git`, then claims that head here. */
|
|
41
|
+
export declare function appSourceOwnerRoutes(env: AppSourceBindingEnv, scope: TenantScope): Hono<OysterEnv>;
|