cf-orbs 0.1.1 → 0.2.2

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.
@@ -0,0 +1,194 @@
1
+ // Checks on the project files: image pins, debugExec, secrets in tracked files, git mode files.
2
+ // Used by doctor (all modes) and by init --no-deploy before its first commit. Read-only.
3
+ import { existsSync } from "node:fs";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { CONFIG_FILE } from "./config.js";
7
+ import { CliError, toFailedLine } from "./errors.js";
8
+ import { readControlToken, templatesDir } from "./project.js";
9
+ import { WRANGLER_PIN } from "./wrangler.js";
10
+ const pass = (name, detail) => ({ name, status: "PASS", detail });
11
+ const failed = (name, e) => ({ name, status: "FAIL", detail: toFailedLine(e) });
12
+ const skip = (name, detail) => ({ name, status: "SKIP", detail });
13
+ const DIGEST_REF = /@sha256:[0-9a-f]{64}$/;
14
+ /** Base image refs in a Dockerfile that have no @sha256 digest. Earlier stage names and scratch are fine. */
15
+ export function unpinnedFroms(dockerfile) {
16
+ const stages = new Set(["scratch"]);
17
+ const out = [];
18
+ for (const line of dockerfile.split(/\r?\n/)) {
19
+ const m = /^\s*FROM\s+(.+)$/i.exec(line);
20
+ if (!m)
21
+ continue;
22
+ const words = m[1].trim().split(/\s+/);
23
+ const ref = words.find((w) => !w.startsWith("--"));
24
+ if (!ref)
25
+ continue;
26
+ const known = stages.has(ref.toLowerCase());
27
+ const as = words.findIndex((w) => w.toLowerCase() === "as");
28
+ if (as !== -1 && words[as + 1])
29
+ stages.add(words[as + 1].toLowerCase());
30
+ if (!known && !DIGEST_REF.test(ref))
31
+ out.push(ref);
32
+ }
33
+ return out;
34
+ }
35
+ /** "<repo>@sha256:0123456789ab" for a PASS line. */
36
+ function shortRefs(dockerfile) {
37
+ const refs = [...dockerfile.matchAll(/^\s*FROM\s+(?:--\S+\s+)*(\S+)/gim)].map((m) => m[1].replace(/@sha256:([0-9a-f]{12})[0-9a-f]{52}$/, "@sha256:$1…"));
38
+ return refs.join(", ");
39
+ }
40
+ async function pinCheck(name, file, label, fix) {
41
+ let text;
42
+ try {
43
+ text = await readFile(file, "utf8");
44
+ }
45
+ catch {
46
+ return failed(name, new CliError(`${label} not found`, "run cf-orbs init in this directory"));
47
+ }
48
+ const bad = unpinnedFroms(text);
49
+ if (bad.length > 0)
50
+ return failed(name, new CliError(`${label} FROM ${bad.join(", ")} has no @sha256 digest`, fix));
51
+ return pass(name, `${label} FROM ${shortRefs(text)} (by digest)`);
52
+ }
53
+ /** image/Dockerfile and the Dockerfile of every extra image key: each FROM is pinned by digest. */
54
+ export async function orbImagePinCheck(p, c) {
55
+ const keys = [...new Set(Object.values(c?.images ?? {}))];
56
+ const files = [
57
+ ["image/Dockerfile", p.imageDockerfile],
58
+ ...keys.map((k) => [`image/${k}.Dockerfile`, join(p.imageDir, `${k}.Dockerfile`)]),
59
+ ];
60
+ const fix = "pin the base image as <image>@sha256:<digest> (for the Amp image: cf-orbs upgrade --digest sha256:<hex>)";
61
+ let first;
62
+ let extra = 0;
63
+ for (const [label, file] of files) {
64
+ if (label !== "image/Dockerfile" && !existsSync(file))
65
+ continue; // deploy reports a missing one
66
+ const c1 = await pinCheck("orb image pin", file, label, fix);
67
+ if (c1.status === "FAIL")
68
+ return c1;
69
+ if (first)
70
+ extra++;
71
+ first ??= c1;
72
+ }
73
+ return extra > 0 && first
74
+ ? { ...first, detail: `${first.detail}; ${extra} extra image key(s) pinned too` }
75
+ : first;
76
+ }
77
+ /** The provider image cf-orbs builds: the project copy (.cf-orbs/provider) if there is one, else the template. */
78
+ export async function providerImagePinCheck(p, templates = templatesDir()) {
79
+ const copy = join(p.stateDir, "provider", "Dockerfile");
80
+ const tpl = join(templates, "provider", "Dockerfile");
81
+ // In a source checkout the templates are synced at build time; before that, read the source file.
82
+ const src = join(templates, "..", "..", "provider", "Dockerfile");
83
+ const [file, label] = existsSync(copy)
84
+ ? [copy, ".cf-orbs/provider/Dockerfile"]
85
+ : [existsSync(tpl) || !existsSync(src) ? tpl : src, "provider/Dockerfile (CLI template)"];
86
+ return pinCheck("provider image pin", file, label, "reinstall cf-orbs (the provider image is a CLI template), then cf-orbs deploy");
87
+ }
88
+ /** debugExec lets the control token run commands in orbs. WARN, never FAIL: the substrate bench needs it. */
89
+ export function debugExecCheck(c, git) {
90
+ if (!c)
91
+ return skip("debugExec", "needs a valid config");
92
+ if (!c.debugExec)
93
+ return pass("debugExec", "false (the control token cannot run commands in orbs)");
94
+ const then = git ? "commit and push" : "run cf-orbs deploy";
95
+ return {
96
+ name: "debugExec",
97
+ status: "WARN",
98
+ detail: toFailedLine(new CliError("debugExec is true: whoever has the control token can run any command in any orb", `set "debugExec": false in ${CONFIG_FILE}, then ${then} (keep it on only for the substrate bench)`)),
99
+ };
100
+ }
101
+ const AOP_TOKEN = /aop_live_[A-Za-z0-9_-]{8,}/;
102
+ const MAX_SCAN_BYTES = 10_000_000;
103
+ /**
104
+ * Files (relative to dir) that hold an Amp provider token or one of the given secret values.
105
+ * Never returns the values. Skips files over 10 MB.
106
+ */
107
+ export async function filesWithSecrets(dir, files, values) {
108
+ const hits = [];
109
+ const vals = values.filter((v) => v.length >= 8);
110
+ for (const f of files) {
111
+ const path = join(dir, f);
112
+ try {
113
+ if ((await stat(path)).size > MAX_SCAN_BYTES)
114
+ continue;
115
+ const text = await readFile(path, "latin1");
116
+ if (AOP_TOKEN.test(text) || vals.some((v) => text.includes(v)))
117
+ hits.push(f);
118
+ }
119
+ catch { }
120
+ }
121
+ return hits;
122
+ }
123
+ /** `git ls-files` of dir, or undefined when dir is not in a git work tree (or git is missing). */
124
+ export async function gitTrackedFiles(run, dir) {
125
+ const r = await run("git", ["-C", dir, "ls-files", "-z"], { timeoutMs: 30_000 });
126
+ if (r.code !== 0)
127
+ return undefined;
128
+ return r.stdout.split("\0").filter(Boolean);
129
+ }
130
+ /** No aop_live_ token and no control token value in a file git tracks. SKIP outside a git work tree. */
131
+ export async function trackedSecretsCheck(run, p, extra = []) {
132
+ const name = "secrets in git";
133
+ const files = await gitTrackedFiles(run, p.dir);
134
+ if (!files)
135
+ return skip(name, "not a git repository");
136
+ const ct = await readControlToken(p);
137
+ const hits = await filesWithSecrets(p.dir, files, [...(ct ? [ct] : []), ...extra]);
138
+ if (hits.length === 0)
139
+ return pass(name, `${files.length} tracked file(s), no aop_live_ or control token`);
140
+ return failed(name, new CliError(`a token is in tracked file(s): ${hits.slice(0, 5).join(", ")}${hits.length > 5 ? ", …" : ""}`, "git rm --cached those files (or remove the token), make a new token, and purge it from history if you pushed"));
141
+ }
142
+ /** Git mode: .cf-orbs/control-token and .cf-orbs/state.json are ignored and not tracked. */
143
+ export async function gitignoreCheck(run, p) {
144
+ const name = "gitignore";
145
+ const paths = [".cf-orbs/control-token", ".cf-orbs/state.json"];
146
+ const notIgnored = [];
147
+ for (const f of paths) {
148
+ // exit 0: ignored; 1: not ignored or tracked; 128: no repo. Fall back to the .gitignore lines then.
149
+ const r = await run("git", ["-C", p.dir, "check-ignore", "-q", f], { timeoutMs: 30_000 });
150
+ if (r.code === 0)
151
+ continue;
152
+ if (r.code === 1) {
153
+ notIgnored.push(f);
154
+ continue;
155
+ }
156
+ let lines = [];
157
+ try {
158
+ lines = (await readFile(p.gitignore, "utf8")).split(/\r?\n/).map((l) => l.trim());
159
+ }
160
+ catch { }
161
+ if (!lines.includes(f))
162
+ notIgnored.push(f);
163
+ }
164
+ if (notIgnored.length === 0)
165
+ return pass(name, `${paths.join(" and ")} are ignored`);
166
+ return failed(name, new CliError(`${notIgnored.join(" and ")} not ignored by git (or tracked)`, `add ${notIgnored.join(" and ")} to .gitignore and run git rm --cached on any that git tracks`));
167
+ }
168
+ /** Git mode: package.json pins the wrangler version the CLI uses (Workers Builds installs that one). */
169
+ export async function wranglerPinCheck(p) {
170
+ const name = "wrangler pin";
171
+ let pkg;
172
+ try {
173
+ pkg = JSON.parse(await readFile(p.packageJson, "utf8"));
174
+ }
175
+ catch {
176
+ return failed(name, new CliError("package.json is missing or not valid JSON", "rerun cf-orbs init --no-deploy (it writes one)"));
177
+ }
178
+ const v = pkg.devDependencies?.wrangler ?? pkg.dependencies?.wrangler;
179
+ if (v === WRANGLER_PIN)
180
+ return pass(name, `package.json pins wrangler ${v}, same as cf-orbs`);
181
+ return failed(name, new CliError(`package.json ${v ? `pins wrangler ${v}` : "has no wrangler dependency"}; cf-orbs uses ${WRANGLER_PIN}`, `set devDependencies.wrangler to "${WRANGLER_PIN}", run npm install, commit and push`));
182
+ }
183
+ /** All file checks for doctor, in output order. git: state.mode is "git". */
184
+ export async function fileChecks(run, p, c, o) {
185
+ const out = [
186
+ await orbImagePinCheck(p, c),
187
+ await providerImagePinCheck(p, o.templates),
188
+ debugExecCheck(c, o.git),
189
+ await trackedSecretsCheck(run, p),
190
+ ];
191
+ if (o.git)
192
+ out.push(await gitignoreCheck(run, p), await wranglerPinCheck(p));
193
+ return out;
194
+ }
package/dist/index.js CHANGED
@@ -30,9 +30,12 @@ export const COMMANDS = {
30
30
  "--provider cloudflare|external where amp orb provider runs (default cloudflare)",
31
31
  "--token-file <file> Amp provider token file (else AMP_ORB_PROVIDER_TOKEN, else prompt)",
32
32
  "--account-id <id> Cloudflare account (else CLOUDFLARE_ACCOUNT_ID, else ask)",
33
- "--provider-name <name> Amp provider name, as in Amp Settings → Orbs (default cf-orbs)",
33
+ "--provider-name <name> Amp provider name, as in Amp Settings → Orbs (else AMP_ORB_PROVIDER_NAME, saved name, config name, then cf-orbs)",
34
34
  "--subdomain <name> workers.dev subdomain to register if the account has none",
35
35
  "--vcpu <1|2|3|4> orb size: N vCPU, N x 3 GiB, 20 GB disk (default 4; 2 is cheaper)",
36
+ "--name <name> Worker name (default cf-orbs; a second install needs another)",
37
+ "--no-deploy preview: no Docker, no deploy. Make a git repo for Workers Builds,",
38
+ " create the Worker and its secrets, print the dashboard steps",
36
39
  ],
