@fungi.computer/oyster 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1082 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +151 -0
- package/dist/app-builds.js +355 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +46 -0
- package/dist/app-sources.js +335 -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/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +197 -0
- package/dist/build/build-step.d.ts +26 -0
- package/dist/build/build-step.js +20 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +231 -0
- package/dist/build/effect.js +396 -0
- package/dist/build/git-environment.d.ts +6 -0
- package/dist/build/git-environment.js +48 -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 +57 -0
- package/dist/build/local-build.js +1078 -0
- package/dist/build/r2-artifact-writer.d.ts +31 -0
- package/dist/build/r2-artifact-writer.js +179 -0
- package/dist/build/snapshot-package-store.d.ts +2 -0
- package/dist/build/snapshot-package-store.js +21 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- package/dist/build/submission-contract.d.ts +384 -0
- package/dist/build/submission-contract.js +70 -0
- package/dist/build/workspace-build.d.ts +50 -0
- package/dist/build/workspace-build.js +156 -0
- package/dist/build-contract.d.ts +4 -0
- package/dist/build-contract.js +4 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +13 -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 +143 -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 +76 -0
- package/dist/pack.js +134 -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/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-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 +72 -0
- package/dist/release.js +164 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +537 -0
- package/dist/shell/app-directory.js +195 -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 +31 -0
- package/dist/shell/shell-module.js +37 -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-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 +11 -0
- package/dist/source/index.js +11 -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-repository.d.ts +2 -0
- package/dist/source/template-repository.js +8 -0
- package/dist/source/template-write.d.ts +69 -0
- package/dist/source/template-write.js +344 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -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 +866 -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 +507 -0
- package/docs/api/build.md +1476 -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 +42 -0
- package/docs/api/pack.md +138 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +332 -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 +143 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench.md +186 -0
- package/docs/api/shell.md +1788 -0
- package/docs/api/tenant.md +194 -0
- package/docs/api/workers-git.md +1527 -0
- package/docs/api/workers.md +1678 -0
- package/docs/media/architecture.mmd +32 -0
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/docs/site/agent-quickstart.mdx +349 -0
- package/examples/git/worker.ts +39 -0
- package/examples/git/wrangler.jsonc +32 -0
- package/examples/standalone/worker.ts +14 -0
- package/examples/standalone/wrangler.jsonc +21 -0
- package/package.json +249 -3
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { appBackendBindingOperation, } from "@fungi.computer/puffball/app";
|
|
2
|
+
import { sha256 } from "../release.js";
|
|
3
|
+
import canonicalJson from "fast-json-stable-stringify";
|
|
4
|
+
import { declaredOperationsSchema, } from "./installation.js";
|
|
5
|
+
/** Qualify backend operations so a same-named browser method grants no backend authority. */
|
|
6
|
+
export function bindingOperationKey(binding, operation) {
|
|
7
|
+
return `binding:${binding}:${operation}`;
|
|
8
|
+
}
|
|
9
|
+
/** Approval identity for one UI-side target method; upgrades intersect these exact pairs. */
|
|
10
|
+
export function appCallOperation(appId, method) {
|
|
11
|
+
return `route:apps.call:${appId}:${method}`;
|
|
12
|
+
}
|
|
13
|
+
/** Sole derivation owner. Inputs are the verified Release declaration and existing host adapters. */
|
|
14
|
+
export async function deriveDeclaredOperations(release, declaration, descriptors) {
|
|
15
|
+
if (descriptors.some((descriptor) => descriptor.id === "app.methods"))
|
|
16
|
+
throw new Error("app_methods_descriptor_reserved");
|
|
17
|
+
const capabilities = new Map(declaration.capabilities.map((capability) => [
|
|
18
|
+
capability,
|
|
19
|
+
new Set(),
|
|
20
|
+
]));
|
|
21
|
+
for (const call of declaration.calls ?? [])
|
|
22
|
+
for (const method of call.methods)
|
|
23
|
+
capabilities.get("apps.call")?.add(appCallOperation(call.appId, method));
|
|
24
|
+
for (const [name, method] of Object.entries(declaration.api?.methods ?? {}))
|
|
25
|
+
for (const capability of method.requires)
|
|
26
|
+
capabilities.get(capability)?.add(`route:app.methods:${name}`);
|
|
27
|
+
const routes = descriptors
|
|
28
|
+
.filter((descriptor) => capabilities.has(descriptor.permission))
|
|
29
|
+
.map((descriptor) => {
|
|
30
|
+
const methods = Object.entries(descriptor.methods).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
31
|
+
for (const [name] of methods)
|
|
32
|
+
capabilities
|
|
33
|
+
.get(descriptor.permission)
|
|
34
|
+
?.add(`route:${descriptor.id}:${name}`);
|
|
35
|
+
return { id: descriptor.id, permission: descriptor.permission, methods };
|
|
36
|
+
})
|
|
37
|
+
.sort((a, b) => a.id < b.id
|
|
38
|
+
? -1
|
|
39
|
+
: a.id > b.id
|
|
40
|
+
? 1
|
|
41
|
+
: a.permission < b.permission
|
|
42
|
+
? -1
|
|
43
|
+
: a.permission > b.permission
|
|
44
|
+
? 1
|
|
45
|
+
: 0);
|
|
46
|
+
const bindings = (declaration.backend?.bindings ?? [])
|
|
47
|
+
.flatMap((binding) => binding.operations.flatMap((name) => {
|
|
48
|
+
const operation = appBackendBindingOperation(binding.name, name);
|
|
49
|
+
if (!operation?.requires.every((capability) => capabilities.has(capability)))
|
|
50
|
+
return [];
|
|
51
|
+
for (const capability of operation.requires)
|
|
52
|
+
capabilities
|
|
53
|
+
.get(capability)
|
|
54
|
+
?.add(bindingOperationKey(binding.name, name));
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
binding: binding.name,
|
|
58
|
+
operation: name,
|
|
59
|
+
effect: operation.effect,
|
|
60
|
+
requires: [...operation.requires].sort(),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => bindingOperationKey(a.binding, a.operation) <
|
|
65
|
+
bindingOperationKey(b.binding, b.operation)
|
|
66
|
+
? -1
|
|
67
|
+
: bindingOperationKey(a.binding, a.operation) >
|
|
68
|
+
bindingOperationKey(b.binding, b.operation)
|
|
69
|
+
? 1
|
|
70
|
+
: 0);
|
|
71
|
+
const rows = [...capabilities]
|
|
72
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
73
|
+
.map(([capability, operations]) => ({
|
|
74
|
+
capability,
|
|
75
|
+
operations: [...operations].sort(),
|
|
76
|
+
}));
|
|
77
|
+
const artifactDigest = release.artifact.artifactDigest;
|
|
78
|
+
const anonymousMethods = await Promise.all(Object.entries(declaration.api?.methods ?? {})
|
|
79
|
+
.filter(([, method]) => method.anonymous === true)
|
|
80
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
81
|
+
.map(async ([name, method]) => ({
|
|
82
|
+
name,
|
|
83
|
+
digest: await sha256(new TextEncoder().encode(canonicalJson(method))),
|
|
84
|
+
})));
|
|
85
|
+
const digest = await sha256(new TextEncoder().encode(JSON.stringify({
|
|
86
|
+
artifactDigest,
|
|
87
|
+
capabilities: rows,
|
|
88
|
+
routes,
|
|
89
|
+
bindings,
|
|
90
|
+
anonymousMethods,
|
|
91
|
+
})));
|
|
92
|
+
return declaredOperationsSchema.parse({
|
|
93
|
+
artifactDigest,
|
|
94
|
+
digest,
|
|
95
|
+
capabilities: rows,
|
|
96
|
+
anonymousMethods,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { AppApi, AppDeclaration } from "@fungi.computer/puffball/app";
|
|
3
|
+
import type { Installation, DeclaredOperations } from "./installation.js";
|
|
4
|
+
/**
|
|
5
|
+
* One App-tool access level, with its own freshness pin. An Agent's comes
|
|
6
|
+
* from the owner's stored direct approval; a key's is computed fresh from
|
|
7
|
+
* its own Ear Tag grants (`keyAppAccess`), never stored as this shape --
|
|
8
|
+
* only read through it, for one call or one installation at a time.
|
|
9
|
+
*/
|
|
10
|
+
export declare const appToolPolicySchema: Schema.Struct<{
|
|
11
|
+
readonly level: Schema.Literals<readonly ["read", "write"]>;
|
|
12
|
+
readonly revision: Schema.NonEmptyString;
|
|
13
|
+
readonly grantedBy: Schema.NonEmptyString;
|
|
14
|
+
}>;
|
|
15
|
+
/** The authenticated attaching member is recorded when this policy is approved. */
|
|
16
|
+
export type AppToolPolicy = typeof appToolPolicySchema.Type;
|
|
17
|
+
/** One installation's binding under the captured host approval. */
|
|
18
|
+
export declare const appToolAccessSchema: Schema.Struct<{
|
|
19
|
+
readonly bindingName: Schema.NonEmptyString;
|
|
20
|
+
readonly level: Schema.Literals<readonly ["read", "write"]>;
|
|
21
|
+
readonly revision: Schema.NonEmptyString;
|
|
22
|
+
readonly grantedBy: Schema.NonEmptyString;
|
|
23
|
+
}>;
|
|
24
|
+
/** One installation binding constrained by an authenticated tool-policy approval. */
|
|
25
|
+
export type AppToolAccess = typeof appToolAccessSchema.Type;
|
|
26
|
+
/** Pre-read entries: the catalog never resolves installations or calls back into a host. */
|
|
27
|
+
export type AppToolEntry = Readonly<{
|
|
28
|
+
installation: Installation;
|
|
29
|
+
declaration: AppDeclaration;
|
|
30
|
+
declaredOperations: DeclaredOperations;
|
|
31
|
+
api: AppApi | null;
|
|
32
|
+
access: AppToolAccess | null;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/**
|
|
3
|
+
* One App-tool access level, with its own freshness pin. An Agent's comes
|
|
4
|
+
* from the owner's stored direct approval; a key's is computed fresh from
|
|
5
|
+
* its own Ear Tag grants (`keyAppAccess`), never stored as this shape --
|
|
6
|
+
* only read through it, for one call or one installation at a time.
|
|
7
|
+
*/
|
|
8
|
+
export const appToolPolicySchema = Schema.Struct({
|
|
9
|
+
level: Schema.Literals(["read", "write"]),
|
|
10
|
+
revision: Schema.NonEmptyString,
|
|
11
|
+
grantedBy: Schema.NonEmptyString,
|
|
12
|
+
});
|
|
13
|
+
/** One installation's binding under the captured host approval. */
|
|
14
|
+
export const appToolAccessSchema = Schema.Struct({
|
|
15
|
+
...appToolPolicySchema.fields,
|
|
16
|
+
bindingName: Schema.NonEmptyString,
|
|
17
|
+
});
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OysterAuth } from "./host.js";
|
|
2
|
+
import type { OysterKeyStore } from "./keys.js";
|
|
3
|
+
/** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
|
|
4
|
+
export declare const ownerKeyCookieName = "oyster_session";
|
|
5
|
+
/**
|
|
6
|
+
* Single-owner sign-in: a bearer token or a session cookie holding the
|
|
7
|
+
* owner key's digest. The key itself is never stored in the browser.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ownerKeyAuth(ownerKey: string, ownerId?: string): OysterAuth;
|
|
10
|
+
/** The owner key, or a named key admitted as its canonical Team-key actor. */
|
|
11
|
+
export declare function ownerOrKeyAuth(ownerKey: string, ownerId: string, keys: () => Pick<OysterKeyStore, "verify">): OysterAuth;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { presentedGitKey } from "./git-auth.js";
|
|
4
|
+
import { sha256 } from "./release.js";
|
|
5
|
+
/** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
|
|
6
|
+
export const ownerKeyCookieName = "oyster_session";
|
|
7
|
+
const loginFormSchema = Schema.Struct({ key: Schema.String });
|
|
8
|
+
/** Compare two secrets through their fixed-length digests, in constant time. */
|
|
9
|
+
async function constantTimeEqual(a, b) {
|
|
10
|
+
const [left, right] = await Promise.all([
|
|
11
|
+
sha256(new TextEncoder().encode(a)),
|
|
12
|
+
sha256(new TextEncoder().encode(b)),
|
|
13
|
+
]);
|
|
14
|
+
let difference = 0;
|
|
15
|
+
for (let index = 0; index < left.length; index += 1)
|
|
16
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
17
|
+
return difference === 0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Single-owner sign-in: a bearer token or a session cookie holding the
|
|
21
|
+
* owner key's digest. The key itself is never stored in the browser.
|
|
22
|
+
*/
|
|
23
|
+
export function ownerKeyAuth(ownerKey, ownerId = "owner") {
|
|
24
|
+
if (ownerKey.length < 24)
|
|
25
|
+
throw new Error("OYSTER_OWNER_KEY must be at least 24 characters");
|
|
26
|
+
const sessionValue = sha256(new TextEncoder().encode(`oyster:${ownerKey}`));
|
|
27
|
+
const owner = {
|
|
28
|
+
kind: "admitted",
|
|
29
|
+
principal: { actor: { kind: "member", memberId: ownerId }, role: "owner" },
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
resolve: (actor) => Promise.resolve(actor.kind === "member" && actor.memberId === ownerId
|
|
33
|
+
? owner.principal
|
|
34
|
+
: null),
|
|
35
|
+
async authenticate(request) {
|
|
36
|
+
const authorization = request.headers.get("Authorization");
|
|
37
|
+
if (authorization?.startsWith("Bearer ") === true)
|
|
38
|
+
return (await constantTimeEqual(authorization.slice(7), ownerKey))
|
|
39
|
+
? owner
|
|
40
|
+
: { kind: "refused", status: 401 };
|
|
41
|
+
const cookie = (request.headers.get("Cookie") ?? "")
|
|
42
|
+
.split(";")
|
|
43
|
+
.map((part) => part.trim())
|
|
44
|
+
.find((part) => part.startsWith(`${ownerKeyCookieName}=`));
|
|
45
|
+
if (cookie !== undefined &&
|
|
46
|
+
(await constantTimeEqual(cookie.slice(ownerKeyCookieName.length + 1), await sessionValue)))
|
|
47
|
+
return owner;
|
|
48
|
+
return { kind: "refused", status: 401 };
|
|
49
|
+
},
|
|
50
|
+
async login(request) {
|
|
51
|
+
const form = Schema.decodeUnknownOption(loginFormSchema)(Object.fromEntries(await request.formData()));
|
|
52
|
+
if (Option.isNone(form) ||
|
|
53
|
+
!(await constantTimeEqual(form.value.key, ownerKey)))
|
|
54
|
+
return new Response("Wrong key", { status: 401 });
|
|
55
|
+
return new Response(null, {
|
|
56
|
+
status: 303,
|
|
57
|
+
headers: {
|
|
58
|
+
Location: "/",
|
|
59
|
+
"Set-Cookie": `${ownerKeyCookieName}=${await sessionValue}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000`,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The owner key, or a named key admitted as its canonical Team-key actor. */
|
|
66
|
+
export function ownerOrKeyAuth(ownerKey, ownerId, keys) {
|
|
67
|
+
const owner = ownerKeyAuth(ownerKey, ownerId);
|
|
68
|
+
return {
|
|
69
|
+
resolve: (actor) => owner.resolve(actor),
|
|
70
|
+
async authenticate(request) {
|
|
71
|
+
const result = await owner.authenticate(request);
|
|
72
|
+
if (result.kind === "admitted")
|
|
73
|
+
return result;
|
|
74
|
+
const presented = presentedGitKey(request);
|
|
75
|
+
if (presented.kind !== "key")
|
|
76
|
+
return result;
|
|
77
|
+
const verified = await keys().verify(presented.value);
|
|
78
|
+
// A revoked or expired key, or one belonging to a different owner (a
|
|
79
|
+
// presented secret can never prove that on its own), authenticates as
|
|
80
|
+
// no one; the owner-key result above still stands.
|
|
81
|
+
return verified.kind === "ok" && verified.value.owner === ownerId
|
|
82
|
+
? {
|
|
83
|
+
kind: "admitted",
|
|
84
|
+
principal: {
|
|
85
|
+
actor: { kind: "team-key", keyId: verified.value.keyId },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
: result;
|
|
89
|
+
},
|
|
90
|
+
login: (request) => owner.login?.(request) ??
|
|
91
|
+
Promise.resolve(new Response("Sign-in is not available", { status: 404 })),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Second boundary before an admitted request enters customer backend code.
|
|
2
|
+
* Hub normally builds this shape itself; callers of the App runtime must not
|
|
3
|
+
* be able to pass their own headers through the facet.
|
|
4
|
+
*/
|
|
5
|
+
import { appBackendActorHeader, appBackendComputerHeader, } from "@fungi.computer/agar/backend";
|
|
6
|
+
export function appFacetRequest(request) {
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
for (const name of [
|
|
9
|
+
"Content-Type",
|
|
10
|
+
appBackendActorHeader,
|
|
11
|
+
appBackendComputerHeader,
|
|
12
|
+
]) {
|
|
13
|
+
const value = request.headers.get(name);
|
|
14
|
+
if (value !== null)
|
|
15
|
+
headers.set(name, value);
|
|
16
|
+
}
|
|
17
|
+
const init = {
|
|
18
|
+
method: request.method,
|
|
19
|
+
headers,
|
|
20
|
+
body: request.body,
|
|
21
|
+
redirect: "manual",
|
|
22
|
+
};
|
|
23
|
+
if (request.body !== null)
|
|
24
|
+
init.duplex = "half";
|
|
25
|
+
return new Request(request.url, init);
|
|
26
|
+
}
|
|
27
|
+
/** Only content metadata crosses back from a guest; origin policy is host-owned. */
|
|
28
|
+
export function appFacetResponse(response) {
|
|
29
|
+
const headers = new Headers();
|
|
30
|
+
for (const name of [
|
|
31
|
+
"Content-Type",
|
|
32
|
+
"Content-Language",
|
|
33
|
+
"Content-Encoding",
|
|
34
|
+
"Content-Length",
|
|
35
|
+
"Content-Disposition",
|
|
36
|
+
"Content-Range",
|
|
37
|
+
"Accept-Ranges",
|
|
38
|
+
"ETag",
|
|
39
|
+
"Last-Modified",
|
|
40
|
+
]) {
|
|
41
|
+
const value = response.headers.get(name);
|
|
42
|
+
if (value !== null)
|
|
43
|
+
headers.set(name, value);
|
|
44
|
+
}
|
|
45
|
+
headers.set("Cache-Control", "no-store");
|
|
46
|
+
headers.set("Content-Security-Policy", "sandbox; default-src 'none'");
|
|
47
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
48
|
+
return new Response(response.body, {
|
|
49
|
+
status: response.status,
|
|
50
|
+
statusText: response.statusText,
|
|
51
|
+
headers,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { Sha256Digest } from "@fungi.computer/puffball";
|
|
3
|
+
import type { PinnedRecipe } from "./effect.js";
|
|
4
|
+
/** The secret-free Team job; no provider token enters publication. */
|
|
5
|
+
declare const jobSchema: Schema.Struct<{
|
|
6
|
+
readonly jobId: Schema.NonEmptyString;
|
|
7
|
+
readonly computerId: Schema.NonEmptyString;
|
|
8
|
+
readonly credentialId: Schema.NonEmptyString;
|
|
9
|
+
readonly issuedAt: Schema.Number;
|
|
10
|
+
readonly repository: Schema.NonEmptyString;
|
|
11
|
+
readonly expiresAt: Schema.Number;
|
|
12
|
+
readonly remote: Schema.NonEmptyString;
|
|
13
|
+
readonly reportUrl: Schema.NonEmptyString;
|
|
14
|
+
readonly status: Schema.Literals<readonly ["running", "succeeded", "failed", "revoked"]>;
|
|
15
|
+
readonly artifact: Schema.NullOr<Schema.Struct<{
|
|
16
|
+
readonly digest: Schema.NonEmptyString;
|
|
17
|
+
readonly bytes: Schema.Number;
|
|
18
|
+
}>>;
|
|
19
|
+
readonly detail: Schema.NullOr<Schema.String>;
|
|
20
|
+
}>;
|
|
21
|
+
export type TeamBuildJob = Schema.Schema.Type<typeof jobSchema>;
|
|
22
|
+
export type BuildCredential = Pick<TeamBuildJob, "jobId" | "computerId" | "credentialId" | "repository" | "issuedAt" | "expiresAt" | "remote" | "reportUrl">;
|
|
23
|
+
declare const executionSchema: Schema.Struct<{
|
|
24
|
+
readonly output: Schema.optional<Schema.NullOr<Schema.Struct<{
|
|
25
|
+
readonly exitCode: Schema.NullOr<Schema.Number>;
|
|
26
|
+
readonly stderr: Schema.optional<Schema.String>;
|
|
27
|
+
}>>>;
|
|
28
|
+
}>;
|
|
29
|
+
export type TeamBuildComputer = Readonly<{
|
|
30
|
+
applyBuildDecision: (decision: {
|
|
31
|
+
kind: "build";
|
|
32
|
+
schemaVersion: 1;
|
|
33
|
+
teamId: string;
|
|
34
|
+
computerId: string;
|
|
35
|
+
jobId: string;
|
|
36
|
+
credentialId: string;
|
|
37
|
+
operations: readonly ["computer.execute"];
|
|
38
|
+
sequence: number;
|
|
39
|
+
issuedAt: number;
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
status: "allowed" | "revoked";
|
|
42
|
+
basis: {
|
|
43
|
+
credentialExpiresAt: number;
|
|
44
|
+
};
|
|
45
|
+
}) => Promise<void>;
|
|
46
|
+
execute: (input: {
|
|
47
|
+
teamId: string;
|
|
48
|
+
computerId: string;
|
|
49
|
+
jobId: string;
|
|
50
|
+
credentialId: string;
|
|
51
|
+
command: string;
|
|
52
|
+
timeoutMs: number;
|
|
53
|
+
}) => Promise<Schema.Schema.Type<typeof executionSchema>>;
|
|
54
|
+
deliverTeamGitEvents: () => Promise<number>;
|
|
55
|
+
}>;
|
|
56
|
+
/** Publisher executes builds; Team retains job authority, Computer runs them. */
|
|
57
|
+
export declare function createTeamBuildPublisher(options: Readonly<{
|
|
58
|
+
team: Readonly<{
|
|
59
|
+
mintTeamBuildCredential: (input: {
|
|
60
|
+
jobId: string;
|
|
61
|
+
repository: string;
|
|
62
|
+
computerId: string;
|
|
63
|
+
ttlMs: number;
|
|
64
|
+
}) => Promise<BuildCredential>;
|
|
65
|
+
revokeTeamBuild: (jobId: string) => Promise<TeamBuildJob>;
|
|
66
|
+
/** One Team-owned door checks Computer ownership and settles its job. */
|
|
67
|
+
build: () => Promise<Readonly<{
|
|
68
|
+
prepare: (jobId: string) => Promise<TeamBuildJob>;
|
|
69
|
+
read: (jobId: string) => Promise<TeamBuildJob | null>;
|
|
70
|
+
complete: (jobId: string, status: "succeeded" | "failed", detail: string) => Promise<TeamBuildJob>;
|
|
71
|
+
deliverProjection: () => Promise<void>;
|
|
72
|
+
[Symbol.dispose]: () => void;
|
|
73
|
+
}>>;
|
|
74
|
+
}>;
|
|
75
|
+
computer: (computerId: string) => TeamBuildComputer;
|
|
76
|
+
now?: () => number;
|
|
77
|
+
teamId: string;
|
|
78
|
+
maxPermissionAgeMs?: number;
|
|
79
|
+
}>): Readonly<{
|
|
80
|
+
mint(input: Parameters<typeof options.team.mintTeamBuildCredential>[0]): Promise<BuildCredential>;
|
|
81
|
+
read(jobId: string): Promise<{
|
|
82
|
+
readonly jobId: string;
|
|
83
|
+
readonly computerId: string;
|
|
84
|
+
readonly credentialId: string;
|
|
85
|
+
readonly issuedAt: number;
|
|
86
|
+
readonly repository: string;
|
|
87
|
+
readonly expiresAt: number;
|
|
88
|
+
readonly remote: string;
|
|
89
|
+
readonly reportUrl: string;
|
|
90
|
+
readonly status: "revoked" | "running" | "failed" | "succeeded";
|
|
91
|
+
readonly artifact: {
|
|
92
|
+
readonly digest: string;
|
|
93
|
+
readonly bytes: number;
|
|
94
|
+
} | null;
|
|
95
|
+
readonly detail: string | null;
|
|
96
|
+
} | null>;
|
|
97
|
+
revoke(jobId: string): Promise<{
|
|
98
|
+
readonly jobId: string;
|
|
99
|
+
readonly computerId: string;
|
|
100
|
+
readonly credentialId: string;
|
|
101
|
+
readonly issuedAt: number;
|
|
102
|
+
readonly repository: string;
|
|
103
|
+
readonly expiresAt: number;
|
|
104
|
+
readonly remote: string;
|
|
105
|
+
readonly reportUrl: string;
|
|
106
|
+
readonly status: "revoked" | "running" | "failed" | "succeeded";
|
|
107
|
+
readonly artifact: {
|
|
108
|
+
readonly digest: string;
|
|
109
|
+
readonly bytes: number;
|
|
110
|
+
} | null;
|
|
111
|
+
readonly detail: string | null;
|
|
112
|
+
}>;
|
|
113
|
+
run(input: {
|
|
114
|
+
jobId: string;
|
|
115
|
+
command: string;
|
|
116
|
+
timeoutMs: number;
|
|
117
|
+
}): Promise<TeamBuildJob>;
|
|
118
|
+
}>;
|
|
119
|
+
/** Fixed captured builder command; neither source nor the customer supplies shell code. */
|
|
120
|
+
export declare function teamAppBuildCommand(configuration: Readonly<{
|
|
121
|
+
appId: string;
|
|
122
|
+
sourceRevision: string;
|
|
123
|
+
source: Readonly<{
|
|
124
|
+
recipe: PinnedRecipe;
|
|
125
|
+
buildRecipeDigest: Sha256Digest;
|
|
126
|
+
}>;
|
|
127
|
+
}>, uploadGrant: string): string;
|
|
128
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import * as Schema from "effect/Schema";
|
|
54
|
+
import { base64url } from "jose";
|
|
55
|
+
/** The secret-free Team job; no provider token enters publication. */
|
|
56
|
+
const jobSchema = Schema.Struct({
|
|
57
|
+
jobId: Schema.NonEmptyString,
|
|
58
|
+
computerId: Schema.NonEmptyString,
|
|
59
|
+
credentialId: Schema.NonEmptyString,
|
|
60
|
+
issuedAt: Schema.Number,
|
|
61
|
+
repository: Schema.NonEmptyString,
|
|
62
|
+
expiresAt: Schema.Number,
|
|
63
|
+
remote: Schema.NonEmptyString,
|
|
64
|
+
reportUrl: Schema.NonEmptyString,
|
|
65
|
+
status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
|
|
66
|
+
artifact: Schema.NullOr(Schema.Struct({ digest: Schema.NonEmptyString, bytes: Schema.Number })),
|
|
67
|
+
detail: Schema.NullOr(Schema.String),
|
|
68
|
+
});
|
|
69
|
+
const executionSchema = Schema.Struct({
|
|
70
|
+
output: Schema.optional(Schema.NullOr(Schema.Struct({
|
|
71
|
+
exitCode: Schema.NullOr(Schema.Number),
|
|
72
|
+
stderr: Schema.optional(Schema.String),
|
|
73
|
+
}))),
|
|
74
|
+
});
|
|
75
|
+
/** Publisher executes builds; Team retains job authority, Computer runs them. */
|
|
76
|
+
export function createTeamBuildPublisher(options) {
|
|
77
|
+
const now = options.now ?? Date.now;
|
|
78
|
+
const maxPermissionAgeMs = options.maxPermissionAgeMs ?? 5 * 60_000;
|
|
79
|
+
const decodeJob = Schema.decodeUnknownSync(jobSchema);
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
mint(input) {
|
|
82
|
+
return options.team.mintTeamBuildCredential(input);
|
|
83
|
+
},
|
|
84
|
+
async read(jobId) {
|
|
85
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
86
|
+
try {
|
|
87
|
+
const build = __addDisposableResource(env_1, await options.team.build(), false);
|
|
88
|
+
return await build.read(jobId);
|
|
89
|
+
}
|
|
90
|
+
catch (e_1) {
|
|
91
|
+
env_1.error = e_1;
|
|
92
|
+
env_1.hasError = true;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
__disposeResources(env_1);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
revoke(jobId) {
|
|
99
|
+
return options.team.revokeTeamBuild(jobId);
|
|
100
|
+
},
|
|
101
|
+
async run(input) {
|
|
102
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
103
|
+
try {
|
|
104
|
+
const build = __addDisposableResource(env_2, await options.team.build(), false);
|
|
105
|
+
const job = decodeJob(await build.prepare(input.jobId));
|
|
106
|
+
const issuedAt = now();
|
|
107
|
+
if (job.status !== "running" || job.expiresAt <= issuedAt)
|
|
108
|
+
throw new Error("Team build expired");
|
|
109
|
+
const computer = options.computer(job.computerId);
|
|
110
|
+
const decision = {
|
|
111
|
+
kind: "build",
|
|
112
|
+
schemaVersion: 1,
|
|
113
|
+
teamId: options.teamId,
|
|
114
|
+
computerId: job.computerId,
|
|
115
|
+
jobId: job.jobId,
|
|
116
|
+
credentialId: job.credentialId,
|
|
117
|
+
operations: ["computer.execute"],
|
|
118
|
+
sequence: 1,
|
|
119
|
+
issuedAt,
|
|
120
|
+
expiresAt: Math.min(job.expiresAt, issuedAt + maxPermissionAgeMs),
|
|
121
|
+
status: "allowed",
|
|
122
|
+
basis: { credentialExpiresAt: job.expiresAt },
|
|
123
|
+
};
|
|
124
|
+
await computer.applyBuildDecision(decision);
|
|
125
|
+
try {
|
|
126
|
+
const result = Schema.decodeUnknownSync(executionSchema)(await computer.execute({
|
|
127
|
+
teamId: options.teamId,
|
|
128
|
+
computerId: job.computerId,
|
|
129
|
+
jobId: job.jobId,
|
|
130
|
+
credentialId: job.credentialId,
|
|
131
|
+
command: "export TEAM_BUILD_REMOTE=" +
|
|
132
|
+
job.remote +
|
|
133
|
+
" TEAM_BUILD_REPORT=" +
|
|
134
|
+
job.reportUrl +
|
|
135
|
+
"\n" +
|
|
136
|
+
input.command,
|
|
137
|
+
timeoutMs: input.timeoutMs,
|
|
138
|
+
}));
|
|
139
|
+
await computer.deliverTeamGitEvents();
|
|
140
|
+
const current = decodeJob(await build.read(input.jobId));
|
|
141
|
+
if (current.status !== "running") {
|
|
142
|
+
await build.deliverProjection();
|
|
143
|
+
return current;
|
|
144
|
+
}
|
|
145
|
+
const exitCode = result.output?.exitCode ?? null;
|
|
146
|
+
const completed = decodeJob(await build.complete(job.jobId, exitCode === 0 && current.artifact !== null
|
|
147
|
+
? "succeeded"
|
|
148
|
+
: "failed", exitCode === 0
|
|
149
|
+
? "Build exited without reporting status"
|
|
150
|
+
: "Build exited with " +
|
|
151
|
+
String(exitCode) +
|
|
152
|
+
": " +
|
|
153
|
+
(result.output?.stderr ?? "").slice(-1_000)));
|
|
154
|
+
await build.deliverProjection();
|
|
155
|
+
return completed;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
await computer.applyBuildDecision({
|
|
159
|
+
...decision,
|
|
160
|
+
sequence: 2,
|
|
161
|
+
status: "revoked",
|
|
162
|
+
issuedAt: Math.min(now(), decision.expiresAt - 1),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e_2) {
|
|
167
|
+
env_2.error = e_2;
|
|
168
|
+
env_2.hasError = true;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
__disposeResources(env_2);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Fixed captured builder command; neither source nor the customer supplies shell code. */
|
|
177
|
+
export function teamAppBuildCommand(configuration, uploadGrant) {
|
|
178
|
+
const encoded = base64url.encode(JSON.stringify({
|
|
179
|
+
uploadGrant,
|
|
180
|
+
appId: configuration.appId,
|
|
181
|
+
sourceRevision: configuration.sourceRevision,
|
|
182
|
+
recipe: configuration.source.recipe,
|
|
183
|
+
buildRecipeDigest: configuration.source.buildRecipeDigest,
|
|
184
|
+
}));
|
|
185
|
+
return [
|
|
186
|
+
"set -eu",
|
|
187
|
+
// Build scratch (clone, snapshot, installed packages) lives on the
|
|
188
|
+
// Computer's disk: an on-box VM's /tmp is RAM-backed tmpfs, which a
|
|
189
|
+
// workspace install fills while the compiler needs that memory.
|
|
190
|
+
"work=$(mktemp -d /var/tmp/fungi-app-build-XXXXXXXX)",
|
|
191
|
+
`trap 'rm -rf "$work"' EXIT`,
|
|
192
|
+
'mkdir "$work/tmp"',
|
|
193
|
+
'export TMPDIR="$work/tmp"',
|
|
194
|
+
'git clone -q --no-checkout -- "$TEAM_BUILD_REMOTE" "$work/source" 2>/dev/null',
|
|
195
|
+
`node /opt/fungi/team-candidate-builder.mjs '${encoded}' "$work/source" "$work/candidate.json"`,
|
|
196
|
+
].join("\n");
|
|
197
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/**
|
|
3
|
+
* One captured dependency build step. Paths are relative to the captured
|
|
4
|
+
* snapshot root. A TypeScript step compiles one config; a Tailwind step
|
|
5
|
+
* compiles one stylesheet input into its output (the same path for an App's
|
|
6
|
+
* own stylesheet, which compiles in place).
|
|
7
|
+
*/
|
|
8
|
+
export declare const workspaceBuildStepSchema: Schema.Union<readonly [Schema.Struct<{
|
|
9
|
+
readonly kind: Schema.Literal<"typescript">;
|
|
10
|
+
readonly config: Schema.String;
|
|
11
|
+
}>, Schema.Struct<{
|
|
12
|
+
readonly kind: Schema.Literal<"tailwind">;
|
|
13
|
+
readonly input: Schema.String;
|
|
14
|
+
readonly output: Schema.String;
|
|
15
|
+
}>]>;
|
|
16
|
+
/** One captured dependency build step. */
|
|
17
|
+
export type WorkspaceBuildStep = Schema.Schema.Type<typeof workspaceBuildStepSchema>;
|
|
18
|
+
/** The steps one dependency-build process runs, encoded as one argument. */
|
|
19
|
+
export declare const workspaceBuildStepsJsonSchema: Schema.fromJsonString<Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
20
|
+
readonly kind: Schema.Literal<"typescript">;
|
|
21
|
+
readonly config: Schema.String;
|
|
22
|
+
}>, Schema.Struct<{
|
|
23
|
+
readonly kind: Schema.Literal<"tailwind">;
|
|
24
|
+
readonly input: Schema.String;
|
|
25
|
+
readonly output: Schema.String;
|
|
26
|
+
}>]>>>;
|