@fungi.computer/oyster 0.0.0-stage → 0.0.1

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.
Files changed (242) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1082 -2
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +322 -0
  5. package/dist/app-builds.d.ts +151 -0
  6. package/dist/app-builds.js +355 -0
  7. package/dist/app-sandbox.d.ts +2 -0
  8. package/dist/app-sandbox.js +2 -0
  9. package/dist/app-sources.d.ts +46 -0
  10. package/dist/app-sources.js +335 -0
  11. package/dist/app-storage.d.ts +141 -0
  12. package/dist/app-storage.js +281 -0
  13. package/dist/apps/gate.d.ts +105 -0
  14. package/dist/apps/gate.js +273 -0
  15. package/dist/apps/index.d.ts +5 -0
  16. package/dist/apps/index.js +5 -0
  17. package/dist/apps/installation.d.ts +248 -0
  18. package/dist/apps/installation.js +307 -0
  19. package/dist/apps/operations.d.ts +8 -0
  20. package/dist/apps/operations.js +98 -0
  21. package/dist/apps/tools.d.ts +33 -0
  22. package/dist/apps/tools.js +17 -0
  23. package/dist/auth.d.ts +11 -0
  24. package/dist/auth.js +93 -0
  25. package/dist/backend-request.d.ts +3 -0
  26. package/dist/backend-request.js +53 -0
  27. package/dist/build/build-jobs.d.ts +128 -0
  28. package/dist/build/build-jobs.js +197 -0
  29. package/dist/build/build-step.d.ts +26 -0
  30. package/dist/build/build-step.js +20 -0
  31. package/dist/build/bundled-html.d.ts +7 -0
  32. package/dist/build/bundled-html.js +64 -0
  33. package/dist/build/definition-emission.d.ts +34 -0
  34. package/dist/build/definition-emission.js +201 -0
  35. package/dist/build/effect.d.ts +231 -0
  36. package/dist/build/effect.js +396 -0
  37. package/dist/build/git-environment.d.ts +6 -0
  38. package/dist/build/git-environment.js +48 -0
  39. package/dist/build/html-recipe.d.ts +20 -0
  40. package/dist/build/html-recipe.js +51 -0
  41. package/dist/build/local-build.d.ts +57 -0
  42. package/dist/build/local-build.js +1078 -0
  43. package/dist/build/r2-artifact-writer.d.ts +31 -0
  44. package/dist/build/r2-artifact-writer.js +179 -0
  45. package/dist/build/snapshot-package-store.d.ts +2 -0
  46. package/dist/build/snapshot-package-store.js +21 -0
  47. package/dist/build/snapshot-program-scripts.d.ts +6 -0
  48. package/dist/build/snapshot-program-scripts.js +2 -0
  49. package/dist/build/submission-contract.d.ts +384 -0
  50. package/dist/build/submission-contract.js +70 -0
  51. package/dist/build/workspace-build.d.ts +50 -0
  52. package/dist/build/workspace-build.js +156 -0
  53. package/dist/build-contract.d.ts +4 -0
  54. package/dist/build-contract.js +4 -0
  55. package/dist/build-jobs.d.ts +2 -0
  56. package/dist/build-jobs.js +2 -0
  57. package/dist/build.d.ts +11 -0
  58. package/dist/build.js +13 -0
  59. package/dist/capability-descriptor.d.ts +46 -0
  60. package/dist/capability-descriptor.js +51 -0
  61. package/dist/catalog-frame.d.ts +39 -0
  62. package/dist/catalog-frame.js +72 -0
  63. package/dist/cause-class.d.ts +19 -0
  64. package/dist/cause-class.js +54 -0
  65. package/dist/cloudflare.d.ts +123 -0
  66. package/dist/cloudflare.js +391 -0
  67. package/dist/computer-terminal.d.ts +38 -0
  68. package/dist/computer-terminal.js +410 -0
  69. package/dist/computer.d.ts +60 -0
  70. package/dist/computer.js +33 -0
  71. package/dist/event-recipients.d.ts +57 -0
  72. package/dist/event-recipients.js +144 -0
  73. package/dist/facet-invocation.d.ts +4 -0
  74. package/dist/facet-invocation.js +45 -0
  75. package/dist/files.d.ts +42 -0
  76. package/dist/files.js +123 -0
  77. package/dist/frame-binding-wire-script.d.ts +2 -0
  78. package/dist/frame-binding-wire-script.js +1 -0
  79. package/dist/frame-binding-wire.d.ts +28 -0
  80. package/dist/frame-binding-wire.js +54 -0
  81. package/dist/git-admission.d.ts +52 -0
  82. package/dist/git-admission.js +188 -0
  83. package/dist/git-auth.d.ts +11 -0
  84. package/dist/git-auth.js +27 -0
  85. package/dist/git-door.d.ts +35 -0
  86. package/dist/git-door.js +108 -0
  87. package/dist/git-journal.d.ts +31 -0
  88. package/dist/git-journal.js +188 -0
  89. package/dist/git.d.ts +31 -0
  90. package/dist/git.js +64 -0
  91. package/dist/guest-policy.d.ts +9 -0
  92. package/dist/guest-policy.js +13 -0
  93. package/dist/host.d.ts +244 -0
  94. package/dist/host.js +1043 -0
  95. package/dist/index.d.ts +166 -0
  96. package/dist/index.js +320 -0
  97. package/dist/installation-consent.d.ts +79 -0
  98. package/dist/installation-consent.js +82 -0
  99. package/dist/internal/background-origins.d.ts +3 -0
  100. package/dist/internal/background-origins.js +8 -0
  101. package/dist/internal/delivery.d.ts +12 -0
  102. package/dist/internal/delivery.js +167 -0
  103. package/dist/internal/host-gate.d.ts +14 -0
  104. package/dist/internal/host-gate.js +62 -0
  105. package/dist/internal/registry-installations.d.ts +6 -0
  106. package/dist/internal/registry-installations.js +12 -0
  107. package/dist/invocation.d.ts +251 -0
  108. package/dist/invocation.js +377 -0
  109. package/dist/keys.d.ts +48 -0
  110. package/dist/keys.js +52 -0
  111. package/dist/mcp-owner.d.ts +27 -0
  112. package/dist/mcp-owner.js +143 -0
  113. package/dist/mcp.d.ts +89 -0
  114. package/dist/mcp.js +429 -0
  115. package/dist/media.d.ts +23 -0
  116. package/dist/media.js +12 -0
  117. package/dist/pack.d.ts +76 -0
  118. package/dist/pack.js +134 -0
  119. package/dist/page/capabilities.d.ts +23 -0
  120. package/dist/page/capabilities.js +256 -0
  121. package/dist/page/computer-handles.d.ts +79 -0
  122. package/dist/page/computer-handles.js +44 -0
  123. package/dist/page/lifetime.d.ts +6 -0
  124. package/dist/page/lifetime.js +19 -0
  125. package/dist/page/mount.d.ts +163 -0
  126. package/dist/page/mount.js +390 -0
  127. package/dist/page/storage.d.ts +3 -0
  128. package/dist/page/storage.js +82 -0
  129. package/dist/page/terminal.d.ts +5 -0
  130. package/dist/page/terminal.js +7 -0
  131. package/dist/page-script.d.ts +14 -0
  132. package/dist/page-script.js +5 -0
  133. package/dist/r2-files.d.ts +3 -0
  134. package/dist/r2-files.js +79 -0
  135. package/dist/release.d.ts +72 -0
  136. package/dist/release.js +164 -0
  137. package/dist/root-data.d.ts +86 -0
  138. package/dist/root-data.js +69 -0
  139. package/dist/shell/app-directory.d.ts +537 -0
  140. package/dist/shell/app-directory.js +195 -0
  141. package/dist/shell/browser-actions.d.ts +7 -0
  142. package/dist/shell/browser-actions.js +56 -0
  143. package/dist/shell/command-host.d.ts +112 -0
  144. package/dist/shell/command-host.js +502 -0
  145. package/dist/shell/commands.d.ts +25 -0
  146. package/dist/shell/commands.js +78 -0
  147. package/dist/shell/index.d.ts +8 -0
  148. package/dist/shell/index.js +8 -0
  149. package/dist/shell/keys.d.ts +41 -0
  150. package/dist/shell/keys.js +134 -0
  151. package/dist/shell/shell-module.d.ts +31 -0
  152. package/dist/shell/shell-module.js +37 -0
  153. package/dist/shell/window-manager.d.ts +124 -0
  154. package/dist/shell/window-manager.js +711 -0
  155. package/dist/shell/workbench-contract.d.ts +68 -0
  156. package/dist/shell/workbench-contract.js +1 -0
  157. package/dist/shell/workbench-host-types.d.ts +96 -0
  158. package/dist/shell/workbench-host-types.js +1 -0
  159. package/dist/shell/workbench-host.d.ts +34 -0
  160. package/dist/shell/workbench-host.js +347 -0
  161. package/dist/shell/workbench-model.d.ts +192 -0
  162. package/dist/shell/workbench-model.js +53 -0
  163. package/dist/shell/workbench.d.ts +4 -0
  164. package/dist/shell/workbench.js +2 -0
  165. package/dist/source/fork.d.ts +27 -0
  166. package/dist/source/fork.js +46 -0
  167. package/dist/source/index.d.ts +11 -0
  168. package/dist/source/index.js +11 -0
  169. package/dist/source/seed-receipt.d.ts +53 -0
  170. package/dist/source/seed-receipt.js +18 -0
  171. package/dist/source/team-source.d.ts +32 -0
  172. package/dist/source/team-source.js +42 -0
  173. package/dist/source/template-repository.d.ts +2 -0
  174. package/dist/source/template-repository.js +8 -0
  175. package/dist/source/template-write.d.ts +69 -0
  176. package/dist/source/template-write.js +344 -0
  177. package/dist/storage-frame.d.ts +64 -0
  178. package/dist/storage-frame.js +28 -0
  179. package/dist/tenant.d.ts +81 -0
  180. package/dist/tenant.js +71 -0
  181. package/dist/testing/capabilities.d.ts +29 -0
  182. package/dist/testing/capabilities.js +130 -0
  183. package/dist/testing/catalog.d.ts +3 -0
  184. package/dist/testing/catalog.js +53 -0
  185. package/dist/testing/index.d.ts +10 -0
  186. package/dist/testing/index.js +10 -0
  187. package/dist/testing/installation.d.ts +11 -0
  188. package/dist/testing/installation.js +25 -0
  189. package/dist/testing/native-worker.d.ts +21 -0
  190. package/dist/testing/native-worker.js +106 -0
  191. package/dist/testing/worker.d.ts +18 -0
  192. package/dist/testing/worker.js +67 -0
  193. package/dist/testing/workers-runtime.d.ts +24 -0
  194. package/dist/testing/workers-runtime.js +33 -0
  195. package/dist/webhook-delivery.d.ts +14 -0
  196. package/dist/webhook-delivery.js +17 -0
  197. package/dist/workers-git.d.ts +285 -0
  198. package/dist/workers-git.js +1174 -0
  199. package/dist/workers.d.ts +352 -0
  200. package/dist/workers.js +866 -0
  201. package/docs/api/README.md +82 -0
  202. package/docs/api/agents.md +332 -0
  203. package/docs/api/app-storage.md +209 -0
  204. package/docs/api/apps.md +1375 -0
  205. package/docs/api/auth.md +63 -0
  206. package/docs/api/build-contract.md +507 -0
  207. package/docs/api/build.md +1476 -0
  208. package/docs/api/cloudflare.md +421 -0
  209. package/docs/api/computer.md +203 -0
  210. package/docs/api/files.md +117 -0
  211. package/docs/api/git.md +470 -0
  212. package/docs/api/host.md +1328 -0
  213. package/docs/api/index.md +430 -0
  214. package/docs/api/invocation.md +738 -0
  215. package/docs/api/keys.md +175 -0
  216. package/docs/api/mcp.md +301 -0
  217. package/docs/api/media.md +40 -0
  218. package/docs/api/modules.md +42 -0
  219. package/docs/api/pack.md +138 -0
  220. package/docs/api/page/mount.md +298 -0
  221. package/docs/api/root-data.md +111 -0
  222. package/docs/api/shell/app-directory.md +332 -0
  223. package/docs/api/shell/command-host.md +224 -0
  224. package/docs/api/shell/commands.md +197 -0
  225. package/docs/api/shell/keys.md +183 -0
  226. package/docs/api/shell/shell-module.md +143 -0
  227. package/docs/api/shell/window-manager.md +267 -0
  228. package/docs/api/shell/workbench.md +186 -0
  229. package/docs/api/shell.md +1788 -0
  230. package/docs/api/tenant.md +194 -0
  231. package/docs/api/workers-git.md +1527 -0
  232. package/docs/api/workers.md +1678 -0
  233. package/docs/media/architecture.mmd +32 -0
  234. package/docs/media/architecture.png +0 -0
  235. package/docs/media/bonsai-network.png +0 -0
  236. package/docs/media/bonsai-repository.png +0 -0
  237. package/docs/site/agent-quickstart.mdx +349 -0
  238. package/examples/git/worker.ts +39 -0
  239. package/examples/git/wrangler.jsonc +32 -0
  240. package/examples/standalone/worker.ts +14 -0
  241. package/examples/standalone/wrangler.jsonc +21 -0
  242. package/package.json +249 -3