37
40
  flags: {
38
41
  provider: { type: "string" },
@@ -41,6 +44,8 @@ export const COMMANDS = {
41
44
  "provider-name": { type: "string" },
42
45
  subdomain: { type: "string" },
43
46
  vcpu: { type: "string" },
47
+ name: { type: "string" },
48
+ "no-deploy": { type: "boolean" },
44
49
  },
45
50
  dirPositional: true,
46
51
  run: initCommand,
package/dist/project.js CHANGED
@@ -6,6 +6,8 @@
6
6
  // cf-orbs.config.jsonc user config (source of truth)
7
7
  // image/Dockerfile orb image, user-editable; upgrade bumps its digest; image/<key>.Dockerfile for extra keys
8
8
  // .gitignore generated; keeps .cf-orbs/ (and its control-token) out of git
9
+ // (git mode: only control-token and state.json; Workers Builds needs the rest)
10
+ // package.json git mode only: pins wrangler for Workers Builds
9
11
  // .cf-orbs/
10
12
  // wrangler.jsonc generated
11
13
  // worker/src/** copied from templates on every deploy
@@ -35,6 +37,7 @@ export function projectPaths(dir) {
35
37
  state: join(s, "state.json"),
36
38
  controlToken: join(s, "control-token"),
37
39
  gitignore: join(d, ".gitignore"),
40
+ packageJson: join(d, "package.json"),
38
41
  };
39
42
  }
