@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,355 @@
1
+ /**
2
+ * Oyster's App build owner: an App's registered source, its build jobs on the
3
+ * host's own Computer, and the Releases those builds produce.
4
+ *
5
+ * A build runs the shared job lifecycle (`build/build-jobs.ts`) and the fixed
6
+ * captured builder command on one Computer. The Computer holds no key: its
7
+ * clone is admitted at egress for exactly the job's one repository (read
8
+ * only), and its candidate upload for exactly the job. The candidate is
9
+ * written to the Release bucket by the shared R2 writer, verified like any
10
+ * installed Release, and recorded here; installing it is a separate owner act.
11
+ */
12
+ import * as Effect from "effect/Effect";
13
+ import * as Schema from "effect/Schema";
14
+ import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
15
+ import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
16
+ import { capturedPinnedRecipeSchema, commitPreparedCandidate, digestPinnedRecipe, } from "./build/effect.js";
17
+ import { encodeTeamAppCandidateSubmission, } from "./build/submission-contract.js";
18
+ import { createTeamBuildPublisher, teamAppBuildCommand, } from "./build/build-jobs.js";
19
+ import { createR2ArtifactWriter, } from "./build/r2-artifact-writer.js";
20
+ import { buildGitPassPrefix } from "./git-auth.js";
21
+ import { verifyRelease } from "./release.js";
22
+ /** A Computer's whole-build budget: a cold standard-2 build measured 185-270 s. */
23
+ export const oysterAppBuildTimeoutMs = 900_000;
24
+ /** The job, its clone admission and its upload grant end together. */
25
+ const jobTtlMs = oysterAppBuildTimeoutMs + 5 * 60_000;
26
+ /** Host names the Computer's egress serves for a build; never public hosts. */
27
+ export const oysterBuildGitHost = "git.team.internal";
28
+ export const oysterBuildReportHost = "build.team.internal";
29
+ /** An App's registered build source: one repository and its pinned recipe. */
30
+ export const oysterBuildSourceSchema = Schema.Struct({
31
+ appId: appIdSchema,
32
+ /** The installation whose Git holds the repository (Bonsai for a fork). */
33
+ installationId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,256}$/u)),
34
+ repository: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,128}$/u)),
35
+ recipe: capturedPinnedRecipeSchema,
36
+ buildRecipeDigest: sha256DigestSchema,
37
+ });
38
+ const jobRow = Schema.Struct({
39
+ job_id: Schema.String,
40
+ app_id: Schema.String,
41
+ release_id: Schema.String,
42
+ source_revision: Schema.String,
43
+ installation_id: Schema.String,
44
+ repository: Schema.String,
45
+ recipe: Schema.String,
46
+ recipe_digest: Schema.String,
47
+ requested_by: Schema.String,
48
+ credential_id: Schema.String,
49
+ upload_grant_digest: Schema.String,
50
+ status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
51
+ artifact_digest: Schema.NullOr(Schema.String),
52
+ artifact_bytes: Schema.NullOr(Schema.Number),
53
+ detail: Schema.NullOr(Schema.String),
54
+ issued_at: Schema.Number,
55
+ expires_at: Schema.Number,
56
+ executable_until: Schema.Number,
57
+ });
58
+ const decodeJob = Schema.decodeUnknownSync(jobRow);
59
+ const sourceRow = Schema.Struct({ source: Schema.String });
60
+ const releaseRow = Schema.Struct({ release: Schema.String });
61
+ const sourceCodec = Schema.fromJsonString(oysterBuildSourceSchema);
62
+ const releaseCodec = Schema.fromJsonString(releaseSchema);
63
+ const recipeCodec = Schema.fromJsonString(capturedPinnedRecipeSchema);
64
+ /** Unguessable hex identity from the platform CSPRNG. */
65
+ function randomHex(bytes) {
66
+ return Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
67
+ }
68
+ async function sha256Hex(bytes) {
69
+ return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
70
+ }
71
+ /** Constant-time comparison of two equal-length hex digests. */
72
+ function sameDigest(left, right) {
73
+ if (left.length !== right.length)
74
+ return false;
75
+ let difference = 0;
76
+ for (let index = 0; index < left.length; index += 1)
77
+ difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
78
+ return difference === 0;
79
+ }
80
+ /** The SQLite tables one Oyster build owner keeps. */
81
+ export function migrateOysterAppBuilds(owner) {
82
+ owner.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
83
+ owner.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (
84
+ job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
85
+ source_revision TEXT NOT NULL, installation_id TEXT NOT NULL, repository TEXT NOT NULL,
86
+ recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
87
+ credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
88
+ status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
89
+ issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL
90
+ ) STRICT`);
91
+ owner.exec("CREATE TABLE IF NOT EXISTS oyster_build_releases (release_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE, release TEXT NOT NULL) STRICT");
92
+ }
93
+ /** The build owner over one tenant's SQLite, Computer and Release bucket. */
94
+ export function createOysterAppBuilds(options) {
95
+ const { owner } = options;
96
+ const now = options.now ?? Date.now;
97
+ migrateOysterAppBuilds(owner.sql);
98
+ const readJob = (jobId) => {
99
+ const [row] = owner.sql
100
+ .exec("SELECT * FROM oyster_build_jobs WHERE job_id=?", jobId)
101
+ .toArray();
102
+ return row === undefined ? null : decodeJob(row);
103
+ };
104
+ const live = (job) => job !== null && job.status === "running" && job.expires_at > now();
105
+ const readRelease = (jobId) => {
106
+ const [row] = owner.sql
107
+ .exec("SELECT release FROM oyster_build_releases WHERE job_id=?", jobId)
108
+ .toArray();
109
+ return row === undefined
110
+ ? null
111
+ : Schema.decodeUnknownSync(releaseCodec)(Schema.decodeUnknownSync(releaseRow)(row).release);
112
+ };
113
+ const lifecycleJob = (job) => ({
114
+ jobId: job.job_id,
115
+ computerId: options.computer.name,
116
+ credentialId: job.credential_id,
117
+ issuedAt: job.issued_at,
118
+ repository: job.repository,
119
+ expiresAt: job.expires_at,
120
+ remote: `https://${oysterBuildGitHost}/git/${job.installation_id}/${job.repository}.git`,
121
+ reportUrl: `https://${oysterBuildReportHost}/jobs/${job.job_id}`,
122
+ status: job.status,
123
+ artifact: job.artifact_digest === null || job.artifact_bytes === null
124
+ ? null
125
+ : { digest: job.artifact_digest, bytes: job.artifact_bytes },
126
+ detail: job.detail,
127
+ });
128
+ const settle = (jobId, status, detail) => {
129
+ owner.sql.exec("UPDATE oyster_build_jobs SET status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'", status, detail, now(), jobId);
130
+ };
131
+ const required = (jobId) => {
132
+ const job = readJob(jobId);
133
+ if (job === null)
134
+ throw new Error("Unknown build job");
135
+ return job;
136
+ };
137
+ const publisher = createTeamBuildPublisher({
138
+ teamId: options.tenantId,
139
+ now,
140
+ maxPermissionAgeMs: jobTtlMs,
141
+ team: {
142
+ // The job row is minted in `start`; the lifecycle only reads it back.
143
+ mintTeamBuildCredential: ({ jobId }) => Promise.resolve(lifecycleJob(required(jobId))),
144
+ revokeTeamBuild: (jobId) => {
145
+ settle(jobId, "revoked", "Build revoked");
146
+ return Promise.resolve(lifecycleJob(required(jobId)));
147
+ },
148
+ build: () => Promise.resolve({
149
+ prepare: (jobId) => Promise.resolve(lifecycleJob(required(jobId))),
150
+ read: (jobId) => {
151
+ const job = readJob(jobId);
152
+ return Promise.resolve(job === null ? null : lifecycleJob(job));
153
+ },
154
+ complete: (jobId, status, detail) => {
155
+ settle(jobId, status, detail);
156
+ return Promise.resolve(lifecycleJob(required(jobId)));
157
+ },
158
+ // Oyster's egress writes job facts here directly; nothing is queued.
159
+ deliverProjection: () => Promise.resolve(),
160
+ [Symbol.dispose]: () => undefined,
161
+ }),
162
+ },
163
+ computer: () => ({
164
+ applyBuildDecision: (decision) => {
165
+ owner.sql.exec("UPDATE oyster_build_jobs SET executable_until=? WHERE job_id=?", decision.status === "allowed" ? decision.expiresAt : 0, decision.jobId);
166
+ return Promise.resolve();
167
+ },
168
+ execute: (input) => options.computer.port.runBuild({
169
+ jobId: input.jobId,
170
+ command: input.command,
171
+ timeoutMs: input.timeoutMs,
172
+ }),
173
+ deliverTeamGitEvents: () => Promise.resolve(0),
174
+ }),
175
+ });
176
+ const readSource = (appId) => {
177
+ const [row] = owner.sql
178
+ .exec("SELECT source FROM oyster_build_sources WHERE app_id=?", appId)
179
+ .toArray();
180
+ return row === undefined
181
+ ? null
182
+ : Schema.decodeUnknownSync(sourceCodec)(Schema.decodeUnknownSync(sourceRow)(row).source);
183
+ };
184
+ const run = async (jobId, command) => {
185
+ try {
186
+ await publisher.run({
187
+ jobId,
188
+ command,
189
+ timeoutMs: oysterAppBuildTimeoutMs,
190
+ });
191
+ }
192
+ catch (cause) {
193
+ settle(jobId, "failed", cause instanceof Error ? cause.message : "Build failed");
194
+ }
195
+ return view(required(jobId));
196
+ };
197
+ const view = (job) => ({
198
+ jobId: job.job_id,
199
+ appId: job.app_id,
200
+ releaseId: job.release_id,
201
+ sourceRevision: job.source_revision,
202
+ status: job.status,
203
+ detail: job.detail,
204
+ release: readRelease(job.job_id),
205
+ });
206
+ return Object.freeze({
207
+ /** Record (or replace) an App's build source; the caller is the owner. */
208
+ registerSource(source) {
209
+ if (digestPinnedRecipe(source.recipe) !== source.buildRecipeDigest)
210
+ throw new Error("Build recipe digest does not match its recipe");
211
+ owner.sql.exec("INSERT INTO oyster_build_sources(app_id, source) VALUES(?, ?) ON CONFLICT(app_id) DO UPDATE SET source=excluded.source", source.appId, Schema.encodeUnknownSync(sourceCodec)(source));
212
+ return source;
213
+ },
214
+ readSource,
215
+ /**
216
+ * Mint one build job for an App's registered source at one exact commit.
217
+ * Answers the job and its one run, which the caller keeps alive (the
218
+ * upload grant exists only inside that run's fixed command).
219
+ */
220
+ async start(input) {
221
+ const sourceRevision = Schema.decodeUnknownSync(sourceRevisionSchema)(input.sourceRevision);
222
+ const source = readSource(input.appId);
223
+ if (source === null)
224
+ throw new Error("App has no build source");
225
+ const jobId = `build_${randomHex(16)}`;
226
+ const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
227
+ const uploadGrant = randomHex(32);
228
+ const issuedAt = now();
229
+ owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, installation_id,
230
+ repository, recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
231
+ status, artifact_digest, artifact_bytes, detail, issued_at, expires_at, executable_until)
232
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, source.installationId, source.repository, Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
233
+ // The grant leaves this owner only inside the fixed command.
234
+ const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
235
+ return { build: view(required(jobId)), run: () => run(jobId, command) };
236
+ },
237
+ read(jobId) {
238
+ const job = readJob(jobId);
239
+ return job === null ? null : view(job);
240
+ },
241
+ /** A built Release by id, for an owner to install or select. */
242
+ release(releaseId) {
243
+ const [row] = owner.sql
244
+ .exec("SELECT release FROM oyster_build_releases WHERE release_id=?", releaseId)
245
+ .toArray();
246
+ return row === undefined
247
+ ? null
248
+ : Schema.decodeUnknownSync(releaseCodec)(Schema.decodeUnknownSync(releaseRow)(row).release);
249
+ },
250
+ /** May this Computer execute this job now? Re-checked by the Computer. */
251
+ admitExecution(jobId, computerObjectId) {
252
+ const job = readJob(jobId);
253
+ return (computerObjectId === options.computer.objectId &&
254
+ live(job) &&
255
+ job.executable_until > now());
256
+ },
257
+ /** The live job a build report from this Computer may speak for. */
258
+ admitBuild(jobId, computerObjectId) {
259
+ const job = readJob(jobId);
260
+ return computerObjectId === options.computer.objectId && live(job)
261
+ ? { credentialId: job.credential_id }
262
+ : null;
263
+ },
264
+ /**
265
+ * The clone credential the egress presents for this Computer's live job
266
+ * on exactly this repository, read only; null when no job admits it.
267
+ */
268
+ gitPassFor(computerObjectId, target) {
269
+ if (computerObjectId !== options.computer.objectId ||
270
+ target.scope !== "read")
271
+ return null;
272
+ const job = owner.sql
273
+ .exec("SELECT * FROM oyster_build_jobs WHERE installation_id=? AND repository=? AND status='running' AND expires_at>? ORDER BY issued_at DESC LIMIT 1", target.installationId, target.repository, now())
274
+ .toArray()
275
+ .map((row) => decodeJob(row))[0];
276
+ return job === undefined
277
+ ? null
278
+ : `${buildGitPassPrefix}${job.job_id}.${job.credential_id}`;
279
+ },
280
+ /** Admit a presented clone credential at the Git door: one repository, read. */
281
+ admitGitPass(pass, target) {
282
+ const match = /^obp\.(build_[0-9a-f]{32})\.([0-9a-f]{64})$/u.exec(pass);
283
+ if (match === null || target.scope !== "read")
284
+ return null;
285
+ const job = readJob(match[1] ?? "");
286
+ return live(job) &&
287
+ sameDigest(job.credential_id, match[2] ?? "") &&
288
+ job.installation_id === target.installationId &&
289
+ job.repository === target.repository
290
+ ? { requestedBy: job.requested_by }
291
+ : null;
292
+ },
293
+ /** A reported artifact or status for a live job. */
294
+ record(event) {
295
+ const job = readJob(event.jobId);
296
+ if (!live(job) || job.credential_id !== event.credentialId)
297
+ return;
298
+ if (event.kind === "build-artifact")
299
+ owner.sql.exec("UPDATE oyster_build_jobs SET artifact_digest=?, artifact_bytes=? WHERE job_id=?", event.digest, event.bytes, event.jobId);
300
+ // Success is the recorded Release's fact, never the report's alone.
301
+ else if (event.status === "failed" || readRelease(event.jobId) !== null)
302
+ settle(event.jobId, event.status, event.detail);
303
+ },
304
+ /**
305
+ * The Computer's candidate for a live job: checked against the job, written
306
+ * by the shared R2 writer, verified as an installable Release and recorded.
307
+ */
308
+ async submitCandidate(jobId, candidate, grant, credentialId) {
309
+ const job = readJob(jobId);
310
+ if (!live(job) ||
311
+ job.credential_id !== credentialId ||
312
+ !sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant))))
313
+ return new Response(null, { status: 403 });
314
+ if (candidate.appId !== job.app_id ||
315
+ candidate.provenance.sourceRevision !== job.source_revision ||
316
+ candidate.provenance.buildRecipeDigest !== job.recipe_digest)
317
+ return new Response(null, { status: 409 });
318
+ if (readRelease(jobId) !== null)
319
+ return new Response(null, { status: 409 });
320
+ const recipe = Schema.decodeUnknownSync(recipeCodec)(job.recipe);
321
+ const digest = await sha256Hex(new TextEncoder().encode(encodeTeamAppCandidateSubmission(candidate)));
322
+ const writerOptions = {
323
+ bucket: options.bucket,
324
+ artifactRef: parseArtifactRef(`artifact:sha256-${digest}`),
325
+ policy: {
326
+ maxBytes: recipe.maxOutputBytes,
327
+ maxManifestBytes: recipe.assets?.maxManifestBytes ?? 1024 * 1024,
328
+ },
329
+ };
330
+ if (recipe.assets !== undefined)
331
+ writerOptions.assets = recipe.assets;
332
+ let release;
333
+ try {
334
+ release = await Effect.runPromise(commitPreparedCandidate({ ...candidate, releaseId: parseReleaseId(job.release_id) }, {
335
+ artifactWriter: createR2ArtifactWriter(writerOptions),
336
+ releaseCatalog: {
337
+ createRelease: (value) => Promise.resolve(value),
338
+ },
339
+ }));
340
+ await verifyRelease(options.artifacts, release);
341
+ }
342
+ catch {
343
+ settle(jobId, "failed", "Candidate could not be stored as a Release");
344
+ return new Response(null, { status: 422 });
345
+ }
346
+ owner.transactionSync(() => {
347
+ owner.sql.exec("INSERT INTO oyster_build_releases(release_id, job_id, release) VALUES(?, ?, ?)", release.releaseId, jobId, Schema.encodeUnknownSync(releaseCodec)(release));
348
+ });
349
+ return Response.json({
350
+ kind: "submitted",
351
+ release: Schema.encodeUnknownSync(releaseSchema)(release),
352
+ });
353
+ },
354
+ });
355
+ }
@@ -0,0 +1,2 @@
1
+ /** The sandbox every installed App frame runs in, shell-mounted or not. */
2
+ export declare const appSandbox = "allow-scripts allow-modals allow-popups allow-forms";
@@ -0,0 +1,2 @@
1
+ /** The sandbox every installed App frame runs in, shell-mounted or not. */
2
+ export const appSandbox = "allow-scripts allow-modals allow-popups allow-forms";
@@ -0,0 +1,46 @@
1
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
+ import type { Artifacts } from "@fungi.computer/burrow";
3
+ import type { CloudflareArtifactsRepositoryMetadataStore } from "@fungi.computer/burrow/cloudflare";
4
+ import { type TeamAppPublisherRegistry } from "@fungi.computer/puffball/publisher";
5
+ import { type PublisherTemplateSeedReceipts } from "./source/index.js";
6
+ /**
7
+ * Oyster's own storage for App source templates, Team-owned fork
8
+ * registrations and the recorded Artifacts repository metadata their fork
9
+ * provenance depends on. Each factory is a pure adapter over a generic
10
+ * synchronous SQLite port, so it is unit-testable with a Node `DatabaseSync`
11
+ * and identically correct over a Durable Object's `ctx.storage.sql`.
12
+ */
13
+ export declare function ensureAppSourceTables(sql: WatchdogSqliteOwner["sql"]): void;
14
+ /** Append-only App source seed receipts, owned by one Oyster's registry. */
15
+ export declare function createOysterAppSourceReceipts(sql: WatchdogSqliteOwner["sql"]): PublisherTemplateSeedReceipts;
16
+ /** Team-owned App publisher registrations (fork destinations), one per App. */
17
+ export declare function createOysterAppRegistrations(sql: WatchdogSqliteOwner["sql"]): TeamAppPublisherRegistry;
18
+ /** Durable repository provenance for Oyster's own `createCloudflareArtifacts`. */
19
+ export declare function createOysterArtifactsRepositoryMetadataStore(sql: WatchdogSqliteOwner["sql"]): CloudflareArtifactsRepositoryMetadataStore;
20
+ /** Translate one local Git smart-HTTP request into an admitted Artifacts
21
+ * exchange; the identical shape Hub's Publisher keeps for its own template
22
+ * writes (`services/app-publisher/src/source-git.ts`), scoped to Oyster's own
23
+ * Artifacts composition since a package cannot depend on that service. */
24
+ export declare function exchangeOysterTemplateGit(artifacts: Pick<Artifacts, "exchangeGit">, remote: string, repository: string, request: Request, permission?: "read" | "write"): Promise<Response>;
25
+ /** Seed Oyster's own template source from the operator owner route, over the
26
+ * durable receipts and the Artifacts composition for this Oyster. */
27
+ export declare function publishOysterAppSource(receipts: PublisherTemplateSeedReceipts, artifacts: Pick<Artifacts, "ensureRepository" | "exchangeGit">, rawAppId: string, request: Request): Promise<{
28
+ readonly kind: "invalid" | "too-large";
29
+ readonly receipt?: undefined;
30
+ } | {
31
+ readonly kind: "conflict";
32
+ readonly receipt?: undefined;
33
+ } | {
34
+ readonly kind: "recorded" | "existing";
35
+ readonly receipt: {
36
+ readonly appId: import("@fungi.computer/puffball").AppId;
37
+ readonly repository: string;
38
+ readonly sourceRevision: import("@fungi.computer/puffball").SourceRevision;
39
+ readonly sourceTreeDigest: import("@fungi.computer/puffball").Sha256Digest;
40
+ readonly seedRevision: import("@fungi.computer/puffball").SourceRevision;
41
+ readonly buildRecipeDigest: import("@fungi.computer/puffball").Sha256Digest;
42
+ };
43
+ } | {
44
+ readonly kind: "unavailable";
45
+ readonly receipt?: undefined;
46
+ }>;