From b8ece6451d507345e962e30ded814bfcea759018 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Fri, 18 Sep 2026 12:58:17 -0400 Subject: [PATCH 1/6] feat(deploy-contract): add the provider-agnostic deploy contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce @mieweb/deploy-contract — a zero-dependency, provider-agnostic control-plane contract that @mieweb/cli consumes and deploy backends implement. Cloudflare/wrangler is the reference provider; opensource-server (os.mieweb.org) and future AWS/GCP are others. Surface: - `DeployProvider` interface: `deploy` is the only required verb; `dev`/`tail`/`destroy`/`login`/`logout`/`whoami` are optional (the CLI degrades with a clear "unsupported by provider" message when absent). - Neutral `DeployContext` (a projection of the CLI's config, incl. the resolved `manifestPath`), `DeployResult`, and `ResourceHandle` whose `id` is opaque — the CLI reports it and (future work) persists it back, never interpreting it. No wrangler.jsonc field names or backend shapes leak in. - Auth: `AuthError` (control-plane analogue of `UnsupportedBindingError`) and `AuthStatus`. Credentials never travel through the contract in a serializable form; providers read them from the environment via `createProvider(env)`. `targetConfig` is documented as non-secret. - `RESOURCE_KINDS` as the single source of truth; the `ResourceKind` type is derived from it so the type and runtime allowlist cannot drift. Packaging (loads under bare node and strict node16/nodenext): - Runtime values (`AuthError`, `RESOURCE_KINDS`) ship as plain ESM (`runtime.mjs` + `runtime.d.mts`); `.` resolves to `index.mjs` at runtime with `index.ts` for types. - `./jsonc`: a shared, string-aware JSONC parser (comments become separators; unterminated block comments throw) used by both the CLI and providers, so parser fixes can't diverge across packages. - `./testkit`: `runProviderConformance()` — the control-plane analogue of @mieweb/test-app. Validates result shape (incl. the closed `ResourceKind` union) and, via a provider-supplied `applyIds` hook, handle stability across reruns (documented as handle stability, not full backend idempotency, which the kit cannot observe). --- packages/deploy-contract/package.json | 35 +++ packages/deploy-contract/src/index.mjs | 10 + packages/deploy-contract/src/index.ts | 331 +++++++++++++++++++++ packages/deploy-contract/src/jsonc.d.ts | 5 + packages/deploy-contract/src/jsonc.mjs | 122 ++++++++ packages/deploy-contract/src/runtime.d.mts | 28 ++ packages/deploy-contract/src/runtime.mjs | 63 ++++ packages/deploy-contract/src/testkit.d.ts | 57 ++++ packages/deploy-contract/src/testkit.mjs | 189 ++++++++++++ 9 files changed, 840 insertions(+) create mode 100644 packages/deploy-contract/package.json create mode 100644 packages/deploy-contract/src/index.mjs create mode 100644 packages/deploy-contract/src/index.ts create mode 100644 packages/deploy-contract/src/jsonc.d.ts create mode 100644 packages/deploy-contract/src/jsonc.mjs create mode 100644 packages/deploy-contract/src/runtime.d.mts create mode 100644 packages/deploy-contract/src/runtime.mjs create mode 100644 packages/deploy-contract/src/testkit.d.ts create mode 100644 packages/deploy-contract/src/testkit.mjs diff --git a/packages/deploy-contract/package.json b/packages/deploy-contract/package.json new file mode 100644 index 0000000..6ec4cd0 --- /dev/null +++ b/packages/deploy-contract/package.json @@ -0,0 +1,35 @@ +{ + "name": "@mieweb/deploy-contract", + "version": "0.2.1", + "description": "The @mieweb deploy-provider contract: a minimal, provider-agnostic TypeScript interface (`DeployProvider`) that @mieweb/cli consumes and deploy backends implement. Cloudflare/wrangler is the reference provider (@mieweb/deploy-wrangler); opensource-server (os.mieweb.org) is another. Zero dependencies — types plus a small conformance test-kit. No wrangler.jsonc field names, no backend API shapes, and no `mieweb+:` ID grammar leak into the contract; those are provider implementation details.", + "type": "module", + "license": "MIT", + "author": "MIEWEB", + "homepage": "https://github.com/mieweb/cloud#readme", + "bugs": "https://github.com/mieweb/cloud/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/mieweb/cloud.git", + "directory": "packages/deploy-contract" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.mjs" + }, + "./testkit": { + "types": "./src/testkit.d.ts", + "default": "./src/testkit.mjs" + }, + "./jsonc": { + "types": "./src/jsonc.d.ts", + "default": "./src/jsonc.mjs" + } + }, + "files": [ + "src" + ] +} diff --git a/packages/deploy-contract/src/index.mjs b/packages/deploy-contract/src/index.mjs new file mode 100644 index 0000000..e774d25 --- /dev/null +++ b/packages/deploy-contract/src/index.mjs @@ -0,0 +1,10 @@ +/** + * Runtime entry for `@mieweb/deploy-contract`. + * + * The contract is mostly types (see `index.ts`, referenced via the package's + * `types` export condition). Only runtime *values* live at this entry so that + * plain-`node` `.mjs` consumers can import them without a TypeScript loader. + * Today that is {@link AuthError} and {@link RESOURCE_KINDS}; both re-export + * from `runtime.mjs`. + */ +export { AuthError, RESOURCE_KINDS } from './runtime.mjs'; diff --git a/packages/deploy-contract/src/index.ts b/packages/deploy-contract/src/index.ts new file mode 100644 index 0000000..60a5853 --- /dev/null +++ b/packages/deploy-contract/src/index.ts @@ -0,0 +1,331 @@ +/** + * `@mieweb/deploy-contract` — the provider-agnostic deploy contract. + * + * ## What this is + * + * `@mieweb/cli` needs to run deploy verbs (`deploy`, `dev`, `tail`, …) without + * knowing *how* any particular backend does the work. This module defines the + * single interface — {@link DeployProvider} — that the CLI consumes and every + * backend implements. Cloudflare is the **reference provider** + * (`@mieweb/deploy-wrangler`, which wraps the real `wrangler` binary); + * os.mieweb.org / opensource-server is another; AWS/GCP could be more. + * + * ## Design bias: the contract owns *what*, providers own *how* + * + * Deliberately absent from this file: + * - wrangler.jsonc field names (`database_id`, `bucket_name`, …), + * - any backend request/response shape (the opensource-server Manager API), + * - the `mieweb+:` resource-URI grammar. + * + * Those are all provider implementation details. The contract speaks only in + * neutral terms: a {@link DeployContext} in, a {@link DeployResult} out, with + * resource identity carried as an **opaque** {@link ResourceHandle.id} that the + * CLI reports and is expected to persist back into the config verbatim + * (short-circuiting the next provision) without ever interpreting it. Note: + * automatic write-back is not yet implemented — today the CLI reports the ids + * for the user to commit; see {@link ResourceHandle} for the current guarantee. + * + * This mirrors the data-plane half of the portability layer: just as + * `@mieweb/cloud` defines Cloudflare-shaped binding *contracts* that + * `@mieweb/cloud-adapters` implement, this package defines a control-plane + * *contract* that deploy providers implement. + */ + +/** + * Which platform a deploy targets. Open-ended on purpose: the contract does not + * enumerate a closed set, so new providers can introduce their own target names + * and advertise them via {@link DeployProvider.supports}. The well-known values + * mirror `CloudTarget` in `@mieweb/cloud`. + */ +export type DeployTarget = 'cloudflare' | 'local' | 'mieweb' | 'aws' | 'gcp' | (string & {}); + +/** + * Structured logging sink the CLI supplies. Providers MUST route their own + * human-facing messages through this rather than `console.*`, so the CLI stays + * in control of formatting, verbosity, and stream routing (and tests can + * capture output). + * + * **Subprocess exception.** A provider that delegates to an interactive child + * process (e.g. the reference provider shelling out to `wrangler`) MAY let that + * child inherit the terminal's stdio directly, because a long-running, + * TTY-aware tool needs live colors, progress, and interactive prompts that a + * line-buffered logger would break. Such passthrough output is the child's, not + * the provider's, and is expected to reach the terminal unmediated. Everything + * the *provider itself* emits still goes through this logger. + */ +export interface DeployLogger { + info(message: string): void; + warn(message: string): void; + error(message: string): void; +} + +/** + * The resolved project context the CLI hands a provider for every verb. This is + * a neutral projection of the CLI's internal config (`MiewebConfig`) — a + * provider receives exactly what it needs to act, and nothing about the CLI's + * own plumbing. + */ +export interface DeployContext { + /** Absolute repository root — the directory holding the resolved config. */ + readonly root: string; + + /** The resolved active target for this invocation. */ + readonly target: DeployTarget; + + /** + * The parsed declarative resource manifest (wrangler.jsonc). Passed as an + * opaque object: a provider reads whatever fields it understands. The + * reference provider hands it straight to wrangler; other providers read the + * bindings/resources they support and ignore the rest. + */ + readonly manifest: Readonly>; + + /** + * Absolute path to the manifest file on disk (the resolved `wrangler.jsonc`, + * honoring any custom location). Providers that shell out to a tool needing an + * explicit `--config`, or that must re-read the file after a deploy to pick up + * written-back resource ids, should use this rather than assuming a fixed + * filename under {@link root}. Undefined when the manifest was synthesized + * rather than loaded from a file. + */ + readonly manifestPath?: string; + + /** + * The parsed mieweb sidecar config (mieweb.jsonc), or `{}` when absent. + * Holds mieweb-specific, non-wrangler configuration (target selection, + * per-target adapter settings). + */ + readonly mieweb: Readonly>; + + /** + * Provider/adapter configuration scoped to the active target + * (`mieweb.jsonc` → `targets[target]`), or `{}` when absent. This is + * **non-secret** configuration — a provider package name, a backend location + * (e.g. a self-hosted instance URL), tuning knobs — because it comes from an + * on-disk, potentially committed file. + * + * **Secrets do not belong here.** Credentials/tokens a provider needs are read + * from the environment via {@link DeployProviderFactory} (`createProvider(env)`), + * never from `targetConfig` and never from {@link manifest}. This keeps the + * "credentials never travel through the contract in a serializable form" + * guarantee (see {@link AuthStatus}) intact and gives provider authors one + * clear secret-handling boundary. + */ + readonly targetConfig: Readonly>; + + /** Passthrough arguments that followed the verb (e.g. `--dry-run`, `--env prod`). */ + readonly argv: readonly string[]; + + /** Structured output sink. Providers must not write to stdout/stderr directly. */ + readonly logger: DeployLogger; + + /** + * Cooperative cancellation. Aborted when the user interrupts (Ctrl-C) or a + * timeout fires; long-running providers should observe it and abort promptly. + */ + readonly signal: AbortSignal; +} + +/** Re-export of the canonical runtime kinds list (single source of truth). */ +export { RESOURCE_KINDS } from './runtime.mjs'; + +/** + * The contract's neutral vocabulary for a provisionable resource, decoupled + * from any provider's naming (D1/R2/KV → database/bucket/kv, etc.). Derived from + * {@link RESOURCE_KINDS} so the type and the runtime allowlist can never drift. + */ +export type ResourceKind = (typeof import('./runtime.mjs').RESOURCE_KINDS)[number]; + +/** + * A resource the provider ensured exists during a deploy. The CLI reports these + * to the user; {@link id} is intended to be persisted back into the committed + * config so the next deploy short-circuits provisioning for that binding. + * + * **Current guarantee: reported-only.** Automatic write-back into the manifest + * is a planned follow-up; today the CLI *reports* the ids and the user commits + * them. Provider authors should populate ids so write-back can be enabled later, + * but must not assume the CLI persists them yet. + */ +export interface ResourceHandle { + /** The binding name as it appears in the manifest (`DB`, `RECORDINGS`, …). */ + readonly binding: string; + + /** The contract's neutral kind for this resource. */ + readonly kind: ResourceKind; + + /** + * Opaque, provider-owned identity for the resource. The CLI treats this as a + * black box: it never parses it, and is expected to write it back into the + * config unchanged once write-back lands (reported-only today). The reference + * provider stores a Cloudflare resource id here; other providers may store any + * string (e.g. a URI) — that is their private affair. + */ + readonly id: string; +} + +/** Outcome of a {@link DeployProvider.deploy} run. Neutral: no provider-specific fields. */ +export interface DeployResult { + /** Public URL the deployed worker is reachable at, when the provider knows it. */ + readonly url?: string; + + /** + * Resources ensured or created during this deploy. Empty is valid (e.g. a + * code-only redeploy). The CLI uses these to report status and (once + * write-back lands) to persist + * ids back into the config. + */ + readonly resources: readonly ResourceHandle[]; +} + +/** + * A long-running handle returned by {@link DeployProvider.dev}. The CLI keeps + * the process alive and calls {@link stop} on interrupt. + */ +export interface DeployHandle { + /** Local URL the dev server is listening on, when known. */ + readonly url?: string; + + /** + * Resolves when the underlying process ends on its own, and rejects if it + * ends abnormally (non-zero exit / killed by a signal). The CLI races this + * against its interrupt signal so a crashed `dev` returns instead of hanging. + * Optional: providers whose `dev` cannot exit on its own may omit it. + */ + readonly closed?: Promise; + + /** Tear down the running dev process. Idempotent. */ + stop(): void | Promise; +} + +/** + * The result of an authentication-status check ({@link DeployProvider.whoami}). + * + * Credentials themselves NEVER travel through the contract in a form that could + * be logged or serialized — a provider reads them from the environment at + * construction time (see {@link DeployProviderFactory}). This type only reports + * *whether* the provider is authenticated and, optionally, a non-secret label + * (account id, username, email) suitable for display. + */ +export interface AuthStatus { + /** Whether the provider currently has usable credentials for its backend. */ + readonly authenticated: boolean; + /** Non-secret identity label to show the user (`account`, `user@host`, …). */ + readonly account?: string; + /** How the credentials were obtained, for diagnostics (`'env'`, `'oauth'`, …). */ + readonly method?: string; +} + +/** + * Thrown by a provider when a verb cannot proceed because the caller is not + * authenticated (or the credentials are expired/insufficient). The + * control-plane analogue of `UnsupportedBindingError` on the data plane. + * + * The runtime implementation lives in `runtime.mjs` (plain ESM) so that + * bare-`node` `.mjs` providers can `throw new AuthError(...)` and the CLI can + * `instanceof`-check it without a TypeScript loader; it is re-exported here so + * TypeScript consumers see a single contract surface. + */ +export { AuthError } from './runtime.mjs'; + +/** + * The interface `@mieweb/cli` imports and drives. A provider is any object + * satisfying this shape; the CLI selects one whose {@link supports} returns + * true for the active target, then calls the requested verb. + * + * Only {@link deploy} is required. Optional verbs let the CLI degrade with a + * clear "not supported by provider X" message instead of silently misbehaving — + * the control-plane analogue of `UnsupportedBindingError` on the data plane. + * + * Contract obligations for implementers: + * - **Idempotency:** repeated {@link deploy} calls with the same context + * converge to the same state. Resources whose id is already present in the + * manifest MUST be reused, not recreated. + * - **No direct I/O to the console:** use {@link DeployContext.logger} for + * the provider's own messages. Delegated interactive child processes may + * inherit stdio (see {@link DeployLogger}). + * - **Honor cancellation:** observe {@link DeployContext.signal}. + * - **Opaque ids:** never require the CLI to understand {@link ResourceHandle.id}. + * - **Signal auth failures with {@link AuthError}:** throw it (not a bare + * `Error`) for backend 401/403, so the CLI can prompt the user to log in. + */ +export interface DeployProvider { + /** Stable identifier for diagnostics (`'wrangler'`, `'opensource-server'`). */ + readonly name: string; + + /** + * Whether this provider can service the given target. The CLI calls this to + * pick a provider; the first that returns true wins. + */ + supports(target: DeployTarget): boolean; + + /** + * Ensure the manifest's resources exist and push the worker. Idempotent. + * This is the core verb every provider must implement. + */ + deploy(context: DeployContext): Promise; + + /** Start a local/remote dev loop. Absent ⇒ CLI reports the verb unsupported. */ + dev?(context: DeployContext): Promise; + + /** Stream logs from the deployed worker. Absent ⇒ CLI reports unsupported. */ + tail?(context: DeployContext): Promise; + + /** Tear down what {@link deploy} created. Absent ⇒ CLI reports unsupported. */ + destroy?(context: DeployContext): Promise; + + /** + * Report whether the provider is currently authenticated against its backend, + * without performing any deploy work. The CLI may call this before a deploy to + * fail fast with a helpful message, and it backs `mieweb whoami`. + * + * Absent ⇒ the CLI assumes the provider self-manages auth ambiently (e.g. via + * environment credentials) and proceeds; verbs may still throw {@link AuthError}. + */ + whoami?(context: DeployContext): Promise; + + /** + * Establish credentials for the backend — typically an interactive flow + * (browser OAuth, device code, token prompt). Backs `mieweb login`. + * + * Absent ⇒ the CLI reports that this provider takes credentials from the + * environment and there is nothing to log into. Providers that authenticate + * purely via env tokens should omit this rather than implement a no-op. + */ + login?(context: DeployContext): Promise; + + /** + * Revoke or clear locally-cached credentials. Backs `mieweb logout`. Absent ⇒ + * CLI reports there is no stored session to clear. + */ + logout?(context: DeployContext): Promise; +} + +/** + * A provider module's expected shape. The CLI resolves a provider by importing + * its package and reading either a `default` export that is a + * {@link DeployProvider}, or a `createProvider` factory that returns one. The + * factory form lets a provider read host-bootstrap configuration from the + * environment (endpoints, credentials) at construction time. + */ +export interface DeployProviderModule { + default?: DeployProvider; + createProvider?: DeployProviderFactory; +} + +/** + * A dependency-free view of the process environment. Equivalent in shape to + * `NodeJS.ProcessEnv`, but declared locally so this zero-dependency contract + * does not require `@types/node` to resolve — a consumer that only installs the + * contract can still type a provider factory. + */ +export type ProviderEnv = Readonly>; + +/** + * Factory that builds a provider. Receives the process environment for + * host-bootstrap config only (never the worker's `env`) — this is where a + * provider reads its backend location + credentials (e.g. a self-hosted + * instance URL and token), keeping them out of the committed manifest and the + * {@link DeployContext}. Kept synchronous so provider selection stays cheap; do + * real I/O inside the verbs. + */ +export type DeployProviderFactory = (env: ProviderEnv) => DeployProvider; diff --git a/packages/deploy-contract/src/jsonc.d.ts b/packages/deploy-contract/src/jsonc.d.ts new file mode 100644 index 0000000..ed1ab68 --- /dev/null +++ b/packages/deploy-contract/src/jsonc.d.ts @@ -0,0 +1,5 @@ +/** + * Parse JSONC (JSON with comments and trailing commas) into a value. + * String-aware: string contents are never rewritten. + */ +export function parseJsonc(text: string): unknown; diff --git a/packages/deploy-contract/src/jsonc.mjs b/packages/deploy-contract/src/jsonc.mjs new file mode 100644 index 0000000..ad6b659 --- /dev/null +++ b/packages/deploy-contract/src/jsonc.mjs @@ -0,0 +1,122 @@ +/** + * Minimal JSONC reader shared by @mieweb tooling (CLI + deploy providers). + * + * wrangler.jsonc (and mieweb.jsonc) use comments and trailing commas, which + * `JSON.parse` rejects. Rather than pull in a dependency (and to keep the + * provider runnable with bare `node`), we strip comments + trailing commas in a + * single string-aware pass, then hand the result to `JSON.parse`. + * + * This is deliberately small: it understands `//` line comments, block + * comments, double-quoted strings with escapes, and trailing commas before + * `}`/`]`. Crucially the trailing-comma removal happens *inside* the scan — only + * when outside a string — so a string value that legitimately contains `,}` or + * `,]` (e.g. `"value,}"`) is never rewritten. + * + * @param {string} text raw JSONC source + * @returns {unknown} parsed value + */ +export function parseJsonc(text) { + let out = ''; + let i = 0; + const n = text.length; + let inString = false; + + while (i < n) { + const ch = text[i]; + const next = text[i + 1]; + + if (inString) { + out += ch; + if (ch === '\\') { + // Copy the escaped character verbatim. + out += text[i + 1] ?? ''; + i += 2; + continue; + } + if (ch === '"') inString = false; + i += 1; + continue; + } + + if (ch === '"') { + inString = true; + out += ch; + i += 1; + continue; + } + + if (ch === '/' && next === '/') { + // Line comment: replace with a newline so tokens on either side stay + // separated, then skip to end of line. + out += '\n'; + i += 2; + while (i < n && text[i] !== '\n') i += 1; + continue; + } + + if (ch === '/' && next === '*') { + // Block comment: emit a single space in its place so token-separated text + // like `1/*x*/2` does not collapse into `12`, then skip to the closing */. + // An unterminated block comment is a syntax error. + out += ' '; + i += 2; + while (i < n && !(text[i] === '*' && text[i + 1] === '/')) i += 1; + if (i >= n) throw new SyntaxError('parseJsonc: unterminated block comment'); + i += 2; + continue; + } + + if (ch === ',') { + // Trailing comma? Look ahead past whitespace and comments (outside any + // string) to the next significant character; if it closes a container, + // drop this comma. Otherwise emit it. This runs only here — never over + // string contents — so `"a,}"` is preserved intact. + if (isTrailingComma(text, i + 1)) { + i += 1; + continue; + } + out += ch; + i += 1; + continue; + } + + out += ch; + i += 1; + } + + return JSON.parse(out); +} + +/** + * From index `j`, skip whitespace and comments and report whether the next + * significant character closes an object/array (`}`/`]`) — i.e. the comma at + * `j-1` is a trailing comma. + * @param {string} text + * @param {number} j + * @returns {boolean} + */ +function isTrailingComma(text, j) { + const n = text.length; + let i = j; + while (i < n) { + const ch = text[i]; + const next = text[i + 1]; + if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { + i += 1; + continue; + } + if (ch === '/' && next === '/') { + i += 2; + while (i < n && text[i] !== '\n') i += 1; + continue; + } + if (ch === '/' && next === '*') { + i += 2; + while (i < n && !(text[i] === '*' && text[i + 1] === '/')) i += 1; + i += 2; + continue; + } + return ch === '}' || ch === ']'; + } + return false; +} diff --git a/packages/deploy-contract/src/runtime.d.mts b/packages/deploy-contract/src/runtime.d.mts new file mode 100644 index 0000000..4ef15b1 --- /dev/null +++ b/packages/deploy-contract/src/runtime.d.mts @@ -0,0 +1,28 @@ +import type { DeployTarget } from './index.js'; + +/** + * Thrown by a provider when a verb cannot proceed because the caller is not + * authenticated. See the runtime implementation in `runtime.mjs`. + */ +export class AuthError extends Error { + readonly provider: string; + readonly target: DeployTarget; + readonly hint?: string; + constructor(provider: string, target: DeployTarget, hint?: string); +} + +/** + * The canonical closed set of neutral resource kinds. Single source of truth for + * both the `ResourceKind` type (derived in `index.ts`) and the test-kit's + * runtime validation. + */ +export const RESOURCE_KINDS: readonly [ + 'database', + 'bucket', + 'kv', + 'queue', + 'vector', + 'stateful', + 'ai', + 'container', +]; diff --git a/packages/deploy-contract/src/runtime.mjs b/packages/deploy-contract/src/runtime.mjs new file mode 100644 index 0000000..96ac832 --- /dev/null +++ b/packages/deploy-contract/src/runtime.mjs @@ -0,0 +1,63 @@ +/** + * Runtime values for `@mieweb/deploy-contract`. + * + * The contract is overwhelmingly *types* (see `index.ts` / `index.d.ts`), but a + * couple of things must exist at runtime — chiefly {@link AuthError}, which + * providers `throw` and the CLI catches with `instanceof`. Those live here in + * plain ESM so a provider written as bare-`node` `.mjs` (like + * `@mieweb/deploy-wrangler`) can `import` them without any TypeScript loader or + * build step. `index.d.ts` re-exports these declarations so TS consumers still + * see one surface. + */ + +/** + * Thrown by a provider when a verb cannot proceed because the caller is not + * authenticated (or the credentials are expired/insufficient). The + * control-plane analogue of `UnsupportedBindingError` on the data plane: the + * CLI catches it and prints an actionable hint (e.g. "run `mieweb login`") + * instead of a generic failure. + * + * Providers should throw this — rather than a bare `Error` — for any 401/403 + * from their backend, so the CLI can distinguish "not logged in" from "the + * deploy genuinely failed." + */ +export class AuthError extends Error { + /** + * @param {string} provider the provider that raised it (`provider.name`) + * @param {string} target the target being acted on, for message context + * @param {string} [hint] optional actionable hint the CLI surfaces verbatim + */ + constructor(provider, target, hint) { + super( + `Not authenticated for provider "${provider}" on target "${target}"${ + hint ? `: ${hint}` : '.' + }`, + ); + this.name = 'AuthError'; + /** @type {string} */ + this.provider = provider; + /** @type {string} */ + this.target = target; + /** @type {string|undefined} */ + this.hint = hint; + } +} + +/** + * The canonical, closed set of neutral resource kinds a `ResourceHandle` may + * declare. This is the **single source of truth**: the `ResourceKind` type in + * `index.ts` is derived from this array (`typeof RESOURCE_KINDS[number]`), and + * the conformance test-kit validates against it — so adding a kind here updates + * both the type and the runtime check at once. + * @type {readonly ['database','bucket','kv','queue','vector','stateful','ai','container']} + */ +export const RESOURCE_KINDS = /** @type {const} */ ([ + 'database', + 'bucket', + 'kv', + 'queue', + 'vector', + 'stateful', + 'ai', + 'container', +]); diff --git a/packages/deploy-contract/src/testkit.d.ts b/packages/deploy-contract/src/testkit.d.ts new file mode 100644 index 0000000..c404591 --- /dev/null +++ b/packages/deploy-contract/src/testkit.d.ts @@ -0,0 +1,57 @@ +import type { DeployProvider, DeployTarget, ResourceHandle } from './index.js'; + +/** A single conformance assertion outcome. */ +export interface ConformanceCheck { + readonly name: string; + readonly ok: boolean; + /** Populated when `ok` is false. */ + readonly detail?: string; +} + +/** Aggregate result of a conformance run. */ +export interface ConformanceReport { + readonly provider: string; + readonly checks: readonly ConformanceCheck[]; + /** Convenience view: the subset of `checks` that failed. */ + readonly failures: readonly ConformanceCheck[]; +} + +/** Options describing the fixture project to exercise the provider against. */ +export interface ConformanceOptions { + /** Target to advertise + deploy for. Must be one the provider `supports`. */ + readonly target: DeployTarget; + /** Fixture manifest (a wrangler.jsonc-shaped object). */ + readonly manifest: Readonly>; + /** Fixture mieweb.jsonc. Defaults to `{}`. */ + readonly mieweb?: Readonly>; + /** Fixture per-target config. Defaults to `{}`. */ + readonly targetConfig?: Readonly>; + /** Absolute root for the fixture. Defaults to `process.cwd()`. */ + readonly root?: string; + /** + * When true, actually invoke `deploy` (side effects!). Defaults to false so + * the kit can validate the interface contract without provisioning anything. + * Set true in an environment where the provider's backend is reachable. + */ + readonly live?: boolean; + /** + * Provider-specific hook that merges the ids from a first deploy back into the + * manifest, so the kit can verify **handle stability** on a second run (that + * the same {binding, kind, id} handles come back — not full backend + * idempotency, which the kit cannot observe). Because id placement is + * provider-specific (there is no mandated manifest layout), the caller supplies + * it. **Required for a fully-passing `live` run:** when omitted on a live run, + * the kit records an explicit *failed* handle-stability check rather than + * silently skipping the obligation. + */ + readonly applyIds?: ( + manifest: Readonly>, + resources: readonly ResourceHandle[], + ) => Record; +} + +/** Run the conformance suite against a provider. */ +export function runProviderConformance( + provider: DeployProvider, + opts: ConformanceOptions, +): Promise; diff --git a/packages/deploy-contract/src/testkit.mjs b/packages/deploy-contract/src/testkit.mjs new file mode 100644 index 0000000..ac5a35a --- /dev/null +++ b/packages/deploy-contract/src/testkit.mjs @@ -0,0 +1,189 @@ +/** + * Provider conformance test-kit. + * + * Any `DeployProvider` implementation should pass this suite; the + * Cloudflare/wrangler reference provider passes it first, and every other + * provider (opensource-server, future AWS/GCP) must pass the *same* checks. + * This is the control-plane analogue of the `@mieweb/test-app` cross-target + * equivalence suite on the data plane. + * + * It is transport- and runner-agnostic: it exposes a single async + * {@link runProviderConformance} that returns structured results, so it can be + * driven from `node:test`, vitest, or a bare script. It asserts *contract* + * behavior, not any provider's internals. + * + * Authored as plain ESM with JSDoc types (types published via `testkit.d.ts`) + * so it loads under bare `node` — the wrangler provider's `node --test` suite + * imports it directly. + * + * @typedef {import('./index.ts').DeployContext} DeployContext + * @typedef {import('./index.ts').DeployProvider} DeployProvider + * @typedef {import('./index.ts').DeployResult} DeployResult + * @typedef {import('./index.ts').DeployTarget} DeployTarget + * @typedef {import('./index.ts').ResourceHandle} ResourceHandle + * @typedef {import('./testkit.d.ts').ConformanceCheck} ConformanceCheck + * @typedef {import('./testkit.d.ts').ConformanceReport} ConformanceReport + * @typedef {import('./testkit.d.ts').ConformanceOptions} ConformanceOptions + */ + +import { RESOURCE_KINDS } from './runtime.mjs'; + +/** + * Build a self-contained {@link DeployContext} plus captured log output. + * @param {ConformanceOptions} opts + * @returns {{ context: DeployContext, logs: string[], abort: AbortController }} + */ +function makeContext(opts) { + /** @type {string[]} */ + const logs = []; + const abort = new AbortController(); + /** @type {DeployContext} */ + const context = { + root: opts.root ?? process.cwd(), + target: opts.target, + manifest: opts.manifest, + mieweb: opts.mieweb ?? {}, + targetConfig: opts.targetConfig ?? {}, + argv: [], + logger: { + info: (m) => logs.push(`info:${m}`), + warn: (m) => logs.push(`warn:${m}`), + error: (m) => logs.push(`error:${m}`), + }, + signal: abort.signal, + }; + return { context, logs, abort }; +} + +/** + * The closed set of neutral resource kinds a {@link ResourceHandle} may declare, + * imported from the contract's single source of truth (no local duplication). + * @type {ReadonlySet} + */ +const RESOURCE_KIND_SET = new Set(RESOURCE_KINDS); + +/** + * Shape-check a {@link DeployResult} without assuming a runner's assert lib. + * @param {unknown} result + * @returns {string|null} an error message, or null when valid + */ +function validateResult(result) { + if (typeof result !== 'object' || result === null) { + return 'deploy() must resolve to a DeployResult object'; + } + const r = /** @type {Partial} */ (result); + if (!Array.isArray(r.resources)) { + return 'DeployResult.resources must be an array'; + } + for (const [i, res] of /** @type {ResourceHandle[]} */ (r.resources).entries()) { + if (typeof res?.binding !== 'string' || res.binding.length === 0) { + return `resources[${i}].binding must be a non-empty string`; + } + if (typeof res?.kind !== 'string' || !RESOURCE_KIND_SET.has(res.kind)) { + return `resources[${i}].kind must be one of the declared ResourceKind values, got ${JSON.stringify(res?.kind)}`; + } + if (typeof res?.id !== 'string') { + return `resources[${i}].id must be a string (opaque provider identity)`; + } + } + if (r.url !== undefined && typeof r.url !== 'string') { + return 'DeployResult.url, when present, must be a string'; + } + return null; +} + +/** + * Run the conformance suite against a provider. + * + * Structural checks (always run): + * 1. `name` is a non-empty string. + * 2. `supports(target)` returns true for the configured target. + * 3. `deploy` is a function. + * + * Behavioral checks (only when `live: true`, since they invoke the backend): + * 4. `deploy` resolves to a well-formed {@link DeployResult}. + * 5. Handle stability: a second run with the ids from the first surfaces the + * same {binding, kind, id} handles (not full backend idempotency — the kit + * cannot observe backend reuse; see the inline note). + * + * @param {DeployProvider} provider + * @param {ConformanceOptions} opts + * @returns {Promise} + */ +export async function runProviderConformance(provider, opts) { + /** @type {ConformanceCheck[]} */ + const checks = []; + /** @param {string} name @param {boolean} ok @param {string} [detail] */ + const record = (name, ok, detail) => checks.push({ name, ok, detail }); + + record('provider.name is a non-empty string', typeof provider.name === 'string' && provider.name.length > 0); + record( + `provider.supports("${opts.target}") is true`, + typeof provider.supports === 'function' && provider.supports(opts.target) === true, + ); + record('provider.deploy is a function', typeof provider.deploy === 'function'); + + if (opts.live) { + try { + const { context } = makeContext(opts); + const first = await provider.deploy(context); + const err = validateResult(first); + record('deploy() resolves to a valid DeployResult', err === null, err ?? undefined); + + // Idempotency: feed the first run's ids back into the manifest and redeploy. + // How ids map back into a manifest is provider-specific, so the caller + // supplies `applyIds`. Without it we skip the round-trip rather than assume + // a wrangler-shaped manifest (which would produce false failures for other + // providers). + if (typeof opts.applyIds === 'function') { + const merged = opts.applyIds(opts.manifest, first.resources ?? []); + const { context: second } = makeContext({ ...opts, manifest: merged }); + const again = await provider.deploy(second); + const againErr = validateResult(again); + record('second deploy() also resolves to a valid DeployResult', againErr === null, againErr ?? undefined); + // NOTE: this verifies *handle stability* — that a redeploy with the + // first run's ids returns the same {binding, kind, id} handles — not + // true backend idempotency. The kit cannot observe whether the backend + // reused vs. destroyed+recreated a resource; a provider that recreates + // while returning the same handle passes. Stronger idempotency proof + // needs provider-observable reuse evidence, out of scope for the kit. + const stable = + againErr === null && + JSON.stringify(idset(first.resources ?? [])) === JSON.stringify(idset(again.resources ?? [])); + record( + 'deploy() handles are stable across reruns (ids/kinds unchanged)', + stable, + stable ? undefined : 'second deploy returned a different/invalid set of resource handles', + ); + } else { + // Handle-stability is a core obligation. Without an `applyIds` hook it + // cannot run — record an explicit *failed* check so a passing report can + // never be mistaken for full conformance. Provide `applyIds` (or run + // without `live`) to satisfy this. + record( + 'deploy() handle stability checked', + false, + 'live conformance requires an `applyIds` hook to verify handle stability; none was provided', + ); + } + } catch (e) { + record('deploy() completed without throwing', false, e instanceof Error ? e.message : String(e)); + } + } + + const failures = checks.filter((c) => !c.ok); + return { provider: provider.name, checks, failures }; +} + +/** + * Sorted `binding→kind:id` view, for order-independent comparison. Includes + * `kind` because a kind change on the second deploy (same binding/id) is not an + * idempotent result — it violates the `ResourceHandle` contract. + * @param {readonly ResourceHandle[]} resources + * @returns {Array<[string, string]>} + */ +function idset(resources) { + return resources + .map((r) => /** @type {[string, string]} */ ([r.binding, `${r.kind}:${r.id}`])) + .sort((a, b) => a[0].localeCompare(b[0])); +} From cfe837ff6725102417ca133fc4ee5539119b1a4b Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Fri, 18 Sep 2026 12:58:31 -0400 Subject: [PATCH 2/6] feat(deploy-wrangler): add the Cloudflare reference provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement @mieweb/deploy-wrangler, the reference DeployProvider: it wraps the project's wrangler binary and is the canonical implementation other providers are measured against via the contract's conformance test-kit. Behavior: - `deploy`/`dev`/`tail` delegate to wrangler; `login`/`logout`/`whoami` map to their wrangler equivalents; passthrough argv and a resolved `--config` are forwarded (a user-supplied `--config` is respected, not duplicated). - Resource handles are read from the manifest, reloaded from disk after a deploy so wrangler's written-back ids surface (vectorize via `index_name`, queues via `queues.producers`; the AI object binding is intentionally omitted — no provisioned identity). - TTY-safe: `deploy`/`dev`/`tail` preserve wrangler's interactive stdio. Auth classification uses the failed verb's OWN stderr as the authority when captured (non-interactive/CI) — catching a 401/403 for a valid-but- forbidden token — and falls back to a strict, marker-only `wrangler whoami` probe only for the interactive-TTY case. Ambiguous/network/signal failures are reported generically, never mislabeled as auth. Captured stderr is bounded to a 64 KiB suffix so long-running `tail` can't leak memory. - A signal-killed child (null exit) is a failure, not success; `dev` exposes a `closed` promise so a crashed dev surfaces instead of hanging; a user Ctrl-C of the interactive `tail`/`dev` is a clean stop. - The runner is resolved from the active package manager (npm/pnpm/yarn/bun, npx fallback); wrangler is pre-resolved so a missing binary raises an actionable prerequisite error instead of a generic runner exit. `wrangler` is an optional peer dependency (with the MIEWEB_REAL_WRANGLER escape hatch). - `// @ts-check` + `@type {DeployProvider}` make `tsc --noEmit` fail on drift from the contract despite the repo's global `checkJs: false`. Includes hermetic fake-wrangler tests covering the subprocess paths: argv/`--config` forwarding, resource extraction + manifest reload, auth vs generic vs valid-token-403 classification, and whoami reporting. --- packages/deploy-wrangler/package.json | 41 ++ packages/deploy-wrangler/src/index.mjs | 614 ++++++++++++++++++++ packages/deploy-wrangler/src/index.test.mjs | 240 ++++++++ 3 files changed, 895 insertions(+) create mode 100644 packages/deploy-wrangler/package.json create mode 100644 packages/deploy-wrangler/src/index.mjs create mode 100644 packages/deploy-wrangler/src/index.test.mjs diff --git a/packages/deploy-wrangler/package.json b/packages/deploy-wrangler/package.json new file mode 100644 index 0000000..420ad79 --- /dev/null +++ b/packages/deploy-wrangler/package.json @@ -0,0 +1,41 @@ +{ + "name": "@mieweb/deploy-wrangler", + "version": "0.2.1", + "description": "The Cloudflare reference implementation of the @mieweb/deploy-contract `DeployProvider`. Wraps the real `wrangler` binary: `deploy`/`dev`/`tail` delegate verbatim so Cloudflare behavior is identical, and resource handles are read back from the wrangler.jsonc manifest. This is the canonical provider every other deploy backend (opensource-server, future AWS/GCP) is measured against via the contract's conformance test-kit.", + "type": "module", + "license": "MIT", + "author": "MIEWEB", + "homepage": "https://github.com/mieweb/cloud#readme", + "bugs": "https://github.com/mieweb/cloud/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/mieweb/cloud.git", + "directory": "packages/deploy-wrangler" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./src/index.mjs" + }, + "scripts": { + "test": "node --test" + }, + "dependencies": { + "@mieweb/deploy-contract": "workspace:*" + }, + "peerDependencies": { + "wrangler": ">=3" + }, + "peerDependenciesMeta": { + "wrangler": { + "optional": true + } + }, + "devDependencies": { + "wrangler": ">=3" + }, + "files": [ + "src" + ] +} diff --git a/packages/deploy-wrangler/src/index.mjs b/packages/deploy-wrangler/src/index.mjs new file mode 100644 index 0000000..c26a2be --- /dev/null +++ b/packages/deploy-wrangler/src/index.mjs @@ -0,0 +1,614 @@ +// @ts-check +import { spawn } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { join, isAbsolute } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createRequire } from 'node:module'; +import { AuthError } from '@mieweb/deploy-contract'; +import { parseJsonc } from '@mieweb/deploy-contract/jsonc'; + +/** + * `@mieweb/deploy-wrangler` — the Cloudflare **reference** deploy provider. + * + * Implements `DeployProvider` from `@mieweb/deploy-contract` by delegating to + * the real `wrangler` binary. Cloudflare is the reference implementation of the + * portability layer, so this provider does the least translation possible: + * `deploy`/`dev`/`tail` shell out to wrangler verbatim, and resource identity is + * read straight back out of the wrangler.jsonc manifest (wrangler itself owns + * provisioning + write-back). + * + * Every other provider (opensource-server, future AWS/GCP) is measured against + * this one via the contract's conformance test-kit. + * + * Authored as plain ESM with JSDoc types (no build step) to match the rest of + * the mieweb CLI tooling. This file opts into `// @ts-check` (top of file) so + * `tsc --noEmit` type-checks the JSDoc against `@mieweb/deploy-contract` — + * despite the repo's global `checkJs: false` — catching drift from + * `DeployProvider`. + * + * @typedef {import('@mieweb/deploy-contract').DeployProvider} DeployProvider + * @typedef {import('@mieweb/deploy-contract').DeployContext} DeployContext + * @typedef {import('@mieweb/deploy-contract').DeployResult} DeployResult + * @typedef {import('@mieweb/deploy-contract').DeployHandle} DeployHandle + * @typedef {import('@mieweb/deploy-contract').ResourceHandle} ResourceHandle + * @typedef {import('@mieweb/deploy-contract').ResourceKind} ResourceKind + * @typedef {import('@mieweb/deploy-contract').DeployTarget} DeployTarget + */ + +/** + * Max bytes of captured stderr retained (a bounded suffix). The capture buffer + * is only used to classify an eventual auth failure, so long-running commands + * (`tail`) must not accumulate it unbounded. 64 KiB comfortably holds any + * wrangler auth message. + */ +const STDERR_CAP = 64 * 1024; + +/** + * Spawn the repo-pinned `wrangler` and resolve with its exit status. + * + * Resolution order mirrors the CLI's existing delegation: an explicit + * `MIEWEB_REAL_WRANGLER` escape hatch first, otherwise a detected package runner + * (npm/pnpm/yarn/bun — see {@link resolveRunner}) so the project's wrangler is + * used even when it isn't on PATH. Output is inherited so wrangler's own UX + * (prompts, colors, progress) is preserved untouched. + * + * With `capture: true`, stderr is additionally teed into an in-memory buffer and + * returned as `stderr` — used to classify failures (e.g. auth errors) without + * losing the live terminal output. Capture is automatically **disabled when + * stderr is a TTY**, so interactive `deploy`/`dev`/`tail` keep wrangler's native + * colors, progress, and prompts (near-verbatim delegation); classification then + * relies on exit codes and the user sees wrangler's own message. + * + * Resolves with the raw exit `code` **and** any terminating `signal`: a child + * killed by a signal reports `code === null`, which callers must treat as + * failure rather than success (see {@link assertOk}). + * + * @param {string[]} args wrangler argv + * @param {{ cwd: string, signal: AbortSignal, capture?: boolean }} opts + * @returns {Promise<{ code: number|null, signal: NodeJS.Signals|null, stderr: string }>} + */ +function runWrangler(args, opts) { + const real = process.env.MIEWEB_REAL_WRANGLER; + if (!real) { + // Using a package runner (npx/pnpm/…) to launch the project's wrangler: the + // runner itself exists, so a missing wrangler surfaces as a generic non-zero + // exit rather than spawn ENOENT. Pre-resolve wrangler from the project so we + // can raise the actionable prerequisite error instead. + if (!wranglerResolvable(opts.cwd)) { + return Promise.reject( + new Error( + 'wrangler binary not found. Install it as a dependency of your project ' + + '(`npm i -D wrangler`) or set MIEWEB_REAL_WRANGLER to its path.', + ), + ); + } + } + const { cmd, prefix } = real ? { cmd: real, prefix: [] } : resolveRunner(); + const finalArgs = [...prefix, ...args]; + // Capture stderr only when requested AND stderr is not a TTY. On a TTY, + // piping would flip wrangler's own `isTTY` detection (changing its colors, + // progress, and prompts), so we must inherit and leave `stderr` empty — the + // caller then falls back to a whoami probe for auth classification. In + // non-interactive/CI runs stderr is already a pipe, so capturing changes + // nothing the user perceives and lets us classify from the verb's own output. + const capture = opts.capture === true && !process.stderr.isTTY; + + return new Promise((resolvePromise, reject) => { + const child = spawn(cmd, finalArgs, { + cwd: opts.cwd, + stdio: capture ? ['inherit', 'inherit', 'pipe'] : 'inherit', + signal: opts.signal, + }); + let stderr = ''; + if (capture && child.stderr) { + child.stderr.on('data', (chunk) => { + // Keep only a bounded suffix: the buffer exists solely to classify an + // eventual auth failure, so an unbounded accumulation would leak memory + // on long-running commands (e.g. `tail`). CAP is generous enough to + // retain any auth message while staying constant-space. + stderr = (stderr + chunk).slice(-STDERR_CAP); + // Tee: keep wrangler's live output on the terminal. This is the child's + // own output under the contract's subprocess-stdio exception (see + // DeployLogger docs), not provider-authored logging. + process.stderr.write(chunk); + }); + } + child.on('error', (/** @type {any} */ err) => { + // ENOENT here means neither MIEWEB_REAL_WRANGLER nor a resolvable + // `wrangler` exists — surface an actionable prerequisite message. + if (err && err.code === 'ENOENT') { + reject( + new Error( + 'wrangler binary not found. Install it as a dependency of your project ' + + '(`npm i -D wrangler`) or set MIEWEB_REAL_WRANGLER to its path.', + ), + ); + return; + } + reject(err); + }); + // Resolve on `close`, not `exit`: `close` fires only after the child's + // stdio streams have fully flushed, so a captured stderr buffer is complete + // (an auth message can otherwise arrive after `exit` and be missed). + child.on('close', (code, signal) => resolvePromise({ code, signal, stderr })); + }); +} + +/** + * Pick a package runner to launch the project's local `wrangler` when no + * `MIEWEB_REAL_WRANGLER` override is given. We honor `npm_config_user_agent` + * (set by the active package manager) so an npm-only consumer isn't forced to + * have pnpm installed, then fall back to `npx` which ships with Node. + * @returns {{ cmd: string, prefix: string[] }} + */ +function resolveRunner() { + const ua = process.env.npm_config_user_agent ?? ''; + if (ua.startsWith('pnpm')) return { cmd: 'pnpm', prefix: ['exec', 'wrangler'] }; + if (ua.startsWith('yarn')) return { cmd: 'yarn', prefix: ['wrangler'] }; + if (ua.startsWith('bun')) return { cmd: 'bun', prefix: ['x', 'wrangler'] }; + // npm and the generic case: npx resolves a local (or fetched) wrangler and is + // always present with Node. + return { cmd: 'npx', prefix: ['--no-install', 'wrangler'] }; +} + +/** + * Whether `wrangler` is resolvable from the project at `cwd` (so a package + * runner will actually find it). Used to raise an actionable prerequisite error + * before spawning a runner that would otherwise fail with a generic non-zero + * exit when wrangler isn't installed. + * @param {string} cwd project directory + * @returns {boolean} + */ +function wranglerResolvable(cwd) { + const req = createRequire(pathToFileURL(join(cwd, 'package.json')).href); + for (const spec of ['wrangler/package.json', 'wrangler']) { + try { + req.resolve(spec); + return true; + } catch { + // try next + } + } + return false; +} + +/** + * Throw a consistent error unless wrangler exited cleanly with code 0. A + * `null` code (child terminated by a signal, e.g. SIGKILL/SIGTERM) is a + * failure, not a success — mapping it to 0 would let a killed deploy/login + * report success. + * + * @param {{ code: number|null, signal: NodeJS.Signals|null }} result + * @param {string} verb for the message + */ +function assertOk(result, verb) { + if (result.code === 0) return; + if (result.signal) { + throw new Error(`wrangler ${verb} was terminated by signal ${result.signal}`); + } + throw new Error(`wrangler ${verb} exited with code ${result.code}`); +} + +/** Exit codes wrangler uses when the caller is unauthenticated/forbidden. */ +const AUTH_EXIT_HINT = + 'run `wrangler login`, or set CLOUDFLARE_API_TOKEN (+ CLOUDFLARE_ACCOUNT_ID) in your environment'; + +/** + * Build wrangler's `--config ` flag from the context's resolved manifest + * path, so a project whose canonical manifest lives at a custom location is + * deployed (and reloaded) correctly. Empty when no path is known (wrangler then + * uses its own discovery) OR when the user already passed their own `--config` + * in argv — in which case we must not add a second, conflicting flag. + * @param {DeployContext} context + * @returns {string[]} + */ +function configFlag(context) { + if (userConfigPath(context.argv) !== null) return []; // user supplied one + return context.manifestPath ? ['--config', context.manifestPath] : []; +} + +/** + * Extract a user-supplied wrangler config path from passthrough argv, if any + * (`--config