40
43
  /** cli/templates, resolved from this file (works from src/ under vitest and from dist/ when installed). */
@@ -105,15 +108,31 @@ export const GITIGNORE_LINES = [
105
108
  ".wrangler/",
106
109
  "node_modules/",
107
110
  ];
111
+ /** Git mode (init --no-deploy): the generated files are committed, so only these stay out of git. */
112
+ export const GIT_MODE_GITIGNORE_LINES = [
113
+ ".cf-orbs/control-token",
114
+ ".cf-orbs/state.json",
115
+ ".wrangler/",
116
+ "node_modules/",
117
+ ];
118
+ /** Lines of the local layout that hide the generated files. Git mode removes them. */
119
+ const LOCAL_ONLY_LINES = new Set([".cf-orbs/*", "!.cf-orbs/state.json"]);
108
120
  /** Add the cf-orbs lines to <dir>/.gitignore, keeping existing lines. */
109
- export async function ensureGitignore(p) {
121
+ export async function ensureGitignore(p, git = false) {
110
122
  let cur = "";
111
123
  try {
112
124
  cur = await readFile(p.gitignore, "utf8");
113
125
  }
114
126
  catch { }
127
+ if (git && cur.split(/\r?\n/).some((l) => LOCAL_ONLY_LINES.has(l.trim()))) {
128
+ cur = cur
129
+ .split(/(?<=\n)/)
130
+ .filter((l) => !LOCAL_ONLY_LINES.has(l.trim()))
131
+ .join("");
132
+ await writeFile(p.gitignore, cur);
133
+ }
115
134
  const have = new Set(cur.split(/\r?\n/).map((l) => l.trim()));
116
- const add = GITIGNORE_LINES.filter((l) => !have.has(l));
135
+ const add = (git ? GIT_MODE_GITIGNORE_LINES : GITIGNORE_LINES).filter((l) => !have.has(l));
117
136
  if (add.length === 0)
118
137
  return;
119
138
  const sep = cur === "" || cur.endsWith("\n") ? "" : "\n";
@@ -155,7 +174,7 @@ export async function materialize(p, opts = {}) {
155
174
  await writeFile(p.config, text);
156
175
  wroteConfig = true;
157
176
  }
158
- await ensureGitignore(p);
177
+ await ensureGitignore(p, opts.git);
159
178
  return { wroteConfig, wroteDockerfile };
160
179
  }
161
180
  /** The files the provider image is built from, relative to .cf-orbs/ (its build context). */
@@ -213,3 +232,15 @@ export async function cliVersion() {
213
232
  return "0.0.0";
214
233
  }
215
234
  }
