diff --git a/.changeset/quiet-workers-validate.md b/.changeset/quiet-workers-validate.md new file mode 100644 index 0000000..6b23606 --- /dev/null +++ b/.changeset/quiet-workers-validate.md @@ -0,0 +1,5 @@ +--- +"@offering-protocol/core": patch +--- + +Precompile bundled ODP validators so document validation works in runtimes that prohibit dynamic JavaScript code generation, including Cloudflare Workers. diff --git a/.gitignore b/.gitignore index 1f20161..62cdbf4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ dist/ coverage/ .conformance/ .turbo/ +.generated/ +.wrangler/ docs/ *.tsbuildinfo diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 00febbe..042a3ef 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -15,6 +15,17 @@ surface of every package. Scope an individual task with a Turbo filter when iter pnpm turbo run typecheck --filter=@offering-protocol/core ``` +## Core validators + +Core's `generate` command compiles the JSON Schemas in `packages/core/src/schemas` into +`packages/core/.generated`. Build, test, lint, typecheck, and TypeDoc commands run generation when +needed. Generated files are untracked and are bundled into the published JavaScript; consumers do +not run the generator. Edit the schemas or generation script, not the generated output. + +Core tests compare generated validators with Ajv and check that they preserve results and errors. +Publication checks load both ESM and CommonJS with string-based code generation disabled. The +Cloudflare example tests run the built Service in `workerd`, with startup code generation disabled. + ## Dependency updates Refresh dependencies throughout the workspace to the newest versions allowed by their declared diff --git a/README.md b/README.md index a03037f..6354e61 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,22 @@ pnpm add @offering-protocol/directory Use `npm install` or `yarn add` if those are the package managers in your application. +## Cloudflare Workers + +To host an ODP catalog on Workers, start with the +[Cloudflare Service example](./examples/odp-service-cloudflare/README.md). It includes a runnable +Worker, the tested compatibility settings, and local tests. You do not need a Cloudflare account +to run it locally. + +`core` ships precompiled validators: importing the package and validating ODP documents does not +generate JavaScript or download schemas. The Service package uses those validators, and the +Directory client uses the runtime's `fetch` API. + +The Agent package has additional requirements and does not have full Workers support. A +compatibility date alone does not solve its transport and runtime-schema limitations; see +[Agent on Workers](./packages/agent/README.md#cloudflare-workers). These limitations concern code +executing inside Workers, not a Node.js application behind Cloudflare's proxy or firewall. + ## Agent Workflow For general Directory discovery, use [`directory.search()`](./packages/directory/README.md#search-the-directory) diff --git a/examples/README.md b/examples/README.md index 1d35def..d594a66 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,6 +3,8 @@ The examples exercise the published package APIs rather than importing package internals. - `odp-service-small` demonstrates configuration-first integration for a small catalog. +- [`odp-service-cloudflare`](./odp-service-cloudflare/README.md) hosts a public catalog in a + Cloudflare Worker, with local tests and deployment instructions. - `odp-service-marketplace` demonstrates storage-style handlers and bounded virtual scale. - `odp-agent-discovery` builds an explicit mock directory from reachable configured Services and walks through their discovery responses. @@ -10,8 +12,8 @@ The examples exercise the published package APIs rather than importing package i payment. - `odp-service-x402` exposes an ODP Action protected by x402. -Each runnable package includes `.env.example`. Copy it to `.env` to make local configuration -explicit; `.env` remains untracked. +The Node server examples include `.env.example`. Copy it to `.env` to make local configuration +explicit; `.env` remains untracked. The Cloudflare example uses `wrangler.json` and needs no secrets. Build and exercise the complete flow with: diff --git a/examples/odp-service-cloudflare/README.md b/examples/odp-service-cloudflare/README.md new file mode 100644 index 0000000..4d05974 --- /dev/null +++ b/examples/odp-service-cloudflare/README.md @@ -0,0 +1,91 @@ +# ODP Service on Cloudflare Workers + +Publish an ODP catalog from a Cloudflare Worker using `@offering-protocol/service`. This example +offers one free incident-response template. It serves the Service Document, Offering list, Offering +details, and the advertised download Action. + +## Run locally + +You need Node.js 22 or newer and pnpm to run the development tools. No Cloudflare account is needed +for local testing. From the repository root: + +```sh +pnpm install +pnpm --filter @offering-protocol/service... build +pnpm --filter @offering-protocol/example-service-cloudflare dev +``` + +Wrangler prints the local address, normally `http://localhost:8787`. Try: + +```sh +curl http://localhost:8787/.well-known/odp +curl http://localhost:8787/odp/offerings +curl http://localhost:8787/odp/offerings/incident-plan +curl http://localhost:8787/downloads/incident-plan.txt +``` + +Edit `src/index.ts` to change the catalog. For a database-backed Service, replace the two catalog +handlers with your application's queries. The ODP handler validates requests and responses and +returns the appropriate HTTP status and headers. + +## Use it in your Worker + +Install `@offering-protocol/service` in your application, copy the integration from `src/index.ts`, +and configure Wrangler: + +```json +{ + "main": "src/index.ts", + "compatibility_date": "2025-06-01", + "compatibility_flags": ["nodejs_compat", "disallow_eval_during_startup"] +} +``` + +`2025-06-01` is the compatibility date tested by this example. Node compatibility provides the URL, +Buffer, and cryptographic APIs used by the SDK. The explicit `disallow_eval_during_startup` flag +demonstrates that ODP's bundled validators do not need permission to generate JavaScript at runtime; +the flag is not required by ODP. See Cloudflare's +[Node compatibility guidance](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) when +choosing a later date for an existing application. + +The SDK ships its compiled ODP validators in the npm package. You do not need to generate them or +download schemas when your Worker starts. + +This example has no accounts, payments, database, or secrets. Those remain application choices; +adding an ODP catalog does not implement authentication or payments. + +If you replace the catalog handlers with `createStaticCatalog`, initialize that helper inside the +Worker request handler and pass a stable `continuationKey` from a Worker secret. Its default random +key cannot be generated during module initialization. Using the same secret across Worker instances +also lets pagination continue when requests reach different instances. + +## Test and deploy + +From the repository root, build the SDK packages and run the local Worker tests: + +```sh +pnpm --filter @offering-protocol/example-service-cloudflare... build +pnpm --filter @offering-protocol/example-service-cloudflare test +``` + +The tests use Cloudflare's local runtime and do not deploy a Worker. They cover discovery, Offering +retrieval, validation errors, conditional requests, and the download Action. The repository's +`pnpm verify` command also runs them. A Directory compatibility test uses Workers' native `fetch` +with a local response fixture; it does not contact the live Directory. + +To deploy, first choose your Worker name in `wrangler.json`. Then authenticate to your Cloudflare +account and publish: + +```sh +pnpm --filter @offering-protocol/example-service-cloudflare exec wrangler login +pnpm --filter @offering-protocol/example-service-cloudflare deploy +``` + +## Calling other Services from Workers + +Hosting this catalog does not require the Agent package. If your Worker also needs directory +search, use `@offering-protocol/directory` and start requests inside your request handler. + +The full `@offering-protocol/agent` package has additional transport and schema-compilation +requirements. It is not covered by this Service example. Read its +[Workers limitations](../../packages/agent/README.md#cloudflare-workers) before using it in a Worker. diff --git a/examples/odp-service-cloudflare/package.json b/examples/odp-service-cloudflare/package.json new file mode 100644 index 0000000..42cc2f3 --- /dev/null +++ b/examples/odp-service-cloudflare/package.json @@ -0,0 +1,21 @@ +{ + "name": "@offering-protocol/example-service-cloudflare", + "private": true, + "type": "module", + "scripts": { + "build": "wrangler deploy --dry-run --outdir dist", + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "lint": "eslint src --max-warnings 0", + "test": "pnpm build && node --test test/*.test.mjs", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@offering-protocol/service": "workspace:^" + }, + "devDependencies": { + "@offering-protocol/directory": "workspace:^", + "miniflare": "5.20260921.0-alpha", + "wrangler": "4.136.0" + } +} diff --git a/examples/odp-service-cloudflare/src/index.ts b/examples/odp-service-cloudflare/src/index.ts new file mode 100644 index 0000000..37b2650 --- /dev/null +++ b/examples/odp-service-cloudflare/src/index.ts @@ -0,0 +1,63 @@ +import { createOdpService } from "@offering-protocol/service"; + +const offering = { + odp_version: "1.0" as const, + id: "incident-plan", + name: "Incident Response Plan", + description: "A downloadable incident-response planning template.", + price: { type: "free" as const }, + actions: [ + { + authentication: "not-required" as const, + id: "download", + rel: "download" as const, + http: { + method: "GET" as const, + href: "/downloads/incident-plan.txt", + response_content_types: ["text/plain"] + } + } + ] +}; + +const service = createOdpService({ + document: { + name: "Cloudflare Example Store", + description: "A public ODP catalog served by a Cloudflare Worker.", + language: "en", + localizations: ["en"], + http: { endpoint_base: "/odp" } + }, + catalog: { + listOfferings: (request) => ({ + odp_version: "1.0", + items: [representOffering(request.representation)] + }), + getOffering: (id, request) => + id === offering.id ? representOffering(request.representation) : undefined + } +}); + +function representOffering(representation: "terse" | "full") { + if (representation === "full") return offering; + return { + odp_version: offering.odp_version, + id: offering.id, + name: offering.name, + price: offering.price + }; +} + +export default { + fetch(request: Request): Promise | Response { + if (new URL(request.url).pathname === "/downloads/incident-plan.txt") { + if (request.method !== "GET" && request.method !== "HEAD") { + return new Response(null, { status: 405, headers: { allow: "GET, HEAD" } }); + } + return new Response(request.method === "HEAD" ? null : "Incident Response Plan\n", { + headers: { "content-type": "text/plain" } + }); + } + return service.fetch(request); + } +}; diff --git a/examples/odp-service-cloudflare/test/directory-worker.mjs b/examples/odp-service-cloudflare/test/directory-worker.mjs new file mode 100644 index 0000000..e37b954 --- /dev/null +++ b/examples/odp-service-cloudflare/test/directory-worker.mjs @@ -0,0 +1,12 @@ +import { createDirectoryClient } from "@offering-protocol/directory"; + +export default { + async fetch() { + const directory = createDirectoryClient(); + const pages = []; + for await (const page of directory.searchServices({ query: "templates" }).pages) { + pages.push(page); + } + return Response.json(pages); + } +}; diff --git a/examples/odp-service-cloudflare/test/worker.test.mjs b/examples/odp-service-cloudflare/test/worker.test.mjs new file mode 100644 index 0000000..c1cd7e5 --- /dev/null +++ b/examples/odp-service-cloudflare/test/worker.test.mjs @@ -0,0 +1,173 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { after, before, test } from "node:test"; +import { promisify } from "node:util"; + +import { Miniflare, Response as RuntimeResponse } from "miniflare"; + +const root = path.resolve(import.meta.dirname, ".."); +const configuration = JSON.parse(await readFile(path.join(root, "wrangler.json"), "utf8")); +let worker; + +async function runtime(file, dev) { + return new Miniflare({ + telemetry: { enabled: false }, + workers: [ + { + config: { + name: "odp-test", + compatibilityDate: configuration.compatibility_date, + compatibilityFlags: configuration.compatibility_flags, + manifest: { + mainModule: "worker.mjs", + modulesRoot: "/", + modules: { + "worker.mjs": { type: "esm", contents: await readFile(path.join(root, file), "utf8") } + } + } + }, + ...(dev === undefined ? {} : { dev }) + } + ] + }); +} + +before(async () => { + worker = await runtime("dist/index.js"); +}); + +after(async () => { + await worker?.dispose(); +}); + +test("serves a Service Document without runtime code generation", async () => { + const response = await worker.dispatchFetch("http://localhost/.well-known/odp"); + assert.equal(response.status, 200); + const document = await response.json(); + assert.equal(document.odp_version, "1.0"); + assert.equal(document.http.endpoint_base, "/odp"); + assert.deepEqual(document.operations.map(({ name }) => name).sort(), [ + "get-offering", + "list-offerings" + ]); +}); + +test("serves terse and full Offerings and the download Action", async () => { + for (const representation of ["terse", "full"]) { + const list = await worker.dispatchFetch( + `http://localhost/odp/offerings?representation=${representation}` + ); + assert.equal(list.status, 200); + const page = await list.json(); + assert.equal(page.odp_version, "1.0"); + assert.equal(page.items.length, 1); + assert.equal(page.items[0].id, "incident-plan"); + assert.equal("actions" in page.items[0], representation === "full"); + const detail = await worker.dispatchFetch( + `http://localhost/odp/offerings/incident-plan?representation=${representation}` + ); + assert.equal(detail.status, 200); + const offering = await detail.json(); + assert.equal(offering.name, "Incident Response Plan"); + assert.equal("actions" in offering, representation === "full"); + if (representation === "full") { + const download = await worker.dispatchFetch( + new URL(offering.actions[0].http.href, "http://localhost") + ); + assert.equal(download.status, 200); + assert.equal(await download.text(), "Incident Response Plan\n"); + } + } +}); + +test("supports conditional GET and HEAD requests", async () => { + const url = "http://localhost/odp/offerings/incident-plan"; + const first = await worker.dispatchFetch(url); + const etag = first.headers.get("etag"); + assert.ok(etag); + await first.arrayBuffer(); + const cached = await worker.dispatchFetch(url, { headers: { "if-none-match": etag } }); + assert.equal(cached.status, 304); + assert.equal(await cached.text(), ""); + const head = await worker.dispatchFetch(url, { method: "HEAD" }); + assert.equal(head.status, 200); + assert.equal(head.headers.get("etag"), etag); + assert.equal(await head.text(), ""); +}); + +test("returns structured errors for invalid requests and missing Offerings", async () => { + for (const [suffix, status, code] of [ + ["offerings?limit=0", 400, "INVALID_REQUEST"], + ["offerings?representation=unknown", 400, "INVALID_REQUEST"], + ["offerings/missing", 404, "NOT_FOUND"] + ]) { + const response = await worker.dispatchFetch(`http://localhost/odp/${suffix}`); + assert.equal(response.status, status); + assert.match(response.headers.get("content-type"), /application\/problem\+json/); + const problem = await response.json(); + assert.equal(problem.code, code); + assert.equal(problem.status, status); + } +}); + +test("uses the Workers fetch API for Directory requests", async () => { + const require = createRequire(import.meta.url); + const wrangler = path.join( + path.dirname(require.resolve("wrangler/package.json")), + "bin/wrangler.js" + ); + await promisify(execFile)( + process.execPath, + [ + wrangler, + "deploy", + "test/directory-worker.mjs", + "--dry-run", + "--outdir", + ".generated/directory" + ], + { + cwd: root, + env: { ...process.env, WRANGLER_SEND_METRICS: "false" } + } + ); + const requests = []; + const service = { + service_origin: "https://example.com", + name: "Templates", + description: "Downloadable templates.", + language: "en", + localizations: ["en"], + operations: [ + { name: "list-offerings", authentication: "not-required" }, + { name: "get-offering", authentication: "not-required" } + ], + indexed_at: "2026-09-23T00:00:00Z" + }; + const directory = await runtime(".generated/directory/directory-worker.js", { + outboundService: { + type: "fetcher", + async handler(request) { + requests.push({ url: request.url, method: request.method, body: await request.json() }); + return RuntimeResponse.json({ items: [service], count: 1 }); + } + } + }); + try { + const response = await directory.dispatchFetch("http://localhost/"); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), [{ items: [service], count: 1 }]); + assert.deepEqual(requests, [ + { + url: "https://api.inflowpay.ai/v1/services/search", + method: "POST", + body: { query: "templates" } + } + ]); + } finally { + await directory.dispose(); + } +}); diff --git a/examples/odp-service-cloudflare/tsconfig.json b/examples/odp-service-cloudflare/tsconfig.json new file mode 100644 index 0000000..6496e4f --- /dev/null +++ b/examples/odp-service-cloudflare/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "noEmit": true, "rootDir": "src", "tsBuildInfoFile": ".tsbuildinfo" }, + "include": ["src/**/*.ts"] +} diff --git a/examples/odp-service-cloudflare/wrangler.json b/examples/odp-service-cloudflare/wrangler.json new file mode 100644 index 0000000..07edb3c --- /dev/null +++ b/examples/odp-service-cloudflare/wrangler.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "odp-service-example", + "main": "src/index.ts", + "compatibility_date": "2025-06-01", + "compatibility_flags": ["nodejs_compat", "disallow_eval_during_startup"] +} diff --git a/package.json b/package.json index 5149b8d..f1e6438 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "typecheck": "turbo run typecheck", "format": "prettier --write \"**/*.{ts,js,mjs,json,md,yml,yaml}\"", "format:check": "prettier --check \"**/*.{ts,js,mjs,json,md,yml,yaml}\"", - "typedoc": "typedoc", + "typedoc": "pnpm --filter @offering-protocol/core generate && typedoc", "conformance": "node scripts/run-conformance.mjs", "smoke:examples": "node scripts/smoke-examples.mjs", "verify": "pnpm format:check && pnpm typecheck && pnpm lint && pnpm test && pnpm coverage:changed && pnpm typedoc && pnpm build && pnpm verify:packages && pnpm smoke:examples && node scripts/benchmark.mjs", diff --git a/packages/agent/README.md b/packages/agent/README.md index 7b3c9d7..ca07feb 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -15,6 +15,31 @@ the Service-only `searchServices()` method; mixed search does not change that or npm install @offering-protocol/agent ``` +## Cloudflare Workers + +Use Node.js for the full Agent workflow. Running Node.js behind Cloudflare's proxy or firewall is +different from executing this package inside a Cloudflare Worker. + +Inside Workers, two limitations remain even when core's bundled ODP validators can run: + +- **Networking:** Cloudflare Workers do not support all the Node.js networking features used by the + Agent's default HTTP transport. Replacing that transport with plain `fetch` does not preserve all + its destination-validation protections. +- **Service-provided schemas:** The Agent retrieves schemas from target Services to validate + Offering attributes and resolve Actions. Processing those schemas requires generating JavaScript + during the request, which Cloudflare Workers prohibit. Precompiling the SDK's bundled ODP schemas + does not address these separately retrieved schemas. + +An Offering with an unusable Attribute Schema is returned without its attributes and with a scoped +issue. An Action that needs an unresolved schema cannot be fully resolved. Do not treat successful +Service inspection as proof that all Agent operations work in Workers. + +These restrictions do not disable normal schema retrieval on Node.js. The Agent retrieves the +supporting documents it needs, subject to its existing network and resource limits. + +For hosting an ODP catalog rather than calling other Services, use the +[Service package's Workers example](../../examples/odp-service-cloudflare/README.md). + ## Discover Offerings Across Services `createOdpAgent` searches the canonical directory and then searches each matching Service. Results diff --git a/packages/core/README.md b/packages/core/README.md index 10b6cb4..551be41 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -49,6 +49,17 @@ names outside the declared ODP version. Agent readers use `parseAgentServiceDocu `safeParseAgentServiceDocument`; these filter unrecognized enrollment, payment, and trust descriptors before validating every recognized descriptor. +## Runtime compatibility + +The npm package includes validators generated from the bundled ODP schemas during the SDK build. +Your application runs those functions directly: it does not download ODP schemas or generate +JavaScript when importing the package or validating a document. + +For Cloudflare Workers, follow the tested configuration in the +[Workers example](../../examples/odp-service-cloudflare/README.md#use-it-in-your-worker). +Precompiled core validators do not remove the Agent package's separate requirements for processing +schemas published by Services. + ## Resource Identity and References `createResourceIdentity` composes the Service origin, resource type, and Service-assigned local diff --git a/packages/core/THIRD-PARTY-NOTICES.md b/packages/core/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..5ed11da --- /dev/null +++ b/packages/core/THIRD-PARTY-NOTICES.md @@ -0,0 +1,27 @@ +# Bundled runtime helpers + +The compiled validators include runtime helpers from these MIT-licensed packages: + +- Ajv: Copyright (c) 2015-2021 Evgeny Poberezkin +- ajv-formats: Copyright (c) 2020 Evgeny Poberezkin +- fast-deep-equal: Copyright (c) 2017 Evgeny Poberezkin + +The following license applies to those helpers: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/core/package.json b/packages/core/package.json index 1c81cdb..db8a17a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -22,13 +22,15 @@ "files": [ "dist", "README.md", - "LICENSE" + "LICENSE", + "THIRD-PARTY-NOTICES.md" ], "scripts": { - "build": "tsup", - "test": "vitest run --coverage", - "lint": "eslint src test --max-warnings 0 --no-error-on-unmatched-pattern", - "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", + "generate": "node scripts/generate-validators.mjs", + "build": "pnpm generate && tsup", + "test": "pnpm generate && vitest run --coverage", + "lint": "pnpm generate && eslint src test --max-warnings 0 --no-error-on-unmatched-pattern", + "typecheck": "pnpm generate && tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "clean": "rm -rf dist .turbo coverage" }, "engines": { @@ -45,8 +47,10 @@ "directory": "packages/core" }, "dependencies": { - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1", "bcp-47": "^2.1.1" + }, + "devDependencies": { + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" } } diff --git a/packages/core/scripts/generate-validators.mjs b/packages/core/scripts/generate-validators.mjs new file mode 100644 index 0000000..22617ef --- /dev/null +++ b/packages/core/scripts/generate-validators.mjs @@ -0,0 +1,58 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises"; + +import { Ajv2020 } from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; +import standaloneCode from "ajv/dist/standalone/index.js"; + +const schemasDirectory = new URL("../src/schemas/", import.meta.url); +const outputDirectory = new URL("../.generated/", import.meta.url); +const schemas = { + collection: "collection", + collectionSearchRequest: "collection-search-request", + filterDefinition: "filter-definition", + filterDefinitionPage: "filter-definition-page", + offering: "offering", + offeringSearchRequest: "offering-search-request", + offeringSearchResponse: "offering-search-response", + page: "page-envelope", + problemDetails: "problem-details", + resourceIdentity: "resource-identity", + serviceDocument: "service-document", + sortDefinition: "sort-definition", + sortDefinitionPage: "sort-definition-page" +}; +const ajv = new Ajv2020({ + allErrors: true, + strict: true, + strictRequired: false, + strictTypes: false, + code: { source: true, esm: true } +}); +addFormats(ajv); +for (const file of (await readdir(schemasDirectory)).sort()) { + if (file.endsWith(".schema.json")) { + ajv.addSchema(JSON.parse(await readFile(new URL(file, schemasDirectory), "utf8"))); + } +} +const validators = Object.fromEntries( + Object.entries(schemas).map(([name, schema]) => [ + name, + `https://offeringprotocol.org/schemas/${schema}.schema.json` + ]) +); +await mkdir(outputDirectory, { recursive: true }); +await writeGeneratedFile("validators.js", standaloneCode(ajv, validators)); +await writeGeneratedFile( + "validators.d.ts", + 'import type { ValidateFunction } from "ajv";\n' + + Object.keys(schemas) + .map((name) => `export declare const ${name}: ValidateFunction;\n`) + .join("") +); + +async function writeGeneratedFile(name, content) { + const temporary = new URL(`${name}.${randomUUID()}`, outputDirectory); + await writeFile(temporary, content); + await rename(temporary, new URL(name, outputDirectory)); +} diff --git a/packages/core/src/schema-registry.ts b/packages/core/src/schema-registry.ts deleted file mode 100644 index 8f3f8bc..0000000 --- a/packages/core/src/schema-registry.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Ajv2020 } from "ajv/dist/2020.js"; -import addFormatsModule from "ajv-formats"; -import schema0 from "./schemas/action-relation.schema.json" with { type: "json" }; -import schema1 from "./schemas/action-request.schema.json" with { type: "json" }; -import schema2 from "./schemas/action.schema.json" with { type: "json" }; -import authenticationRequirementSchema from "./schemas/authentication-requirement.schema.json" with { type: "json" }; -import schema3 from "./schemas/attribute-schema-reference.schema.json" with { type: "json" }; -import schema4 from "./schemas/capability-identifier.schema.json" with { type: "json" }; -import schema5 from "./schemas/capability-link.schema.json" with { type: "json" }; -import schema6 from "./schemas/collection-search-request.schema.json" with { type: "json" }; -import schema7 from "./schemas/collection.schema.json" with { type: "json" }; -import schema8 from "./schemas/detail-fields.schema.json" with { type: "json" }; -import enrollmentProtocolSchema from "./schemas/enrollment-protocol.schema.json" with { type: "json" }; -import schema9 from "./schemas/filter-capability-source.schema.json" with { type: "json" }; -import schema10 from "./schemas/filter-definition-page.schema.json" with { type: "json" }; -import schema11 from "./schemas/filter-definition.schema.json" with { type: "json" }; -import schema12 from "./schemas/filter-expression.schema.json" with { type: "json" }; -import schema13 from "./schemas/filter-operator.schema.json" with { type: "json" }; -import schema14 from "./schemas/filter-type.schema.json" with { type: "json" }; -import schema15 from "./schemas/filter-unit.schema.json" with { type: "json" }; -import schema16 from "./schemas/http-action-target.schema.json" with { type: "json" }; -import schema17 from "./schemas/invalid-parameter.schema.json" with { type: "json" }; -import schema18 from "./schemas/local-resource-identifier-list.schema.json" with { type: "json" }; -import schema19 from "./schemas/local-resource-identifier.schema.json" with { type: "json" }; -import mcpEndpointSchema from "./schemas/mcp-endpoint.schema.json" with { type: "json" }; -import schema20 from "./schemas/offering-search-request.schema.json" with { type: "json" }; -import schema21 from "./schemas/offering-search-response.schema.json" with { type: "json" }; -import schema22 from "./schemas/offering.schema.json" with { type: "json" }; -import schema23 from "./schemas/openapi-action-target.schema.json" with { type: "json" }; -import operationDescriptorSchema from "./schemas/operation-descriptor.schema.json" with { type: "json" }; -import schema24 from "./schemas/page-envelope.schema.json" with { type: "json" }; -import schema25 from "./schemas/page-limit.schema.json" with { type: "json" }; -import schema26 from "./schemas/price-preview.schema.json" with { type: "json" }; -import paymentOptionSchema from "./schemas/payment-option.schema.json" with { type: "json" }; -import paymentProtocolSchema from "./schemas/payment-protocol.schema.json" with { type: "json" }; -import schema27 from "./schemas/problem-code.schema.json" with { type: "json" }; -import schema28 from "./schemas/problem-details.schema.json" with { type: "json" }; -import schema29 from "./schemas/protocol-version.schema.json" with { type: "json" }; -import schema30 from "./schemas/refinement-bucket.schema.json" with { type: "json" }; -import schema31 from "./schemas/refinement-group.schema.json" with { type: "json" }; -import schema32 from "./schemas/representation.schema.json" with { type: "json" }; -import resourceImageSchema from "./schemas/resource-image.schema.json" with { type: "json" }; -import schema33 from "./schemas/resource-identity.schema.json" with { type: "json" }; -import schema34 from "./schemas/resource-reference.schema.json" with { type: "json" }; -import schema35 from "./schemas/schema-reference.schema.json" with { type: "json" }; -import schema36 from "./schemas/search-capabilities.schema.json" with { type: "json" }; -import serviceBrandingImageSchema from "./schemas/service-branding-image.schema.json" with { type: "json" }; -import serviceBrandingSchema from "./schemas/service-branding.schema.json" with { type: "json" }; -import schema37 from "./schemas/service-document.schema.json" with { type: "json" }; -import serviceOpenApiSchema from "./schemas/service-openapi.schema.json" with { type: "json" }; -import schema38 from "./schemas/service-origin.schema.json" with { type: "json" }; -import schema39 from "./schemas/service-protocols.schema.json" with { type: "json" }; -import schema40 from "./schemas/sort-capability-source.schema.json" with { type: "json" }; -import schema41 from "./schemas/sort-definition-page.schema.json" with { type: "json" }; -import schema42 from "./schemas/sort-definition.schema.json" with { type: "json" }; -import schema43 from "./schemas/sort-key.schema.json" with { type: "json" }; -import schema44 from "./schemas/top-level-document.schema.json" with { type: "json" }; -import trustProtocolSchema from "./schemas/trust-protocol.schema.json" with { type: "json" }; - -export const ajv = new Ajv2020({ - allErrors: true, - strict: true, - strictRequired: false, - strictTypes: false -}); -addFormatsModule.default(ajv); - -const schemas = [ - schema0, - schema1, - schema2, - authenticationRequirementSchema, - schema3, - schema4, - schema5, - schema6, - schema7, - schema8, - enrollmentProtocolSchema, - schema9, - schema10, - schema11, - schema12, - schema13, - schema14, - schema15, - schema16, - schema17, - schema18, - schema19, - mcpEndpointSchema, - schema20, - schema21, - schema22, - schema23, - operationDescriptorSchema, - schema24, - schema25, - schema26, - paymentOptionSchema, - paymentProtocolSchema, - schema27, - schema28, - schema29, - schema30, - schema31, - schema32, - resourceImageSchema, - schema33, - schema34, - schema35, - schema36, - serviceBrandingImageSchema, - serviceBrandingSchema, - schema37, - serviceOpenApiSchema, - schema38, - schema39, - schema40, - schema41, - schema42, - schema43, - schema44, - trustProtocolSchema -]; -for (const schema of schemas) ajv.addSchema(schema); diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index b74493d..693b27b 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -1,4 +1,4 @@ -import type { ErrorObject } from "ajv"; +import type { ErrorObject, ValidateFunction } from "ajv"; import { parse as parseLanguageTag } from "bcp-47"; import type { @@ -15,7 +15,7 @@ import type { SortDefinition } from "./models.js"; import { PAYMENT_OPTIONS } from "./models.js"; -import { ajv } from "./schema-registry.js"; +import * as validators from "../.generated/validators.js"; function isLanguageTag(value: string): boolean { const parsed = parseLanguageTag(value, { normalize: false }); @@ -196,15 +196,10 @@ function filterDefinitionIssues(value: FilterDefinition): ValidationIssue[] { } function validator( - schemaId: string, + validate: ValidateFunction, documentType: string, refine?: (value: Value) => ValidationIssue[] ) { - const validate = ajv.getSchema(schemaId); - if (validate === undefined) { - throw new Error(`Missing bundled ODP schema: ${schemaId}`); - } - const safeParse = (value: unknown): SafeParseResult => { if (validate(value)) { const data = value as Value; @@ -225,7 +220,7 @@ function validator( } const serviceDocument = validator( - "https://offeringprotocol.org/schemas/service-document.schema.json", + validators.serviceDocument, "Service Document", serviceDocumentIssues ); @@ -600,56 +595,42 @@ export function parseAgentServiceDocument(value: unknown): ServiceDocument { export function safeParseAgentServiceDocument(value: unknown): SafeParseResult { return serviceDocument.safeParse(agentServiceDocumentValue(value)); } -const collection = validator( - "https://offeringprotocol.org/schemas/collection.schema.json", - "Collection", - representationIssues -); -const offering = validator( - "https://offeringprotocol.org/schemas/offering.schema.json", - "Offering", - representationIssues -); +const collection = validator(validators.collection, "Collection", representationIssues); +const offering = validator(validators.offering, "Offering", representationIssues); const problemDetails = validator( - "https://offeringprotocol.org/schemas/problem-details.schema.json", + validators.problemDetails, "Problem Details", problemDetailsIssues ); const resourceIdentity = validator( - "https://offeringprotocol.org/schemas/resource-identity.schema.json", + validators.resourceIdentity, "resource identity" ); -const page = validator( - "https://offeringprotocol.org/schemas/page-envelope.schema.json", - "page envelope" -); +const page = validator(validators.page, "page envelope"); const collectionSearchRequest = validator( - "https://offeringprotocol.org/schemas/collection-search-request.schema.json", + validators.collectionSearchRequest, "Collection search request" ); const offeringSearchRequest = validator( - "https://offeringprotocol.org/schemas/offering-search-request.schema.json", + validators.offeringSearchRequest, "Offering search request" ); const offeringSearchResponse = validator( - "https://offeringprotocol.org/schemas/offering-search-response.schema.json", + validators.offeringSearchResponse, "Offering search response" ); const filterDefinition = validator( - "https://offeringprotocol.org/schemas/filter-definition.schema.json", + validators.filterDefinition, "Filter Definition", filterDefinitionIssues ); -const sortDefinition = validator( - "https://offeringprotocol.org/schemas/sort-definition.schema.json", - "Sort Definition" -); +const sortDefinition = validator(validators.sortDefinition, "Sort Definition"); const filterDefinitionPage = validator>( - "https://offeringprotocol.org/schemas/filter-definition-page.schema.json", + validators.filterDefinitionPage, "Filter Definition page" ); const sortDefinitionPage = validator>( - "https://offeringprotocol.org/schemas/sort-definition-page.schema.json", + validators.sortDefinitionPage, "Sort Definition page" ); diff --git a/packages/core/test/unit/generated-validators.test.ts b/packages/core/test/unit/generated-validators.test.ts new file mode 100644 index 0000000..6bd1647 --- /dev/null +++ b/packages/core/test/unit/generated-validators.test.ts @@ -0,0 +1,107 @@ +import { readdirSync, readFileSync } from "node:fs"; + +import { Ajv2020 } from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; +import { describe, expect, it } from "vitest"; + +import * as generated from "../../.generated/validators.js"; + +const schemaNames = { + collection: "collection", + collectionSearchRequest: "collection-search-request", + filterDefinition: "filter-definition", + filterDefinitionPage: "filter-definition-page", + offering: "offering", + offeringSearchRequest: "offering-search-request", + offeringSearchResponse: "offering-search-response", + page: "page-envelope", + problemDetails: "problem-details", + resourceIdentity: "resource-identity", + serviceDocument: "service-document", + sortDefinition: "sort-definition", + sortDefinitionPage: "sort-definition-page" +}; +const ajv = new Ajv2020({ + allErrors: true, + strict: true, + strictRequired: false, + strictTypes: false +}); +addFormats.default(ajv); +const directory = new URL("../../src/schemas/", import.meta.url); +for (const file of readdirSync(directory).sort()) { + if (file.endsWith(".schema.json")) { + const schema: unknown = JSON.parse(readFileSync(new URL(file, directory), "utf8")); + if (typeof schema !== "object" || schema === null) throw new Error("Expected a schema object"); + ajv.addSchema(schema); + } +} + +const documents: Record[] = [ + { + odp_version: "1.0", + name: "Example", + description: "An example Service.", + language: "en", + localizations: ["en"], + operations: [ + { authentication: "not-required", name: "list-offerings" }, + { authentication: "not-required", name: "get-offering" } + ], + http: { endpoint_base: "/odp" } + }, + { odp_version: "1.0", id: "example", name: "Example" }, + { odp_version: "1.0", items: [] }, + { odp_version: "1.0", query: "plants" }, + { + status: 400, + code: "INVALID_REQUEST", + title: "Invalid request", + type: "https://offeringprotocol.org/problems/invalid-request" + }, + { service: "https://example.com", type: "offering", id: "example" }, + { + id: "price", + title: "Price", + description: "Filter by price.", + type: "number", + operators: ["eq"] + }, + { + id: "by-price", + title: "By price", + description: "Sort by price.", + keys: [{ filter_id: "price", direction: "ascending", missing: "last" }] + } +]; +const inputs: unknown[] = [undefined, null, false, 0, "", [], {}, ...documents]; +for (const document of documents) { + for (const key of Object.keys(document)) { + const omitted = { ...document }; + delete omitted[key]; + inputs.push(omitted); + for (const value of [null, false, -1, [], ""]) inputs.push({ ...document, [key]: value }); + } +} + +describe("precompiled schema validators", () => { + it("exports every core validator", () => { + expect(Object.keys(generated).sort()).toEqual(Object.keys(schemaNames).sort()); + }); + + for (const name of Object.keys(schemaNames) as (keyof typeof schemaNames)[]) { + it(`preserves ${name} validation results and errors`, () => { + const runtime = ajv.getSchema( + `https://offeringprotocol.org/schemas/${schemaNames[name]}.schema.json` + ); + if (runtime === undefined) throw new Error(`Missing schema: ${name}`); + expect(documents.some((document) => runtime(document) === true)).toBe(true); + for (const input of inputs) { + const value = structuredClone(input); + expect(generated[name](value)).toBe(runtime(input)); + expect(generated[name].errors).toEqual(runtime.errors); + expect(value).toEqual(input); + } + }); + } +}); diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 8e65ade..02de5c3 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ format: ["esm", "cjs"], dts: true, sourcemap: true, + noExternal: [/^ajv(?:-formats)?(?:\/|$)/], clean: true }); diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 3ef00da..8b46b89 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", - coverage: { provider: "v8", reporter: ["text", "json", "html", "lcov"] } + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + reporter: ["text", "json", "html", "lcov"] + } } }); diff --git a/packages/directory/README.md b/packages/directory/README.md index e6d4e18..5f8e334 100644 --- a/packages/directory/README.md +++ b/packages/directory/README.md @@ -16,6 +16,15 @@ The package has two environments and no configurable base URL: A fetch-compatible `transport` can be injected for testing without changing the selected origin. +## Cloudflare Workers + +The Directory client uses the runtime's `fetch` API. Call it from a Worker request handler, where +outbound requests are allowed, and use the Node compatibility settings from the +[Workers example](../../examples/odp-service-cloudflare/README.md#use-it-in-your-worker). + +Directory search does not resolve Offering Attribute Schemas. Navigating a Service through the +Agent package has [additional Workers limitations](../agent/README.md#cloudflare-workers). + ## Search the Directory `search()` returns native ODP Services, imported OpenAPI Services, and indexed Collections. It searches cached names, diff --git a/packages/service/README.md b/packages/service/README.md index a3cf0d1..fc2c203 100644 --- a/packages/service/README.md +++ b/packages/service/README.md @@ -23,6 +23,20 @@ Responses are validated against the protocol's byte and nesting-depth limits bef Service: a catalog that produces an over-limit document gets a `500` here rather than a truncated read at the Agent. +## Cloudflare Workers + +The [Workers example](../../examples/odp-service-cloudflare/README.md) shows how to export a Worker +request handler, configure Node compatibility, run locally, and deploy. Core's ODP validators ship +as ordinary JavaScript functions, so your Worker does not compile schemas at startup or fetch them +from the protocol website. + +Use this package to expose your own catalog. You do not need the Agent package to host a Service. + +If you use `createStaticCatalog`, initialize it from a Worker request handler and supply a stable +`continuationKey` from a Worker secret. Without that key, the helper generates random bytes, which +Workers forbids during module initialization. Share the key across instances so pagination cursors +remain usable when a later request reaches another instance. + ## Small catalogs `createStaticCatalog` provides the required `list-offerings` and `get-offering` operations from a diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 100bce1..1812987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,22 @@ importers: specifier: ^5.0.6 version: 5.0.6 + examples/odp-service-cloudflare: + dependencies: + '@offering-protocol/service': + specifier: workspace:^ + version: link:../../packages/service + devDependencies: + '@offering-protocol/directory': + specifier: workspace:^ + version: link:../../packages/directory + miniflare: + specifier: 5.20260921.0-alpha + version: 5.20260921.0-alpha(@types/node@26.5.0) + wrangler: + specifier: 4.136.0 + version: 4.136.0(@types/node@26.5.0) + examples/odp-service-marketplace: dependencies: '@offering-protocol/core': @@ -189,15 +205,16 @@ importers: packages/core: dependencies: + bcp-47: + specifier: ^2.1.1 + version: 2.1.1 + devDependencies: ajv: specifier: ^8.20.0 version: 8.20.0 ajv-formats: specifier: ^3.0.1 version: 3.0.1(ajv@8.20.0) - bcp-47: - specifier: ^2.1.1 - version: 2.1.1 packages/directory: dependencies: @@ -326,6 +343,56 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@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.20260921.1': + resolution: {integrity: sha512-3iB2WnYOlZ29T+1zhCwbHFExCBp6E9bgmDUMryATYwrIGEQ1YbvR78m4ydm56XKN/d/yF3803ivMGfZMYDtiMg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260921.1': + resolution: {integrity: sha512-FpqVR7IQXVBmGtajyonEmhmb5UAsmV7dTaIkpemmHZXHEw7uYpkhkzKPjc4BOPhNQy8iwt2p+RZBPMY3Y7/bvQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260921.1': + resolution: {integrity: sha512-riAJIohaVp5A8Sqy4yKlzHOaLPOICMf5oey+jC2rm45RVT+wK8+7UU0d31Dy/02Nc8YUkobAFwNVjX06P8WQ5g==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260921.1': + resolution: {integrity: sha512-tnJu08tT7s0XWDqp3O0H/vCp0voy9OqVAzspb89biMo1dh8IiEpnyXnoPmdJ7H4qBnXCmXgy0kuEphuvpDPj9w==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260921.1': + resolution: {integrity: sha512-VgNcRPstoZMb1G94JTrx+jU24GtkkazNfox0gnF/2fkuXpcfW/M0e0xvdMovYfwt8ZxG5AB2ZNvanD6ufBwiuQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@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.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -564,6 +631,168 @@ packages: '@hyperjump/uri@1.3.6': resolution: {integrity: sha512-9a2/wOIp666Veq1a8QctYPRtyFHH0p1yw8Nt/DEf06M+eDpwBpeuc2rGqqjBthENTOy1NyJ/xhSiDtkjU5LfKA==} + '@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] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@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] + libc: [musl] + + '@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] + '@inflowpayai/mpp-seller@0.6.2': resolution: {integrity: sha512-M7qkSE/uYHX64s5BEv3KTM4aCHYxYCNiGUMVQImec7CX73uBNHm0jyO5uJF20+tALV1eV0/4h4Z+3VDN1/SU9w==} engines: {node: '>=22.0.0'} @@ -610,6 +839,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -654,6 +886,15 @@ packages: '@oxc-project/types@0.149.0': resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rolldown/binding-android-arm-eabi@1.2.8': resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -929,6 +1170,13 @@ packages: viem: optional: true + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1234,6 +1482,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -1334,6 +1585,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + 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'} @@ -1392,6 +1647,9 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1778,6 +2036,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1932,6 +2194,10 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + miniflare@5.20260921.0-alpha: + resolution: {integrity: sha512-vHH/unOYvV2jA1Q9SdkmzrQhhMoksdwg5jegu6ZeKaaRzgxZhVbt1NdTpQjHF2VTgiBjgP8SiUlUMfruB3N3SQ==} + engines: {node: '>=22.0.0'} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -2081,6 +2347,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -2239,6 +2508,15 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + 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'} @@ -2319,6 +2597,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2376,6 +2658,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -2442,10 +2727,17 @@ packages: resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} engines: {node: '>=18.17'} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + undici@7.29.1: resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} 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'} @@ -2576,6 +2868,21 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerd@1.20260921.1: + resolution: {integrity: sha512-4HyG7G1W4ksa6tUZ8bV2jxDRWuL5PXnHm9+Z1sjFPb9OZNoYtXz4y7QQRh4ibi0BF/lOmlAVjbhkUqsAVZuUKA==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.136.0: + resolution: {integrity: sha512-CjSe3/k8UEWqbky218Z3f4gsDAe3QuKylkeZAXatfsM9SH+MGRH6kxaOGT1fgM/5kr8zpI1A5o6k6jvqB/CImA==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^5.20260921.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2600,6 +2907,12 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2809,6 +3122,38 @@ snapshots: human-id: 4.2.1 prettier: 2.8.8 + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260921.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260921.1 + + '@cloudflare/workerd-darwin-64@1.20260921.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260921.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260921.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260921.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260921.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true @@ -2987,6 +3332,112 @@ snapshots: '@hyperjump/uri@1.3.6': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.4': + optionalDependencies: + '@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 + '@inflowpayai/mpp-seller@0.6.2(mppx@0.8.19(express@5.2.1)(typescript@5.9.3)(viem@2.56.3(typescript@5.9.3)(zod@4.5.4)))': dependencies: '@inflowpayai/mpp': 0.7.1(mppx@0.8.19(express@5.2.1)(typescript@5.9.3)(viem@2.56.3(typescript@5.9.3)(zod@4.5.4))) @@ -3026,6 +3477,11 @@ snapshots: '@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 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.7 @@ -3071,6 +3527,18 @@ snapshots: '@oxc-project/types@0.149.0': {} + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@rolldown/binding-android-arm-eabi@1.2.8': optional: true @@ -3237,6 +3705,10 @@ snapshots: optionalDependencies: viem: 2.56.3(typescript@5.9.3)(zod@3.25.76) + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.24': {} + '@standard-schema/spec@1.1.0': {} '@stripe/stripe-js@9.13.0': {} @@ -3594,6 +4066,8 @@ snapshots: dependencies: is-windows: 1.0.2 + blake3-wasm@2.1.5: {} + body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -3681,6 +4155,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3724,6 +4200,8 @@ snapshots: entities@4.5.0: {} + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -4148,6 +4626,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@4.1.5: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4270,6 +4750,19 @@ snapshots: dependencies: mime-db: 1.54.0 + miniflare@5.20260921.0-alpha(@types/node@26.5.0): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.4(@types/node@26.5.0) + undici: 7.29.0 + workerd: 1.20260921.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 @@ -4428,6 +4921,8 @@ snapshots: path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} path-type@4.0.0: {} @@ -4611,6 +5106,39 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.35.4(@types/node@26.5.0): + 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': 26.5.0 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4688,6 +5216,8 @@ snapshots: tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4731,6 +5261,9 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.8.1: + optional: true + tsup@8.5.1(postcss@8.5.28)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.28.2) @@ -4810,8 +5343,14 @@ snapshots: undici@6.28.0: {} + undici@7.29.0: {} + undici@7.29.1: {} + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + universalify@0.1.2: {} unpipe@1.0.0: {} @@ -4928,6 +5467,31 @@ snapshots: word-wrap@1.2.5: {} + workerd@1.20260921.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260921.1 + '@cloudflare/workerd-darwin-arm64': 1.20260921.1 + '@cloudflare/workerd-linux-64': 1.20260921.1 + '@cloudflare/workerd-linux-arm64': 1.20260921.1 + '@cloudflare/workerd-windows-64': 1.20260921.1 + + wrangler@4.136.0(@types/node@26.5.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260921.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.2 + miniflare: 5.20260921.0-alpha(@types/node@26.5.0) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260921.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + wrappy@1.0.2: {} ws@8.21.0: {} @@ -4936,6 +5500,19 @@ snapshots: yocto-queue@0.1.0: {} + 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 + zod@3.25.76: {} zod@4.5.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a79edea..dc2b1af 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,7 @@ confirmModulesPurge: false allowBuilds: esbuild: true + workerd: true overrides: esbuild: ">=0.28.1 <0.29.0" diff --git a/scripts/verify-packages.mjs b/scripts/verify-packages.mjs index 430f606..32395e9 100644 --- a/scripts/verify-packages.mjs +++ b/scripts/verify-packages.mjs @@ -4,6 +4,7 @@ import { execFile } from "node:child_process"; import { readFile, stat } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; +import { pathToFileURL } from "node:url"; const execFileAsync = promisify(execFile); const root = path.resolve(import.meta.dirname, ".."); @@ -21,7 +22,7 @@ const packages = { "ipaddr.js", "undici" ], - core: ["ajv", "ajv-formats", "bcp-47"], + core: ["bcp-47"], directory: ["@offering-protocol/core"], service: ["@offering-protocol/core"] }; @@ -78,6 +79,9 @@ async function verifyPackage(directory, expectedDependencies) { for (const required of requiredFiles) { if (!files.has(required)) throw new Error(`${directory}: tarball is missing ${required}`); } + if (directory === "core" && !files.has("THIRD-PARTY-NOTICES.md")) { + throw new Error("core: tarball is missing bundled dependency licenses"); + } for (const file of files) { if (file.startsWith("src/") || file.startsWith("test/")) { throw new Error(`${directory}: tarball exposes ${file}`); @@ -89,4 +93,20 @@ for (const [directory, dependencies] of Object.entries(packages)) { await verifyPackage(directory, dependencies); } +for (const file of ["index.js", "index.cjs"]) { + const entry = pathToFileURL(path.join(root, "packages/core/dist", file)).href; + await execFileAsync(process.execPath, [ + "--disallow-code-generation-from-strings", + "--input-type=module", + "--eval", + `import assert from "node:assert/strict"; + const core = await import(${JSON.stringify(entry)}); + const offering = {odp_version: "1.0", id: "template", name: "Template"}; + assert.deepEqual(core.parseOffering(offering), offering); + for (const [name, validate] of Object.entries(core)) { + if (name.startsWith("safeParse")) assert.equal(validate(null).success, false, name); + }` + ]); +} + console.log("Package publication surfaces OK"); diff --git a/turbo.json b/turbo.json index d3a9496..95becdf 100644 --- a/turbo.json +++ b/turbo.json @@ -5,21 +5,49 @@ "tasks": { "build": { "dependsOn": ["^build"], - "inputs": ["src/**", "tsconfig.json", "tsup.config.ts", "package.json"], + "inputs": [ + "src/**", + "scripts/**", + "tsconfig.json", + "tsup.config.ts", + "wrangler.json", + "package.json" + ], "outputs": ["dist/**"] }, "lint": { "dependsOn": ["^build"], - "inputs": ["src/**", "test/**", "tsconfig.json", "tsconfig.test.json"] + "inputs": [ + "src/**", + "scripts/**", + "test/**", + "tsconfig.json", + "tsconfig.test.json", + "package.json" + ] }, "typecheck": { "dependsOn": ["^build"], - "inputs": ["src/**", "test/**", "tsconfig.json", "tsconfig.test.json"], + "inputs": [ + "src/**", + "scripts/**", + "test/**", + "tsconfig.json", + "tsconfig.test.json", + "package.json" + ], "outputs": [] }, "test": { "dependsOn": ["^build"], - "inputs": ["src/**", "test/**", "vitest.config.ts"], + "inputs": [ + "src/**", + "scripts/**", + "test/**", + "vitest.config.ts", + "wrangler.json", + "package.json" + ], "outputs": [] }, "clean": {