From 5cb020dbf7d910bd653e8d7fea29181c677f656a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:26:07 +0900 Subject: [PATCH 01/33] test(toolchain): require GPL-free Worker boundary --- ...udflare-toolchain-license-boundary.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/cloudflare-toolchain-license-boundary.test.ts diff --git a/test/cloudflare-toolchain-license-boundary.test.ts b/test/cloudflare-toolchain-license-boundary.test.ts new file mode 100644 index 000000000..ef146bad2 --- /dev/null +++ b/test/cloudflare-toolchain-license-boundary.test.ts @@ -0,0 +1,62 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +function readJson(path: string): Record { + return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")) as Record; +} + +describe("Cloudflare Worker toolchain license boundary", () => { + it("keeps Wrangler, Miniflare, Sharp, and libvips out of the committed dependency graph", () => { + const pkg = readJson("../package.json") as { + scripts?: Record; + devDependencies?: Record; + }; + const lockText = readFileSync(new URL("../package-lock.json", import.meta.url), "utf8"); + + expect(pkg.devDependencies?.wrangler).toBeUndefined(); + expect(pkg.devDependencies?.esbuild).toBe("0.28.1"); + expect(pkg.devDependencies?.workerd).toBe("1.20260625.1"); + expect(pkg.scripts?.deploy).toBe("node scripts/cloudflare-worker-deploy.mjs"); + expect(pkg.scripts?.dev).toBe("node scripts/cloudflare-worker-dev.mjs"); + + for (const forbidden of [ + '"node_modules/wrangler"', + '"node_modules/miniflare"', + '"node_modules/sharp"', + '"node_modules/@img/sharp-libvips-', + '"LGPL-3.0', + '"GPL-3.0', + '"AGPL-3.0', + ]) { + expect(lockText).not.toContain(forbidden); + } + }); + + it("uses a direct Cloudflare API deployment boundary with immutable source annotations", () => { + const deploy = readFileSync( + new URL("../scripts/cloudflare-worker-deploy.mjs", import.meta.url), + "utf8", + ); + + expect(deploy).toContain("/workers/scripts/${encodeURIComponent(scriptName)}/versions"); + expect(deploy).toContain('type: "durable_object_namespace"'); + expect(deploy).toContain('"workers/commit_sha"'); + expect(deploy).toContain("CLOUDFLARE_API_TOKEN"); + expect(deploy).not.toContain("wrangler"); + expect(deploy).not.toContain("miniflare"); + }); + + it("runs local development on pinned workerd with local-only Durable Object storage", () => { + const dev = readFileSync( + new URL("../scripts/cloudflare-worker-dev.mjs", import.meta.url), + "utf8", + ); + + expect(dev).toContain("workerd serve"); + expect(dev).toContain("durableObjectNamespaces"); + expect(dev).toContain("localDisk"); + expect(dev).toContain('address = "127.0.0.1:8787"'); + expect(dev).not.toContain("wrangler"); + expect(dev).not.toContain("miniflare"); + }); +}); From 17e35f6bb04f4c03897b94e774b831d8065ce357 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:18:50 +0900 Subject: [PATCH 02/33] fix(toolchain): replace Wrangler package scripts --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a8bcbf59f..b84959e05 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "workerd@1.20260625.1": true }, "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", + "deploy": "node scripts/cloudflare-worker-deploy.mjs", + "dev": "node scripts/cloudflare-worker-dev.mjs", "kpi:compute": "node scripts/compute-kpi.mjs", "kpi:collect": "bash scripts/collect-kpi-logs.sh", "kpi:check": "node scripts/check-kpi.mjs", @@ -62,12 +62,12 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20260630.0", "@vitest/coverage-v8": "^4.1.9", + "esbuild": "0.28.1", "typescript": "^5.9.0", "vitest": "^4.1.9", - "wrangler": "^4.25.0" + "workerd": "1.20260625.1" }, "overrides": { - "sharp": "0.35.3", "postcss": "^8.5.18", "undici": "7.29.0" } From 23ef15949719896e190c3095dec3f8ffea1bad72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:21:26 +0900 Subject: [PATCH 03/33] fix(toolchain): add strict Worker config adapter --- scripts/lib/cloudflare-worker-config.mjs | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 scripts/lib/cloudflare-worker-config.mjs diff --git a/scripts/lib/cloudflare-worker-config.mjs b/scripts/lib/cloudflare-worker-config.mjs new file mode 100644 index 000000000..92583d11e --- /dev/null +++ b/scripts/lib/cloudflare-worker-config.mjs @@ -0,0 +1,121 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +const ROOT_KEYS = new Set(["name", "main", "compatibility_date"]); +const DURABLE_OBJECT_KEYS = new Set(["name", "class_name"]); +const EXPORT_KEYS = new Set(["type", "storage"]); +const ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([^"\\]*)"$/; +const EXPORT_SECTION = /^\[exports\.([A-Za-z_][A-Za-z0-9_]*)\]$/; + +function assignUnique(target, key, value, context) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + throw new Error(`Duplicate ${context} key: ${key}`); + } + target[key] = value; +} + +/** + * Read the narrow Worker configuration surface that Noema owns. + * + * The parser is intentionally fail-closed instead of implementing general TOML. It accepts + * only the root identity, Durable Object bindings/exports, and plain-text vars currently used + * by Noema. Any new configuration shape must receive an explicit adapter decision rather than + * being silently omitted from direct Cloudflare API uploads or local workerd development. + */ +export async function readNoemaWorkerConfig(repositoryRoot) { + const source = await readFile(join(repositoryRoot, "wrangler.toml"), "utf8"); + const root = {}; + const durableObjects = []; + const exportsByClass = new Map(); + const vars = {}; + let section = "root"; + let currentDurableObject = null; + let currentExport = null; + + for (const [index, rawLine] of source.split(/\r?\n/u).entries()) { + const line = rawLine.trim(); + if (line === "" || line.startsWith("#")) continue; + + if (line === "[[durable_objects.bindings]]") { + currentDurableObject = {}; + durableObjects.push(currentDurableObject); + currentExport = null; + section = "durable-object"; + continue; + } + if (line === "[vars]") { + currentDurableObject = null; + currentExport = null; + section = "vars"; + continue; + } + const exportMatch = EXPORT_SECTION.exec(line); + if (exportMatch) { + const className = exportMatch[1]; + if (exportsByClass.has(className)) { + throw new Error(`Duplicate Worker export section: ${className}`); + } + currentExport = {}; + exportsByClass.set(className, currentExport); + currentDurableObject = null; + section = "export"; + continue; + } + if (line.startsWith("[") || line.startsWith("[[")) { + throw new Error(`Unsupported Worker configuration section at line ${index + 1}: ${line}`); + } + + const assignment = ASSIGNMENT.exec(line); + if (!assignment) { + throw new Error(`Unsupported Worker configuration syntax at line ${index + 1}`); + } + const [, key, value] = assignment; + + if (section === "root") { + if (!ROOT_KEYS.has(key)) throw new Error(`Unsupported root Worker key: ${key}`); + assignUnique(root, key, value, "root Worker"); + continue; + } + if (section === "durable-object") { + if (!currentDurableObject || !DURABLE_OBJECT_KEYS.has(key)) { + throw new Error(`Unsupported Durable Object binding key: ${key}`); + } + assignUnique(currentDurableObject, key, value, "Durable Object binding"); + continue; + } + if (section === "export") { + if (!currentExport || !EXPORT_KEYS.has(key)) { + throw new Error(`Unsupported Worker export key: ${key}`); + } + assignUnique(currentExport, key, value, "Worker export"); + continue; + } + assignUnique(vars, key, value, "Worker var"); + } + + for (const required of ROOT_KEYS) { + if (!root[required]) throw new Error(`Missing required Worker key: ${required}`); + } + if (durableObjects.length === 0) throw new Error("No Durable Object bindings configured"); + + for (const binding of durableObjects) { + if (!binding.name || !binding.class_name) { + throw new Error("Durable Object bindings require name and class_name"); + } + const exported = exportsByClass.get(binding.class_name); + if (!exported || exported.type !== "durable-object" || exported.storage !== "sqlite") { + throw new Error(`Durable Object export ${binding.class_name} must remain durable-object/sqlite`); + } + } + if (exportsByClass.size !== durableObjects.length) { + throw new Error("Every Worker export must correspond to exactly one Durable Object binding"); + } + + return Object.freeze({ + name: root.name, + main: root.main, + compatibilityDate: root.compatibility_date, + durableObjects: durableObjects.map((binding) => Object.freeze({ ...binding })), + vars: Object.freeze({ ...vars }), + }); +} From f8a253d6dd667cb86aa0f0a26cbfe51c99d56ac4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:22:52 +0900 Subject: [PATCH 04/33] fix(toolchain): deploy Worker through version API --- scripts/cloudflare-worker-deploy.mjs | 243 +++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 scripts/cloudflare-worker-deploy.mjs diff --git a/scripts/cloudflare-worker-deploy.mjs b/scripts/cloudflare-worker-deploy.mjs new file mode 100644 index 000000000..d0b8402ba --- /dev/null +++ b/scripts/cloudflare-worker-deploy.mjs @@ -0,0 +1,243 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { mkdtemp } from "node:fs/promises"; +import { build } from "esbuild"; +import { readNoemaWorkerConfig } from "./lib/cloudflare-worker-config.mjs"; + +const API_ORIGIN = "https://api.cloudflare.com"; +const API_PREFIX = "/client/v4"; +const REPOSITORY_URL = "https://github.com/ContextualWisdomLab/noema"; +const REQUIRED_SECRET_BINDINGS = ["GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PEM"]; +const OPTIONAL_SECRET_BINDINGS = ["GITHUB_APP_INSTALLATION_ID"]; +const MAX_RESPONSE_BYTES = 1024 * 1024; +const SHA_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u; +const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9_-]{1,32}$/u; +const SCRIPT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u; + +function requiredEnvironment(name) { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`Missing required environment variable: ${name}`); + return value; +} + +function repositorySourceSha(repositoryRoot) { + const head = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: repositoryRoot, + encoding: "utf8", + }).trim().toLowerCase(); + if (!SHA_PATTERN.test(head)) throw new Error("Repository HEAD is not a full commit SHA"); + + const dirty = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { + cwd: repositoryRoot, + encoding: "utf8", + }); + if (dirty !== "") { + throw new Error("Refusing deployment from a dirty checkout; commit the exact source first"); + } + + const declared = process.env.GITHUB_SHA?.trim().toLowerCase(); + if (declared && declared !== head) { + throw new Error("GITHUB_SHA does not match the exact checked-out repository HEAD"); + } + if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_REPOSITORY !== "ContextualWisdomLab/noema") { + throw new Error("GITHUB_REPOSITORY does not identify ContextualWisdomLab/noema"); + } + return head; +} + +async function parseCloudflareResponse(response, operation) { + const text = await response.text(); + if (Buffer.byteLength(text, "utf8") > MAX_RESPONSE_BYTES) { + throw new Error(`${operation} returned an oversized response`); + } + let payload; + try { + payload = JSON.parse(text); + } catch { + throw new Error(`${operation} returned non-JSON data (HTTP ${response.status})`); + } + if (!response.ok || payload?.success === false) { + const codes = Array.isArray(payload?.errors) + ? payload.errors.map((error) => error?.code).filter(Boolean).join(",") + : ""; + throw new Error(`${operation} failed (HTTP ${response.status}${codes ? `; codes=${codes}` : ""})`); + } + return payload?.result ?? payload; +} + +async function cloudflareJson(url, token, operation, init = {}) { + const response = await fetch(url, { + ...init, + headers: { + authorization: `Bearer ${token}`, + ...(init.headers ?? {}), + }, + signal: AbortSignal.timeout(120_000), + }); + return parseCloudflareResponse(response, operation); +} + +function currentBindingMap(settings) { + const bindings = Array.isArray(settings?.bindings) ? settings.bindings : []; + return new Map(bindings.map((binding) => [binding?.name, binding])); +} + +function verifyExistingRuntimeBindings(config, settings) { + const current = currentBindingMap(settings); + for (const secretName of REQUIRED_SECRET_BINDINGS) { + if (current.get(secretName)?.type !== "secret_text") { + throw new Error(`Existing Worker is missing required secret binding: ${secretName}`); + } + } + for (const durableObject of config.durableObjects) { + const binding = current.get(durableObject.name); + if ( + binding?.type !== "durable_object_namespace" + || binding?.class_name !== durableObject.class_name + ) { + throw new Error(`Existing Durable Object binding does not match ${durableObject.name}`); + } + } + return current; +} + +function uploadBindings(config, currentBindings) { + const bindings = [ + ...Object.entries(config.vars).map(([name, text]) => ({ + type: "plain_text", + name, + text, + })), + ...config.durableObjects.map(({ name, class_name }) => ({ + type: "durable_object_namespace", + name, + class_name, + })), + ...REQUIRED_SECRET_BINDINGS.map((name) => ({ + type: "inherit", + name, + version_id: "latest", + })), + ]; + for (const name of OPTIONAL_SECRET_BINDINGS) { + if (currentBindings.get(name)?.type === "secret_text") { + bindings.push({ type: "inherit", name, version_id: "latest" }); + } + } + return bindings; +} + +async function bundleWorker(repositoryRoot, entryPoint, outputFile) { + await build({ + absWorkingDir: repositoryRoot, + entryPoints: [entryPoint], + outfile: outputFile, + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", + conditions: ["workerd", "worker", "browser"], + sourcemap: false, + legalComments: "none", + logLevel: "warning", + }); +} + +async function main() { + const repositoryRoot = resolve(new URL("..", import.meta.url).pathname); + const config = await readNoemaWorkerConfig(repositoryRoot); + const accountId = requiredEnvironment("CLOUDFLARE_ACCOUNT_ID"); + const apiToken = requiredEnvironment("CLOUDFLARE_API_TOKEN"); + const scriptName = (process.env.CLOUDFLARE_WORKER_NAME?.trim() || config.name); + if (!ACCOUNT_ID_PATTERN.test(accountId)) throw new Error("CLOUDFLARE_ACCOUNT_ID is malformed"); + if (!SCRIPT_NAME_PATTERN.test(scriptName)) throw new Error("CLOUDFLARE_WORKER_NAME is malformed"); + + const sourceSha = repositorySourceSha(repositoryRoot); + const encodedAccount = encodeURIComponent(accountId); + const encodedScript = encodeURIComponent(scriptName); + const settingsUrl = `${API_ORIGIN}${API_PREFIX}/accounts/${encodedAccount}/workers/scripts/${encodedScript}/settings`; + const settings = await cloudflareJson(settingsUrl, apiToken, "Worker settings read"); + const currentBindings = verifyExistingRuntimeBindings(config, settings); + + const temporaryDirectory = await mkdtemp(join(tmpdir(), "noema-worker-deploy-")); + const moduleName = "worker.mjs"; + const outputFile = join(temporaryDirectory, moduleName); + try { + await bundleWorker(repositoryRoot, config.main, outputFile); + const moduleBytes = await readFile(outputFile); + const metadata = { + main_module: moduleName, + compatibility_date: config.compatibilityDate, + annotations: { + "workers/commit_sha": sourceSha, + "workers/repository_url": REPOSITORY_URL, + "workers/message": `Noema source ${sourceSha}`, + "workers/tag": sourceSha.slice(0, 12), + }, + bindings: uploadBindings(config, currentBindings), + }; + const form = new FormData(); + form.append( + "metadata", + new Blob([JSON.stringify(metadata)], { type: "application/json" }), + "metadata.json", + ); + form.append( + moduleName, + new Blob([moduleBytes], { type: "application/javascript+module" }), + moduleName, + ); + + const versionsPath = `/accounts/${encodedAccount}/workers/scripts/${encodeURIComponent(scriptName)}/versions`; + const version = await cloudflareJson( + `${API_ORIGIN}${API_PREFIX}${versionsPath}?bindings_inherit=strict`, + apiToken, + "Worker version upload", + { method: "POST", body: form }, + ); + const versionId = version?.id; + if (typeof versionId !== "string" || versionId.length === 0) { + throw new Error("Worker version upload returned no version id"); + } + + const deployment = await cloudflareJson( + `${API_ORIGIN}${API_PREFIX}/accounts/${encodedAccount}/workers/scripts/${encodedScript}/deployments`, + apiToken, + "Worker deployment", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + strategy: "percentage", + versions: [{ version_id: versionId, percentage: 100 }], + annotations: { + "workers/message": `Deploy Noema ${sourceSha}`, + "workers/triggered_by": "noema-direct-api-toolchain", + }, + }), + }, + ); + const deploymentId = deployment?.id; + if (typeof deploymentId !== "string" || deploymentId.length === 0) { + throw new Error("Worker deployment returned no deployment id"); + } + + process.stdout.write(`${JSON.stringify({ + worker: scriptName, + source_sha: sourceSha, + version_id: versionId, + deployment_id: deploymentId, + })}\n`); + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`Noema Worker deployment failed: ${message}\n`); + process.exitCode = 1; +}); From c80cbd8deb982e99c0eadc632e3536b35d8b9eaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:23:32 +0900 Subject: [PATCH 05/33] fix(toolchain): run local Worker on workerd --- scripts/cloudflare-worker-dev.mjs | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 scripts/cloudflare-worker-dev.mjs diff --git a/scripts/cloudflare-worker-dev.mjs b/scripts/cloudflare-worker-dev.mjs new file mode 100644 index 000000000..47391eb59 --- /dev/null +++ b/scripts/cloudflare-worker-dev.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; +import { readNoemaWorkerConfig } from "./lib/cloudflare-worker-config.mjs"; + +const REQUIRED_LOCAL_SECRETS = ["GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PEM"]; +const OPTIONAL_LOCAL_SECRETS = ["GITHUB_APP_INSTALLATION_ID"]; +const workerdCommand = "workerd serve"; + +function capnpText(value) { + return JSON.stringify(String(value)); +} + +function requireLocalSecrets() { + for (const name of REQUIRED_LOCAL_SECRETS) { + if (!process.env[name]) throw new Error(`Missing required local Worker binding: ${name}`); + } +} + +function bindingLines(config) { + const lines = []; + for (const [name, value] of Object.entries(config.vars)) { + lines.push(` (name = ${capnpText(name)}, text = ${capnpText(value)})`); + } + for (const { name, class_name } of config.durableObjects) { + lines.push( + ` (name = ${capnpText(name)}, durableObjectNamespace = ${capnpText(class_name)})`, + ); + } + for (const name of REQUIRED_LOCAL_SECRETS) { + lines.push(` (name = ${capnpText(name)}, fromEnvironment = ${capnpText(name)})`); + } + for (const name of OPTIONAL_LOCAL_SECRETS) { + if (process.env[name]) { + lines.push(` (name = ${capnpText(name)}, fromEnvironment = ${capnpText(name)})`); + } + } + return lines.join(",\n"); +} + +function durableObjectNamespaceLines(config) { + return config.durableObjects.map(({ class_name }, index) => [ + " (", + ` className = ${capnpText(class_name)},`, + ` uniqueKey = ${capnpText(`noema-local-${index + 1}-${class_name}`)},`, + " enableSql = true", + " )", + ].join("\n")).join(",\n"); +} + +function workerdConfig(config, storageDirectory) { + return `using Workerd = import "/workerd/workerd.capnp"; + +const config :Workerd.Config = ( + services = [ + (name = "main", worker = .mainWorker), + (name = "do-storage", disk = (path = ${capnpText(storageDirectory)}, writable = true)), + (name = "internet", network = (allow = ["public"], tlsOptions = (trustBrowserCas = true))) + ], + sockets = [ + ( + name = "http", + address = "127.0.0.1:8787", + http = (), + service = "main" + ) + ] +); + +const mainWorker :Workerd.Worker = ( + modules = [(name = "worker.mjs", esModule = embed "worker.mjs")], + compatibilityDate = ${capnpText(config.compatibilityDate)}, + bindings = [ +${bindingLines(config)} + ], + durableObjectNamespaces = [ +${durableObjectNamespaceLines(config)} + ], + durableObjectStorage = (localDisk = "do-storage") +); +`; +} + +async function bundleWorker(repositoryRoot, config, outputFile) { + await build({ + absWorkingDir: repositoryRoot, + entryPoints: [config.main], + outfile: outputFile, + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", + conditions: ["workerd", "worker", "browser"], + sourcemap: false, + legalComments: "none", + logLevel: "warning", + }); +} + +async function runWorkerd(executable, configPath, repositoryRoot) { + const child = spawn(executable, ["serve", configPath], { + cwd: repositoryRoot, + env: process.env, + stdio: "inherit", + }); + const forwardSignal = (signal) => { + if (!child.killed) child.kill(signal); + }; + process.once("SIGINT", forwardSignal); + process.once("SIGTERM", forwardSignal); + try { + return await new Promise((resolvePromise, reject) => { + child.once("error", reject); + child.once("exit", (code, signal) => { + if (signal) reject(new Error(`${workerdCommand} exited from signal ${signal}`)); + else resolvePromise(code ?? 1); + }); + }); + } finally { + process.removeListener("SIGINT", forwardSignal); + process.removeListener("SIGTERM", forwardSignal); + } +} + +async function main() { + const repositoryRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); + const config = await readNoemaWorkerConfig(repositoryRoot); + requireLocalSecrets(); + + const executable = join( + repositoryRoot, + "node_modules", + ".bin", + process.platform === "win32" ? "workerd.cmd" : "workerd", + ); + await access(executable); + + const storageDirectory = join(repositoryRoot, ".noema-dev", "durable-objects"); + await mkdir(storageDirectory, { recursive: true }); + const temporaryDirectory = await mkdtemp(join(tmpdir(), "noema-worker-dev-")); + const outputFile = join(temporaryDirectory, "worker.mjs"); + const configPath = join(temporaryDirectory, "config.capnp"); + + try { + await bundleWorker(repositoryRoot, config, outputFile); + await writeFile(configPath, workerdConfig(config, storageDirectory), { mode: 0o600 }); + const exitCode = await runWorkerd(executable, configPath, repositoryRoot); + if (exitCode !== 0) throw new Error(`${workerdCommand} exited with code ${exitCode}`); + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`Noema local Worker failed: ${message}\n`); + process.exitCode = 1; +}); From e211369dfd78e0869857872230b795422c00e586 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:32:28 +0900 Subject: [PATCH 06/33] test(toolchain): emit canonical lockfile evidence --- .../workflows/lockfile-reproducibility.yml | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/lockfile-reproducibility.yml diff --git a/.github/workflows/lockfile-reproducibility.yml b/.github/workflows/lockfile-reproducibility.yml new file mode 100644 index 000000000..5e3428a9f --- /dev/null +++ b/.github/workflows/lockfile-reproducibility.yml @@ -0,0 +1,97 @@ +name: lockfile-reproducibility + +on: + pull_request: + push: + branches: + - main + +concurrency: + group: noema-lockfile-reproducibility-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: verify + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: checkout exact source + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: verify exact checkout + shell: bash + env: + NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + if [[ ! "$NOEMA_EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then + printf '::error::Invalid expected head SHA.\n' + exit 1 + fi + test "$(git rev-parse HEAD)" = "$NOEMA_EXPECTED_HEAD_SHA" + + - name: setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "24.19.0" + cache: npm + + - name: verify package-manager identity + shell: bash + run: | + set -euo pipefail + test "$(node --version)" = "v24.19.0" + test "$(npm --version)" = "11.17.0" + + - name: regenerate canonical lockfile in disposable workspace + id: regenerate + shell: bash + run: | + set -euo pipefail + regeneration_root="$RUNNER_TEMP/noema-lockfile-regeneration" + rm -rf "$regeneration_root" + mkdir -p "$regeneration_root" + cp package.json package-lock.json .npmrc "$regeneration_root/" + ( + cd "$regeneration_root" + npm install \ + --package-lock-only \ + --ignore-scripts \ + --no-audit \ + --no-fund \ + --legacy-peer-deps=false \ + --install-links=false + ) + cp "$regeneration_root/package-lock.json" "$RUNNER_TEMP/noema-package-lock-regenerated.json" + if cmp -s package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json"; then + printf 'match=true\n' >> "$GITHUB_OUTPUT" + else + printf 'match=false\n' >> "$GITHUB_OUTPUT" + diff -u package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json" \ + > "$RUNNER_TEMP/noema-package-lock-regeneration.diff" || true + fi + + - name: upload regenerated lockfile evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: noema-lockfile-reproducibility-${{ github.event.pull_request.head.sha || github.sha }} + path: | + ${{ runner.temp }}/noema-package-lock-regenerated.json + ${{ runner.temp }}/noema-package-lock-regeneration.diff + if-no-files-found: error + retention-days: 1 + + - name: require committed lockfile reproducibility + if: steps.regenerate.outputs.match != 'true' + shell: bash + run: | + printf '::error::package-lock.json is not the canonical output of the pinned Node/npm toolchain.\n' + exit 1 From 95757027058600bd84a80b4b930457f344ed7e0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:33:01 +0900 Subject: [PATCH 07/33] fix(toolchain): ignore local workerd state --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 910e84693..659de7229 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ .wrangler/ +.noema-dev/ coverage/ dist/ *.log From ac619fea506d01f8ba1b706fc38f0f4a38a65544 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:33:41 +0900 Subject: [PATCH 08/33] fix(toolchain): make deploy path portable --- scripts/cloudflare-worker-deploy.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/cloudflare-worker-deploy.mjs b/scripts/cloudflare-worker-deploy.mjs index d0b8402ba..ca81127e6 100644 --- a/scripts/cloudflare-worker-deploy.mjs +++ b/scripts/cloudflare-worker-deploy.mjs @@ -1,9 +1,10 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; import { readFile, rm } from "node:fs/promises"; +import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; -import { mkdtemp } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; import { build } from "esbuild"; import { readNoemaWorkerConfig } from "./lib/cloudflare-worker-config.mjs"; @@ -147,11 +148,11 @@ async function bundleWorker(repositoryRoot, entryPoint, outputFile) { } async function main() { - const repositoryRoot = resolve(new URL("..", import.meta.url).pathname); + const repositoryRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); const config = await readNoemaWorkerConfig(repositoryRoot); const accountId = requiredEnvironment("CLOUDFLARE_ACCOUNT_ID"); const apiToken = requiredEnvironment("CLOUDFLARE_API_TOKEN"); - const scriptName = (process.env.CLOUDFLARE_WORKER_NAME?.trim() || config.name); + const scriptName = process.env.CLOUDFLARE_WORKER_NAME?.trim() || config.name; if (!ACCOUNT_ID_PATTERN.test(accountId)) throw new Error("CLOUDFLARE_ACCOUNT_ID is malformed"); if (!SCRIPT_NAME_PATTERN.test(scriptName)) throw new Error("CLOUDFLARE_WORKER_NAME is malformed"); @@ -191,7 +192,7 @@ async function main() { moduleName, ); - const versionsPath = `/accounts/${encodedAccount}/workers/scripts/${encodeURIComponent(scriptName)}/versions`; + const versionsPath = `/accounts/${encodedAccount}/workers/scripts/${encodedScript}/versions`; const version = await cloudflareJson( `${API_ORIGIN}${API_PREFIX}${versionsPath}?bindings_inherit=strict`, apiToken, From 27b3c66b404341050d403ff5bcb68627d6f53db6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:36:28 +0900 Subject: [PATCH 09/33] test(toolchain): exercise Worker config boundary --- test/cloudflare-worker-config.test.mjs | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 test/cloudflare-worker-config.test.mjs diff --git a/test/cloudflare-worker-config.test.mjs b/test/cloudflare-worker-config.test.mjs new file mode 100644 index 000000000..4d0e3118f --- /dev/null +++ b/test/cloudflare-worker-config.test.mjs @@ -0,0 +1,89 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { readNoemaWorkerConfig } from "../scripts/lib/cloudflare-worker-config.mjs"; + +const temporaryRoots = []; + +async function fixture(source) { + const root = await mkdtemp(join(tmpdir(), "noema-worker-config-")); + temporaryRoots.push(root); + await mkdir(root, { recursive: true }); + await writeFile(join(root, "wrangler.toml"), source, "utf8"); + return root; +} + +const validConfig = ` +name = "noema" +main = "src/runtime-entrypoint.ts" +compatibility_date = "2026-06-30" + +[[durable_objects.bindings]] +name = "NOEMA_RATE_LIMITER" +class_name = "NoemaRateLimiter" + +[exports.NoemaRateLimiter] +type = "durable-object" +storage = "sqlite" + +[vars] +ALLOWED_ISSUER = "https://token.actions.githubusercontent.com" +`; + +afterEach(async () => { + await Promise.all( + temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })), + ); +}); + +describe("Noema Worker configuration adapter", () => { + it("preserves the owned Worker identity, Durable Object binding, and plain-text vars", async () => { + const root = await fixture(validConfig); + + await expect(readNoemaWorkerConfig(root)).resolves.toEqual({ + name: "noema", + main: "src/runtime-entrypoint.ts", + compatibilityDate: "2026-06-30", + durableObjects: [ + { name: "NOEMA_RATE_LIMITER", class_name: "NoemaRateLimiter" }, + ], + vars: { + ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", + }, + }); + }); + + it("fails closed when an unimplemented configuration section appears", async () => { + const root = await fixture(`${validConfig}\n[observability]\nenabled = "true"\n`); + + await expect(readNoemaWorkerConfig(root)).rejects.toThrow( + /Unsupported Worker configuration section/u, + ); + }); + + it("fails closed when a root field would be silently omitted", async () => { + const root = await fixture(`${validConfig}\ncompatibility_flags = "nodejs_compat"\n`); + + await expect(readNoemaWorkerConfig(root)).rejects.toThrow( + /Unsupported root Worker key/u, + ); + }); + + it("rejects duplicate configuration authority", async () => { + const root = await fixture(validConfig.replace( + 'ALLOWED_ISSUER = "https://token.actions.githubusercontent.com"', + 'ALLOWED_ISSUER = "https://token.actions.githubusercontent.com"\nALLOWED_ISSUER = "https://example.invalid"', + )); + + await expect(readNoemaWorkerConfig(root)).rejects.toThrow(/Duplicate Worker var key/u); + }); + + it("requires every Durable Object binding to keep its declared sqlite export", async () => { + const root = await fixture(validConfig.replace('storage = "sqlite"', 'storage = "memory"')); + + await expect(readNoemaWorkerConfig(root)).rejects.toThrow( + /must remain durable-object\/sqlite/u, + ); + }); +}); From 47b49fa9bd38a983ab5f3451134e987378ec80fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:15:09 +0900 Subject: [PATCH 10/33] test(toolchain): expose lock verification and validator isolation gaps --- .../lockfile-reproducibility-workflow.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/lockfile-reproducibility-workflow.test.ts diff --git a/test/lockfile-reproducibility-workflow.test.ts b/test/lockfile-reproducibility-workflow.test.ts new file mode 100644 index 000000000..a6a6f9209 --- /dev/null +++ b/test/lockfile-reproducibility-workflow.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const lockfileWorkflowPath = ".github/workflows/lockfile-reproducibility.yml"; +const validatorWorkflowPath = ".github/workflows/patch-validator-image.yml"; + +function readWorkflow(path: string): string { + return readFileSync(path, "utf8"); +} + +describe("Cloudflare toolchain lockfile and validator isolation", () => { + it("verifies the committed lock with a read-only token and lockfile-pinned npm ci", () => { + const workflow = readWorkflow(lockfileWorkflowPath); + const jobsStart = workflow.indexOf("\njobs:"); + + expect(jobsStart).toBeGreaterThan(0); + expect(workflow.slice(0, jobsStart)).toContain( + "permissions:\n contents: read", + ); + expect(workflow).toContain("npm ci"); + expect(workflow).not.toMatch(/\bnpm\s+(?:install|i)\b/u); + expect(workflow).toContain("package-lock.json"); + expect(workflow).toContain("persist-credentials: false"); + expect(workflow).toContain( + "test \"$(git rev-parse HEAD)\" = \"$NOEMA_EXPECTED_HEAD_SHA\"", + ); + }); + + it("prunes builder-only workerd and esbuild from patch-validator dependencies", () => { + const workflow = readWorkflow(validatorWorkflowPath); + + expect(workflow).toContain("devDependencies.workerd"); + expect(workflow).toContain("devDependencies.esbuild"); + expect(workflow).toContain("test ! -e node_modules/workerd"); + expect(workflow).toContain("test ! -e node_modules/esbuild"); + expect(workflow).toContain("test ! -e node_modules/@esbuild"); + expect(workflow).toContain("test ! -e node_modules/miniflare"); + }); +}); From 612ec14be46ee966f723fef676c85668f97936f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:16:02 +0900 Subject: [PATCH 11/33] fix(ci): verify the committed lock with pinned npm ci --- .../workflows/lockfile-reproducibility.yml | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/.github/workflows/lockfile-reproducibility.yml b/.github/workflows/lockfile-reproducibility.yml index 5e3428a9f..c28f130a0 100644 --- a/.github/workflows/lockfile-reproducibility.yml +++ b/.github/workflows/lockfile-reproducibility.yml @@ -10,6 +10,9 @@ concurrency: group: noema-lockfile-reproducibility-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: verify: name: verify @@ -50,48 +53,37 @@ jobs: test "$(node --version)" = "v24.19.0" test "$(npm --version)" = "11.17.0" - - name: regenerate canonical lockfile in disposable workspace - id: regenerate + - name: verify committed lockfile in disposable workspace shell: bash run: | set -euo pipefail - regeneration_root="$RUNNER_TEMP/noema-lockfile-regeneration" - rm -rf "$regeneration_root" - mkdir -p "$regeneration_root" - cp package.json package-lock.json .npmrc "$regeneration_root/" + verification_root="$RUNNER_TEMP/noema-lockfile-verification" + receipt="$RUNNER_TEMP/noema-lockfile-reproducibility.txt" + rm -rf "$verification_root" + mkdir -p "$verification_root" + cp package.json package-lock.json .npmrc "$verification_root/" ( - cd "$regeneration_root" - npm install \ - --package-lock-only \ + cd "$verification_root" + npm ci \ --ignore-scripts \ --no-audit \ --no-fund \ --legacy-peer-deps=false \ --install-links=false ) - cp "$regeneration_root/package-lock.json" "$RUNNER_TEMP/noema-package-lock-regenerated.json" - if cmp -s package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json"; then - printf 'match=true\n' >> "$GITHUB_OUTPUT" - else - printf 'match=false\n' >> "$GITHUB_OUTPUT" - diff -u package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json" \ - > "$RUNNER_TEMP/noema-package-lock-regeneration.diff" || true - fi + { + printf 'source_sha=%s\n' "$(git rev-parse HEAD)" + printf 'node_version=%s\n' "$(node --version)" + printf 'npm_version=%s\n' "$(npm --version)" + printf 'package_json_sha256=%s\n' "$(sha256sum package.json | cut -d' ' -f1)" + printf 'package_lock_sha256=%s\n' "$(sha256sum package-lock.json | cut -d' ' -f1)" + printf 'npm_ci=verified\n' + } >"$receipt" - - name: upload regenerated lockfile evidence - if: always() + - name: upload lockfile verification evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: noema-lockfile-reproducibility-${{ github.event.pull_request.head.sha || github.sha }} - path: | - ${{ runner.temp }}/noema-package-lock-regenerated.json - ${{ runner.temp }}/noema-package-lock-regeneration.diff + path: ${{ runner.temp }}/noema-lockfile-reproducibility.txt if-no-files-found: error retention-days: 1 - - - name: require committed lockfile reproducibility - if: steps.regenerate.outputs.match != 'true' - shell: bash - run: | - printf '::error::package-lock.json is not the canonical output of the pinned Node/npm toolchain.\n' - exit 1 From 0ef13b92321c1b3835b9cdca8206fbfab3fcf466 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:19:14 +0900 Subject: [PATCH 12/33] fix(ci): isolate validator dependencies from Worker builders --- .github/workflows/patch-validator-image.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 89ed4139b..cdb241a55 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -147,7 +147,11 @@ jobs: npm_config_os=wasip1-threads \ npm_config_cpu=wasm32 \ npm ci --include=optional --ignore-scripts --no-audit --no-fund - npm pkg delete devDependencies.@cloudflare/workers-types devDependencies.wrangler + npm pkg delete \ + devDependencies.@cloudflare/workers-types \ + devDependencies.wrangler \ + devDependencies.workerd \ + devDependencies.esbuild timeout --signal=TERM --kill-after=30s 5m env \ npm_config_os=wasip1-threads \ npm_config_cpu=wasm32 \ @@ -160,6 +164,8 @@ jobs: test ! -e node_modules/@cloudflare/workers-types test ! -e node_modules/wrangler test ! -e node_modules/workerd + test ! -e node_modules/esbuild + test ! -e node_modules/@esbuild test ! -e node_modules/miniflare ) From 59a575689a655f85bc0477191eaf03f2e08c4d5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:29:36 +0900 Subject: [PATCH 13/33] test(ci): require actual lockfile regeneration evidence --- test/lockfile-reproducibility-workflow.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/lockfile-reproducibility-workflow.test.ts b/test/lockfile-reproducibility-workflow.test.ts index a6a6f9209..576915820 100644 --- a/test/lockfile-reproducibility-workflow.test.ts +++ b/test/lockfile-reproducibility-workflow.test.ts @@ -10,7 +10,7 @@ function readWorkflow(path: string): string { } describe("Cloudflare toolchain lockfile and validator isolation", () => { - it("verifies the committed lock with a read-only token and lockfile-pinned npm ci", () => { + it("regenerates the canonical lock in isolation before comparing and installing it", () => { const workflow = readWorkflow(lockfileWorkflowPath); const jobsStart = workflow.indexOf("\njobs:"); @@ -18,10 +18,12 @@ describe("Cloudflare toolchain lockfile and validator isolation", () => { expect(workflow.slice(0, jobsStart)).toContain( "permissions:\n contents: read", ); + expect(workflow).toContain("npm install"); + expect(workflow).toContain("--package-lock-only"); + expect(workflow).toContain("cmp --silent package-lock.json"); expect(workflow).toContain("npm ci"); - expect(workflow).not.toMatch(/\bnpm\s+(?:install|i)\b/u); - expect(workflow).toContain("package-lock.json"); expect(workflow).toContain("persist-credentials: false"); + expect(workflow).toContain("upload regenerated lockfile evidence"); expect(workflow).toContain( "test \"$(git rev-parse HEAD)\" = \"$NOEMA_EXPECTED_HEAD_SHA\"", ); @@ -37,4 +39,4 @@ describe("Cloudflare toolchain lockfile and validator isolation", () => { expect(workflow).toContain("test ! -e node_modules/@esbuild"); expect(workflow).toContain("test ! -e node_modules/miniflare"); }); -}); +}); \ No newline at end of file From 1fddc6d2ce536b11dac6d4ecc569d82cf17126a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 22:30:15 +0900 Subject: [PATCH 14/33] fix(ci): restore exact lockfile regeneration proof --- .../workflows/lockfile-reproducibility.yml | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lockfile-reproducibility.yml b/.github/workflows/lockfile-reproducibility.yml index c28f130a0..b0edf0ad4 100644 --- a/.github/workflows/lockfile-reproducibility.yml +++ b/.github/workflows/lockfile-reproducibility.yml @@ -53,7 +53,54 @@ jobs: test "$(node --version)" = "v24.19.0" test "$(npm --version)" = "11.17.0" - - name: verify committed lockfile in disposable workspace + - name: regenerate canonical lockfile in disposable workspace + id: regenerate + shell: bash + run: | + set -euo pipefail + regeneration_root="$RUNNER_TEMP/noema-lockfile-regeneration" + rm -rf "$regeneration_root" + mkdir -p "$regeneration_root" + cp package.json package-lock.json .npmrc "$regeneration_root/" + ( + cd "$regeneration_root" + npm install \ + --package-lock-only \ + --ignore-scripts \ + --no-audit \ + --no-fund \ + --legacy-peer-deps=false \ + --install-links=false + ) + cp "$regeneration_root/package-lock.json" "$RUNNER_TEMP/noema-package-lock-regenerated.json" + if cmp --silent package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json"; then + printf 'match=true\n' >> "$GITHUB_OUTPUT" + else + printf 'match=false\n' >> "$GITHUB_OUTPUT" + diff -u package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json" \ + > "$RUNNER_TEMP/noema-package-lock-regeneration.diff" || true + fi + + - name: upload regenerated lockfile evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: noema-lockfile-regeneration-${{ github.event.pull_request.head.sha || github.sha }} + path: | + ${{ runner.temp }}/noema-package-lock-regenerated.json + ${{ runner.temp }}/noema-package-lock-regeneration.diff + if-no-files-found: error + retention-days: 1 + + - name: require committed lockfile reproducibility + if: steps.regenerate.outputs.match != 'true' + shell: bash + run: | + printf '::error::package-lock.json is not the canonical output of the pinned Node/npm toolchain.\n' + exit 1 + + - name: verify committed lockfile install in disposable workspace + if: steps.regenerate.outputs.match == 'true' shell: bash run: | set -euo pipefail @@ -77,10 +124,12 @@ jobs: printf 'npm_version=%s\n' "$(npm --version)" printf 'package_json_sha256=%s\n' "$(sha256sum package.json | cut -d' ' -f1)" printf 'package_lock_sha256=%s\n' "$(sha256sum package-lock.json | cut -d' ' -f1)" + printf 'regenerated_match=true\n' printf 'npm_ci=verified\n' } >"$receipt" - - name: upload lockfile verification evidence + - name: upload lockfile verification receipt + if: steps.regenerate.outputs.match == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: noema-lockfile-reproducibility-${{ github.event.pull_request.head.sha || github.sha }} From 39683c71c2bcb5f13a662b408dfb9e5a50bbdae2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:12:19 +0900 Subject: [PATCH 15/33] test(deploy): retain declarative Durable Object exports --- test/cloudflare-worker-config.test.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/cloudflare-worker-config.test.mjs b/test/cloudflare-worker-config.test.mjs index 4d0e3118f..d4495972f 100644 --- a/test/cloudflare-worker-config.test.mjs +++ b/test/cloudflare-worker-config.test.mjs @@ -38,7 +38,7 @@ afterEach(async () => { }); describe("Noema Worker configuration adapter", () => { - it("preserves the owned Worker identity, Durable Object binding, and plain-text vars", async () => { + it("preserves Worker identity, Durable Object bindings/exports, and plain-text vars", async () => { const root = await fixture(validConfig); await expect(readNoemaWorkerConfig(root)).resolves.toEqual({ @@ -48,6 +48,9 @@ describe("Noema Worker configuration adapter", () => { durableObjects: [ { name: "NOEMA_RATE_LIMITER", class_name: "NoemaRateLimiter" }, ], + exports: { + NoemaRateLimiter: { type: "durable-object", storage: "sqlite" }, + }, vars: { ALLOWED_ISSUER: "https://token.actions.githubusercontent.com", }, From 684bf60b25be0fcc207b32999519a1dfc20ad4b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:12:40 +0900 Subject: [PATCH 16/33] test(deploy): require lifecycle exports in version upload --- test/cloudflare-toolchain-license-boundary.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cloudflare-toolchain-license-boundary.test.ts b/test/cloudflare-toolchain-license-boundary.test.ts index ef146bad2..857e1807c 100644 --- a/test/cloudflare-toolchain-license-boundary.test.ts +++ b/test/cloudflare-toolchain-license-boundary.test.ts @@ -32,14 +32,15 @@ describe("Cloudflare Worker toolchain license boundary", () => { } }); - it("uses a direct Cloudflare API deployment boundary with immutable source annotations", () => { + it("uses a direct Cloudflare API deployment boundary with immutable source and lifecycle metadata", () => { const deploy = readFileSync( new URL("../scripts/cloudflare-worker-deploy.mjs", import.meta.url), "utf8", ); - expect(deploy).toContain("/workers/scripts/${encodeURIComponent(scriptName)}/versions"); + expect(deploy).toContain("/workers/scripts/${encodedScript}/versions"); expect(deploy).toContain('type: "durable_object_namespace"'); + expect(deploy).toContain("exports: config.exports"); expect(deploy).toContain('"workers/commit_sha"'); expect(deploy).toContain("CLOUDFLARE_API_TOKEN"); expect(deploy).not.toContain("wrangler"); From c4e9e2a95f90ebda4486460ded3c71b1748986c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:13:33 +0900 Subject: [PATCH 17/33] test(deploy): allow declarative creation of new durable objects --- test/cloudflare-worker-config.test.mjs | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/test/cloudflare-worker-config.test.mjs b/test/cloudflare-worker-config.test.mjs index d4495972f..eb4a97f86 100644 --- a/test/cloudflare-worker-config.test.mjs +++ b/test/cloudflare-worker-config.test.mjs @@ -2,7 +2,10 @@ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { readNoemaWorkerConfig } from "../scripts/lib/cloudflare-worker-config.mjs"; +import { + readNoemaWorkerConfig, + validateExistingDurableObjectBindings, +} from "../scripts/lib/cloudflare-worker-config.mjs"; const temporaryRoots = []; @@ -89,4 +92,33 @@ describe("Noema Worker configuration adapter", () => { /must remain durable-object\/sqlite/u, ); }); + + it("permits a newly declared Durable Object while rejecting drift in an existing binding", () => { + const config = { + durableObjects: [ + { name: "NOEMA_RATE_LIMITER", class_name: "NoemaRateLimiter" }, + { name: "NOEMA_WORKFLOW_STATE", class_name: "NoemaWorkflowState" }, + ], + }; + + expect(() => validateExistingDurableObjectBindings(config, { + bindings: [ + { + type: "durable_object_namespace", + name: "NOEMA_RATE_LIMITER", + class_name: "NoemaRateLimiter", + }, + ], + })).not.toThrow(); + + expect(() => validateExistingDurableObjectBindings(config, { + bindings: [ + { + type: "durable_object_namespace", + name: "NOEMA_RATE_LIMITER", + class_name: "WrongClass", + }, + ], + })).toThrow(/Existing Durable Object binding does not match NOEMA_RATE_LIMITER/u); + }); }); From 10c6e80e369b5971558a3be8e03fe7bd99c25d81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:14:13 +0900 Subject: [PATCH 18/33] fix(deploy): retain durable object lifecycle authority --- scripts/lib/cloudflare-worker-config.mjs | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/lib/cloudflare-worker-config.mjs b/scripts/lib/cloudflare-worker-config.mjs index 92583d11e..f446d019f 100644 --- a/scripts/lib/cloudflare-worker-config.mjs +++ b/scripts/lib/cloudflare-worker-config.mjs @@ -14,6 +14,30 @@ function assignUnique(target, key, value, context) { target[key] = value; } +/** + * Validate already-provisioned Durable Object bindings without rejecting newly declared exports. + * + * A missing binding is allowed because Cloudflare's declarative `exports` reconciliation creates + * a new namespace during the version upload. If a binding already exists, however, its type and + * class identity must match exactly so a deployment cannot silently attach Noema to foreign state. + */ +export function validateExistingDurableObjectBindings(config, settings) { + const bindings = Array.isArray(settings?.bindings) ? settings.bindings : []; + const current = new Map(bindings.map((binding) => [binding?.name, binding])); + + for (const durableObject of config.durableObjects) { + const binding = current.get(durableObject.name); + if (binding === undefined) continue; + if ( + binding?.type !== "durable_object_namespace" + || binding?.class_name !== durableObject.class_name + ) { + throw new Error(`Existing Durable Object binding does not match ${durableObject.name}`); + } + } + return current; +} + /** * Read the narrow Worker configuration surface that Noema owns. * @@ -111,11 +135,19 @@ export async function readNoemaWorkerConfig(repositoryRoot) { throw new Error("Every Worker export must correspond to exactly one Durable Object binding"); } + const exports = Object.fromEntries( + [...exportsByClass.entries()].map(([className, exported]) => [ + className, + Object.freeze({ ...exported }), + ]), + ); + return Object.freeze({ name: root.name, main: root.main, compatibilityDate: root.compatibility_date, durableObjects: durableObjects.map((binding) => Object.freeze({ ...binding })), + exports: Object.freeze(exports), vars: Object.freeze({ ...vars }), }); } From 035aa8e4c2f40ea197b57531ce2257ff9ad8eb74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:15:01 +0900 Subject: [PATCH 19/33] fix(deploy): reconcile durable object exports --- scripts/cloudflare-worker-deploy.mjs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/scripts/cloudflare-worker-deploy.mjs b/scripts/cloudflare-worker-deploy.mjs index ca81127e6..db25df84a 100644 --- a/scripts/cloudflare-worker-deploy.mjs +++ b/scripts/cloudflare-worker-deploy.mjs @@ -6,7 +6,10 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { build } from "esbuild"; -import { readNoemaWorkerConfig } from "./lib/cloudflare-worker-config.mjs"; +import { + readNoemaWorkerConfig, + validateExistingDurableObjectBindings, +} from "./lib/cloudflare-worker-config.mjs"; const API_ORIGIN = "https://api.cloudflare.com"; const API_PREFIX = "/client/v4"; @@ -81,27 +84,13 @@ async function cloudflareJson(url, token, operation, init = {}) { return parseCloudflareResponse(response, operation); } -function currentBindingMap(settings) { - const bindings = Array.isArray(settings?.bindings) ? settings.bindings : []; - return new Map(bindings.map((binding) => [binding?.name, binding])); -} - function verifyExistingRuntimeBindings(config, settings) { - const current = currentBindingMap(settings); + const current = validateExistingDurableObjectBindings(config, settings); for (const secretName of REQUIRED_SECRET_BINDINGS) { if (current.get(secretName)?.type !== "secret_text") { throw new Error(`Existing Worker is missing required secret binding: ${secretName}`); } } - for (const durableObject of config.durableObjects) { - const binding = current.get(durableObject.name); - if ( - binding?.type !== "durable_object_namespace" - || binding?.class_name !== durableObject.class_name - ) { - throw new Error(`Existing Durable Object binding does not match ${durableObject.name}`); - } - } return current; } @@ -178,6 +167,7 @@ async function main() { "workers/message": `Noema source ${sourceSha}`, "workers/tag": sourceSha.slice(0, 12), }, + exports: config.exports, bindings: uploadBindings(config, currentBindings), }; const form = new FormData(); From 58073e62461cea81e655ededdc7b56e0e64e9566 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:19:46 +0900 Subject: [PATCH 20/33] test(dev): require stable durable object local identity --- test/cloudflare-worker-config.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/cloudflare-worker-config.test.mjs b/test/cloudflare-worker-config.test.mjs index eb4a97f86..7ebec132f 100644 --- a/test/cloudflare-worker-config.test.mjs +++ b/test/cloudflare-worker-config.test.mjs @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { + localDurableObjectStorageKey, readNoemaWorkerConfig, validateExistingDurableObjectBindings, } from "../scripts/lib/cloudflare-worker-config.mjs"; @@ -121,4 +122,20 @@ describe("Noema Worker configuration adapter", () => { ], })).toThrow(/Existing Durable Object binding does not match NOEMA_RATE_LIMITER/u); }); + + it("keeps local Durable Object storage identity stable across class renames and declaration order", () => { + const original = { name: "NOEMA_RATE_LIMITER", class_name: "NoemaRateLimiter" }; + const renamedClass = { name: "NOEMA_RATE_LIMITER", class_name: "RenamedRateLimiter" }; + const other = { name: "NOEMA_OIDC_REPLAY_GUARD", class_name: "NoemaOidcReplayGuard" }; + + expect(localDurableObjectStorageKey(original)).toBe(localDurableObjectStorageKey(renamedClass)); + expect(localDurableObjectStorageKey(original)).toBe("noema-local-NOEMA_RATE_LIMITER"); + expect([ + localDurableObjectStorageKey(original), + localDurableObjectStorageKey(other), + ]).toEqual([ + "noema-local-NOEMA_RATE_LIMITER", + "noema-local-NOEMA_OIDC_REPLAY_GUARD", + ]); + }); }); From cfcccc8b3a5e5ec47f70373acd55e149b14f5bb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:20:40 +0900 Subject: [PATCH 21/33] fix(dev): derive durable object local identity from binding --- scripts/lib/cloudflare-worker-config.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/lib/cloudflare-worker-config.mjs b/scripts/lib/cloudflare-worker-config.mjs index f446d019f..6d7e1a595 100644 --- a/scripts/lib/cloudflare-worker-config.mjs +++ b/scripts/lib/cloudflare-worker-config.mjs @@ -14,6 +14,17 @@ function assignUnique(target, key, value, context) { target[key] = value; } +/** + * Derive the persistent local workerd namespace identity from the binding authority. + * + * Workerd uses `uniqueKey` as the durable namespace identity. Binding order and implementation + * class names may change without intending to replace a namespace, so neither can participate in + * the key. Renaming the binding is the explicit local namespace replacement boundary. + */ +export function localDurableObjectStorageKey(binding) { + return `noema-local-${binding.name}`; +} + /** * Validate already-provisioned Durable Object bindings without rejecting newly declared exports. * From c09f1cb2df04baa2989f2a1f676d19f13c0321b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:21:17 +0900 Subject: [PATCH 22/33] fix(dev): preserve local durable object namespace identity --- scripts/cloudflare-worker-dev.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/cloudflare-worker-dev.mjs b/scripts/cloudflare-worker-dev.mjs index 47391eb59..145b5c3ea 100644 --- a/scripts/cloudflare-worker-dev.mjs +++ b/scripts/cloudflare-worker-dev.mjs @@ -5,7 +5,10 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { build } from "esbuild"; -import { readNoemaWorkerConfig } from "./lib/cloudflare-worker-config.mjs"; +import { + localDurableObjectStorageKey, + readNoemaWorkerConfig, +} from "./lib/cloudflare-worker-config.mjs"; const REQUIRED_LOCAL_SECRETS = ["GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PEM"]; const OPTIONAL_LOCAL_SECRETS = ["GITHUB_APP_INSTALLATION_ID"]; @@ -43,10 +46,10 @@ function bindingLines(config) { } function durableObjectNamespaceLines(config) { - return config.durableObjects.map(({ class_name }, index) => [ + return config.durableObjects.map((binding) => [ " (", - ` className = ${capnpText(class_name)},`, - ` uniqueKey = ${capnpText(`noema-local-${index + 1}-${class_name}`)},`, + ` className = ${capnpText(binding.class_name)},`, + ` uniqueKey = ${capnpText(localDurableObjectStorageKey(binding))},`, " enableSql = true", " )", ].join("\n")).join(",\n"); From 5c17c828d618a0c7d8d31d90ef96de85f75b379d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:44:31 +0000 Subject: [PATCH 23/33] fix(toolchain): regenerate lockfile and repair stale test fixtures Regenerate package-lock.json with the exact pinned toolchain (Node.js 24.19.0 / npm 11.17.0) to remove the remaining Wrangler/Miniflare/Sharp/Libvips (LGPL-3.0) dependency path the PR description flagged as the last causal gap. Verified byte-identical to the lockfile-reproducibility workflow's own fresh-directory regeneration. Repair three test fixtures that had drifted from already-correct production changes on this branch, each confirmed against a Node 24.19.0/npm 11.17.0 run: - test/upload-artifact-node24-integrity.test.ts: add the new lockfile-reproducibility.yml workflow to the reviewed upload-artifact inventory (its two uses already pin the reviewed SHA). - test/patch-validator-image-contract.test.ts: match the current multi-line `npm pkg delete` block, which now also strips workerd and esbuild (added by this PR) from the validator image, plus the corresponding node_modules absence checks. - test/cloudflare-worker-config.test.mjs: move the "unsupported root key" fixture's new field ahead of the `[vars]` section header. TOML is section-scoped, so appending it after `[vars]` exercised the (intentionally open-ended) vars path instead of the root-key allowlist the test means to cover; the parser itself was already correct. Confirmed via `npm run typecheck` and the full `vitest` suite on the exact pinned toolchain. Remaining local failures (acquisition symlink/owner-mode checks, a SIGTERM-reaping timing test) reproduce only because this sandbox runs as root/uid 0, unlike the CI runner, and are not touched here. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- CHANGELOG.md | 1 + package-lock.json | 1006 ++--------------- test/cloudflare-worker-config.test.mjs | 13 +- test/patch-validator-image-contract.test.ts | 10 +- test/upload-artifact-node24-integrity.test.ts | 1 + 5 files changed, 93 insertions(+), 938 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27019e507..1f532bb70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- Wrangler/Miniflare/Sharp/Libvips 제거 후 정확히 Node.js 24.19.0/npm 11.17.0으로 `package-lock.json`을 재생성해 남아 있던 GPL-family 의존성 경로(`node_modules/wrangler`, `node_modules/miniflare`, `node_modules/sharp`, `@img/sharp-libvips-*`, LGPL-3.0)를 제거한다. Node 24 supply-chain 계약 테스트가 새 `lockfile-reproducibility` 워크플로를 검토된 `actions/upload-artifact` 인벤토리에 포함하고, patch-validator 이미지 워크플로의 `npm pkg delete`가 `workerd`/`esbuild`까지 devDependencies에서 제거하는 다중 라인 형태를 검증하도록 갱신한다. Worker 설정 파서 회귀 테스트의 "root field silently omitted" 픽스처가 TOML 섹션 스코프상 실제로 `[vars]` 섹션에 귀속되던 위치 오류를 수정해, 인식되지 않은 root-level 키가 여전히 root 섹션에서 거부되는지를 올바르게 검증한다. - Workflow / Task Execution은 untrusted DAG를 execution/plan identity에 결합한 detached immutable snapshot으로 승인하고, validated array bounds 안에서만 task/dependency/state evidence를 읽는다. runnable 선택은 cross-execution·foreign·duplicate·non-canonical evidence, admitted concurrency를 초과한 running state, 성공하지 않은 prerequisite 뒤에 존재하는 causally impossible executed state를 실패-폐쇄하며, 선택 결과는 reservation이나 side-effect authority가 아닌 후보임을 명시한다. Agent Runtime lifecycle·State & Checkpoint·Workflow admission은 null·throwing accessor·revoked proxy 같은 malformed runtime input의 임의 JavaScript 예외를 각 bounded-context domain error로 정규화한다. - State & Checkpoint admission은 accepted/replay 결과와 내부 checkpoint를 모두 caller-owned alias에서 분리한 frozen snapshot으로 반환한다. TypeScript `readonly`만으로는 막을 수 없는 JavaScript 런타임 alias mutation이 승인된 checkpoint authority나 `accepted`/`replay` 분류를 사후 변경하지 못하도록 실패-폐쇄한다. - Noema의 필수 PR 워크플로 `ci`, `reviewer-ci`, `patch-validator-image`를 부동 `ubuntu-latest` 대신 명시적 `ubuntu-24.04` GitHub-hosted runner에 고정하고, 인용 여부와 무관하게 `ubuntu-latest` 회귀를 탐지하는 계약 테스트를 추가해 pre-checkout runner-assignment stall의 repository-owned selector 원인을 제거한다. 중앙 `Security Scan`의 runner/control-plane 권한은 별도 `.github` owner 경계에 유지한다. diff --git a/package-lock.json b/package-lock.json index 91da46972..f9e787137 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,10 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20260630.0", "@vitest/coverage-v8": "^4.1.9", + "esbuild": "0.28.1", "typescript": "^5.9.0", "vitest": "^4.1.9", - "wrangler": "^4.25.0" + "workerd": "1.20260625.1" }, "engines": { "node": ">=22" @@ -78,32 +79,6 @@ "node": ">=18" } }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", - "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", - "dev": true, - "license": "MIT OR Apache-2.0", - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", - "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.24", - "workerd": ">1.20260305.0 <2.0.0-0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, "node_modules/@cloudflare/workerd-darwin-64": { "version": "1.20260625.1", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260625.1.tgz", @@ -196,19 +171,6 @@ "dev": true, "license": "MIT OR Apache-2.0" }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -523,693 +485,166 @@ "x64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ - "s390x" + "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + "node": ">=18" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.1" - }, + "os": [ + "openharmony" + ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ - "wasm32" + "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, + "os": [ + "sunos" + ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": "^20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1229,17 +664,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -1269,35 +693,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@poppinss/colors": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^4.1.5" - } - }, - "node_modules/@poppinss/dumper": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", - "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@sindresorhus/is": "^7.0.2", - "supports-color": "^10.0.0" - } - }, - "node_modules/@poppinss/exception": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", - "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", - "dev": true, - "license": "MIT" - }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", @@ -1562,26 +957,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", - "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1802,13 +1177,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true, - "license": "MIT" - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1826,20 +1194,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1850,16 +1204,6 @@ "node": ">=8" } }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/es-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", @@ -2038,16 +1382,6 @@ "dev": true, "license": "MIT" }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2347,27 +1681,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/miniflare": { - "version": "4.20260625.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz", - "integrity": "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.34.5", - "undici": "7.28.0", - "workerd": "1.20260625.1", - "ws": "8.21.0", - "youch": "4.1.0-beta.10" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -2401,13 +1714,6 @@ "node": ">=12.20.0" } }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2511,56 +1817,6 @@ "node": ">=10" } }, - "node_modules/sharp": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.1.0", - "detect-libc": "^2.1.2", - "semver": "^7.8.5" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.3", - "@img/sharp-darwin-x64": "0.35.3", - "@img/sharp-freebsd-wasm32": "0.35.3", - "@img/sharp-libvips-darwin-arm64": "1.3.2", - "@img/sharp-libvips-darwin-x64": "1.3.2", - "@img/sharp-libvips-linux-arm": "1.3.2", - "@img/sharp-libvips-linux-arm64": "1.3.2", - "@img/sharp-libvips-linux-ppc64": "1.3.2", - "@img/sharp-libvips-linux-riscv64": "1.3.2", - "@img/sharp-libvips-linux-s390x": "1.3.2", - "@img/sharp-libvips-linux-x64": "1.3.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", - "@img/sharp-libvips-linuxmusl-x64": "1.3.2", - "@img/sharp-linux-arm": "0.35.3", - "@img/sharp-linux-arm64": "0.35.3", - "@img/sharp-linux-ppc64": "0.35.3", - "@img/sharp-linux-riscv64": "0.35.3", - "@img/sharp-linux-s390x": "0.35.3", - "@img/sharp-linux-x64": "0.35.3", - "@img/sharp-linuxmusl-arm64": "0.35.3", - "@img/sharp-linuxmusl-x64": "0.35.3", - "@img/sharp-webcontainers-wasm32": "0.35.3", - "@img/sharp-win32-arm64": "0.35.3", - "@img/sharp-win32-ia32": "0.35.3", - "@img/sharp-win32-x64": "0.35.3" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2592,19 +1848,6 @@ "dev": true, "license": "MIT" }, - "node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2671,26 +1914,6 @@ "node": ">=14.17" } }, - "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3" - } - }, "node_modules/vite": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.1.tgz", @@ -2896,89 +2119,6 @@ "@cloudflare/workerd-linux-arm64": "1.20260625.1", "@cloudflare/workerd-windows-64": "1.20260625.1" } - }, - "node_modules/wrangler": { - "version": "4.105.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz", - "integrity": "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.5.0", - "@cloudflare/unenv-preset": "2.16.1", - "blake3-wasm": "2.1.5", - "esbuild": "0.28.1", - "miniflare": "4.20260625.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.24", - "workerd": "1.20260625.1" - }, - "bin": { - "cf-wrangler": "bin/cf-wrangler.js", - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=22.0.0" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20260625.1" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/youch": { - "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@poppinss/dumper": "^0.6.4", - "@speed-highlight/core": "^1.2.7", - "cookie": "^1.0.2", - "youch-core": "^0.3.3" - } - }, - "node_modules/youch-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/exception": "^1.2.2", - "error-stack-parser-es": "^1.0.5" - } } } } diff --git a/test/cloudflare-worker-config.test.mjs b/test/cloudflare-worker-config.test.mjs index 7ebec132f..e54bd4da5 100644 --- a/test/cloudflare-worker-config.test.mjs +++ b/test/cloudflare-worker-config.test.mjs @@ -70,10 +70,19 @@ describe("Noema Worker configuration adapter", () => { }); it("fails closed when a root field would be silently omitted", async () => { - const root = await fixture(`${validConfig}\ncompatibility_flags = "nodejs_compat"\n`); + // The unrecognized key must appear while the parser is still in the "root" section + // (i.e. before any `[[...]]`/`[section]` header). TOML section scoping means a line + // appended after `[vars]` belongs to `vars`, not root, and Noema's vars section is + // intentionally open-ended (operator-configured key/value pairs) rather than allow-listed. + const root = await fixture( + validConfig.replace( + 'compatibility_date = "2026-06-30"', + 'compatibility_date = "2026-06-30"\ncompatibility_flags = "nodejs_compat"', + ), + ); await expect(readNoemaWorkerConfig(root)).rejects.toThrow( - /Unsupported root Worker key/u, + /Unsupported root Worker key: compatibility_flags/u, ); }); diff --git a/test/patch-validator-image-contract.test.ts b/test/patch-validator-image-contract.test.ts index 53be0d0e9..10d698699 100644 --- a/test/patch-validator-image-contract.test.ts +++ b/test/patch-validator-image-contract.test.ts @@ -154,9 +154,11 @@ describe("patch-validator image contract", () => { expect(dockerfile).not.toContain("npm_config_cpu=wasm32"); expect(imageWorkflow).toContain("npm_config_os=wasip1-threads"); expect(imageWorkflow).toContain("npm_config_cpu=wasm32"); - expect(imageWorkflow).toContain( - "npm pkg delete devDependencies.@cloudflare/workers-types devDependencies.wrangler", - ); + expect(imageWorkflow).toContain("npm pkg delete \\"); + expect(imageWorkflow).toContain("devDependencies.@cloudflare/workers-types \\"); + expect(imageWorkflow).toContain("devDependencies.wrangler \\"); + expect(imageWorkflow).toContain("devDependencies.workerd \\"); + expect(imageWorkflow).toContain("devDependencies.esbuild"); expect(imageWorkflow).toContain( "npm prune --include=optional --ignore-scripts --no-audit --no-fund", ); @@ -166,6 +168,8 @@ describe("patch-validator image contract", () => { expect(imageWorkflow).toContain("test ! -e node_modules/@cloudflare/workers-types"); expect(imageWorkflow).toContain("test ! -e node_modules/wrangler"); expect(imageWorkflow).toContain("test ! -e node_modules/workerd"); + expect(imageWorkflow).toContain("test ! -e node_modules/esbuild"); + expect(imageWorkflow).toContain("test ! -e node_modules/@esbuild"); expect(imageWorkflow).toContain("test ! -e node_modules/miniflare"); }); }); diff --git a/test/upload-artifact-node24-integrity.test.ts b/test/upload-artifact-node24-integrity.test.ts index e917f0b3a..da3d99e2f 100644 --- a/test/upload-artifact-node24-integrity.test.ts +++ b/test/upload-artifact-node24-integrity.test.ts @@ -12,6 +12,7 @@ const supportedWorkflowPaths = [ ".github/workflows/central-review.yml", ".github/workflows/hourly-commercial-readiness.yml", ".github/workflows/hourly-product-development.yml", + ".github/workflows/lockfile-reproducibility.yml", ".github/workflows/maintainer-app-readiness.yml", ".github/workflows/patch-validator-image.yml", ".github/workflows/private-vulnerability-reporting-audit.yml", From 889ceb0da591c430fd5ef15044162aaf06101269 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:15:53 +0900 Subject: [PATCH 24/33] test(ci): require lockfile proof on enabled CI identity --- test/lockfile-reproducibility-workflow.test.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/lockfile-reproducibility-workflow.test.ts b/test/lockfile-reproducibility-workflow.test.ts index 576915820..b011c7b2a 100644 --- a/test/lockfile-reproducibility-workflow.test.ts +++ b/test/lockfile-reproducibility-workflow.test.ts @@ -1,8 +1,9 @@ -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -const lockfileWorkflowPath = ".github/workflows/lockfile-reproducibility.yml"; +const ciWorkflowPath = ".github/workflows/ci.yml"; +const retiredLockfileWorkflowPath = ".github/workflows/lockfile-reproducibility.yml"; const validatorWorkflowPath = ".github/workflows/patch-validator-image.yml"; function readWorkflow(path: string): string { @@ -10,14 +11,10 @@ function readWorkflow(path: string): string { } describe("Cloudflare toolchain lockfile and validator isolation", () => { - it("regenerates the canonical lock in isolation before comparing and installing it", () => { - const workflow = readWorkflow(lockfileWorkflowPath); - const jobsStart = workflow.indexOf("\njobs:"); + it("keeps canonical lockfile regeneration on the established application CI identity", () => { + const workflow = readWorkflow(ciWorkflowPath); - expect(jobsStart).toBeGreaterThan(0); - expect(workflow.slice(0, jobsStart)).toContain( - "permissions:\n contents: read", - ); + expect(workflow).toContain("name: ci"); expect(workflow).toContain("npm install"); expect(workflow).toContain("--package-lock-only"); expect(workflow).toContain("cmp --silent package-lock.json"); @@ -27,6 +24,7 @@ describe("Cloudflare toolchain lockfile and validator isolation", () => { expect(workflow).toContain( "test \"$(git rev-parse HEAD)\" = \"$NOEMA_EXPECTED_HEAD_SHA\"", ); + expect(existsSync(retiredLockfileWorkflowPath)).toBe(false); }); it("prunes builder-only workerd and esbuild from patch-validator dependencies", () => { From 531c0b1e286b963e61e912c894e3752e19f62a1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:18:31 +0900 Subject: [PATCH 25/33] fix(ci): run lockfile reproducibility on enabled CI --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d83efcc04..62bbd85a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,6 +146,52 @@ jobs: console.log(`Lockfile change control passed for ${result.changedPackages.length} changed package node(s).`); NODE + - name: regenerate canonical lockfile in disposable workspace + id: regenerate_lockfile + shell: bash + run: | + set -euo pipefail + regeneration_root="$RUNNER_TEMP/noema-lockfile-regeneration" + rm -rf "$regeneration_root" + mkdir -p "$regeneration_root" + cp package.json package-lock.json .npmrc "$regeneration_root/" + ( + cd "$regeneration_root" + npm install \ + --package-lock-only \ + --ignore-scripts \ + --no-audit \ + --no-fund \ + --legacy-peer-deps=false \ + --install-links=false + ) + cp "$regeneration_root/package-lock.json" "$RUNNER_TEMP/noema-package-lock-regenerated.json" + if cmp --silent package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json"; then + printf 'match=true\n' >> "$GITHUB_OUTPUT" + else + printf 'match=false\n' >> "$GITHUB_OUTPUT" + diff -u package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json" \ + > "$RUNNER_TEMP/noema-package-lock-regeneration.diff" || true + fi + + - name: upload regenerated lockfile evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: noema-lockfile-regeneration-${{ github.event.pull_request.head.sha || github.sha }} + path: | + ${{ runner.temp }}/noema-package-lock-regenerated.json + ${{ runner.temp }}/noema-package-lock-regeneration.diff + if-no-files-found: error + retention-days: 1 + + - name: require committed lockfile reproducibility + if: steps.regenerate_lockfile.outputs.match != 'true' + shell: bash + run: | + printf '::error::package-lock.json is not the canonical output of the pinned Node/npm toolchain.\n' + exit 1 + - name: install run: npm ci --legacy-peer-deps=false --install-links=false From 49a66e4e27f9d223e15014e1231b81a8087fb8e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:18:47 +0900 Subject: [PATCH 26/33] fix(ci): retire disabled branch-only lockfile workflow --- .../workflows/lockfile-reproducibility.yml | 138 ------------------ 1 file changed, 138 deletions(-) delete mode 100644 .github/workflows/lockfile-reproducibility.yml diff --git a/.github/workflows/lockfile-reproducibility.yml b/.github/workflows/lockfile-reproducibility.yml deleted file mode 100644 index b0edf0ad4..000000000 --- a/.github/workflows/lockfile-reproducibility.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: lockfile-reproducibility - -on: - pull_request: - push: - branches: - - main - -concurrency: - group: noema-lockfile-reproducibility-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - verify: - name: verify - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - steps: - - name: checkout exact source - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - fetch-depth: 1 - persist-credentials: false - - - name: verify exact checkout - shell: bash - env: - NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - set -euo pipefail - if [[ ! "$NOEMA_EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then - printf '::error::Invalid expected head SHA.\n' - exit 1 - fi - test "$(git rev-parse HEAD)" = "$NOEMA_EXPECTED_HEAD_SHA" - - - name: setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "24.19.0" - cache: npm - - - name: verify package-manager identity - shell: bash - run: | - set -euo pipefail - test "$(node --version)" = "v24.19.0" - test "$(npm --version)" = "11.17.0" - - - name: regenerate canonical lockfile in disposable workspace - id: regenerate - shell: bash - run: | - set -euo pipefail - regeneration_root="$RUNNER_TEMP/noema-lockfile-regeneration" - rm -rf "$regeneration_root" - mkdir -p "$regeneration_root" - cp package.json package-lock.json .npmrc "$regeneration_root/" - ( - cd "$regeneration_root" - npm install \ - --package-lock-only \ - --ignore-scripts \ - --no-audit \ - --no-fund \ - --legacy-peer-deps=false \ - --install-links=false - ) - cp "$regeneration_root/package-lock.json" "$RUNNER_TEMP/noema-package-lock-regenerated.json" - if cmp --silent package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json"; then - printf 'match=true\n' >> "$GITHUB_OUTPUT" - else - printf 'match=false\n' >> "$GITHUB_OUTPUT" - diff -u package-lock.json "$RUNNER_TEMP/noema-package-lock-regenerated.json" \ - > "$RUNNER_TEMP/noema-package-lock-regeneration.diff" || true - fi - - - name: upload regenerated lockfile evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: noema-lockfile-regeneration-${{ github.event.pull_request.head.sha || github.sha }} - path: | - ${{ runner.temp }}/noema-package-lock-regenerated.json - ${{ runner.temp }}/noema-package-lock-regeneration.diff - if-no-files-found: error - retention-days: 1 - - - name: require committed lockfile reproducibility - if: steps.regenerate.outputs.match != 'true' - shell: bash - run: | - printf '::error::package-lock.json is not the canonical output of the pinned Node/npm toolchain.\n' - exit 1 - - - name: verify committed lockfile install in disposable workspace - if: steps.regenerate.outputs.match == 'true' - shell: bash - run: | - set -euo pipefail - verification_root="$RUNNER_TEMP/noema-lockfile-verification" - receipt="$RUNNER_TEMP/noema-lockfile-reproducibility.txt" - rm -rf "$verification_root" - mkdir -p "$verification_root" - cp package.json package-lock.json .npmrc "$verification_root/" - ( - cd "$verification_root" - npm ci \ - --ignore-scripts \ - --no-audit \ - --no-fund \ - --legacy-peer-deps=false \ - --install-links=false - ) - { - printf 'source_sha=%s\n' "$(git rev-parse HEAD)" - printf 'node_version=%s\n' "$(node --version)" - printf 'npm_version=%s\n' "$(npm --version)" - printf 'package_json_sha256=%s\n' "$(sha256sum package.json | cut -d' ' -f1)" - printf 'package_lock_sha256=%s\n' "$(sha256sum package-lock.json | cut -d' ' -f1)" - printf 'regenerated_match=true\n' - printf 'npm_ci=verified\n' - } >"$receipt" - - - name: upload lockfile verification receipt - if: steps.regenerate.outputs.match == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: noema-lockfile-reproducibility-${{ github.event.pull_request.head.sha || github.sha }} - path: ${{ runner.temp }}/noema-lockfile-reproducibility.txt - if-no-files-found: error - retention-days: 1 From efe407351345002633a0412582b4f5c838eccea0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:19:06 +0900 Subject: [PATCH 27/33] test(ci): track lockfile evidence on application CI --- test/upload-artifact-node24-integrity.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/upload-artifact-node24-integrity.test.ts b/test/upload-artifact-node24-integrity.test.ts index da3d99e2f..9b2d8bd2d 100644 --- a/test/upload-artifact-node24-integrity.test.ts +++ b/test/upload-artifact-node24-integrity.test.ts @@ -10,9 +10,9 @@ const supportedWorkflowPaths = [ ".github/workflows/acquisition-readiness-scan.yml", ".github/workflows/cd.yml", ".github/workflows/central-review.yml", + ".github/workflows/ci.yml", ".github/workflows/hourly-commercial-readiness.yml", ".github/workflows/hourly-product-development.yml", - ".github/workflows/lockfile-reproducibility.yml", ".github/workflows/maintainer-app-readiness.yml", ".github/workflows/patch-validator-image.yml", ".github/workflows/private-vulnerability-reporting-audit.yml", From 85589b198eae10e8ba6f2b7f62ff9ac330b43a83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:22:42 +0900 Subject: [PATCH 28/33] docs(gaps): reconcile live Noema commercial lanes --- docs/product-technical-gap-baseline.md | 52 ++++++++++++++------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b748d67da..dfc3484ef 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,48 +2,54 @@ ## Authority and update rule -이 문서는 제품 요구, 구현, 검증, 운영 증거 사이의 현재 차이를 한곳에서 추적한다. 저장소 파일과 테스트는 revision-local 또는 protected-source 구현만 증명한다. PR 상태는 exact head와 live base에서, 운영·배포·고객·매출·법적 증거는 해당 외부 권한에서 각각 다시 확인해야 한다. 문서나 성공 boolean만으로 이후 단계의 증거를 만들지 않는다. +이 문서는 제품 요구, 구현, 검증, 운영 증거 사이의 현재 차이를 추적한다. 저장소 파일과 테스트는 revision-local 또는 protected-source 구현만 증명한다. PR 상태는 exact head와 live base에서, 운영·배포·고객·매출·법적 증거는 해당 외부 권한에서 다시 확인해야 한다. 문서나 성공 boolean, predecessor check, cancelled/queued run으로 이후 단계의 증거를 만들지 않는다. -이 baseline의 protected-source snapshot은 `main@5aad3e410703faaf52882e2f33fadd25d217bcdd`이며, README/license candidate truth는 PR #530 exact head에만 적용한다. issues #3, #5, #27, #29, #66, #227, #531의 live 상태를 GitHub 권위로 다시 읽어야 하며, protected/main·PR·외부 증거를 서로 대체하지 않는다. +현재 protected-source snapshot은 `main@e1ac9d50f6c646f04be8c137c8acdc7200182fcd`다. 이 문서에서 PR candidate를 언급하는 경우 그 구현은 protected truth가 아니며, unchanged exact head의 검증과 정상 merge 뒤에만 protected implementation으로 승격한다. -## Live external observation — 2026-09-01 KST +## Live external observation — 2026-09-05 KST | Authority | Observation | Consequence | | --- | --- | --- | -| README/license lane | PR #530 is open and carries the product-first README plus Apache-2.0 root source grant; every push invalidates predecessor-head checks | protected main remains unlicensed until the unchanged exact head integrates | -| npm package boundary | `package.json` remains `private` and the npm package is not a product distribution channel; no package-publication license field is introduced | root `LICENSE` controls source rights without forcing unrelated lockfile metadata churn | -| Dependency licensing | `package-lock.json` contains `LGPL-3.0-or-later` optional dev/build packages on `wrangler → miniflare → sharp → @img/sharp-libvips-*`; issue #531 owns removal/replacement | source Apache-2.0 does not make the current toolchain compliant with the organization no-GPL-family default | -| Release/publication | immutable release/deployment/customer/revenue/transfer evidence remains a separate authority class | source licensing cannot be promoted into acquisition readiness | +| Protected Noema source | `main@e1ac9d50f6c646f04be8c137c8acdc7200182fcd` | 모든 candidate PR은 이 protected truth와 별도 revision-local authority다 | +| Toolchain/license lane | PR #540 exact head `efe407351345002633a0412582b4f5c838eccea0` replaces the Wrangler/Miniflare/Sharp/Libvips path with pinned workerd/esbuild and carries the regenerated lockfile | GPL-family toolchain removal is implemented only on the Draft candidate until exact-head gates and protected integration complete | +| Lockfile reproducibility | A predecessor hosted run proved stale committed lockfile bytes. The later branch-only `lockfile-reproducibility` workflow became non-rerunnable as a disabled workflow identity; #540 therefore moves canonical regeneration/evidence into established application `ci` and deletes that branch-only workflow | reproducibility remains a hard CI prerequisite; the workflow-registry defect is repaired in candidate source rather than bypassed | +| Reviewer semantic gate | PR #546 exact head `a20ea3065c44d37b4a66740d7d2098ffa55d3da8` repairs a hosted stale reviewer fixture after a real 1-failed/504-passed RED | all pre-#546 reviewer success is historical until semantic-review truth is integrated and regenerated | +| OIDC source authority | PR #527 exact head `179613b43d38c3c9e7b5e51b70234e4850c141f2` binds Noema's exact `job_workflow_sha` authority to audited central `.github/main@3f2f21c577804a473d3c63f87226948dd9b9257a` | central protected movement requires a fresh audit and exact pin roll-forward; unchanged trust-bearing blobs alone do not authorize a stale source commit | +| Hourly writer ownership | Noema still has a repository-local scheduled product writer on protected main. Candidate #551 is Draft because protected central coordinator admission is not yet compatible with Noema's contextual-orchestrator-only provider boundary | do not remove local schedule until a protected central manual-entrypoint/DDD contract can admit Noema without provider-direct credentials | +| Release/publication | no source change by itself establishes immutable release, deployment, customer, revenue or transfer evidence | release and acquisition readiness stay open until the external evidence chain exists | ## Current baseline | Requirement family | Canonical decision / boundary | Protected or active implementation surface | Executable proof | Residual evidence | Maturity | | --- | --- | --- | --- | --- | --- | -| Credential exchange and readiness | Worker trust contract와 runtime threat model | `src/index.ts`, `src/worker.ts`, `src/entrypoint.ts`, `src/runtime-entrypoint.ts`, OIDC/replay/rate-limit 모듈 | typecheck, runtime/API/security tests, exact configured coverage | protected deployment smoke와 실제 binding/storage 증거 | Implemented on protected main; operational evidence remains separate | -| Reviewer and maintenance control plane | 독립 App identity, bounded manifest, deterministic fail-closed gates | `reviewer/noema_reviewer/`, maintainer/reviewer workflows, capability-file ingress | reviewer tests, workflow contract tests, current-head review artifacts | Maintainer/Reviewer App 설치·권한·key custody·rotation 및 publication identity | Source contract implemented; external activation evidence is open | -| Hourly product-development loop | `contextual-orchestrator` inference와 별도 Maintainer App publication identity를 사용하는 work-conserving loop | `.github/workflows/hourly-product-development.yml`, orchestrator gateway contract, publication/readiness validators | workflow shape, gateway preflight, lease, publication prerequisite and stale-head refusal tests | zero-PR scheduled proposal publication과 rollback/recovery exercise | Implemented source; production activation incomplete | -| Patch-validator supply chain | exact source/image/receipt binding과 fail-closed vulnerability policy | `Dockerfile.patch-validator`, image workflow, validator/SBOM/receipt modules | build, runtime, smoke, SBOM, vulnerability and receipt tests | protected-main operational receipt와 registry publication/signing/attestation | Implemented source; operational/publication evidence incomplete | -| Source licensing | Noema-owned source uses one explicit commercial-friendly outbound grant; package publication and dependencies retain independent terms | PR #530 `LICENSE`, root `README.md`, `docs/LICENSING_AND_IP_TRANSFER.md`; private `package.json` remains non-distribution metadata | exact-head repository/doc/test consistency | protected integration plus third-party/tooling policy resolution | Apache-2.0 candidate truth on #530; not yet protected truth | -| Third-party/tooling licensing | GPL-family packages are not accepted as the normal inbound dependency baseline | current lockfile + dependency-license inventory + issue #531 | exact lockfile scan/inventory must become free of GPL/LGPL/AGPL toolchain entries | commercially compatible Wrangler/Miniflare/build-tool replacement or exact approved exception | Open compliance gap; source license does not resolve it | +| Credential exchange and readiness | Worker trust contract와 runtime threat model | `src/index.ts`, `src/worker.ts`, `src/entrypoint.ts`, `src/runtime-entrypoint.ts`, OIDC/replay/rate-limit modules | typecheck, runtime/API/security tests, exact configured coverage | protected deployment smoke와 실제 binding/storage evidence | Implemented on protected main; operational evidence remains separate | +| Reviewer and maintenance control plane | independent App identity, bounded manifest, deterministic fail-closed gates | `reviewer/noema_reviewer/`, maintainer/reviewer workflows, capability-file ingress | reviewer tests, workflow contract tests, current-head review artifacts | #546 protected integration, Maintainer/Reviewer App installation/permission/key-custody evidence | Source contract implemented; semantic-review prerequisite still open | +| Workflow / Task Execution | Noema owns workflow/task state, policy/approval and recovery semantics without copying foreign domain truth | workflow/task modules, state/checkpoint contracts, recovery/observability surfaces | unit/edge contract tests and exact-head CI | current protected integration of open execution lanes and operational recovery exercise | Implemented with active candidate increments | +| Hourly product-development writer | every Noema LLM call remains contextual-orchestrator-owned; central scheduling may dispatch only through an explicit compatible handoff | protected `.github/workflows/hourly-product-development.yml`; #551 is only a Draft handoff candidate | workflow-shape, gateway, lease/publication and stale-head refusal tests | protected central provider-neutral manual-entrypoint/DDD admission plus same-head Noema handoff test | Protected local writer remains canonical; central handoff incomplete | +| Patch-validator supply chain | exact source/image/receipt binding and fail-closed vulnerability policy | image workflow, validator/SBOM/receipt modules | build, runtime, smoke, SBOM, vulnerability and receipt tests | protected operational receipt and registry publication/signing/attestation | Implemented source; operational/publication evidence incomplete | +| Third-party/tooling licensing | GPL/LGPL/AGPL path is not accepted as normal inbound tooling baseline | protected lockfile plus #540 workerd/esbuild candidate | dependency inventory, canonical lock regeneration, install/typecheck/tests/security | #540 unchanged exact-head GREEN and protected merge | Candidate repair implemented; protected gap remains open | +| Lockfile reproducibility | generated dependency bytes must be reproducible under the pinned Node/npm toolchain and evidenced by an enabled canonical workflow identity | #540 moves regeneration into `.github/workflows/ci.yml` and retires branch-only workflow identity | disposable `npm install --package-lock-only`, byte comparison, immutable artifact, fail-before-install mismatch | exact #540 hosted CI GREEN | Candidate repair implemented; hosted acceptance pending | | Release and deployment | source → package/SBOM/provenance → immutable publication → deployment/rollback | release, publication, deployment and readiness scripts | exact-source/reproducibility/receipt/rollback contract tests | immutable release, protected deployment, recovery and production smoke evidence | Incomplete; repository evidence cannot establish deployment | -| KPI, customer and acquisition | authentic evidence must retain source, time and buyer/legal authority | KPI, acquisition manifest/integrity/readiness and license validators | bounded input, provenance, ordering, integrity and fail-closed tests | authentic 30-day production KPI, customer/revenue and transfer evidence | Incomplete; no commercial-readiness claim | +| KPI, customer and acquisition | authentic evidence retains source, time and buyer/legal authority | KPI, acquisition manifest/integrity/readiness validators | bounded input, provenance, ordering, integrity and fail-closed tests | authentic 30-day production KPI, customer/revenue and transfer evidence | Incomplete; no commercial-readiness claim | ## Prioritized residual gaps | Priority | Gap | Buyer/operator impact | Current owner | Authoritative completion evidence | Next executable action | | --- | --- | --- | --- | --- | --- | -| P0 | GPL-family development/build dependency path | 조직의 상업용 inbound 정책과 현재 npm toolchain이 충돌한다 | issue #531 | exact-head `package-lock.json`과 dependency inventory에서 GPL/LGPL/AGPL 경로가 사라지고 Worker dev/deploy·typecheck·tests·security가 그대로 통과 | Wrangler/Miniflare/Sharp 경로를 상업적으로 호환되는 도구 경계로 교체하고 lockfile을 재검증한다 | -| P0 | Maintainer/Reviewer App 및 hourly publication identity 활성화 | 자동 유지보수와 독립 리뷰가 production capability로 동작한다는 증거가 없다 | issues #29 / #227 | 현재 App 설치·권한·key custody/rotation, 성공한 scheduled publication artifact와 rollback 결과 | 외부 App 구성을 완료한 뒤 readiness와 scheduled run을 실행하고 artifact를 보존한다 | -| P0 | protected `main` governance 목표와 live policy 정합성 | source 검증만으로 실제 merge/release 통제를 보장할 수 없다 | issue #27 | live ruleset/branch-protection API와 관찰된 required workflow/status 결과 | governance audit을 live policy에 실행하고 차이를 owning control에서 수정한다 | -| P1 | Apache-2.0 source grant integration | 공개 저장소가 protected main에서는 아직 명시적 사용권을 제공하지 않는다 | PR #530 | unchanged exact-head README/LICENSE + applicable reviews/checks + protected merge | #530 exact head를 정상 protected path로 통합한다 | -| P1 | patch-validator 운영·배포 증거 | 검증된 source image가 실제 배포·서명·활성화됐는지 구매자가 확인할 수 없다 | issue #66 | protected-main operational receipt, registry digest, signature/attestation과 activation proof | exact protected source에서 publication pipeline을 실행한다 | -| P1 | authentic 30-day KPI | 신뢰성·성능·운영가치를 fixture가 아닌 실운영 자료로 입증하지 못한다 | issue #3 | production-origin, time-bound, integrity-checked 30-day KPI evidence | 승인된 production source에서 collector와 verifier를 실행한다 | -| P1 | release/deployment/acquisition evidence | buyer/legal/commercial 권한이 없어 매각 readiness를 선언할 수 없다 | issue #5 | immutable release/deployment/customer/revenue/legal transfer evidence | 앞선 evidence family를 순서대로 충족하고 acquisition audit을 재실행한다 | +| P0 | Semantic reviewer prerequisite | old reviewer successes can admit non-semantic evidence | PR #546 | unchanged exact head terminal CI/reviewer/Security/image evidence, valid threads resolved, protected merge | wait only for that lane's hosted gates; on failure perform RCA and repair | +| P0 | Toolchain/license and reproducible lock integration | commercial dependency policy and deterministic build evidence are not protected truth yet | PR #540 / issue #531 | exact-head CI regeneration + install/license/security/image evidence and protected merge | validate `efe407351...`; do not restore the disabled branch-only workflow | +| P0 | Exact central OIDC source pin | stale source commit rejects legitimate protected central reviews or weakens source identity if loosened | PR #527 | audited current central protected commit, matching exact Noema pin, exact-head gates, protected merge | re-audit on every central movement; never replace exact equality with a mutable ref | +| P0 | Maintainer/Reviewer App and hourly publication identity activation | automated maintenance and independent review are not proven as production capabilities | issues #29 / #227 | current App installation/permission/key custody/rotation plus successful publication/recovery evidence | complete external App configuration and preserve immutable receipts | +| P0 | Protected governance vs live policy | source verification alone cannot prove merge/release control | issue #27 | live ruleset/branch-protection evidence and observed required workflows | run governance audit against live policy and repair in owning control plane | +| P1 | Provider-neutral central writer handoff | removing Noema's local cron too early creates a writer outage; adding direct NVIDIA/provider keys violates the LLM owner boundary | PR #551 + central owner prerequisite | protected central manual-entrypoint/DDD admission compatible with CO-only Noema, same-head workflow test, then normal Noema merge | keep #551 Draft until central owner contract lands; adopt the handshake and remove cron atomically | +| P1 | Patch-validator operational evidence | verified source image is not yet proven deployed/signed/active | issue #66 | protected receipt, registry digest, signature/attestation and activation proof | run publication pipeline from exact protected source | +| P1 | Authentic 30-day KPI | reliability/performance/operational value is not proven by production-origin data | issue #3 | production-origin time-bound integrity-checked 30-day KPI | run approved collector/verifier against production source | +| P1 | Release/deployment/acquisition evidence | buyer/legal/commercial authority is absent | issue #5 | immutable release/deployment/customer/revenue/legal transfer evidence | satisfy evidence families in order and rerun acquisition audit | ## Documentation contradictions -과거 PR 번호와 당시 상태는 historical provenance일 뿐 현재 owner나 구현 상태가 아니다. Canonical TRD와 ADR은 protected implementation surface와 durable live issue owner를 사용하며, historical PR을 current owner로 사용하지 않는다. PR #530의 Apache-2.0 grant도 merge 전에는 protected truth로 표현하지 않는다. +Historical PR numbers, predecessor heads and past workflow results are provenance only. They are not current owner authority or completion evidence. The Noema product boundary must not absorb contextual-orchestrator provider routing, central `.github` scheduler/reviewer policy, quarantine/security/outbound authority or another product's domain truth. Candidate source is described as candidate until protected integration. ## Completion discipline -각 gap은 표의 authoritative completion evidence가 실제로 존재하고 현재 source/head에 결합될 때만 닫는다. queued/skipped/cancelled/stale check, predecessor-head 결과, 문서 존재, synthetic fixture 또는 model judgement는 완료 증거가 아니다. Noema source의 Apache-2.0 grant, npm package-publication metadata, 제3자 package license evidence는 서로 별도 권위로 유지한다. +A gap closes only when its authoritative completion evidence exists and is bound to the current source/head. Queued, skipped, cancelled, stale, absent, predecessor, synthetic-merge-only or status-only evidence is non-passing. A failed workflow that actually checks out the exact head is source evidence and receives code/config RCA; a workflow that never acquires a runner is control-plane evidence and does not justify source churn. Release, deployment, KPI, customer and transfer claims remain separate authority classes. \ No newline at end of file From 29ad7acfb5dae324bd6711d353f698440b4e89a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:26:36 +0900 Subject: [PATCH 29/33] feat(ci): add exact lockfile policy candidate generator --- scripts/lockfile-change-policy-candidate.mjs | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 scripts/lockfile-change-policy-candidate.mjs diff --git a/scripts/lockfile-change-policy-candidate.mjs b/scripts/lockfile-change-policy-candidate.mjs new file mode 100644 index 000000000..c2f2bddc1 --- /dev/null +++ b/scripts/lockfile-change-policy-candidate.mjs @@ -0,0 +1,82 @@ +import { readFileSync } from "node:fs"; +import { + lockfileMetadataDigest, + lockfilePackagesDigest, + packageObjectDigest, +} from "./lockfile-change-control.mjs"; + +function parseLockfile(path) { + const value = JSON.parse(readFileSync(path, "utf8")); + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`lockfile at ${path} must be a JSON object`); + } + if (value.packages === null || typeof value.packages !== "object" || Array.isArray(value.packages)) { + throw new Error(`lockfile at ${path} must contain a packages object`); + } + return value; +} + +/** + * Build exact schema-v3 lockfile change-control evidence from one reviewed base/head pair. + * + * The candidate is diagnostic only: writing it to the policy file still requires review of the + * changed package set, justification, and source provenance. Reusing the enforcement gate's + * exported digest functions prevents an independent hashing implementation from drifting. + */ +export function buildLockfileChangePolicyCandidate({ basePath, headPath, baseSha }) { + if (typeof baseSha !== "string" || !/^[0-9a-f]{40}$/u.test(baseSha)) { + throw new Error("candidate generation requires an exact lowercase 40-character base SHA"); + } + const base = parseLockfile(basePath); + const head = parseLockfile(headPath); + const packageKeys = [...new Set([ + ...Object.keys(base.packages), + ...Object.keys(head.packages), + ])].sort(); + const targetPackages = packageKeys.filter( + (packagePath) => packageObjectDigest(base.packages[packagePath]) !== packageObjectDigest(head.packages[packagePath]), + ); + const packageDigests = Object.fromEntries( + targetPackages.map((packagePath) => [ + packagePath, + { + afterSha256: packageObjectDigest(head.packages[packagePath]), + beforeSha256: packageObjectDigest(base.packages[packagePath]), + }, + ]), + ); + const bulkChange = targetPackages.length <= 128 + ? null + : { + afterPackagesSha256: lockfilePackagesDigest(head), + beforePackagesSha256: lockfilePackagesDigest(base), + targetPackageCount: targetPackages.length, + }; + return { + baseSha, + bulkChange, + justification: "REVIEW REQUIRED: describe why this exact lockfile package set changes and what unrelated package metadata is preserved.", + packageDigests, + schemaVersion: 3, + sources: ["https://review-required.invalid/replace-with-reviewed-provenance"], + targetPackages, + topLevelMetadataDigests: { + afterSha256: lockfileMetadataDigest(head), + beforeSha256: lockfileMetadataDigest(base), + }, + }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const basePath = process.env.NOEMA_LOCKFILE_BASE_PATH; + const baseSha = process.env.NOEMA_LOCKFILE_BASE_SHA; + if (!basePath || !baseSha) { + throw new Error("NOEMA_LOCKFILE_BASE_PATH and NOEMA_LOCKFILE_BASE_SHA are required"); + } + const candidate = buildLockfileChangePolicyCandidate({ + basePath, + headPath: "package-lock.json", + baseSha, + }); + process.stdout.write(`${JSON.stringify(candidate, null, 2)}\n`); +} From cfe98c8a5bddfd3275b97b7c2a0372f71aadd55f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:43:09 +0900 Subject: [PATCH 30/33] fix(ci): bind exact toolchain lockfile transition --- .github/lockfile-change-policy.json | 271 +++++++++++++++++++++++++++- 1 file changed, 262 insertions(+), 9 deletions(-) diff --git a/.github/lockfile-change-policy.json b/.github/lockfile-change-policy.json index ece397d4f..62100a360 100644 --- a/.github/lockfile-change-policy.json +++ b/.github/lockfile-change-policy.json @@ -1,23 +1,276 @@ { - "baseSha": "6bc8ed016dc07f95d4e041a3b79ac00c4086b182", + "baseSha": "e1ac9d50f6c646f04be8c137c8acdc7200182fcd", "bulkChange": null, - "justification": "Remediate GHSA-2v37-7h3g-55p8 by advancing the single transitive nanoid package-lock node from 3.3.17 to the patched 3.3.18 release. Preserve all top-level lock metadata, PostCSS dependency declarations, and unrelated package nodes.", + "justification": "Replace the Wrangler/Miniflare/Sharp transitive development path with direct pinned workerd@1.20260625.1 and esbuild@0.28.1 dependencies for Noema Worker development and deployment tooling. The reviewed lockfile transition removes the Wrangler-owned Miniflare/Sharp/Libvips package set, preserves unchanged package objects and top-level lockfile metadata, and binds the exact protected-main base and regenerated head bytes.", "packageDigests": { - "node_modules/nanoid": { - "afterSha256": "d05f52cccf4bb2b3faa241c82560bdff38872191f8c2fc9e0fe11d1863c6689c", - "beforeSha256": "eb31926c2b062d6831f465580d52d350ebd0ec8cb0ae8c9b36a92e1bec871af4" + "": { + "afterSha256": "bc4820765f3986a162070a7c499943d4976663ce9dbf4bc0d039bc8111d14c87", + "beforeSha256": "bc4df75e5f7a57a7b5cbb8fca21fe3aada716dcd26e4bad5b889d93c5251e20c" + }, + "node_modules/@cloudflare/kv-asset-handler": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "baf9a6828aa48335b6b1ddc90c064891668bf48ed319cb98bad1aae065e5b110" + }, + "node_modules/@cloudflare/unenv-preset": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "3975dc435686ec2387ff6065520031589c8608c4040c1bffcfcf169693670bc6" + }, + "node_modules/@cspotcode/source-map-support": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "be3b4d0e114620b28f168efe57e2f082751ec98c255e5ff44642903ca8c8abc1" + }, + "node_modules/@img/colour": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "0ec9a3855c0d275ee3ddf26fc218c24bcd73c70ae1be64bc783adcee728fabd3" + }, + "node_modules/@img/sharp-darwin-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "33753afdce1a4ef04bdbe3955ee21f6f7ec2d0e24950d2d48853ac21d06e0207" + }, + "node_modules/@img/sharp-darwin-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "92c5e9c8a824389e0d714e015b25bbfe4304b1968f9fc4551c31aeaa84953d7e" + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "e93d997495809b38e24ee02aae3570fd5388f6f29aca13ecc5790df62c4bf2ee" + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "5cbb579d4f882d736f41709f4ab8df92b444cc24a04ddf4568b6248903988dea" + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "52cc3b0d34d5f51e30fc3018eea0cf640c5963e6b662cacf9f6c377cc35b6dda" + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "e5de57abbf3750ebae77db880bdee4dd9bd5823468fe4d6a54b6f0076508c120" + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "b86ba40d539fb3254d0a045e330fa90141a3907deefadaf264ce4831512035a4" + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "666eb414e63b3f4a6f2338f14d6f59127c6f73562659425004d9258a499160a8" + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "c3e3d0a53cec8bb22b1319219dfcd6fc5d059cd2b133827668fac6e301f77c53" + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "6273b939a75e550fe2088ac52d90f1bb9918f93330d6b83a7df7db46b7b41efd" + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "21453f05d9d156d477d80b5f6726993033c6e7cc1ffba71d93042fa51648acb4" + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "84a3b95e19257d67c939f31d82dc6549cad376ead8dc7a9e451e6cfb1d1b3b3e" + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "4c5fa48ebba69feada47c1b31653a099b461543c104807afdcecf437a1f05f3e" + }, + "node_modules/@img/sharp-linux-arm": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "a65b10a97d8310bcb4d5e97be980320e91267d69b2b8fcd80aaa8134609e282c" + }, + "node_modules/@img/sharp-linux-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "07b70ea8a68735233da5aabe69863f359f77bf152addd717311907255038bd5c" + }, + "node_modules/@img/sharp-linux-ppc64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "5a3f8bb47ada74df86462a8eb4c283cf2f5fc072974c81db4d98a5bd2774bfc6" + }, + "node_modules/@img/sharp-linux-riscv64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "807f16be2b136919b9089a0df5fc506b3f66f2708205659eb7e11945a578e070" + }, + "node_modules/@img/sharp-linux-s390x": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "dea04183a47348ebf4455ffc9f7b56d750a388bd59900937a3501a5f886fb0b5" + }, + "node_modules/@img/sharp-linux-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "e8b884ec932accbb31472f9532d30d9dea8cf69e3665a02a47ff8a278f4a5d26" + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "5d81370f988ddf9cfd71f818640fb1ddba3c61f34936ccd16f9318abb45e070a" + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "dcb1c509e3d9a5a917cfe6b3868acfe372bd4045a4bdeedb3c265d1c9ab2c1d8" + }, + "node_modules/@img/sharp-wasm32": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "d372231a4a3a965acefef6e4082f35d7faafee7c0ac332d333267eed0230f73f" + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "3950aee6b7b49d472361e907dd0a38966a4362a9dcd8e3a94cb91187965051da" + }, + "node_modules/@img/sharp-win32-arm64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "d7b719d77aad3ce761066678008a2b59ee708da1eac1edca5a52d595d064623c" + }, + "node_modules/@img/sharp-win32-ia32": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "6ac2a1af086a1bd93c725d3379a2d63abb1ccb96fa22d11c320fab8ee9323c0e" + }, + "node_modules/@img/sharp-win32-x64": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "83788206d3b5d601a59383a2e647e679ae3499f41a4c7c609f5d414fc876edf8" + }, + "node_modules/@jridgewell/trace-mapping": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "946e048fd4f5f06fd3a2558cecdd7a7e1a179d1c60f88c1a10deff92904cefb6" + }, + "node_modules/@poppinss/colors": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "9f6d9e5e656687bf9365aad30b1cd57e2005670d483825ff6a9041eb264c0a8c" + }, + "node_modules/@poppinss/dumper": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "4636d1e8ce5d92e9e6a74b8e331a1d0599151c423620605b7f0d391a6a324f45" + }, + "node_modules/@poppinss/exception": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "511ab6d7dda3a25412e2d6459b7458c52d35e8d5a38ccea9e8a1c767c2c2b6a3" + }, + "node_modules/@sindresorhus/is": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "9cf703780184209ca125688afa81e6cf6a49ca4cee7a6442648003875ffa7ede" + }, + "node_modules/@speed-highlight/core": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "b0d9aede1a43525b35c83c66cfe23301fefa32d437344a723f156bcb3bde75c6" + }, + "node_modules/blake3-wasm": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "2edd7b9afb0a3edfde7bf65df2176834db86926fb79bcb81757f823ece33e0e8" + }, + "node_modules/cookie": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "c83cc50b9edf74fff002ee1696f718a7893a2790be1c87668878d4259a9ed661" + }, + "node_modules/error-stack-parser-es": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "07d175e9a9ce5da0ce6a91827c6c941d31f51684281f0060b3dc3df25db6cc02" + }, + "node_modules/kleur": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "03d1698c44fce7057c0b68d8cba4bfad5ca7382a948e162d49d806f81ee4859c" + }, + "node_modules/miniflare": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "3e4f60462e1a727ae18971cc7805b70cee7a5e1ba6265490550d3cd545ce7c2e" + }, + "node_modules/path-to-regexp": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "206d20489bfee22f1bd8a9ef8b31f0c7bdf9544b7272decd73314db823528dd1" + }, + "node_modules/sharp": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "185d39448de75db02f7418462440e6b8755e9f1e94fd88b66712835a9f22153a" + }, + "node_modules/supports-color": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "1a548a2b86a1d2addc0f2fd3dc4a3da1f2a81cd8e94fe1f0d431de96989d234c" + }, + "node_modules/undici": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "ab97f8ae955e187ed30dae56574c6f24277da4c4068383998f42dd18990d6c9b" + }, + "node_modules/unenv": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "cd1ef9a2d07200fe1d861ae9a4f81c1b1990312c1c6d88ecf844246efb33f6fe" + }, + "node_modules/wrangler": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "b52ea831e8e92ebe06b3ed1776cc1f781f7de77ece30a4237a95ff477df65455" + }, + "node_modules/ws": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "8312a6b5d3e17eda63344fe09189e016ad35b526bbbef54af5468d22eb9902a6" + }, + "node_modules/youch": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "faaf7ca34f95ab4401519c3222a9b37ef594158220cdb37ea4f3c483817e81d4" + }, + "node_modules/youch-core": { + "afterSha256": "398b676e47d03a29016ee92fe378b8b4f1b3e965390c4c64ce78d27f79df74d1", + "beforeSha256": "054aee49bedca6747ec8256719b1a9d6c5e834903f6606daa12ef8497dc70043" } }, "schemaVersion": 3, "sources": [ - "https://github.com/advisories/GHSA-2v37-7h3g-55p8", - "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz" + "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz", + "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz", + "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz", + "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz" ], "targetPackages": [ - "node_modules/nanoid" + "", + "node_modules/@cloudflare/kv-asset-handler", + "node_modules/@cloudflare/unenv-preset", + "node_modules/@cspotcode/source-map-support", + "node_modules/@img/colour", + "node_modules/@img/sharp-darwin-arm64", + "node_modules/@img/sharp-darwin-x64", + "node_modules/@img/sharp-freebsd-wasm32", + "node_modules/@img/sharp-libvips-darwin-arm64", + "node_modules/@img/sharp-libvips-darwin-x64", + "node_modules/@img/sharp-libvips-linux-arm", + "node_modules/@img/sharp-libvips-linux-arm64", + "node_modules/@img/sharp-libvips-linux-ppc64", + "node_modules/@img/sharp-libvips-linux-riscv64", + "node_modules/@img/sharp-libvips-linux-s390x", + "node_modules/@img/sharp-libvips-linux-x64", + "node_modules/@img/sharp-libvips-linuxmusl-arm64", + "node_modules/@img/sharp-libvips-linuxmusl-x64", + "node_modules/@img/sharp-linux-arm", + "node_modules/@img/sharp-linux-arm64", + "node_modules/@img/sharp-linux-ppc64", + "node_modules/@img/sharp-linux-riscv64", + "node_modules/@img/sharp-linux-s390x", + "node_modules/@img/sharp-linux-x64", + "node_modules/@img/sharp-linuxmusl-arm64", + "node_modules/@img/sharp-linuxmusl-x64", + "node_modules/@img/sharp-wasm32", + "node_modules/@img/sharp-webcontainers-wasm32", + "node_modules/@img/sharp-win32-arm64", + "node_modules/@img/sharp-win32-ia32", + "node_modules/@img/sharp-win32-x64", + "node_modules/@jridgewell/trace-mapping", + "node_modules/@poppinss/colors", + "node_modules/@poppinss/dumper", + "node_modules/@poppinss/exception", + "node_modules/@sindresorhus/is", + "node_modules/@speed-highlight/core", + "node_modules/blake3-wasm", + "node_modules/cookie", + "node_modules/error-stack-parser-es", + "node_modules/kleur", + "node_modules/miniflare", + "node_modules/path-to-regexp", + "node_modules/sharp", + "node_modules/supports-color", + "node_modules/undici", + "node_modules/unenv", + "node_modules/wrangler", + "node_modules/ws", + "node_modules/youch", + "node_modules/youch-core" ], "topLevelMetadataDigests": { "afterSha256": "354c77096d1795b6f33b903ac8b54c3922a045279413f3e8681c78c1fe5278b1", "beforeSha256": "354c77096d1795b6f33b903ac8b54c3922a045279413f3e8681c78c1fe5278b1" } -} \ No newline at end of file +} From 7224d654b936c5a61b5fb0bde60bd2ac6fbd2571 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 11:41:37 +0900 Subject: [PATCH 31/33] fix(lockfile): rebind policy to protected context admission --- .github/lockfile-change-policy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/lockfile-change-policy.json b/.github/lockfile-change-policy.json index fbc913f74..216323736 100644 --- a/.github/lockfile-change-policy.json +++ b/.github/lockfile-change-policy.json @@ -1,5 +1,5 @@ { - "baseSha": "85b17014b8d46eacc95e096ca114568c321d0263", + "baseSha": "71cd0fb6f3cf6ed1b886c8c312bfe96e7613f155", "bulkChange": null, "justification": "Replace the Wrangler/Miniflare/Sharp transitive development path with direct pinned workerd@1.20260625.1 and esbuild@0.28.1 dependencies for Noema Worker development and deployment tooling. The reviewed lockfile transition removes the Wrangler-owned Miniflare/Sharp/Libvips package set, preserves unchanged package objects and top-level lockfile metadata, and binds the exact protected-main base and regenerated head bytes.", "packageDigests": { From 424c149005b39faa266bd43d2d6e21c3c9e903e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:22:38 +0900 Subject: [PATCH 32/33] fix(toolchain): rebind lock policy to current protected base --- .github/lockfile-change-policy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/lockfile-change-policy.json b/.github/lockfile-change-policy.json index 216323736..fe3bc8420 100644 --- a/.github/lockfile-change-policy.json +++ b/.github/lockfile-change-policy.json @@ -1,5 +1,5 @@ { - "baseSha": "71cd0fb6f3cf6ed1b886c8c312bfe96e7613f155", + "baseSha": "5b8e620dbb01a794c1a38535bbcc32e41a80d0df", "bulkChange": null, "justification": "Replace the Wrangler/Miniflare/Sharp transitive development path with direct pinned workerd@1.20260625.1 and esbuild@0.28.1 dependencies for Noema Worker development and deployment tooling. The reviewed lockfile transition removes the Wrangler-owned Miniflare/Sharp/Libvips package set, preserves unchanged package objects and top-level lockfile metadata, and binds the exact protected-main base and regenerated head bytes.", "packageDigests": { From 7c8c26983f5628ccf6125ed3d3cb52edc9782f9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:17:08 +0900 Subject: [PATCH 33/33] fix(toolchain): rebind lockfile policy to protected #527 trust merge --- .github/lockfile-change-policy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/lockfile-change-policy.json b/.github/lockfile-change-policy.json index fe3bc8420..88046b42a 100644 --- a/.github/lockfile-change-policy.json +++ b/.github/lockfile-change-policy.json @@ -1,5 +1,5 @@ { - "baseSha": "5b8e620dbb01a794c1a38535bbcc32e41a80d0df", + "baseSha": "e26d771470a4ece873c367b40b3cd6cb03ac7de3", "bulkChange": null, "justification": "Replace the Wrangler/Miniflare/Sharp transitive development path with direct pinned workerd@1.20260625.1 and esbuild@0.28.1 dependencies for Noema Worker development and deployment tooling. The reviewed lockfile transition removes the Wrangler-owned Miniflare/Sharp/Libvips package set, preserves unchanged package objects and top-level lockfile metadata, and binds the exact protected-main base and regenerated head bytes.", "packageDigests": {