@@ -0,0 +1,20 @@
1
+ import * as Schema from "effect/Schema";
2
+ /**
3
+ * One captured dependency build step. Paths are relative to the captured
4
+ * snapshot root. A TypeScript step compiles one config; a Tailwind step
5
+ * compiles one stylesheet input into its output (the same path for an App's
6
+ * own stylesheet, which compiles in place).
7
+ */
8
+ export const workspaceBuildStepSchema = Schema.Union([
9
+ Schema.Struct({
10
+ kind: Schema.Literal("typescript"),
11
+ config: Schema.String,
12
+ }),
13
+ Schema.Struct({
14
+ kind: Schema.Literal("tailwind"),
15
+ input: Schema.String,
16
+ output: Schema.String,
17
+ }),
18
+ ]);
19
+ /** The steps one dependency-build process runs, encoded as one argument. */
20
+ export const workspaceBuildStepsJsonSchema = Schema.fromJsonString(Schema.Array(workspaceBuildStepSchema));
@@ -0,0 +1,7 @@
1
+ import type { PinnedRecipe, RawBuildOutput } from "./effect.js";
2
+ /** Read only regular files under the captured entry's directory. No scripts,
3
+ * dependency hydration, URL rewriting, or access to the producer's live build.
4
+ */
5
+ export declare function readBundledHtml(snapshot: string, recipe: PinnedRecipe): Promise<Extract<RawBuildOutput, {
6
+ kind: "assets";
7
+ }>>;
@@ -0,0 +1,64 @@
1
+ import { lstat, opendir, readFile } from "node:fs/promises";
2
+ import { dirname, extname, join } from "node:path";
3
+ import * as Schema from "effect/Schema";
4
+ import { assetPathSchema } from "@fungi.computer/puffball/assets";
5
+ import { boundedBundlePolicy, bundledHtmlRecipe } from "./html-recipe.js";
6
+ /** Read only regular files under the captured entry's directory. No scripts,
7
+ * dependency hydration, URL rewriting, or access to the producer's live build.
8
+ */
9
+ export async function readBundledHtml(snapshot, recipe) {
10
+ const policy = recipe.assets;
11
+ if (!policy ||
12
+ !recipe.assetBase ||
13
+ !recipe.entry.endsWith("/index.html") ||
14
+ !boundedBundlePolicy(policy, recipe.maxOutputBytes))
15
+ throw new Error("Invalid bundled HTML policy");
16
+ // Check every ancestor before descending: a symlink is not a bundle root.
17
+ const parts = dirname(recipe.entry).split("/");
18
+ let root = snapshot;
19
+ for (const part of parts) {
20
+ if (!part || part === "." || part === ".." || part.includes("\\"))
21
+ throw new Error("Invalid bundle directory");
22
+ root = join(root, part);
23
+ if (!(await lstat(root)).isDirectory())
24
+ throw new Error("Bundle directory must be a captured directory");
25
+ }
26
+ const assets = [];
27
+ let total = 0;
28
+ let entries = 0;
29
+ const visit = async (directory, prefix) => {
30
+ const children = await opendir(directory);
31
+ for await (const child of children) {
32
+ // Bound traversal too, including empty directories.
33
+ if (++entries > policy.maxObjects * 2)
34
+ throw new Error("Bundle directory count exceeds policy");
35
+ const path = Schema.decodeUnknownSync(assetPathSchema)(`${prefix}/${child.name}`);
36
+ const source = join(directory, child.name);
37
+ if (child.isDirectory()) {
38
+ await visit(source, path);
39
+ continue;
40
+ }
41
+ if (!child.isFile() ||
42
+ !bundledHtmlRecipe.extensions.includes(extname(child.name)))
43
+ throw new Error("Bundle contains an unsupported file type");
44
+ if (assets.length >= policy.maxObjects)
45
+ throw new Error("Bundle file count exceeds policy");
46
+ const stat = await lstat(source);
47
+ total += stat.size;
48
+ if (!stat.isFile() ||
49
+ stat.size > policy.maxObjectBytes ||
50
+ !Number.isSafeInteger(total) ||
51
+ total > policy.maxTotalBytes)
52
+ throw new Error("Bundle bytes exceed policy");
53
+ const bytes = await readFile(source);
54
+ if (bytes.byteLength !== stat.size)
55
+ throw new Error("Bundle changed during capture");
56
+ assets.push({ path, bytes });
57
+ }
58
+ };
59
+ await visit(root, "");
60
+ if (!assets.some((asset) => asset.path === "/index.html"))
61
+ throw new Error("Bundle is missing its HTML entry");
62
+ assets.sort((a, b) => a.path.localeCompare(b.path));
63
+ return { kind: "assets", base: recipe.assetBase, assets };
64
+ }
@@ -0,0 +1,34 @@
1
+ import type * as esbuild from "esbuild";
2
+ import type { AppDeclaration } from "@fungi.computer/puffball";
3
+ import type { CapturedSource, PinnedRecipe } from "./effect.js";
4
+ /** Package entry policy pinned only by the captured source recipes that need it. */
5
+ export declare function appBackendPackageMainFields(recipe: PinnedRecipe): string[] | undefined;
6
+ /** The checked Worker output of one App backend build. */
7
+ export type AppBackendOutput = Readonly<{
8
+ file: esbuild.OutputFile;
9
+ /** Source inputs contributing to the Worker bundle. */
10
+ inputs: readonly string[];
11
+ }>;
12
+ /**
13
+ * Build one App backend and derive its declaration from that same build.
14
+ *
15
+ * A declared `fungi.backendDefinition` adds a second, build-only entry that
16
+ * derives the declaration with `buildAppDeclaration(appBackend, fungi.app)`.
17
+ * esbuild loads every path and resolves every import once per build, so the
18
+ * Worker serves exactly the definition bytes and module resolution the
19
+ * declaration describes. The Worker output is then checked to include that
20
+ * definition and no build-only declaration code, and the declaration output to
21
+ * import nothing at runtime.
22
+ */
23
+ export declare function createDefinitionEmission(source: CapturedSource, recipe: PinnedRecipe): Promise<{
24
+ /** Entries for the one backend build: the Worker and, if defined, its declaration. */
25
+ entryPoints: (entry: string) => Record<string, string>;
26
+ outdir: string;
27
+ plugins: esbuild.Plugin[];
28
+ /** Check one finished backend build and return its Worker output. */
29
+ worker(result: esbuild.BuildResult): AppBackendOutput;
30
+ /** Evaluate the declaration derived in the checked backend build. */
31
+ declare(signal: AbortSignal): Promise<AppDeclaration>;
32
+ }>;
33
+ /** One App backend build and the declaration derived from it. */
34
+ export type DefinitionEmission = Awaited<ReturnType<typeof createDefinitionEmission>>;
@@ -0,0 +1,201 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, readFile, realpath, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { promisify } from "node:util";
6
+ import * as Schema from "effect/Schema";
7
+ import { appDeclarationSchema } from "@fungi.computer/puffball/app";
8
+ import { emptyAppDeclaration } from "@fungi.computer/puffball";
9
+ const packageSchema = Schema.fromJsonString(Schema.Struct({
10
+ fungi: Schema.optional(Schema.Struct({
11
+ app: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
12
+ backendDefinition: Schema.optional(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256))),
13
+ })),
14
+ }));
15
+ // The Computer VM is the customer-code sandbox. These rules enforce the SDK's
16
+ // runtime-neutral definition convention; they are not a Node security sandbox.
17
+ const evaluateDefinition = `
18
+ import { registerHooks } from 'node:module';
19
+ const entry = process.argv[1];
20
+ const write = process.stdout.write.bind(process.stdout);
21
+ const writeError = process.stderr.write.bind(process.stderr);
22
+ const exit = process.exit.bind(process);
23
+ registerHooks({ resolve(specifier, context, next) {
24
+ if (specifier !== entry) throw new Error('App backend definition imports ' + specifier + '. Move runtime-specific imports into the Worker composition; keep the definition runtime-neutral.');
25
+ return next(specifier, context);
26
+ }});
27
+ globalThis.fetch = () => { throw new Error('App backend definition attempted network access. Move network calls into method handlers; evaluation must be runtime-neutral.'); };
28
+ globalThis.WebSocket = undefined;
29
+ globalThis.process = undefined;
30
+ try {
31
+ const { appDeclaration } = await import(entry);
32
+ write(JSON.stringify(appDeclaration));
33
+ } catch (error) {
34
+ writeError((error instanceof Error ? error.message : "App definition evaluation failed").split("\\n")[0] + "\\n");
35
+ exit(1);
36
+ }
37
+ `;
38
+ const evaluationFailureSchema = Schema.Struct({ stderr: Schema.String });
39
+ const evaluationFailure = (cause) => {
40
+ if (Schema.is(evaluationFailureSchema)(cause)) {
41
+ const message = cause.stderr.trim().split("\n")[0];
42
+ if (message)
43
+ return new Error(message);
44
+ }
45
+ return new Error("App definition evaluation failed before emitting a declaration");
46
+ };
47
+ async function appPackage(source, recipe) {
48
+ let directory = dirname(join(source.snapshotPath, recipe.entry));
49
+ for (;;) {
50
+ const file = join(directory, "package.json");
51
+ const present = await access(file).then(() => true, () => false);
52
+ if (present)
53
+ return {
54
+ directory,
55
+ metadata: Schema.decodeUnknownSync(packageSchema)(await readFile(file, "utf8")),
56
+ };
57
+ if (directory === source.snapshotPath)
58
+ return { directory, metadata: {} };
59
+ const parent = dirname(directory);
60
+ const path = relative(source.snapshotPath, parent);
61
+ if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path))
62
+ throw new Error("App entry is outside its captured source");
63
+ directory = parent;
64
+ }
65
+ }
66
+ /** Package entry policy pinned only by the captured source recipes that need it. */
67
+ export function appBackendPackageMainFields(recipe) {
68
+ return recipe.toolchain.packageResolution ===
69
+ "captured-browser-module-main-v1"
70
+ ? ["browser", "module", "main"]
71
+ : undefined;
72
+ }
73
+ /** Output directory naming the Worker and declaration bundles of one build. */
74
+ const backendOutputDirectory = ".fungi-backend";
75
+ const declarationEntry = "fungi:app-declaration";
76
+ const declarationNamespace = "fungi-app-declaration";
77
+ const buildOnlySdk = "@fungi.computer/agar/backend-declaration";
78
+ const buildOnlySdkFile = /(?:^|\/)agar\/(?:src|dist)\/backend-declaration\.(?:js|ts)$/;
79
+ /** Metafile paths are relative to the build's working directory, with `/`. */
80
+ const metafilePath = (source, path) => relative(source.snapshotPath, path).split(sep).join("/");
81
+ /**
82
+ * Build one App backend and derive its declaration from that same build.
83
+ *
84
+ * A declared `fungi.backendDefinition` adds a second, build-only entry that
85
+ * derives the declaration with `buildAppDeclaration(appBackend, fungi.app)`.
86
+ * esbuild loads every path and resolves every import once per build, so the
87
+ * Worker serves exactly the definition bytes and module resolution the
88
+ * declaration describes. The Worker output is then checked to include that
89
+ * definition and no build-only declaration code, and the declaration output to
90
+ * import nothing at runtime.
91
+ */
92
+ export async function createDefinitionEmission(source, recipe) {
93
+ const { directory, metadata } = await appPackage(source, recipe);
94
+ const app = metadata.fungi?.app;
95
+ const packageDeclaration = app === undefined
96
+ ? emptyAppDeclaration
97
+ : Schema.decodeUnknownSync(appDeclarationSchema)({
98
+ handlers: [],
99
+ roles: [],
100
+ ...app,
101
+ });
102
+ const definition = metadata.fungi?.backendDefinition;
103
+ let definitionPath = null;
104
+ if (definition !== undefined) {
105
+ definitionPath = await realpath(join(directory, definition));
106
+ const path = relative(await realpath(source.snapshotPath), definitionPath);
107
+ if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path))
108
+ throw new Error("App backend definition must be inside its captured source");
109
+ if (recipe.backend === null)
110
+ throw new Error("An App backend definition requires a backend build recipe");
111
+ }
112
+ const workerPath = join(source.snapshotPath, backendOutputDirectory, "worker.js");
113
+ const declarationPath = join(source.snapshotPath, backendOutputDirectory, "declaration.js");
114
+ let declarationCode;
115
+ const declarationPlugin = {
116
+ name: "derived-app-declaration",
117
+ setup(build) {
118
+ // Only the build's own declaration entry may load the derivation.
119
+ build.onResolve({ filter: /^fungi:app-declaration$/ }, (args) => args.kind === "entry-point"
120
+ ? { path: "app-declaration.js", namespace: declarationNamespace }
121
+ : undefined);
122
+ build.onLoad({ filter: /.*/, namespace: declarationNamespace }, () => ({
123
+ contents: `import { appBackend } from ${JSON.stringify(definitionPath)}; import { buildAppDeclaration } from ${JSON.stringify(buildOnlySdk)}; export const appDeclaration = buildAppDeclaration(appBackend, ${JSON.stringify(packageDeclaration)});`,
124
+ resolveDir: directory,
125
+ loader: "js",
126
+ }));
127
+ },
128
+ };
129
+ return {
130
+ /** Entries for the one backend build: the Worker and, if defined, its declaration. */
131
+ entryPoints: (entry) => definitionPath === null
132
+ ? { worker: entry }
133
+ : { worker: entry, declaration: declarationEntry },
134
+ outdir: backendOutputDirectory,
135
+ plugins: definitionPath === null ? [] : [declarationPlugin],
136
+ /** Check one finished backend build and return its Worker output. */
137
+ worker(result) {
138
+ const metafile = result.metafile;
139
+ if (metafile === undefined)
140
+ throw new Error("App backend build did not return its metafile");
141
+ const outputs = result.outputFiles ?? [];
142
+ const expected = definitionPath === null ? [workerPath] : [workerPath, declarationPath];
143
+ const file = outputs.find((output) => output.path === workerPath);
144
+ if (file === undefined ||
145
+ outputs.length !== expected.length ||
146
+ !outputs.every((output) => expected.includes(output.path)))
147
+ throw new Error("App backend build must emit exactly its Worker and declaration");
148
+ const workerMeta = metafile.outputs[metafilePath(source, workerPath)];
149
+ if (workerMeta === undefined)
150
+ throw new Error("App backend build did not describe its Worker");
151
+ const inputs = Object.keys(workerMeta.inputs);
152
+ if (definitionPath === null)
153
+ return { file, inputs };
154
+ if (!inputs.includes(metafilePath(source, definitionPath)))
155
+ throw new Error("The Worker must import its declared fungi.backendDefinition; the declaration is derived from the definition it serves");
156
+ if (inputs.some((input) => input.startsWith(`${declarationNamespace}:`) ||
157
+ buildOnlySdkFile.test(input) ||
158
+ metafile.inputs[input]?.imports.some((imported) => imported.original === buildOnlySdk) === true))
159
+ throw new Error("The Worker must not import build-only App declaration code");
160
+ const declaration = outputs.find((output) => output.path === declarationPath);
161
+ const declarationMeta = metafile.outputs[metafilePath(source, declarationPath)];
162
+ if (declaration === undefined || declarationMeta === undefined)
163
+ throw new Error("App backend build did not derive its declaration");
164
+ const external = declarationMeta.imports.find((imported) => imported.external === true);
165
+ if (external !== undefined)
166
+ throw new Error(`App backend definition imports ${external.path}. Move runtime-specific imports into the Worker composition; keep the definition runtime-neutral.`);
167
+ if (declaration.contents.byteLength > recipe.maxOutputBytes)
168
+ throw new Error("App backend definition exceeds its pinned build bound");
169
+ declarationCode = declaration.text;
170
+ return { file, inputs };
171
+ },
172
+ /** Evaluate the declaration derived in the checked backend build. */
173
+ async declare(signal) {
174
+ if (definitionPath === null) {
175
+ if (packageDeclaration.api &&
176
+ Object.keys(packageDeclaration.api.methods).length > 0)
177
+ throw new Error("App API methods require fungi.backendDefinition; the build derives them from its methods instead of a handwritten table");
178
+ return packageDeclaration;
179
+ }
180
+ if (declarationCode === undefined)
181
+ throw new Error("The App declaration is derived from a checked backend build");
182
+ const file = join(source.snapshotPath, ".fungi-built-definition.mjs");
183
+ await writeFile(file, declarationCode, { flag: "wx", mode: 0o600 });
184
+ const result = await promisify(execFile)(process.execPath, [
185
+ "--input-type=module",
186
+ "--eval",
187
+ evaluateDefinition,
188
+ pathToFileURL(file).href,
189
+ ], {
190
+ cwd: source.snapshotPath,
191
+ env: {},
192
+ maxBuffer: 1024 * 1024,
193
+ timeout: 30_000,
194
+ signal,
195
+ }).catch((cause) => {
196
+ throw evaluationFailure(cause);
197
+ });
198
+ return Schema.decodeUnknownSync(Schema.fromJsonString(appDeclarationSchema), { onExcessProperty: "error" })(result.stdout);
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,231 @@
1
+ import { type AppId, type ArtifactRef, type AppReleaseCatalog, type Release, type ReleaseId, type Sha256Digest, type SourceRevision } from "@fungi.computer/puffball";
2
+ import type { AppDeclaration } from "@fungi.computer/puffball";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Schema from "effect/Schema";
5
+ import type * as Scope from "effect/Scope";
6
+ /** Canonical recipe policy shared by core and the local adapter. */
7
+ export declare const pinnedRecipeSchema: Schema.Struct<{
8
+ readonly kind: Schema.optional<Schema.Literals<readonly ["bundled-html", "source-html"]>>;
9
+ readonly entry: Schema.String;
10
+ readonly assets: Schema.optional<Schema.Struct<{
11
+ readonly maxManifestBytes: Schema.Number;
12
+ readonly maxObjects: Schema.Number;
13
+ readonly maxObjectBytes: Schema.Number;
14
+ readonly maxTotalBytes: Schema.Number;
15
+ }>>;
16
+ readonly assetBase: Schema.optionalKey<Schema.String>;
17
+ readonly backend: Schema.Union<readonly [Schema.Null, Schema.Struct<{
18
+ readonly entry: Schema.optional<Schema.String>;
19
+ readonly runtime: Schema.Struct<{
20
+ readonly kind: Schema.Literal<"durable-facet">;
21
+ readonly stateSchema: Schema.Number;
22
+ }>;
23
+ readonly compatibilityDate: Schema.String;
24
+ readonly exportName: Schema.String;
25
+ }>]>;
26
+ readonly maxOutputBytes: Schema.Number;
27
+ }>;
28
+ /** Core configuration contains only immutable App identity and recipe policy. */
29
+ export declare const AppPublisherCoreConfigSchema: Schema.Struct<{
30
+ readonly appId: Schema.refine<AppId, Schema.String>;
31
+ readonly recipe: Schema.Struct<{
32
+ readonly kind: Schema.optional<Schema.Literals<readonly ["bundled-html", "source-html"]>>;
33
+ readonly entry: Schema.String;
34
+ readonly assets: Schema.optional<Schema.Struct<{
35
+ readonly maxManifestBytes: Schema.Number;
36
+ readonly maxObjects: Schema.Number;
37
+ readonly maxObjectBytes: Schema.Number;
38
+ readonly maxTotalBytes: Schema.Number;
39
+ }>>;
40
+ readonly assetBase: Schema.optionalKey<Schema.String>;
41
+ readonly backend: Schema.Union<readonly [Schema.Null, Schema.Struct<{
42
+ readonly entry: Schema.optional<Schema.String>;
43
+ readonly runtime: Schema.Struct<{
44
+ readonly kind: Schema.Literal<"durable-facet">;
45
+ readonly stateSchema: Schema.Number;
46
+ }>;
47
+ readonly compatibilityDate: Schema.String;
48
+ readonly exportName: Schema.String;
49
+ }>]>;
50
+ readonly maxOutputBytes: Schema.Number;
51
+ }>;
52
+ }>;
53
+ /** App identity and caller-selected recipe inputs before normalization. */
54
+ export type AppPublisherCoreConfigInput = Schema.Codec.Encoded<typeof AppPublisherCoreConfigSchema>;
55
+ /** Package-export conditions used by the pinned Workers builder. */
56
+ export declare const appBackendExportConditions: readonly string[];
57
+ /** Persisted, fully resolved recipe including its code-owned toolchain. */
58
+ export declare const capturedPinnedRecipeSchema: Schema.Struct<{
59
+ readonly kind: Schema.optional<Schema.Literals<readonly ["bundled-html", "source-html"]>>;
60
+ readonly bundle: Schema.Literal<true>;
61
+ readonly entry: Schema.String;
62
+ readonly format: Schema.Literals<readonly ["html", "esm", "assets"]>;
63
+ readonly assets: Schema.optional<Schema.Struct<{
64
+ readonly maxManifestBytes: Schema.Number;
65
+ readonly maxObjects: Schema.Number;
66
+ readonly maxObjectBytes: Schema.Number;
67
+ readonly maxTotalBytes: Schema.Number;
68
+ }>>;
69
+ readonly assetBase: Schema.optionalKey<Schema.String>;
70
+ readonly toolchain: Schema.$Record<Schema.String, Schema.String>;
71
+ readonly maxOutputBytes: Schema.Number;
72
+ readonly platform: Schema.Literals<readonly ["browser", "neutral"]>;
73
+ readonly backend: Schema.Union<readonly [Schema.Null, Schema.Struct<{
74
+ readonly entry: Schema.optional<Schema.String>;
75
+ readonly runtime: Schema.Struct<{
76
+ readonly kind: Schema.Literal<"durable-facet">;
77
+ readonly stateSchema: Schema.Number;
78
+ }>;
79
+ readonly compatibilityDate: Schema.String;
80
+ readonly exportName: Schema.String;
81
+ }>]>;
82
+ readonly target: Schema.Tuple<readonly [Schema.Literal<"es2022">]>;
83
+ }>;
84
+ /** Fully resolved recipe with its pinned toolchain. */
85
+ export type PinnedRecipe = Schema.Schema.Type<typeof capturedPinnedRecipeSchema>;
86
+ /** A source tree captured into a scoped local snapshot directory. */
87
+ export type CapturedSource = Readonly<{
88
+ readonly sourceRevision: SourceRevision;
89
+ readonly sourceTreeDigest: Sha256Digest;
90
+ readonly snapshotPath: string;
91
+ }>;
92
+ /** Compiler bytes or a bounded collection of emitted UI assets. */
93
+ export type RawBuildOutput = Readonly<{
94
+ readonly kind: "bytes";
95
+ readonly bytes: Uint8Array;
96
+ }> | Readonly<{
97
+ readonly kind: "assets";
98
+ readonly base: string;
99
+ readonly assets: readonly {
100
+ readonly path: string;
101
+ readonly bytes: Uint8Array;
102
+ }[];
103
+ }>;
104
+ /** A complete HTML document or an asset collection. */
105
+ export type UiBuildOutput = Readonly<{
106
+ readonly kind: "html";
107
+ readonly bytes: Uint8Array;
108
+ }> | Extract<RawBuildOutput, {
109
+ readonly kind: "assets";
110
+ }>;
111
+ /** Raw executor result before the recipe owner assigns App surface meaning. */
112
+ export type BuildExecutorOutput = RawBuildOutput | Readonly<{
113
+ readonly kind: "app";
114
+ readonly ui: PreparedUiOutput | null;
115
+ readonly backend: Uint8Array | null;
116
+ }>;
117
+ /** UI output paired with its immutable artifact media type. */
118
+ export type PreparedUiOutput = Readonly<{
119
+ readonly content: Extract<UiBuildOutput, {
120
+ readonly kind: "html";
121
+ }>;
122
+ readonly mediaType: "text/html; charset=utf-8";
123
+ }> | Readonly<{
124
+ readonly content: Extract<UiBuildOutput, {
125
+ readonly kind: "assets";
126
+ }>;
127
+ readonly mediaType: "application/vnd.fungi.app-assets+json";
128
+ }>;
129
+ /** Complete, non-empty App content accepted by immutable artifact writers. */
130
+ export type BuildOutput = (Readonly<{
131
+ readonly kind: "ui";
132
+ readonly ui: PreparedUiOutput;
133
+ }> | Readonly<{
134
+ readonly kind: "backend";
135
+ readonly backend: Uint8Array;
136
+ }> | Readonly<{
137
+ readonly kind: "app";
138
+ readonly ui: PreparedUiOutput;
139
+ readonly backend: Uint8Array;
140
+ }>) & Readonly<{
141
+ /** The source's declared request; absent means the App asks for nothing. */
142
+ readonly declaration?: AppDeclaration;
143
+ }>;
144
+ /** Reference and digest of the persisted App build root. */
145
+ export type StoredArtifact = Readonly<{
146
+ readonly artifactRef: ArtifactRef;
147
+ readonly artifactDigest: Sha256Digest;
148
+ }>;
149
+ /** A locally captured and built candidate awaiting native artifact/catalog custody. */
150
+ export type PreparedCandidate = Readonly<{
151
+ readonly appId: AppId;
152
+ readonly releaseId: ReleaseId;
153
+ readonly output: BuildOutput;
154
+ readonly provenance: Readonly<{
155
+ readonly sourceRevision: SourceRevision;
156
+ readonly sourceTreeDigest: Sha256Digest;
157
+ readonly buildRecipeDigest: Sha256Digest;
158
+ }>;
159
+ }>;
160
+ /** Four structural roles used by the private composition. */
161
+ export type SourceCapture = {
162
+ /** Acquire a snapshot whose cleanup follows the Effect scope. */
163
+ readonly capture: () => Effect.Effect<CapturedSource, unknown, Scope.Scope>;
164
+ };
165
+ /** Compiles captured source and reads its matching declaration. */
166
+ export type BuildExecutor = {
167
+ /** Compile only the captured source under its pinned recipe. */
168
+ readonly build: (source: CapturedSource, recipe: PinnedRecipe) => Effect.Effect<BuildExecutorOutput, unknown, Scope.Scope>;
169
+ /**
170
+ * Read the App's declaration from the same captured tree. Every App, a fork
171
+ * included, declares through its own source; there is no other source.
172
+ */
173
+ readonly declaration?: (source: CapturedSource, recipe: PinnedRecipe) => Effect.Effect<AppDeclaration, unknown>;
174
+ };
175
+ /** Persists one complete App build as immutable artifacts. */
176
+ export type ArtifactWriter = {
177
+ /** Persist the App root and return its reference and digest. */
178
+ readonly write: (output: BuildOutput) => Effect.Effect<StoredArtifact, unknown>;
179
+ };
180
+ /** Release catalog role needed to record a built candidate. */
181
+ export type ReleaseCreator = Pick<AppReleaseCatalog, "createRelease">;
182
+ /** Captured build inputs and the resource owners used to publish a candidate. */
183
+ export type AppPublisherOptions = Readonly<{
184
+ readonly config: AppPublisherCoreConfigInput;
185
+ readonly releaseCatalog: ReleaseCreator;
186
+ readonly sourceCapture: SourceCapture;
187
+ readonly buildExecutor: BuildExecutor;
188
+ readonly artifactWriter: ArtifactWriter;
189
+ }>;
190
+ /** Builds and records immutable App Release candidates. */
191
+ export type AppPublisher = Readonly<{
192
+ /** Capture, build, store, attest, and create exactly one candidate. */
193
+ readonly createCandidate: (releaseId: ReleaseId) => Effect.Effect<Release, AppPublisherError>;
194
+ }>;
195
+ /** Builds a candidate before artifact storage and catalog publication. */
196
+ export type AppCandidatePreparer = Readonly<{
197
+ /** Capture and build once; persistence remains with the resource-native host. */
198
+ readonly prepareCandidate: (releaseId: ReleaseId) => Effect.Effect<PreparedCandidate, AppPublisherError>;
199
+ }>;
200
+ /** The bounded stages reported by AppPublisherError. */
201
+ export type AppPublisherStage = "configuration" | "source" | "build" | "artifact" | "release";
202
+ /** Closed failure boundary for the four private composition stages. */
203
+ export declare class AppPublisherError extends Error {
204
+ readonly stage: AppPublisherStage;
205
+ readonly _tag: "AppPublisherError";
206
+ constructor(stage: AppPublisherStage, options?: ErrorOptions);
207
+ }
208
+ /** App identity and normalized recipe with its digest. */
209
+ export type CapturedConfig = Readonly<{
210
+ readonly appId: AppId;
211
+ readonly recipe: PinnedRecipe;
212
+ readonly buildRecipeDigest: Sha256Digest;
213
+ }>;
214
+ /** Hash every captured build and output-policy input in the Publisher recipe. */
215
+ export declare function digestPinnedRecipe(recipe: PinnedRecipe): Sha256Digest;
216
+ /** Decode and normalize Publisher-owned recipe policy for durable seed proof. */
217
+ export declare function normalizeAppPublisherRecipe(input: {
218
+ appId: string;
219
+ recipe: AppPublisherCoreConfigInput["recipe"];
220
+ }): Readonly<{
221
+ recipe: PinnedRecipe;
222
+ buildRecipeDigest: Sha256Digest;
223
+ }>;
224
+ /** Construct one private App Publisher with copied core configuration and roles. */
225
+ export declare function createAppCandidatePreparer(options: Pick<AppPublisherOptions, "config" | "sourceCapture" | "buildExecutor">): AppCandidatePreparer;
226
+ /** Prepare a candidate from a previously captured recipe. */
227
+ export declare function createCapturedCandidatePreparer(config: CapturedConfig, options: Pick<AppPublisherOptions, "sourceCapture" | "buildExecutor">): AppCandidatePreparer;
228
+ /** Persist a prepared build through the native artifact and App Release owners. */
229
+ export declare function commitPreparedCandidate(prepared: PreparedCandidate, options: Pick<AppPublisherOptions, "artifactWriter" | "releaseCatalog">): Effect.Effect<Release, AppPublisherError>;
230
+ /** Compose capture, compilation, immutable writing and Release recording. */
231
+ export declare function createAppPublisher(options: AppPublisherOptions): AppPublisher;