@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,180 @@
|
|
|
1
|
+
import { assetBaseSchema, assetPathSchema, } from "@fungi.computer/puffball/assets";
|
|
2
|
+
import { appIdSchema, releaseIdSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
3
|
+
import { appDeclarationSchema } from "@fungi.computer/puffball/app";
|
|
4
|
+
import { backendJsonSchema } from "@fungi.computer/puffball/backend";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import { isSigilName } from "@fungi.computer/sigils";
|
|
7
|
+
/**
|
|
8
|
+
* The largest candidate manifest request body, in bytes. A manifest names
|
|
9
|
+
* objects; their bytes are uploaded one at a time, never inside it.
|
|
10
|
+
*/
|
|
11
|
+
export const TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES = 1024 * 1024;
|
|
12
|
+
const byteLengthSchema = Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.makeFilter(Number.isSafeInteger)));
|
|
13
|
+
/** One content-addressed object a build uploaded before its manifest. */
|
|
14
|
+
export const candidateObjectSchema = Schema.Struct({
|
|
15
|
+
digest: sha256DigestSchema,
|
|
16
|
+
byteLength: byteLengthSchema,
|
|
17
|
+
});
|
|
18
|
+
const uiManifestSchema = Schema.Union([
|
|
19
|
+
Schema.Struct({
|
|
20
|
+
mediaType: Schema.Literal("text/html; charset=utf-8"),
|
|
21
|
+
content: Schema.Struct({
|
|
22
|
+
kind: Schema.Literal("html"),
|
|
23
|
+
...candidateObjectSchema.fields,
|
|
24
|
+
}),
|
|
25
|
+
}),
|
|
26
|
+
Schema.Struct({
|
|
27
|
+
mediaType: Schema.Literal("application/vnd.fungi.app-assets+json"),
|
|
28
|
+
content: Schema.Struct({
|
|
29
|
+
kind: Schema.Literal("assets"),
|
|
30
|
+
base: assetBaseSchema,
|
|
31
|
+
assets: Schema.Array(Schema.Struct({
|
|
32
|
+
path: assetPathSchema,
|
|
33
|
+
...candidateObjectSchema.fields,
|
|
34
|
+
})),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
/** A request only; the Team's installation grants remain the authority. */
|
|
39
|
+
const declaration = {
|
|
40
|
+
declaration: Schema.optionalKey(appDeclarationSchema.check(Schema.makeFilter((declaration) => declaration.icon === undefined || isSigilName(declaration.icon), { message: "App icon must name a registered Sigil" }))),
|
|
41
|
+
};
|
|
42
|
+
/** A build's output by reference: the same surfaces as its bytes, sized. */
|
|
43
|
+
export const candidateOutputManifestSchema = Schema.Union([
|
|
44
|
+
Schema.Struct({
|
|
45
|
+
kind: Schema.Literal("ui"),
|
|
46
|
+
ui: uiManifestSchema,
|
|
47
|
+
...declaration,
|
|
48
|
+
}),
|
|
49
|
+
Schema.Struct({
|
|
50
|
+
kind: Schema.Literal("backend"),
|
|
51
|
+
backend: candidateObjectSchema,
|
|
52
|
+
...declaration,
|
|
53
|
+
}),
|
|
54
|
+
Schema.Struct({
|
|
55
|
+
kind: Schema.Literal("app"),
|
|
56
|
+
ui: uiManifestSchema,
|
|
57
|
+
backend: candidateObjectSchema,
|
|
58
|
+
...declaration,
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
const provenanceSchema = Schema.Struct({
|
|
62
|
+
sourceRevision: sourceRevisionSchema,
|
|
63
|
+
sourceTreeDigest: sha256DigestSchema,
|
|
64
|
+
buildRecipeDigest: sha256DigestSchema,
|
|
65
|
+
});
|
|
66
|
+
const manifestFields = {
|
|
67
|
+
appId: appIdSchema,
|
|
68
|
+
output: candidateOutputManifestSchema,
|
|
69
|
+
provenance: provenanceSchema,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* A submitted Team App candidate: its output's objects by digest and size,
|
|
73
|
+
* and its provenance, before the Team assigns a stable Release ID.
|
|
74
|
+
*/
|
|
75
|
+
export const teamAppCandidateManifestSchema = Schema.Struct(manifestFields);
|
|
76
|
+
/** Publisher persistence input after TeamDO reserves retry-stable identities. */
|
|
77
|
+
export const preparedCandidateManifestSchema = Schema.Struct({
|
|
78
|
+
...manifestFields,
|
|
79
|
+
releaseId: releaseIdSchema,
|
|
80
|
+
});
|
|
81
|
+
/** One operator-published byte string; its request has a separate total bound. */
|
|
82
|
+
const bytesSchema = Schema.Uint8ArrayFromBase64.pipe(Schema.check(Schema.makeFilter((bytes) => bytes.byteLength <= 16 * 1024 * 1024)));
|
|
83
|
+
const backendBytesSchema = bytesSchema.pipe(Schema.check(Schema.makeFilter((bytes) => Schema.decodeUnknownResult(backendJsonSchema)(new TextDecoder().decode(bytes), { onExcessProperty: "error" })._tag === "Success")));
|
|
84
|
+
const uiBytesSchema = Schema.Union([
|
|
85
|
+
Schema.Struct({
|
|
86
|
+
mediaType: Schema.Literal("text/html; charset=utf-8"),
|
|
87
|
+
content: Schema.Struct({
|
|
88
|
+
kind: Schema.Literal("html"),
|
|
89
|
+
bytes: bytesSchema,
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
Schema.Struct({
|
|
93
|
+
mediaType: Schema.Literal("application/vnd.fungi.app-assets+json"),
|
|
94
|
+
content: Schema.Struct({
|
|
95
|
+
kind: Schema.Literal("assets"),
|
|
96
|
+
base: assetBaseSchema,
|
|
97
|
+
assets: Schema.Array(Schema.Struct({ path: assetPathSchema, bytes: bytesSchema })),
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* A platform App candidate the trusted operator built and packed itself,
|
|
103
|
+
* with its bytes inline: the operator's platform publication request only.
|
|
104
|
+
* A Team candidate never carries bytes; it is a manifest.
|
|
105
|
+
*/
|
|
106
|
+
export const preparedCandidateSchema = Schema.Struct({
|
|
107
|
+
appId: appIdSchema,
|
|
108
|
+
releaseId: releaseIdSchema,
|
|
109
|
+
output: Schema.Union([
|
|
110
|
+
Schema.Struct({
|
|
111
|
+
kind: Schema.Literal("ui"),
|
|
112
|
+
ui: uiBytesSchema,
|
|
113
|
+
...declaration,
|
|
114
|
+
}),
|
|
115
|
+
Schema.Struct({
|
|
116
|
+
kind: Schema.Literal("backend"),
|
|
117
|
+
backend: backendBytesSchema,
|
|
118
|
+
...declaration,
|
|
119
|
+
}),
|
|
120
|
+
Schema.Struct({
|
|
121
|
+
kind: Schema.Literal("app"),
|
|
122
|
+
ui: uiBytesSchema,
|
|
123
|
+
backend: backendBytesSchema,
|
|
124
|
+
...declaration,
|
|
125
|
+
}),
|
|
126
|
+
]),
|
|
127
|
+
provenance: provenanceSchema,
|
|
128
|
+
});
|
|
129
|
+
/** Canonical manifest bytes, so a digest is independent of JSON spacing. */
|
|
130
|
+
export function encodeTeamAppCandidateManifest(manifest) {
|
|
131
|
+
return JSON.stringify(Schema.encodeUnknownSync(teamAppCandidateManifestSchema)(manifest));
|
|
132
|
+
}
|
|
133
|
+
/** The hex SHA-256 of a manifest's canonical bytes: its App root's name. */
|
|
134
|
+
export async function digestTeamAppCandidateManifest(manifest) {
|
|
135
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(encodeTeamAppCandidateManifest(manifest)))), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The media type an uploaded object is stored with. A store that reads an
|
|
139
|
+
* HTML document or backend descriptor by its stored type (the Publisher's)
|
|
140
|
+
* needs it; an asset's type is its manifest's, so it is an octet stream.
|
|
141
|
+
*/
|
|
142
|
+
export const candidateObjectMediaTypes = [
|
|
143
|
+
"text/html; charset=utf-8",
|
|
144
|
+
"application/vnd.fungi.app-backend+json",
|
|
145
|
+
"application/octet-stream",
|
|
146
|
+
];
|
|
147
|
+
/** The stored media type a request names, if it is one an object may carry. */
|
|
148
|
+
export function candidateObjectMediaType(value) {
|
|
149
|
+
return candidateObjectMediaTypes.find((type) => type === value) ?? null;
|
|
150
|
+
}
|
|
151
|
+
/** Every distinct object a manifest names, each once, in manifest order. */
|
|
152
|
+
export function candidateManifestObjects(output) {
|
|
153
|
+
const objects = [];
|
|
154
|
+
const add = (object, mediaType) => {
|
|
155
|
+
const seen = objects.find(({ digest }) => digest === object.digest);
|
|
156
|
+
if (seen === undefined)
|
|
157
|
+
objects.push({
|
|
158
|
+
digest: object.digest,
|
|
159
|
+
byteLength: object.byteLength,
|
|
160
|
+
mediaType,
|
|
161
|
+
});
|
|
162
|
+
else if (seen.byteLength !== object.byteLength)
|
|
163
|
+
throw new Error("One object digest names two sizes");
|
|
164
|
+
};
|
|
165
|
+
if (output.kind !== "backend") {
|
|
166
|
+
const { content } = output.ui;
|
|
167
|
+
if (content.kind === "html")
|
|
168
|
+
add(content, "text/html; charset=utf-8");
|
|
169
|
+
else
|
|
170
|
+
for (const asset of content.assets)
|
|
171
|
+
add(asset, "application/octet-stream");
|
|
172
|
+
}
|
|
173
|
+
if (output.kind !== "ui")
|
|
174
|
+
add(output.backend, "application/vnd.fungi.app-backend+json");
|
|
175
|
+
return objects;
|
|
176
|
+
}
|
|
177
|
+
/** Decode a stored backend descriptor; a manifest's backend must be one. */
|
|
178
|
+
export function decodeCandidateBackend(bytes) {
|
|
179
|
+
return Schema.decodeUnknownSync(backendJsonSchema)(new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), { onExcessProperty: "error" });
|
|
180
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { WorkspaceBuildStep } from "./build-step.js";
|
|
3
|
+
/**
|
|
4
|
+
* `package.json#fungi.build`. An App manifest (the manifest nearest the
|
|
5
|
+
* entry) declares how its page builds: `react`, `shellEntry`, the workspace
|
|
6
|
+
* packages whose declared stylesheets compile first (`packageStylesheets`)
|
|
7
|
+
* and its own `stylesheets`, compiled in place, in order. A workspace
|
|
8
|
+
* package declares its build products: extra TypeScript configs compiled
|
|
9
|
+
* after its `tsconfig.build.json`, and its stylesheet input and output; or,
|
|
10
|
+
* instead of TypeScript configs, the one `package.json` script that builds
|
|
11
|
+
* it (`script`), which only an admitted Computer build runs. Every path is
|
|
12
|
+
* relative to the manifest that declares it. Unknown build fields are
|
|
13
|
+
* refused, so a builder never silently ignores a policy it does not know.
|
|
14
|
+
*/
|
|
15
|
+
export declare const workspaceBuildPolicySchema: Schema.Struct<{
|
|
16
|
+
readonly react: Schema.optional<Schema.Boolean>;
|
|
17
|
+
readonly shellEntry: Schema.optional<Schema.String>;
|
|
18
|
+
readonly packageStylesheets: Schema.optional<Schema.$Array<Schema.String>>;
|
|
19
|
+
readonly stylesheets: Schema.optional<Schema.$Array<Schema.String>>;
|
|
20
|
+
readonly typescript: Schema.optional<Schema.$Array<Schema.String>>;
|
|
21
|
+
/** A script name in this manifest's own `scripts`. */
|
|
22
|
+
readonly script: Schema.optional<Schema.NonEmptyString>;
|
|
23
|
+
readonly stylesheet: Schema.optional<Schema.Struct<{
|
|
24
|
+
readonly input: Schema.String;
|
|
25
|
+
readonly output: Schema.String;
|
|
26
|
+
}>>;
|
|
27
|
+
}>;
|
|
28
|
+
/** The build policy one manifest declares. */
|
|
29
|
+
export type WorkspaceBuildPolicy = Schema.Schema.Type<typeof workspaceBuildPolicySchema>;
|
|
30
|
+
/** The pnpm arguments that list a source tree's workspace projects. */
|
|
31
|
+
export declare const workspaceProjectsArguments: readonly string[];
|
|
32
|
+
/** One App page build derived from its manifests and package graph. */
|
|
33
|
+
export type WorkspaceBuild = Readonly<{
|
|
34
|
+
/** The pnpm workspace that owns the entry, relative to the source root. */
|
|
35
|
+
directory: string;
|
|
36
|
+
packageName: string;
|
|
37
|
+
react: boolean;
|
|
38
|
+
/** A shell module entry, relative to `directory`. */
|
|
39
|
+
shellEntry?: string;
|
|
40
|
+
/** The workspace's runtime workspace dependencies, dependencies first. */
|
|
41
|
+
dependencies: readonly string[];
|
|
42
|
+
steps: readonly WorkspaceBuildStep[];
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Derive one App's page build from the captured source alone: the manifest
|
|
46
|
+
* nearest `entry` declares the App policy, the owning pnpm workspace's
|
|
47
|
+
* runtime `workspace:` dependencies give the compile order, and each
|
|
48
|
+
* dependency's manifest declares its own extra build products.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveWorkspaceBuild(options: {
|
|
51
|
+
readonly root: string;
|
|
52
|
+
readonly entry: string;
|
|
53
|
+
/** `pnpm ls -r --depth -1 --json` output for the same source tree. */
|
|
54
|
+
readonly projects: string;
|
|
55
|
+
}): Promise<WorkspaceBuild>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
const manifestPathSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((path) => path.length > 0 &&
|
|
5
|
+
!path.startsWith("/") &&
|
|
6
|
+
!path.includes("\\") &&
|
|
7
|
+
path
|
|
8
|
+
.split("/")
|
|
9
|
+
.every((part) => part !== "" && part !== "." && part !== ".."))));
|
|
10
|
+
/**
|
|
11
|
+
* `package.json#fungi.build`. An App manifest (the manifest nearest the
|
|
12
|
+
* entry) declares how its page builds: `react`, `shellEntry`, the workspace
|
|
13
|
+
* packages whose declared stylesheets compile first (`packageStylesheets`)
|
|
14
|
+
* and its own `stylesheets`, compiled in place, in order. A workspace
|
|
15
|
+
* package declares its build products: extra TypeScript configs compiled
|
|
16
|
+
* after its `tsconfig.build.json`, and its stylesheet input and output; or,
|
|
17
|
+
* instead of TypeScript configs, the one `package.json` script that builds
|
|
18
|
+
* it (`script`), which only an admitted Computer build runs. Every path is
|
|
19
|
+
* relative to the manifest that declares it. Unknown build fields are
|
|
20
|
+
* refused, so a builder never silently ignores a policy it does not know.
|
|
21
|
+
*/
|
|
22
|
+
export const workspaceBuildPolicySchema = Schema.Struct({
|
|
23
|
+
react: Schema.optional(Schema.Boolean),
|
|
24
|
+
shellEntry: Schema.optional(manifestPathSchema),
|
|
25
|
+
packageStylesheets: Schema.optional(Schema.Array(Schema.String)),
|
|
26
|
+
stylesheets: Schema.optional(Schema.Array(manifestPathSchema)),
|
|
27
|
+
typescript: Schema.optional(Schema.Array(manifestPathSchema)),
|
|
28
|
+
/** A script name in this manifest's own `scripts`. */
|
|
29
|
+
script: Schema.optional(Schema.NonEmptyString),
|
|
30
|
+
stylesheet: Schema.optional(Schema.Struct({ input: manifestPathSchema, output: manifestPathSchema })),
|
|
31
|
+
});
|
|
32
|
+
const manifestSchema = Schema.fromJsonString(Schema.Struct({
|
|
33
|
+
name: Schema.optional(Schema.String),
|
|
34
|
+
dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
35
|
+
scripts: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
36
|
+
fungi: Schema.optional(Schema.Struct({ build: Schema.optional(Schema.Unknown) })),
|
|
37
|
+
}));
|
|
38
|
+
/** A manifest's build policy; only `fungi.build` itself is held strict. */
|
|
39
|
+
function policyOf(manifest) {
|
|
40
|
+
const build = manifest.fungi?.build;
|
|
41
|
+
return build === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: Schema.decodeUnknownSync(workspaceBuildPolicySchema, {
|
|
44
|
+
onExcessProperty: "error",
|
|
45
|
+
})(build);
|
|
46
|
+
}
|
|
47
|
+
/** One pnpm workspace project, as `pnpm ls -r --depth -1 --json` lists it. */
|
|
48
|
+
const workspaceProjectsSchema = Schema.fromJsonString(Schema.Array(Schema.Struct({
|
|
49
|
+
name: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
50
|
+
path: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
|
|
51
|
+
})));
|
|
52
|
+
/** The pnpm arguments that list a source tree's workspace projects. */
|
|
53
|
+
export const workspaceProjectsArguments = Object.freeze([
|
|
54
|
+
"ls",
|
|
55
|
+
"--recursive",
|
|
56
|
+
"--depth",
|
|
57
|
+
"-1",
|
|
58
|
+
"--json",
|
|
59
|
+
]);
|
|
60
|
+
function within(root, path) {
|
|
61
|
+
const relativePath = relative(root, resolve(root, path));
|
|
62
|
+
if (relativePath === ".." ||
|
|
63
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
64
|
+
isAbsolute(relativePath))
|
|
65
|
+
throw new Error(`Build input escapes the captured source: ${path}`);
|
|
66
|
+
return relativePath.split(sep).join("/");
|
|
67
|
+
}
|
|
68
|
+
async function exists(path) {
|
|
69
|
+
return access(path).then(() => true, () => false);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive one App's page build from the captured source alone: the manifest
|
|
73
|
+
* nearest `entry` declares the App policy, the owning pnpm workspace's
|
|
74
|
+
* runtime `workspace:` dependencies give the compile order, and each
|
|
75
|
+
* dependency's manifest declares its own extra build products.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveWorkspaceBuild(options) {
|
|
78
|
+
const root = resolve(options.root);
|
|
79
|
+
const entry = within(root, options.entry);
|
|
80
|
+
const manifests = new Map();
|
|
81
|
+
const manifest = async (directory) => {
|
|
82
|
+
const known = manifests.get(directory);
|
|
83
|
+
if (known)
|
|
84
|
+
return known;
|
|
85
|
+
const decoded = Schema.decodeUnknownSync(manifestSchema)(await readFile(join(root, directory, "package.json"), "utf8"));
|
|
86
|
+
manifests.set(directory, decoded);
|
|
87
|
+
return decoded;
|
|
88
|
+
};
|
|
89
|
+
const projects = new Map();
|
|
90
|
+
let rootProject = null;
|
|
91
|
+
for (const project of Schema.decodeUnknownSync(workspaceProjectsSchema)(options.projects)) {
|
|
92
|
+
const directory = within(root, project.path);
|
|
93
|
+
if (directory === "") {
|
|
94
|
+
rootProject = project.name;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (projects.has(project.name))
|
|
98
|
+
throw new Error(`Workspace package name repeats: ${project.name}`);
|
|
99
|
+
projects.set(project.name, directory);
|
|
100
|
+
}
|
|
101
|
+
const owners = [...projects]
|
|
102
|
+
.filter(([, directory]) => entry.startsWith(`${directory}/`))
|
|
103
|
+
.sort(([, left], [, right]) => right.length - left.length);
|
|
104
|
+
// A workspace member owns its entry; with none, an App's own repository
|
|
105
|
+
// (one package, the App itself at the root) owns it.
|
|
106
|
+
const owner = owners[0] ??
|
|
107
|
+
(projects.size === 0 && rootProject !== null
|
|
108
|
+
? [rootProject, ""]
|
|
109
|
+
: undefined);
|
|
110
|
+
if (!owner)
|
|
111
|
+
throw new Error(`No workspace package owns ${entry}`);
|
|
112
|
+
const [packageName, directory] = owner;
|
|
113
|
+
const parent = (path) => {
|
|
114
|
+
const up = dirname(path);
|
|
115
|
+
return up === "." ? "" : up;
|
|
116
|
+
};
|
|
117
|
+
let appDirectory = parent(entry);
|
|
118
|
+
while (appDirectory !== directory &&
|
|
119
|
+
!(await exists(join(root, appDirectory, "package.json"))))
|
|
120
|
+
appDirectory = parent(appDirectory);
|
|
121
|
+
const policy = policyOf(await manifest(appDirectory));
|
|
122
|
+
if (policy.script !== undefined)
|
|
123
|
+
throw new Error(`${appDirectory} is the App; its page builds with the recipe, not a script`);
|
|
124
|
+
const dependencies = [];
|
|
125
|
+
const visiting = new Set([directory]);
|
|
126
|
+
const visit = async (from) => {
|
|
127
|
+
for (const [name, range] of Object.entries((await manifest(from)).dependencies ?? {})) {
|
|
128
|
+
if (!range.startsWith("workspace:"))
|
|
129
|
+
continue;
|
|
130
|
+
const dependency = projects.get(name);
|
|
131
|
+
if (dependency === undefined)
|
|
132
|
+
throw new Error(`${from} depends on an unlisted workspace ${name}`);
|
|
133
|
+
if (dependencies.includes(dependency))
|
|
134
|
+
continue;
|
|
135
|
+
if (visiting.has(dependency))
|
|
136
|
+
throw new Error(`Workspace dependency cycle through ${name}`);
|
|
137
|
+
visiting.add(dependency);
|
|
138
|
+
await visit(dependency);
|
|
139
|
+
visiting.delete(dependency);
|
|
140
|
+
dependencies.push(dependency);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
await visit(directory);
|
|
144
|
+
const steps = [];
|
|
145
|
+
for (const dependency of dependencies) {
|
|
146
|
+
const declared = await manifest(dependency);
|
|
147
|
+
const own = policyOf(declared);
|
|
148
|
+
if (own.script !== undefined) {
|
|
149
|
+
if (own.typescript !== undefined)
|
|
150
|
+
throw new Error(`${dependency} declares both a build script and TypeScript configs`);
|
|
151
|
+
if (declared.scripts === undefined ||
|
|
152
|
+
!Object.hasOwn(declared.scripts, own.script))
|
|
153
|
+
throw new Error(`${dependency} has no script named ${own.script}`);
|
|
154
|
+
steps.push({ kind: "script", directory: dependency, script: own.script });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (await exists(join(root, dependency, "tsconfig.build.json")))
|
|
158
|
+
steps.push({
|
|
159
|
+
kind: "typescript",
|
|
160
|
+
config: `${dependency}/tsconfig.build.json`,
|
|
161
|
+
});
|
|
162
|
+
for (const config of own.typescript ?? [])
|
|
163
|
+
steps.push({ kind: "typescript", config: `${dependency}/${config}` });
|
|
164
|
+
}
|
|
165
|
+
const appManifest = await manifest(appDirectory);
|
|
166
|
+
for (const name of policy.packageStylesheets ?? []) {
|
|
167
|
+
const owner = projects.get(name);
|
|
168
|
+
// A package installed from a registry ships its built stylesheet; only a
|
|
169
|
+
// workspace package's stylesheet compiles here.
|
|
170
|
+
if (owner === undefined && appManifest.dependencies?.[name] !== undefined)
|
|
171
|
+
continue;
|
|
172
|
+
if (owner === undefined || !dependencies.includes(owner))
|
|
173
|
+
throw new Error(`Stylesheet package ${name} is not a workspace dependency`);
|
|
174
|
+
const declared = policyOf(await manifest(owner)).stylesheet;
|
|
175
|
+
if (!declared)
|
|
176
|
+
throw new Error(`${name} declares no stylesheet`);
|
|
177
|
+
steps.push({
|
|
178
|
+
kind: "tailwind",
|
|
179
|
+
input: `${owner}/${declared.input}`,
|
|
180
|
+
output: `${owner}/${declared.output}`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
for (const stylesheet of policy.stylesheets ?? []) {
|
|
184
|
+
const path = within(root, join(appDirectory, stylesheet));
|
|
185
|
+
steps.push({ kind: "tailwind", input: path, output: path });
|
|
186
|
+
}
|
|
187
|
+
const build = {
|
|
188
|
+
directory,
|
|
189
|
+
packageName,
|
|
190
|
+
react: policy.react === true,
|
|
191
|
+
dependencies: Object.freeze(dependencies),
|
|
192
|
+
steps: Object.freeze(steps),
|
|
193
|
+
};
|
|
194
|
+
if (policy.shellEntry === undefined)
|
|
195
|
+
return Object.freeze(build);
|
|
196
|
+
const shellEntry = relative(join(root, directory), join(root, appDirectory, policy.shellEntry))
|
|
197
|
+
.split(sep)
|
|
198
|
+
.join("/");
|
|
199
|
+
return Object.freeze({ ...build, shellEntry });
|
|
200
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Provider-neutral App build recipes and candidate orchestration. */
|
|
2
|
+
export * from "./build/effect.js";
|
|
3
|
+
/** Team App candidate wire contract: submission schema and codec. */
|
|
4
|
+
export * from "./build/submission-contract.js";
|
|
5
|
+
export { workspaceBuildPolicy } from "./build/build-policy.js";
|
|
6
|
+
/** A build's uploaded objects in an R2-shaped, content-addressed store. */
|
|
7
|
+
export * from "./build/content-objects.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Provider-neutral App build recipes and candidate orchestration. */
|
|
2
|
+
export * from "./build/effect.js";
|
|
3
|
+
/** Team App candidate wire contract: submission schema and codec. */
|
|
4
|
+
export * from "./build/submission-contract.js";
|
|
5
|
+
export { workspaceBuildPolicy } from "./build/build-policy.js";
|
|
6
|
+
/** A build's uploaded objects in an R2-shaped, content-addressed store. */
|
|
7
|
+
export * from "./build/content-objects.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type CandidateObject, type CandidateObjectMediaType, type TeamAppCandidateManifest } from "./build-contract.js";
|
|
2
|
+
/** The two facts a build job reports; a host's Team Git event is a superset
|
|
3
|
+
* that also carries pushes, so a host's own record callback is always
|
|
4
|
+
* assignable here. */
|
|
5
|
+
export type BuildGitEvent = Readonly<{
|
|
6
|
+
kind: "build-artifact";
|
|
7
|
+
jobId: string;
|
|
8
|
+
credentialId: string;
|
|
9
|
+
digest: string;
|
|
10
|
+
bytes: number;
|
|
11
|
+
}> | Readonly<{
|
|
12
|
+
kind: "build-status";
|
|
13
|
+
jobId: string;
|
|
14
|
+
credentialId: string;
|
|
15
|
+
status: "succeeded" | "failed";
|
|
16
|
+
detail: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
/** A Git principal admitted to read exactly one live build job's one
|
|
19
|
+
* repository: the job-scoped read clone. It never pushes, and its reads are
|
|
20
|
+
* never recorded as Team Git push events. */
|
|
21
|
+
export type BuildGitPrincipal = Readonly<{
|
|
22
|
+
kind: "build";
|
|
23
|
+
jobId: string;
|
|
24
|
+
credentialId: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** True when an admitted Git principal is a build job's job-scoped read
|
|
27
|
+
* credential rather than an Agent or a Workflow Git pass. A host's Git
|
|
28
|
+
* egress uses this to admit the job-scoped read clone while refusing any
|
|
29
|
+
* write and skipping push-event recording for it. Generic so a host's own
|
|
30
|
+
* wider principal union still narrows away the "build" member afterward,
|
|
31
|
+
* exactly as the literal `principal.kind === "build"` comparison it
|
|
32
|
+
* replaces did. */
|
|
33
|
+
export declare function isBuildGitPrincipal<P extends Readonly<{
|
|
34
|
+
kind: string;
|
|
35
|
+
}>>(principal: P): principal is Extract<P, Readonly<{
|
|
36
|
+
kind: "build";
|
|
37
|
+
}>>;
|
|
38
|
+
/**
|
|
39
|
+
* The build-report handler for `/jobs/:id/(candidate|artifact|status)` and
|
|
40
|
+
* `PUT /jobs/:id/objects/sha256-<hex>`. `admitBuild` is the host's own
|
|
41
|
+
* live-job lookup; `admitObject` admits one object upload for a live job's
|
|
42
|
+
* grant and answers its size bound; `storeObject` streams an admitted object
|
|
43
|
+
* into the host's content-addressed store, which verifies its digest;
|
|
44
|
+
* `submitCandidate` hands an admitted manifest to the host's Publisher
|
|
45
|
+
* custody; `record` delivers a reported fact to the host's own projection.
|
|
46
|
+
* `report` observes a body or JSON decode failure without changing its 400
|
|
47
|
+
* outcome; a host may wire its own structured logger or omit it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createBuildEgress(options: Readonly<{
|
|
50
|
+
admitBuild: (jobId: string) => Promise<Readonly<{
|
|
51
|
+
credentialId: string;
|
|
52
|
+
}> | null>;
|
|
53
|
+
admitObject: (jobId: string, grant: string) => Promise<Readonly<{
|
|
54
|
+
maxObjectBytes: number;
|
|
55
|
+
}> | null>;
|
|
56
|
+
storeObject: (object: CandidateObject, body: ReadableStream<Uint8Array> | null, mediaType: CandidateObjectMediaType) => Promise<"stored" | "refused">;
|
|
57
|
+
submitCandidate: (jobId: string, manifest: TeamAppCandidateManifest, grant: string, credentialId: string) => Promise<Response>;
|
|
58
|
+
record: (event: BuildGitEvent) => Promise<void>;
|
|
59
|
+
report?: (operation: string, cause: unknown) => void;
|
|
60
|
+
}>): Readonly<{
|
|
61
|
+
fetch(request: Request, url: URL): Promise<Response>;
|
|
62
|
+
}>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { teamAppCandidateManifestSchema, TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES, candidateObjectMediaType, candidateObjectSchema, } from "./build-contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* The Computer's own egress for its build job's `/jobs/:id/*` reports. A
|
|
6
|
+
* live build uploads each built object alone, streamed straight into the
|
|
7
|
+
* host's content-addressed store, then submits the small manifest naming
|
|
8
|
+
* them through Team submission custody, then reports bytes/status. Selection
|
|
9
|
+
* and promotion are never admitted here. The
|
|
10
|
+
* Computer presents no credential of its own: every decision is
|
|
11
|
+
* port-injected so a host (Hub today, Oyster's standalone host once it
|
|
12
|
+
* mounts a Computer build) supplies its own admission, submission and event
|
|
13
|
+
* recording without this module reaching into either host's state.
|
|
14
|
+
*/
|
|
15
|
+
const statusSchema = z.strictObject({
|
|
16
|
+
status: z.enum(["succeeded", "failed"]),
|
|
17
|
+
detail: z.string().max(2_000).optional(),
|
|
18
|
+
});
|
|
19
|
+
const artifactSchema = z.strictObject({
|
|
20
|
+
digest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
21
|
+
bytes: z.number().int().nonnegative(),
|
|
22
|
+
});
|
|
23
|
+
/** True when an admitted Git principal is a build job's job-scoped read
|
|
24
|
+
* credential rather than an Agent or a Workflow Git pass. A host's Git
|
|
25
|
+
* egress uses this to admit the job-scoped read clone while refusing any
|
|
26
|
+
* write and skipping push-event recording for it. Generic so a host's own
|
|
27
|
+
* wider principal union still narrows away the "build" member afterward,
|
|
28
|
+
* exactly as the literal `principal.kind === "build"` comparison it
|
|
29
|
+
* replaces did. */
|
|
30
|
+
export function isBuildGitPrincipal(principal) {
|
|
31
|
+
return principal.kind === "build";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The build-report handler for `/jobs/:id/(candidate|artifact|status)` and
|
|
35
|
+
* `PUT /jobs/:id/objects/sha256-<hex>`. `admitBuild` is the host's own
|
|
36
|
+
* live-job lookup; `admitObject` admits one object upload for a live job's
|
|
37
|
+
* grant and answers its size bound; `storeObject` streams an admitted object
|
|
38
|
+
* into the host's content-addressed store, which verifies its digest;
|
|
39
|
+
* `submitCandidate` hands an admitted manifest to the host's Publisher
|
|
40
|
+
* custody; `record` delivers a reported fact to the host's own projection.
|
|
41
|
+
* `report` observes a body or JSON decode failure without changing its 400
|
|
42
|
+
* outcome; a host may wire its own structured logger or omit it.
|
|
43
|
+
*/
|
|
44
|
+
export function createBuildEgress(options) {
|
|
45
|
+
const report = options.report ??
|
|
46
|
+
((operation, cause) => {
|
|
47
|
+
console.error(operation, cause);
|
|
48
|
+
});
|
|
49
|
+
const putObject = async (request, jobId, hex) => {
|
|
50
|
+
const admitted = await options.admitObject(jobId, request.headers.get("X-Team-App-Build") ?? "");
|
|
51
|
+
if (admitted === null)
|
|
52
|
+
return new Response(null, { status: 403 });
|
|
53
|
+
// The stated length bounds what the store takes before a byte moves.
|
|
54
|
+
const length = request.headers.get("Content-Length") ?? "";
|
|
55
|
+
if (!/^(0|[1-9][0-9]{0,14})$/u.test(length))
|
|
56
|
+
return new Response(null, { status: 411 });
|
|
57
|
+
const byteLength = Number(length);
|
|
58
|
+
if (byteLength > admitted.maxObjectBytes) {
|
|
59
|
+
await request.body?.cancel();
|
|
60
|
+
return new Response(null, { status: 413 });
|
|
61
|
+
}
|
|
62
|
+
const mediaType = candidateObjectMediaType(request.headers.get("Content-Type"));
|
|
63
|
+
if (mediaType === null) {
|
|
64
|
+
await request.body?.cancel();
|
|
65
|
+
return new Response(null, { status: 415 });
|
|
66
|
+
}
|
|
67
|
+
const stored = await options.storeObject(Schema.decodeUnknownSync(candidateObjectSchema)({
|
|
68
|
+
digest: `sha256:${hex}`,
|
|
69
|
+
byteLength,
|
|
70
|
+
}), request.body, mediaType);
|
|
71
|
+
return new Response(null, { status: stored === "stored" ? 204 : 400 });
|
|
72
|
+
};
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
async fetch(request, url) {
|
|
75
|
+
if (url.search !== "" || url.username || url.password)
|
|
76
|
+
return new Response(null, { status: 403 });
|
|
77
|
+
const object = /^\/jobs\/([a-zA-Z0-9_-]{1,128})\/objects\/sha256-([0-9a-f]{64})$/u.exec(url.pathname);
|
|
78
|
+
if (object !== null) {
|
|
79
|
+
const [, jobId = "", hex = ""] = object;
|
|
80
|
+
return request.method === "PUT"
|
|
81
|
+
? putObject(request, jobId, hex)
|
|
82
|
+
: new Response(null, { status: 403 });
|
|
83
|
+
}
|
|
84
|
+
const match = /^\/jobs\/([a-zA-Z0-9_-]{1,128})\/(status|artifact|candidate)$/u.exec(url.pathname);
|
|
85
|
+
const jobId = match?.[1];
|
|
86
|
+
const action = match?.[2];
|
|
87
|
+
if (jobId === undefined || request.method !== "POST")
|
|
88
|
+
return new Response(null, { status: 403 });
|
|
89
|
+
const job = await options.admitBuild(jobId);
|
|
90
|
+
if (job === null)
|
|
91
|
+
return new Response(null, { status: 403 });
|
|
92
|
+
if (action === "candidate") {
|
|
93
|
+
let manifest;
|
|
94
|
+
try {
|
|
95
|
+
if (request.body === null)
|
|
96
|
+
return new Response(null, { status: 400 });
|
|
97
|
+
let size = 0;
|
|
98
|
+
const bounded = request.body.pipeThrough(new TransformStream({
|
|
99
|
+
transform(chunk, controller) {
|
|
100
|
+
size += chunk.byteLength;
|
|
101
|
+
if (size > TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES)
|
|
102
|
+
throw new Error("candidate manifest oversize");
|
|
103
|
+
controller.enqueue(chunk);
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
manifest = Schema.decodeUnknownSync(teamAppCandidateManifestSchema, {
|
|
107
|
+
onExcessProperty: "error",
|
|
108
|
+
})(await new Response(bounded).json());
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return new Response(null, { status: 400 });
|
|
112
|
+
}
|
|
113
|
+
return options.submitCandidate(jobId, manifest, request.headers.get("X-Team-App-Build") ?? "", job.credentialId);
|
|
114
|
+
}
|
|
115
|
+
let json;
|
|
116
|
+
try {
|
|
117
|
+
json = await request.json();
|
|
118
|
+
}
|
|
119
|
+
catch (cause) {
|
|
120
|
+
report("packages/oyster/src/build-egress.ts:report", cause);
|
|
121
|
+
return new Response(null, { status: 400 });
|
|
122
|
+
}
|
|
123
|
+
if (action === "artifact") {
|
|
124
|
+
const artifact = artifactSchema.safeParse(json);
|
|
125
|
+
if (!artifact.success)
|
|
126
|
+
return new Response(null, { status: 400 });
|
|
127
|
+
await options.record({
|
|
128
|
+
kind: "build-artifact",
|
|
129
|
+
jobId,
|
|
130
|
+
credentialId: job.credentialId,
|
|
131
|
+
...artifact.data,
|
|
132
|
+
});
|
|
133
|
+
return new Response(null, { status: 204 });
|
|
134
|
+
}
|
|
135
|
+
const status = statusSchema.safeParse(json);
|
|
136
|
+
if (!status.success)
|
|
137
|
+
return new Response(null, { status: 400 });
|
|
138
|
+
await options.record({
|
|
139
|
+
kind: "build-status",
|
|
140
|
+
jobId,
|
|
141
|
+
credentialId: job.credentialId,
|
|
142
|
+
status: status.data.status,
|
|
143
|
+
detail: status.data.detail ?? null,
|
|
144
|
+
});
|
|
145
|
+
return new Response(null, { status: 204 });
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|