From f0b8d4a1bf334d415067319660f836c25eb9faba Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 19:09:12 +0000 Subject: [PATCH 01/63] chore: build the registry on install instead of vendoring the Daml deps --- package-lock.json | 1300 +++++++++++++++++++++++++++++++++++- package.json | 15 +- registry/package-lock.json | 1 + registry/package.json | 1 + 4 files changed, 1313 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8d75c2..d4c22d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,1304 @@ "": { "name": "canton-token-forge", "version": "0.0.1", - "hasInstallScript": true, - "license": "MIT" + "license": "MIT", + "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" + } + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "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..c6eddfb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "type": "commonjs", "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", "smoke": "bash scripts/consumer-smoke.sh", "build": "cd daml/canton-token-forge && LANG=C.UTF-8 dpm build && cd ../canton-token-forge-test && LANG=C.UTF-8 dpm build", @@ -14,6 +13,18 @@ "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": "tsc -p registry/tsconfig.json" + }, + "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/package-lock.json b/registry/package-lock.json index c279de4..915d31f 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", diff --git a/registry/package.json b/registry/package.json index 8740926..933cf66 100644 --- a/registry/package.json +++ b/registry/package.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", From f8ee6903ae0bcf24ebb5b8ee81c1378a4ead4843 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 19:12:48 +0000 Subject: [PATCH 02/63] docs: name npm run setup where the docs credited postinstall --- ARCHITECTURE.md | 2 +- CLAUDE.md | 16 ++++++++++------ README.md | 7 ++++--- RUNBOOK.md | 5 +++-- SPEC.md | 2 +- scripts/fetch-dep.sh | 6 +++--- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 575780b..75e3131 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -363,7 +363,7 @@ 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. | diff --git a/CLAUDE.md b/CLAUDE.md index 8c98f98..f158215 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,12 +88,16 @@ 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. Preconditions: +`dpm` + JDK 17+ on `PATH`, `git` + network. First run takes a few minutes. For +deps only, run `bash scripts/fetch-dep.sh`. + +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 | | --- | --- | @@ -266,7 +270,7 @@ 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 diff --git a/README.md b/README.md index fd8f601..9c02268 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 diff --git a/RUNBOOK.md b/RUNBOOK.md index 6e50f0f..7728e92 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -17,7 +17,7 @@ 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 +- `deps/` vendored: `npm run setup`, or `bash scripts/fetch-dep.sh` directly - Node 18+ 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 diff --git a/SPEC.md b/SPEC.md index 1b26f54..0244a46 100644 --- a/SPEC.md +++ b/SPEC.md @@ -457,7 +457,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 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 From 5639fd787188cc9c6702d2f6a9536e9d56bc92cb Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 19:52:26 +0000 Subject: [PATCH 03/63] chore: remove registry/dist in npm run clean --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6eddfb..cd1752e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "commonjs", "scripts": { "setup": "bash scripts/fetch-dep.sh", - "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", "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", From 93477b9d1b85b20885b2421c3a37c6d61f47b06c Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 19:52:26 +0000 Subject: [PATCH 04/63] chore: hold the root @types/node at the version the registry resolves --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4c22d6..af6cb48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,9 +111,9 @@ } }, "node_modules/@types/node": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", - "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "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" } From 9b94000408dd32ec19ade479212af90b256934f1 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 19:52:26 +0000 Subject: [PATCH 05/63] docs: correct the setup preconditions and what the npm scripts do --- ARCHITECTURE.md | 4 ++-- CLAUDE.md | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 75e3131..8de5f7c 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 | wrap `dpm` and set `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 | @@ -370,7 +370,7 @@ overrides from `SEED_*`/`LEDGER_*` ([`RUNBOOK.md`](RUNBOOK.md)). | `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. | | `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 f158215..3866c0b 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 | wrap `dpm` and set `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 | @@ -91,9 +93,11 @@ throws "lexical error (UTF-8 decoding error)" under a POSIX/`C` locale.) ### Setup (`npm run setup`) `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`. +creates the stable-name symlinks for the token interface DARs. It needs `git` +and network and nothing else: the DARs are pre-built upstream, so the script +invokes neither `dpm` nor a JVM. Run `bash scripts/fetch-dep.sh` directly and it +does the same work without Node. It writes over 100 MB into `deps/`; the wait is +mostly network (6 seconds on a cold CI runner). A root `npm install` vendors nothing. It installs the registry service's runtime dependencies and compiles `registry/src` to `registry/dist` through `prepare`, so @@ -106,7 +110,7 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice. | `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 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`. | From 3e4c44664c941f1851fd66494b8906387e85565a Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 19:53:03 +0000 Subject: [PATCH 06/63] docs: drop an absolute and an unmeasured claim from the setup paragraph --- ARCHITECTURE.md | 2 +- CLAUDE.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8de5f7c..ae798fe 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 | wrap `dpm` and set `LANG=C.UTF-8`; `prepare` builds `registry/` | +| 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 | diff --git a/CLAUDE.md b/CLAUDE.md index 3866c0b..0b2f629 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,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 | wrap `dpm` and set `LANG=C.UTF-8`; `prepare` builds `registry/` | +| 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 | @@ -94,10 +94,10 @@ throws "lexical error (UTF-8 decoding error)" under a POSIX/`C` locale.) `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 nothing else: the DARs are pre-built upstream, so the script -invokes neither `dpm` nor a JVM. Run `bash scripts/fetch-dep.sh` directly and it -does the same work without Node. It writes over 100 MB into `deps/`; the wait is -mostly network (6 seconds on a cold CI runner). +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 From ca9ebca664572aa2c0611ec5a047c35af9932ddc Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 20:07:49 +0000 Subject: [PATCH 07/63] feat: publish the registry as the root package's bin --- .gitignore | 6 ++++++ package-lock.json | 14 ++++++++++---- package.json | 10 +++++++--- registry/.gitignore | 1 - registry/src/index.ts | 1 + 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index aaef965..97163a6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ 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. +registry/dist diff --git a/package-lock.json b/package-lock.json index af6cb48..d89dfc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "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", + "name": "@bootnodedev/canton-token-forge", + "version": "0.2.0", "license": "MIT", "dependencies": { "dotenv": "^16.4.5", @@ -14,10 +14,16 @@ "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": ">=18" } }, "node_modules/@apidevtools/json-schema-ref-parser": { diff --git a/package.json b/package.json index cd1752e..b387cd3 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "canton-token-forge", - "version": "0.0.1", + "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", "license": "MIT", - "type": "commonjs", + "type": "module", "scripts": { "setup": "bash scripts/fetch-dep.sh", "clean": "rm -rf daml/canton-token-forge/.daml daml/canton-token-forge-test/.daml consumer-smoke/consumer/.daml consumer-smoke/consumer/vendor registry/dist", @@ -16,6 +16,10 @@ "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": "tsc -p registry/tsconfig.json" }, + "repository": "github:BootNodeDev/canton-token-forge", + "engines": { "node": ">=18" }, + "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", 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/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' From ec8124f7ccd072cf40f94083f272ece81a17994e Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 20:28:59 +0000 Subject: [PATCH 08/63] fix: raise the node floor to 20, which two runtime dependencies require --- README.md | 2 +- RUNBOOK.md | 2 +- SPEC.md | 4 ++-- package-lock.json | 2 +- package.json | 2 +- registry/package-lock.json | 2 +- registry/package.json | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9c02268..99e31e9 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ from that file. - `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 7728e92..57a8264 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -18,7 +18,7 @@ been re-probed since. - `dpm` and a JDK 17+ on `PATH` (see `CLAUDE.md`) - `deps/` vendored: `npm run setup`, or `bash scripts/fetch-dep.sh` directly -- Node 18+ for the seed script and the registry service +- Node 20+ for the seed script and the registry service ## 1. Start the sandbox diff --git a/SPEC.md b/SPEC.md index 0244a46..ea885a1 100644 --- a/SPEC.md +++ b/SPEC.md @@ -310,7 +310,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 @@ -470,7 +470,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. --- diff --git a/package-lock.json b/package-lock.json index d89dfc4..3f586f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "typescript": "^5.5.4" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@apidevtools/json-schema-ref-parser": { diff --git a/package.json b/package.json index b387cd3..9d58499 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "prepare": "tsc -p registry/tsconfig.json" }, "repository": "github:BootNodeDev/canton-token-forge", - "engines": { "node": ">=18" }, + "engines": { "node": ">=20" }, "bin": { "canton-token-forge-registry": "registry/dist/index.js" }, "files": ["registry/dist", "registry/openapi", "registry/.env.example"], "dependencies": { diff --git a/registry/package-lock.json b/registry/package-lock.json index 915d31f..2f5659f 100644 --- a/registry/package-lock.json +++ b/registry/package-lock.json @@ -27,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 933cf66..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", From 7d04a6c240bdd0af1944c079c1946caa75b8000f Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 20:29:25 +0000 Subject: [PATCH 09/63] fix: keep a dist at any depth under registry ignored --- .gitignore | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 97163a6..8c61bce 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,7 @@ docs/ # 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. -registry/dist +# 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 From 1fc58160148e2c3ffea779eced1d0e1f6df732b8 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:14:07 +0000 Subject: [PATCH 10/63] feat: fail when the root and registry manifests disagree on a dependency --- package.json | 1 + scripts/check-registry-deps.mjs | 117 ++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 scripts/check-registry-deps.mjs diff --git a/package.json b/package.json index 9d58499..71084a8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "setup": "bash scripts/fetch-dep.sh", "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", + "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", diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs new file mode 100644 index 0000000..c5472ac --- /dev/null +++ b/scripts/check-registry-deps.mjs @@ -0,0 +1,117 @@ +#!/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: this +// guard fails when they drift apart, in either the declared ranges or the +// versions each lockfile actually resolved. + +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; the section is kept +// only to name where each range was found in a message. +function rangesBySection(manifest) { + const bySection = new Map() + for (const section of SECTIONS) { + for (const [name, range] of Object.entries(manifest[section] ?? {})) { + bySection.set(name, { section, range }) + } + } + return bySection +} + +const rootRanges = rangesBySection(rootManifest) +const registryRanges = rangesBySection(registryManifest) + +const failures = [] + +for (const name of Object.keys(rootManifest.dependencies ?? {})) { + if (!(name in (registryManifest.dependencies ?? {}))) { + 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 (!(name in (rootManifest.dependencies ?? {}))) { + 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() + +for (const name of sharedNames) { + const root = rootRanges.get(name) + const registry = 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.`, + ) + } +} + +for (const field of ['node', 'type']) { + const rootValue = field === 'node' ? rootManifest.engines?.node : rootManifest.type + const registryValue = field === 'node' ? registryManifest.engines?.node : registryManifest.type + const label = field === 'node' ? 'engines.node' : 'type' + 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.`, + ) + } +} + +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 npm install to refresh it.`, + ) + continue + } + if (!registryEntry) { + failures.push( + `${name} is declared in both manifests but has no "node_modules/${name}" entry in registry/package-lock.json; run npm install to refresh it.`, + ) + continue + } + if (rootEntry.version !== registryEntry.version) { + failures.push( + `${name} resolves to ${rootEntry.version} in package-lock.json and ${registryEntry.version} in registry/package-lock.json; regenerate one lockfile so both trees run the same code.`, + ) + continue + } + resolvedMatches += 1 +} + +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`, +) From f28172676bfcc16f6cf0b8524b75b1b544222b50 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:40:51 +0000 Subject: [PATCH 11/63] fix: compare every section a package is declared in, not the last one seen --- scripts/check-registry-deps.mjs | 41 +++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index c5472ac..1582dbd 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -23,13 +23,17 @@ 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; the section is kept -// only to name where each range was found in a message. +// 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] ?? {})) { - bySection.set(name, { section, range }) + const occurrences = bySection.get(name) ?? [] + occurrences.push({ section, range }) + bySection.set(name, occurrences) } } return bySection @@ -58,13 +62,32 @@ for (const name of Object.keys(registryManifest.dependencies ?? {})) { 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.`, + ) + } + } + } +} + for (const name of sharedNames) { - const root = rootRanges.get(name) - const registry = 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.`, - ) + 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.`, + ) + } + } } } From 3d3eb6ac5b9e3b30ece62b974eede3dd1bcb77f8 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:41:07 +0000 Subject: [PATCH 12/63] fix: fail when engines.node or type is absent from both manifests --- scripts/check-registry-deps.mjs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 1582dbd..38d70ce 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -91,14 +91,24 @@ for (const name of sharedNames) { } } -for (const field of ['node', 'type']) { - const rootValue = field === 'node' ? rootManifest.engines?.node : rootManifest.type - const registryValue = field === 'node' ? registryManifest.engines?.node : registryManifest.type - const label = field === 'node' ? 'engines.node' : 'type' +// 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.`) } } From d2cadd454175bcea1e75d7ad8d534be07488ff39 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:42:20 +0000 Subject: [PATCH 13/63] fix: name the operation that actually converges two lockfiles --- scripts/check-registry-deps.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 38d70ce..9ae1cb1 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -129,8 +129,13 @@ for (const name of sharedNames) { continue } if (rootEntry.version !== registryEntry.version) { + // A plain npm install keeps any locked resolution that still satisfies the + // range, so it leaves this untouched; rebuilding the lockfile from scratch + // is what moves it. Naming the version instead (npm install pkg@version) + // also rewrites the manifest range, and when neither tree holds the newest + // version the ranges allow, both lockfiles have to be rebuilt. failures.push( - `${name} resolves to ${rootEntry.version} in package-lock.json and ${registryEntry.version} in registry/package-lock.json; regenerate one lockfile so both trees run the same code.`, + `${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 the lockfile you are correcting and reinstall.`, ) continue } From 31273a25ccafc9391bc1e9417db93ed539604027 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:42:29 +0000 Subject: [PATCH 14/63] fix: report both lockfiles when neither records a shared package --- scripts/check-registry-deps.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 9ae1cb1..0b465f9 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -120,14 +120,13 @@ for (const name of sharedNames) { failures.push( `${name} is declared in both manifests but has no "node_modules/${name}" entry in package-lock.json; run npm install to refresh it.`, ) - continue } if (!registryEntry) { failures.push( `${name} is declared in both manifests but has no "node_modules/${name}" entry in registry/package-lock.json; run npm install to refresh it.`, ) - continue } + 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; rebuilding the lockfile from scratch From c423634f0f3829d7be989b511ff3d6170e2ed516 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:43:00 +0000 Subject: [PATCH 15/63] feat: fail when a lockfile no longer records the manifest beside it --- scripts/check-registry-deps.mjs | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 0b465f9..2724157 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -141,6 +141,47 @@ for (const name of sharedNames) { resolvedMatches += 1 } +// Making two ranges identical satisfies the rule above 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', + }, + { + manifest: registryManifest, + manifestFile: 'registry/package.json', + lock: registryLock, + lockFile: 'registry/package-lock.json', + }, +] + +for (const { manifest, manifestFile, lock, lockFile } 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) { + failures.push( + `${name} is "${range}" in ${manifestFile}'s "${section}" but ${lockFile} records ${JSON.stringify(locked[name])}; run npm install to bring the lockfile up to date.`, + ) + } + } + for (const name of Object.keys(locked)) { + if (!(name in declared)) { + failures.push( + `${lockFile} still records ${name} in "${section}" but ${manifestFile} no longer declares it; run npm 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) { @@ -150,5 +191,5 @@ if (failures.length > 0) { } 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`, + `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`, ) From 7724fd9e98d12a4ad34f451b72b0df6ad996dc5b Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:43:24 +0000 Subject: [PATCH 16/63] fix: say a lockfile does not record a package instead of printing undefined --- scripts/check-registry-deps.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 2724157..cb8fe9c 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -167,8 +167,10 @@ for (const { manifest, manifestFile, lock, lockFile } of TREES) { 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} records ${JSON.stringify(locked[name])}; run npm install to bring the lockfile up to date.`, + `${name} is "${range}" in ${manifestFile}'s "${section}" but ${lockFile} ${recordedRange}; run npm install to bring the lockfile up to date.`, ) } } From 0a61363d5641d4b6ae1ebf092b9abb1d9de66f6f Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Wed, 2 Sep 2026 21:48:42 +0000 Subject: [PATCH 17/63] docs: describe every rule the guard enforces in its header comment --- scripts/check-registry-deps.mjs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index cb8fe9c..97c73fd 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -3,9 +3,12 @@ // 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: this -// guard fails when they drift apart, in either the declared ranges or the -// versions each lockfile actually resolved. +// registry/package.json's list. The two are deliberate duplicates, and this +// guard fails on four ways they come apart: a runtime dependency declared on +// one side only, a shared package at two ranges, a mismatched engines.node or +// type, and a package the two lockfiles resolve differently. It also fails +// when a lockfile stops recording the manifest beside it, which is the state +// an edit to one of the ranges leaves behind. import { readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' @@ -72,7 +75,7 @@ for (const [file, ranges] of [ 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.`, + `${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.`, ) } } @@ -141,7 +144,7 @@ for (const name of sharedNames) { resolvedMatches += 1 } -// Making two ranges identical satisfies the rule above while leaving each +// 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. From e2e28ff395cd7cbec6e2f15e6f8166997ab35ced Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 11:47:02 +0000 Subject: [PATCH 18/63] fix: name the directory each lockfile is refreshed from --- scripts/check-registry-deps.mjs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 97c73fd..598649e 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -115,18 +115,24 @@ for (const { label, read } of FIELDS) { } } +// 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 npm install to refresh it.`, + `${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 npm install to refresh it.`, + `${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 @@ -154,16 +160,18 @@ const TREES = [ 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 } of TREES) { +for (const { manifest, manifestFile, lock, lockFile, install } of TREES) { const recorded = lock.packages?.[''] ?? {} for (const section of SECTIONS) { const declared = manifest[section] ?? {} @@ -173,14 +181,14 @@ for (const { manifest, manifestFile, lock, lockFile } of TREES) { 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 npm install to bring the lockfile up to date.`, + `${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 (!(name in declared)) { failures.push( - `${lockFile} still records ${name} in "${section}" but ${manifestFile} no longer declares it; run npm install to bring the lockfile up to date.`, + `${lockFile} still records ${name} in "${section}" but ${manifestFile} no longer declares it; run ${install} to bring the lockfile up to date.`, ) } } From a04d0617e06acba788538423d665d8a83eaf8500 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 11:48:09 +0000 Subject: [PATCH 19/63] fix: rebuild both lockfiles, since deleting one does not converge --- scripts/check-registry-deps.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 598649e..a8d9d53 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -138,12 +138,12 @@ for (const name of sharedNames) { 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; rebuilding the lockfile from scratch - // is what moves it. Naming the version instead (npm install pkg@version) - // also rewrites the manifest range, and when neither tree holds the newest - // version the ranges allow, both lockfiles have to be rebuilt. + // 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 the lockfile you are correcting and reinstall.`, + `${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 } From 87ed925a6bfdb66be0dd9eea150fa39c7a9136b4 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 11:48:26 +0000 Subject: [PATCH 20/63] docs: add the three rules the header comment left out --- scripts/check-registry-deps.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index a8d9d53..2e194fd 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -4,11 +4,13 @@ // 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 four ways they come apart: a runtime dependency declared on -// one side only, a shared package at two ranges, a mismatched engines.node or -// type, and a package the two lockfiles resolve differently. It also fails -// when a lockfile stops recording the manifest beside it, which is the state -// an edit to one of the ranges leaves behind. +// 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' From a752abd944e4209b2e1dd7d02f643315fa9e7355 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 12:18:05 +0000 Subject: [PATCH 21/63] test: prove the packed registry installs and serves --- package.json | 1 + scripts/registry-install-smoke.sh | 153 ++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100755 scripts/registry-install-smoke.sh diff --git a/package.json b/package.json index 71084a8..db02882 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "setup": "bash scripts/fetch-dep.sh", "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", diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh new file mode 100755 index 0000000..4320759 --- /dev/null +++ b/scripts/registry-install-smoke.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Proves the npm package a consumer installs is complete and runnable: the +# tarball carries the built service and the OpenAPI specs it reads at boot, the +# root manifest declares every runtime import the bin makes, and the linked bin +# starts a server. This is the npm counterpart of `npm run smoke`, which proves +# the same thing about the DAR. +# +# 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. + +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 + +fail() { echo "smoke: $*" >&2; exit 1; } + +# 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. +tarball_name="$(cd "$repo_root" && npm pack --silent --pack-destination "$work")" +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}" +( cd "$consumer" && npm install --silent --no-audit --no-fund "$tarball" ) + +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)" +prefix='#canton-token-forge:Canton.TokenForge' +# Same $PWD/.env concern as the no-config run above; the explicit env +# assignments below are the only configuration this run gets regardless. +# exec is a special builtin, so a VAR=val ahead of it is an argument to exec +# itself rather than an environment assignment for what it execs; the +# assignments have to precede exec, not follow it. +( cd "$consumer" && \ +LEDGER_API_URL="http://127.0.0.1:${dead_port}" \ +LEDGER_API_TOKEN=smoke \ +ADMIN_PARTY='admin::1220smoke' \ +INSTRUMENT_CONFIG_TEMPLATE_ID="${prefix}.Registry:InstrumentConfig" \ +TRANSFER_INSTRUCTION_TEMPLATE_ID="${prefix}.Instruction:TokenTransferInstruction" \ +PREAPPROVAL_TEMPLATE_ID="${prefix}.Registry:TokenTransferPreapproval" \ +LOCKED_TOKEN_TEMPLATE_ID="${prefix}.Locked:LockedToken" \ +ALLOCATION_TEMPLATE_ID="${prefix}.Allocation:TokenAllocation" \ +PORT="${serve_port}" \ + exec "$bin" ) > "${work}/server.log" 2>&1 & +server_pid=$! + +health="" +for _ in $(seq 1 60); do + if ! kill -0 "$server_pid" 2>/dev/null; then + cat "${work}/server.log" >&2 + 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" ] || { cat "${work}/server.log" >&2; 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. +info_status="$(curl -s -o "${work}/info.json" -w '%{http_code}' \ + "http://127.0.0.1:${serve_port}/registry/metadata/v1/info")" +[ "$info_status" = "200" ] \ + || { cat "${work}/info.json" >&2; fail "expected 200 from /registry/metadata/v1/info, got ${info_status}"; } + +echo "smoke: terminating" +kill -TERM "$server_pid" +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)" From 6ccb95080bf4eeb6c82b48de7ba784f4380cb333 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 12:38:52 +0000 Subject: [PATCH 22/63] fix: report a failed pack instead of ending the run at the pack line --- scripts/registry-install-smoke.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 4320759..8cde2ae 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -36,7 +36,20 @@ free_port() { 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. -tarball_name="$(cd "$repo_root" && npm pack --silent --pack-destination "$work")" +# 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. +# npm keeps its own output on stderr, so stdout is the tarball name alone, and +# the notice listing is discarded on the path that succeeds. +set +e +tarball_name="$(cd "$repo_root" && npm pack --pack-destination "$work" 2>"${work}/pack.err")" +pack_status=$? +set -e +if [ "$pack_status" -ne 0 ]; then + cat "${work}/pack.err" >&2 + fail "npm pack failed with exit ${pack_status}" +fi tarball="${work}/${tarball_name}" [ -f "$tarball" ] || fail "npm pack produced no tarball at ${tarball}" From 3c8a5c4e1031a4a8e4581715fd0d7a013648ecdc Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 12:40:37 +0000 Subject: [PATCH 23/63] fix: give the serving run the same clean environment as the no-config run --- scripts/registry-install-smoke.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 8cde2ae..4ca4a48 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -110,12 +110,12 @@ echo "smoke: running against an unreachable participant" serve_port="$(free_port)" dead_port="$(free_port)" prefix='#canton-token-forge:Canton.TokenForge' -# Same $PWD/.env concern as the no-config run above; the explicit env -# assignments below are the only configuration this run gets regardless. -# exec is a special builtin, so a VAR=val ahead of it is an argument to exec -# itself rather than an environment assignment for what it execs; the -# assignments have to precede exec, not follow it. -( cd "$consumer" && \ +# 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. +( 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' \ @@ -125,7 +125,7 @@ PREAPPROVAL_TEMPLATE_ID="${prefix}.Registry:TokenTransferPreapproval" \ LOCKED_TOKEN_TEMPLATE_ID="${prefix}.Locked:LockedToken" \ ALLOCATION_TEMPLATE_ID="${prefix}.Allocation:TokenAllocation" \ PORT="${serve_port}" \ - exec "$bin" ) > "${work}/server.log" 2>&1 & + "$bin" ) > "${work}/server.log" 2>&1 & server_pid=$! health="" From 062b2cd45ebd87f57119c11c2c0158392cbc1646 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 12:41:39 +0000 Subject: [PATCH 24/63] fix: clean up on an interrupt instead of leaving a server and a temp dir --- scripts/registry-install-smoke.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 4ca4a48..34abb32 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -23,6 +23,11 @@ cleanup() { 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; } From 4a77882dcfaeb3f753282f6e53a355b658d9dd6c Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 12:41:58 +0000 Subject: [PATCH 25/63] fix: name a service that died before the shutdown assertion could run --- scripts/registry-install-smoke.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 34abb32..8364406 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -159,7 +159,13 @@ info_status="$(curl -s -o "${work}/info.json" -w '%{http_code}' \ || { cat "${work}/info.json" >&2; fail "expected 200 from /registry/metadata/v1/info, got ${info_status}"; } echo "smoke: terminating" -kill -TERM "$server_pid" +# 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 + cat "${work}/server.log" >&2 + fail "the service was already gone when the run asked it to shut down" +fi set +e wait "$server_pid" shutdown_status=$? From a5be060e29ebbf91ba39e4e3540ae68f13b50111 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 12:42:53 +0000 Subject: [PATCH 26/63] docs: say what the check establishes, what it does not, and what it needs --- scripts/registry-install-smoke.sh | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 8364406..bbcdd0e 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -1,15 +1,30 @@ #!/usr/bin/env bash set -euo pipefail -# Proves the npm package a consumer installs is complete and runnable: the -# tarball carries the built service and the OpenAPI specs it reads at boot, the -# root manifest declares every runtime import the bin makes, and the linked bin -# starts a server. This is the npm counterpart of `npm run smoke`, which proves -# the same thing about the DAR. +# 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)" From 7b47b560acd97559d166c4003e59fb2357b8cae8 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 13:21:01 +0000 Subject: [PATCH 27/63] fix: print a captured body without gluing the next line onto it --- scripts/registry-install-smoke.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index bbcdd0e..3ece049 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -46,6 +46,16 @@ 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. @@ -67,7 +77,7 @@ tarball_name="$(cd "$repo_root" && npm pack --pack-destination "$work" 2>"${work pack_status=$? set -e if [ "$pack_status" -ne 0 ]; then - cat "${work}/pack.err" >&2 + dump "${work}/pack.err" fail "npm pack failed with exit ${pack_status}" fi tarball="${work}/${tarball_name}" @@ -151,14 +161,14 @@ server_pid=$! health="" for _ in $(seq 1 60); do if ! kill -0 "$server_pid" 2>/dev/null; then - cat "${work}/server.log" >&2 + 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" ] || { cat "${work}/server.log" >&2; fail "no 200 from /healthz on port ${serve_port}"; } +[ -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}" ;; @@ -171,14 +181,14 @@ esac info_status="$(curl -s -o "${work}/info.json" -w '%{http_code}' \ "http://127.0.0.1:${serve_port}/registry/metadata/v1/info")" [ "$info_status" = "200" ] \ - || { cat "${work}/info.json" >&2; fail "expected 200 from /registry/metadata/v1/info, got ${info_status}"; } + || { 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 - cat "${work}/server.log" >&2 + dump "${work}/server.log" fail "the service was already gone when the run asked it to shut down" fi set +e From 8b485637ae9730db4c2d26be18b25db936207a7d Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 13:21:09 +0000 Subject: [PATCH 28/63] fix: read the tarball name from the last line, since npm 10 shares stdout --- scripts/registry-install-smoke.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 3ece049..196b96d 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -70,16 +70,22 @@ echo "smoke: packing ${repo_root}" # 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. -# npm keeps its own output on stderr, so stdout is the tarball name alone, and -# the notice listing is discarded on the path that succeeds. +# 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 -tarball_name="$(cd "$repo_root" && npm pack --pack-destination "$work" 2>"${work}/pack.err")" +( 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}" From 8539831153007b8362853945b67a73fd74c27192 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 13:21:15 +0000 Subject: [PATCH 29/63] fix: report why the consumer install failed instead of silencing npm --- scripts/registry-install-smoke.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 196b96d..e50e8e7 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -118,7 +118,19 @@ cat > "${consumer}/package.json" <<'JSON' JSON echo "smoke: installing ${tarball_name}" -( cd "$consumer" && npm install --silent --no-audit --no-fund "$tarball" ) +# --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}" From b94fcb5f2d1d0ca038a205b6e57c42792aff448d Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 13:21:23 +0000 Subject: [PATCH 30/63] fix: report a service that died before the metadata request --- scripts/registry-install-smoke.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index e50e8e7..0457393 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -196,8 +196,11 @@ esac # /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")" + "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}"; } From f4337708bec0e71cab2eff2801222e98bdffafb8 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 17:39:19 +0000 Subject: [PATCH 31/63] ci: check the npm package installs and the manifests agree --- .github/workflows/ci.yml | 124 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b548b4d..777b949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -603,3 +603,127 @@ 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. Both halves are asserted: a rule that stops + # covering the tree is as wrong as one that reaches past registry/'s own + # build output. 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 + # git check-ignore exits 1 for a path no rule matches, which under -e + # would end the step at the first mismatch and hide every case after + # it. None of these paths need to exist: the command matches patterns. + for path in registry/dist/index.js \ + registry/src/dist/bundle.js \ + registry/test/dist/bundle.js; do + if git check-ignore -q "$path"; then + echo "ok: ${path} is ignored" + else + echo "${path} is not ignored: build output under registry/ is committable" >&2 + status=1 + fi + done + for path in registry/dist-backup/index.js \ + registry/src/index.ts \ + dist/index.js \ + scripts/dist/index.js; do + if git check-ignore -q "$path"; then + echo "${path} is ignored: the rule reaches past registry/'s build output" >&2 + status=1 + else + echo "ok: ${path} is not ignored" + fi + done + 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: both manifests, both lockfiles by way of + # package-lock.json, everything the tarball carries, and the two + # scripts the steps below run. .gitignore is listed because it is in + # neither other gate, so a pull request narrowing the build-output + # rule would otherwise report two green checks having looked at + # nothing; the step above is what reads it once this job is here. + if grep -Eq '^(package\.json$|package-lock\.json$|registry/|scripts/check-registry-deps\.mjs$|scripts/registry-install-smoke\.sh$|\.gitignore$|\.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 + + # 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: Check the manifests agree + if: steps.scope.outputs.run == 'true' + run: npm run check:deps + + - name: Install smoke test + if: steps.scope.outputs.run == 'true' + run: npm run smoke:registry From c660316f5dabd0bbba00ea1f7a2c23b4ad317970 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 17:54:12 +0000 Subject: [PATCH 32/63] fix: read the ignore rules without the index, which hides tracked paths --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 777b949..15a5aed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -644,10 +644,15 @@ jobs: # git check-ignore exits 1 for a path no rule matches, which under -e # would end the step at the first mismatch and hide every case after # it. 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 - if git check-ignore -q "$path"; then + if git check-ignore -q --no-index "$path"; then echo "ok: ${path} is ignored" else echo "${path} is not ignored: build output under registry/ is committable" >&2 @@ -658,7 +663,7 @@ jobs: registry/src/index.ts \ dist/index.js \ scripts/dist/index.js; do - if git check-ignore -q "$path"; then + if git check-ignore -q --no-index "$path"; then echo "${path} is ignored: the rule reaches past registry/'s build output" >&2 status=1 else From 11e0ae02b9e5c6f177174802de2c37149dfca200 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 17:54:45 +0000 Subject: [PATCH 33/63] fix: run the manifest guard before the install it guards --- .github/workflows/ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15a5aed..5d044f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -716,6 +716,16 @@ jobs: 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. A lockfile that has + # drifted from the manifest beside it reds npm ci too, but with npm's + # generic "package.json and package-lock.json are not in sync", and every + # finding this names, on either side, would go unprinted. + - 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 @@ -725,10 +735,6 @@ jobs: if: steps.scope.outputs.run == 'true' run: npm ci - - name: Check the manifests agree - if: steps.scope.outputs.run == 'true' - run: npm run check:deps - - name: Install smoke test if: steps.scope.outputs.run == 'true' run: npm run smoke:registry From 1b0bd10f87e3f6460a764a40b399797dd53d808f Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 17:55:34 +0000 Subject: [PATCH 34/63] fix: report a check-ignore that could not answer instead of passing it --- .github/workflows/ci.yml | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d044f7..79d6771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -641,9 +641,13 @@ jobs: run: | set -euo pipefail status=0 - # git check-ignore exits 1 for a path no rule matches, which under -e - # would end the step at the first mismatch and hide every case after - # it. None of these paths need to exist: the command matches patterns. + # 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 @@ -652,23 +656,29 @@ jobs: for path in registry/dist/index.js \ registry/src/dist/bundle.js \ registry/test/dist/bundle.js; do - if git check-ignore -q --no-index "$path"; then - echo "ok: ${path} is ignored" - else - echo "${path} is not ignored: build output under registry/ is committable" >&2 - status=1 - fi + 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 - if git check-ignore -q --no-index "$path"; then - echo "${path} is ignored: the rule reaches past registry/'s build output" >&2 - status=1 - else - echo "ok: ${path} is not ignored" - fi + 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 exit "$status" From 964b35dbe768cbc552dfbcc96c34078aee274748 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 17:56:04 +0000 Subject: [PATCH 35/63] ci: gate on the npmrc that reconfigures every step below --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79d6771..4a009cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -713,7 +713,12 @@ jobs: # neither other gate, so a pull request narrowing the build-output # rule would otherwise report two green checks having looked at # nothing; the step above is what reads it once this job is here. - if grep -Eq '^(package\.json$|package-lock\.json$|registry/|scripts/check-registry-deps\.mjs$|scripts/registry-install-smoke\.sh$|\.gitignore$|\.github/workflows/ci\.yml$)' <<<"$changed"; then + # .npmrc is listed for that same reason, and neither gate holds it + # either. 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, which is 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" From f4cf64541fe52fd4e9725f389b3fc4a86061a0fb Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 18:31:43 +0000 Subject: [PATCH 36/63] ci: assert where the build-output ignore rule lives, not only what it covers --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a009cc..8d7332f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -632,9 +632,11 @@ jobs: # 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. Both halves are asserted: a rule that stops + # 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. Ungated, like the sweeps at the top of the daml job: it + # 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 @@ -680,6 +682,30 @@ jobs: 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 From b6b5ed8d0158279f94fafb8113f7ac639a604902 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 18:31:54 +0000 Subject: [PATCH 37/63] docs: correct the gate's input list and why .gitignore sits in it --- .github/workflows/ci.yml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d7332f..f1d2ff4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -733,17 +733,20 @@ jobs: 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: both manifests, both lockfiles by way of - # package-lock.json, everything the tarball carries, and the two - # scripts the steps below run. .gitignore is listed because it is in - # neither other gate, so a pull request narrowing the build-output - # rule would otherwise report two green checks having looked at - # nothing; the step above is what reads it once this job is here. - # .npmrc is listed for that same reason, and neither gate holds it - # either. 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, which is exit 127 before anything builds. + # 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 From 5a6113116e10e7883a54c88fdef825fb5e3a2c0f Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 18:32:03 +0000 Subject: [PATCH 38/63] docs: state what npm ci actually refuses, since it is not this --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1d2ff4..eb4f4c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -762,10 +762,12 @@ jobs: # 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. A lockfile that has - # drifted from the manifest beside it reds npm ci too, but with npm's - # generic "package.json and package-lock.json are not in sync", and every - # finding this names, on either side, would go unprinted. + # 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 From c247e84b25681a40e878ac3290b9a0225e62e6d2 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:03:32 +0000 Subject: [PATCH 39/63] docs: document consuming the registry as an npm dependency --- ARCHITECTURE.md | 2 ++ CLAUDE.md | 26 ++++++++++++++++++- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ RUNBOOK.md | 6 +++++ SPEC.md | 29 ++++++++++++++++------ package.json | 2 +- 6 files changed, 121 insertions(+), 10 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ae798fe..d4c546e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -369,6 +369,8 @@ overrides from `SEED_*`/`LEDGER_*` ([`RUNBOOK.md`](RUNBOOK.md)). | `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, 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. | diff --git a/CLAUDE.md b/CLAUDE.md index 0b2f629..2623a36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,6 +110,8 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice. | `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 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. | @@ -119,7 +121,9 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice. `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 | | --- | --- | @@ -281,6 +285,8 @@ Run before declaring work done: - `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` +- `npm run smoke:registry` - when you changed the root manifest, the + registry's dependencies, or either ignore file Every pull request gets three comparisons whatever it touches, because the `daml` check runs them ahead of its toolchain install and outside its own @@ -299,6 +305,24 @@ first re-runs Splice's own suites, the second needs a live participant. 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 `registry/dist` build output stays out of the package +and that the rule ignoring it lives in the root `.gitignore` rather than a +nested one, since a nested rule would still pass the smoke test and the +manifest guard while silently no longer covering `registry/src` and +`registry/test`. + 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 99e31e9..90e8098 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,72 @@ 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 tag must name a commit whose DAR is uploaded to the +participant the service points at, since the template ids are checked at boot +and an id the participant cannot resolve stops it starting. + +Every `v[0-9]*` tag is 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, and its own refusal error prints the line to +paste. 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, and +`npm run seed` against a local sandbox prints the block filled in with the real +admin party and the five template ids. `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` diff --git a/RUNBOOK.md b/RUNBOOK.md index 57a8264..1e0e48e 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -311,6 +311,12 @@ 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` +regardless of which tag is cut, so `v0.2.0`'s DAR asset is byte-identical to +`v0.1.0`'s and carries the same sha256. + `.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 diff --git a/SPEC.md b/SPEC.md index ea885a1..22f52d7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -63,8 +63,11 @@ 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/canton-token-forge-test`, `registry/src` and +`registry/test` respectively. 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. @@ -578,10 +581,15 @@ 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 + the registry unit suite as the `registry` check. The same change also runs + a third job, `package`, gated on `registry/` among other inputs: it compiles + `registry/src` a second time, against the root dependency set rather than + `registry/`'s own, by running `npm ci` (which triggers `prepare`), then packs + the npm package and installs it into a scratch consumer to run the bin, + neither of which the `registry` check's own install and build can see. 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 @@ -590,9 +598,12 @@ Stated plainly, because they are what an evaluation turns on. 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` +- **Pre-release.** Version `0.2.0`, 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. + package version), and the npm package itself consumed from git at `v0.2.0`. + The two tags exist and mean different things: `v0.1.0` names the DAR release, + `v0.2.0` the commit a consumer's `package.json` pins. No migration story and + no compatibility guarantees. --- @@ -624,5 +635,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.json b/package.json index db02882..952b85d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "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", + "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": "module", "scripts": { From 6d29befe068630997510c774adbcf6b4580f13d7 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:34 +0000 Subject: [PATCH 40/63] docs: say the specifier pnpm prints has to be quoted before it parses --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 90e8098..c179b30 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,10 @@ onlyBuiltDependencies: 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, and its own refusal error prints the line to -paste. 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 +matches on that exact string. Its own refusal error prints that specifier, but +unquoted: a YAML key cannot start with `@`, and the `#` before the sha would +open a comment, so quote it as the block above does. 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` From 8786fc38c4122ec2ef1ac18714c7dc81cf76a28a Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:39 +0000 Subject: [PATCH 41/63] docs: tie the boot check to the package name it resolves, not to the tag --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c179b30..1421688 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,10 @@ consumed from git at a tag rather than from the public registry: 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 tag must name a commit whose DAR is uploaded to the -participant the service points at, since the template ids are checked at boot -and an id the participant cannot resolve stops it starting. +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 any release's DAR +serves, and one the participant cannot resolve stops the service starting. Every `v[0-9]*` tag is both a DAR release and an npm package pin: see "Consuming a release" above for the DAR itself. From 49808696f6d62a0e9221d9320a7ddd240f4682ed Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:50 +0000 Subject: [PATCH 42/63] docs: name which package each pre-release version belongs to --- SPEC.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index 22f52d7..b353f7e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -598,9 +598,10 @@ Stated plainly, because they are what an evaluation turns on. 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`, with the build published as release `v0.1.0` - for downstream repositories to pin (the tag is deliberately decoupled from the - package version), and the npm package itself consumed from git at `v0.2.0`. +- **Pre-release.** Version `0.2.0` of the npm package, `0.0.1` of the Daml + package, with the Daml build published as release `v0.1.0` for downstream + repositories to pin (that tag is deliberately decoupled from the Daml package + version), and the npm package consumed from git at `v0.2.0`. The two tags exist and mean different things: `v0.1.0` names the DAR release, `v0.2.0` the commit a consumer's `package.json` pins. No migration story and no compatibility guarantees. From e39048c22047b0b5cc1eb31b37507e38c39b57c1 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:54 +0000 Subject: [PATCH 43/63] docs: stop asserting a tag that has not been cut yet --- SPEC.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index b353f7e..11ea37f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -602,8 +602,8 @@ Stated plainly, because they are what an evaluation turns on. package, with the Daml build published as release `v0.1.0` for downstream repositories to pin (that tag is deliberately decoupled from the Daml package version), and the npm package consumed from git at `v0.2.0`. - The two tags exist and mean different things: `v0.1.0` names the DAR release, - `v0.2.0` the commit a consumer's `package.json` pins. No migration story and + The two tags mean different things: `v0.1.0` names the DAR release, `v0.2.0` + the commit a consumer's `package.json` pins. No migration story and no compatibility guarantees. --- From b9fd720f0416ca9017c42ba909e5bd6d7370e243 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:19:07 +0000 Subject: [PATCH 44/63] docs: scope the ungated comparison count to the check that runs them --- CLAUDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2623a36..dbc3c52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -288,9 +288,9 @@ Run before declaring work done: - `npm run smoke:registry` - when you changed the root manifest, the registry's dependencies, or either ignore file -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 +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 From 8e4c2494881796b68a5f5e8e20337ab326357cea Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:19:12 +0000 Subject: [PATCH 45/63] docs: stop denying the root commands a registry change now runs --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index dbc3c52..0c59743 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -298,7 +298,8 @@ installs and LF 2.1. One that touches 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 +`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 From 4965a15ad5b3b0f1066265cc61679f00b9aae3de Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:19:17 +0000 Subject: [PATCH 46/63] docs: list both new scripts in the structure tree, not only the table --- ARCHITECTURE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d4c546e..7cef2bf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 From 274c4108705e7866c674dd01013a83c464b16b6e Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:20:07 +0000 Subject: [PATCH 47/63] docs: say the DAR version is irrelevant rather than that any DAR serves --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1421688..a8085a5 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,9 @@ 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 any release's DAR -serves, and one the participant cannot resolve stops the service starting. +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. Every `v[0-9]*` tag is both a DAR release and an npm package pin: see "Consuming a release" above for the DAR itself. From 427969ccbcf736b1201297569efe212de7657ef9 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:20:26 +0000 Subject: [PATCH 48/63] docs: rewrap the two lines the corrections left ragged --- CLAUDE.md | 5 +++-- README.md | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c59743..a70f5c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -290,8 +290,9 @@ Run before declaring work done: 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`; +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 diff --git a/README.md b/README.md index a8085a5..ce7338e 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ 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, but unquoted: a YAML key cannot start with `@`, and the `#` before the sha would open a comment, so quote it as the block above does. 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. +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: From 6812b84b5968c75bf7b106cf1c04d28cfb25a66a Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:20 +0000 Subject: [PATCH 49/63] docs: say the ignore table reads git, and name the mutation it catches --- CLAUDE.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a70f5c4..9bac7f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -319,11 +319,18 @@ 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 `registry/dist` build output stays out of the package -and that the rule ignoring it lives in the root `.gitignore` rather than a -nested one, since a nested rule would still pass the smoke test and the -manifest guard while silently no longer covering `registry/src` and -`registry/test`. +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 From e814f42cadfc6caf1a111a0891d178050bd88a26 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 50/63] docs: send a manifest dependency change to the guard that compares them --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9bac7f7..7e7e06e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,6 +285,8 @@ Run before declaring work done: - `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` +- `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 From 07b83ea6ef82a8b823f231408f4068cf79a45b16 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 51/63] docs: reflow the paragraph the previous rewrap left ragged --- CLAUDE.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7e7e06e..ec1282a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -294,17 +294,16 @@ 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 +`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, 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 +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. From 42c2513bdc15bc23f72c1b6bcd91b4b953168612 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 52/63] docs: stop offering a tag that is not cut and a pin that installs no service --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce7338e..fa5b76c 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,14 @@ 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. -Every `v[0-9]*` tag is both a DAR release and an npm package pin: see -"Consuming a release" above for the DAR itself. +`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` From 38f8ce589379c9d4bbbfd6f5941acf9f527aaf14 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 53/63] docs: blame the reserved @ rather than the # for that key not parsing --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa5b76c..6b5e7aa 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,12 @@ onlyBuiltDependencies: 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, but -unquoted: a YAML key cannot start with `@`, and the `#` before the sha would -open a comment, so quote it as the block above does. Since the sha is resolved +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. From bc823cde8c42d61f23763f7daee818133025b3a2 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 54/63] docs: say the template ids need quoting and where seed can be run --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6b5e7aa..9568ed1 100644 --- a/README.md +++ b/README.md @@ -214,11 +214,22 @@ loaded from the working directory it is started in. Required, eight: `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, and -`npm run seed` against a local sandbox prints the block filled in with the real -admin party and the five template ids. `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. +`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 From dbc0707c3a06724c6ad3e2e55aeac50cd38b18fe Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:31 +0000 Subject: [PATCH 55/63] docs: derive DAR byte-identity from the sources rather than the version string --- RUNBOOK.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/RUNBOOK.md b/RUNBOOK.md index 1e0e48e..3420aad 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -314,8 +314,19 @@ reason the seed script looks the way it does. 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` -regardless of which tag is cut, so `v0.2.0`'s DAR asset is byte-identical to -`v0.1.0`'s and carries the same sha256. +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 From 240c6951cca65d1227fc5bdeee618adf5bf6adc5 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:31 +0000 Subject: [PATCH 56/63] docs: add the package.json version bump the release procedure never named --- RUNBOOK.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/RUNBOOK.md b/RUNBOOK.md index 3420aad..4f82145 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -374,7 +374,10 @@ 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 @@ -382,7 +385,7 @@ CI just proved compile. git push origin v0.1.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. From 76a3c9a8a2c98d0bf029c3a80f2fc64252452b8c Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:31 +0000 Subject: [PATCH 57/63] docs: point the line-count recipe at the source dir, not the package root --- SPEC.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index 11ea37f..2d694f7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -66,8 +66,12 @@ resulting exercise itself over the JSON Ledger API, forwarding the service's 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/canton-token-forge-test`, `registry/src` and -`registry/test` respectively. MIT licensed. Pre-release: the Daml package +`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. From 3aad5df2f833d2e281d55f4a5b5eec18149a9fd3 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:57 +0000 Subject: [PATCH 58/63] docs: say which package step is ungated instead of gating the whole job --- SPEC.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/SPEC.md b/SPEC.md index 2d694f7..b2670b4 100644 --- a/SPEC.md +++ b/SPEC.md @@ -586,22 +586,26 @@ Stated plainly, because they are what an evaluation turns on. 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 same change also runs - a third job, `package`, gated on `registry/` among other inputs: it compiles - `registry/src` a second time, against the root dependency set rather than - `registry/`'s own, by running `npm ci` (which triggers `prepare`), then packs - the npm package and installs it into a scratch consumer to run the bin, - neither of which the `registry` check's own install and build can see. 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. + 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, with the Daml build published as release `v0.1.0` for downstream repositories to pin (that tag is deliberately decoupled from the Daml package From 24437f238f4484f00b6669b871db2e90c6079526 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:57 +0000 Subject: [PATCH 59/63] docs: describe the release tags as one namespace rather than a split --- SPEC.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index b2670b4..9a3ed3b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -607,12 +607,12 @@ Stated plainly, because they are what an evaluation turns on. 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, with the Daml build published as release `v0.1.0` for downstream - repositories to pin (that tag is deliberately decoupled from the Daml package - version), and the npm package consumed from git at `v0.2.0`. - The two tags mean different things: `v0.1.0` names the DAR release, `v0.2.0` - the commit a consumer's `package.json` pins. No migration story and - no compatibility guarantees. + package, both deliberately decoupled from the release tags. 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. --- From bf5a9d5567cdb18ee0de254b0b2d833557fe4038 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 20:56:23 +0000 Subject: [PATCH 60/63] fix: spell the smoke test's template ids out, so the sweep can see them --- .github/workflows/ci.yml | 5 +++++ scripts/registry-install-smoke.sh | 21 +++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb4f4c0..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 diff --git a/scripts/registry-install-smoke.sh b/scripts/registry-install-smoke.sh index 0457393..34d2f17 100755 --- a/scripts/registry-install-smoke.sh +++ b/scripts/registry-install-smoke.sh @@ -157,21 +157,30 @@ esac echo "smoke: running against an unreachable participant" serve_port="$(free_port)" dead_port="$(free_port)" -prefix='#canton-token-forge:Canton.TokenForge' # 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="${prefix}.Registry:InstrumentConfig" \ -TRANSFER_INSTRUCTION_TEMPLATE_ID="${prefix}.Instruction:TokenTransferInstruction" \ -PREAPPROVAL_TEMPLATE_ID="${prefix}.Registry:TokenTransferPreapproval" \ -LOCKED_TOKEN_TEMPLATE_ID="${prefix}.Locked:LockedToken" \ -ALLOCATION_TEMPLATE_ID="${prefix}.Allocation:TokenAllocation" \ +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=$! From 8197c53e94d6336ffc619527b0ad9f0300857ed4 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 20:57:11 +0000 Subject: [PATCH 61/63] fix: clear the registry build output before rebuilding it --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 952b85d..ebbc59e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "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$'", - "prepare": "tsc -p registry/tsconfig.json" + "prepare": "rm -rf registry/dist && tsc -p registry/tsconfig.json" }, "repository": "github:BootNodeDev/canton-token-forge", "engines": { "node": ">=20" }, From c0677ae6a60697d6d6a6056052dc28aa98f6b32c Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 21:38:33 +0000 Subject: [PATCH 62/63] fix: compare declared packages by own property, not by prototype membership --- scripts/check-registry-deps.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/check-registry-deps.mjs b/scripts/check-registry-deps.mjs index 2e194fd..491cdd1 100644 --- a/scripts/check-registry-deps.mjs +++ b/scripts/check-registry-deps.mjs @@ -49,8 +49,13 @@ 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 (!(name in (registryManifest.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.`, ) @@ -58,7 +63,7 @@ for (const name of Object.keys(rootManifest.dependencies ?? {})) { } for (const name of Object.keys(registryManifest.dependencies ?? {})) { - if (!(name in (rootManifest.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.`, ) @@ -188,7 +193,7 @@ for (const { manifest, manifestFile, lock, lockFile, install } of TREES) { } } for (const name of Object.keys(locked)) { - if (!(name in declared)) { + 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.`, ) From 156efc43d5fa1395c67bdb81ad67d2f5ede6edcc Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 21:38:33 +0000 Subject: [PATCH 63/63] docs: name the tag the release procedure is about to cut, not the one already cut --- RUNBOOK.md | 10 +++++----- SPEC.md | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RUNBOOK.md b/RUNBOOK.md index 4f82145..2ef7f01 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -344,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 @@ -364,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 @@ -381,8 +381,8 @@ CI just proved compile. 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 ``` 4. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body @@ -414,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 9a3ed3b..f85e2d3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -607,7 +607,9 @@ Stated plainly, because they are what an evaluation turns on. 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, both deliberately decoupled from the release tags. Those tags are + 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