openvibers/openvibe.trade Commits

Pin openvibe-contracts v0.97.0, drop the proposed-capability shim, move shutdown onto the service kit (#12)

OP OpenVibers committed 5 Oct 2026 · 03c0021457 · 11 files, +166 −537

## Files changed

- `server/auth/capabilities.js` — deleted `PROPOSED`, the `capabilities.grants` fallback branch, and `PROPOSED` from exports; `checkCapability` now delegates straight to `capabilities.check` (all twelve `trade.*` ids are defined by v0.97.0), and the doc comment reflects that.
- `test/capabilities.test.js` (new, per Reviews `440a865`) — pins that every guarded id is defined by the installed contracts and the `trade` service manifest, and that `checkCapability` matches `capabilities.check` for an exact grant, a `trade.*` prefix grant, a sibling/no grant (`capability.denied`) and an undefined id (`capability.unknown`).
- `test/contracts.test.js` — imported `PROPOSED` → local `CAPS` set from `CAPABILITIES`; the released-id check now asserts the id is defined and owned by `trade` instead of "unknown or owned"; comment updated.
- `test/route-inventory.test.js` — iterates `Object.values(CAPABILITIES)` instead of the removed `PROPOSED`.
- `server/index.js` — hand-rolled `shutdown()`/`process.on(...)`/5 s timer replaced with `gracefulStop({ name: 'Trade', server, deadlineExitCode: 0, stop: [worker.stop], close: [outbox.stop, store.close] })` from `openvibe-sdk/service`; header comment updated.
- `test/service-kit.test.js` — added a spawn-SIGTERM check: boots the real `server/index.js` on an isolated PGlite DB, holds a form POST in flight, sends SIGTERM, and asserts the response drains with `Connection: close`, the service-kit stop is logged, and the process exits 0.
- `STATUS.json` — `contracts` and package list to `v0.97.0`, `openvibe-sdk` v0.25.0 → v0.26.0, `updated` → 2026-10-04.
- `README.md` — every contracts version reference (v0.22.0/v0.76.0) → v0.97.0, including the packages line; also corrected the stale publishing (v1.1.0→v1.3.0), shared (v2.2.0→v2.6.0) and SDK (v0.20.0→v0.26.0) pins on that line to match `package.json`.
- `CHANGELOG.md` — Unreleased entry for the v0.97.0 move, the guard simplification, and the shutdown move.

## Tests run

- `node test/capabilities.test.js` → **2 ok, all passed**.
- `node node_modules/openvibe-contracts/bin/check-service.js --service trade --src server` → **ok** (contracts 0.97.0, manifest range satisfied).
- `ov test test/capabilities.test.js test/contracts.test.js test/route-inventory.test.js` → **3 passed in 7 s**.
- `ov test test/service-kit.test.js test/subscribe.test.js` → **both passed** (subscribe.js path stays green); service-kit's four checks including the new SIGTERM drain pass, re-run 3× for stability.
- `ov test test/staff-map.test.js test/chassis.test.js test/pages.test.js test/delegation.test.js test/privacy.test.js test/perf-budget.test.js` → **6 passed in 17 s** (covers the contracts-schema consumers, service-token auth paths, and the other spawner of `server/index.js`).

## Notes / not done

- I did not touch `package.json` or `package-lock.json`; they already carried the v0.97.0 pin (harness-set).
- One judgment call: I added `deadlineExitCode: 0` to `gracefulStop`. The brief's snippet omits it, but the deleted hand-rolled timer exited 0 past its deadline, and `openvibe-sdk/docs/service.md` explicitly names Trade in the "5 s family" and says to keep that with `deadlineExitCode: 0` — so this preserves existing behaviour rather than silently changing the deadline exit to 1.
- Per this environment's instructions I did not run `ov test --all`; CI's verify runs the full suite after push. Nothing was blocked.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex bb6d003..52484f2 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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.
ddiff --git a/README.md b/README.md
iindex 963010d..7f09dc1 100644
---- a/README.md
++++ b/README.md
@@@ -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 empty
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").
**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; private
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`,
`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 are
decided 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 is
disabled 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 to
migrate (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 enabled
in Sources (after a person verifies its terms); numbers appear only once a market data source
ddiff --git a/STATUS.json b/STATUS.json
iindex 086bd05..407fd60 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)",
ddiff --git a/package-lock.json b/package-lock.json
iindex c055e35..72acf81 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex aeb0bf6..7cc5cac 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/server/auth/capabilities.js b/server/auth/capabilities.js
iindex c03d42e..60061c7 100644
---- a/server/auth/capabilities.js
++++ b/server/auth/capabilities.js
@@@ -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 };
ddiff --git a/server/index.js b/server/index.js
iindex 17dfcb1..3307989 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -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); });
ddiff --git a/test/capabilities.test.js b/test/capabilities.test.js
nnew file mode 100644
iindex 0000000..857dbdd
---- /dev/null
++++ b/test/capabilities.test.js
@@@ -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); });
ddiff --git a/test/contracts.test.js b/test/contracts.test.js
iindex 3e0397b..118d55b 100644
---- a/test/contracts.test.js
++++ b/test/contracts.test.js
@@@ -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`);
});
ddiff --git a/test/route-inventory.test.js b/test/route-inventory.test.js
iindex 14d248a..a7cd6b8 100644
---- a/test/route-inventory.test.js
++++ b/test/route-inventory.test.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);
ddiff --git a/test/service-kit.test.js b/test/service-kit.test.js
iindex 65bf22a..26e09a1 100644
---- a/test/service-kit.test.js
++++ b/test/service-kit.test.js
@@@ -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); });