235
+ /** Git mode deploy command: Workers Builds runs it in the repo root. */
236
+ export const GIT_DEPLOY_COMMAND = "npx wrangler deploy --config .cf-orbs/wrangler.jsonc";
237
+ /** Root package.json for git mode: Workers Builds installs the wrangler version it names. */
238
+ export function gitPackageJson(name, wrangler) {
239
+ const pkg = {
240
+ name,
241
+ private: true,
242
+ scripts: { deploy: "wrangler deploy --config .cf-orbs/wrangler.jsonc" },
243
+ devDependencies: { wrangler },
244
+ };
245
+ return `${JSON.stringify(pkg, null, 2)}\n`;
246
+ }
@@ -40,6 +40,9 @@ export function buildWranglerConfig(c, o = {}) {
40
40
  main: "worker/src/index.ts",
41
41
  compatibility_date: COMPATIBILITY_DATE,
42
42
  compatibility_flags: ["nodejs_compat"],
43
+ // explicit, so wrangler does not warn: the control plane is on workers.dev; preview URLs are not used
44
+ workers_dev: true,
45
+ preview_urls: false,
43
46
  observability: { enabled: true },
44
47
  containers: [
45
48
  orbContainer,
package/dist/wrangler.js CHANGED
@@ -37,6 +37,8 @@ export const runProcess = (cmd, args, o = {}) => new Promise((resolve) => {
37
37
  resolve({ code: code ?? 1, stdout, stderr });
38
38
  });
39
39
  });
40
+ /** The wrangler version the CLI depends on (cli/package.json). Git mode pins the same version in package.json. */
41
+ export const WRANGLER_PIN = "4.156.0";
40
42
  /** [command, ...prefix args] that runs the pinned wrangler. Prefers the CLI's own dependency over npx. */
41
43
  export function wranglerCommand() {
42
44
  try {
@@ -46,7 +48,7 @@ export function wranglerCommand() {
46
48
  return [process.execPath, [bin]];
47
49
  }
48
50
  catch { }
49
- return [process.platform === "win32" ? "npx.cmd" : "npx", ["--yes", "wrangler@4.156.0"]];
51
+ return [process.platform === "win32" ? "npx.cmd" : "npx", ["--yes", `wrangler@${WRANGLER_PIN}`]];
50
52
  }
51
53
  export function makeWrangler(run = runProcess, base = {}) {
52
54
  const [cmd, prefix] = wranglerCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-orbs",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "description": "Run Amp orbs on Cloudflare Containers: self-hosted orb provider, one command to install.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -5,7 +5,7 @@
5
5
  "name": "cf-orbs",
6
6
 
7
7
  // Orb size. One size per install: Amp sends no size to the provider.
8
- // Custom: vcpu 1-4, memoryMib up to 12288 and at least 3072 per vCPU, diskMb 8000-20000.
8
+ // Custom: vcpu 1-4 (fractions allowed), memoryMib up to 12288 and at least 3072 per vCPU, diskMb 8000-20000.
9
9
  // Or a named type: "standard-1" .. "standard-4". "lite" and "basic" are too small for the orb image.
10
10
  // Default 4 vCPU, 12 GiB, 20 GB. `cf-orbs init --vcpu N` writes N vCPU, N x 3 GiB, 20 GB (--vcpu 2 is cheaper).
11
11
  "instance": { "vcpu": 4, "memoryMib": 12288, "diskMb": 20000 },
@@ -53,4 +53,18 @@
53
53
 
54
54
  // Enable wrangler containers ssh for orbs (debug only).
55
55
  "debugSsh": false
56
+
57
+ // Turn on the exec and bench routes. Only for the substrate benchmark: the control token can then run
58
+ // commands in orbs. true or false.
59
+ // "debugExec": false,
60
+
61
+ // Minutes an orb may sit idle before the container stops. 5-360.
62
+ // "inactivityTimeoutMinutes": 15,
63
+
64
+ // Seconds between watchdog checks of an orb. 10-3600, and shorter than inactivityTimeoutMinutes.
65
+ // "alarmSeconds": 60,
66
+
67
+ // Digest of the Amp orb image the "orb" image is built from. Used for log lines only.
68
+ // Must look like sha256:<64 hex digits>.
69
+ // "ampImageDigest": "sha256:378c5e2188f00960eca36dc806aa04e1cacd92c2341665bcdf4692326c462d6d"
56
70
  }
@@ -1,5 +1,5 @@
1
1
  #!/bin/sh
2
- # cf-orbs dispatch program. `amp orb provider` runs it once per job (D11, D12).
2
+ # cf-orbs dispatch program. `amp orb provider` runs it once per job.
3
3
  #
4
4
  # Input (env, from Amp): ORB_OPERATION ORB_ID ORB_IMAGE ORB_SNAPSHOT_ID ORB_TOKEN AMP_URL
5
5
  # Input (env, from cf-orbs):
@@ -190,7 +190,7 @@ redact() { CFO_CTL=$CTL_TOKEN awk "$REDACT_AWK"; }
190
190
  say() { printf '%s\n' "$*" | redact; }
191
191
 
192
192
  # fail <message> [exit code]. One final line on stdout and stderr. Worker errors already start
193
- # with "Failed: "; drop it so the line has one prefix (D12).
193
+ # with "Failed: "; drop it so the line has one prefix.
194
194
  fail() {
195
195
  msg=$1
196
196
  while :; do
@@ -223,7 +223,7 @@ case $OP in
223
223
  *) fail "unknown ORB_OPERATION '$(clean "$OP")' — fix: update cf-orbs (npx cf-orbs@latest deploy)" 2 ;;
224
224
  esac
225
225
 
226
- # Same rule as the server (D2): ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$
226
+ # Same rule as the server: ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$
227
227
  orb_ok=1
228
228
  case $ORB in
229
229
  "" | [!A-Za-z0-9]* | *[!A-Za-z0-9_-]*) orb_ok=0 ;;
