diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b548b4d..8cfc3d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,6 +181,11 @@ jobs: registry/test/e2e/helpers/sandbox.ts registry/test/helpers/fixtures.ts registry/test/ledger.test.ts + # Its ids configure a service the check never lets reach a + # participant, so nothing there resolves them and a wrong one + # would not red that script. They are compared here for the same + # reason the rest are: daml/ is where they can go stale. + scripts/registry-install-smoke.sh scripts/seed.mjs ) # Two more files carry the prefix and must not be checked against @@ -603,3 +608,184 @@ jobs: SKIP_BUILD: '1' ALLOW_UNTAGGED: '1' run: bash scripts/release-notes.sh "${GITHUB_REF_NAME}" + + # This job's name is the status check context that branch protection or a + # ruleset would match on. Renaming it, including a capitalisation change, + # stops that check from reporting on every pull request, this rename's own + # included. The scope gate is per-step rather than one job-level `if:` so + # that a scoped-out pull request still reports a green check carrying the + # scope log that says which paths it looked at. A job-level `if:` would + # report `skipped` instead, which a required check accepts but which reads + # on the pull request as though the job never ran. + package: + name: package + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # registry/dist is the package's payload and must not be committed, while + # a dist directory anywhere else under registry/ is build output of a kind + # nothing ships. The rule drawing that line lives in the ROOT .gitignore + # rather than beside the code, because npm applies a nested ignore file to + # the pack walk even for a path the manifest's "files" allowlist names, + # while the allowlist outranks the root file. That placement is exactly + # what hides the rule from every other check here: the root file cannot + # subtract from the tarball, so re-anchoring it to `registry/dist` leaves + # the smoke test and the manifest guard below both at exit 0 while build + # output under registry/src and registry/test silently stops being + # ignored. That is the regression 7d04a6c fixed, and this table is the + # only thing that holds it. Three things are asserted: a rule that stops + # covering the tree is as wrong as one that reaches past registry/'s own + # build output, and either pattern satisfies both of those from inside + # registry/.gitignore while emptying the package, so where the rule lives + # is read as well. Ungated, like the sweeps at the top of the daml job: it + # needs nothing but the checkout, so it reports in seconds and cannot be + # silenced by a later narrowing of the gate below. + - name: Verify the build-output ignore rule + run: | + set -euo pipefail + status=0 + # check-ignore answers 0 when a rule matches and 1 when none does, + # and both are results rather than failures, so the status is held + # rather than left to -e, which would end the step at the first + # mismatch and hide every case after it. Anything above 1 means it + # could not answer at all: taking that for "no rule matched" would + # print the second list as passing while git was failing outright. + # None of these paths need to exist; the command matches patterns. + # --no-index because without it check-ignore consults the index and + # calls every TRACKED path unignored whatever the patterns say. That + # would make the registry/src/index.ts case below vacuous, since it is + # the one tracked path here: a rule that swallowed the service's own + # source would still read as a pass. + for path in registry/dist/index.js \ + registry/src/dist/bundle.js \ + registry/test/dist/bundle.js; do + rc=0 + git check-ignore -q --no-index "$path" || rc=$? + case "$rc" in + 0) echo "ok: ${path} is ignored" ;; + 1) echo "${path} is not ignored: build output under registry/ is committable" >&2 + status=1 ;; + *) echo "git check-ignore could not answer for ${path}: status ${rc}" >&2 + status=1 ;; + esac + done + for path in registry/dist-backup/index.js \ + registry/src/index.ts \ + dist/index.js \ + scripts/dist/index.js; do + rc=0 + git check-ignore -q --no-index "$path" || rc=$? + case "$rc" in + 1) echo "ok: ${path} is not ignored" ;; + 0) echo "${path} is ignored: the rule reaches past registry/'s build output" >&2 + status=1 ;; + *) echo "git check-ignore could not answer for ${path}: status ${rc}" >&2 + status=1 ;; + esac + done + # Both loops read what the rule COVERS, and the same patterns cover + # the same tree from inside registry/.gitignore, where npm applies + # them to the pack walk as well: measured at 8 tarball entries rather + # than 24, registry/dist gone and the bin target with it, while all + # seven verdicts above stay green. The smoke test below is the only + # other thing that sees it, and only when the gate lets it run, so the + # file carrying the rule is read here too. -v prints the deciding + # source as `::`; the first field is the whole + # assertion. + rc=0 + match="$(git check-ignore -v --no-index registry/dist/index.js)" || rc=$? + case "$rc" in + 0) ignore_file="${match%%:*}" + if [ "$ignore_file" = ".gitignore" ]; then + echo "ok: the rule ignoring registry/dist lives in the root .gitignore" + else + echo "registry/dist is ignored by ${ignore_file} rather than the root .gitignore: a nested ignore file subtracts it from the npm pack walk" >&2 + status=1 + fi ;; + 1) echo "no rule ignores registry/dist, so there is no placement to read" >&2 + status=1 ;; + *) echo "git check-ignore could not answer for registry/dist/index.js: status ${rc}" >&2 + status=1 ;; + esac + exit "$status" + + - name: Scope + id: scope + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "Not a pull request: verifying unconditionally." + echo "run=true" | tee -a "$GITHUB_OUTPUT" + exit 0 + fi + # A blip on this fetch reds the job having verified nothing, so it is + # retried rather than taken at its word. The step above is ungated and + # sits there so that a red here finds it already run and reported. + for attempt in 1 2 3; do + git fetch --no-tags --prune origin \ + "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" && break + echo "git fetch attempt ${attempt} of 3 failed" >&2 + [ "$attempt" -lt 3 ] || exit 1 + sleep 5 + done + changed="$(git -c core.quotePath=false diff --name-only --no-renames "origin/${BASE_REF}...HEAD")" + echo "Changed files:" + echo "$changed" + # The package's own inputs: the root manifest and lockfile by name, + # the registry ones and every packed source through the registry/ + # prefix, and the two scripts the steps below run. LICENSE and + # README.md are packed too, whatever "files" says, and are left out + # on purpose: no check here or downstream asserts either, so gating + # on them would only lengthen the job. .gitignore is listed to keep + # this enumeration complete rather than for coverage, since the step + # above is ungated and reads that file on every event whether or not + # the gate names it, while the three steps below are blind to the + # root rule. .npmrc is the opposite case and this gate is the only + # thing holding it. It reconfigures npm for every step below rather + # than naming a file any of them read, which is what keeps it out of + # a list written by thinking about inputs: `omit=dev` on its own + # leaves prepare with no compiler, exit 127 before anything builds. + if grep -Eq '^(package\.json$|package-lock\.json$|registry/|scripts/check-registry-deps\.mjs$|scripts/registry-install-smoke\.sh$|\.gitignore$|\.npmrc$|\.github/workflows/ci\.yml$)' <<<"$changed"; then + echo "run=true" | tee -a "$GITHUB_OUTPUT" + else + echo "run=false" | tee -a "$GITHUB_OUTPUT" + fi + + - uses: actions/setup-node@v4 + if: steps.scope.outputs.run == 'true' + with: + node-version: '22' + cache: npm + cache-dependency-path: package-lock.json + + # Ahead of the install it guards, for the reason the daml job places its + # own sweeps first: the script only reads four tracked JSON files, so it + # needs no node_modules and reports in a moment. npm ci is not a second + # reading of this. It reds only where the lockfile can no longer satisfy + # the range beside it, and then with npm's own "Invalid: lock file's + # express@4.22.2 does not satisfy express@5.2.1"; a widened range the + # lockfile still satisfies installs clean, and a disagreement between the + # two trees is invisible to it whatever the ranges say. + - name: Check the manifests agree + if: steps.scope.outputs.run == 'true' + run: npm run check:deps + + # npm ci runs `prepare`, so this compiles registry/src against the ROOT + # dependency set, which is the set a consumer gets. The registry job + # compiles the same source against registry/node_modules; only this one + # would catch a type package that arrives there transitively and is + # declared nowhere. + - name: Install + if: steps.scope.outputs.run == 'true' + run: npm ci + + - name: Install smoke test + if: steps.scope.outputs.run == 'true' + run: npm run smoke:registry diff --git a/.gitignore b/.gitignore index aaef965..8c61bce 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,11 @@ docs/ # Canton sandbox ports/ready file written by scripts/sandbox.sh .canton-ports.json + +# The registry's build output ships inside the npm package, so the rule that +# keeps it out of git lives here rather than beside it: npm applies a NESTED +# ignore file to the pack walk even for a path the root "files" allowlist +# names, while the allowlist does outrank this file. The glob is what makes an +# anchored root rule cover what the unanchored one beside the code covered, a +# dist directory at any depth under registry/, not only the compiler's own. +registry/**/dist diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 575780b..7cef2bf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -12,7 +12,7 @@ | Language | Daml | LF target 2.1 (`build-options: --target=2.1`) | | SDK | Daml SDK 3.4.11 | installed and driven via `dpm` | | Build tool | `dpm` (Digital Asset Package Manager) | not the legacy `daml` assistant (removed as of SDK 3.5) | -| Task runner | `npm` scripts | thin wrappers over `dpm`; set `LANG=C.UTF-8` | +| Task runner | `npm` scripts | the Daml ones wrap `dpm` with `LANG=C.UTF-8`; `prepare` builds `registry/` | | Standard | CIP-0056 (CN Token Standard) | interface-faithful, clean-room (no economics) | | Dependencies | `splice-api-token-*` interface DARs | vendored from `canton-network/splice`; NOT `splice-amulet` | | Testing | `daml-script` (`dpm test`) | lives in the separate `canton-token-forge-test` package | @@ -80,6 +80,8 @@ scripts/ release-notes.sh Emit the release body for a tag, snippet extracted from consumer-smoke/consumer/daml.yaml sandbox.sh Build the DAR and run a local Canton sandbox with the JSON Ledger API seed.mjs Seed a running sandbox with an admin, demo users, and one InstrumentConfig + check-registry-deps.mjs Fail when the root and registry manifests disagree on a dependency + registry-install-smoke.sh Pack the npm package, install it into a scratch consumer, and run the bin deps/ Vendored Splice sources + built DARs (gitignored; never edit or commit) multi-package.yaml Wires the two daml/ packages into one workspace; consumer-smoke/ has its own versions.env Single version knob: SPLICE_TAG @@ -363,14 +365,16 @@ overrides from `SEED_*`/`LEDGER_*` ([`RUNBOOK.md`](RUNBOOK.md)). | Command | Purpose | |---------|---------| -| `npm install` / `npm run setup` | Vendor Splice into `deps/` and create the stable-name symlinks (`scripts/fetch-dep.sh`). | +| `npm run setup` | Vendor Splice into `deps/` and create the stable-name symlinks (`scripts/fetch-dep.sh`). | | `npm run build` | Build both packages (production, then test). | | `npm run build:canton-token-forge` | Build only the production package. | | `npm test` | Build the production DAR, then run the `canton-token-forge-test` suite. | | `npm run test:coverage` | Same as `npm test` with a template-focused coverage report. | | `npm run smoke` | Compile a package that data-depends on nothing but the built DAR (`scripts/consumer-smoke.sh`); proves the release artifact is consumable on its own. | +| `npm run check:deps` | Fail if the root and `registry/` manifests disagree on any dependency. | +| `npm run smoke:registry` | Pack the npm package, install it into a scratch consumer, and run the bin; proves the published service is consumable on its own. | | `bash scripts/release-notes.sh ` | Emit the release body, with the consumer snippet extracted from `consumer-smoke/consumer/daml.yaml`. Refuses if `` does not name the checked-out commit, if the working tree is dirty, or if `deps/` carries no commit stamp (`npm run setup` writes it); `ALLOW_UNTAGGED=1` previews a body before the tag exists ([`RUNBOOK.md`](RUNBOOK.md#cutting-a-release)). | -| `npm run clean` | Remove both `.daml` build dirs and the consumer smoke test's output. | +| `npm run clean` | Remove both `.daml` build dirs, the consumer smoke test's output, and `registry/dist`. | | `npm run sandbox` | Build the DAR and run a local Canton sandbox with the JSON Ledger API. | | `npm run seed` | Seed a running sandbox with an admin, demo users, and one `InstrumentConfig`. | | `bash scripts/build-harness.sh` | Build the Amulet test harness (unused by default; conformance only). | diff --git a/CLAUDE.md b/CLAUDE.md index 8c98f98..ec1282a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,9 +7,11 @@ Claude Code reads this file natively. Other agents (Cursor, Windsurf, etc.) read [`AGENTS.md`](AGENTS.md), which points here. This is a **Daml** project built with **`dpm`**, not a JavaScript project - the -`npm` scripts just wrap `dpm` and vendor dependencies. Generic JS/`npm` -assumptions do not apply here, and this file overrides any parent-directory or -global config that describes generic JS/`npm` workflows. +`npm` scripts mostly wrap `dpm` and vendor dependencies, and the one genuine +JavaScript build among them (`prepare`, which compiles `registry/`) never +touches the Daml side. Generic JS/`npm` assumptions do not apply here, and this +file overrides any parent-directory or global config that describes generic +JS/`npm` workflows. ## What this repo is @@ -33,7 +35,7 @@ Two packages: | Language | Daml | LF target **2.1** (`build-options: --target=2.1`) | | SDK | 3.4.11 | pinned in all three `daml.yaml` files; CI asserts them | | Build tool | `dpm` (Digital Asset Package Manager) | NOT the legacy `daml` assistant (removed as of SDK 3.5) | -| Task runner | `npm` scripts | thin wrappers over `dpm`; they set `LANG=C.UTF-8` | +| Task runner | `npm` scripts | the Daml ones wrap `dpm` with `LANG=C.UTF-8`; `prepare` builds `registry/` | | Dependencies | Splice interface DARs | vendored into `deps/` by `scripts/fetch-dep.sh` (gitignored) | | Runtime | JDK 17+ | required on `PATH` for `dpm` | | Choice naming | `TemplateName_ChoiceName` | matches the CN Token Standard convention | @@ -88,12 +90,18 @@ Use the `package.json` npm scripts - they set `LANG=C.UTF-8` and handle the per-package layout. (damlc regenerates data-dependency interface source and throws "lexical error (UTF-8 decoding error)" under a POSIX/`C` locale.) -### Setup (`npm install`) +### Setup (`npm run setup`) -`postinstall` runs `npm run setup` (= `scripts/fetch-dep.sh`): vendors Splice into -`deps/` and creates the stable-name symlinks for the token interface DARs. -Preconditions: `dpm` + JDK 17+ on `PATH`, `git` + network. First run takes a few -minutes. For deps only, run `bash scripts/fetch-dep.sh`. +`npm run setup` (= `scripts/fetch-dep.sh`) vendors Splice into `deps/` and +creates the stable-name symlinks for the token interface DARs. It needs `git` +and network, and invokes neither `dpm` nor a JVM: the DARs are pre-built +upstream. Run `bash scripts/fetch-dep.sh` directly and it does the same work +without Node. First run writes over 100 MB into `deps/` and took 6 seconds on a +cold CI runner; a slow link will take longer. + +A root `npm install` vendors nothing. It installs the registry service's runtime +dependencies and compiles `registry/src` to `registry/dist` through `prepare`, so +installing this repository needs no `dpm`, no JDK and no clone of Splice. | Command | Does | | --- | --- | @@ -102,7 +110,9 @@ minutes. For deps only, run `bash scripts/fetch-dep.sh`. | `npm test` | Build the `canton-token-forge` DAR, then run the `canton-token-forge-test` suite. | | `npm run test:coverage` | Same, with a coverage report focused on your templates. | | `npm run smoke` | Build the DAR, then compile a package that data-depends on nothing but it, proving the artifact is consumable on its own. | -| `npm run clean` | Remove both `.daml` build dirs and the consumer smoke test's output. | +| `npm run check:deps` | Fail if the root and `registry/` manifests disagree on any dependency. | +| `npm run smoke:registry` | Pack the npm package, install it into a scratch consumer, and run the bin; proves the published service is consumable on its own. | +| `npm run clean` | Remove both `.daml` build dirs, the consumer smoke test's output, and `registry/dist`. | | `npm run setup` | Re-vendor deps + re-create the stable symlinks. | | `npm run sandbox` | Build the DAR and run a local Canton sandbox with the JSON Ledger API. | | `npm run seed` | Seed a running sandbox with an admin, demo users, and one `InstrumentConfig`. | @@ -111,7 +121,9 @@ minutes. For deps only, run `bash scripts/fetch-dep.sh`. `registry/` is a separate npm package with its own dependency tree; the root `npm install` does not populate `registry/node_modules`. Run its commands from -that directory. +that directory. The root manifest is also what builds and ships this service +as an installable package, and `npm run check:deps` is what keeps the two +dependency lists in step. | Command | Does | | --- | --- | @@ -266,31 +278,61 @@ The `/sdlc:issue` skill applies these labels automatically when creating issues Run before declaring work done: -- `npm install` (or `npm run setup`) once, so `deps/` are vendored +- `npm run setup` once, so `deps/` are vendored - `npm run build` - both packages compile - `npm test` - the integration suite passes - `npm run test:coverage` - when you touched or added templates - `npm run smoke` - when you changed what the production DAR exposes: a renamed module or template, its dependencies, its interface instances, or its `build-options` - -Every pull request gets three comparisons whatever it touches, because the -`daml` check runs them ahead of its toolchain install and outside its own -scope gate: the strings that spell a template id, against `daml/`; the -consumer snippet in `README.md`, against `consumer-smoke/consumer/daml.yaml`; -and every tracked `daml.yaml`, against both the SDK version the workflow -installs and LF 2.1. One that touches +- `npm run check:deps` - when you changed a dependency in the root or the + `registry/` manifest; `npm run smoke:registry` never compares the two +- `npm run smoke:registry` - when you changed the root manifest, the + registry's dependencies, or either ignore file + +Every pull request gets three comparisons in the `daml` check whatever it +touches, because that check runs them ahead of its toolchain install and +outside its own scope gate: the strings that spell a template id, against +`daml/`; the consumer snippet in `README.md`, against +`consumer-smoke/consumer/daml.yaml`; and every tracked `daml.yaml`, against +both the SDK version the workflow installs and LF 2.1. One that touches `daml/`, `consumer-smoke/`, `scripts/consumer-smoke.sh` or the root build inputs runs four of these five automatically in that same check, after those -three steps: only `npm run test:coverage` does not. One that touches `registry/` -runs that package's own lint, both typechecks, and unit suite as the -`registry` check, not the root commands above. `npm run test:coverage` and -the registry's `npm run test:e2e` are both off the pull-request path: the -first re-runs Splice's own suites, the second needs a live participant. The +three steps: only `npm run test:coverage` does not. One that touches +`registry/` runs that package's own lint, both typechecks, and unit suite as +the `registry` check, none of the Daml root commands above, and the `package` +check below alongside it. `npm run test:coverage` and the registry's `npm run +test:e2e` are both off the pull-request path: the first re-runs Splice's own +suites, the second needs a live participant. The `release` workflow adds checks of its own that no pull request runs. The end-to-end suite is typechecked on that path even so, which is the point of typechecking it separately from the run. +A third check, `package`, gates on `package.json`, `package-lock.json`, +`registry/`, `scripts/check-registry-deps.mjs`, +`scripts/registry-install-smoke.sh`, `.gitignore`, `.npmrc` and +`.github/workflows/ci.yml` itself, so a pull request touching `registry/` runs +both the `registry` check and this one. It has four verification steps, not +three, and only three of them sit behind that gate: `npm run check:deps`; +`npm ci`, which runs `prepare` and so compiles `registry/src` against the root +dependency set rather than `registry/`'s own, catching a type package that +arrives there transitively and is declared nowhere; and `npm run +smoke:registry`. The fourth is ungated and runs on every trigger of the +workflow regardless of what changed, ahead of the other three exactly as the +`daml` check's own comparisons run ahead of its gate: a `git check-ignore` +table confirming that build output under `registry/` stays out of git, +`registry/dist` included, which must not be committed even though it is the +package's own payload, and that the rule ignoring it lives in the root +`.gitignore` rather than a nested one. Each half catches a mutation the other +cannot see. Narrowing the root rule to `registry/dist` passes the smoke test, +the manifest guard and the placement read alike, while build output under +`registry/src` and `registry/test` silently stops being ignored, so the table +reads what the rule covers. Moving the same pattern into +`registry/.gitignore` leaves that coverage intact but subtracts +`registry/dist` from the npm pack walk, emptying the package to 8 entries from +24; only the smoke test sees that, and only when the gate lets it run, so the +table reads where the rule lives as well. + A pushed tag whose `v` is followed by a digit (`v[0-9]*`, so `vnext` and `vendor` trigger nothing) runs the `release` workflow instead: it refuses a tag `main` does not reach, builds from a clean checkout, runs the suite, diff --git a/README.md b/README.md index fd8f601..9568ed1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ and every holding, and is the same party the registry API reports as its ```bash # 1. Vendor the Splice interface DARs into deps/ (clones canton-network/splice) -npm install +npm run setup # 2. Build the production DAR and run the Daml test suite npm test @@ -30,7 +30,8 @@ allocations, the faucet and burn-mint. The scripts run in-process, so no ledger or sandbox is needed. `registry/` is a separate npm package with its own dependencies: the root -`npm install` vendors the Daml deps and does not populate `registry/node_modules`. +`npm install` does not populate `registry/node_modules`, and vendoring the Daml +deps is a separate `npm run setup`. Its suite runs against an in-process server with a stubbed ledger, so it needs no sandbox either. @@ -67,7 +68,7 @@ and the service look the way they do. contexts a client needs to submit a transfer or an allocation. It submits nothing to the ledger itself. - `scripts/fetch-dep.sh` - vendor Splice into `deps/`, derive versions, - stable-symlink DARs. Run by `npm install`. + stable-symlink DARs. Run by `npm run setup`. - `scripts/sandbox.sh` - build the DAR and run a local Canton sandbox with the JSON Ledger API (`npm run sandbox`). - `scripts/seed.mjs` - seed a running sandbox with an admin, demo users, and one @@ -147,12 +148,101 @@ in both directions, so a flag, target or path that changes on one side reds rather than ships. The release body carries the same blocks, generated straight from that file. +## Consuming the registry service + +The registry service is published from this repository as an npm package, +consumed from git at a tag rather than from the public registry: + +```json +{ + "dependencies": { + "@bootnodedev/canton-token-forge": "github:BootNodeDev/canton-token-forge#v0.2.0" + } +} +``` + +Two preconditions. This repository is private, so the install needs +credentials that can read it: a token-based HTTPS credential helper or an SSH +key for `github.com`, whichever your environment already uses for private git +dependencies. And the participant the service points at must host the +`canton-token-forge` package the configured template ids name: they are checked +at boot and resolve by package name, not by package id, so which release built +the DAR does not matter, and an id the participant cannot resolve stops the +service starting. + +`v0.2.0` is the first tag to carry the npm package, and the tag this section +is written against; until it is cut, pin the commit sha instead. Do not pin +`v0.1.0`: it predates the package, so its manifest is unscoped, declares no +`bin` and no `files`, and runs `scripts/fetch-dep.sh` as a `postinstall`, +which installs no service and clones Splice into your `node_modules`. From +`v0.2.0` on the tags are one namespace, every `v[0-9]*` tag being both a DAR +release and an npm package pin. See "Consuming a release" above for the DAR +itself. + +`npm install` builds `registry/src` through the package's `prepare` script and +links one bin, `canton-token-forge-registry`, unmodified. `pnpm install` +refuses by default: pnpm will not run a git-hosted package's build scripts +unless the consumer allowlists it, and `registry/dist` is gitignored, so +`prepare` is the only thing that produces the bin. Add the resolved git +specifier to `pnpm-workspace.yaml`, using whichever key your pnpm major reads: + +```yaml +# pnpm 11 and later +allowBuilds: + "@bootnodedev/canton-token-forge@git+https://github.com/BootNodeDev/canton-token-forge.git#": true + +# pnpm 10 +onlyBuiltDependencies: + - "@bootnodedev/canton-token-forge@git+https://github.com/BootNodeDev/canton-token-forge.git#" +``` + +The key is the full resolved git specifier, not the bare package name, a +version range, or a wildcard: pnpm resolves the tag to its commit sha and +matches on that exact string. Its own refusal error prints that specifier +unquoted, and pasted that way it does not parse: `@` is a reserved indicator +in YAML, so a plain scalar cannot begin with one, and the parser answers `bad +indentation of a mapping entry`. Quote it as the block above does. The `#` is +harmless either way, since YAML opens a comment only at a `#` that follows +whitespace, and this one sits inside the scalar. Since the sha is resolved +from the tag, this entry changes whenever the pin does. With it present, +`pnpm exec canton-token-forge-registry` runs the same as `npm`'s link. + +The service reads its whole configuration from the environment, plus a `.env` +loaded from the working directory it is started in. Required, eight: +`LEDGER_API_URL`, `LEDGER_API_TOKEN`, `ADMIN_PARTY`, and the five template ids +(`INSTRUMENT_CONFIG_TEMPLATE_ID`, `PREAPPROVAL_TEMPLATE_ID`, +`LOCKED_TOKEN_TEMPLATE_ID`, `TRANSFER_INSTRUCTION_TEMPLATE_ID`, +`ALLOCATION_TEMPLATE_ID`). Optional, four: `PORT`, `LEDGER_USER_ID`, +`SHUTDOWN_TIMEOUT_MS`, `DIRECT_TRANSFER_MARGIN_MS`. The package ships +`registry/.env.example` with the full list and what each variable is for. + +Quote all five template ids in a `.env` file. Every one of them begins with +`#`, which dotenv reads as the start of a comment, so an unquoted +`INSTRUMENT_CONFIG_TEMPLATE_ID=#canton-token-forge:...` parses to the empty +string and the boot rejects it as `missing required env var +INSTRUMENT_CONFIG_TEMPLATE_ID`, naming a variable that is in fact set. Values +passed through the environment rather than a file need no quoting beyond +whatever the shell wants. + +`npm run seed`, run from a clone of this repository against a local sandbox, +prints the whole block filled in and quoted with the real admin party and the +five template ids. `scripts/seed.mjs` is not part of the package, so that is a +step you take in a checkout, not in the consumer. `ADMIN_PARTY` and all five +template ids are checked against the participant at boot, so a value it cannot +resolve stops the service starting rather than failing later. + +What the package contains: `registry/dist`, `registry/openapi` and +`registry/.env.example`, plus `package.json`, `README.md` and `LICENSE` (24 +entries). The manifest that travels with it is this repository's own, so its +`dpm` and `daml/` scripts cannot run from an installed copy; `prepare` is the +only entry npm acts on. + ## Requirements - `dpm` (Digital Asset Package Manager) and a JDK 17+ on `PATH` (`curl https://get.digitalasset.com/install/install.sh | sh`, then `dpm install 3.4.11`). -- Node 18+ for the registry service, its test suites, and the seed script. +- Node 20+ for the registry service, its test suites, and the seed script. - `git` + network access (setup clones `canton-network/splice`). ## Bumping Splice diff --git a/RUNBOOK.md b/RUNBOOK.md index 6e50f0f..2ef7f01 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -17,8 +17,8 @@ been re-probed since. ## Prerequisites - `dpm` and a JDK 17+ on `PATH` (see `CLAUDE.md`) -- `deps/` vendored: `npm install`, or `bash scripts/fetch-dep.sh` for deps only -- Node 18+ for the seed script and the registry service +- `deps/` vendored: `npm run setup`, or `bash scripts/fetch-dep.sh` directly +- Node 20+ for the seed script and the registry service ## 1. Start the sandbox @@ -81,7 +81,8 @@ npm start ``` `registry/` is a separate package with its own dependencies: the root -`npm install` vendors the Daml deps and does not populate `registry/node_modules`. +`npm install` does not populate `registry/node_modules`, and vendoring the Daml +deps is a separate `npm run setup`. `GET /healthz` and `GET /readyz` answer, `GET /registry/metadata/v1/info` returns the admin party as `adminId` with the six supported APIs, and @@ -310,6 +311,23 @@ reason the seed script looks the way it does. ## Cutting a release +Every `v[0-9]*` tag is a DAR release, including the tag a consumer's +`package.json` pins the npm package at (see the README's "Consuming the +registry service"). `daml/canton-token-forge/daml.yaml` stays at `0.0.1` +whatever tag is cut, so every release attaches an asset under the same name, +`canton-token-forge-0.0.1.dar`. Whether two releases' assets are the same +bytes does not follow from that version string: it follows from `daml/`, +`versions.env` and the SDK being unchanged between them. Nothing under those +paths has moved since `v0.1.0`, so a tag cut from this commit would carry the +sha256 the `v0.1.0` body records; read it off the workflow's own output rather +than assuming it, as steps 1 and 4 below do. + +The root `package.json`'s `version` is the npm package's, and nothing checks +it against the tag: `release.yml` reads only `daml/canton-token-forge/daml.yaml`. +Bring it in step with the tag you are about to cut, in a commit merged to +`main` before you tag, or consumers install a package whose manifest names a +version the pin does not. + `.github/workflows/release.yml` builds and publishes. It runs the full suite, checks the DAR is byte-reproducible, and compiles `consumer-smoke/` against the built artifact before anything is published. The release body, including the @@ -326,7 +344,7 @@ CI just proved compile. with it the comparison against the checked-out commit that only a real tag reaches, so the body it emits names the dispatched ref. To rehearse all three, push a - hyphenated tag such as `v0.1.0-rc1` first: + hyphenated tag such as `v0.2.0-rc1` first: the workflow marks any hyphenated tag as a pre-release, so it does not become the release that `/releases/latest` serves. Tag a commit that is already on `main`, for the rehearsal as much as for the real thing: the @@ -346,7 +364,7 @@ CI just proved compile. one step meant to exercise the downloaded one. ```bash - gh release download v0.1.0-rc1 --pattern '*.dar' --dir /tmp/rc + gh release download v0.2.0-rc1 --pattern '*.dar' --dir /tmp/rc shasum -a 256 /tmp/rc/canton-token-forge-0.0.1.dar # must match the body rm -rf consumer-smoke/consumer/vendor consumer-smoke/consumer/.daml mkdir -p consumer-smoke/consumer/vendor @@ -356,15 +374,18 @@ CI just proved compile. ``` Then `npm run clean`, and delete that release and its tag. -2. Tag and push. This is the decision that matters: a downstream repository pins +2. Set the root `package.json`'s `version` to the tag without its `v`, and land + that on `main`. Nothing enforces this, and the tag is what a consumer's + `package.json` resolves to. +3. Tag and push. This is the decision that matters: a downstream repository pins it permanently. ```bash - git tag v0.1.0 - git push origin v0.1.0 + git tag v0.2.0 + git push origin v0.2.0 ``` -3. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body +4. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body shows the sha256 and package-id. The download-and-compile from step 1 is worth repeating here, against the real tag. @@ -393,7 +414,7 @@ Two environment variables tune this path: run. Preview a body locally with: ```bash - ALLOW_UNTAGGED=1 bash scripts/release-notes.sh v0.1.0 + ALLOW_UNTAGGED=1 bash scripts/release-notes.sh v0.2.0 ``` This one refuses on a dirty working tree, untracked files included, because diff --git a/SPEC.md b/SPEC.md index 1b26f54..f85e2d3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -63,8 +63,15 @@ resulting exercise itself over the JSON Ledger API, forwarding the service's ### Size and status -976 lines of production Daml, 2508 lines of Daml tests, 1724 lines of TypeScript -service, 4343 lines of TypeScript tests. MIT licensed. Pre-release: the package +976 lines of production Daml, 2508 lines of Daml tests, 1739 lines of +TypeScript service, 4349 lines of TypeScript tests, each figure a +`find -name '*.daml'` (or `'*.ts'`) `| xargs wc -l` count over +`daml/canton-token-forge/daml`, `daml/canton-token-forge-test/daml`, +`registry/src` and `registry/test` respectively. The two Daml paths name the +source directory rather than the package root on purpose: a package root that +has been built also holds a `.daml/` build directory, whose name the `*.daml` +glob matches and whose regenerated data-dependency sources dwarf the figure +above. MIT licensed. Pre-release: the Daml package version is `0.0.1`, and the build is published as release `v0.1.0` for downstream repositories to pin (the tag is deliberately decoupled from the package version), with no compatibility guarantee offered across releases. @@ -310,7 +317,7 @@ holding for any surplus, so no value is created or destroyed. ## 6. Registry HTTP service -A TypeScript service (Express, `express-openapi-validator`, pino; Node 18+) that +A TypeScript service (Express, `express-openapi-validator`, pino; Node 20+) that validates incoming requests against the four CN Token Standard OpenAPI specs it ships. Responses are covered by the unit suite rather than by runtime schema validation. The service is **read-only**: it queries the JSON Ledger API for @@ -457,7 +464,7 @@ instrument, then prints a ready-to-paste service configuration. ## 8. Running it ```bash -npm install # vendors the Splice interface DARs into deps/ +npm run setup # vendors the Splice interface DARs into deps/ npm test # builds the production DAR, runs 80 Daml scenarios cd registry && npm install && npm test # 205 unit tests, no ledger needed @@ -470,7 +477,7 @@ The sandbox runs in the foreground, so the seed and the end-to-end suite go in a second shell. The end-to-end suite creates everything it needs, so seeding is only required if you also want to drive the service by hand. -Requirements: `dpm` and a JDK 17+ on `PATH` for the Daml build, Node 18+ for the +Requirements: `dpm` and a JDK 17+ on `PATH` for the Daml build, Node 20+ for the service and its suites, and `git` plus network access for the initial vendoring. --- @@ -578,21 +585,36 @@ Stated plainly, because they are what an evaluation turns on. ship a new interface version that compiles green while the smoke package still names the old one, and compiling that package is what catches it. One that touches `registry/` runs that package's lint, its typechecks and - the registry unit suite as the `registry` check. The end-to-end suite - needs a live participant, so only its types are checked there and it is - never run. Off that path too is `npm run test:coverage`, which re-runs - Splice's own suites and which nothing runs automatically. A pull request - in the `daml` check's scope compiles the smoke package, generates the - release body, which is the check that compares the published snippet - against the artifact, and asserts that every tracked manifest still pins - the SDK and targets LF 2.1. The release workflow still carries the rebuild - that proves the DAR is byte-reproducible, the refusal of a tag `main` does - not reach, and `release-notes.sh`'s tag guard, which the pull-request path - waives with `ALLOW_UNTAGGED` so that a body can be generated for a ref that - is not a tag. -- **Pre-release.** Version `0.0.1`, with the build published as release `v0.1.0` - for downstream repositories to pin (the tag is deliberately decoupled from the - package version). No migration story and no compatibility guarantees. + the registry unit suite as the `registry` check. The same change also runs + a third job, `package`. Three of its four steps are gated on `registry/` + among other inputs: they compare the root and `registry/` manifests, compile + `registry/src` a second time against the root dependency set rather than + `registry/`'s own by running `npm ci` (which triggers `prepare`), then pack + the npm package and install it into a scratch consumer to run the bin. The + `registry` check's own install and build see neither of those last two. The + fourth step is ungated and runs on every trigger of the workflow, a + docs-only pull request included: it reads the `.gitignore` rules that keep + build output under `registry/` out of git, and reads which file carries + them. The end-to-end suite needs a live participant, so only its types are + checked there and it is never run. Off that path too is `npm run + test:coverage`, which re-runs Splice's own suites and which nothing runs + automatically. A pull request in the `daml` check's scope compiles the smoke + package, generates the release body, which is the check that compares the + published snippet against the artifact, and asserts that every tracked + manifest still pins the SDK and targets LF 2.1. The release workflow still + carries the rebuild that proves the DAR is byte-reproducible, the refusal + of a tag `main` does not reach, and `release-notes.sh`'s tag guard, which + the pull-request path waives with `ALLOW_UNTAGGED` so that a body can be + generated for a ref that is not a tag. +- **Pre-release.** Version `0.2.0` of the npm package, `0.0.1` of the Daml + package. The Daml version is deliberately decoupled from the release tags; + the npm version is brought in step with the tag by hand before it is cut, + since nothing checks the two against each other. Those tags are + one namespace rather than two: every `v[0-9]*` tag publishes the DAR as a + release asset, and from `v0.2.0` on the same tag is what a consumer's + `package.json` pins the npm package at. `v0.1.0` is the only tag cut so far + and predates the npm package, so it serves the DAR alone. No migration story + and no compatibility guarantees. --- @@ -624,5 +646,7 @@ scripts/ release-notes.sh Emit the release body from the smoke package sandbox.sh Local Canton sandbox with the JSON Ledger API seed.mjs Seed an admin, demo users and one instrument + check-registry-deps.mjs Fail when the root and registry manifests disagree + registry-install-smoke.sh Pack, install and run the npm package versions.env The single version knob: SPLICE_TAG ``` diff --git a/package-lock.json b/package-lock.json index a8d75c2..3f586f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,1316 @@ { - "name": "canton-token-forge", - "version": "0.0.1", + "name": "@bootnodedev/canton-token-forge", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "canton-token-forge", - "version": "0.0.1", - "hasInstallScript": true, - "license": "MIT" + "name": "@bootnodedev/canton-token-forge", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-openapi-validator": "^5.6.2", + "pino": "^10.3.1" + }, + "bin": { + "canton-token-forge-registry": "registry/dist/index.js" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^26.1.1", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", + "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "peer": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-openapi-validator": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.6.2.tgz", + "integrity": "sha512-fkDn4+ImUC4HTJ1g0cek/ItqYhmEO19AglJd2Iw2OJco0jLIbxIlDGVazmXbvvYeziU4Bnah2h+S2tb6NtWg8w==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^14.2.1", + "@types/multer": "^2.0.0", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "json-schema-traverse": "^1.0.0", + "lodash.clonedeep": "^4.5.0", + "lodash.get": "^4.4.2", + "media-typer": "^1.1.0", + "multer": "^2.0.2", + "ono": "^7.1.3", + "path-to-regexp": "^8.3.0", + "qs": "^6.14.1" + }, + "peerDependencies": { + "express": "*" + } + }, + "node_modules/express-openapi-validator/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead." + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-9jnfVriq7uJM4o5ganUY54ntUm+5EK21EGaQ5NWnkWg3zz5ywbbonlBguRcnmF1/HDiIe3zxNxXcO1YPBmPcQQ==", + "dependencies": { + "@jsdevtools/ono": "7.1.3" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } } } } diff --git a/package.json b/package.json index c1f32ca..ebbc59e 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,36 @@ { - "name": "canton-token-forge", - "version": "0.0.1", - "description": "A reusable, multi-instrument CIP-0056 (CN Token Standard) compliant token for demos and sandboxes", + "name": "@bootnodedev/canton-token-forge", + "version": "0.2.0", + "description": "A reusable, multi-instrument CIP-0056 (CN Token Standard) compliant token for demos and sandboxes, plus its read-only registry HTTP service", "license": "MIT", - "type": "commonjs", + "type": "module", "scripts": { "setup": "bash scripts/fetch-dep.sh", - "postinstall": "npm run setup", - "clean": "rm -rf daml/canton-token-forge/.daml daml/canton-token-forge-test/.daml consumer-smoke/consumer/.daml consumer-smoke/consumer/vendor", + "clean": "rm -rf daml/canton-token-forge/.daml daml/canton-token-forge-test/.daml consumer-smoke/consumer/.daml consumer-smoke/consumer/vendor registry/dist", "smoke": "bash scripts/consumer-smoke.sh", + "smoke:registry": "bash scripts/registry-install-smoke.sh", + "check:deps": "node scripts/check-registry-deps.mjs", "build": "cd daml/canton-token-forge && LANG=C.UTF-8 dpm build && cd ../canton-token-forge-test && LANG=C.UTF-8 dpm build", "build:canton-token-forge": "cd daml/canton-token-forge && LANG=C.UTF-8 dpm build", "sandbox": "bash scripts/sandbox.sh", "seed": "node scripts/seed.mjs", "test": "npm run build:canton-token-forge && cd daml/canton-token-forge-test && LANG=C.UTF-8 dpm test", - "test:coverage": "npm run build:canton-token-forge && cd daml/canton-token-forge-test && LANG=C.UTF-8 dpm test --all --show-coverage --coverage-ignore-choice '^splice' --coverage-ignore-choice ':Archive$'" + "test:coverage": "npm run build:canton-token-forge && cd daml/canton-token-forge-test && LANG=C.UTF-8 dpm test --all --show-coverage --coverage-ignore-choice '^splice' --coverage-ignore-choice ':Archive$'", + "prepare": "rm -rf registry/dist && tsc -p registry/tsconfig.json" + }, + "repository": "github:BootNodeDev/canton-token-forge", + "engines": { "node": ">=20" }, + "bin": { "canton-token-forge-registry": "registry/dist/index.js" }, + "files": ["registry/dist", "registry/openapi", "registry/.env.example"], + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-openapi-validator": "^5.6.2", + "pino": "^10.3.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^26.1.1", + "typescript": "^5.5.4" } } diff --git a/registry/.gitignore b/registry/.gitignore index af45a21..327f74d 100644 --- a/registry/.gitignore +++ b/registry/.gitignore @@ -1,5 +1,4 @@ node_modules -dist .env .env.* !.env.example diff --git a/registry/package-lock.json b/registry/package-lock.json index c279de4..2f5659f 100644 --- a/registry/package-lock.json +++ b/registry/package-lock.json @@ -16,6 +16,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.10", "@types/express": "^4.17.21", + "@types/node": "^26.1.1", "@types/supertest": "^6.0.2", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", @@ -26,7 +27,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@apidevtools/json-schema-ref-parser": { diff --git a/registry/package.json b/registry/package.json index 8740926..f0fec3d 100644 --- a/registry/package.json +++ b/registry/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": ">=18" + "node": ">=20" }, "scripts": { "build": "tsc -p tsconfig.json", @@ -25,6 +25,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.10", "@types/express": "^4.17.21", + "@types/node": "^26.1.1", "@types/supertest": "^6.0.2", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", diff --git a/registry/src/index.ts b/registry/src/index.ts index 2ba4429..649ae2a 100644 --- a/registry/src/index.ts +++ b/registry/src/index.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import 'dotenv/config' import { type Config, loadConfig } from './config.js' import { HttpLedgerClient } from './ledger.js' diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs new file mode 100644 index 0000000..491cdd1 --- /dev/null +++ b/scripts/check-registry-deps.mjs @@ -0,0 +1,215 @@ +#!/usr/bin/env node +// +// check-registry-deps.mjs - the root package.json ships registry/dist as its +// bin, so a consumer install resolves the service's imports against the root +// dependency list, while every test suite that vetted that code ran against +// registry/package.json's list. The two are deliberate duplicates, and this +// guard fails on every way they come apart: a runtime dependency declared on +// one side only, one manifest naming a package at two ranges, a shared package +// at two ranges across the two, an engines.node or type that differs or is +// absent from both, a shared package the two lockfiles resolve differently or +// that one lockfile does not record at all, and a lockfile that no longer +// records the manifest beside it, which is the state an edit to a range leaves +// behind. + +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')) + +const rootManifest = readJson(resolve(repoRoot, 'package.json')) +const registryManifest = readJson(resolve(repoRoot, 'registry/package.json')) +const rootLock = readJson(resolve(repoRoot, 'package-lock.json')) +const registryLock = readJson(resolve(repoRoot, 'registry/package-lock.json')) + +const SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] + +// A package can be a runtime dep on one side and a devDependency on the +// other, so ranges are compared regardless of section. Every occurrence is +// kept rather than the last one seen: a name declared in two sections of one +// manifest would otherwise shadow itself, and the surviving entry can agree +// across the two files while the shadowed one drifts. +function rangesBySection(manifest) { + const bySection = new Map() + for (const section of SECTIONS) { + for (const [name, range] of Object.entries(manifest[section] ?? {})) { + const occurrences = bySection.get(name) ?? [] + occurrences.push({ section, range }) + bySection.set(name, occurrences) + } + } + return bySection +} + +const rootRanges = rangesBySection(rootManifest) +const registryRanges = rangesBySection(registryManifest) + +const failures = [] + +// Object.hasOwn rather than `in` at every membership test below: these objects +// come from JSON.parse, so they carry Object.prototype, and `constructor` and +// `toString` are both real published package names. `in` answers true for them +// whatever the manifest says, which would pass a package declared on one side +// only, the exact drift this script exists to catch. +for (const name of Object.keys(rootManifest.dependencies ?? {})) { + if (!Object.hasOwn(registryManifest.dependencies ?? {}, name)) { + failures.push( + `${name} is a runtime dependency of package.json but is not in registry/package.json's "dependencies"; no suite installs it. Add it to registry/package.json.`, + ) + } +} + +for (const name of Object.keys(registryManifest.dependencies ?? {})) { + if (!Object.hasOwn(rootManifest.dependencies ?? {}, name)) { + failures.push( + `${name} is a runtime dependency of registry/package.json but is not in the root "dependencies"; a consumer install would not resolve it. Add it to package.json.`, + ) + } +} + +const sharedNames = [...rootRanges.keys()].filter((name) => registryRanges.has(name)).sort() + +// A manifest naming one package at two ranges is incoherent on its own, and +// reporting that as a cross-manifest disagreement would point at the wrong file. +for (const [file, ranges] of [ + ['package.json', rootRanges], + ['registry/package.json', registryRanges], +]) { + for (const [name, [first, ...rest]] of ranges) { + for (const other of rest) { + if (other.range !== first.range) { + failures.push( + `${name} is "${first.range}" in ${file}'s "${first.section}" and "${other.range}" in its "${other.section}"; a manifest cannot name one package at two ranges, so drop one of them.`, + ) + } + } + } +} + +for (const name of sharedNames) { + for (const root of rootRanges.get(name)) { + for (const registry of registryRanges.get(name)) { + if (root.range !== registry.range) { + failures.push( + `${name} is "${root.range}" in package.json ("${root.section}") and "${registry.range}" in registry/package.json ("${registry.section}"); make the two ranges identical.`, + ) + } + } + } +} + +// Absence on both sides is a disagreement with npm's defaults rather than +// between the two files: an omitted "type" means commonjs, which the compiled +// ESM bin cannot be loaded under, and an omitted floor lets a consumer install +// on a runtime the closure does not support. +const FIELDS = [ + { label: 'engines.node', read: (manifest) => manifest.engines?.node }, + { label: 'type', read: (manifest) => manifest.type }, +] + +for (const { label, read } of FIELDS) { + const rootValue = read(rootManifest) + const registryValue = read(registryManifest) + if (rootValue !== registryValue) { + failures.push( + `${label} is ${JSON.stringify(rootValue)} in package.json and ${JSON.stringify(registryValue)} in registry/package.json; make the two identical.`, + ) + } else if (rootValue === undefined) { + failures.push(`${label} is missing from both package.json and registry/package.json; set it in both.`) + } +} + +// registry/ is not a workspace of the root package: each tree is installed from +// its own directory, so a root npm install leaves registry/package-lock.json +// exactly as it found it. +const ROOT_INSTALL = 'npm install' +const REGISTRY_INSTALL = 'npm install in registry/' + +let resolvedMatches = 0 +for (const name of sharedNames) { + const rootEntry = rootLock.packages?.[`node_modules/${name}`] + const registryEntry = registryLock.packages?.[`node_modules/${name}`] + if (!rootEntry) { + failures.push( + `${name} is declared in both manifests but has no "node_modules/${name}" entry in package-lock.json; run ${ROOT_INSTALL} to refresh it.`, + ) + } + if (!registryEntry) { + failures.push( + `${name} is declared in both manifests but has no "node_modules/${name}" entry in registry/package-lock.json; run ${REGISTRY_INSTALL} to refresh it.`, + ) + } + if (!rootEntry || !registryEntry) continue + if (rootEntry.version !== registryEntry.version) { + // A plain npm install keeps any locked resolution that still satisfies the + // range, so it leaves this untouched. Deleting one lockfile does not settle + // it either: that tree re-resolves to the newest version its range allows, + // which matches the other tree only by luck. Naming the version instead + // (npm install pkg@version) converges but rewrites the manifest range. + failures.push( + `${name} resolves to ${rootEntry.version} in package-lock.json and ${registryEntry.version} in registry/package-lock.json; npm install keeps a resolution that still satisfies the range, so delete both lockfiles and rebuild them together: ${ROOT_INSTALL}, then ${REGISTRY_INSTALL}.`, + ) + continue + } + resolvedMatches += 1 +} + +// Making two ranges identical satisfies the range rule while leaving each +// lockfile recording the range it was generated from, and npm ci refuses a tree +// in that state. Comparing the root entry npm writes into every lockfile against +// the manifest beside it is the cheap half of what npm ci validates. +const TREES = [ + { + manifest: rootManifest, + manifestFile: 'package.json', + lock: rootLock, + lockFile: 'package-lock.json', + install: ROOT_INSTALL, + }, + { + manifest: registryManifest, + manifestFile: 'registry/package.json', + lock: registryLock, + lockFile: 'registry/package-lock.json', + install: REGISTRY_INSTALL, + }, +] + +for (const { manifest, manifestFile, lock, lockFile, install } of TREES) { + const recorded = lock.packages?.[''] ?? {} + for (const section of SECTIONS) { + const declared = manifest[section] ?? {} + const locked = recorded[section] ?? {} + for (const [name, range] of Object.entries(declared)) { + if (locked[name] !== range) { + const recordedRange = + locked[name] === undefined ? 'does not record it' : `records "${locked[name]}"` + failures.push( + `${name} is "${range}" in ${manifestFile}'s "${section}" but ${lockFile} ${recordedRange}; run ${install} to bring the lockfile up to date.`, + ) + } + } + for (const name of Object.keys(locked)) { + if (!Object.hasOwn(declared, name)) { + failures.push( + `${lockFile} still records ${name} in "${section}" but ${manifestFile} no longer declares it; run ${install} to bring the lockfile up to date.`, + ) + } + } + } +} + +if (failures.length > 0) { + console.error('the root and registry/ packages disagree:') + for (const failure of failures) { + console.error(` - ${failure}`) + } + process.exit(1) +} + +console.log( + `manifests agree: ${sharedNames.length} packages named in both carry identical ranges, ${resolvedMatches} resolve to the same version in both lockfiles, engines.node and type match, and each lockfile records the manifest beside it`, +) diff --git a/scripts/fetch-dep.sh b/scripts/fetch-dep.sh index 3a684ab..aa507bb 100755 --- a/scripts/fetch-dep.sh +++ b/scripts/fetch-dep.sh @@ -74,9 +74,9 @@ else # a release body needs the commit. Refusing here would mean the fallback # aborts for precisely the reason it was written - a broken git transport # to this host - and every contributor on such a network would find - # `npm install` failing where it used to work. The stamp records what is - # known, and scripts/release-notes.sh is what refuses to publish without - # the rest. + # `npm run setup` failing where it used to work. The stamp records what + # is known, and scripts/release-notes.sh is what refuses to publish + # without the rest. if [ -z "$splice_commit" ]; then echo "warning: could not resolve the commit for ${SPLICE_TAG}" >&2 echo " deps/ will still be vendored; scripts/release-notes.sh will refuse" >&2 diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh new file mode 100755 index 0000000..34d2f17 --- /dev/null +++ b/scripts/registry-install-smoke.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +set -euo pipefail + +# registry-install-smoke.sh - pack the repository, install the tarball into a +# scratch consumer, and run the bin that install links. +# +# This is the npm counterpart of `npm run smoke`, which proves the same thing +# about the DAR: it is the only check here that exercises what a consumer +# actually receives. What it guards is what a green build cannot see: a file +# `files` failed to pack, a spec that ships but does not parse, a module system +# the package cannot be loaded under, a bin pointing at nothing. +# +# It does NOT establish that the root manifest declares every runtime import. +# `express` is a peer dependency of `express-openapi-validator`, so npm installs +# it at the consumer's top level and the service runs whether or not the root +# names it; comparing the two manifests is `npm run check:deps`'s job. +# +# No participant is needed. The boot fails only for a fault it can attribute to +# our own configuration, so an unreachable ledger warns and continues, and +# /healthz answers without touching it. +# +# Usage: +# npm run smoke:registry +# +# Requires a root `npm install` first, since npm pack runs prepare and prepare +# needs tsc, and network for the consumer install. Rewrites registry/dist as a +# side effect, which `npm run clean` removes. + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +work="$(mktemp -d)" +server_pid="" + +cleanup() { + if [ -n "$server_pid" ] && kill -0 "$server_pid" 2>/dev/null; then + kill -TERM "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + rm -rf "$work" +} +trap cleanup EXIT +# A signal has to end the run rather than return into it, because by then +# cleanup has removed the work directory the next line would read. Exiting here +# lets the EXIT trap do the one cleanup, with the conventional signal status. +trap 'exit 130' INT +trap 'exit 143' TERM + +fail() { echo "smoke: $*" >&2; exit 1; } + +# Print a captured body, ending it with a newline whether or not it has one, so +# the smoke: line that follows starts a line of its own. A JSON error body and a +# log a crash cut off mid-write both arrive without a trailing newline. +dump() { + if [ -s "$1" ]; then + cat "$1" >&2 + [ -z "$(tail -c 1 "$1")" ] || echo >&2 + fi +} + +# A port nothing is listening on. Asking the kernel for one and closing it +# immediately races with anything else on the machine, which is why the closed +# port is only ever connected TO and the served port is asserted by polling. +free_port() { + node -e 'const net = require("node:net"); const s = net.createServer(); s.listen(0, "127.0.0.1", () => { const p = s.address().port; s.close(() => console.log(p)) })' +} + +echo "smoke: packing ${repo_root}" +# npm pack runs `prepare`, so the tarball carries a build made from the source +# in this tree rather than whatever registry/dist happened to hold. +# A compile error in prepare is the likeliest way this whole check fails, so +# the status is held rather than left to set -e, which would end the run here +# with nothing said. --silent is deliberately not passed: it silences the +# prepare script too, which is where the compiler names the file and the line. +# Which stream carries that depends on the npm version. npm 9 keeps everything +# but the tarball name on stderr; npm 10 runs prepare in the foreground and +# writes its banner, and a failing compiler's output, to stdout. So both streams +# are captured, both are printed on failure, and the name is the LAST line of +# stdout rather than the whole of it. +set +e +( cd "$repo_root" && npm pack --pack-destination "$work" ) \ + >"${work}/pack.out" 2>"${work}/pack.err" +pack_status=$? +set -e +if [ "$pack_status" -ne 0 ]; then + dump "${work}/pack.out" + dump "${work}/pack.err" + fail "npm pack failed with exit ${pack_status}" +fi +tarball_name="$(tail -n 1 "${work}/pack.out")" +tarball="${work}/${tarball_name}" +[ -f "$tarball" ] || fail "npm pack produced no tarball at ${tarball}" + +# The bin and the OpenAPI specs are the two things `files` can silently drop: +# a nested .gitignore outranks the root allowlist for a path inside it, and the +# validator reads its spec lazily, so a spec left out of the tarball is a 500 +# on the first request rather than a boot failure. Both are asserted here on +# the archive itself, before anything installs it. +listing="$(tar tzf "$tarball")" +for entry in \ + package/registry/dist/index.js \ + package/registry/openapi/token-metadata-v1.yaml \ + package/registry/openapi/transfer-instruction-v1.yaml \ + package/registry/openapi/allocation-v1.yaml \ + package/registry/openapi/allocation-instruction-v1.yaml +do + grep -qxF "$entry" <<<"$listing" || fail "the tarball carries no ${entry#package/}" +done + +# The consumer lives outside the repository so npm resolves against its own +# manifest instead of walking up into ours. +consumer="${work}/consumer" +mkdir -p "$consumer" +cat > "${consumer}/package.json" <<'JSON' +{ + "name": "registry-install-smoke-consumer", + "version": "0.0.0", + "private": true +} +JSON + +echo "smoke: installing ${tarball_name}" +# --silent is left off for the same reason as the pack above, and here it is +# npm's own error that it would suppress: a silenced install failure prints +# nothing at all, on either stream. This is the step the network prerequisite +# can fail, so the status is held and the captured output printed. +set +e +( cd "$consumer" && npm install --no-audit --no-fund "$tarball" ) \ + >"${work}/install.log" 2>&1 +install_status=$? +set -e +if [ "$install_status" -ne 0 ]; then + dump "${work}/install.log" + fail "installing the tarball failed with exit ${install_status}" +fi + +bin="${consumer}/node_modules/.bin/canton-token-forge-registry" +[ -x "$bin" ] || fail "the install linked no executable bin at ${bin}" + +echo "smoke: running with no configuration" +# The logger writes to stdout, so the streams are joined rather than asserted +# on stderr, where nothing would ever appear. +set +e +# index.ts loads dotenv/config, which reads $PWD/.env: run from the consumer +# directory so this asserts on a clean environment instead of whatever .env +# happens to sit in the caller's own working directory. env -i clears every +# inherited variable so a LEDGER_API_URL exported outside this script can't +# shift the failure past the one asserted below. +no_config_output="$( cd "$consumer" && env -i PATH="$PATH" "$bin" 2>&1 )" +no_config_status=$? +set -e +[ "$no_config_status" -eq 1 ] \ + || fail "expected exit 1 with no configuration, got ${no_config_status}" +case "$no_config_output" in + *"missing required env var LEDGER_API_URL"*) ;; + *) fail "expected the missing LEDGER_API_URL message, got: ${no_config_output}" ;; +esac + +echo "smoke: running against an unreachable participant" +serve_port="$(free_port)" +dead_port="$(free_port)" +# Same $PWD/.env concern as the no-config run above, and env -i for the same +# reason: passing the configuration through it makes these variables the only +# ones the service sees, so an optional one exported in the caller's shell +# (SHUTDOWN_TIMEOUT_MS, NODE_OPTIONS) cannot change what this run tests. +# +# Each id is spelled in full rather than built from a shared prefix. Nothing +# here resolves them: no participant answers, so every boot probe reports the +# question unanswered and warns, and a run configured with ids naming no +# package at all is just as green. What reads them is the workflow's ungated +# "Verify the hardcoded template ids" sweep, which finds a file by grepping +# for the module prefix followed by a dot. Built from a variable, that dot sat +# on the interpolated side, so this file matched nothing, appeared in neither +# of the sweep's two lists, and its exhaustiveness comparison stayed green +# while five ids went unchecked here. +( cd "$consumer" && exec env -i \ +PATH="$PATH" \ +LEDGER_API_URL="http://127.0.0.1:${dead_port}" \ +LEDGER_API_TOKEN=smoke \ +ADMIN_PARTY='admin::1220smoke' \ +INSTRUMENT_CONFIG_TEMPLATE_ID='#canton-token-forge:Canton.TokenForge.Registry:InstrumentConfig' \ +TRANSFER_INSTRUCTION_TEMPLATE_ID='#canton-token-forge:Canton.TokenForge.Instruction:TokenTransferInstruction' \ +PREAPPROVAL_TEMPLATE_ID='#canton-token-forge:Canton.TokenForge.Registry:TokenTransferPreapproval' \ +LOCKED_TOKEN_TEMPLATE_ID='#canton-token-forge:Canton.TokenForge.Locked:LockedToken' \ +ALLOCATION_TEMPLATE_ID='#canton-token-forge:Canton.TokenForge.Allocation:TokenAllocation' \ +PORT="${serve_port}" \ + "$bin" ) > "${work}/server.log" 2>&1 & +server_pid=$! + +health="" +for _ in $(seq 1 60); do + if ! kill -0 "$server_pid" 2>/dev/null; then + dump "${work}/server.log" + fail "the service exited before it listened" + fi + health="$(curl -sf "http://127.0.0.1:${serve_port}/healthz" || true)" + [ -n "$health" ] && break + sleep 0.5 +done +[ -n "$health" ] || { dump "${work}/server.log"; fail "no 200 from /healthz on port ${serve_port}"; } +case "$health" in + *'"status":"ok"'*) ;; + *) fail "unexpected /healthz body: ${health}" ;; +esac + +# /healthz is served before any validator, so it says nothing about the specs. +# /registry/metadata/v1/info is the cheapest request that passes through one of +# them and answers from configuration alone, so it needs no ledger: it is 200 +# with the specs shipped and 500 ("spec could not be read") without them. +# || true so a service that died between the poll above and this request is +# reported by the status check below (curl writes 000 and exits non-zero on a +# refused connection) rather than aborting the run silently through set -e. +info_status="$(curl -s -o "${work}/info.json" -w '%{http_code}' \ + "http://127.0.0.1:${serve_port}/registry/metadata/v1/info" || true)" +[ "$info_status" = "200" ] \ + || { dump "${work}/info.json"; fail "expected 200 from /registry/metadata/v1/info, got ${info_status}"; } + +echo "smoke: terminating" +# A service that died between serving the two requests above and this line is a +# real failure, and an unguarded kill would report it as set -e ending the run +# on bash's own "no such process" rather than as something this check saw. +if ! kill -TERM "$server_pid" 2>/dev/null; then + dump "${work}/server.log" + fail "the service was already gone when the run asked it to shut down" +fi +set +e +wait "$server_pid" +shutdown_status=$? +set -e +server_pid="" +[ "$shutdown_status" -eq 0 ] \ + || fail "expected a clean exit on SIGTERM, got ${shutdown_status}" + +echo "smoke: ok (${tarball_name} installs, refuses an empty environment, serves /healthz and the metadata API, and shuts down cleanly)"