`, `--config=

`, `-c

`). Returns the path, or null when the + * user did not specify one. + * @param {readonly string[]} argv + * @returns {string|null} + */ +function userConfigPath(argv) { + for (let i = 0; i < argv.length; i += 1) { + const a = argv[i]; + if (a === '--config' || a === '-c') return argv[i + 1] ?? ''; + if (a.startsWith('--config=')) return a.slice('--config='.length); + } + return null; +} + +/** + * The manifest path to reload after a deploy: the user's `--config` if they + * supplied one (resolved against root), else the context's manifest path. + * Ensures reported resource handles come from the manifest actually deployed. + * @param {DeployContext} context + * @returns {string|undefined} + */ +function effectiveManifestPath(context) { + const user = userConfigPath(context.argv); + if (user) return isAbsolute(user) ? user : join(context.root, user); + return context.manifestPath; +} + +/** + * Heuristically decide whether captured wrangler stderr indicates a genuine + * authentication/authorization failure (as opposed to a network, config, or + * resource error). We look for explicit markers so we only steer the user to + * `mieweb login` when that is actually the problem. + * @param {string} stderr + * @returns {boolean} + */ +function isAuthFailure(stderr) { + if (!stderr) return false; + const s = stderr.toLowerCase(); + return ( + s.includes('[code: 10000]') || // wrangler: Authentication error + /\b(401|403)\b/.test(s) || + s.includes('unauthorized') || + s.includes('not authenticated') || + s.includes('authentication error') || + s.includes('please run `wrangler login`') || + s.includes('you are not authenticated') + ); +} + +/** + * Decide whether a failed `deploy`/`dev`/`tail` was an authentication/permission + * failure, so the CLI can offer the login hint. + * + * Authority order: + * 1. **The verb's own captured stderr** (present in non-interactive/CI runs, + * where we can capture without harming wrangler's UX). An explicit auth + * marker there — including 401/403 for a valid-but-unauthorized token — is + * the definitive signal. + * 2. **A strict `whoami` probe**, used only when the verb's stderr was not + * captured (interactive TTY). It classifies as auth *only* on an explicit + * not-authenticated marker from whoami — never on an empty stderr or a bare + * non-zero exit, so an ambiguous/network whoami failure does not get + * mislabeled as auth. (This path cannot see operation-level 403s when the + * token is otherwise valid; those are reported generically rather than + * guessed.) + * + * @param {DeployContext} context + * @param {{ stderr: string }} result the failed verb's result + * @returns {Promise} + */ +async function failedDueToAuth(context, result) { + // 1. Prefer the verb's own output when we have it (captures 401/403 including + // valid-token-but-forbidden, which a whoami probe cannot see). + if (result.stderr && result.stderr.trim() !== '') { + return isAuthFailure(result.stderr); + } + // 2. Interactive TTY: the verb's stderr wasn't captured. Probe whoami, but + // only trust an *explicit* not-authenticated marker. + try { + const who = await runWrangler(['whoami'], { + cwd: context.root, + signal: context.signal, + capture: true, + }); + if (who.code === 0) return false; // clearly authenticated + return isAuthFailure(who.stderr); // explicit marker only; no empty fallback + } catch { + return false; // couldn't probe → don't mislabel + } +} + +/** + * Read the resources declared in a wrangler manifest as neutral + * {@link ResourceHandle}s. On Cloudflare, identity lives in wrangler's native + * fields (`database_id`, `bucket_name`, `id`, …); after a successful deploy the + * committed manifest is authoritative, so we surface whatever ids are present. + * Entries still missing an id (freshly auto-provisioned, not yet written back) + * are reported with an empty id — the contract permits it, and the reference + * flow leaves persistence to wrangler/the user committing the file. + * + * @param {Readonly>} manifest + * @returns {ResourceHandle[]} + */ +function readResources(manifest) { + /** @type {ResourceHandle[]} */ + const out = []; + /** + * @param {string} key manifest array key + * @param {ResourceKind} kind neutral kind + * @param {string} idField wrangler's identity field for this kind + */ + const collect = (key, kind, idField) => { + const arr = manifest[key]; + if (!Array.isArray(arr)) return; + for (const entry of arr) { + if (!entry || typeof entry !== 'object') continue; + const binding = entry.binding ?? entry.name; + if (typeof binding !== 'string') continue; + out.push({ binding, kind, id: typeof entry[idField] === 'string' ? entry[idField] : '' }); + } + }; + + collect('d1_databases', 'database', 'database_id'); + collect('r2_buckets', 'bucket', 'bucket_name'); + collect('kv_namespaces', 'kv', 'id'); + collect('vectorize', 'vector', 'index_name'); + // Note: the AI binding (`ai: { binding: "AI" }`) is intentionally omitted. + // It is an object, not an array, and Workers AI is not provisioned per-app — + // there is no backend resource id to report, and the binding name is not one. + + // Queue *producers* live under `queues.producers`, each with a `binding` and + // a `queue` (the queue name = its identity). + const queues = /** @type {any} */ (manifest.queues); + if (queues && typeof queues === 'object' && Array.isArray(queues.producers)) { + for (const p of queues.producers) { + if (p && typeof p === 'object' && typeof p.binding === 'string') { + out.push({ binding: p.binding, kind: 'queue', id: typeof p.queue === 'string' ? p.queue : '' }); + } + } + } + + return out; +} + +/** + * Re-read the manifest from disk after a deploy. When wrangler auto-provisions a + * resource it writes the new id back into the on-disk `wrangler.jsonc`; the + * in-memory {@link DeployContext.manifest} was parsed *before* the deploy and is + * stale, so reading ids from it would miss freshly-created resources. We reload + * the file so surfaced {@link ResourceHandle}s carry the written-back ids. + * + * Uses {@link DeployContext.manifestPath} (the resolved path, honoring a custom + * location) when present, falling back to the conventional filenames under + * {@link DeployContext.root}. Best-effort: if the file can't be found/parsed we + * fall back to the in-memory manifest rather than fail the deploy — the deploy + * already succeeded. + * + * @param {DeployContext} context + * @returns {Readonly>} + */ +function reloadManifest(context) { + const effective = effectiveManifestPath(context); + const candidates = effective + ? [effective] + : [join(context.root, 'wrangler.jsonc'), join(context.root, 'wrangler.json')]; + for (const p of candidates) { + if (!existsSync(p)) continue; + try { + return /** @type {Record} */ (parseJsonc(readFileSync(p, 'utf8'))); + } catch { + break; + } + } + return context.manifest; +} + +/** + * The reference provider instance. Stateless — safe to share. + * @type {DeployProvider} + */ +export const wranglerProvider = { + name: 'wrangler', + + /** + * The reference provider owns the `cloudflare` target only. Other targets are + * served by their own providers. + * @param {DeployTarget} target + */ + supports(target) { + return target === 'cloudflare'; + }, + + /** + * Deploy by delegating to `wrangler deploy`. wrangler reads the manifest, + * ensures resources exist (native provisioning), and pushes the worker; we + * then reload the (possibly written-back) manifest and surface its resources + * as neutral handles. Idempotent because wrangler's own deploy is. + * + * A genuine auth failure (explicit 401/403 / "not authenticated" in wrangler's + * output) is mapped to {@link AuthError} so the CLI can offer the login hint. + * We only translate *explicitly identified* auth errors: other non-zero exits + * (network, config, resource errors) preserve the original deploy failure so + * we don't mislead the user into re-authenticating. + * @param {DeployContext} context + * @returns {Promise} + */ + async deploy(context) { + context.logger.info('wrangler: deploying via the pinned wrangler binary'); + // Request capture: honored only in non-TTY (CI), where the verb's own stderr + // becomes the authoritative auth signal; on a TTY it inherits and we fall + // back to a whoami probe. Either way TTY UX is preserved. + const result = await runWrangler(['deploy', ...configFlag(context), ...context.argv], { + cwd: context.root, + signal: context.signal, + capture: true, + }); + if (result.code !== 0) { + if (await failedDueToAuth(context, result)) { + throw new AuthError('wrangler', context.target, AUTH_EXIT_HINT); + } + assertOk(result, 'deploy'); + } + // wrangler may have written new ids back into wrangler.jsonc; read those. + const resources = readResources(reloadManifest(context)); + context.logger.info(`wrangler: deploy complete (${resources.length} resource binding(s))`); + return { resources }; + }, + + /** + * Start `wrangler dev`. wrangler stays in the foreground until interrupted; we + * bridge that to the contract's {@link DeployHandle}. The handle's `closed` + * promise resolves/rejects when the child exits on its own (crash, or the user + * quitting wrangler), so the CLI can stop waiting instead of hanging. + * @param {DeployContext} context + * @returns {Promise} + */ + async dev(context) { + const controller = new AbortController(); + // Fold the caller's signal into ours so either can stop the child. Handle an + // already-aborted incoming signal too (don't miss the event). + if (context.signal.aborted) controller.abort(); + else context.signal.addEventListener('abort', () => controller.abort(), { once: true }); + + const done = runWrangler(['dev', ...configFlag(context), ...context.argv], { + cwd: context.root, + signal: controller.signal, + capture: true, + }).then( + async (result) => { + // A clean exit (code 0) or a stop()-triggered abort is fine; anything + // else is a dev failure the CLI should learn about via `closed`. + if (result.code === 0 || controller.signal.aborted) return; + // Classify: the verb's own stderr (CI) or a whoami probe (TTY). + if (await failedDueToAuth(context, result)) { + throw new AuthError('wrangler', context.target, AUTH_EXIT_HINT); + } + throw new Error( + result.signal + ? `wrangler dev was terminated by signal ${result.signal}` + : `wrangler dev exited with code ${result.code}`, + ); + }, + (err) => { + if (/** @type {any} */ (err)?.name === 'AbortError') return; // expected on stop() + throw err; + }, + ); + + context.logger.info('wrangler: dev server started'); + return { + closed: done, + stop() { + controller.abort(); + return done.catch(() => undefined); + }, + }; + }, + + /** + * Stream logs via `wrangler tail`. This is an interactive, long-running + * command: a user Ctrl-C is normal termination and returns success (like + * {@link dev}), not an error. Genuine child failures still throw, and an + * explicit auth failure is translated to {@link AuthError} for the login hint. + * @param {DeployContext} context + */ + async tail(context) { + let result; + try { + result = await runWrangler(['tail', ...configFlag(context), ...context.argv], { + cwd: context.root, + signal: context.signal, + capture: true, + }); + } catch (err) { + // The caller interrupted (Ctrl-C) → aborting the child surfaces as + // AbortError. For an interactive stream that is a clean stop, not a failure. + if (/** @type {any} */ (err)?.name === 'AbortError' && context.signal.aborted) return; + throw err; + } + // A signal stop we initiated via context.signal is also a clean exit. + if (context.signal.aborted) return; + if (result.code !== 0 && (await failedDueToAuth(context, result))) { + throw new AuthError('wrangler', context.target, AUTH_EXIT_HINT); + } + assertOk(result, 'tail'); + }, + + /** + * Report auth status via `wrangler whoami`. We classify the result rather than + * treating every non-zero exit as "not authenticated": only an explicit auth + * failure yields `authenticated: false`. Other failures (network/DNS, config, + * signal kill) throw, so the CLI surfaces "status could not be determined" + * instead of falsely claiming the user is logged out. + * @param {DeployContext} context + * @returns {Promise} + */ + async whoami(context) { + const result = await runWrangler(['whoami', ...context.argv], { + cwd: context.root, + signal: context.signal, + capture: true, + }); + if (result.code === 0) { + // Env-token auth vs. the OAuth cache both satisfy wrangler; report the + // dominant method for diagnostics without asserting which one wrangler used. + const method = process.env.CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_API_KEY ? 'env' : 'oauth'; + return { authenticated: true, method }; + } + // Non-zero: only an explicit auth signal means "logged out". + if (isAuthFailure(result.stderr)) { + return { authenticated: false }; + } + // Anything else (signal kill, network, config) is an *undetermined* status. + if (result.signal) { + throw new Error(`wrangler whoami was terminated by signal ${result.signal}`); + } + throw new Error( + `wrangler whoami could not determine auth status (exited with code ${result.code})`, + ); + }, + + /** + * Authenticate via `wrangler login` (interactive browser OAuth). Inherits + * stdio so wrangler's prompts/redirect flow work unchanged. A non-zero exit + * (or a signal kill) means the flow was declined or failed. + * @param {DeployContext} context + */ + async login(context) { + context.logger.info('wrangler: starting interactive login (browser OAuth)'); + let result; + try { + result = await runWrangler(['login', ...context.argv], { + cwd: context.root, + signal: context.signal, + }); + } catch (err) { + // Ctrl-C during the interactive flow aborts the child (AbortError); treat + // that as a cancelled login, consistent with the non-zero-exit path. + if (/** @type {any} */ (err)?.name === 'AbortError' && context.signal.aborted) { + throw new AuthError('wrangler', context.target, 'wrangler login was cancelled'); + } + throw err; + } + if (result.code !== 0) { + throw new AuthError('wrangler', context.target, 'wrangler login was cancelled or failed'); + } + }, + + /** + * Clear the cached OAuth session via `wrangler logout`. Note this does not + * (and cannot) unset `CLOUDFLARE_API_TOKEN`-style env credentials — those are + * host-injected and outside wrangler's control; we surface that as a warning. + * @param {DeployContext} context + */ + async logout(context) { + if (process.env.CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_API_KEY) { + context.logger.warn( + 'wrangler: environment credentials (CLOUDFLARE_API_TOKEN/KEY) are set and ' + + 'take precedence — unset them in your shell to fully log out.', + ); + } + const result = await runWrangler(['logout', ...context.argv], { + cwd: context.root, + signal: context.signal, + }); + assertOk(result, 'logout'); + }, +}; + +/** + * Factory form of the provider, per the contract's `DeployProviderModule` + * convention. Ignores `env` — the reference provider takes its configuration + * from wrangler.jsonc/PATH, not host env vars (beyond the + * `MIEWEB_REAL_WRANGLER` escape hatch read at spawn time). + * + * @param {import('@mieweb/deploy-contract').ProviderEnv} [_env] + * @returns {DeployProvider} + */ +export function createProvider(_env) { + return wranglerProvider; +} + +export default wranglerProvider; diff --git a/packages/deploy-wrangler/src/index.test.mjs b/packages/deploy-wrangler/src/index.test.mjs new file mode 100644 index 0000000..37e1e08 --- /dev/null +++ b/packages/deploy-wrangler/src/index.test.mjs @@ -0,0 +1,240 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync, chmodSync, rmSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { runProviderConformance } from '@mieweb/deploy-contract/testkit'; +import provider, { wranglerProvider, createProvider } from '@mieweb/deploy-wrangler'; + +/** + * Structural conformance for the reference provider. We run the kit WITHOUT + * `live: true` so the suite validates the interface contract (name, supports, + * deploy signature, result shape rules) without actually invoking wrangler / + * hitting Cloudflare. A live variant belongs in an integration job with real + * credentials + the test-app fixture. + */ + +test('exports a DeployProvider by default and via factory', () => { + assert.equal(provider, wranglerProvider); + assert.equal(createProvider(process.env), wranglerProvider); + assert.equal(typeof wranglerProvider.deploy, 'function'); +}); + +test('supports only the cloudflare target', () => { + assert.equal(wranglerProvider.supports('cloudflare'), true); + assert.equal(wranglerProvider.supports('mieweb'), false); + assert.equal(wranglerProvider.supports('local'), false); +}); + +test('implements the auth verbs (login/logout/whoami)', () => { + assert.equal(typeof wranglerProvider.login, 'function'); + assert.equal(typeof wranglerProvider.logout, 'function'); + assert.equal(typeof wranglerProvider.whoami, 'function'); +}); + +test('passes the contract conformance test-kit (structural)', async () => { + const report = await runProviderConformance(wranglerProvider, { + target: 'cloudflare', + manifest: { + name: 'demo', + d1_databases: [{ binding: 'DB', database_id: 'abc-123' }], + r2_buckets: [{ binding: 'RECORDINGS', bucket_name: 'demo-recordings' }], + kv_namespaces: [{ binding: 'SESSIONS', id: 'kv-1' }], + }, + }); + assert.deepEqual( + report.failures, + [], + `conformance failures:\n${report.failures.map((f) => ` - ${f.name}: ${f.detail}`).join('\n')}`, + ); +}); + +test('conformance kit rejects a bogus ResourceKind (live)', async () => { + // A stub provider that returns an invalid `kind` must fail the result-shape + // check — proving the kit validates against the closed ResourceKind union. + const bogus = { + name: 'bogus', + supports: () => true, + async deploy() { + return { resources: [{ binding: 'X', kind: 'not-a-kind', id: 'y' }] }; + }, + }; + const report = await runProviderConformance(bogus, { + target: 'cloudflare', + manifest: {}, + live: true, + }); + assert.ok( + report.failures.some((f) => /ResourceKind/.test(f.detail ?? '')), + 'expected a ResourceKind validation failure', + ); +}); + +/* ------------------------------------------------------------------ * + * Hermetic fake-wrangler coverage for the reference provider's actual + * subprocess paths (deploy/argv/reload/resource-extraction, auth mapping, + * whoami classification). No network, no real wrangler — a shell stub echoes + * args + can simulate manifest write-back and specific exit/stderr. + * ------------------------------------------------------------------ */ + +/** Create a temp project with a fake wrangler; returns helpers + cleanup. */ +function makeFixture(manifest, script) { + const dir = mkdtempSync(join(tmpdir(), 'mieweb-wrangler-')); + const manifestPath = join(dir, 'wrangler.jsonc'); + writeFileSync(manifestPath, JSON.stringify(manifest)); + const fake = join(dir, 'fake-wrangler'); + const argsLog = join(dir, 'args.log'); + writeFileSync(fake, `#!/usr/bin/env bash\necho "$@" >> "${argsLog}"\n${script}\n`); + chmodSync(fake, 0o755); + const ctx = (extra = {}) => ({ + root: dir, + target: 'cloudflare', + manifest, + manifestPath, + mieweb: {}, + targetConfig: {}, + argv: [], + logger: { info() {}, warn() {}, error() {} }, + signal: new AbortController().signal, + ...extra, + }); + return { + dir, + fake, + ctx, + readArgs: () => (readFileSync(argsLog, 'utf8')), + cleanup: () => rmSync(dir, { recursive: true, force: true }), + }; +} + +test('deploy: forwards --config + argv, extracts resources, reloads written-back ids', async () => { + // Fake wrangler writes a KV id back into the manifest on deploy. + const fx = makeFixture( + { + name: 'demo', + d1_databases: [{ binding: 'DB', database_id: 'd1' }], + vectorize: [{ binding: 'VEC', index_name: 'idx' }], + kv_namespaces: [{ binding: 'S' }], + ai: { binding: 'AI' }, + }, + ` +if [ "$1" = "deploy" ]; then + node -e 'const f=process.env.MF;const j=JSON.parse(require("fs").readFileSync(f));j.kv_namespaces[0].id="kv-new";require("fs").writeFileSync(f,JSON.stringify(j))' +fi +exit 0`, + ); + process.env.MIEWEB_REAL_WRANGLER = fx.fake; + process.env.MF = join(fx.dir, 'wrangler.jsonc'); + try { + const res = await wranglerProvider.deploy(fx.ctx({ argv: ['--env', 'prod'] })); + const byBinding = Object.fromEntries(res.resources.map((r) => [r.binding, r])); + // argv + --config forwarded + const args = fx.readArgs(); + assert.match(args, /deploy/); + assert.match(args, /--config/); + assert.match(args, /--env prod/); + // resource extraction: vectorize via index_name, KV id reloaded, AI omitted + assert.equal(byBinding.DB.id, 'd1'); + assert.equal(byBinding.VEC.kind, 'vector'); + assert.equal(byBinding.VEC.id, 'idx'); + assert.equal(byBinding.S.id, 'kv-new'); // reloaded from written-back manifest + assert.equal(byBinding.AI, undefined); // AI not reported + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + delete process.env.MF; + fx.cleanup(); + } +}); + +test('deploy: auth failure (whoami probe unauth) maps to AuthError; generic failure does not', async () => { + // deploy fails; the whoami probe reports NOT authenticated → AuthError. + const authFx = makeFixture( + { name: 'demo' }, + 'if [ "$1" = "whoami" ]; then echo "You are not authenticated" >&2; exit 1; fi\nexit 1', + ); + process.env.MIEWEB_REAL_WRANGLER = authFx.fake; + try { + await assert.rejects(() => wranglerProvider.deploy(authFx.ctx()), (e) => e.name === 'AuthError'); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + authFx.cleanup(); + } + + // deploy fails but whoami reports authenticated (exit 0) → generic failure. + const netFx = makeFixture( + { name: 'demo' }, + 'if [ "$1" = "whoami" ]; then exit 0; fi\necho "getaddrinfo ENOTFOUND" >&2; exit 1', + ); + process.env.MIEWEB_REAL_WRANGLER = netFx.fake; + try { + await assert.rejects( + () => wranglerProvider.deploy(netFx.ctx()), + (e) => e.name !== 'AuthError' && /exited with code 1/.test(e.message), + ); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + netFx.cleanup(); + } +}); + +test("deploy: the verb's own 403 is authoritative even when whoami is authenticated", async () => { + // Valid token lacking operation permission: deploy prints 403, whoami exits 0. + // The verb's own stderr must win (path 1), yielding AuthError — a whoami-only + // probe would miss this (whoami is authenticated). + const fx = makeFixture( + { name: 'demo' }, + 'if [ "$1" = "whoami" ]; then exit 0; fi\necho "A request failed [code: 10000] 403" >&2; exit 1', + ); + process.env.MIEWEB_REAL_WRANGLER = fx.fake; + try { + await assert.rejects(() => wranglerProvider.deploy(fx.ctx()), (e) => e.name === 'AuthError'); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + fx.cleanup(); + } +}); + +test('whoami: authed=0, explicit-unauth via marker, network failure throws undetermined', async () => { + const okFx = makeFixture({}, 'exit 0'); + process.env.MIEWEB_REAL_WRANGLER = okFx.fake; + try { + assert.equal((await wranglerProvider.whoami(okFx.ctx())).authenticated, true); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + okFx.cleanup(); + } + + const unauthFx = makeFixture({}, 'echo "You are not authenticated" >&2; exit 1'); + process.env.MIEWEB_REAL_WRANGLER = unauthFx.fake; + try { + assert.equal((await wranglerProvider.whoami(unauthFx.ctx())).authenticated, false); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + unauthFx.cleanup(); + } + + const netFx = makeFixture({}, 'echo "getaddrinfo ENOTFOUND" >&2; exit 1'); + process.env.MIEWEB_REAL_WRANGLER = netFx.fake; + try { + await assert.rejects(() => wranglerProvider.whoami(netFx.ctx()), /could not determine/); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + netFx.cleanup(); + } +}); + +test('dev: a credentialed 401/403 exit rejects `closed` with AuthError', async () => { + // dev exits non-zero; the whoami probe reports NOT authenticated → AuthError. + const fx = makeFixture( + { name: 'demo' }, + 'if [ "$1" = "whoami" ]; then echo "You are not authenticated" >&2; exit 1; fi\nexit 1', + ); + process.env.MIEWEB_REAL_WRANGLER = fx.fake; + try { + const handle = await wranglerProvider.dev(fx.ctx()); + await assert.rejects(() => handle.closed, (e) => e.name === 'AuthError'); + } finally { + delete process.env.MIEWEB_REAL_WRANGLER; + fx.cleanup(); + } +}); From 326d02e10282e49770bc31ac24dda7e9225434ac Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Fri, 18 Sep 2026 12:58:47 -0400 Subject: [PATCH 3/6] feat(cli): route deploy lifecycle verbs through a DeployProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire @mieweb/cli to the deploy contract. `deploy`/`dev`/`tail`/`login`/ `logout`/`whoami`/`destroy` now resolve a `DeployProvider` for the active target and run through it; Cloudflare resolves to the wrangler reference provider, and any target can name a provider package in `mieweb.jsonc` (`targets[t].provider`). Targets without a provider fall through to the existing behavior unchanged (local/mieweb host harness, wrangler delegation). - Provider resolution: bare specifiers resolve from the *project's* module graph via Node's ESM resolver (rooted at the project package.json, honoring the `import` condition), with a project-rooted CJS fallback; relative and absolute (incl. Windows) paths resolve against the project root. - Neutral `DeployContext` construction: forwards a `manifestPath` only when the file exists (or is explicitly configured), an AbortSignal wired to SIGINT/SIGTERM, and a structured logger. - Secret boundary: `targetConfig` and the `mieweb` view are recursively redacted before reaching a provider — the data-plane `bindings` bag and any secret-bearing key (token/password/accessKey/…) at any depth are dropped, and the top-level `mieweb` projection is allowlisted. Deploy credentials come only from the environment. - `AuthError` is surfaced with the provider's login hint, or a fallback `Run \`mieweb login\`` nudge; it propagates from a failed `dev` too. - The shared JSONC parser moves to @mieweb/deploy-contract/jsonc (the CLI's local copy is removed), so config loading and manifest reload share one string-aware implementation. --- packages/cli/package.json | 4 +- packages/cli/src/config.mjs | 2 +- packages/cli/src/index.mjs | 50 +++- packages/cli/src/jsonc.mjs | 68 ------ packages/cli/src/provider.mjs | 441 ++++++++++++++++++++++++++++++++++ 5 files changed, 487 insertions(+), 78 deletions(-) delete mode 100644 packages/cli/src/jsonc.mjs create mode 100644 packages/cli/src/provider.mjs diff --git a/packages/cli/package.json b/packages/cli/package.json index 7fb5f68..1f3969a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,7 +30,9 @@ "mieweb": "./src/index.mjs" }, "dependencies": { - "@mieweb/cloud-adapters": "workspace:*" + "@mieweb/cloud-adapters": "workspace:*", + "@mieweb/deploy-contract": "workspace:*", + "@mieweb/deploy-wrangler": "workspace:*" }, "files": [ "src", diff --git a/packages/cli/src/config.mjs b/packages/cli/src/config.mjs index 311c511..193f0a2 100644 --- a/packages/cli/src/config.mjs +++ b/packages/cli/src/config.mjs @@ -1,6 +1,6 @@ import { readFileSync, existsSync } from 'node:fs'; import { dirname, resolve, isAbsolute } from 'node:path'; -import { parseJsonc } from './jsonc.mjs'; +import { parseJsonc } from '@mieweb/deploy-contract/jsonc'; /** * @typedef {'cloudflare'|'local'|'mieweb'|'aws'|'gcp'} CloudTarget diff --git a/packages/cli/src/index.mjs b/packages/cli/src/index.mjs index c87a1d1..5abf6f2 100755 --- a/packages/cli/src/index.mjs +++ b/packages/cli/src/index.mjs @@ -7,11 +7,14 @@ * mieweb tail * mieweb d1 migrations apply bluehive-hum * - * On the `cloudflare` target (the default) every command is forwarded - * verbatim to the real `wrangler` binary, so Cloudflare behavior is identical - * and nothing about the existing workflow changes. Select another environment - * with `--target ` or `MIEWEB_TARGET=`; those commands are handled by the - * matching @mieweb adapter instead. + * On the `cloudflare` target (the default) most commands are forwarded verbatim + * to the real `wrangler` binary. The deploy lifecycle verbs (`deploy`, `dev`, + * `tail`, plus `login`/`logout`/`whoami`/`destroy`) instead run through a + * pluggable deploy provider (`@mieweb/deploy-contract`); the Cloudflare + * reference provider still drives `wrangler` underneath, adding config + * injection, structured logging, resource reporting, and auth-aware errors. + * Select another environment with `--target ` or `MIEWEB_TARGET=`; those + * commands are handled by the matching @mieweb adapter/provider instead. * * This file is plain ESM JavaScript on purpose so `mieweb` runs with bare * `node` — no build step, no transpiler, no extra runtime dependency. @@ -23,6 +26,10 @@ import { delegateToWrangler } from './cloudflare.mjs'; import { runHostTarget } from './local.mjs'; import { runInit } from './init.mjs'; import { runImagesCommand, runRegistryCommand } from './images.mjs'; +import { resolveProvider, runProviderVerb } from './provider.mjs'; + +/** Verbs handled by the deploy-contract provider layer. */ +const PROVIDER_VERBS = new Set(['deploy', 'dev', 'tail', 'login', 'logout', 'whoami', 'destroy']); /** Read this CLI's version from its package.json. */ function miewebVersion() { @@ -94,8 +101,32 @@ async function main(argv) { }); } + // Deploy-contract verbs route through a DeployProvider when one resolves for + // the active target: deploy, dev, tail, login, logout, whoami, destroy (see + // PROVIDER_VERBS). Cloudflare resolves to the wrangler reference provider; + // other targets can name a provider package in mieweb.jsonc + // (`targets[t].provider`). Everything else (d1 migrations, etc.) and any + // target without a provider falls through to the legacy paths below. + if (PROVIDER_VERBS.has(args[0])) { + let provider = null; + try { + provider = await resolveProvider(config); + } catch (err) { + console.error(`mieweb: ${err?.message ?? err}`); + return 1; + } + if (provider) { + return runProviderVerb( + /** @type {'deploy'|'dev'|'tail'|'login'|'logout'|'whoami'|'destroy'} */ (args[0]), + provider, + config, + args.slice(1), + ); + } + } + if (config.target === 'cloudflare') { - // Reference path: hand everything to wrangler untouched. + // Reference path for non-provider commands: hand to wrangler untouched. return delegateToWrangler(args, { cwd: config.root }); } @@ -128,9 +159,12 @@ function printHelp() { '', 'Common commands:', ' mieweb init [dir] Scaffold a new mieweb project.', + " mieweb login Authenticate the active target's provider.", + " mieweb logout Clear the provider's stored session.", + " mieweb whoami Show the provider's auth status.", ' mieweb dev Start a dev server for the active target.', - ' mieweb deploy Deploy (cloudflare only).', - ' mieweb tail Stream logs (cloudflare only).', + " mieweb deploy Deploy via the active target's provider.", + ' mieweb tail Stream logs from the deployed worker.', ' mieweb d1 migrations apply Apply ./migrations to the target DB.', ' mieweb images build|push|inspect|status Build & skopeo-push container images.', ' mieweb registry login|logout skopeo login to the target registry.', diff --git a/packages/cli/src/jsonc.mjs b/packages/cli/src/jsonc.mjs deleted file mode 100644 index 3933813..0000000 --- a/packages/cli/src/jsonc.mjs +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Minimal JSONC reader shared by the mieweb CLI. - * - * wrangler.jsonc (and mieweb.jsonc) use comments and trailing commas, which - * `JSON.parse` rejects. Rather than pull in a dependency (and to keep the CLI - * runnable with bare `node`), we strip comments + trailing commas in a - * string-aware single pass, then hand the result to `JSON.parse`. - * - * This is deliberately small: it understands `//` line comments, `/​* *​/` - * block comments, double-quoted strings with escapes, and trailing commas - * before `}`/`]`. That covers everything wrangler emits. - * - * @param {string} text raw JSONC source - * @returns {unknown} parsed value - */ -export function parseJsonc(text) { - let out = ''; - let i = 0; - const n = text.length; - let inString = false; - - while (i < n) { - const ch = text[i]; - const next = text[i + 1]; - - if (inString) { - out += ch; - if (ch === '\\') { - // Copy the escaped character verbatim. - out += text[i + 1] ?? ''; - i += 2; - continue; - } - if (ch === '"') inString = false; - i += 1; - continue; - } - - if (ch === '"') { - inString = true; - out += ch; - i += 1; - continue; - } - - if (ch === '/' && next === '/') { - // Line comment: skip to end of line. - i += 2; - while (i < n && text[i] !== '\n') i += 1; - continue; - } - - if (ch === '/' && next === '*') { - // Block comment: skip to closing */. - i += 2; - while (i < n && !(text[i] === '*' && text[i + 1] === '/')) i += 1; - i += 2; - continue; - } - - out += ch; - i += 1; - } - - // Remove trailing commas (`,]` / `,}`), tolerating whitespace between. - out = out.replace(/,(\s*[}\]])/g, '$1'); - return JSON.parse(out); -} diff --git a/packages/cli/src/provider.mjs b/packages/cli/src/provider.mjs new file mode 100644 index 0000000..22f86ac --- /dev/null +++ b/packages/cli/src/provider.mjs @@ -0,0 +1,441 @@ +// @ts-check +import { pathToFileURL } from 'node:url'; +import { resolve, join, isAbsolute } from 'node:path'; +import { createRequire } from 'node:module'; +import { existsSync } from 'node:fs'; + +/** + * Provider selection + context construction for the mieweb CLI. + * + * The CLI does not know *how* any target deploys — it delegates to a + * `DeployProvider` (see `@mieweb/deploy-contract`). This module owns two things: + * + * 1. **Selection** — map the active target to a provider. Cloudflare uses the + * reference provider (`@mieweb/deploy-wrangler`); other targets may name a + * provider package in `mieweb.jsonc` (`targets[t].provider`), which we + * dynamically `import()`. The first provider whose `supports(target)` + * returns true wins. + * 2. **Context** — project the CLI's internal `MiewebConfig` down to the + * neutral `DeployContext` the contract defines. Providers see only what + * they need, never the CLI's plumbing. + * + * Plain ESM + JSDoc (no build step); contract types referenced via `import()`. + * + * @typedef {import('@mieweb/deploy-contract').DeployProvider} DeployProvider + * @typedef {import('@mieweb/deploy-contract').DeployContext} DeployContext + * @typedef {import('@mieweb/deploy-contract').DeployProviderModule} DeployProviderModule + * @typedef {import('./config.mjs').MiewebConfig} MiewebConfig + */ + +/** + * Built-in provider mapping. Kept tiny and explicit for the POC: only the + * reference (cloudflare → wrangler) is wired in-repo. Other targets resolve + * their provider dynamically from config (below), so opensource-server can ship + * its provider as a separate package without a change here. + * + * @type {Record} + */ +const BUILTIN_PROVIDERS = { + cloudflare: '@mieweb/deploy-wrangler', +}; + +/** + * Normalize a dynamically-imported module into a `DeployProvider`, honoring the + * contract's `DeployProviderModule` convention (a `default` provider or a + * `createProvider(env)` factory). + * + * @param {DeployProviderModule} mod + * @param {string} specifier for diagnostics + * @returns {DeployProvider} + */ +function toProvider(mod, specifier) { + if (typeof mod.createProvider === 'function') return mod.createProvider(process.env); + if (mod.default && typeof mod.default.deploy === 'function') return mod.default; + throw new Error( + `provider "${specifier}" does not export a DeployProvider ` + + '(expected a `default` export or a `createProvider` factory).', + ); +} + +/** + * Resolve the provider for the active target. + * + * Resolution order: + * 1. `mieweb.jsonc` → `targets[target].provider` (a package name or path). + * 2. the built-in mapping (currently just cloudflare → wrangler). + * + * @param {MiewebConfig} config + * @returns {Promise} the provider, or null if none applies + */ +export async function resolveProvider(config) { + const configured = + config.targetConfig && typeof config.targetConfig.provider === 'string' + ? config.targetConfig.provider + : undefined; + const specifier = configured ?? BUILTIN_PROVIDERS[config.target]; + if (!specifier) return null; + + // Resolve the provider module. Two shapes: + // * a filesystem path (dev/local providers) → resolve against the project + // root (where mieweb.jsonc lives), NOT the process cwd, so running the CLI + // from a subdirectory still finds `./provider.mjs`. + // * a bare package specifier → resolve from the *project's* node_modules + // (rooted at config.root), NOT the CLI's own dependency tree, so a provider + // the consumer installed (e.g. `@mieweb/os-provider`) is found even when it + // isn't hoisted into the CLI's deps. + let importable; + if (specifier.startsWith('.') || isAbsolute(specifier)) { + // Relative → resolve against project root; absolute (incl. Windows + // `C:\...`) → use as-is. `resolve` handles both correctly. + importable = pathToFileURL(resolve(config.root, specifier)).href; + } else { + // Bare package specifier: resolve from the *project's* module graph using + // Node's ESM resolver (honors the `import` condition, so an ESM-only + // provider resolves), rooted at the project's package.json — not the CLI's + // own dependency tree. Fall back to a CLI-relative import only for the + // built-in providers that ARE the CLI's deps (e.g. @mieweb/deploy-wrangler). + const parentUrl = pathToFileURL(join(config.root, 'package.json')).href; + try { + importable = import.meta.resolve(specifier, parentUrl); + } catch { + try { + // Secondary attempt via CJS resolver rooted at the project (covers + // packages exposing only a `require`/`main` entry). + const requireFromProject = createRequire(parentUrl); + importable = pathToFileURL(requireFromProject.resolve(specifier)).href; + } catch { + importable = specifier; // last resort: CLI-relative (built-ins) + } + } + } + + /** @type {DeployProviderModule} */ + const mod = await import(importable); + const provider = toProvider(mod, specifier); + + if (!provider.supports(config.target)) { + throw new Error( + `provider "${provider.name}" (${specifier}) does not support target "${config.target}".`, + ); + } + return provider; +} + +/** + * Keys under `targets[t]` that hold **data-plane** configuration for the local + * host harness / runtime adapters — NOT deploy-provider config. These can carry + * driver secrets (`secretAccessKey`, `authToken`, …) and must never be handed to + * a control-plane deploy provider, which the contract documents as receiving + * only non-secret config. See packages/cli/mieweb-config.schema.json. + */ +const DATA_PLANE_KEYS = new Set(['bindings']); + +/** + * Matches key names that look secret-bearing. Because the config schema allows + * arbitrary `targets.` properties, a denylist of one bag (`bindings`) is not + * enough: a custom `providerToken`/`password`/`accessKey` at the target level + * would otherwise reach the provider. We drop any key whose name matches this, + * as defense-in-depth on top of the `bindings` removal. Deploy credentials are + * meant to come from the environment (`createProvider(env)`), never config. + */ +const SECRETISH_KEY = /(secret|token|password|passwd|credential|apikey|api_key|accesskey|access_key|privatekey|private_key|auth)/i; + +/** + * Recursively strip secret-bearing values from an arbitrary config value: + * - drops the data-plane `bindings` bag entirely, + * - drops any object key whose *name* looks secret-bearing ({@link SECRETISH_KEY}), + * - recurses into nested objects/arrays so a secret nested inside an otherwise + * non-secret setting (e.g. `registry.password`) is also removed. + * Non-secret scalars/objects pass through. This upholds the contract's + * guarantee that provider context is non-secret; deploy credentials come from + * the environment via `createProvider(env)`. + * + * @param {unknown} value + * @returns {unknown} + */ +function redactSecrets(value) { + if (Array.isArray(value)) return value.map(redactSecrets); + if (value && typeof value === 'object') { + /** @type {Record} */ + const out = {}; + for (const [k, v] of Object.entries(value)) { + if (DATA_PLANE_KEYS.has(k) || SECRETISH_KEY.test(k)) continue; + out[k] = redactSecrets(v); + } + return out; + } + return value; +} + +/** + * Project the raw per-target config down to what a deploy provider legitimately + * needs: recursively redacted of the data-plane bags and any secret-bearing key, + * keeping the open-ended non-secret config (provider name, instance URL, tuning, + * …). Deploy credentials come from the environment via `createProvider(env)`. + * + * @param {Record} [targetConfig] + * @returns {Record} + */ +function sanitizeTargetConfig(targetConfig) { + return /** @type {Record} */ (redactSecrets(targetConfig ?? {})); +} + +/** + * Non-secret top-level keys of `mieweb.jsonc` that are safe to expose to a + * deploy provider. Allowlisted (not denylisted): the config schema permits + * additional top-level properties, so an unknown key like `apiToken` must be + * dropped rather than passed through. Deploy credentials live in the environment + * (`createProvider(env)`), never in this context. + */ +const MIEWEB_PUBLIC_KEYS = new Set(['target', 'targets', 'wrangler']); + +/** + * Project the whole parsed `mieweb.jsonc` (`config.raw`) down to a non-secret + * view for `DeployContext.mieweb`. Only allowlisted top-level keys survive, and + * each `targets[*]` has its data-plane `bindings` bag (driver secrets) stripped — + * otherwise sanitizing `targetConfig` alone would still leak secrets here. + * + * @param {Record} [raw] + * @returns {Record} + */ +function sanitizeMieweb(raw) { + /** @type {Record} */ + const out = {}; + for (const [k, v] of Object.entries(raw ?? {})) { + if (!MIEWEB_PUBLIC_KEYS.has(k)) continue; // drop unknown/secret top-level keys + if (k !== 'targets') { + out[k] = v; + continue; + } + // Scrub each target's data-plane bags. + /** @type {Record} */ + const targets = {}; + for (const [t, cfg] of Object.entries(v ?? {})) { + targets[t] = + cfg && typeof cfg === 'object' ? sanitizeTargetConfig(/** @type {any} */ (cfg)) : cfg; + } + out.targets = targets; + } + return out; +} + +/** + * Decide the `manifestPath` to advertise on the context. + * - Explicitly configured (`mieweb.jsonc` → `wrangler` is a string): always + * pass it through, even if missing, so wrangler surfaces the error instead + * of silently discovering a different default. + * - Implicit default: pass it only when it exists; otherwise leave undefined + * so wrangler's own discovery runs. + * @param {MiewebConfig} config + * @returns {string|undefined} + */ +function resolveManifestPath(config) { + const explicit = typeof config.raw?.wrangler === 'string'; + if (explicit) return config.wranglerPath || undefined; + return config.wranglerPath && existsSync(config.wranglerPath) ? config.wranglerPath : undefined; +} + +/** + * Build the neutral {@link DeployContext} from the CLI's config + this + * invocation's passthrough args + an abort signal wired to process interrupts. + * + * @param {MiewebConfig} config + * @param {string[]} argv passthrough args (verb already removed) + * @returns {{ context: DeployContext, dispose: () => void }} + */ +export function buildContext(config, argv) { + const controller = new AbortController(); + const onSignal = () => controller.abort(); + process.once('SIGINT', onSignal); + process.once('SIGTERM', onSignal); + + /** @type {DeployContext} */ + const context = { + root: config.root, + target: /** @type {any} */ (config.target), + manifest: /** @type {Record} */ (config.wrangler), + // Advertise a manifest path when the user explicitly configured one (via + // `mieweb.jsonc` → `wrangler`), even if missing, so wrangler reports it + // rather than silently discovering a different default. For the IMPLICIT + // default path, only advertise it when the file actually exists — otherwise + // forwarding `--config ` would defeat wrangler's discovery. + manifestPath: resolveManifestPath(config), + mieweb: sanitizeMieweb(config.raw), + targetConfig: sanitizeTargetConfig(config.targetConfig), + argv, + logger: { + info: (m) => process.stderr.write(`[mieweb] ${m}\n`), + warn: (m) => process.stderr.write(`[mieweb] WARN ${m}\n`), + error: (m) => process.stderr.write(`[mieweb] ERROR ${m}\n`), + }, + signal: controller.signal, + }; + + const dispose = () => { + process.removeListener('SIGINT', onSignal); + process.removeListener('SIGTERM', onSignal); + }; + return { context, dispose }; +} + +/** + * Drive a single deploy-contract verb through the resolved provider. + * + * Handles the provider verbs the CLI surfaces (`deploy`, `dev`, `tail`, + * `login`, `logout`, `whoami`, `destroy`), mapping optional/absent verbs to a + * clear "unsupported by provider" message — the control-plane analogue of + * `UnsupportedBindingError`. {@link AuthError} is caught specially so the user + * gets an actionable hint (e.g. "run `mieweb login`") instead of a raw failure. + * Returns a process exit code. + * + * @param {'deploy'|'dev'|'tail'|'login'|'logout'|'whoami'|'destroy'} verb + * @param {DeployProvider} provider + * @param {MiewebConfig} config + * @param {string[]} argv passthrough args (verb already removed) + * @returns {Promise} + */ +export async function runProviderVerb(verb, provider, config, argv) { + const { context, dispose } = buildContext(config, argv); + try { + if (verb === 'deploy') { + const result = await provider.deploy(context); + reportDeploy(result); + return 0; + } + + if (verb === 'dev') { + if (!provider.dev) return unsupported(provider, 'dev'); + const handle = await provider.dev(context); + if (handle.url) context.logger.info(`dev server: ${handle.url}`); + + // Two ways this ends: (a) the user interrupts (signal aborts) → we stop the + // handle; (b) the dev process exits on its own → `handle.closed` settles. + // Race them so a crashed/finished dev returns instead of hanging forever. + // Guard the already-aborted case: an aborted signal won't re-emit 'abort' + // to a listener added afterwards, so check up front. + const interrupted = new Promise((res) => { + if (context.signal.aborted) return res('interrupt'); + context.signal.addEventListener('abort', () => res('interrupt'), { once: true }); + }); + const closed = handle.closed + ? handle.closed.then(() => 'closed', (err) => { throw err; }) + : new Promise(() => {}); // provider can't self-exit → only interrupt ends it + + try { + const how = await Promise.race([interrupted, closed]); + if (how === 'interrupt') await handle.stop(); + return 0; + } catch (err) { + // dev process ended abnormally (non-zero/killed): stop the handle, then + // let AuthError propagate to the outer handler so it gets the login-hint + // treatment (parity with deploy/tail/login). Other errors are reported here. + await handle.stop(); + if (/** @type {any} */ (err)?.name === 'AuthError') throw err; + context.logger.error(err instanceof Error ? err.message : String(err)); + return 1; + } + } + + if (verb === 'tail') { + if (!provider.tail) return unsupported(provider, 'tail'); + await provider.tail(context); + return 0; + } + + if (verb === 'destroy') { + if (!provider.destroy) return unsupported(provider, 'destroy'); + await provider.destroy(context); + context.logger.info(`destroyed via provider "${provider.name}".`); + return 0; + } + + if (verb === 'login') { + if (!provider.login) { + context.logger.info( + `provider "${provider.name}" takes credentials from the environment; ` + + 'there is nothing to log into. Set the appropriate credentials in your shell.', + ); + return 0; + } + await provider.login(context); + context.logger.info(`logged in via provider "${provider.name}".`); + return 0; + } + + if (verb === 'logout') { + if (!provider.logout) { + context.logger.info( + `provider "${provider.name}" has no stored session to clear ` + + '(credentials come from the environment).', + ); + return 0; + } + await provider.logout(context); + context.logger.info(`logged out of provider "${provider.name}".`); + return 0; + } + + if (verb === 'whoami') { + if (!provider.whoami) { + context.logger.info( + `provider "${provider.name}" does not report auth status ` + + '(it self-manages credentials via the environment).', + ); + return 0; + } + const status = await provider.whoami(context); + if (status.authenticated) { + const who = status.account ? ` as ${status.account}` : ''; + const how = status.method ? ` (${status.method})` : ''; + process.stdout.write(`Authenticated${who}${how} — provider "${provider.name}".\n`); + return 0; + } + process.stdout.write(`Not authenticated — provider "${provider.name}".\n`); + return 1; + } + + return unsupported(provider, verb); + } catch (err) { + // AuthError gets a friendlier, actionable message than a generic failure. + // Its `message` already renders any provider-supplied hint, so we only add + // the CLI's fallback "run login" nudge when the provider gave none. + const authErr = /** @type {{ name?: string, hint?: string, message?: string }} */ (err); + if (authErr && authErr.name === 'AuthError') { + const nudge = authErr.hint ? '' : ` Run \`mieweb login --target ${config.target}\`.`; + context.logger.error(`${authErr.message}${nudge}`); + return 1; + } + context.logger.error(err instanceof Error ? err.message : String(err)); + return 1; + } finally { + dispose(); + } +} + +/** + * Report a deploy result to the user, including any resource ids the provider + * wants persisted back into the committed config. + * @param {import('@mieweb/deploy-contract').DeployResult} result + */ +function reportDeploy(result) { + if (result.url) process.stdout.write(`Deployed: ${result.url}\n`); + const withIds = result.resources.filter((r) => r.id); + if (withIds.length) { + process.stdout.write('Resources:\n'); + for (const r of withIds) { + process.stdout.write(` ${r.binding} (${r.kind}): ${r.id}\n`); + } + } +} + +/** + * @param {DeployProvider} provider + * @param {string} verb + * @returns {number} + */ +function unsupported(provider, verb) { + process.stderr.write( + `[mieweb] provider "${provider.name}" does not implement "${verb}".\n`, + ); + return 1; +} From 2bd2f789cb177168a3d18941a3bf12263eebfe64 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Fri, 18 Sep 2026 12:59:14 -0400 Subject: [PATCH 4/6] docs(deploy): document the provider layer + changeset - README + packages/cli/README: describe the deploy-provider layer and add @mieweb/deploy-contract / @mieweb/deploy-wrangler to the package inventory. Clarify that on Cloudflare the deploy lifecycle verbs run through the wrangler reference provider (config injection, structured logging, resource reporting, auth-aware errors) rather than a verbatim passthrough, and that the host-harness description applies to the built-in local/mieweb targets. - mieweb-config.schema.json: allow custom `target` names (examples, not a closed enum) and document `targets[t].provider`. - Add a changeset (minor bumps for the three published packages). - Lockfile: wrangler recorded as a devDependency of deploy-wrangler (kept as an optional peer for consumers), so the importer matches the manifest. --- .changeset/deploy-provider-contract.md | 40 + README.md | 32 +- packages/cli/README.md | 12 +- packages/cli/mieweb-config.schema.json | 5 +- pnpm-lock.yaml | 1491 +++++++++++++++++------- 5 files changed, 1171 insertions(+), 409 deletions(-) create mode 100644 .changeset/deploy-provider-contract.md diff --git a/.changeset/deploy-provider-contract.md b/.changeset/deploy-provider-contract.md new file mode 100644 index 0000000..757fa6a --- /dev/null +++ b/.changeset/deploy-provider-contract.md @@ -0,0 +1,40 @@ +--- +"@mieweb/deploy-contract": minor +"@mieweb/deploy-wrangler": minor +"@mieweb/cli": minor +--- + +Introduce the deploy-provider contract and a Cloudflare reference provider. + +- `@mieweb/deploy-contract` (new): a minimal, provider-agnostic `DeployProvider` + TypeScript interface plus a conformance test-kit. The CLI consumes it; deploy + backends implement it. No wrangler.jsonc field names, backend API shapes, or + resource-URI grammar leak into the contract — those stay provider details. + Includes an auth surface: optional `login`/`logout`/`whoami` verbs, an + `AuthStatus` type, and an `AuthError` (the control-plane analogue of + `UnsupportedBindingError`) providers throw on backend 401/403 so the CLI can + prompt the user to log in. Credentials never travel through the contract — a + provider reads them from the environment via `createProvider(env)`, and + `targetConfig` is documented as non-secret. Runtime values (`AuthError`, + `RESOURCE_KINDS`) ship as plain ESM so bare-`node` providers can import them + without a TypeScript loader; declarations use `.d.mts` and the factory env + type is a dependency-free record (no `@types/node` required). Also exports a + shared string-aware `./jsonc` parser used by the CLI and providers. +- `@mieweb/deploy-wrangler` (new): the Cloudflare **reference** provider. Wraps + the pinned `wrangler` binary (`deploy`/`dev`/`tail`, plus + `login`/`logout`/`whoami` mapped to their wrangler equivalents) and reads + resource handles back from the manifest — reloading `wrangler.jsonc` after a + deploy so auto-provisioned, written-back ids are surfaced. Deploy failures that + are actually auth failures map to `AuthError`; a signal-killed child is treated + as failure; `dev` exposes a `closed` promise so a crashed dev returns instead + of hanging. `wrangler` is an optional peer dependency (the `MIEWEB_REAL_WRANGLER` + escape hatch also satisfies it). It is the canonical implementation other + providers (opensource-server, future AWS/GCP) are measured against via the + test-kit. +- `@mieweb/cli`: `deploy`/`dev`/`tail`/`login`/`logout`/`whoami`/`destroy` now + route through a resolved `DeployProvider`. Cloudflare resolves to the wrangler + reference provider; other targets can name a provider package in + `mieweb.jsonc` (`targets[t].provider`). Targets without a provider fall + through to the existing behavior unchanged. Provider context is recursively + redacted of secrets before it reaches a provider, and `AuthError` is surfaced + with an actionable "run `mieweb login`" hint. diff --git a/README.md b/README.md index 9d8c57a..6c7a4d8 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,15 @@ organizing principle: ## Packages -Three packages, split by *what a consumer must install*, not by module: +Packages, split by *what a consumer must install*, not by module: | Package | Role | | ------- | ---- | | [`@mieweb/cloud`](packages/cloud) | **Zero dependencies.** The portable contracts (`CloudDatabase`, `CloudBucket`, `CloudKV`, `CloudQueue`, `CloudStatefulNamespace`, `CloudVectorIndex`, `CloudAI`, `CloudContainerNamespace`, `UnsupportedBindingError`) and, at `@mieweb/cloud/workers`, the `DurableObject` base behind the **`mieweb:workers`** import — re-exports `cloudflare:workers` on Cloudflare (workerd export condition), pure-JS base everywhere else. This is the only package a Cloudflare app touches. | | [`@mieweb/cloud-adapters`](packages/cloud-adapters) | Off-Cloudflare **adapters** + the Node host harness and migration runner. `./local`: D1→SQLite, R2→filesystem, KV→in-memory, Queues→in-process, Durable Objects→in-process, Vectorize→sqlite-vec. `./os` (os.mieweb.org / self-hosted): D1→libSQL, Vectorize→libSQL vectors, R2→S3/MinIO, KV+Queues→Valkey; ships a `docker-compose.yml`. Backend SDKs are **optional peers** — install only what your target needs. | -| [`@mieweb/cli`](packages/cli) | The **`mieweb`** CLI. On the `cloudflare` target it delegates verbatim to `wrangler`; on the `local`/`mieweb` targets it runs the matching adapter via the Node host harness. | +| [`@mieweb/cli`](packages/cli) | The **`mieweb`** CLI. On the `cloudflare` target most commands delegate to `wrangler`; the deploy lifecycle verbs (`deploy`/`dev`/`tail`, plus `login`/`logout`/`whoami`/`destroy`) run through a deploy provider (below). On the `local`/`mieweb` targets it runs the matching adapter via the Node host harness. | +| [`@mieweb/deploy-contract`](packages/deploy-contract) | **Zero dependencies.** The provider-agnostic control-plane contract: the `DeployProvider` interface (+ `AuthError`, `RESOURCE_KINDS`), a `./jsonc` parser, and a `./testkit` conformance suite. The CLI consumes it; deploy backends implement it. | +| [`@mieweb/deploy-wrangler`](packages/deploy-wrangler) | The **Cloudflare reference** `DeployProvider` — wraps the `wrangler` binary (an optional peer). The canonical implementation other providers (opensource-server, future AWS/GCP) are measured against via the test-kit. | | [`@mieweb/test-app`](packages/test-app) *(private)* | A tiny worker that exercises **every** contract surface over plain HTTP, plus a cross-target runner. The same worker + the same assertions prove the layer on `cloudflare`, `local`, and `mieweb`. See [Try it](#try-it-the-test-app). | ## How a consuming app wires it in @@ -47,16 +49,23 @@ Three packages, split by *what a consumer must install*, not by module: ## Using the `mieweb` CLI What the CLI *is* depends on where you point it. **If you're targeting -Cloudflare (or already know `wrangler`), think of it as a thin pass-through:** -every command is forwarded verbatim to `wrangler`, so there's nothing new to -learn and zero overhead. **On the other targets it is not a wrapper** — there is -no `wrangler` underneath; the CLI runs your unchanged worker on a Node host -harness backed by the adapters, reusing your `wrangler.jsonc` purely as -configuration. The active target comes from `--target `, `MIEWEB_TARGET`, or -the `target` field in `mieweb.jsonc` (default `cloudflare`). +Cloudflare (or already know `wrangler`), think of it as a near pass-through:** +most commands are forwarded verbatim to `wrangler`, and the deploy lifecycle +verbs (`deploy`/`dev`/`tail`, plus `login`/`logout`/`whoami`/`destroy`) run +through a pluggable deploy provider whose Cloudflare reference implementation +still drives `wrangler` underneath — so behavior matches `wrangler` with a thin +layer of structured logging, resource reporting, and auth-aware errors on top. +**On the built-in `local`/`mieweb` targets it is not a wrapper** — there is no +`wrangler` underneath; the CLI runs your unchanged worker on a Node host harness +backed by the adapters, reusing your `wrangler.jsonc` purely as configuration. +(A custom target that sets `targets[t].provider` instead routes the provider +verbs through that provider, not the host harness.) The active target comes from +`--target `, `MIEWEB_TARGET`, or the `target` field in `mieweb.jsonc` +(default `cloudflare`). ```sh -# cloudflare (default): every command is forwarded verbatim to wrangler +# cloudflare (default): commands run via wrangler (deploy/dev/tail through the +# wrangler reference deploy provider, others forwarded verbatim) mieweb dev mieweb deploy mieweb d1 migrations apply @@ -69,7 +78,8 @@ mieweb --target local dev mieweb --target mieweb dev ``` -On `cloudflare`, behavior is identical to `wrangler` (zero overhead). On the +On `cloudflare`, behavior tracks `wrangler` (the deploy provider drives it +directly). On the `local`/`mieweb` targets the CLI imports the worker your `wrangler.jsonc` `main` points at, builds an `Env` from the `mieweb.jsonc` driver hints, and serves `fetch`/`queue`/`scheduled` over HTTP — the same handler Cloudflare runs. diff --git a/packages/cli/README.md b/packages/cli/README.md index 1fbb24f..eee5ab3 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,9 +1,15 @@ # `@mieweb/cli` — the `mieweb` command Target-aware wrapper over `wrangler`. On the `cloudflare` target (default) -every command is forwarded verbatim to `wrangler`; on `local`/`mieweb` the CLI -runs your unchanged worker on the Node host harness backed by the matching -adapters. See the [root README](../../README.md) for the full model. +most commands are forwarded verbatim to `wrangler`; the deploy lifecycle verbs +(`deploy`, `dev`, `tail`, plus `login`/`logout`/`whoami`, and `destroy`) go +through a pluggable **deploy provider** (`@mieweb/deploy-contract`) whose +Cloudflare reference implementation still delegates to `wrangler` — adding +structured logging, resource reporting, and auth-aware error handling around it. +On the built-in `local`/`mieweb` targets the CLI runs your unchanged worker on +the Node host harness backed by the matching adapters (a custom target with +`targets[t].provider` routes provider verbs through that provider instead). See +the [root README](../../README.md) for the full model. ```sh mieweb [--target ] [...args] diff --git a/packages/cli/mieweb-config.schema.json b/packages/cli/mieweb-config.schema.json index 820aafc..2775308 100644 --- a/packages/cli/mieweb-config.schema.json +++ b/packages/cli/mieweb-config.schema.json @@ -11,8 +11,8 @@ }, "target": { "type": "string", - "enum": ["cloudflare", "local", "mieweb", "aws", "gcp"], - "description": "Default deployment target when none is given via --target or MIEWEB_TARGET." + "examples": ["cloudflare", "local", "mieweb", "aws", "gcp"], + "description": "Default deployment target when none is given via --target or MIEWEB_TARGET. The built-ins are cloudflare/local/mieweb (aws/gcp reserved); custom deploy providers may introduce their own target names (see targets..provider), so any string is accepted." }, "targets": { "type": "object", @@ -21,6 +21,7 @@ "type": "object", "properties": { "runtime": { "type": "string" }, + "provider": { "type": "string", "description": "Deploy provider for this target: a package name or path to a module exporting a DeployProvider (@mieweb/deploy-contract). Cloudflare defaults to the wrangler reference provider; a custom provider may serve a custom target name." }, "port": { "type": "number" }, "registry": { "type": "object", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 104dba3..64aa549 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,16 +10,16 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.27.0 - version: 2.31.0(@types/node@22.19.20) + version: 2.31.1(@types/node@22.20.3) '@cloudflare/workers-types': specifier: ^4.20240620.0 - version: 4.20260607.1 + version: 4.20260702.1 '@mieweb/cli': specifier: workspace:* version: link:packages/cli '@types/node': specifier: ^22.0.0 - version: 22.19.20 + version: 22.20.3 typescript: specifier: ^5.6.0 version: 5.9.3 @@ -29,6 +29,12 @@ importers: '@mieweb/cloud-adapters': specifier: workspace:* version: link:../cloud-adapters + '@mieweb/deploy-contract': + specifier: workspace:* + version: link:../deploy-contract + '@mieweb/deploy-wrangler': + specifier: workspace:* + version: link:../deploy-wrangler packages/cloud: {} @@ -36,11 +42,11 @@ importers: dependencies: '@hono/node-server': specifier: ^2.0.10 - version: 2.1.1(hono@4.12.23) + version: 2.1.1(hono@4.13.8) devDependencies: '@aws-sdk/client-s3': specifier: ^3.600.0 - version: 3.1063.0 + version: 3.1135.0 '@libsql/client': specifier: ^0.14.0 version: 0.14.0 @@ -54,6 +60,18 @@ importers: specifier: ^0.1.6 version: 0.1.9 + packages/deploy-contract: {} + + packages/deploy-wrangler: + dependencies: + '@mieweb/deploy-contract': + specifier: workspace:* + version: link:../deploy-contract + devDependencies: + wrangler: + specifier: '>=3' + version: 4.135.0(@cloudflare/workers-types@4.20260702.1)(@types/node@22.20.3) + packages/test-app: dependencies: '@mieweb/cli': @@ -65,7 +83,7 @@ importers: optionalDependencies: '@aws-sdk/client-s3': specifier: ^3.600.0 - version: 3.1063.0 + version: 3.1135.0 '@libsql/client': specifier: ^0.14.0 version: 0.14.0 @@ -81,107 +99,76 @@ importers: packages: - '@aws-crypto/crc32@5.2.0': - resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/crc32c@5.2.0': - resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} - - '@aws-crypto/sha1-browser@5.2.0': - resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/checksums@3.1000.2': - resolution: {integrity: sha512-PIha+kauTbp6IRmOpYktPTrlfrrSqDVixvhO/EUOFOf62DPX81CaJoHJreuA1m9HYpSKyXf99BKjU1dvJPeUfw==} + '@aws-sdk/checksums@3.1001.0': + resolution: {integrity: sha512-6uTniZc87q+B5eXouGTl+7Tmc482rEeCcvxpsvREP8EfF0gvloRZ41UOA9sbSJlyy8TbqIBXb3kKfKarEArUQA==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-s3@3.1063.0': - resolution: {integrity: sha512-ETn+vvmZVK1MmOZwVBXmWANpmD5iTbzojIqyEIoZ86qo+8oWy35S8QyQNE/ZDI+WHgMU1dS+VSYbpRl1QkEySg==} + '@aws-sdk/client-s3@3.1135.0': + resolution: {integrity: sha512-/1CVA0M+TaEOzgM02DUST1NzQTjbY8OumDKPETjzo4owypkZZMxu1ADecFbzeG73hje/XLNAJfGepQ3a40CGnw==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.974.18': - resolution: {integrity: sha512-JDYCPI0j7zGrzXTDFsLB346cxss7J/AxH7+O0MzWlqppJBEyB9Qe6TQXRL6iwLUo/xZkNv9KFmBL2hqElmwW0g==} + '@aws-sdk/core@3.978.0': + resolution: {integrity: sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.44': - resolution: {integrity: sha512-3hKJVrZ7bqXzDAXCQp+OaQ1ASN+vWstaNuEH418wQVl//cRZhqhfR9Bjk1qIWmgUGe8/D3gdO73PgidRj378EQ==} + '@aws-sdk/credential-provider-env@3.972.71': + resolution: {integrity: sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.46': - resolution: {integrity: sha512-VhwC9pGAZHhiQ2xSViyOPDFqvr9aRxGCAXZtADsUhU3R65nad7y//CwynE6mQnWNR+suRlqE79W36IVayL+m1g==} + '@aws-sdk/credential-provider-http@3.972.73': + resolution: {integrity: sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.50': - resolution: {integrity: sha512-09Xi6ovxiK42+De/qBGF71sT5F2bWgYM+1fFyDwSOpy1xpsQ5R/naIu7MVDpH6Dic36QNc8dAv4KADtMGK2JYg==} + '@aws-sdk/credential-provider-ini@3.973.16': + resolution: {integrity: sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.49': - resolution: {integrity: sha512-EfJF/1Fh9mI4pZyoheU2RY9xUhTcugIZNkD63+orXMkYj/QXacJNbKVDUK90Yv5hE+aX+rt9J/EZ9Qr3vKOa7g==} + '@aws-sdk/credential-provider-login@3.972.78': + resolution: {integrity: sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.52': - resolution: {integrity: sha512-7QX+PbyiWBEOVipJq8Nke/TqXT6lAPLE7fvTaopa39/IVWuLfS+Fzdy71sZJONf/mLGgmtj6aU17+REw3+aRrw==} + '@aws-sdk/credential-provider-node@3.972.83': + resolution: {integrity: sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.44': - resolution: {integrity: sha512-V+UUhZpRP7QDRhi+qgBDisM9tUBnYmMje8Bk77A6MZsfeGeGdMsQXmaHP1CDYFcept0o/Rz5g2Y0TMeVlG9dzg==} + '@aws-sdk/credential-provider-process@3.972.71': + resolution: {integrity: sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.49': - resolution: {integrity: sha512-9QqOYGuh5tZ76OzaT68kwI78AH+5lS/uZGGvkfxb3fc8FzRrIz2jOufNTliEBEeSAwmgK2rWLNsK+IB3zbtNPA==} + '@aws-sdk/credential-provider-sso@3.973.15': + resolution: {integrity: sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.49': - resolution: {integrity: sha512-IYx1lN38MnnPXv+NBLpuATu0cZakbZ321TAfjW+aVkw7HIJF38YnEwdeEO55MSl3pl7hIX1IvvnD6EmnAzmAJw==} + '@aws-sdk/credential-provider-web-identity@3.972.77': + resolution: {integrity: sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.974.27': - resolution: {integrity: sha512-bZqezPLdllFC4VAeV/f+EIc/hz56ab3TD/+4zNCgOgmG5ZHAE5dMHrX1gtTwdcQXbPr3KR7x3zTC3zuCTE6+ng==} + '@aws-sdk/middleware-sdk-s3@3.972.76': + resolution: {integrity: sha512-NfnTkVUTBKTBuBgqaapFK9r3YdkKt1b2oRvgLzZq91bwNKh6ZS0S7sEcheguttREaL4iyfs/xQnqD7Z7AsWSsA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.48': - resolution: {integrity: sha512-MRTqx8wD/T3REt6LTT3/yN8rrp6+xIHrbUekkDYJTYWVch70mwtdJBovR4qKJz1jIPlbN+9R/Sn6R04BfsglzA==} + '@aws-sdk/nested-clients@3.997.45': + resolution: {integrity: sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.997.17': - resolution: {integrity: sha512-lDRgraoTfKRawUyc176Ow93mrNrOho/x+EoK4C+lKU+vKkHWhNhzvSMVAx0WEJUJoeQxxDN5ZdKMfiGEyNejig==} + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.32': - resolution: {integrity: sha512-llvApLcsWtmRFhG2wT3WIp1CmDeRaIYutqty1ZZXoMzK7TiJ6MOLOimk9eXUS8PwgG4ew4pa4QAbt0lfhn++1w==} + '@aws-sdk/token-providers@3.1129.0': + resolution: {integrity: sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1063.0': - resolution: {integrity: sha512-nYDaWWdzjKiDP5xj8k4oUgcYd4WPgzfAOgdU5vJsaqH/07Dfvm7ffisHCFJ+NEl7kUC9JEIUxh0kznvenbo3NQ==} + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.11': - resolution: {integrity: sha512-YjS0qFuECClRh4qhEyW8XagW0fwEPBeZ1cfsW/gU73Kh/ExFILxbzxOfPCmzF/2DwEvhvsHYt0b0qnvStwKYrg==} + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.965.6': - resolution: {integrity: sha512-ZfHjfwSzeXj+Lg9AK5ZNmeDkXev6V+w2tn1t4kgDdRtUaRCthepTQiFwbD06EF9oNGH4LaLg+Mb6U16Ypv5bSw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/xml-builder@3.972.28': - resolution: {integrity: sha512-lI/l3c/vPvsxmspzV63NfS3x9q4CkMmdhJy4QiM+NThAufVkDvi/PZZQ6xETnICL0UD7jI808pY83gllf86RFg==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.2.4': - resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} '@babel/runtime@7.29.7': @@ -197,8 +184,8 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.31.0': - resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} hasBin: true '@changesets/config@3.1.4': @@ -243,8 +230,214 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cloudflare/workers-types@4.20260607.1': - resolution: {integrity: sha512-TSiusluJ8+5esTMYwxGFuT1SNU/PRzPmt9VMsmAlzjIK0mhc24Zsc1bbGEVH5qyMZ8hrdRtrAPdt2+T8Vph2+Q==} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260918.1': + resolution: {integrity: sha512-H5Em6Wd0jjxaloYh2rp+WLBl2eWbkk7nSP1svGt6K1RSv/rNVqmKdpjZPJTBSavOmeYGa88qvtOWwS+33OHTqQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260918.1': + resolution: {integrity: sha512-CR9JRZEQo93fNgBVF4Df2H2/VYO4n7rxSseSwCVv3bJQEb0huADUSCj2FK4ipxar8ToOEB4wawyw0vJo5U/rQQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260918.1': + resolution: {integrity: sha512-UQ2nnY3qpXLzQ80frmWO+8HvtqyWaQILe8QYZwpemdjT+sqwCz4Dz+0/WVkFco0v/04kIIqikVeLTY/7gEhmkw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260918.1': + resolution: {integrity: sha512-4rib51MaLNWweUIUxM/Xj558M5QmyZoBSf0ffv+lYah5VTrvwnez3XGxX72pElnBKHROvAPOi5msQ5Ts8JSI0A==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260918.1': + resolution: {integrity: sha512-sATrMx5ShYYgmgUGrcTmvsFSJBFuN95NEkX3xwb1qk8w1A6h5N11sea7yN2IeibwPyPmXKjWNjXOno0hZAM71Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260702.1': + resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@hono/node-server@2.1.1': resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} @@ -252,6 +445,152 @@ packages: peerDependencies: hono: ^4 + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -264,6 +603,16 @@ packages: '@ioredis/commands@1.10.0': resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@libsql/client@0.14.0': resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} @@ -324,9 +673,6 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@nodable/entities@2.1.1': - resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -339,47 +685,51 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@smithy/core@3.24.6': - resolution: {integrity: sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==} - engines: {node: '>=18.0.0'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - '@smithy/credential-provider-imds@4.3.8': - resolution: {integrity: sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg==} - engines: {node: '>=18.0.0'} + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} - '@smithy/fetch-http-handler@5.4.6': - resolution: {integrity: sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==} + '@smithy/core@3.34.1': + resolution: {integrity: sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==} engines: {node: '>=18.0.0'} - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} + '@smithy/credential-provider-imds@4.5.2': + resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} + engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.7.7': - resolution: {integrity: sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A==} + '@smithy/fetch-http-handler@5.8.0': + resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.4.6': - resolution: {integrity: sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==} + '@smithy/node-http-handler@4.12.1': + resolution: {integrity: sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==} engines: {node: '>=18.0.0'} - '@smithy/types@4.14.3': - resolution: {integrity: sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==} + '@smithy/signature-v4@5.7.3': + resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} engines: {node: '>=18.0.0'} - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} + '@smithy/types@4.18.0': + resolution: {integrity: sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==} + engines: {node: '>=18.0.0'} - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.19.20': - resolution: {integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==} + '@types/node@22.20.3': + resolution: {integrity: sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==} '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -410,6 +760,9 @@ packages: resolution: {integrity: sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==} engines: {node: '>=22'} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -417,13 +770,17 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -453,6 +810,10 @@ packages: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -461,6 +822,14 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -473,15 +842,8 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-xml-builder@1.2.0: - resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - - fast-xml-parser@5.7.3: - resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} - hasBin: true - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} @@ -507,6 +869,11 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -518,16 +885,16 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hono@4.12.23: - resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + hono@4.13.8: + resolution: {integrity: sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==} engines: {node: '>=16.9.0'} - human-id@4.2.0: - resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} + human-id@4.2.1: + resolution: {integrity: sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==} hasBin: true - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} ignore@5.3.2: @@ -561,20 +928,24 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - js-base64@3.7.8: - resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-base64@3.9.3: + resolution: {integrity: sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + libsql@0.4.7: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} cpu: [x64, arm64, wasm32] @@ -595,6 +966,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + miniflare@5.20260918.0-alpha: + resolution: {integrity: sha512-vyIes7yW/OTzHtz4GhcBCTohZfqk2eQNiIkngZ07VRA5Qu24aNgW/TrFwlPkMLMjWDQ8R4JJBBHR/KX+liSDtg==} + engines: {node: '>=22.0.0'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -645,18 +1020,20 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -708,11 +1085,20 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@7.8.2: - resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -774,8 +1160,9 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strnum@2.3.0: - resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -796,6 +1183,13 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -809,6 +1203,21 @@ packages: engines: {node: '>= 8'} hasBin: true + workerd@1.20260918.1: + resolution: {integrity: sha512-NsjfQlBNQ0iEniv/STOy4zbp8s5k60PzL1Ter02Eg44arbDhHtf6UOs03E31XDRmmBFxSIkAZuCyld3RKH1wqA==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.135.0: + resolution: {integrity: sha512-WrNBQSfIG6YcILJcodYr5ty8vgkzGsV8YX+kfd+uZ5/Bd8cUW0GnUIRKAVfn5kYKqh1m/BPcji9h1d7mE8euFw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260918.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -821,241 +1230,190 @@ packages: utf-8-validate: optional: true - xml-naming@0.1.0: - resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} - engines: {node: '>=16.0.0'} - -snapshots: - - '@aws-crypto/crc32@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.11 - tslib: 2.8.1 - - '@aws-crypto/crc32c@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.11 - tslib: 2.8.1 - - '@aws-crypto/sha1-browser@5.2.0': - dependencies: - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.11 - '@aws-sdk/util-locate-window': 3.965.6 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.11 - '@aws-sdk/util-locate-window': 3.965.6 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + 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 - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.11 - tslib: 2.8.1 + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.973.11 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 +snapshots: - '@aws-sdk/checksums@3.1000.2': + '@aws-sdk/checksums@3.1001.0': dependencies: - '@aws-crypto/crc32': 5.2.0 - '@aws-crypto/crc32c': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.974.18 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1063.0': - dependencies: - '@aws-crypto/sha1-browser': 5.2.0 - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.18 - '@aws-sdk/credential-provider-node': 3.972.52 - '@aws-sdk/middleware-flexible-checksums': 3.974.27 - '@aws-sdk/middleware-sdk-s3': 3.972.48 - '@aws-sdk/signature-v4-multi-region': 3.996.32 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/fetch-http-handler': 5.4.6 - '@smithy/node-http-handler': 4.7.7 - '@smithy/types': 4.14.3 + '@aws-sdk/client-s3@3.1135.0': + dependencies: + '@aws-sdk/checksums': 3.1001.0 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/credential-provider-node': 3.972.83 + '@aws-sdk/middleware-sdk-s3': 3.972.76 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/core@3.974.18': + '@aws-sdk/core@3.978.0': dependencies: - '@aws-sdk/types': 3.973.11 - '@aws-sdk/xml-builder': 3.972.28 - '@aws/lambda-invoke-store': 0.2.4 - '@smithy/core': 3.24.6 - '@smithy/signature-v4': 5.4.6 - '@smithy/types': 4.14.3 + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.34.1 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.18.0 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.44': + '@aws-sdk/credential-provider-env@3.972.71': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.46': + '@aws-sdk/credential-provider-http@3.972.73': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/fetch-http-handler': 5.4.6 - '@smithy/node-http-handler': 4.7.7 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.50': - dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/credential-provider-env': 3.972.44 - '@aws-sdk/credential-provider-http': 3.972.46 - '@aws-sdk/credential-provider-login': 3.972.49 - '@aws-sdk/credential-provider-process': 3.972.44 - '@aws-sdk/credential-provider-sso': 3.972.49 - '@aws-sdk/credential-provider-web-identity': 3.972.49 - '@aws-sdk/nested-clients': 3.997.17 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/credential-provider-imds': 4.3.8 - '@smithy/types': 4.14.3 + '@aws-sdk/credential-provider-ini@3.973.16': + dependencies: + '@aws-sdk/core': 3.978.0 + '@aws-sdk/credential-provider-env': 3.972.71 + '@aws-sdk/credential-provider-http': 3.972.73 + '@aws-sdk/credential-provider-login': 3.972.78 + '@aws-sdk/credential-provider-process': 3.972.71 + '@aws-sdk/credential-provider-sso': 3.973.15 + '@aws-sdk/credential-provider-web-identity': 3.972.77 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-login@3.972.49': + '@aws-sdk/credential-provider-login@3.972.78': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/nested-clients': 3.997.17 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-node@3.972.52': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.44 - '@aws-sdk/credential-provider-http': 3.972.46 - '@aws-sdk/credential-provider-ini': 3.972.50 - '@aws-sdk/credential-provider-process': 3.972.44 - '@aws-sdk/credential-provider-sso': 3.972.49 - '@aws-sdk/credential-provider-web-identity': 3.972.49 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/credential-provider-imds': 4.3.8 - '@smithy/types': 4.14.3 + '@aws-sdk/credential-provider-node@3.972.83': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.71 + '@aws-sdk/credential-provider-http': 3.972.73 + '@aws-sdk/credential-provider-ini': 3.973.16 + '@aws-sdk/credential-provider-process': 3.972.71 + '@aws-sdk/credential-provider-sso': 3.973.15 + '@aws-sdk/credential-provider-web-identity': 3.972.77 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/credential-provider-imds': 4.5.2 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.972.44': + '@aws-sdk/credential-provider-process@3.972.71': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.49': + '@aws-sdk/credential-provider-sso@3.973.15': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/nested-clients': 3.997.17 - '@aws-sdk/token-providers': 3.1063.0 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/token-providers': 3.1129.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.49': + '@aws-sdk/credential-provider-web-identity@3.972.77': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/nested-clients': 3.997.17 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.974.27': + '@aws-sdk/middleware-sdk-s3@3.972.76': dependencies: - '@aws-sdk/checksums': 3.1000.2 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.48': + '@aws-sdk/nested-clients@3.997.45': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/signature-v4-multi-region': 3.996.32 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.997.17': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.18 - '@aws-sdk/signature-v4-multi-region': 3.996.32 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/fetch-http-handler': 5.4.6 - '@smithy/node-http-handler': 4.7.7 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.32': + '@aws-sdk/signature-v4-multi-region@3.996.46': dependencies: - '@aws-sdk/types': 3.973.11 - '@smithy/signature-v4': 5.4.6 - '@smithy/types': 4.14.3 + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.3 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1063.0': + '@aws-sdk/token-providers@3.1129.0': dependencies: - '@aws-sdk/core': 3.974.18 - '@aws-sdk/nested-clients': 3.997.17 - '@aws-sdk/types': 3.973.11 - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@aws-sdk/core': 3.978.0 + '@aws-sdk/nested-clients': 3.997.45 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/types@3.973.11': + '@aws-sdk/types@3.974.5': dependencies: - '@smithy/types': 4.14.3 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.965.6': + '@aws-sdk/xml-builder@3.972.40': dependencies: + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.28': - dependencies: - '@smithy/types': 4.14.3 - fast-xml-parser: 5.7.3 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.2.4': {} + '@aws/lambda-invoke-store@0.3.0': {} '@babel/runtime@7.29.7': {} @@ -1073,7 +1431,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.8.2 + semver: 7.8.5 '@changesets/assemble-release-plan@6.0.10': dependencies: @@ -1082,13 +1440,13 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.8.2 + semver: 7.8.5 '@changesets/changelog-git@0.2.1': dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@22.19.20)': + '@changesets/cli@2.31.1(@types/node@22.20.3)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -1104,7 +1462,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@22.19.20) + '@inquirer/external-editor': 1.0.3(@types/node@22.20.3) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -1113,7 +1471,7 @@ snapshots: package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.8.2 + semver: 7.8.5 spawndamnit: 3.0.1 term-size: 2.2.1 transitivePeerDependencies: @@ -1139,7 +1497,7 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.8.2 + semver: 7.8.5 '@changesets/get-release-plan@4.0.16': dependencies: @@ -1167,7 +1525,7 @@ snapshots: '@changesets/parse@0.4.3': dependencies: '@changesets/types': 6.1.0 - js-yaml: 4.2.0 + js-yaml: 4.3.2 '@changesets/pre@2.0.2': dependencies: @@ -1199,29 +1557,254 @@ snapshots: dependencies: '@changesets/types': 6.1.0 fs-extra: 7.0.1 - human-id: 4.2.0 + human-id: 4.2.1 prettier: 2.8.8 - '@cloudflare/workers-types@4.20260607.1': {} + '@cloudflare/kv-asset-handler@0.5.0': {} - '@hono/node-server@2.1.1(hono@4.12.23)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260918.1)': dependencies: - hono: 4.12.23 + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260918.1 + + '@cloudflare/workerd-darwin-64@1.20260918.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260918.1': + optional: true - '@inquirer/external-editor@1.0.3(@types/node@22.19.20)': + '@cloudflare/workerd-linux-64@1.20260918.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260918.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260918.1': + optional: true + + '@cloudflare/workers-types@4.20260702.1': {} + + '@cspotcode/source-map-support@0.8.1': dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@hono/node-server@2.1.1(hono@4.13.8)': + dependencies: + hono: 4.13.8 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@types/node': 22.19.20 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + optional: true + + '@img/sharp-darwin-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.3 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + optional: true + + '@img/sharp-linux-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.3 + optional: true + + '@img/sharp-linux-arm@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.3 + optional: true + + '@img/sharp-linux-ppc64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.3 + optional: true + + '@img/sharp-linux-riscv64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.3 + optional: true + + '@img/sharp-linux-s390x@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.3 + optional: true + + '@img/sharp-linux-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + optional: true + + '@img/sharp-wasm32@0.35.4': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-win32-arm64@0.35.4': + optional: true + + '@img/sharp-win32-ia32@0.35.4': + optional: true + + '@img/sharp-win32-x64@0.35.4': + optional: true + + '@inquirer/external-editor@1.0.3(@types/node@22.20.3)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.3 '@ioredis/commands@1.10.0': {} + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + '@libsql/client@0.14.0': dependencies: '@libsql/core': 0.14.0 '@libsql/hrana-client': 0.7.0 - js-base64: 3.7.8 + js-base64: 3.9.3 libsql: 0.4.7 promise-limit: 2.7.0 transitivePeerDependencies: @@ -1230,7 +1813,7 @@ snapshots: '@libsql/core@0.14.0': dependencies: - js-base64: 3.7.8 + js-base64: 3.9.3 '@libsql/darwin-arm64@0.4.7': optional: true @@ -1242,7 +1825,7 @@ snapshots: dependencies: '@libsql/isomorphic-fetch': 0.3.1 '@libsql/isomorphic-ws': 0.1.5 - js-base64: 3.7.8 + js-base64: 3.9.3 node-fetch: 3.3.2 transitivePeerDependencies: - bufferutil @@ -1253,7 +1836,7 @@ snapshots: '@libsql/isomorphic-ws@0.1.5': dependencies: '@types/ws': 8.18.1 - ws: 8.21.0 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -1291,8 +1874,6 @@ snapshots: '@neon-rs/load@0.0.4': {} - '@nodable/entities@2.1.1': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1303,65 +1884,66 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.20.3 - '@smithy/core@3.24.6': + '@poppinss/colors@4.1.6': dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.14.3 - tslib: 2.8.1 + kleur: 4.1.5 - '@smithy/credential-provider-imds@4.3.8': + '@poppinss/dumper@0.6.5': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 - tslib: 2.8.1 + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 - '@smithy/fetch-http-handler@5.4.6': - dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 - tslib: 2.8.1 + '@poppinss/exception@1.2.3': {} - '@smithy/is-array-buffer@2.2.0': + '@sindresorhus/is@7.2.0': {} + + '@smithy/core@3.34.1': dependencies: + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.7.7': + '@smithy/credential-provider-imds@4.5.2': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/signature-v4@5.4.6': + '@smithy/fetch-http-handler@5.8.0': dependencies: - '@smithy/core': 3.24.6 - '@smithy/types': 4.14.3 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/types@4.14.3': + '@smithy/node-http-handler@4.12.1': dependencies: + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/util-buffer-from@2.2.0': + '@smithy/signature-v4@5.7.3': dependencies: - '@smithy/is-array-buffer': 2.2.0 + '@smithy/core': 3.34.1 + '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/util-utf8@2.3.0': + '@smithy/types@4.18.0': dependencies: - '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 + '@speed-highlight/core@1.2.24': {} + '@types/node@12.20.55': {} - '@types/node@22.19.20': + '@types/node@22.20.3': dependencies: undici-types: 6.21.0 '@types/ws@8.18.1': dependencies: - '@types/node': 22.19.20 + '@types/node': 22.20.3 ansi-colors@4.1.3: {} @@ -1383,16 +1965,20 @@ snapshots: dependencies: node-addon-api: 8.9.2 + blake3-wasm@2.1.5: {} + bowser@2.14.1: {} braces@3.0.3: dependencies: fill-range: 7.1.1 - chardet@2.1.1: {} + chardet@2.2.0: {} cluster-key-slot@1.1.1: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1411,6 +1997,8 @@ snapshots: detect-libc@2.0.2: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -1420,6 +2008,37 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + error-stack-parser-es@1.0.5: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + esprima@4.0.1: {} extendable-error@0.1.7: {} @@ -1432,19 +2051,7 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-xml-builder@1.2.0: - dependencies: - path-expression-matcher: 1.5.0 - xml-naming: 0.1.0 - - fast-xml-parser@5.7.3: - dependencies: - '@nodable/entities': 2.1.1 - fast-xml-builder: 1.2.0 - path-expression-matcher: 1.5.0 - strnum: 2.3.0 - - fastq@1.20.1: + fastq@1.20.3: dependencies: reusify: 1.1.0 @@ -1478,6 +2085,9 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fsevents@2.3.3: + optional: true + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -1493,11 +2103,11 @@ snapshots: graceful-fs@4.2.11: {} - hono@4.12.23: {} + hono@4.13.8: {} - human-id@4.2.0: {} + human-id@4.2.1: {} - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -1531,14 +2141,14 @@ snapshots: isexe@2.0.0: {} - js-base64@3.7.8: {} + js-base64@3.9.3: {} - js-yaml@3.14.2: + js-yaml@3.15.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.2.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -1546,6 +2156,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + kleur@4.1.5: {} + libsql@0.4.7: dependencies: '@neon-rs/load': 0.0.4 @@ -1572,6 +2184,19 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + miniflare@5.20260918.0-alpha(@types/node@22.20.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.4(@types/node@22.20.3) + undici: 7.29.0 + workerd: 1.20260918.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + mri@1.2.0: {} ms@2.1.3: {} @@ -1610,12 +2235,14 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.5.0: {} - path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -1633,7 +2260,7 @@ snapshots: read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 - js-yaml: 3.14.2 + js-yaml: 3.15.2 pify: 4.0.1 strip-bom: 3.0.0 @@ -1653,7 +2280,40 @@ snapshots: safer-buffer@2.1.2: {} - semver@7.8.2: {} + semver@7.8.5: {} + + sharp@0.35.4(@types/node@22.20.3): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 22.20.3 shebang-command@2.0.0: dependencies: @@ -1703,7 +2363,7 @@ snapshots: strip-bom@3.0.0: {} - strnum@2.3.0: {} + supports-color@10.2.2: {} term-size@2.2.1: {} @@ -1717,6 +2377,12 @@ snapshots: undici-types@6.21.0: {} + undici@7.29.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + universalify@0.1.2: {} web-streams-polyfill@3.3.3: {} @@ -1725,6 +2391,45 @@ snapshots: dependencies: isexe: 2.0.0 + workerd@1.20260918.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260918.1 + '@cloudflare/workerd-darwin-arm64': 1.20260918.1 + '@cloudflare/workerd-linux-64': 1.20260918.1 + '@cloudflare/workerd-linux-arm64': 1.20260918.1 + '@cloudflare/workerd-windows-64': 1.20260918.1 + + wrangler@4.135.0(@cloudflare/workers-types@4.20260702.1)(@types/node@22.20.3): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260918.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 5.20260918.0-alpha(@types/node@22.20.3) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260918.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260702.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + ws@8.21.0: {} - xml-naming@0.1.0: {} + ws@8.21.3: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 From 6c14af12e622b54e295c38da4baa3d23cf73b075 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Wed, 30 Sep 2026 11:17:49 -0400 Subject: [PATCH 5/6] ci: publish PR preview packages to GitHub Packages --- .github/workflows/pr-preview.yml | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/pr-preview.yml diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..009a857 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,86 @@ +# PR preview packages. +# +# On every push to a same-repo pull request, publish every public @mieweb/* +# workspace package to the GitHub Packages npm registry (npm.pkg.github.com) +# as `-pr.` under the dist-tag `pr`. This lets +# cross-repo PRs depend on each other with ordinary semver references while +# they are in review, e.g. +# +# "@mieweb/deploy-contract": "0.2.1-pr14.3" +# +# plus, in the consumer, an .npmrc line: +# +# @mieweb:registry=https://npm.pkg.github.com +# +# Once the PRs merge, the real versions go to npmjs via release.yml and the +# consumers drop the .npmrc line and switch to the released version. +# +# Installing from GitHub Packages needs a token with `read:packages`, even for +# public packages. +name: PR preview packages + +on: + pull_request: + +concurrency: + group: pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +jobs: + publish: + # GITHUB_TOKEN is read-only on fork PRs. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + registry-url: https://npm.pkg.github.com + scope: '@mieweb' + + - run: pnpm install --frozen-lockfile + + - name: Stamp preview versions + id: stamp + env: + SUFFIX: pr${{ github.event.pull_request.number }}.${{ github.run_number }} + run: | + node -e ' + const fs = require("fs"), path = require("path"); + const out = []; + for (const dir of fs.readdirSync("packages")) { + const f = path.join("packages", dir, "package.json"); + if (!fs.existsSync(f)) continue; + const p = JSON.parse(fs.readFileSync(f, "utf8")); + if (p.private) continue; + p.version = `${p.version.replace(/-.*$/, "")}-${process.env.SUFFIX}`; + fs.writeFileSync(f, JSON.stringify(p, null, 2) + "\n"); + out.push(`${p.name}@${p.version}`); + } + fs.appendFileSync(process.env.GITHUB_OUTPUT, `packages=${out.join(" ")}\n`); + ' + + # pnpm rewrites workspace:* to the stamped versions and runs prepack. + - name: Publish to GitHub Packages + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: pnpm -r publish --no-git-checks --tag pr${{ github.event.pull_request.number }} + + - name: Summary + run: | + { + echo "### Preview packages (npm.pkg.github.com)" + echo + echo "Add \`@mieweb:registry=https://npm.pkg.github.com\` to .npmrc, then:" + echo + for p in ${{ steps.stamp.outputs.packages }}; do echo "- \`$p\`"; done + } >> "$GITHUB_STEP_SUMMARY" From 978abdcc137e1b22dcb3b2be035d596f7eaa5d28 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Thu, 1 Oct 2026 14:21:45 -0400 Subject: [PATCH 6/6] feat(deploy-contract): optional DeployContext.persistTargetConfig for writing back resolved settings --- packages/deploy-contract/src/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/deploy-contract/src/index.ts b/packages/deploy-contract/src/index.ts index 60a5853..68bcfe7 100644 --- a/packages/deploy-contract/src/index.ts +++ b/packages/deploy-contract/src/index.ts @@ -124,6 +124,17 @@ export interface DeployContext { * timeout fires; long-running providers should observe it and abort promptly. */ readonly signal: AbortSignal; + + /** + * Persist non-secret settings the provider resolved interactively (e.g. a + * site the user picked) into `mieweb.jsonc` → `targets[target]`, so the next + * run doesn't ask again. Values must be JSON-serializable; the host keeps + * comments/formatting and refuses secret-looking keys (credentials stay in the + * environment, see {@link targetConfig}). Resolves to whether anything was + * written. Optional: hosts without a writable config omit it, so providers + * must call it as `ctx.persistTargetConfig?.(patch)`. + */ + readonly persistTargetConfig?: (patch: Readonly>) => Promise; } /** Re-export of the canonical runtime kinds list (single source of truth). */