openvibers/openvibe.trade Commits
Pin openvibe-contracts v0.97.0, drop the proposed-capability shim, move shutdown onto the service kit (#12)
Changes
@@@ -2,4 +2,5 @@## Unreleased++- openvibe-contracts moves to v0.97.0 (pin, lockfile and `node_modules`). All twelve `trade.*` ids are now defined by the release, so `server/auth/capabilities.js` drops its local fallback for proposed ids and sends every check through the library's grant rule; `test/capabilities.test.js` pins that every guarded id is defined and that exact, prefix, denied and unknown ids answer as `capabilities.check()` does. `server/index.js` moves its shutdown onto `openvibe-sdk/service`'s `gracefulStop` (drain with `Connection: close`, then the worker, the outbox and the store; SIGTERM/SIGINT replace the hand-rolled handlers and timer), pinned by the spawn-SIGTERM check in `test/service-kit.test.js`. README and STATUS.json name v0.97.0 and the pinned v0.26.0 SDK.- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; Trade keeps its site stylesheet, the disclaimers, the account bar and the "Recently shipped" line. Pins `openvibe-shared` v2.6.0 and `openvibe-publishing` v1.2.0.@@@ -6,7 +6,7 @@internally, not launched**: it runs on the production host on 127.0.0.1:4860 only (release`9a55644`; `/api/ready` reports `degraded: ["freshness"]` because there is no feed), with an emptydatabase (0 instruments), and `openvibe.trade` still shows its placeholder from OpenVibe.Sites. Its--capabilities and service manifest are registered in openvibe-contracts v0.22.0. No market data++capabilities and service manifest are registered in openvibe-contracts v0.97.0. No market datafeed is configured yet (see "What it shows today").**Domain:** `openvibe.trade` · **Port:** 4860 · **Service id:** `trade`**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.11;@@@ -178,9 +178,9 @@ Service tokens use audience `openvibe.trade`, one capability per route; privateand the editor list. The charter's `trade.watchlist.create|update`, `trade.instrument.resolve`,`trade.alert.create|delete` and `trade.context.read` are all 3-segment ids here; `read`, `delete`,`manage`, `observation.write` and `context.propose` are additions. They and the service manifest--are released in openvibe-contracts v0.22.0 from the proposals in++are released in openvibe-contracts (pinned at v0.97.0; the source proposals stay in[docs/capabilities-proposal/](docs/capabilities-proposal/) and--[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). Grants for these ids are++[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)). Grants for these ids aredecided with the contracts library's matching rule (`server/auth/capabilities.js`).### Events (SDK outbox, same transaction as the change)@@@ -222,10 +222,10 @@ services need to call Trade, is under [Grants the Network must hold](#grants-the- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).--- **Packages** (pinned by release tarball): `openvibe-contracts` v0.76.0, `openvibe-publishing`-- v1.1.0 (revisions, authorship, seo, index-hooks, ssr, ingest, publication), `openvibe-shared` v2.2.0 (Frame, app icon,-- footer, legal, release, metrics, ready), `openvibe-sdk` v0.20.0 (events outbox and inbox, webhook-- signatures v2, service tokens, per-actor limits).++- **Packages** (pinned by release tarball): `openvibe-contracts` v0.97.0, `openvibe-publishing`++ v1.3.0 (revisions, authorship, seo, index-hooks, ssr, ingest, publication), `openvibe-shared` v2.6.0 (Frame, app icon,++ footer, legal, release, metrics, ready), `openvibe-sdk` v0.26.0 (events outbox and inbox, webhook++ signatures v2, service tokens, per-actor limits, service kit lifecycle).- **OpenVibe.Network:** SSO (OAuth client `trade`, redirect `https://openvibe.trade/auth/callback`),JWKS, client-credentials tokens.- **OpenVibe.Sources:** `sources.item.read` and `sources.source.read` (category `trade`).@@@ -280,7 +280,7 @@ exists. Status against each point:empty database); in production there is no market data source yet and the filings source isdisabled in Sources (see "What it shows today").5. **Capability and event registration against OpenVibe.Contracts:** done (openvibe-contracts-- v0.22.0).++ v0.97.0).6. **Migration and seed strategy, threat review, sitemap/robots/feed behaviour:** done. Nothing tomigrate (no current implementation); no seed (editors add instruments); threat review below.7. **Acceptance tests:** done.@@@ -382,7 +382,7 @@ First install (done once; kept for a rebuild):4. **Search:** add `trade` to `SEARCH_EVENT_OWNERS` if it is not there.5. **systemd:** install `deploy/systemd/openvibe-trade.service` (port 4860, `StateDirectory=openvibe-trade`).6. **nginx:** install `deploy/nginx/openvibe.trade.conf` (`/metrics` and `/internal/` never proxied).--7. **Contracts:** done: the capabilities and manifest are released in openvibe-contracts v0.22.0,++7. **Contracts:** done: the capabilities and manifest are released in openvibe-contracts v0.97.0,and CI's contracts check runs against them.8. **Data:** editors add instruments at `/editor`. Filings appear once `sec-xbrl-filings` is enabledin Sources (after a person verifies its terms); numbers appear only once a market data source@@@ -8,12 +8,12 @@"port": 4860,"domain": "openvibe.trade","scope": "informational only (ADR-025): no custody, no order execution, no escrow, no listings between users, no personalised advice",-- "contracts": "openvibe-contracts v0.79.0",++ "contracts": "openvibe-contracts v0.97.0","packages": [-- "openvibe-contracts v0.79.0",++ "openvibe-contracts v0.97.0","openvibe-publishing v1.3.0","openvibe-shared v2.6.0",-- "openvibe-sdk v0.25.0"++ "openvibe-sdk v0.26.0"],"capabilities": ["trade.instrument.resolve",@@@ -36,7 +36,7 @@"deployed": true,"deployment": "Deployed with `sudo ovhost deploy trade`: unit openvibe-trade, 127.0.0.1:4860 loopback only; on 2026-09-23 /api/ready answered 200 with status degraded [freshness], which is honest: no feed is configured; Network OAuth client trade and its grants exist; no Events subscription; production database held 0 instruments and 0 observations. Deployed internally, not launched: openvibe.trade still serves the OpenVibe.Sites placeholder until a person verifies the SEC XBRL terms and enables sec-xbrl-filings in Sources, a licensed market data feed exists, and editors add instruments","placeholderInSites": true,-- "updated": "2026-09-29",++ "updated": "2026-10-04","features": ["informational only: a route-inventory test refuses any execution, custody or advice endpoint","instruments with exact, deterministic resolution (CIK, ticker, name)",@@@ -15,7 +15,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",@@@ -94,65 +94,6 @@"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==","license": "MIT"},-- "node_modules/base64-js": {-- "version": "1.5.1",-- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",-- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true-- },-- "node_modules/better-sqlite3": {-- "version": "11.10.0",-- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",-- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",-- "hasInstallScript": true,-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "bindings": "^1.5.0",-- "prebuild-install": "^7.1.1"-- }-- },-- "node_modules/bindings": {-- "version": "1.5.0",-- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",-- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "file-uri-to-path": "1.0.0"-- }-- },-- "node_modules/bl": {-- "version": "4.1.0",-- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",-- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "buffer": "^5.5.0",-- "inherits": "^2.0.4",-- "readable-stream": "^3.4.0"-- }-- },"node_modules/body-parser": {"version": "1.20.8","resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",@@@ -177,32 +118,6 @@"npm": "1.2.8000 || >= 1.4.16"}},-- "node_modules/buffer": {-- "version": "5.7.1",-- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",-- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "base64-js": "^1.3.1",-- "ieee754": "^1.1.13"-- }-- },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -247,14 +162,6 @@"url": "https://github.com/sponsors/ljharb"}},-- "node_modules/chownr": {-- "version": "1.1.4",-- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",-- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",-- "license": "ISC",-- "optional": true,-- "peer": true-- },"node_modules/cluster-key-slot": {"version": "1.1.2","resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",@@@ -322,34 +229,6 @@"ms": "2.0.0"}},-- "node_modules/decompress-response": {-- "version": "6.0.0",-- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",-- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "mimic-response": "^3.1.0"-- },-- "engines": {-- "node": ">=10"-- },-- "funding": {-- "url": "https://github.com/sponsors/sindresorhus"-- }-- },-- "node_modules/deep-extend": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",-- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=4.0.0"-- }-- },"node_modules/denque": {"version": "2.1.0","resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",@@@ -378,17 +257,6 @@"npm": "1.2.8000 || >= 1.4.16"}},-- "node_modules/detect-libc": {-- "version": "2.1.2",-- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",-- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",-- "license": "Apache-2.0",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=8"-- }-- },"node_modules/dotenv": {"version": "16.6.1","resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",@@@ -439,17 +307,6 @@"node": ">= 0.8"}},-- "node_modules/end-of-stream": {-- "version": "1.4.5",-- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",-- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "once": "^1.4.0"-- }-- },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -495,17 +352,6 @@"node": ">= 0.6"}},-- "node_modules/expand-template": {-- "version": "2.0.3",-- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",-- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",-- "license": "(MIT OR WTFPL)",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=6"-- }-- },"node_modules/express": {"version": "4.22.3","resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",@@@ -589,14 +435,6 @@],"license": "BSD-3-Clause"},-- "node_modules/file-uri-to-path": {-- "version": "1.0.0",-- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",-- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/finalhandler": {"version": "1.3.2","resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",@@@ -633,14 +471,6 @@"node": ">= 0.6"}},-- "node_modules/fs-constants": {-- "version": "1.0.0",-- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",-- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/function-bind": {"version": "1.1.2","resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",@@@ -687,14 +517,6 @@"node": ">= 0.4"}},-- "node_modules/github-from-package": {-- "version": "0.0.0",-- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",-- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/gopd": {"version": "1.2.0","resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",@@@ -775,42 +597,12 @@"node": ">=0.10.0"}},-- "node_modules/ieee754": {-- "version": "1.2.1",-- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",-- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "BSD-3-Clause",-- "optional": true,-- "peer": true-- },"node_modules/inherits": {"version": "2.0.4","resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz","integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==","license": "ISC"},-- "node_modules/ini": {-- "version": "1.3.8",-- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",-- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",-- "license": "ISC",-- "optional": true,-- "peer": true-- },"node_modules/iovalkey": {"version": "0.4.0","resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",@@@ -1041,53 +833,12 @@"node": ">= 0.6"}},-- "node_modules/mimic-response": {-- "version": "3.1.0",-- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",-- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=10"-- },-- "funding": {-- "url": "https://github.com/sponsors/sindresorhus"-- }-- },-- "node_modules/minimist": {-- "version": "1.2.8",-- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",-- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "funding": {-- "url": "https://github.com/sponsors/ljharb"-- }-- },-- "node_modules/mkdirp-classic": {-- "version": "0.5.3",-- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",-- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/ms": {"version": "2.0.0","resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz","integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==","license": "MIT"},-- "node_modules/napi-build-utils": {-- "version": "2.0.0",-- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",-- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/negotiator": {"version": "0.6.3","resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",@@@ -1097,20 +848,6 @@"node": ">= 0.6"}},-- "node_modules/node-abi": {-- "version": "3.96.0",-- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",-- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "semver": "^7.3.5"-- },-- "engines": {-- "node": ">=10"-- }-- },"node_modules/object-inspect": {"version": "1.13.4","resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",@@@ -1135,21 +872,10 @@"node": ">= 0.8"}},-- "node_modules/once": {-- "version": "1.4.0",-- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",-- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",-- "license": "ISC",-- "optional": true,-- "peer": true,-- "dependencies": {-- "wrappy": "1"-- }-- },"node_modules/openvibe-contracts": {-- "version": "0.79.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",-- "integrity": "sha512-FY5GLbiOq0kDw5lUPPzX6h3ATbzvsr/mX5DUCF4GRfn6vGMP0p89rZZDhgtVDaS+yCBCYMZ8zFaZxuHEkEVwVg==",++ "version": "0.97.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",++ "integrity": "sha512-HPGBJYx6Dh2kaTg5B41WGhpw38zUdoU1VnWtkcsFkaU4E521uQOJxf6sIaLdHMsahqov7b9k2HKEFCnD9065MQ==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1364,35 +1090,6 @@"node": ">=0.10.0"}},-- "node_modules/prebuild-install": {-- "version": "7.1.3",-- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",-- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",-- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "detect-libc": "^2.0.0",-- "expand-template": "^2.0.3",-- "github-from-package": "0.0.0",-- "minimist": "^1.2.3",-- "mkdirp-classic": "^0.5.3",-- "napi-build-utils": "^2.0.0",-- "node-abi": "^3.3.0",-- "pump": "^3.0.0",-- "rc": "^1.2.7",-- "simple-get": "^4.0.0",-- "tar-fs": "^2.0.0",-- "tunnel-agent": "^0.6.0"-- },-- "bin": {-- "prebuild-install": "bin.js"-- },-- "engines": {-- "node": ">=10"-- }-- },"node_modules/proxy-addr": {"version": "2.0.8","resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",@@@ -1410,18 +1107,6 @@"url": "https://opencollective.com/express"}},-- "node_modules/pump": {-- "version": "3.0.4",-- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",-- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "end-of-stream": "^1.1.0",-- "once": "^1.3.1"-- }-- },"node_modules/qs": {"version": "6.16.0","resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",@@@ -1462,39 +1147,6 @@"node": ">= 0.8"}},-- "node_modules/rc": {-- "version": "1.2.8",-- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",-- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",-- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",-- "optional": true,-- "peer": true,-- "dependencies": {-- "deep-extend": "^0.6.0",-- "ini": "~1.3.0",-- "minimist": "^1.2.0",-- "strip-json-comments": "~2.0.1"-- },-- "bin": {-- "rc": "cli.js"-- }-- },-- "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==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "inherits": "^2.0.3",-- "string_decoder": "^1.1.1",-- "util-deprecate": "^1.0.1"-- },-- "engines": {-- "node": ">= 6"-- }-- },"node_modules/redis-errors": {"version": "1.2.0","resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",@@@ -1686,55 +1338,6 @@"url": "https://github.com/sponsors/ljharb"}},-- "node_modules/simple-concat": {-- "version": "1.0.1",-- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",-- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true-- },-- "node_modules/simple-get": {-- "version": "4.0.1",-- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",-- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "decompress-response": "^6.0.0",-- "once": "^1.3.1",-- "simple-concat": "^1.0.0"-- }-- },"node_modules/split2": {"version": "4.2.0","resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",@@@ -1759,60 +1362,6 @@"node": ">= 0.8"}},-- "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==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "safe-buffer": "~5.2.0"-- }-- },-- "node_modules/strip-json-comments": {-- "version": "2.0.1",-- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",-- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=0.10.0"-- }-- },-- "node_modules/tar-fs": {-- "version": "2.1.5",-- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",-- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "chownr": "^1.1.1",-- "mkdirp-classic": "^0.5.2",-- "pump": "^3.0.0",-- "tar-stream": "^2.1.4"-- }-- },-- "node_modules/tar-stream": {-- "version": "2.2.0",-- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",-- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "bl": "^4.0.3",-- "end-of-stream": "^1.4.1",-- "fs-constants": "^1.0.0",-- "inherits": "^2.0.3",-- "readable-stream": "^3.1.1"-- },-- "engines": {-- "node": ">=6"-- }-- },"node_modules/toidentifier": {"version": "1.0.1","resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",@@@ -1822,20 +1371,6 @@"node": ">=0.6"}},-- "node_modules/tunnel-agent": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",-- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",-- "license": "Apache-2.0",-- "optional": true,-- "peer": true,-- "dependencies": {-- "safe-buffer": "^5.0.1"-- },-- "engines": {-- "node": "*"-- }-- },"node_modules/type-is": {"version": "1.6.18","resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",@@@ -1858,14 +1393,6 @@"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==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/utils-merge": {"version": "1.0.1","resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",@@@ -1884,14 +1411,6 @@"node": ">= 0.8"}},-- "node_modules/wrappy": {-- "version": "1.0.2",-- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",-- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",-- "license": "ISC",-- "optional": true,-- "peer": true-- },"node_modules/xtend": {"version": "4.0.2","resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",@@@ -27,7 +27,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",@@@ -1,14 +1,10 @@'use strict';/**-- * Capability checks for service tokens (audience openvibe.trade), including the ids Trade-- * introduces before the contracts library knows them.-- *-- * openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its-- * manifests yet. Trade's ids are proposed in docs/capabilities-proposal/ for the next contracts-- * release; until then a grant is decided locally with the library's own matching rule (the exact-- * id, or a `prefix.*` grant covering it). An id the library does know always goes through the-- * library, so the day the release lands nothing changes here.++ * Capability checks for service tokens (audience openvibe.trade). The trade.* ids this service++ * introduces are defined by the installed openvibe-contracts, so a grant is decided by the++ * library's own matching rule (the exact id, or a `prefix.*` grant covering it). CAPABILITIES keeps++ * the ids in one place for the guards, the proposal documents and the tests.** Browsers (Network user JWTs) are never judged by capabilities: watchlists and alert rules by* ownership, editing by the editor list (domain/access.js). A service token is judged by its@@@ -32,16 +28,10 @@ const CAPABILITIES = Object.freeze({ALERT_CREATE: 'trade.alert.create',ALERT_DELETE: 'trade.alert.delete',});--const PROPOSED = new Set(Object.values(CAPABILITIES));/** → { allowed, code, reason } like capabilities.check(). */function checkCapability(claims, capabilityId) {-- if (!capabilities.get(capabilityId) && PROPOSED.has(capabilityId)) {-- return capabilities.grants(claims && claims.cap, capabilityId)-- ? { allowed: true, code: null, reason: null }-- : { allowed: false, code: 'capability.denied', reason: `${capabilityId} not granted` };-- }return capabilities.check(claims, capabilityId);}--module.exports = { CAPABILITIES, PROPOSED, checkCapability };++module.exports = { CAPABILITIES, checkCapability };@@@ -3,8 +3,11 @@/*** OpenVibe.Trade — process entry. `node server/index.js`* Listens on PORT (4860) behind nginx (deploy/). Starts the outbox relay (when EVENTS_URL and the-- * client secret are set) and the worker (Sources sync, freshness).++ * client secret are set) and the worker (Sources sync, freshness). On SIGTERM/SIGINT the service++ * kit drains in-flight requests (Connection: close), stops the worker and the relay, closes the++ * store, and exits 0 (the 5 s family's deadline exit code).*/++const { gracefulStop } = require('openvibe-sdk/service');const { createApp } = require('./app');(async () => {@@@ -19,16 +22,9 @@ server.keepAliveTimeout = 65_000;ctx.outbox.start();ctx.worker.start();--function shutdown(signal) {-- console.log(`[Trade] ${signal}: closing`);-- ctx.worker.stop();-- server.close(async () => {-- try { await ctx.outbox.stop(); } catch { /* best effort */ }-- try { await ctx.store.close(); } catch { /* already closed */ }-- process.exit(0);-- });-- setTimeout(() => process.exit(0), 5000).unref();--}--process.on('SIGTERM', () => shutdown('SIGTERM'));--process.on('SIGINT', () => shutdown('SIGINT'));++gracefulStop({++ name: 'Trade', server, deadlineExitCode: 0,++ stop: [() => ctx.worker.stop()],++ close: [() => ctx.outbox.stop(), () => ctx.store.close()],++});})().catch((err) => { console.error('[Trade] failed to start:', err); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,41 @@++'use strict';++/**++ * The trade.* capabilities are released by openvibe-contracts, so the service guards delegate to the++ * library's grant rule rather than deciding a proposed id locally (the fallback that existed while++ * the ids were only proposals). These pin that: every guarded id is defined by the installed++ * contracts and by the trade service manifest, and checkCapability agrees with capabilities.check++ * for an exact grant, a prefix grant, no grant, and an id the contracts do not define.++ */++const assert = require('assert');++const { capabilities, services } = require('openvibe-contracts');++const { checkCapability, CAPABILITIES } = require('../server/auth/capabilities');++const { check, done } = require('./helpers/boot');++++const GRANTED = { cap: ['trade.watchlist.create'], sub: 'svc:tools' };++++(async () => {++ await check('every guarded capability is defined by the installed contracts and the trade manifest', async () => {++ const manifest = services.get('trade');++ assert.ok(manifest, 'openvibe-contracts defines the trade service manifest');++ assert.deepStrictEqual([...manifest.capabilities].sort(), Object.values(CAPABILITIES).sort());++ for (const id of Object.values(CAPABILITIES)) {++ const cap = capabilities.get(id);++ assert.ok(cap, `${id} is defined by openvibe-contracts`);++ assert.strictEqual(cap.owner, 'trade', `${id} is owned by trade`);++ }++ });++++ await check('checkCapability follows the contracts grant rule (exact, prefix, denied, unknown)', async () => {++ // An exact grant and a `prefix.*` grant the library's matching rule accepts.++ assert.deepStrictEqual(checkCapability(GRANTED, 'trade.watchlist.create'), { allowed: true, code: null, reason: null });++ assert.deepStrictEqual(checkCapability({ cap: ['trade.*'] }, 'trade.watchlist.create'), { allowed: true, code: null, reason: null });++ // A grant of a sibling capability, or no cap at all, is denied with the library's code.++ assert.strictEqual(checkCapability({ cap: ['trade.watchlist.read'] }, 'trade.watchlist.create').code, 'capability.denied');++ assert.strictEqual(checkCapability(null, 'trade.watchlist.create').code, 'capability.denied');++ // An id the contracts do not define still answers capability.unknown — the guard delegates,++ // it never decides an unknown id on its own.++ assert.strictEqual(checkCapability({ cap: ['trade.*'] }, 'trade.not.a.capability').code, 'capability.unknown');++ });++++ done();++})().catch((err) => { console.error(err); process.exit(1); });@@@ -1,15 +1,18 @@'use strict';/**-- * The proposals the lead releases in the next openvibe-contracts version are valid against the-- * released schemas, match what the code enforces and emits, and every envelope Trade produces is a-- * valid events.event-envelope@1.++ * The capability proposals in docs/capabilities-proposal/ are valid against the released schemas and++ * match what the code enforces and emits; the ids are now released by openvibe-contracts (checked++ * here and pinned in test/capabilities.test.js), and every envelope Trade produces is a valid++ * events.event-envelope@1.*/const assert = require('assert');const fs = require('fs');const path = require('path');const contracts = require('openvibe-contracts');const { boot, check, done } = require('./helpers/boot');--const { PROPOSED } = require('../server/auth/capabilities');++const { CAPABILITIES } = require('../server/auth/capabilities');++++const CAPS = new Set(Object.values(CAPABILITIES));const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');@@@ -25,14 +28,16 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');assert.strictEqual(c.owner, 'trade');assert.ok(c.id.split('.').length >= 3);assert.strictEqual(`${c.id}.json`, files[caps.indexOf(c)]);-- assert.ok(!contracts.capabilities.get(c.id) || contracts.capabilities.get(c.id).owner === 'trade', `${c.id} collides with a released capability`);++ const released = contracts.capabilities.get(c.id);++ assert.ok(released, `${c.id} is defined by openvibe-contracts`);++ assert.strictEqual(released.owner, 'trade', `${c.id} collides with a released capability`);}});await check('the proposals are exactly the capabilities the code enforces, and cover the charter\'s', async () => {-- assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...PROPOSED].sort());-- assert.deepStrictEqual([...manifest.capabilities].sort(), [...PROPOSED].sort());-- for (const id of ['trade.watchlist.create', 'trade.watchlist.update', 'trade.instrument.resolve', 'trade.alert.create', 'trade.alert.delete', 'trade.context.read']) assert.ok(PROPOSED.has(id), id);++ assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...CAPS].sort());++ assert.deepStrictEqual([...manifest.capabilities].sort(), [...CAPS].sort());++ for (const id of ['trade.watchlist.create', 'trade.watchlist.update', 'trade.instrument.resolve', 'trade.alert.create', 'trade.alert.delete', 'trade.context.read']) assert.ok(CAPS.has(id), id);const src = fs.readFileSync(path.join(__dirname, '..', 'server', 'http', 'api.js'), 'utf8');for (const c of caps) for (const route of c.implementedBy) assert.ok(src.includes(`'${route.split(' ')[1].replace('/api/v1', '')}'`), `${c.id}: ${route} is not in http/api.js`);});@@@ -15,7 +15,7 @@ const fs = require('fs');const path = require('path');const express = require('express');const { boot, check, done } = require('./helpers/boot');--const { PROPOSED } = require('../server/auth/capabilities');++const { CAPABILITIES } = require('../server/auth/capabilities');const FORBIDDEN = ['order', 'buy', 'sell', 'execut', 'custod', 'wallet', 'escrow', 'checkout', 'listing','purchas', 'payment', 'payout', 'deposit', 'withdraw', 'cart'];@@@ -94,7 +94,7 @@ function inventory(app) {});await check('no capability id and no produced event type carries those semantics', async () => {-- for (const id of PROPOSED) assert.deepStrictEqual(violations(id), [], id);++ for (const id of Object.values(CAPABILITIES)) assert.deepStrictEqual(violations(id), [], id);const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));for (const e of manifest.eventsProduced) assert.deepStrictEqual(violations(e), [], e);for (const c of manifest.capabilities) assert.deepStrictEqual(violations(c), [], c);@@@ -5,11 +5,24 @@* request.too_large for a body over the limit (the hand-rolled one folded that into 400* request.invalid_json), malformed JSON is still 400 request.invalid_json, and an ApiError carrying* `extra` still answers its status/code/detail with extra spread into the problem body.++ *++ * server/index.js now stops through openvibe-sdk/service's gracefulStop too (plan T2): the last check++ * spawns the real entry point, holds a request in flight, sends SIGTERM, and pins that the drain++ * answers it with Connection: close and the process exits 0.*/const assert = require('assert');++const fs = require('fs');++const http = require('http');++const net = require('net');++const os = require('os');++const path = require('path');++const { once } = require('events');++const { spawn } = require('child_process');const { boot, check, done } = require('./helpers/boot');const { ApiError, asApiError, run } = require('../server/http/errors');++const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });++(async () => {const t = await boot();const svc = t.network.serviceToken('tools', ['trade.watchlist.create']);@@@ -42,5 +55,69 @@ const { ApiError, asApiError, run } = require('../server/http/errors');});await t.close();++++ await check('SIGTERM drains an in-flight request (Connection: close) and exits 0', async () => {++ // A database of its own, like the perf budget test: the real entry point, nothing shared.++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-trade-stop-'));++ const port = await freePort();++ const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {++ cwd: path.join(__dirname, '..'),++ env: {++ ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test',++ DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '',++ TRADE_PGLITE_DIR: path.join(dir, 'pglite'), TRADE_WORKER: 'off',++ },++ stdio: ['ignore', 'pipe', 'pipe'],++ });++ let log = '';++ child.stdout.on('data', (d) => { log = (log + d).slice(-4000); });++ child.stderr.on('data', (d) => { log = (log + d).slice(-4000); });++ const agent = new http.Agent({ keepAlive: true });++ try {++ const base = `http://127.0.0.1:${port}`;++ let up = false;++ for (let i = 0; i < 150 && !up; i++) {++ up = await fetch(`${base}/api/health`).then((r) => r.ok).catch(() => false);++ if (!up) await new Promise((r) => setTimeout(r, 100));++ }++ assert.ok(up, `the server did not start:\n${log}`);++++ // A form POST whose body is held open (the shared urlencoded parser waits for the end):++ // it is in flight when SIGTERM arrives, so the drain must answer it rather than cut it.++ let held = null;++ const inFlight = new Promise((resolve, reject) => {++ const req = http.request(`${base}/watchlists`, {++ method: 'POST', agent, headers: { 'content-type': 'application/x-www-form-urlencoded' },++ }, (res) => {++ const chunks = [];++ res.on('data', (c) => chunks.push(c));++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers }));++ res.on('error', reject);++ });++ req.on('error', reject);++ req.write('_csrf=held');++ held = req;++ });++ await new Promise((r) => setTimeout(r, 300));++ const exited = once(child, 'exit');++ child.kill('SIGTERM');++ held.end('&name=Held');++++ const res = await inFlight;++ assert.strictEqual(res.headers.connection, 'close', `in-flight response was not Connection: close: ${JSON.stringify(res.headers)}`);++ assert.ok(res.status >= 300, `an unsigned form POST answered ${res.status}`);++ const [code] = await exited;++ assert.strictEqual(code, 0, `expected exit 0, got ${code}\n${log}`);++ assert.ok(/\[Trade\] SIGTERM: stopping/.test(log), `the service kit did not log the stop:\n${log}`);++ } finally {++ agent.destroy();++ if (child.exitCode === null && child.signalCode === null) {++ child.kill('SIGKILL');++ await once(child, 'exit').catch(() => {});++ }++ fs.rmSync(dir, { recursive: true, force: true });++ }++ });++done();})().catch((err) => { console.error(err); process.exit(1); });
@@ -2,4 +2,5 @@
@@ -2,4 +2,5 @@
## Unreleased
## Unreleased
+- openvibe-contracts moves to v0.97.0 (pin, lockfile and `node_modules`). All twelve `trade.*` ids are now defined by the release, so `server/auth/capabilities.js` drops its local fallback for proposed ids and sends every check through the library's grant rule; `test/capabilities.test.js` pins that every guarded id is defined and that exact, prefix, denied and unknown ids answer as `capabilities.check()` does. `server/index.js` moves its shutdown onto `openvibe-sdk/service`'s `gracefulStop` (drain with `Connection: close`, then the worker, the outbox and the store; SIGTERM/SIGINT replace the hand-rolled handlers and timer), pinned by the spawn-SIGTERM check in `test/service-kit.test.js`. README and STATUS.json name v0.97.0 and the pinned v0.26.0 SDK.
- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; Trade keeps its site stylesheet, the disclaimers, the account bar and the "Recently shipped" line. Pins `openvibe-shared` v2.6.0 and `openvibe-publishing` v1.2.0.
- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; Trade keeps its site stylesheet, the disclaimers, the account bar and the "Recently shipped" line. Pins `openvibe-shared` v2.6.0 and `openvibe-publishing` v1.2.0.
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
internally, not launched**: it runs on the production host on 127.0.0.1:4860 only (release
internally, not launched**: it runs on the production host on 127.0.0.1:4860 only (release
`9a55644`; `/api/ready` reports `degraded: ["freshness"]` because there is no feed), with an empty
`9a55644`; `/api/ready` reports `degraded: ["freshness"]` because there is no feed), with an empty
database (0 instruments), and `openvibe.trade` still shows its placeholder from OpenVibe.Sites. Its
database (0 instruments), and `openvibe.trade` still shows its placeholder from OpenVibe.Sites. Its
-capabilities and service manifest are registered in openvibe-contracts v0.22.0. No market data
+capabilities and service manifest are registered in openvibe-contracts v0.97.0. No market data
feed is configured yet (see "What it shows today").
feed is configured yet (see "What it shows today").
**Domain:** `openvibe.trade` · **Port:** 4860 · **Service id:** `trade`
**Domain:** `openvibe.trade` · **Port:** 4860 · **Service id:** `trade`
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.11;
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.11;
@@ -178,9 +178,9 @@ Service tokens use audience `openvibe.trade`, one capability per route; private
@@ -178,9 +178,9 @@ Service tokens use audience `openvibe.trade`, one capability per route; private
and the editor list. The charter's `trade.watchlist.create|update`, `trade.instrument.resolve`,
and the editor list. The charter's `trade.watchlist.create|update`, `trade.instrument.resolve`,
`trade.alert.create|delete` and `trade.context.read` are all 3-segment ids here; `read`, `delete`,
`trade.alert.create|delete` and `trade.context.read` are all 3-segment ids here; `read`, `delete`,
`manage`, `observation.write` and `context.propose` are additions. They and the service manifest
`manage`, `observation.write` and `context.propose` are additions. They and the service manifest
-are released in openvibe-contracts v0.22.0 from the proposals in
+are released in openvibe-contracts (pinned at v0.97.0; the source proposals stay in
[docs/capabilities-proposal/](docs/capabilities-proposal/) and
[docs/capabilities-proposal/](docs/capabilities-proposal/) and
-[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). Grants for these ids are
+[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)). Grants for these ids are
decided with the contracts library's matching rule (`server/auth/capabilities.js`).
decided with the contracts library's matching rule (`server/auth/capabilities.js`).
### Events (SDK outbox, same transaction as the change)
### Events (SDK outbox, same transaction as the change)
@@ -222,10 +222,10 @@ services need to call Trade, is under [Grants the Network must hold](#grants-the
@@ -222,10 +222,10 @@ services need to call Trade, is under [Grants the Network must hold](#grants-the
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).
`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).
-- **Packages** (pinned by release tarball): `openvibe-contracts` v0.76.0, `openvibe-publishing`
- v1.1.0 (revisions, authorship, seo, index-hooks, ssr, ingest, publication), `openvibe-shared` v2.2.0 (Frame, app icon,
- footer, legal, release, metrics, ready), `openvibe-sdk` v0.20.0 (events outbox and inbox, webhook
- signatures v2, service tokens, per-actor limits).
+- **Packages** (pinned by release tarball): `openvibe-contracts` v0.97.0, `openvibe-publishing`
+ v1.3.0 (revisions, authorship, seo, index-hooks, ssr, ingest, publication), `openvibe-shared` v2.6.0 (Frame, app icon,
+ footer, legal, release, metrics, ready), `openvibe-sdk` v0.26.0 (events outbox and inbox, webhook
+ signatures v2, service tokens, per-actor limits, service kit lifecycle).
- **OpenVibe.Network:** SSO (OAuth client `trade`, redirect `https://openvibe.trade/auth/callback`),
- **OpenVibe.Network:** SSO (OAuth client `trade`, redirect `https://openvibe.trade/auth/callback`),
JWKS, client-credentials tokens.
JWKS, client-credentials tokens.
- **OpenVibe.Sources:** `sources.item.read` and `sources.source.read` (category `trade`).
- **OpenVibe.Sources:** `sources.item.read` and `sources.source.read` (category `trade`).
@@ -280,7 +280,7 @@ exists. Status against each point:
@@ -280,7 +280,7 @@ exists. Status against each point:
empty database); in production there is no market data source yet and the filings source is
empty database); in production there is no market data source yet and the filings source is
disabled in Sources (see "What it shows today").
disabled in Sources (see "What it shows today").
5. **Capability and event registration against OpenVibe.Contracts:** done (openvibe-contracts
5. **Capability and event registration against OpenVibe.Contracts:** done (openvibe-contracts
- v0.22.0).
+ v0.97.0).
6. **Migration and seed strategy, threat review, sitemap/robots/feed behaviour:** done. Nothing to
6. **Migration and seed strategy, threat review, sitemap/robots/feed behaviour:** done. Nothing to
migrate (no current implementation); no seed (editors add instruments); threat review below.
migrate (no current implementation); no seed (editors add instruments); threat review below.
7. **Acceptance tests:** done.
7. **Acceptance tests:** done.
@@ -382,7 +382,7 @@ First install (done once; kept for a rebuild):
@@ -382,7 +382,7 @@ First install (done once; kept for a rebuild):
4. **Search:** add `trade` to `SEARCH_EVENT_OWNERS` if it is not there.
4. **Search:** add `trade` to `SEARCH_EVENT_OWNERS` if it is not there.
5. **systemd:** install `deploy/systemd/openvibe-trade.service` (port 4860, `StateDirectory=openvibe-trade`).
5. **systemd:** install `deploy/systemd/openvibe-trade.service` (port 4860, `StateDirectory=openvibe-trade`).
6. **nginx:** install `deploy/nginx/openvibe.trade.conf` (`/metrics` and `/internal/` never proxied).
6. **nginx:** install `deploy/nginx/openvibe.trade.conf` (`/metrics` and `/internal/` never proxied).
-7. **Contracts:** done: the capabilities and manifest are released in openvibe-contracts v0.22.0,
+7. **Contracts:** done: the capabilities and manifest are released in openvibe-contracts v0.97.0,
and CI's contracts check runs against them.
and CI's contracts check runs against them.
8. **Data:** editors add instruments at `/editor`. Filings appear once `sec-xbrl-filings` is enabled
8. **Data:** editors add instruments at `/editor`. Filings appear once `sec-xbrl-filings` is enabled
in Sources (after a person verifies its terms); numbers appear only once a market data source
in Sources (after a person verifies its terms); numbers appear only once a market data source
@@ -8,12 +8,12 @@
@@ -8,12 +8,12 @@
"port": 4860,
"port": 4860,
"domain": "openvibe.trade",
"domain": "openvibe.trade",
"scope": "informational only (ADR-025): no custody, no order execution, no escrow, no listings between users, no personalised advice",
"scope": "informational only (ADR-025): no custody, no order execution, no escrow, no listings between users, no personalised advice",
- "contracts": "openvibe-contracts v0.79.0",
+ "contracts": "openvibe-contracts v0.97.0",
"packages": [
"packages": [
- "openvibe-contracts v0.79.0",
+ "openvibe-contracts v0.97.0",
"openvibe-publishing v1.3.0",
"openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
"openvibe-shared v2.6.0",
- "openvibe-sdk v0.25.0"
+ "openvibe-sdk v0.26.0"
],
],
"capabilities": [
"capabilities": [
"trade.instrument.resolve",
"trade.instrument.resolve",
@@ -36,7 +36,7 @@
@@ -36,7 +36,7 @@
"deployed": true,
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy trade`: unit openvibe-trade, 127.0.0.1:4860 loopback only; on 2026-09-23 /api/ready answered 200 with status degraded [freshness], which is honest: no feed is configured; Network OAuth client trade and its grants exist; no Events subscription; production database held 0 instruments and 0 observations. Deployed internally, not launched: openvibe.trade still serves the OpenVibe.Sites placeholder until a person verifies the SEC XBRL terms and enables sec-xbrl-filings in Sources, a licensed market data feed exists, and editors add instruments",
"deployment": "Deployed with `sudo ovhost deploy trade`: unit openvibe-trade, 127.0.0.1:4860 loopback only; on 2026-09-23 /api/ready answered 200 with status degraded [freshness], which is honest: no feed is configured; Network OAuth client trade and its grants exist; no Events subscription; production database held 0 instruments and 0 observations. Deployed internally, not launched: openvibe.trade still serves the OpenVibe.Sites placeholder until a person verifies the SEC XBRL terms and enables sec-xbrl-filings in Sources, a licensed market data feed exists, and editors add instruments",
"placeholderInSites": true,
"placeholderInSites": true,
- "updated": "2026-09-29",
+ "updated": "2026-10-04",
"features": [
"features": [
"informational only: a route-inventory test refuses any execution, custody or advice endpoint",
"informational only: a route-inventory test refuses any execution, custody or advice endpoint",
"instruments with exact, deterministic resolution (CIK, ticker, name)",
"instruments with exact, deterministic resolution (CIK, ticker, name)",
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@ -94,65 +94,6 @@
@@ -94,65 +94,6 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
"license": "MIT"
},
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "node_modules/better-sqlite3": {
- "version": "11.10.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
- }
- },
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
"node_modules/body-parser": {
"node_modules/body-parser": {
"version": "1.20.8",
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
@@ -177,32 +118,6 @@
@@ -177,32 +118,6 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -247,14 +162,6 @@
@@ -247,14 +162,6 @@
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
},
},
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/cluster-key-slot": {
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -322,34 +229,6 @@
@@ -322,34 +229,6 @@
"ms": "2.0.0"
"ms": "2.0.0"
}
}
},
},
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mimic-response": "^3.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=4.0.0"
- }
- },
"node_modules/denque": {
"node_modules/denque": {
"version": "2.1.0",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -378,17 +257,6 @@
@@ -378,17 +257,6 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/dotenv": {
"node_modules/dotenv": {
"version": "16.6.1",
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -439,17 +307,6 @@
@@ -439,17 +307,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/end-of-stream": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "once": "^1.4.0"
- }
- },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -495,17 +352,6 @@
@@ -495,17 +352,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/expand-template": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "license": "(MIT OR WTFPL)",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/express": {
"node_modules/express": {
"version": "4.22.3",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
@@ -589,14 +435,6 @@
@@ -589,14 +435,6 @@
],
],
"license": "BSD-3-Clause"
"license": "BSD-3-Clause"
},
},
- "node_modules/file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/finalhandler": {
"node_modules/finalhandler": {
"version": "1.3.2",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -633,14 +471,6 @@
@@ -633,14 +471,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/fs-constants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/function-bind": {
"node_modules/function-bind": {
"version": "1.1.2",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -687,14 +517,6 @@
@@ -687,14 +517,6 @@
"node": ">= 0.4"
"node": ">= 0.4"
}
}
},
},
- "node_modules/github-from-package": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/gopd": {
"node_modules/gopd": {
"version": "1.2.0",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -775,42 +597,12 @@
@@ -775,42 +597,12 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "peer": true
- },
"node_modules/inherits": {
"node_modules/inherits": {
"version": "2.0.4",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
"license": "ISC"
},
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/iovalkey": {
"node_modules/iovalkey": {
"version": "0.4.0",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
@@ -1041,53 +833,12 @@
@@ -1041,53 +833,12 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/ms": {
"node_modules/ms": {
"version": "2.0.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
"license": "MIT"
},
},
- "node_modules/napi-build-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/negotiator": {
"node_modules/negotiator": {
"version": "0.6.3",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1097,20 +848,6 @@
@@ -1097,20 +848,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/node-abi": {
- "version": "3.96.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/object-inspect": {
"node_modules/object-inspect": {
"version": "1.13.4",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -1135,21 +872,10 @@
@@ -1135,21 +872,10 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "optional": true,
- "peer": true,
- "dependencies": {
- "wrappy": "1"
- }
- },
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.79.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
- "integrity": "sha512-FY5GLbiOq0kDw5lUPPzX6h3ATbzvsr/mX5DUCF4GRfn6vGMP0p89rZZDhgtVDaS+yCBCYMZ8zFaZxuHEkEVwVg==",
+ "version": "0.97.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
+ "integrity": "sha512-HPGBJYx6Dh2kaTg5B41WGhpw38zUdoU1VnWtkcsFkaU4E521uQOJxf6sIaLdHMsahqov7b9k2HKEFCnD9065MQ==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1364,35 +1090,6 @@
@@ -1364,35 +1090,6 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
- "node_modules/prebuild-install": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^2.0.0",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- },
- "bin": {
- "prebuild-install": "bin.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/proxy-addr": {
"node_modules/proxy-addr": {
"version": "2.0.8",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
@@ -1410,18 +1107,6 @@
@@ -1410,18 +1107,6 @@
"url": "https://opencollective.com/express"
"url": "https://opencollective.com/express"
}
}
},
},
- "node_modules/pump": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
"node_modules/qs": {
"node_modules/qs": {
"version": "6.16.0",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
@@ -1462,39 +1147,6 @@
@@ -1462,39 +1147,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
- "optional": true,
- "peer": true,
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "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==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/redis-errors": {
"node_modules/redis-errors": {
"version": "1.2.0",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@@ -1686,55 +1338,6 @@
@@ -1686,55 +1338,6 @@
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
},
},
- "node_modules/simple-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "node_modules/simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
"node_modules/split2": {
"node_modules/split2": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1759,60 +1362,6 @@
@@ -1759,60 +1362,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "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==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tar-fs": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/toidentifier": {
"node_modules/toidentifier": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1822,20 +1371,6 @@
@@ -1822,20 +1371,6 @@
"node": ">=0.6"
"node": ">=0.6"
}
}
},
},
- "node_modules/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/type-is": {
"node_modules/type-is": {
"version": "1.6.18",
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1858,14 +1393,6 @@
@@ -1858,14 +1393,6 @@
"node": ">= 0.8"
"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==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/utils-merge": {
"node_modules/utils-merge": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1884,14 +1411,6 @@
@@ -1884,14 +1411,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/xtend": {
"node_modules/xtend": {
"version": "4.0.2",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -27,7 +27,7 @@
@@ -27,7 +27,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@ -1,14 +1,10 @@
@@ -1,14 +1,10 @@
'use strict';
'use strict';
/**
/**
- * Capability checks for service tokens (audience openvibe.trade), including the ids Trade
- * introduces before the contracts library knows them.
- *
- * openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its
- * manifests yet. Trade's ids are proposed in docs/capabilities-proposal/ for the next contracts
- * release; until then a grant is decided locally with the library's own matching rule (the exact
- * id, or a `prefix.*` grant covering it). An id the library does know always goes through the
- * library, so the day the release lands nothing changes here.
+ * Capability checks for service tokens (audience openvibe.trade). The trade.* ids this service
+ * introduces are defined by the installed openvibe-contracts, so a grant is decided by the
+ * library's own matching rule (the exact id, or a `prefix.*` grant covering it). CAPABILITIES keeps
+ * the ids in one place for the guards, the proposal documents and the tests.
*
*
* Browsers (Network user JWTs) are never judged by capabilities: watchlists and alert rules by
* Browsers (Network user JWTs) are never judged by capabilities: watchlists and alert rules by
* ownership, editing by the editor list (domain/access.js). A service token is judged by its
* ownership, editing by the editor list (domain/access.js). A service token is judged by its
@@ -32,16 +28,10 @@ const CAPABILITIES = Object.freeze({
@@ -32,16 +28,10 @@ const CAPABILITIES = Object.freeze({
ALERT_CREATE: 'trade.alert.create',
ALERT_CREATE: 'trade.alert.create',
ALERT_DELETE: 'trade.alert.delete',
ALERT_DELETE: 'trade.alert.delete',
});
});
-const PROPOSED = new Set(Object.values(CAPABILITIES));
/** → { allowed, code, reason } like capabilities.check(). */
/** → { allowed, code, reason } like capabilities.check(). */
function checkCapability(claims, capabilityId) {
function checkCapability(claims, capabilityId) {
- if (!capabilities.get(capabilityId) && PROPOSED.has(capabilityId)) {
- return capabilities.grants(claims && claims.cap, capabilityId)
- ? { allowed: true, code: null, reason: null }
- : { allowed: false, code: 'capability.denied', reason: `${capabilityId} not granted` };
- }
return capabilities.check(claims, capabilityId);
return capabilities.check(claims, capabilityId);
}
}
-module.exports = { CAPABILITIES, PROPOSED, checkCapability };
+module.exports = { CAPABILITIES, checkCapability };
@@ -3,8 +3,11 @@
@@ -3,8 +3,11 @@
/**
/**
* OpenVibe.Trade — process entry. `node server/index.js`
* OpenVibe.Trade — process entry. `node server/index.js`
* Listens on PORT (4860) behind nginx (deploy/). Starts the outbox relay (when EVENTS_URL and the
* Listens on PORT (4860) behind nginx (deploy/). Starts the outbox relay (when EVENTS_URL and the
- * client secret are set) and the worker (Sources sync, freshness).
+ * client secret are set) and the worker (Sources sync, freshness). On SIGTERM/SIGINT the service
+ * kit drains in-flight requests (Connection: close), stops the worker and the relay, closes the
+ * store, and exits 0 (the 5 s family's deadline exit code).
*/
*/
+const { gracefulStop } = require('openvibe-sdk/service');
const { createApp } = require('./app');
const { createApp } = require('./app');
(async () => {
(async () => {
@@ -19,16 +22,9 @@ server.keepAliveTimeout = 65_000;
@@ -19,16 +22,9 @@ server.keepAliveTimeout = 65_000;
ctx.outbox.start();
ctx.outbox.start();
ctx.worker.start();
ctx.worker.start();
-function shutdown(signal) {
- console.log(`[Trade] ${signal}: closing`);
- ctx.worker.stop();
- server.close(async () => {
- try { await ctx.outbox.stop(); } catch { /* best effort */ }
- try { await ctx.store.close(); } catch { /* already closed */ }
- process.exit(0);
- });
- setTimeout(() => process.exit(0), 5000).unref();
-}
-process.on('SIGTERM', () => shutdown('SIGTERM'));
-process.on('SIGINT', () => shutdown('SIGINT'));
+gracefulStop({
+ name: 'Trade', server, deadlineExitCode: 0,
+ stop: [() => ctx.worker.stop()],
+ close: [() => ctx.outbox.stop(), () => ctx.store.close()],
+});
})().catch((err) => { console.error('[Trade] failed to start:', err); process.exit(1); });
})().catch((err) => { console.error('[Trade] failed to start:', err); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+'use strict';
+/**
+ * The trade.* capabilities are released by openvibe-contracts, so the service guards delegate to the
+ * library's grant rule rather than deciding a proposed id locally (the fallback that existed while
+ * the ids were only proposals). These pin that: every guarded id is defined by the installed
+ * contracts and by the trade service manifest, and checkCapability agrees with capabilities.check
+ * for an exact grant, a prefix grant, no grant, and an id the contracts do not define.
+ */
+const assert = require('assert');
+const { capabilities, services } = require('openvibe-contracts');
+const { checkCapability, CAPABILITIES } = require('../server/auth/capabilities');
+const { check, done } = require('./helpers/boot');
+
+const GRANTED = { cap: ['trade.watchlist.create'], sub: 'svc:tools' };
+
+(async () => {
+ await check('every guarded capability is defined by the installed contracts and the trade manifest', async () => {
+ const manifest = services.get('trade');
+ assert.ok(manifest, 'openvibe-contracts defines the trade service manifest');
+ assert.deepStrictEqual([...manifest.capabilities].sort(), Object.values(CAPABILITIES).sort());
+ for (const id of Object.values(CAPABILITIES)) {
+ const cap = capabilities.get(id);
+ assert.ok(cap, `${id} is defined by openvibe-contracts`);
+ assert.strictEqual(cap.owner, 'trade', `${id} is owned by trade`);
+ }
+ });
+
+ await check('checkCapability follows the contracts grant rule (exact, prefix, denied, unknown)', async () => {
+ // An exact grant and a `prefix.*` grant the library's matching rule accepts.
+ assert.deepStrictEqual(checkCapability(GRANTED, 'trade.watchlist.create'), { allowed: true, code: null, reason: null });
+ assert.deepStrictEqual(checkCapability({ cap: ['trade.*'] }, 'trade.watchlist.create'), { allowed: true, code: null, reason: null });
+ // A grant of a sibling capability, or no cap at all, is denied with the library's code.
+ assert.strictEqual(checkCapability({ cap: ['trade.watchlist.read'] }, 'trade.watchlist.create').code, 'capability.denied');
+ assert.strictEqual(checkCapability(null, 'trade.watchlist.create').code, 'capability.denied');
+ // An id the contracts do not define still answers capability.unknown — the guard delegates,
+ // it never decides an unknown id on its own.
+ assert.strictEqual(checkCapability({ cap: ['trade.*'] }, 'trade.not.a.capability').code, 'capability.unknown');
+ });
+
+ done();
+})().catch((err) => { console.error(err); process.exit(1); });
@@ -1,15 +1,18 @@
@@ -1,15 +1,18 @@
'use strict';
'use strict';
/**
/**
- * The proposals the lead releases in the next openvibe-contracts version are valid against the
- * released schemas, match what the code enforces and emits, and every envelope Trade produces is a
- * valid events.event-envelope@1.
+ * The capability proposals in docs/capabilities-proposal/ are valid against the released schemas and
+ * match what the code enforces and emits; the ids are now released by openvibe-contracts (checked
+ * here and pinned in test/capabilities.test.js), and every envelope Trade produces is a valid
+ * events.event-envelope@1.
*/
*/
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
-const { PROPOSED } = require('../server/auth/capabilities');
+const { CAPABILITIES } = require('../server/auth/capabilities');
+
+const CAPS = new Set(Object.values(CAPABILITIES));
const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
@@ -25,14 +28,16 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
@@ -25,14 +28,16 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
assert.strictEqual(c.owner, 'trade');
assert.strictEqual(c.owner, 'trade');
assert.ok(c.id.split('.').length >= 3);
assert.ok(c.id.split('.').length >= 3);
assert.strictEqual(`${c.id}.json`, files[caps.indexOf(c)]);
assert.strictEqual(`${c.id}.json`, files[caps.indexOf(c)]);
- assert.ok(!contracts.capabilities.get(c.id) || contracts.capabilities.get(c.id).owner === 'trade', `${c.id} collides with a released capability`);
+ const released = contracts.capabilities.get(c.id);
+ assert.ok(released, `${c.id} is defined by openvibe-contracts`);
+ assert.strictEqual(released.owner, 'trade', `${c.id} collides with a released capability`);
}
}
});
});
await check('the proposals are exactly the capabilities the code enforces, and cover the charter\'s', async () => {
await check('the proposals are exactly the capabilities the code enforces, and cover the charter\'s', async () => {
- assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...PROPOSED].sort());
- assert.deepStrictEqual([...manifest.capabilities].sort(), [...PROPOSED].sort());
- for (const id of ['trade.watchlist.create', 'trade.watchlist.update', 'trade.instrument.resolve', 'trade.alert.create', 'trade.alert.delete', 'trade.context.read']) assert.ok(PROPOSED.has(id), id);
+ assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...CAPS].sort());
+ assert.deepStrictEqual([...manifest.capabilities].sort(), [...CAPS].sort());
+ for (const id of ['trade.watchlist.create', 'trade.watchlist.update', 'trade.instrument.resolve', 'trade.alert.create', 'trade.alert.delete', 'trade.context.read']) assert.ok(CAPS.has(id), id);
const src = fs.readFileSync(path.join(__dirname, '..', 'server', 'http', 'api.js'), 'utf8');
const src = fs.readFileSync(path.join(__dirname, '..', 'server', 'http', 'api.js'), 'utf8');
for (const c of caps) for (const route of c.implementedBy) assert.ok(src.includes(`'${route.split(' ')[1].replace('/api/v1', '')}'`), `${c.id}: ${route} is not in http/api.js`);
for (const c of caps) for (const route of c.implementedBy) assert.ok(src.includes(`'${route.split(' ')[1].replace('/api/v1', '')}'`), `${c.id}: ${route} is not in http/api.js`);
});
});
@@ -15,7 +15,7 @@ const fs = require('fs');
@@ -15,7 +15,7 @@ const fs = require('fs');
const path = require('path');
const path = require('path');
const express = require('express');
const express = require('express');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
-const { PROPOSED } = require('../server/auth/capabilities');
+const { CAPABILITIES } = require('../server/auth/capabilities');
const FORBIDDEN = ['order', 'buy', 'sell', 'execut', 'custod', 'wallet', 'escrow', 'checkout', 'listing',
const FORBIDDEN = ['order', 'buy', 'sell', 'execut', 'custod', 'wallet', 'escrow', 'checkout', 'listing',
'purchas', 'payment', 'payout', 'deposit', 'withdraw', 'cart'];
'purchas', 'payment', 'payout', 'deposit', 'withdraw', 'cart'];
@@ -94,7 +94,7 @@ function inventory(app) {
@@ -94,7 +94,7 @@ function inventory(app) {
});
});
await check('no capability id and no produced event type carries those semantics', async () => {
await check('no capability id and no produced event type carries those semantics', async () => {
- for (const id of PROPOSED) assert.deepStrictEqual(violations(id), [], id);
+ for (const id of Object.values(CAPABILITIES)) assert.deepStrictEqual(violations(id), [], id);
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));
for (const e of manifest.eventsProduced) assert.deepStrictEqual(violations(e), [], e);
for (const e of manifest.eventsProduced) assert.deepStrictEqual(violations(e), [], e);
for (const c of manifest.capabilities) assert.deepStrictEqual(violations(c), [], c);
for (const c of manifest.capabilities) assert.deepStrictEqual(violations(c), [], c);
@@ -5,11 +5,24 @@
@@ -5,11 +5,24 @@
* request.too_large for a body over the limit (the hand-rolled one folded that into 400
* request.too_large for a body over the limit (the hand-rolled one folded that into 400
* request.invalid_json), malformed JSON is still 400 request.invalid_json, and an ApiError carrying
* request.invalid_json), malformed JSON is still 400 request.invalid_json, and an ApiError carrying
* `extra` still answers its status/code/detail with extra spread into the problem body.
* `extra` still answers its status/code/detail with extra spread into the problem body.
+ *
+ * server/index.js now stops through openvibe-sdk/service's gracefulStop too (plan T2): the last check
+ * spawns the real entry point, holds a request in flight, sends SIGTERM, and pins that the drain
+ * answers it with Connection: close and the process exits 0.
*/
*/
const assert = require('assert');
const assert = require('assert');
+const fs = require('fs');
+const http = require('http');
+const net = require('net');
+const os = require('os');
+const path = require('path');
+const { once } = require('events');
+const { spawn } = require('child_process');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
const { ApiError, asApiError, run } = require('../server/http/errors');
const { ApiError, asApiError, run } = require('../server/http/errors');
+const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });
+
(async () => {
(async () => {
const t = await boot();
const t = await boot();
const svc = t.network.serviceToken('tools', ['trade.watchlist.create']);
const svc = t.network.serviceToken('tools', ['trade.watchlist.create']);
@@ -42,5 +55,69 @@ const { ApiError, asApiError, run } = require('../server/http/errors');
@@ -42,5 +55,69 @@ const { ApiError, asApiError, run } = require('../server/http/errors');
});
});
await t.close();
await t.close();
+
+ await check('SIGTERM drains an in-flight request (Connection: close) and exits 0', async () => {
+ // A database of its own, like the perf budget test: the real entry point, nothing shared.
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-trade-stop-'));
+ const port = await freePort();
+ const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {
+ cwd: path.join(__dirname, '..'),
+ env: {
+ ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test',
+ DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '',
+ TRADE_PGLITE_DIR: path.join(dir, 'pglite'), TRADE_WORKER: 'off',
+ },
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ let log = '';
+ child.stdout.on('data', (d) => { log = (log + d).slice(-4000); });
+ child.stderr.on('data', (d) => { log = (log + d).slice(-4000); });
+ const agent = new http.Agent({ keepAlive: true });
+ try {
+ const base = `http://127.0.0.1:${port}`;
+ let up = false;
+ for (let i = 0; i < 150 && !up; i++) {
+ up = await fetch(`${base}/api/health`).then((r) => r.ok).catch(() => false);
+ if (!up) await new Promise((r) => setTimeout(r, 100));
+ }
+ assert.ok(up, `the server did not start:\n${log}`);
+
+ // A form POST whose body is held open (the shared urlencoded parser waits for the end):
+ // it is in flight when SIGTERM arrives, so the drain must answer it rather than cut it.
+ let held = null;
+ const inFlight = new Promise((resolve, reject) => {
+ const req = http.request(`${base}/watchlists`, {
+ method: 'POST', agent, headers: { 'content-type': 'application/x-www-form-urlencoded' },
+ }, (res) => {
+ const chunks = [];
+ res.on('data', (c) => chunks.push(c));
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers }));
+ res.on('error', reject);
+ });
+ req.on('error', reject);
+ req.write('_csrf=held');
+ held = req;
+ });
+ await new Promise((r) => setTimeout(r, 300));
+ const exited = once(child, 'exit');
+ child.kill('SIGTERM');
+ held.end('&name=Held');
+
+ const res = await inFlight;
+ assert.strictEqual(res.headers.connection, 'close', `in-flight response was not Connection: close: ${JSON.stringify(res.headers)}`);
+ assert.ok(res.status >= 300, `an unsigned form POST answered ${res.status}`);
+ const [code] = await exited;
+ assert.strictEqual(code, 0, `expected exit 0, got ${code}\n${log}`);
+ assert.ok(/\[Trade\] SIGTERM: stopping/.test(log), `the service kit did not log the stop:\n${log}`);
+ } finally {
+ agent.destroy();
+ if (child.exitCode === null && child.signalCode === null) {
+ child.kill('SIGKILL');
+ await once(child, 'exit').catch(() => {});
+ }
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
done();
done();
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });