@fungi.computer/oyster 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1134 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +210 -0
- package/dist/app-builds.js +499 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +31 -0
- package/dist/app-sources.js +370 -0
- package/dist/app-storage.d.ts +141 -0
- package/dist/app-storage.js +281 -0
- package/dist/apps/gate.d.ts +105 -0
- package/dist/apps/gate.js +273 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/installation.d.ts +248 -0
- package/dist/apps/installation.js +307 -0
- package/dist/apps/operations.d.ts +8 -0
- package/dist/apps/operations.js +98 -0
- package/dist/apps/tools.d.ts +33 -0
- package/dist/apps/tools.js +17 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +93 -0
- package/dist/backend-request.d.ts +3 -0
- package/dist/backend-request.js +53 -0
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +203 -0
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +37 -0
- package/dist/build/build-step.js +28 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28170 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +249 -0
- package/dist/build/effect.js +436 -0
- package/dist/build/git-environment.d.ts +9 -0
- package/dist/build/git-environment.js +60 -0
- package/dist/build/html-recipe.d.ts +20 -0
- package/dist/build/html-recipe.js +51 -0
- package/dist/build/local-build.d.ts +67 -0
- package/dist/build/local-build.js +1128 -0
- package/dist/build/snapshot-package-store.d.ts +12 -0
- package/dist/build/snapshot-package-store.js +36 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +828 -0
- package/dist/build/submission-contract.js +180 -0
- package/dist/build/workspace-build.d.ts +55 -0
- package/dist/build/workspace-build.js +200 -0
- package/dist/build-contract.d.ts +7 -0
- package/dist/build-contract.js +7 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +12 -0
- package/dist/build.js +14 -0
- package/dist/capability-descriptor.d.ts +46 -0
- package/dist/capability-descriptor.js +51 -0
- package/dist/catalog-frame.d.ts +39 -0
- package/dist/catalog-frame.js +72 -0
- package/dist/cause-class.d.ts +19 -0
- package/dist/cause-class.js +54 -0
- package/dist/cloudflare.d.ts +123 -0
- package/dist/cloudflare.js +391 -0
- package/dist/computer-terminal.d.ts +38 -0
- package/dist/computer-terminal.js +410 -0
- package/dist/computer.d.ts +60 -0
- package/dist/computer.js +33 -0
- package/dist/event-recipients.d.ts +57 -0
- package/dist/event-recipients.js +144 -0
- package/dist/facet-invocation.d.ts +4 -0
- package/dist/facet-invocation.js +45 -0
- package/dist/files.d.ts +42 -0
- package/dist/files.js +123 -0
- package/dist/frame-binding-wire-script.d.ts +2 -0
- package/dist/frame-binding-wire-script.js +1 -0
- package/dist/frame-binding-wire.d.ts +28 -0
- package/dist/frame-binding-wire.js +54 -0
- package/dist/git-admission.d.ts +52 -0
- package/dist/git-admission.js +188 -0
- package/dist/git-auth.d.ts +11 -0
- package/dist/git-auth.js +27 -0
- package/dist/git-door.d.ts +35 -0
- package/dist/git-door.js +108 -0
- package/dist/git-journal.d.ts +31 -0
- package/dist/git-journal.js +188 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +64 -0
- package/dist/guest-policy.d.ts +9 -0
- package/dist/guest-policy.js +13 -0
- package/dist/host.d.ts +244 -0
- package/dist/host.js +1043 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +320 -0
- package/dist/installation-consent.d.ts +79 -0
- package/dist/installation-consent.js +82 -0
- package/dist/internal/background-origins.d.ts +3 -0
- package/dist/internal/background-origins.js +8 -0
- package/dist/internal/delivery.d.ts +12 -0
- package/dist/internal/delivery.js +167 -0
- package/dist/internal/host-gate.d.ts +14 -0
- package/dist/internal/host-gate.js +62 -0
- package/dist/internal/registry-installations.d.ts +6 -0
- package/dist/internal/registry-installations.js +12 -0
- package/dist/invocation.d.ts +251 -0
- package/dist/invocation.js +377 -0
- package/dist/keys.d.ts +48 -0
- package/dist/keys.js +52 -0
- package/dist/mcp-owner.d.ts +27 -0
- package/dist/mcp-owner.js +155 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +429 -0
- package/dist/media.d.ts +23 -0
- package/dist/media.js +12 -0
- package/dist/pack.d.ts +79 -0
- package/dist/pack.js +151 -0
- package/dist/page/capabilities.d.ts +23 -0
- package/dist/page/capabilities.js +256 -0
- package/dist/page/computer-handles.d.ts +79 -0
- package/dist/page/computer-handles.js +44 -0
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/lifetime.d.ts +6 -0
- package/dist/page/lifetime.js +19 -0
- package/dist/page/mount.d.ts +163 -0
- package/dist/page/mount.js +390 -0
- package/dist/page/storage.d.ts +3 -0
- package/dist/page/storage.js +82 -0
- package/dist/page/terminal.d.ts +5 -0
- package/dist/page/terminal.js +7 -0
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.d.ts +14 -0
- package/dist/page-script.js +5 -0
- package/dist/r2-files.d.ts +3 -0
- package/dist/r2-files.js +79 -0
- package/dist/release.d.ts +83 -0
- package/dist/release.js +167 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +542 -0
- package/dist/shell/app-directory.js +199 -0
- package/dist/shell/browser-actions.d.ts +7 -0
- package/dist/shell/browser-actions.js +56 -0
- package/dist/shell/command-host.d.ts +112 -0
- package/dist/shell/command-host.js +502 -0
- package/dist/shell/commands.d.ts +25 -0
- package/dist/shell/commands.js +78 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/keys.d.ts +41 -0
- package/dist/shell/keys.js +134 -0
- package/dist/shell/shell-module.d.ts +55 -0
- package/dist/shell/shell-module.js +86 -0
- package/dist/shell/window-manager.d.ts +124 -0
- package/dist/shell/window-manager.js +711 -0
- package/dist/shell/workbench-contract.d.ts +68 -0
- package/dist/shell/workbench-contract.js +1 -0
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +96 -0
- package/dist/shell/workbench-host-types.js +1 -0
- package/dist/shell/workbench-host.d.ts +34 -0
- package/dist/shell/workbench-host.js +347 -0
- package/dist/shell/workbench-model.d.ts +192 -0
- package/dist/shell/workbench-model.js +53 -0
- package/dist/shell/workbench.d.ts +4 -0
- package/dist/shell/workbench.js +2 -0
- package/dist/source/fork.d.ts +27 -0
- package/dist/source/fork.js +46 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.js +10 -0
- package/dist/source/seed-receipt.d.ts +53 -0
- package/dist/source/seed-receipt.js +18 -0
- package/dist/source/team-source.d.ts +32 -0
- package/dist/source/team-source.js +42 -0
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +4 -0
- package/dist/source/template-repository.js +21 -0
- package/dist/source/template-seed.d.ts +68 -0
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +220 -0
- package/dist/standalone/agents.js +795 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +931 -0
- package/dist/standalone/workflow-host.js +751 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1694 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +657 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/tenant.d.ts +81 -0
- package/dist/tenant.js +71 -0
- package/dist/testing/capabilities.d.ts +29 -0
- package/dist/testing/capabilities.js +130 -0
- package/dist/testing/catalog.d.ts +3 -0
- package/dist/testing/catalog.js +53 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/installation.d.ts +11 -0
- package/dist/testing/installation.js +25 -0
- package/dist/testing/native-worker.d.ts +21 -0
- package/dist/testing/native-worker.js +106 -0
- package/dist/testing/worker.d.ts +18 -0
- package/dist/testing/worker.js +67 -0
- package/dist/testing/workers-runtime.d.ts +24 -0
- package/dist/testing/workers-runtime.js +33 -0
- package/dist/webhook-delivery.d.ts +14 -0
- package/dist/webhook-delivery.js +17 -0
- package/dist/workers-git.d.ts +285 -0
- package/dist/workers-git.js +1174 -0
- package/dist/workers.d.ts +352 -0
- package/dist/workers.js +868 -0
- package/docs/api/README.md +82 -0
- package/docs/api/agents.md +332 -0
- package/docs/api/app-storage.md +209 -0
- package/docs/api/apps.md +1375 -0
- package/docs/api/auth.md +63 -0
- package/docs/api/build-contract.md +1304 -0
- package/docs/api/build.md +1712 -0
- package/docs/api/cloudflare.md +421 -0
- package/docs/api/computer.md +203 -0
- package/docs/api/files.md +117 -0
- package/docs/api/git.md +470 -0
- package/docs/api/host.md +1328 -0
- package/docs/api/index.md +430 -0
- package/docs/api/invocation.md +738 -0
- package/docs/api/keys.md +175 -0
- package/docs/api/mcp.md +301 -0
- package/docs/api/media.md +40 -0
- package/docs/api/modules.md +50 -0
- package/docs/api/pack.md +141 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +599 -0
- package/docs/api/shell/command-host.md +224 -0
- package/docs/api/shell/commands.md +197 -0
- package/docs/api/shell/keys.md +183 -0
- package/docs/api/shell/shell-module.md +204 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +205 -0
- package/docs/api/shell.md +1915 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/tenant.md +194 -0
- package/docs/api/workers-git.md +1646 -0
- package/docs/api/workers.md +1691 -0
- package/docs/site/agent-quickstart.mdx +346 -0
- package/examples/standalone/worker.ts +7 -0
- package/examples/standalone/wrangler.jsonc +82 -0
- package/package.json +300 -3
|
@@ -0,0 +1,499 @@
|
|
|
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. A source is either a
|
|
9
|
+
* repository in an installation's Git (a fork, through the Git door) or the
|
|
10
|
+
* App's own published template, built only at a recorded seed revision of
|
|
11
|
+
* its pinned recipe (no installed App is needed). The build streams each
|
|
12
|
+
* built object into the host's artifact store itself, through the egress, and
|
|
13
|
+
* then submits a small manifest naming them. This owner only records facts:
|
|
14
|
+
* it checks each named object's stored size and digest, writes the asset
|
|
15
|
+
* manifest and App root in Oyster's one immutable artifact representation
|
|
16
|
+
* (the same objects a packed upload writes), verifies the Release like any
|
|
17
|
+
* installed one, and records it; installing it is a separate owner act.
|
|
18
|
+
*/
|
|
19
|
+
import * as Schema from "effect/Schema";
|
|
20
|
+
import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
|
|
21
|
+
import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
22
|
+
import { capturedPinnedRecipeSchema, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
|
|
23
|
+
import { candidateManifestObjects, decodeCandidateBackend, digestTeamAppCandidateManifest, } from "./build/submission-contract.js";
|
|
24
|
+
import { contentObjectRef } from "./build/content-objects.js";
|
|
25
|
+
import { createTeamBuildPublisher, teamAppBuildCommand, } from "./build/build-jobs.js";
|
|
26
|
+
import { writeArtifactRoot, } from "./build/artifact-writer.js";
|
|
27
|
+
import { buildGitPassPrefix } from "./git-auth.js";
|
|
28
|
+
import { defaultAssetPolicy, readVerified, verifyRelease, } from "./release.js";
|
|
29
|
+
import { appForTemplateRepository, templateRepositoryForApp, } from "./source/template-repository.js";
|
|
30
|
+
/** A Computer's whole-build budget: a cold build on a 6 GiB Computer measured 185-270 s. */
|
|
31
|
+
export const oysterAppBuildTimeoutMs = 900_000;
|
|
32
|
+
/** The job, its clone admission and its upload grant end together. */
|
|
33
|
+
const jobTtlMs = oysterAppBuildTimeoutMs + 5 * 60_000;
|
|
34
|
+
/** Host names the Computer's egress serves for a build; never public hosts. */
|
|
35
|
+
export const oysterBuildGitHost = "git.team.internal";
|
|
36
|
+
export const oysterBuildReportHost = "build.team.internal";
|
|
37
|
+
/** Where an App's source is cloned from. */
|
|
38
|
+
export const oysterBuildLocationSchema = Schema.Union([
|
|
39
|
+
Schema.Struct({
|
|
40
|
+
kind: Schema.Literal("installation"),
|
|
41
|
+
/** The installation whose Git holds the repository (Bonsai for a fork). */
|
|
42
|
+
installationId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,256}$/u)),
|
|
43
|
+
repository: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,128}$/u)),
|
|
44
|
+
}),
|
|
45
|
+
/** The App's own published template (`templateRepositoryForApp`). */
|
|
46
|
+
Schema.Struct({ kind: Schema.Literal("template") }),
|
|
47
|
+
]);
|
|
48
|
+
/** An App's registered build source: where it is cloned and its pinned recipe. */
|
|
49
|
+
export const oysterBuildSourceSchema = Schema.Struct({
|
|
50
|
+
appId: appIdSchema,
|
|
51
|
+
location: oysterBuildLocationSchema,
|
|
52
|
+
recipe: capturedPinnedRecipeSchema,
|
|
53
|
+
buildRecipeDigest: sha256DigestSchema,
|
|
54
|
+
});
|
|
55
|
+
/** One Computer as the build owner reaches it. */
|
|
56
|
+
/** The Computer established no terminal result for a build command, for
|
|
57
|
+
* example because its connection to the container dropped mid-command. */
|
|
58
|
+
class BuildOutcomeUnknown extends Error {
|
|
59
|
+
constructor() {
|
|
60
|
+
super("Build outcome unknown");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The repository a job clones: its installation repository, or its App's template. */
|
|
64
|
+
const cloneRepository = (job) => job.location.kind === "installation"
|
|
65
|
+
? job.location.repository
|
|
66
|
+
: templateRepositoryForApp(job.app_id);
|
|
67
|
+
const jobRow = Schema.Struct({
|
|
68
|
+
job_id: Schema.String,
|
|
69
|
+
app_id: Schema.String,
|
|
70
|
+
release_id: Schema.String,
|
|
71
|
+
source_revision: Schema.String,
|
|
72
|
+
/** The job's clone location, as its source named it when the job started. */
|
|
73
|
+
location: Schema.fromJsonString(oysterBuildLocationSchema),
|
|
74
|
+
recipe: Schema.String,
|
|
75
|
+
recipe_digest: Schema.String,
|
|
76
|
+
requested_by: Schema.String,
|
|
77
|
+
credential_id: Schema.String,
|
|
78
|
+
upload_grant_digest: Schema.String,
|
|
79
|
+
status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
|
|
80
|
+
artifact_digest: Schema.NullOr(Schema.String),
|
|
81
|
+
artifact_bytes: Schema.NullOr(Schema.Number),
|
|
82
|
+
detail: Schema.NullOr(Schema.String),
|
|
83
|
+
issued_at: Schema.Number,
|
|
84
|
+
expires_at: Schema.Number,
|
|
85
|
+
executable_until: Schema.Number,
|
|
86
|
+
});
|
|
87
|
+
const decodeJob = Schema.decodeUnknownSync(jobRow);
|
|
88
|
+
const sourceRow = Schema.Struct({ source: Schema.String });
|
|
89
|
+
const releaseRow = Schema.Struct({ release: Schema.String });
|
|
90
|
+
const sourceCodec = Schema.fromJsonString(oysterBuildSourceSchema);
|
|
91
|
+
const releaseCodec = Schema.fromJsonString(releaseSchema);
|
|
92
|
+
const recipeCodec = Schema.fromJsonString(capturedPinnedRecipeSchema);
|
|
93
|
+
const locationCodec = Schema.fromJsonString(oysterBuildLocationSchema);
|
|
94
|
+
/** Unguessable hex identity from the platform CSPRNG. */
|
|
95
|
+
function randomHex(bytes) {
|
|
96
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
97
|
+
}
|
|
98
|
+
async function sha256Hex(bytes) {
|
|
99
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
100
|
+
}
|
|
101
|
+
/** Constant-time comparison of two equal-length hex digests. */
|
|
102
|
+
function sameDigest(left, right) {
|
|
103
|
+
if (left.length !== right.length)
|
|
104
|
+
return false;
|
|
105
|
+
let difference = 0;
|
|
106
|
+
for (let index = 0; index < left.length; index += 1)
|
|
107
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
108
|
+
return difference === 0;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The SQLite tables one Oyster build owner keeps. Creation and any carrying
|
|
112
|
+
* forward of retained facts run as one transaction of the owner's storage, so
|
|
113
|
+
* an interrupted conversion leaves the facts as they were for a retry.
|
|
114
|
+
*/
|
|
115
|
+
export function migrateOysterAppBuilds(owner) {
|
|
116
|
+
const { sql } = owner;
|
|
117
|
+
owner.transactionSync(() => {
|
|
118
|
+
sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
|
|
119
|
+
// Sources recorded before the location union named only an installation
|
|
120
|
+
// repository; carry each forward once, in place.
|
|
121
|
+
sql.exec(`UPDATE oyster_build_sources SET source = json_set(
|
|
122
|
+
json_remove(source, '$.installationId', '$.repository'), '$.location',
|
|
123
|
+
json_object('kind', 'installation',
|
|
124
|
+
'installationId', json_extract(source, '$.installationId'),
|
|
125
|
+
'repository', json_extract(source, '$.repository')))
|
|
126
|
+
WHERE json_type(source, '$.location') IS NULL`);
|
|
127
|
+
const jobColumns = `
|
|
128
|
+
job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
|
|
129
|
+
source_revision TEXT NOT NULL, location TEXT NOT NULL,
|
|
130
|
+
recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
|
|
131
|
+
credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
|
|
132
|
+
status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
|
|
133
|
+
issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL`;
|
|
134
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (${jobColumns}) STRICT`);
|
|
135
|
+
// Jobs recorded before the location union named an installation repository
|
|
136
|
+
// in two columns; carry each into the one location column, once.
|
|
137
|
+
if (sql
|
|
138
|
+
.exec("SELECT name FROM pragma_table_info('oyster_build_jobs') WHERE name='installation_id'")
|
|
139
|
+
.toArray().length > 0) {
|
|
140
|
+
sql.exec(`CREATE TABLE oyster_build_jobs_current (${jobColumns}) STRICT`);
|
|
141
|
+
sql.exec(`INSERT INTO oyster_build_jobs_current SELECT job_id, app_id, release_id,
|
|
142
|
+
source_revision,
|
|
143
|
+
json_object('kind', 'installation', 'installationId', installation_id,
|
|
144
|
+
'repository', repository),
|
|
145
|
+
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
146
|
+
status, artifact_digest, artifact_bytes, detail, issued_at, expires_at,
|
|
147
|
+
executable_until
|
|
148
|
+
FROM oyster_build_jobs`);
|
|
149
|
+
sql.exec("DROP TABLE oyster_build_jobs");
|
|
150
|
+
sql.exec("ALTER TABLE oyster_build_jobs_current RENAME TO oyster_build_jobs");
|
|
151
|
+
}
|
|
152
|
+
sql.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");
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** The build owner over one tenant's SQLite, Computer and artifact store. */
|
|
156
|
+
export function createOysterAppBuilds(options) {
|
|
157
|
+
const { owner } = options;
|
|
158
|
+
const now = options.now ?? Date.now;
|
|
159
|
+
// The host store overwrites on put, so write-once is checked here first.
|
|
160
|
+
const storeObjects = {
|
|
161
|
+
async create(artifactRef, bytes) {
|
|
162
|
+
if ((await options.artifacts.get(artifactRef)) !== null)
|
|
163
|
+
return "exists";
|
|
164
|
+
await options.artifacts.put(artifactRef, bytes);
|
|
165
|
+
return "created";
|
|
166
|
+
},
|
|
167
|
+
read: (artifactRef) => options.artifacts.get(artifactRef),
|
|
168
|
+
};
|
|
169
|
+
migrateOysterAppBuilds(owner);
|
|
170
|
+
// A job outlives the owner instance that started it: the Computer keeps
|
|
171
|
+
// building and its own reports (objects, candidate, status) settle the
|
|
172
|
+
// job, so an owner restart must not fail it. A job that reaches its expiry
|
|
173
|
+
// without a result can never settle, so it fails then instead of reading
|
|
174
|
+
// as running for as long as anyone asks.
|
|
175
|
+
const expire = () => owner.sql.exec("UPDATE oyster_build_jobs SET status='failed', detail=? WHERE status='running' AND expires_at<=?", "Build expired without a result", now());
|
|
176
|
+
expire();
|
|
177
|
+
const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
|
|
178
|
+
const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
|
|
179
|
+
const readJob = (jobId) => {
|
|
180
|
+
const [row] = owner.sql
|
|
181
|
+
.exec("SELECT * FROM oyster_build_jobs WHERE job_id=?", jobId)
|
|
182
|
+
.toArray();
|
|
183
|
+
return row === undefined ? null : decodeJob(row);
|
|
184
|
+
};
|
|
185
|
+
const live = (job) => job !== null && job.status === "running" && job.expires_at > now();
|
|
186
|
+
const readRelease = (jobId) => {
|
|
187
|
+
const [row] = owner.sql
|
|
188
|
+
.exec("SELECT release FROM oyster_build_releases WHERE job_id=?", jobId)
|
|
189
|
+
.toArray();
|
|
190
|
+
return row === undefined
|
|
191
|
+
? null
|
|
192
|
+
: Schema.decodeUnknownSync(releaseCodec)(Schema.decodeUnknownSync(releaseRow)(row).release);
|
|
193
|
+
};
|
|
194
|
+
const lifecycleJob = (job) => ({
|
|
195
|
+
jobId: job.job_id,
|
|
196
|
+
computerId: options.computer.name,
|
|
197
|
+
credentialId: job.credential_id,
|
|
198
|
+
issuedAt: job.issued_at,
|
|
199
|
+
repository: cloneRepository(job),
|
|
200
|
+
expiresAt: job.expires_at,
|
|
201
|
+
remote: job.location.kind === "installation"
|
|
202
|
+
? `https://${oysterBuildGitHost}/git/${job.location.installationId}/${job.location.repository}.git`
|
|
203
|
+
: `https://${oysterBuildGitHost}/${cloneRepository(job)}.git`,
|
|
204
|
+
reportUrl: `https://${oysterBuildReportHost}/jobs/${job.job_id}`,
|
|
205
|
+
status: job.status,
|
|
206
|
+
artifact: job.artifact_digest === null || job.artifact_bytes === null
|
|
207
|
+
? null
|
|
208
|
+
: { digest: job.artifact_digest, bytes: job.artifact_bytes },
|
|
209
|
+
detail: job.detail,
|
|
210
|
+
});
|
|
211
|
+
const settle = (jobId, status, detail) => {
|
|
212
|
+
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);
|
|
213
|
+
};
|
|
214
|
+
const required = (jobId) => {
|
|
215
|
+
const job = readJob(jobId);
|
|
216
|
+
if (job === null)
|
|
217
|
+
throw new Error("Unknown build job");
|
|
218
|
+
return job;
|
|
219
|
+
};
|
|
220
|
+
const publisher = createTeamBuildPublisher({
|
|
221
|
+
teamId: options.tenantId,
|
|
222
|
+
now,
|
|
223
|
+
maxPermissionAgeMs: jobTtlMs,
|
|
224
|
+
team: {
|
|
225
|
+
// The job row is minted in `start`; the lifecycle only reads it back.
|
|
226
|
+
mintTeamBuildCredential: ({ jobId }) => Promise.resolve(lifecycleJob(required(jobId))),
|
|
227
|
+
revokeTeamBuild: (jobId) => {
|
|
228
|
+
settle(jobId, "revoked", "Build revoked");
|
|
229
|
+
return Promise.resolve(lifecycleJob(required(jobId)));
|
|
230
|
+
},
|
|
231
|
+
build: () => Promise.resolve({
|
|
232
|
+
prepare: (jobId) => Promise.resolve(lifecycleJob(required(jobId))),
|
|
233
|
+
read: (jobId) => {
|
|
234
|
+
const job = readJob(jobId);
|
|
235
|
+
return Promise.resolve(job === null ? null : lifecycleJob(job));
|
|
236
|
+
},
|
|
237
|
+
complete: (jobId, status, detail) => {
|
|
238
|
+
settle(jobId, status, detail);
|
|
239
|
+
return Promise.resolve(lifecycleJob(required(jobId)));
|
|
240
|
+
},
|
|
241
|
+
// Oyster's egress writes job facts here directly; nothing is queued.
|
|
242
|
+
deliverProjection: () => Promise.resolve(),
|
|
243
|
+
[Symbol.dispose]: () => undefined,
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
computer: () => ({
|
|
247
|
+
applyBuildDecision: (decision) => {
|
|
248
|
+
owner.sql.exec("UPDATE oyster_build_jobs SET executable_until=? WHERE job_id=?", decision.status === "allowed" ? decision.expiresAt : 0, decision.jobId);
|
|
249
|
+
return Promise.resolve();
|
|
250
|
+
},
|
|
251
|
+
execute: async (input) => {
|
|
252
|
+
const result = await options.computer.port.runBuild({
|
|
253
|
+
jobId: input.jobId,
|
|
254
|
+
command: input.command,
|
|
255
|
+
timeoutMs: input.timeoutMs,
|
|
256
|
+
});
|
|
257
|
+
if (result.output === undefined || result.output === null)
|
|
258
|
+
throw new BuildOutcomeUnknown();
|
|
259
|
+
return result;
|
|
260
|
+
},
|
|
261
|
+
deliverTeamGitEvents: () => Promise.resolve(0),
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
const readSource = (appId) => {
|
|
265
|
+
const [row] = owner.sql
|
|
266
|
+
.exec("SELECT source FROM oyster_build_sources WHERE app_id=?", appId)
|
|
267
|
+
.toArray();
|
|
268
|
+
return row === undefined
|
|
269
|
+
? null
|
|
270
|
+
: Schema.decodeUnknownSync(sourceCodec)(Schema.decodeUnknownSync(sourceRow)(row).source);
|
|
271
|
+
};
|
|
272
|
+
const run = async (jobId, command) => {
|
|
273
|
+
try {
|
|
274
|
+
await publisher.run({
|
|
275
|
+
jobId,
|
|
276
|
+
command,
|
|
277
|
+
timeoutMs: oysterAppBuildTimeoutMs,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (cause) {
|
|
281
|
+
// Without a terminal result the build may still be running on the
|
|
282
|
+
// Computer; its own reports settle the job, or its expiry fails it.
|
|
283
|
+
if (!(cause instanceof BuildOutcomeUnknown))
|
|
284
|
+
settle(jobId, "failed", cause instanceof Error ? cause.message : "Build failed");
|
|
285
|
+
}
|
|
286
|
+
return view(required(jobId));
|
|
287
|
+
};
|
|
288
|
+
const view = (job) => ({
|
|
289
|
+
jobId: job.job_id,
|
|
290
|
+
appId: job.app_id,
|
|
291
|
+
releaseId: job.release_id,
|
|
292
|
+
sourceRevision: job.source_revision,
|
|
293
|
+
status: job.status,
|
|
294
|
+
detail: job.detail,
|
|
295
|
+
release: readRelease(job.job_id),
|
|
296
|
+
});
|
|
297
|
+
return Object.freeze({
|
|
298
|
+
/** Record (or replace) an App's build source; the caller is the owner. */
|
|
299
|
+
registerSource(source) {
|
|
300
|
+
if (digestPinnedRecipe(source.recipe) !== source.buildRecipeDigest)
|
|
301
|
+
throw new Error("Build recipe digest does not match its recipe");
|
|
302
|
+
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));
|
|
303
|
+
return source;
|
|
304
|
+
},
|
|
305
|
+
readSource,
|
|
306
|
+
/**
|
|
307
|
+
* Mint one build job for an App's registered source at one exact commit.
|
|
308
|
+
* A template source builds only a seed revision recorded for its pinned
|
|
309
|
+
* recipe.
|
|
310
|
+
* Answers the job and its one run, which the caller keeps alive (the
|
|
311
|
+
* upload grant exists only inside that run's fixed command).
|
|
312
|
+
*/
|
|
313
|
+
async start(input) {
|
|
314
|
+
const sourceRevision = Schema.decodeUnknownSync(sourceRevisionSchema)(input.sourceRevision);
|
|
315
|
+
const source = readSource(input.appId);
|
|
316
|
+
if (source === null)
|
|
317
|
+
throw new Error("App has no build source");
|
|
318
|
+
if (source.location.kind === "template") {
|
|
319
|
+
const receipt = await options.templates.read(source.appId, sourceRevision);
|
|
320
|
+
if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
|
|
321
|
+
throw new Error("Revision is not a seed of this App's template");
|
|
322
|
+
}
|
|
323
|
+
const jobId = `build_${randomHex(16)}`;
|
|
324
|
+
const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
|
|
325
|
+
const uploadGrant = randomHex(32);
|
|
326
|
+
const issuedAt = now();
|
|
327
|
+
owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
|
|
328
|
+
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
329
|
+
status, artifact_digest, artifact_bytes, detail, issued_at, expires_at, executable_until)
|
|
330
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
|
|
331
|
+
// The grant leaves this owner only inside the fixed command.
|
|
332
|
+
const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
|
|
333
|
+
return { build: view(required(jobId)), run: () => run(jobId, command) };
|
|
334
|
+
},
|
|
335
|
+
read(jobId) {
|
|
336
|
+
expire();
|
|
337
|
+
const job = readJob(jobId);
|
|
338
|
+
return job === null ? null : view(job);
|
|
339
|
+
},
|
|
340
|
+
/** A built Release by id, for an owner to install or select. */
|
|
341
|
+
release(releaseId) {
|
|
342
|
+
const [row] = owner.sql
|
|
343
|
+
.exec("SELECT release FROM oyster_build_releases WHERE release_id=?", releaseId)
|
|
344
|
+
.toArray();
|
|
345
|
+
return row === undefined
|
|
346
|
+
? null
|
|
347
|
+
: Schema.decodeUnknownSync(releaseCodec)(Schema.decodeUnknownSync(releaseRow)(row).release);
|
|
348
|
+
},
|
|
349
|
+
/** May this Computer execute this job now? Re-checked by the Computer. */
|
|
350
|
+
admitExecution(jobId, computerObjectId) {
|
|
351
|
+
const job = readJob(jobId);
|
|
352
|
+
return (computerObjectId === options.computer.objectId &&
|
|
353
|
+
live(job) &&
|
|
354
|
+
job.executable_until > now());
|
|
355
|
+
},
|
|
356
|
+
/** The live job a build report from this Computer may speak for. */
|
|
357
|
+
admitBuild(jobId, computerObjectId) {
|
|
358
|
+
const job = readJob(jobId);
|
|
359
|
+
return computerObjectId === options.computer.objectId && live(job)
|
|
360
|
+
? { credentialId: job.credential_id }
|
|
361
|
+
: null;
|
|
362
|
+
},
|
|
363
|
+
/**
|
|
364
|
+
* The clone credential the egress presents for this Computer's live job
|
|
365
|
+
* on exactly this repository, read only; null when no job admits it.
|
|
366
|
+
*/
|
|
367
|
+
gitPassFor(computerObjectId, target) {
|
|
368
|
+
if (computerObjectId !== options.computer.objectId ||
|
|
369
|
+
target.scope !== "read")
|
|
370
|
+
return null;
|
|
371
|
+
const job = owner.sql
|
|
372
|
+
.exec(`SELECT * FROM oyster_build_jobs WHERE json_extract(location, '$.kind')='installation'
|
|
373
|
+
AND json_extract(location, '$.installationId')=? AND json_extract(location, '$.repository')=?
|
|
374
|
+
AND status='running' AND expires_at>? ORDER BY issued_at DESC LIMIT 1`, target.installationId, target.repository, now())
|
|
375
|
+
.toArray()
|
|
376
|
+
.map((row) => decodeJob(row))[0];
|
|
377
|
+
return job === undefined
|
|
378
|
+
? null
|
|
379
|
+
: `${buildGitPassPrefix}${job.job_id}.${job.credential_id}`;
|
|
380
|
+
},
|
|
381
|
+
/** Admit a presented clone credential at the Git door: one repository, read. */
|
|
382
|
+
admitGitPass(pass, target) {
|
|
383
|
+
const match = /^obp\.(build_[0-9a-f]{32})\.([0-9a-f]{64})$/u.exec(pass);
|
|
384
|
+
if (match === null || target.scope !== "read")
|
|
385
|
+
return null;
|
|
386
|
+
const job = readJob(match[1] ?? "");
|
|
387
|
+
return live(job) &&
|
|
388
|
+
job.location.kind === "installation" &&
|
|
389
|
+
sameDigest(job.credential_id, match[2] ?? "") &&
|
|
390
|
+
job.location.installationId === target.installationId &&
|
|
391
|
+
job.location.repository === target.repository
|
|
392
|
+
? { requestedBy: job.requested_by }
|
|
393
|
+
: null;
|
|
394
|
+
},
|
|
395
|
+
/**
|
|
396
|
+
* May this Computer's clone read this template repository now? Only while
|
|
397
|
+
* a live template job on this Computer names it.
|
|
398
|
+
*/
|
|
399
|
+
admitTemplateClone(computerObjectId, repository) {
|
|
400
|
+
if (computerObjectId !== options.computer.objectId)
|
|
401
|
+
return false;
|
|
402
|
+
// The template repository names exactly one App (templateRepositoryForApp).
|
|
403
|
+
const appId = appForTemplateRepository(repository);
|
|
404
|
+
if (appId === null)
|
|
405
|
+
return false;
|
|
406
|
+
return (owner.sql
|
|
407
|
+
.exec(`SELECT 1 FROM oyster_build_jobs WHERE app_id=? AND json_extract(location, '$.kind')='template'
|
|
408
|
+
AND status='running' AND expires_at>? LIMIT 1`, appId, now())
|
|
409
|
+
.toArray().length > 0);
|
|
410
|
+
},
|
|
411
|
+
/** A reported artifact or status for a live job. */
|
|
412
|
+
record(event) {
|
|
413
|
+
const job = readJob(event.jobId);
|
|
414
|
+
if (!live(job) || job.credential_id !== event.credentialId)
|
|
415
|
+
return;
|
|
416
|
+
if (event.kind === "build-artifact")
|
|
417
|
+
owner.sql.exec("UPDATE oyster_build_jobs SET artifact_digest=?, artifact_bytes=? WHERE job_id=?", event.digest, event.bytes, event.jobId);
|
|
418
|
+
// Success is the recorded Release's fact, never the report's alone.
|
|
419
|
+
else if (event.status === "failed" || readRelease(event.jobId) !== null)
|
|
420
|
+
settle(event.jobId, event.status, event.detail);
|
|
421
|
+
},
|
|
422
|
+
/**
|
|
423
|
+
* May this live job's build upload one object now, and how large? The
|
|
424
|
+
* grant is the one inside the job's fixed command; the bound is its
|
|
425
|
+
* recipe's (`pinnedRecipeObjectBound`).
|
|
426
|
+
*/
|
|
427
|
+
async admitObjectUpload(jobId, computerObjectId, grant) {
|
|
428
|
+
const job = readJob(jobId);
|
|
429
|
+
if (computerObjectId !== options.computer.objectId ||
|
|
430
|
+
!live(job) ||
|
|
431
|
+
!(await grantAdmits(job, grant)))
|
|
432
|
+
return null;
|
|
433
|
+
return {
|
|
434
|
+
maxObjectBytes: pinnedRecipeObjectBound(decodeRecipe(job.recipe)),
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
/**
|
|
438
|
+
* The Computer's candidate manifest for a live job: checked against the
|
|
439
|
+
* job and its pinned recipe, each named object found stored with its
|
|
440
|
+
* size and verified digest (one at a time; only the backend descriptor
|
|
441
|
+
* is read), its asset manifest and App root written, verified as an
|
|
442
|
+
* installable Release and recorded. An over-policy candidate stores
|
|
443
|
+
* nothing.
|
|
444
|
+
*/
|
|
445
|
+
async submitCandidate(jobId, manifest, grant, credentialId) {
|
|
446
|
+
const job = readJob(jobId);
|
|
447
|
+
if (!live(job) ||
|
|
448
|
+
job.credential_id !== credentialId ||
|
|
449
|
+
!(await grantAdmits(job, grant)))
|
|
450
|
+
return new Response(null, { status: 403 });
|
|
451
|
+
if (manifest.appId !== job.app_id ||
|
|
452
|
+
manifest.provenance.sourceRevision !== job.source_revision ||
|
|
453
|
+
manifest.provenance.buildRecipeDigest !== job.recipe_digest)
|
|
454
|
+
return new Response(null, { status: 409 });
|
|
455
|
+
if (readRelease(jobId) !== null)
|
|
456
|
+
return new Response(null, { status: 409 });
|
|
457
|
+
const recipe = decodeRecipe(job.recipe);
|
|
458
|
+
let release;
|
|
459
|
+
try {
|
|
460
|
+
if (!fitsPinnedRecipe(manifest.output, recipe))
|
|
461
|
+
throw new Error("Candidate exceeds its pinned recipe");
|
|
462
|
+
for (const object of candidateManifestObjects(manifest.output)) {
|
|
463
|
+
const head = await options.artifacts.head(contentObjectRef(object.digest));
|
|
464
|
+
if (head?.byteLength !== object.byteLength ||
|
|
465
|
+
head.digest !== object.digest)
|
|
466
|
+
throw new Error("Candidate object is not stored as named");
|
|
467
|
+
}
|
|
468
|
+
if (manifest.output.kind !== "ui")
|
|
469
|
+
decodeCandidateBackend(await readVerified(options.artifacts, contentObjectRef(manifest.output.backend.digest), manifest.output.backend.digest));
|
|
470
|
+
const root = await writeArtifactRoot(manifest.output, storeObjects, {
|
|
471
|
+
rootArtifactRef: parseArtifactRef(`artifact:sha256-${await digestTeamAppCandidateManifest(manifest)}`),
|
|
472
|
+
assetPolicy: recipe.assets,
|
|
473
|
+
});
|
|
474
|
+
release = Schema.decodeUnknownSync(releaseSchema)({
|
|
475
|
+
releaseId: job.release_id,
|
|
476
|
+
appId: manifest.appId,
|
|
477
|
+
artifact: {
|
|
478
|
+
artifactRef: root.artifactRef,
|
|
479
|
+
artifactDigest: root.artifactDigest,
|
|
480
|
+
provenance: manifest.provenance,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
// Each object was checked by its stored digest above.
|
|
484
|
+
await verifyRelease(options.artifacts, release, defaultAssetPolicy, false);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
settle(jobId, "failed", "Candidate could not be stored as a Release");
|
|
488
|
+
return new Response(null, { status: 422 });
|
|
489
|
+
}
|
|
490
|
+
owner.transactionSync(() => {
|
|
491
|
+
owner.sql.exec("INSERT INTO oyster_build_releases(release_id, job_id, release) VALUES(?, ?, ?)", release.releaseId, jobId, Schema.encodeUnknownSync(releaseCodec)(release));
|
|
492
|
+
});
|
|
493
|
+
return Response.json({
|
|
494
|
+
kind: "submitted",
|
|
495
|
+
release: Schema.encodeUnknownSync(releaseSchema)(release),
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
/** The operator's smart-HTTP door to one App's template repository: an
|
|
26
|
+
* ordinary `git push` (or the fetch before it) to
|
|
27
|
+
* `<base>/<suffix>` reaches `template-<app>` through the same admitted
|
|
28
|
+
* Artifacts exchange, which streams both bodies and holds every provider
|
|
29
|
+
* credential. An advertisement first opens the repository, so a fresh Oyster's
|
|
30
|
+
* first seed creates it. Admission (the owner) is the caller's. */
|
|
31
|
+
export declare function exchangeOysterTemplateSource(artifacts: Pick<Artifacts, "ensureRepository" | "exchangeGit">, rawAppId: string, suffix: string, request: Request): Promise<Response>;
|