@@ -1,4 +1,4 @@
1
- # cf-orbs orb image (D4). Built by `cf-orbs deploy` (wrangler) and stored under the image key "orb".
1
+ # cf-orbs orb image. Built by `cf-orbs deploy` (wrangler) and stored under the image key "orb".
2
2
  #
3
3
  # Why a digest pin and not :latest
4
4
  # A snapshot only restores onto the image it was taken from. A moving tag gives a new image on the
@@ -10,7 +10,7 @@
10
10
  # The base image has no init and its CMD (python3) exits at once. tini is PID 1, keeps the container
11
11
  # up with `sleep infinity`, and reaps the processes that cf-orbs starts with setsid (amp orb bootstrap
12
12
  # and its children). -s makes tini a subreaper, so orphans of those processes are reaped too.
13
- # tini is pinned to one Debian version so that a rebuild without cache gives the same layer (R18).
13
+ # tini is pinned to one Debian version so that a rebuild without cache gives the same layer.
14
14
  # If apt cannot find that version any more, bump it and treat the result as a new image.
15
15
  #
16
16
  # How to add toolchains
@@ -1,4 +1,4 @@
1
- # cf-orbs provider image (D18). Runs `amp orb provider /app/dispatch.sh` under tini.
1
+ # cf-orbs provider image. Runs `amp orb provider /app/dispatch.sh` under tini.
2
2
  # Build context is the repo root (wrangler image entry: build_context ".."):
3
3
  # docker build -f provider/Dockerfile -t cf-orbs-provider .
4
4
  FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c
@@ -6,7 +6,7 @@ FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc
6
6
  # Pinned Amp CLI. Bump with Renovate or by hand; the provider must match Amp's job protocol.
7
7
  ARG AMP_CLI_VERSION=0.0.1793390435-g08e9ec
8
8
 
9
- # curl: dispatch.sh (D12). tini: PID 1 that forwards signals and reaps children.
9
+ # curl: dispatch.sh. tini: PID 1 that forwards signals and reaps children.
10
10
  RUN apt-get update \
11
11
  && apt-get install -y --no-install-recommends curl ca-certificates tini \
12
12
  && rm -rf /var/lib/apt/lists/*
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # cf-orbs provider entry (D18). Runs `amp orb provider <dispatch>` in a loop.
2
+ # cf-orbs provider entry. Runs `amp orb provider <dispatch>` in a loop.
3
3
  # Output is redacted, then written to stdout and to the log file. The log is capped.
4
4
  # Env:
5
5
  # AMP_ORB_PROVIDER_TOKEN required. Read by amp; never printed.
@@ -18,7 +18,7 @@ import { type Env, orbDoName, PROVIDER_DO_NAME, REGISTRY_DO_NAME } from "./env";
18
18
  import { BENCH_ORB_RE, type BenchStartRequest, parseBenchStart } from "./orb/bench";
19
19
  import { restartProvider } from "./provider-restart";
20
20
 
21
- export const API_VERSION = "0.1.0";
21
+ export const API_VERSION = "0.2.2";
22
22
 
23
23
  /** D2. Amp sends `amp-<thread id>`. Never starts with `_` or `.`, so it cannot look like an internal name. */
24
24
  export const ORB_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
@@ -5,6 +5,7 @@ import {
5
5
  DEFAULT_CONFIG,
6
6
  type InstanceSpec,
7
7
  MAX_PREWARM,
8
+ type OrbInstanceSpec,
8
9
  type ProjectSnapshotRefresh,
9
10
  type ProviderConfig,
10
11
  } from "./types";
@@ -37,9 +38,9 @@ function num(v: unknown, name: string, min: number, max: number, integer = true)
37
38
  return v;
38
39
  }
39
40
 
40
- export function validateInstance(v: unknown): InstanceSpec {
41
+ export function validateInstance(v: unknown): OrbInstanceSpec {
41
42
  if (typeof v === "string") {
42
- if ((NAMED as readonly string[]).includes(v)) return v as InstanceSpec;
43
+ if ((NAMED as readonly string[]).includes(v)) return v as OrbInstanceSpec;
43
44
  if (v === "lite" || v === "basic" || v === "dev") {
44
45
  fail(
45
46
  `instance ${v} is too small for the orb image`,
@@ -49,7 +49,7 @@ export interface SnapshotHandle {
49
49
  id: string; // Cloudflare snapshot id
50
50
  size: number; // bytes reported by snapshotContainer
51
51
  imageKey: string; // key in images map the snapshot was taken from
52
- instance: InstanceSpec;
52
+ instance: OrbInstanceSpec;
53
53
  createdAt: string; // ISO
54
54
  lastRestoredAt?: string; // ISO; set on every successful restore. Expiry = max(createdAt, lastRestoredAt) + 30 d (D7)
55
55
  name?: string;
@@ -70,6 +70,7 @@ export interface SpawnEntry {
70
70
  /** OrbRecord.spawns keeps this many entries, oldest first. */
71
71
  export const SPAWN_HISTORY = 10;
72
72
 
73
+ /** Any Cloudflare Containers instance size. "lite" (256 MiB) is used only by the provider container. */
73
74
  export type InstanceSpec =
74
75
  | "lite"
75
76
  | "standard-1"
@@ -78,6 +79,9 @@ export type InstanceSpec =
78
79
  | "standard-4"
79
80
  | { vcpu: number; memoryMib: number; diskMb: number };
80
81
 
82
+ /** An orb size. "lite" is too small for the orb image, so config validation rejects it. */
83
+ export type OrbInstanceSpec = Exclude<InstanceSpec, "lite">;
84
+
81
85
  /** Per-orb record in OrbSession storage under key "orb". */
82
86
  export interface OrbRecord {
83
87
  /** Schema version (D13). Old and new Worker code share storage; a missing v reads as 1. */
@@ -85,7 +89,7 @@ export interface OrbRecord {
85
89
  orbId: string;
86
90
  state: OrbState;
87
91
  imageKey: string;
88
- instance: InstanceSpec;
92
+ instance: OrbInstanceSpec;
89
93
  handle?: SnapshotHandle; // per-orb snapshot (last pause or checkpoint)
90
94
  bootstrapTokenSha?: string; // first 16 hex of sha256(ORB_TOKEN) for the running bootstrap
91
95
  awakeSince?: string;
@@ -136,7 +140,7 @@ export type ProjectSnapshotRefresh = "off" | "on-change";
136
140
 
137
141
  /** Provider-level configuration, derived from cf-orbs.config.jsonc and passed as Worker vars (JSON string in CF_ORBS_CONFIG). */
138
142
  export interface ProviderConfig {
139
- instance: InstanceSpec; // default { vcpu: 4, memoryMib: 12288, diskMb: 20000 }
143
+ instance: OrbInstanceSpec; // default { vcpu: 4, memoryMib: 12288, diskMb: 20000 }
140
144
  maxRunning: number; // default 20
141
145
  maxAwakeHours: number; // default 12
142
146
  checkpointMinutes: number; // default 0 = off