openvibers/openvibe.sdk Commits
openvibe-sdk 0.44.0: the SQLite kits are gone (PostgreSQL only) (#41)
Changes
@@@ -3,6 +3,22 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease may change an API and says so here.++## 0.44.0 (2026-10-11)++++The SQLite kits are gone (plan T1, the deletion ledger): every OpenVibe service has run on PostgreSQL since++2026-10-08, and nothing in the estate used them. **Breaking** for a caller of what was removed:++++- `createOutbox` / `createInbox` (`openvibe-sdk/events`, better-sqlite3): use `createPgOutbox` / `createPgInbox` on an++ `openvibe-sdk/db` handle (`createDb({ pglite: true })` for a small embedded app), with `outboxSchema()` /++ `inboxSchema()` in its migration.++- `createRevocationStore` (`openvibe-sdk/auth`, better-sqlite3 or memory): use `createPgRevocationStore`.++- The stale `importSqlite` / `runSqliteMigration` type declarations (their code was already gone).++- `better-sqlite3` is no longer a devDependency or an optional peer dependency, so it leaves every service's lockfile++ with this pin.++- **Tests:** the SQLite outbox suite's two cases the PostgreSQL suite lacked move there (a token-endpoint refusal is++ retried, never rejected; a flush during a pass also publishes what was committed after it claimed). The delivery++ worker test's inbox runs on PGlite.++## 0.43.0 (2026-10-10)No sequence number from Events (Contracts 0.136.x, ADR-042 decision 7, its last step). **Breaking** only for code that@@@ -248,7 +248,7 @@ Server-only subpaths are declared `"browser": null` in the exports map, so a bun- `startAuthorization()` / `buildAuthorizeUrl()` default `scope` to `'profile theme'` only for first-party sign-in (no `audience`). Apps pass `audience` and capability ids. Network refuses `prompt=none` for apps.- `createServiceTokenClient().getTokenInfo({ audience })` returns `{ accessToken, scope, expiresAt, unverifiedClaims }`. `decodeUnverified()` / `unverifiedClaims()` decode a JWT without verifying it: for display and diagnostics, never for authorization.- PKCE follows RFC 7636 S256. The verifier is 64 characters from the unreserved set, and the challenge is `BASE64URL(SHA-256(verifier))`. Network verifies the verifier whenever the authorization carried a challenge, and requires S256 from every developer app.--- `createRevocationStore(db?)` keeps each person's token cutoff from Network's `network.user.token_valid_after` event (Contracts 0.39.0: sign out everywhere, a password change or reset, a ban, staff ending someone's sessions). Subscribe to the event, pass each delivery to `store.apply(event)` (`'revoked'` means the cutoff moved: close that person's sockets, drop their cached sessions), and after `verifyUserToken()` refuse the token when `store.isRevoked(claims)`. That is Network's rule, `iat * 1000 < valid_after`. With a better-sqlite3 handle the cutoffs survive restarts; the store only ever moves a cutoff forward and ignores anything not from `network`.++- `createPgRevocationStore(db)` keeps each person's token cutoff from Network's `network.user.token_valid_after` event (Contracts 0.39.0: sign out everywhere, a password change or reset, a ban, staff ending someone's sessions). Subscribe to the event, pass each delivery to `store.apply(event)` (`'revoked'` means the cutoff moved: close that person's sockets, drop their cached sessions), and after `verifyUserToken()` refuse the token when `store.isRevoked(claims)`. That is Network's rule, `iat * 1000 < valid_after`. The cutoffs live in the service's PostgreSQL (`revocationSchema()` for its migration; `await store.load()` at boot reads them into memory); the store only ever moves a cutoff forward and ignores anything not from `network`.- `createSsoClient()` (`openvibe-sdk/sso`) is the site side of first-party sign-in, the layer Blog, Coupons, Deals, Host, News and Trade each carried a copy of. `/auth/login` sends the browser to the Network with a random `state` and a PKCE S256 challenge kept in short-lived httpOnly cookies on `/auth`; `/auth/callback` requires the state (constant-time compare) and exchanges the code server-side with the client secret and the verifier. `next` is a same-site path, this site's https origin or the Network's; anything else, including control characters and backslashes, goes home. Sessions are verified offline with `verifyUserToken` through the shared JWKS client (the last good keys survive a Network outage), so a service principal or a typed token (a realtime ticket, a FedCM assertion) is never a session. Cookies are host-only: `ov_token` (the access JWT, readable by the shared navbar), `ov_refresh` (httpOnly, `Path=/auth`) and `ov_sso_hint`.@@@ -438,7 +438,7 @@ const thumbs = createQueue({ valkey, name: 'thumbs' });thumbs.process(async (job) => { … }, { concurrency: 4 });```--Install the drivers the service uses: `pg` and `iovalkey` (production), `@electric-sql/pglite` (tests). `better-sqlite3` is only for a small app that keeps its outbox or inbox in SQLite (`createOutbox`/`createInbox`).++Install the drivers the service uses: `pg` and `iovalkey` (production), `@electric-sql/pglite` (tests and small embedded apps). PostgreSQL is the only database: the better-sqlite3 outbox, inbox and revocation store were removed in 0.44.0.### Browser without a bundler@@@ -523,7 +523,7 @@ Service implementations and their data (each service), the contracts (OpenVibe.C## Depends on--Nothing at runtime. `openvibe-contracts` v0.86.0 and `better-sqlite3` are devDependencies (type checks, the outbox and inbox tests). At run time the clients talk to OpenVibe.Network (tokens, registry, discovery) and the service each one wraps.++Nothing at runtime. `openvibe-contracts` and `@electric-sql/pglite` are devDependencies (type checks, the database, outbox and inbox tests). At run time the clients talk to OpenVibe.Network (tokens, registry, discovery) and the service each one wraps.---@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.43.0",++ "version": "0.44.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.43.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.44.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, bot, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.43.0';++const SDK_VERSION = '0.44.0';module.exports = {createClient,@@@ -5,6 +5,6 @@ export const {createCodeVerifier, pkceChallenge, createPkcePair, createState, buildAuthorizeUrl, startAuthorization,readCallback, base64url, decodeUnverified, unverifiedClaims, createServiceTokenClient, verifyUserToken,verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys,-- exchangeCode, refreshUserToken, createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER,++ exchangeCode, refreshUserToken, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER,} = cjs;export default cjs;@@@ -2,8 +2,8 @@import cjs from '../src/events.js';export const {-- createEventsClient, verifyDelivery, signDelivery, parseDelivery, createOutbox, createInbox,-- createPgOutbox, createPgInbox, outboxSchema, inboxSchema, projectKey, appSource,-- createAppEvents, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders, createServiceOutbox,++ createEventsClient, verifyDelivery, signDelivery, parseDelivery, createPgOutbox, createPgInbox,++ outboxSchema, inboxSchema, projectKey, appSource, createAppEvents, verifyDeliveryV2,++ signDeliveryV2, signDeliveryHeaders, createServiceOutbox,} = cjs;export default cjs;@@@ -1,16 +1,15 @@{"name": "openvibe-sdk",-- "version": "0.43.0",++ "version": "0.44.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.43.0",++ "version": "0.44.0","license": "MIT","devDependencies": {"@electric-sql/pglite": "^0.5.8",-- "better-sqlite3": "^11.10.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0","pg": "^8.23.0"@@@ -20,7 +19,6 @@},"peerDependencies": {"@electric-sql/pglite": "^0.5.8",-- "better-sqlite3": ">=11","iovalkey": "^0.4.0","pg": "^8.23.0"},@@@ -28,9 +26,6 @@"@electric-sql/pglite": {"optional": true},-- "better-sqlite3": {-- "optional": true-- },"iovalkey": {"optional": true},@@@ -88,93 +83,6 @@}}},-- "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==",-- "dev": true,-- "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"-- },-- "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==",-- "dev": true,-- "hasInstallScript": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "dependencies": {-- "buffer": "^5.5.0",-- "inherits": "^2.0.4",-- "readable-stream": "^3.4.0"-- }-- },-- "node_modules/buffer": {-- "version": "5.7.1",-- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",-- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",-- "dev": true,-- "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",-- "dependencies": {-- "base64-js": "^1.3.1",-- "ieee754": "^1.1.13"-- }-- },-- "node_modules/chownr": {-- "version": "1.1.4",-- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",-- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",-- "dev": true,-- "license": "ISC"-- },"node_modules/cluster-key-slot": {"version": "1.1.2","resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",@@@ -203,32 +111,6 @@}}},-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "engines": {-- "node": ">=4.0.0"-- }-- },"node_modules/denque": {"version": "2.1.0","resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",@@@ -239,36 +121,6 @@"node": ">=0.10"}},-- "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==",-- "dev": true,-- "license": "Apache-2.0",-- "engines": {-- "node": ">=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==",-- "dev": true,-- "license": "MIT",-- "dependencies": {-- "once": "^1.4.0"-- }-- },-- "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==",-- "dev": true,-- "license": "(MIT OR WTFPL)",-- "engines": {-- "node": ">=6"-- }-- },"node_modules/fast-deep-equal": {"version": "3.1.3","resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",@@@ -293,62 +145,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==",-- "dev": true,-- "license": "MIT"-- },-- "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==",-- "dev": true,-- "license": "MIT"-- },-- "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==",-- "dev": true,-- "license": "MIT"-- },-- "node_modules/ieee754": {-- "version": "1.2.1",-- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",-- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",-- "dev": true,-- "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"-- },-- "node_modules/inherits": {-- "version": "2.0.4",-- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",-- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",-- "dev": true,-- "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==",-- "dev": true,-- "license": "ISC"-- },"node_modules/iovalkey": {"version": "0.4.0","resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",@@@ -391,36 +187,6 @@"dev": true,"license": "MIT"},-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT"-- },"node_modules/ms": {"version": "2.1.3","resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",@@@ -428,36 +194,6 @@"dev": true,"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==",-- "dev": true,-- "license": "MIT"-- },-- "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==",-- "dev": true,-- "license": "MIT",-- "dependencies": {-- "semver": "^7.3.5"-- },-- "engines": {-- "node": ">=10"-- }-- },-- "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==",-- "dev": true,-- "license": "ISC",-- "dependencies": {-- "wrappy": "1"-- }-- },"node_modules/openvibe-contracts": {"version": "0.129.0","resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",@@@ -615,76 +351,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.",-- "dev": true,-- "license": "MIT",-- "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/pump": {-- "version": "3.0.4",-- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",-- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",-- "dev": true,-- "license": "MIT",-- "dependencies": {-- "end-of-stream": "^1.1.0",-- "once": "^1.3.1"-- }-- },-- "node_modules/rc": {-- "version": "1.2.8",-- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",-- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",-- "dev": true,-- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",-- "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==",-- "dev": true,-- "license": "MIT",-- "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",@@@ -718,87 +384,6 @@"node": ">=0.10.0"}},-- "node_modules/safe-buffer": {-- "version": "5.2.1",-- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",-- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",-- "dev": true,-- "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"-- },-- "node_modules/semver": {-- "version": "7.8.5",-- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",-- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",-- "dev": true,-- "license": "ISC",-- "bin": {-- "semver": "bin/semver.js"-- },-- "engines": {-- "node": ">=10"-- }-- },-- "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==",-- "dev": true,-- "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"-- },-- "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==",-- "dev": true,-- "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",-- "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",@@@ -816,83 +401,6 @@"dev": true,"license": "MIT"},-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "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==",-- "dev": true,-- "license": "MIT",-- "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/tunnel-agent": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",-- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",-- "dev": true,-- "license": "Apache-2.0",-- "dependencies": {-- "safe-buffer": "^5.0.1"-- },-- "engines": {-- "node": "*"-- }-- },-- "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==",-- "dev": true,-- "license": "MIT"-- },-- "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==",-- "dev": true,-- "license": "ISC"-- },"node_modules/xtend": {"version": "4.0.2","resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.43.0",++ "version": "0.44.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -297,14 +297,12 @@"license": "MIT","devDependencies": {"@electric-sql/pglite": "^0.5.8",-- "better-sqlite3": "^11.10.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0","pg": "^8.23.0"},"peerDependencies": {"@electric-sql/pglite": "^0.5.8",-- "better-sqlite3": ">=11","iovalkey": "^0.4.0","pg": "^8.23.0"},@@@ -317,9 +315,6 @@},"iovalkey": {"optional": true-- },-- "better-sqlite3": {-- "optional": true}}}@@@ -10,6 +10,6 @@ const { verifyServiceToken } = require('./service');const { createJwksClient, jwksClient, jwksStatus } = require('./jwks');const { createNetworkKeys } = require('./keys');const { exchangeCode, refreshUserToken } = require('./oauth');--const { createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('./revocations');++const { createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('./revocations');--module.exports = { ...browser, createServiceTokenClient, verifyUserToken, verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys, exchangeCode, refreshUserToken, createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER };++module.exports = { ...browser, createServiceTokenClient, verifyUserToken, verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys, exchangeCode, refreshUserToken, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER };@@@ -7,7 +7,7 @@* tokens subscribes to the event, applies it here and asks isRevoked() after verifying a token:* Network's rule is `iat * 1000 < valid_after` → refuse.*-- * const cutoffs = createRevocationStore(db) // a better-sqlite3 handle; omit for memory only++ * const cutoffs = createPgRevocationStore(db) // an openvibe-sdk/db handle; await cutoffs.load() at boot* cutoffs.apply(envelope) → 'revoked' | 'unchanged' | 'ignored:<why>' (idempotent: keeps the later cutoff)* cutoffs.isRevoked(claims) → true when claims.iat is before claims.subject_id's cutoff* cutoffs.cutoffFor(subject) → ms (0 = none)@@@ -22,67 +22,6 @@ const EVENT_TYPE = 'network.user.token_valid_after';const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;const TABLE_RE = /^[a-z_][a-z0-9_]{0,62}$/;--function createRevocationStore(db = null, { table = 'ov_token_revocations', now = () => Date.now(), maxCache = 50000 } = {}) {-- if (!TABLE_RE.test(table)) throw new TypeError(`createRevocationStore: bad table name ${table}`);-- const cache = new Map();-- let q = null;-- function ready() {-- if (!db || q) return q;-- // floating-ok: better-sqlite3 db.exec is synchronous-- db.exec(`CREATE TABLE IF NOT EXISTS ${table} (-- subject_id TEXT PRIMARY KEY,-- valid_after_ms INTEGER NOT NULL,-- reason TEXT,-- updated_at INTEGER NOT NULL-- )`);-- q = {-- get: db.prepare(`SELECT valid_after_ms FROM ${table} WHERE subject_id = ?`),-- put: db.prepare(`INSERT INTO ${table} (subject_id, valid_after_ms, reason, updated_at) VALUES (?, ?, ?, ?)-- ON CONFLICT(subject_id) DO UPDATE SET valid_after_ms = excluded.valid_after_ms, reason = excluded.reason, updated_at = excluded.updated_at-- WHERE excluded.valid_after_ms > ${table}.valid_after_ms`),-- };-- return q;-- }---- function cutoffFor(subject) {-- if (!subject) return 0;-- if (cache.has(subject)) return cache.get(subject);-- const s = ready();-- const row = s ? s.get.get(subject) : null;-- const ms = row ? Number(row.valid_after_ms) || 0 : 0;-- if (cache.size >= maxCache) cache.clear();-- cache.set(subject, ms);-- return ms;-- }---- function record(subject, validAfterMs, reason = null) {-- if (!SUBJECT_RE.test(String(subject || '')) || !Number.isFinite(validAfterMs)) return false;-- if (!(validAfterMs > cutoffFor(subject))) return false;-- const s = ready();-- if (s) s.put.run(subject, validAfterMs, reason, now());-- cache.set(subject, validAfterMs);-- return true;-- }---- function apply(event) {-- if (!event || event.event_type !== EVENT_TYPE) return 'ignored:type';-- if (event.source !== 'network') return 'ignored:source';-- const p = event.payload && typeof event.payload === 'object' ? event.payload : {};-- const subject = p.subject && p.subject.type === 'user' ? p.subject.id : null;-- const ms = Date.parse(p.valid_after);-- if (!SUBJECT_RE.test(String(subject || '')) || !Number.isFinite(ms)) return 'ignored:payload';-- return record(subject, ms, typeof p.reason === 'string' ? p.reason.slice(0, 40) : null) ? 'revoked' : 'unchanged';-- }---- function isRevoked(claims) {-- if (!claims || typeof claims.iat !== 'number' || typeof claims.subject_id !== 'string') return false;-- return claims.iat * 1000 < cutoffFor(claims.subject_id);-- }---- return { apply, record, isRevoked, cutoffFor, EVENT_TYPE };--}----/** The PostgreSQL table createPgRevocationStore uses, for the service's migration. */function revocationSchema(table = 'ov_token_revocations') {if (!TABLE_RE.test(table)) throw new TypeError(`revocationSchema: bad table name ${table}`);return `CREATE TABLE IF NOT EXISTS ${table} (@@@ -145,4 +84,4 @@ function createPgRevocationStore(db, { table = 'ov_token_revocations', now = ()return { load, apply, record, isRevoked, cutoffFor, loaded: () => loaded, EVENT_TYPE };}--module.exports = { createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER: EVENT_TYPE };++module.exports = { createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER: EVENT_TYPE };@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.43.0';++const SDK_VERSION = '0.44.0';module.exports = {createClient,@@@ -374,11 +374,11 @@ function createAppEvents(client, { projectId, appId, onBehalfOf, baseUrl } = {})};}--const { createOutbox, createInbox, createPgOutbox, createPgInbox, outboxSchema, inboxSchema } = require('./outbox');++const { createPgOutbox, createPgInbox, outboxSchema, inboxSchema } = require('./outbox');const { createServiceOutbox } = require('./service-outbox');module.exports = {-- createEventsClient, verifyDelivery, signDelivery, parseDelivery, createOutbox, createInbox,++ createEventsClient, verifyDelivery, signDelivery, parseDelivery,createPgOutbox, createPgInbox, outboxSchema, inboxSchema,projectKey, appSource, createAppEvents, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders, createServiceOutbox,};@@@ -1,21 +1,19 @@'use strict';/**-- * openvibe-sdk/events (server): transactional outbox and inbox (ADR-004), on the service's own database:-- * createOutbox/createInbox on better-sqlite3 (services not yet moved), createPgOutbox/createPgInbox on an-- * openvibe-sdk/db handle (PostgreSQL, ADR-035; below). No dependency: the caller passes its database handle.++ * openvibe-sdk/events (server): transactional outbox and inbox (ADR-004) on the service's own PostgreSQL database++ * (an openvibe-sdk/db handle, ADR-035): createPgOutbox / createPgInbox, with outboxSchema / inboxSchema for the++ * service's migration. (The better-sqlite3 kits were removed in 0.44.0: every service runs on PostgreSQL.)** const events = createEventsClient(client, { source: 'live' });-- * const outbox = createOutbox(db, { events });-- * outbox.ensureSchema();-- * db.transaction(() => {-- * db.prepare('UPDATE streams SET is_live = 1 WHERE id = ?').run(id);-- * outbox.enqueue({ event_type: 'live.stream.started', actor, subject, payload });-- * })(); // the event exists if and only if the change committed++ * const outbox = createPgOutbox(db, { events });++ * await db.tx(async (t) => {++ * await t.exec(sql`UPDATE streams SET is_live = true WHERE id = ${id}`);++ * await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject, payload });++ * }); // the event exists if and only if the change committed* outbox.start(); // relay: publishes due rows, marks them sent, backs off on failure*-- * const inbox = createInbox(db);-- * inbox.ensureSchema();-- * inbox.once('network', event.event_id, () => { ...synchronous db writes... });++ * const inbox = createPgInbox(db);++ * await inbox.once('network', event.event_id, async (t) => { ...writes on t... });** Delivery is at least once: a crash between publish and mark-sent republishes the same event_id,* which OpenVibe.Events answers as a duplicate. A 4xx other than 401/408/409/425/429 is permanent@@@ -49,178 +47,6 @@ function isPermanent(err) {return Number.isInteger(s) && s >= 400 && s < 500 && !RETRYABLE_4XX.has(s);}--function createOutbox(db, {-- events, table = 'event_outbox', batchSize = 50, intervalMs = 1000,-- backoffMs = [1000, 5000, 30000, 120000, 600000], now = () => Date.now(), onError = null, allowOutsideTransaction = false,--} = {}) {-- if (!db || typeof db.prepare !== 'function') throw new TypeError('a better-sqlite3 database is required');-- if (!events || typeof events.publish !== 'function' || typeof events.prepare !== 'function') {-- throw new TypeError('events must be an openvibe-sdk events client (createEventsClient)');-- }-- if (!TABLE_RE.test(table)) throw new TypeError('bad outbox table name');-- let stmts = null;-- let timer = null;-- let running = false;---- function ensureSchema() {-- // floating-ok: better-sqlite3 db.exec is synchronous-- db.exec(`CREATE TABLE IF NOT EXISTS ${table} (-- id INTEGER PRIMARY KEY AUTOINCREMENT,-- event_id TEXT NOT NULL UNIQUE,-- envelope TEXT NOT NULL,-- traceparent TEXT,-- created_at INTEGER NOT NULL,-- attempts INTEGER NOT NULL DEFAULT 0,-- next_attempt_at INTEGER NOT NULL DEFAULT 0,-- sent_at INTEGER,-- seq INTEGER,-- rejected_at INTEGER,-- last_error TEXT-- );-- CREATE INDEX IF NOT EXISTS idx_${table}_due ON ${table}(sent_at, rejected_at, next_attempt_at);`);-- stmts = null;-- }---- function q() {-- if (!stmts) {-- stmts = {-- insert: db.prepare(`INSERT INTO ${table} (event_id, envelope, traceparent, created_at, next_attempt_at) VALUES (?, ?, ?, ?, 0)`),-- due: db.prepare(`SELECT * FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL AND next_attempt_at <= ? ORDER BY id LIMIT ?`),-- sent: db.prepare(`UPDATE ${table} SET sent_at = ?, attempts = attempts + 1, last_error = NULL WHERE id = ?`),-- failed: db.prepare(`UPDATE ${table} SET attempts = attempts + 1, next_attempt_at = ?, last_error = ? WHERE id = ?`),-- rejected: db.prepare(`UPDATE ${table} SET rejected_at = ?, attempts = attempts + 1, last_error = ? WHERE id = ?`),-- pending: db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL`),-- rejectedCount: db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE rejected_at IS NOT NULL`),-- prune: db.prepare(`DELETE FROM ${table} WHERE sent_at IS NOT NULL AND sent_at < ?`),-- };-- }-- return stmts;-- }---- /** INSIDE the caller's transaction. Returns the complete envelope (event_id, timestamp, ...). */-- function enqueue(envelope, { traceparent } = {}) {-- if (!allowOutsideTransaction && !db.inTransaction) {-- throw new Error('outbox.enqueue() must run inside the transaction that makes the change');-- }-- const m = traceparent && TRACEPARENT_RE.exec(traceparent);-- const env = events.prepare(envelope, { traceId: m ? m[1] : undefined, now: now() });-- q().insert.run(env.event_id, JSON.stringify(env), m ? traceparent : null, now());-- return env;-- }---- const backoff = (attempts) => backoffMs[Math.min(attempts, backoffMs.length - 1)];---- function markFailure(row, err) {-- const message = String((err && err.message) || err).slice(0, 500);-- if (isPermanent(err)) q().rejected.run(now(), message, row.id);-- else q().failed.run(now() + backoff(row.attempts), message, row.id);-- if (onError) { try { onError(err, row); } catch { /* reporting must not break the relay */ } }-- }---- async function publishRows(rows) {-- const stats = { sent: 0, failed: 0, rejected: 0 };-- const envelopes = rows.map((r) => JSON.parse(r.envelope));-- const traceparent = rows[0].traceparent || undefined;-- try {-- await events.publish(rows.length === 1 ? envelopes[0] : envelopes, { traceparent });-- db.transaction(() => { for (const row of rows) q().sent.run(now(), row.id); })();-- stats.sent += rows.length;-- } catch (err) {-- if (rows.length > 1 && isPermanent(err)) {-- // One bad envelope rejects the whole batch: isolate it.-- for (const row of rows) {-- const s = await publishRows([row]);-- stats.sent += s.sent; stats.failed += s.failed; stats.rejected += s.rejected;-- }-- } else {-- // floating-ok: markFailure here is the better-sqlite3 one above (synchronous .run)-- for (const row of rows) markFailure(row, err);-- if (isPermanent(err)) stats.rejected += rows.length; else stats.failed += rows.length;-- }-- }-- return stats;-- }---- async function doFlush() {-- const total = { sent: 0, failed: 0, rejected: 0 };-- for (;;) {-- const rows = q().due.all(now(), batchSize);-- if (!rows.length) break;-- const s = await publishRows(rows);-- total.sent += s.sent; total.failed += s.failed; total.rejected += s.rejected;-- if (s.failed || rows.length < batchSize) break;-- }-- return total;-- }---- /** Publish due rows once (serialFlush: a call during a pass also covers rows committed since that pass claimed). */-- const flush = serialFlush(doFlush);---- function schedule(ms) {-- if (!running) return;-- clearTimeout(timer);-- timer = setTimeout(async () => {-- timer = null;-- try { await flush(); } catch (err) { if (onError) { try { onError(err); } catch { /* ignore */ } } }-- schedule(intervalMs);-- }, ms);-- if (timer.unref) timer.unref();-- }---- return {-- ensureSchema,-- enqueue,-- flush,-- start() { if (!running) { running = true; schedule(0); } },-- stop() { running = false; clearTimeout(timer); timer = null; return flush.idle(); },-- /** Wake the relay now (e.g. right after the transaction commits). */-- kick() { schedule(0); },-- pending: () => q().pending.get().n,-- rejected: () => q().rejectedCount.get().n,-- /** Delete sent rows older than `olderThanMs` (default 7 days). */-- prune(olderThanMs = 7 * 24 * 60 * 60 * 1000) { return q().prune.run(now() - olderThanMs).changes; },-- };--}----function createInbox(db, { table = 'idempotency_receipts', now = () => Date.now() } = {}) {-- if (!db || typeof db.prepare !== 'function') throw new TypeError('a better-sqlite3 database is required');-- if (!TABLE_RE.test(table)) throw new TypeError('bad inbox table name');-- let claim = null;---- function ensureSchema() {-- // floating-ok: better-sqlite3 db.exec is synchronous-- db.exec(`CREATE TABLE IF NOT EXISTS ${table} (-- consumer TEXT NOT NULL,-- event_id TEXT NOT NULL,-- processed_at INTEGER NOT NULL,-- PRIMARY KEY (consumer, event_id)-- )`);-- claim = null;-- }---- /** Run fn exactly once per (consumer, eventId), in one SQLite transaction with the receipt. */-- function once(consumer, eventId, fn) {-- if (!consumer || !eventId) throw new TypeError('consumer and eventId are required');-- if (!claim) claim = db.prepare(`INSERT OR IGNORE INTO ${table} (consumer, event_id, processed_at) VALUES (?, ?, ?)`);-- return db.transaction(() => {-- if (claim.run(String(consumer), String(eventId), now()).changes === 0) return { duplicate: true };-- const result = fn();-- if (result && typeof result.then === 'function') {-- throw new TypeError('inbox.once(): fn must be synchronous (it runs inside a SQLite transaction)');-- }-- return { duplicate: false, result };-- })();-- }---- function seen(consumer, eventId) {-- return Boolean(db.prepare(`SELECT 1 FROM ${table} WHERE consumer = ? AND event_id = ?`).get(String(consumer), String(eventId)));-- }---- return { ensureSchema, once, seen };--}----// ── PostgreSQL (ADR-035): the same outbox and inbox on an openvibe-sdk/db handle ─────────────────--/** The outbox table's DDL, for a service migration (the runtime role cannot create tables). */function outboxSchema(table = 'event_outbox') {if (!TABLE_RE.test(table)) throw new TypeError('bad outbox table name');@@@ -382,4 +208,4 @@ function createPgInbox(db, { table = 'idempotency_receipts', now = () => Date.no};}--module.exports = { createOutbox, createInbox, createPgOutbox, createPgInbox, outboxSchema, inboxSchema, isPermanent };++module.exports = { createPgOutbox, createPgInbox, outboxSchema, inboxSchema, isPermanent };@@@ -5,7 +5,8 @@ const crypto = require('node:crypto');const { stubServer, send, run, waitFor } = require('./helpers');const { createClient, parseTraceparent } = require('../src/core');const { createServiceTokenClient } = require('../src/auth');--const { createEventsClient, verifyDelivery, signDelivery, parseDelivery, createInbox, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders } = require('../src/events');++const { createEventsClient, verifyDelivery, signDelivery, parseDelivery, createPgInbox, inboxSchema, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders } = require('../src/events');++const { createDb } = require('../src/db');const { subscribe } = require('../src/realtime');const { createMockPlatform } = require('../src/testing');@@@ -293,18 +294,18 @@ run([['deliverEvents: signed deliveries to a local endpoint, retried in order, dead after max attempts', async () => {const { platform, events } = setup();-- const Database = require('better-sqlite3');-- const inbox = createInbox(new Database(':memory:'));-- inbox.ensureSchema();++ const idb = createDb({ pglite: true, service: 'sdk-test' });++ await idb.query(inboxSchema());++ const inbox = createPgInbox(idb);let failNext = 1;let secret;const handled = [];-- const srv = await stubServer((req, res, body) => {++ const srv = await stubServer(async (req, res, body) => {const d = parseDelivery(body, req.headers, secret, { requireV2: true });if (!d) return send(res, 401, { error: 'bad signature' });if (failNext > 0) { failNext--; return send(res, 503, { error: 'busy' }); }assert.ok(!('seq' in d) && !('seq' in JSON.parse(body)) && req.headers['x-openvibe-seq'] === undefined, 'a delivery carries no sequence number');-- const r = inbox.once('webhook', d.event.event_id, () => handled.push([d.event.subject.id, d.attempt, req.headers['x-openvibe-event-type']]));++ const r = await inbox.once('webhook', d.event.event_id, async () => { handled.push([d.event.subject.id, d.attempt, req.headers['x-openvibe-event-type']]); });return send(res, 200, { duplicate: r.duplicate });});platform.publishEvent({ event_type: 'media.vod.ready', source: 'media', actor, subject: { type: 'vod', id: '0' } }); // before the subscription: not delivered@@@ -339,6 +340,7 @@ run([await waitFor(() => handled.length === 3);await worker.stop();await srv.close();++ await idb.close();}],['iterate pages by cursor alone: the unchanged cursor or latest_cursor ends it', async () => {const asked = [];ddeleted file mode 100644@@@ -1,139 +0,0 @@--'use strict';--/** Outbox/inbox: enqueue only inside a transaction, relay publishes once, retries, rejects, dedupes. */--const assert = require('node:assert/strict');--const fs = require('node:fs');--const os = require('node:os');--const path = require('node:path');--const Database = require('better-sqlite3');--const { run } = require('./helpers');--const { createClient } = require('../src/core');--const { createServiceTokenClient } = require('../src/auth');--const { createEventsClient, createOutbox, createInbox } = require('../src/events');--const { createMockPlatform } = require('../src/testing');----const actor = { type: 'service', id: 'live' };--const subject = { type: 'stream', id: '12', revision: 1 };----function setup() {-- const platform = createMockPlatform({-- clients: { live: { secret: 'live-secret', grants: [['events.event.publish', 'openvibe.events']] } },-- });-- const tokens = createServiceTokenClient({ clientId: 'live', clientSecret: 'live-secret', fetch: platform.fetch });-- const client = createClient({ fetch: platform.fetch, tokenProvider: tokens, retries: 0 });-- const events = createEventsClient(client, { source: 'live' });-- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-outbox-'));-- const db = new Database(path.join(dir, 'svc.db'));-- db.exec('CREATE TABLE streams (id INTEGER PRIMARY KEY, is_live INTEGER)');-- return { platform, events, db };--}----run([-- ['enqueue refuses to run outside a transaction', async () => {-- const { events, db } = setup();-- const outbox = createOutbox(db, { events });-- outbox.ensureSchema();-- assert.throws(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject }), /inside the transaction/);-- }],-- ['a rolled-back change leaves no event; a committed one is published once', async () => {-- const { platform, events, db } = setup();-- const outbox = createOutbox(db, { events });-- outbox.ensureSchema();-- assert.throws(() => db.transaction(() => {-- db.prepare('INSERT INTO streams (id, is_live) VALUES (1, 1)').run();-- outbox.enqueue({ event_type: 'live.stream.started', actor, subject });-- throw new Error('boom');-- })());-- assert.equal(outbox.pending(), 0);-- let env;-- db.transaction(() => {-- db.prepare('INSERT INTO streams (id, is_live) VALUES (2, 1)').run();-- env = outbox.enqueue({ event_type: 'live.stream.started', actor, subject, payload: { stream_id: 2 } }, { traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' });-- })();-- assert.match(env.event_id, /^evt_/);-- assert.equal(env.trace_id, '0af7651916cd43dd8448eb211c80319c');-- assert.equal(outbox.pending(), 1);-- const s = await outbox.flush();-- assert.deepEqual(s, { sent: 1, failed: 0, rejected: 0 });-- assert.equal(outbox.pending(), 0);-- assert.equal(platform.state.events.length, 1);-- assert.equal(platform.state.events[0].event.event_id, env.event_id);-- await outbox.flush();-- assert.equal(platform.state.events.length, 1, 'nothing republished');-- }],-- ['a transient failure backs off and retries; a batch publishes together', async () => {-- const { platform, events, db } = setup();-- let t = 1_000_000;-- let fail = true;-- const flaky = { prepare: events.prepare, publish: (...a) => (fail ? Promise.reject(Object.assign(new Error('down'), { status: 503 })) : events.publish(...a)) };-- const outbox = createOutbox(db, { events: flaky, now: () => t });-- outbox.ensureSchema();-- db.transaction(() => { for (let i = 0; i < 3; i++) outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: String(i) } }); })();-- assert.deepEqual(await outbox.flush(), { sent: 0, failed: 3, rejected: 0 });-- fail = false;-- assert.deepEqual(await outbox.flush(), { sent: 0, failed: 0, rejected: 0 }, 'not due yet');-- t += 1000;-- assert.deepEqual(await outbox.flush(), { sent: 3, failed: 0, rejected: 0 });-- assert.equal(platform.state.events.length, 3);-- }],-- ['a permanent 4xx rejects only the bad row', async () => {-- const { platform, events, db } = setup();-- const picky = {-- prepare: events.prepare,-- publish: (input, o) => {-- const list = Array.isArray(input) ? input : [input];-- if (list.some((e) => e.subject.id === 'bad')) return Promise.reject(Object.assign(new Error('events.type_not_allowed'), { status: 403 }));-- return events.publish(input, o);-- },-- };-- const outbox = createOutbox(db, { events: picky });-- outbox.ensureSchema();-- db.transaction(() => {-- outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'ok1' } });-- outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'bad' } });-- outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'ok2' } });-- })();-- assert.deepEqual(await outbox.flush(), { sent: 2, failed: 0, rejected: 1 });-- assert.equal(outbox.pending(), 0);-- assert.equal(outbox.rejected(), 1);-- assert.equal(platform.state.events.length, 2);-- }],-- ['a token-endpoint refusal is retried, never rejected', async () => {-- const { events, db } = setup();-- const noGrant = { prepare: events.prepare, publish: () => Promise.reject(Object.assign(new Error('unauthorized_client'), { status: 400, url: 'http://127.0.0.1:4000/oauth/token' })) };-- const outbox = createOutbox(db, { events: noGrant });-- outbox.ensureSchema();-- db.transaction(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject }))();-- assert.deepEqual(await outbox.flush(), { sent: 0, failed: 1, rejected: 0 });-- assert.equal(outbox.rejected(), 0);-- assert.equal(outbox.pending(), 1);-- }],-- ['a flush called during a pass also publishes what was committed after that pass claimed', async () => {-- const { events, db } = setup();-- let release; const gate = new Promise((r) => { release = r; });-- const sent = [];-- const slow = { prepare: events.prepare, publish: async (input, o) => { await gate; sent.push(input.subject.id); return await events.publish(input, o); } };-- const outbox = createOutbox(db, { events: slow });-- outbox.ensureSchema();-- db.transaction(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject }))();-- const first = outbox.flush();-- await new Promise((r) => setImmediate(r));-- db.transaction(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: '13', revision: 1 } }))();-- const second = outbox.flush();-- assert.equal(outbox.flush(), second, 'calls during a pass share the next one');-- release();-- await first; await second;-- assert.deepEqual(sent.sort(), ['12', '13']);-- assert.equal(outbox.pending(), 0);-- }],-- ['inbox runs a handler exactly once and rolls back with it', async () => {-- const { db } = setup();-- const inbox = createInbox(db);-- inbox.ensureSchema();-- assert.throws(() => inbox.once('network', 'evt_1', () => { db.prepare('INSERT INTO streams (id, is_live) VALUES (9, 1)').run(); throw new Error('crash'); }));-- assert.equal(inbox.seen('network', 'evt_1'), false);-- assert.equal(db.prepare('SELECT COUNT(*) AS n FROM streams').get().n, 0);-- assert.deepEqual(inbox.once('network', 'evt_1', () => { db.prepare('INSERT INTO streams (id, is_live) VALUES (9, 1)').run(); return 'done'; }), { duplicate: false, result: 'done' });-- assert.deepEqual(inbox.once('network', 'evt_1', () => { throw new Error('must not run'); }), { duplicate: true });-- assert.throws(() => inbox.once('network', 'evt_2', async () => {}), /synchronous/);-- }],--]);@@@ -81,6 +81,38 @@ function cases(label, open) {assert.equal(await o2.rejected(), 1);} finally { await done(); }}],++ [`${label}: a token-endpoint refusal is retried, never rejected`, async () => {++ const { db, done } = await open();++ try {++ const { events } = eventsClient();++ const noGrant = { prepare: events.prepare, publish: () => Promise.reject(Object.assign(new Error('unauthorized_client'), { status: 400, url: 'http://127.0.0.1:4000/oauth/token' })) };++ const outbox = createPgOutbox(db, { events: noGrant });++ await db.tx(async (t) => { await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'tok' } }); });++ assert.deepEqual(await outbox.flush(), { sent: 0, failed: 1, rejected: 0 }, 'no grant yet says nothing about the event');++ assert.equal(await outbox.rejected(), 0);++ assert.equal(await outbox.pending(), 1);++ } finally { await done(); }++ }],++ [`${label}: a flush during a pass also publishes what was committed after that pass claimed`, async () => {++ const { db, done } = await open();++ try {++ const { events } = eventsClient();++ let release; const gate = new Promise((r) => { release = r; });++ const sent = [];++ const slow = { prepare: events.prepare, publish: async (input, o) => { await gate; for (const e of [].concat(input)) sent.push(e.subject.id); return await events.publish(input, o); } };++ const outbox = createPgOutbox(db, { events: slow });++ await db.tx(async (t) => { await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: '12' } }); });++ const first = outbox.flush();++ await new Promise((r) => setTimeout(r, 20));++ await db.tx(async (t) => { await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: '13' } }); });++ const second = outbox.flush();++ assert.equal(outbox.flush(), second, 'calls during a pass share the next one');++ release();++ await first; await second;++ assert.deepEqual(sent.sort(), ['12', '13']);++ assert.equal(await outbox.pending(), 0);++ } finally { await done(); }++ }],[`${label}: the inbox runs a handler once, inside its transaction`, async () => {const { db, done } = await open();try {@@@ -1,38 +1,14 @@'use strict';--// openvibe-sdk/auth createRevocationStore: network.user.token_valid_after cutoffs (Contracts 0.39.0).++// openvibe-sdk/auth createPgRevocationStore: network.user.token_valid_after cutoffs (Contracts 0.39.0).const assert = require('assert');--const Database = require('better-sqlite3');--const { createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('../src/auth');++const { createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('../src/auth');const A = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';const B = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4';const ev = (subject, iso, over = {}) => ({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', event_type: TOKEN_VALID_AFTER, version: 1, source: 'network', payload: { subject: { type: 'user', id: subject }, valid_after: iso, reason: 'signed_out_everywhere' }, ...over });const T = Date.parse('2026-09-24T20:00:00Z');--const db = new Database(':memory:');--const s = createRevocationStore(db);--assert.strictEqual(s.isRevoked({ subject_id: A, iat: T / 1000 - 10 }), false, 'nothing known');--assert.strictEqual(s.apply(ev(A, new Date(T).toISOString())), 'revoked');--assert.strictEqual(s.isRevoked({ subject_id: A, iat: T / 1000 - 1 }), true, 'issued before: refused');--assert.strictEqual(s.isRevoked({ subject_id: A, iat: T / 1000 }), false, 'issued in the cutoff second: accepted (Network rule)');--assert.strictEqual(s.isRevoked({ subject_id: B, iat: T / 1000 - 1 }), false, 'someone else');--assert.strictEqual(s.isRevoked({ subject_id: A }), false, 'no iat, nothing to compare');--assert.strictEqual(s.apply(ev(A, new Date(T - 60000).toISOString())), 'unchanged', 'never moves back');--assert.strictEqual(s.apply(ev(A, new Date(T).toISOString())), 'unchanged', 'a redelivery');--assert.strictEqual(s.apply(ev(A, new Date(T + 1).toISOString(), { source: 'live' })), 'ignored:source');--assert.strictEqual(s.apply(ev('gst_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', new Date(T).toISOString())), 'ignored:payload');--assert.strictEqual(s.apply({ ...ev(A, 'soon') }), 'ignored:payload');--assert.strictEqual(s.apply({ event_type: 'network.module.updated' }), 'ignored:type');--assert.strictEqual(s.cutoffFor(A), T);----// Survives a restart (a new store on the same database); memory-only works without one.--assert.strictEqual(createRevocationStore(db).cutoffFor(A), T);--const mem = createRevocationStore();--assert.strictEqual(mem.apply(ev(B, new Date(T).toISOString())), 'revoked');--assert.strictEqual(mem.isRevoked({ subject_id: B, iat: T / 1000 - 5 }), true);--assert.throws(() => createRevocationStore(db, { table: 'x; DROP TABLE y' }), /bad table name/);----// createPgRevocationStore: the same rules on PostgreSQL (PGlite), cutoffs read into memory by load().++// createPgRevocationStore on PostgreSQL (PGlite): cutoffs read into memory by load(), written through.(async () => {const { createDb } = require('../src/db');const pg = createDb({ pglite: true, service: 'sdk-test' });@@@ -44,6 +20,12 @@ assert.throws(() => createRevocationStore(db, { table: 'x; DROP TABLE y' }), /baassert.strictEqual(p.isRevoked({ subject_id: A, iat: T / 1000 }), false);assert.strictEqual(await p.apply(ev(A, new Date(T - 60000).toISOString())), 'unchanged', 'never moves back');assert.strictEqual(await p.apply(ev(A, new Date(T + 1).toISOString(), { source: 'live' })), 'ignored:source');++ assert.strictEqual(p.isRevoked({ subject_id: B, iat: T / 1000 - 1 }), false, 'someone else');++ assert.strictEqual(p.isRevoked({ subject_id: A }), false, 'no iat, nothing to compare');++ assert.strictEqual(await p.apply(ev(A, new Date(T).toISOString())), 'unchanged', 'a redelivery');++ assert.strictEqual(await p.apply(ev('gst_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', new Date(T).toISOString())), 'ignored:payload');++ assert.strictEqual(await p.apply({ ...ev(A, 'soon') }), 'ignored:payload');++ assert.strictEqual(await p.apply({ event_type: 'network.module.updated' }), 'ignored:type');// Another process moved it further: record() refuses to go back and takes the stored value.await pg.prepare('UPDATE token_revocations SET valid_after_ms = ? WHERE subject_id = ?').run(T + 5000, A);assert.strictEqual(await p.record(A, T + 1000), false);@@@ -224,22 +224,7 @@ export interface ExchangeCodeOptions {export declare function exchangeCode(opts: ExchangeCodeOptions): Promise<UserTokenResponse>;export declare function refreshUserToken(opts: { refreshToken: string; clientId: string; clientSecret: string; network?: string; tokenUrl?: string; fetch?: FetchLike; timeoutMs?: number }): Promise<UserTokenResponse>;--/** Per-person token cutoffs from network.user.token_valid_after (Contracts 0.39.0). */--export interface RevocationStore {-- /** Apply an envelope: 'revoked' when the cutoff moved, 'unchanged', or 'ignored:<why>'. */-- apply(event: unknown): 'revoked' | 'unchanged' | `ignored:${string}`;-- /** Keep the later cutoff; true when it moved forward. */-- record(subject: string, validAfterMs: number, reason?: string | null): boolean;-- /** True when claims.iat (seconds) is before claims.subject_id's cutoff. */-- isRevoked(claims: { iat?: number; subject_id?: string } | null | undefined): boolean;-- /** The cutoff in ms, 0 when none. */-- cutoffFor(subject: string): number;-- readonly EVENT_TYPE: 'network.user.token_valid_after';--}--export interface RevocationStoreOptions { table?: string; now?: () => number; maxCache?: number }--/** A better-sqlite3 handle keeps cutoffs across restarts; without one they live in memory. */--export declare function createRevocationStore(db?: unknown, opts?: RevocationStoreOptions): RevocationStore;--/** The same cutoffs on PostgreSQL: load() reads them into memory; apply()/record() write through (async). */++/** Per-person token cutoffs from network.user.token_valid_after (Contracts 0.39.0), on PostgreSQL: load() reads them into memory; apply()/record() write through (async). */export interface PgRevocationStore {/** Read every stored cutoff into memory (at boot); resolves to how many. */load(): Promise<number>;@@@ -80,17 +80,5 @@ export interface CreateDbOptions {}export function createDb(opts?: CreateDbOptions): Db;export declare class DbError extends Error { code?: string; detail?: string; constraint?: string; table?: string; column?: string; statement?: string; cause?: unknown }--export interface ImportTableOptions { from?: string; dropColumns?: string[]; where?: string; map?: (row: Row) => Row | null }--export interface ImportReport { ok: boolean; tables: { table: string; source: string; rows: number; ms: number; checksum?: string }[]; problems: { table: string; problem: string }[] }--/** The one-time SQLite → PostgreSQL move with verification (needs better-sqlite3). */--export function importSqlite(o: { sqlite: string | object; db: Db; tables?: Record<string, ImportTableOptions>; only?: string[]; truncate?: boolean; verify?: boolean; log?: { log(msg: string): void } }): Promise<ImportReport>;export const PARSERS: Record<number, (v: string) => unknown>;export const ISOLATION: Record<string, string>;--/**-- * A service's scripts/migrate-to-postgres.js in one call: migrate (or --pglite), import into emptied tables, clean-- * text PostgreSQL refuses (reported), verify counts and checksums; resolves 0 when everything verified, else 1.-- */--export function runSqliteMigration(opts: {-- service: string; sqlite: string; directUrl?: string; migrations: string;-- tables?: Record<string, ImportTableOptions>; argv?: string[]; out?: (line: string) => void;--}): Promise<0 | 1>;@@@ -120,37 +120,7 @@ export interface ParseDeliveryOptions extends DeliveryV2Options {}export declare function parseDelivery(rawBody: RawBody, headers: Record<string, any> | Headers, secret: string, opts?: ParseDeliveryOptions): { event: EventEnvelope; subscriptionId: string | null; attempt: number } | null;--/** The subset of a better-sqlite3 Database the outbox and inbox use. */--export interface SqliteDatabase {-- prepare(sql: string): any;-- exec(sql: string): any;-- transaction<F extends (...args: any[]) => any>(fn: F): F;-- readonly inTransaction: boolean;--}export interface FlushStats { sent: number; failed: number; rejected: number; }--export interface Outbox {-- ensureSchema(): void;-- /** Inside the caller's transaction; returns the complete envelope. */-- enqueue(envelope: EventInput, opts?: { traceparent?: string }): EventEnvelope;-- flush(): Promise<FlushStats>;-- start(): void;-- stop(): Promise<unknown>;-- kick(): void;-- pending(): number;-- rejected(): number;-- prune(olderThanMs?: number): number;--}--export declare function createOutbox(db: SqliteDatabase, opts: {-- events: Pick<EventsClient, 'publish' | 'prepare'>;-- table?: string; batchSize?: number; intervalMs?: number; backoffMs?: number[];-- now?: () => number; onError?: (err: unknown, row?: unknown) => void; allowOutsideTransaction?: boolean;--}): Outbox;--export interface Inbox {-- ensureSchema(): void;-- once<T>(consumer: string, eventId: string, fn: () => T): { duplicate: true } | { duplicate: false; result: T };-- seen(consumer: string, eventId: string): boolean;--}--export declare function createInbox(db: SqliteDatabase, opts?: { table?: string; now?: () => number }): Inbox;/** The PostgreSQL outbox (ADR-035) on an openvibe-sdk/db handle; several relays may share one table. */export interface PgOutbox {
@@ -3,6 +3,22 @@
@@ -3,6 +3,22 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
release may change an API and says so here.
+## 0.44.0 (2026-10-11)
+
+The SQLite kits are gone (plan T1, the deletion ledger): every OpenVibe service has run on PostgreSQL since
+2026-10-08, and nothing in the estate used them. **Breaking** for a caller of what was removed:
+
+- `createOutbox` / `createInbox` (`openvibe-sdk/events`, better-sqlite3): use `createPgOutbox` / `createPgInbox` on an
+ `openvibe-sdk/db` handle (`createDb({ pglite: true })` for a small embedded app), with `outboxSchema()` /
+ `inboxSchema()` in its migration.
+- `createRevocationStore` (`openvibe-sdk/auth`, better-sqlite3 or memory): use `createPgRevocationStore`.
+- The stale `importSqlite` / `runSqliteMigration` type declarations (their code was already gone).
+- `better-sqlite3` is no longer a devDependency or an optional peer dependency, so it leaves every service's lockfile
+ with this pin.
+- **Tests:** the SQLite outbox suite's two cases the PostgreSQL suite lacked move there (a token-endpoint refusal is
+ retried, never rejected; a flush during a pass also publishes what was committed after it claimed). The delivery
+ worker test's inbox runs on PGlite.
+
## 0.43.0 (2026-10-10)
## 0.43.0 (2026-10-10)
No sequence number from Events (Contracts 0.136.x, ADR-042 decision 7, its last step). **Breaking** only for code that
No sequence number from Events (Contracts 0.136.x, ADR-042 decision 7, its last step). **Breaking** only for code that
@@ -248,7 +248,7 @@ Server-only subpaths are declared `"browser": null` in the exports map, so a bun
@@ -248,7 +248,7 @@ Server-only subpaths are declared `"browser": null` in the exports map, so a bun
- `startAuthorization()` / `buildAuthorizeUrl()` default `scope` to `'profile theme'` only for first-party sign-in (no `audience`). Apps pass `audience` and capability ids. Network refuses `prompt=none` for apps.
- `startAuthorization()` / `buildAuthorizeUrl()` default `scope` to `'profile theme'` only for first-party sign-in (no `audience`). Apps pass `audience` and capability ids. Network refuses `prompt=none` for apps.
- `createServiceTokenClient().getTokenInfo({ audience })` returns `{ accessToken, scope, expiresAt, unverifiedClaims }`. `decodeUnverified()` / `unverifiedClaims()` decode a JWT without verifying it: for display and diagnostics, never for authorization.
- `createServiceTokenClient().getTokenInfo({ audience })` returns `{ accessToken, scope, expiresAt, unverifiedClaims }`. `decodeUnverified()` / `unverifiedClaims()` decode a JWT without verifying it: for display and diagnostics, never for authorization.
- PKCE follows RFC 7636 S256. The verifier is 64 characters from the unreserved set, and the challenge is `BASE64URL(SHA-256(verifier))`. Network verifies the verifier whenever the authorization carried a challenge, and requires S256 from every developer app.
- PKCE follows RFC 7636 S256. The verifier is 64 characters from the unreserved set, and the challenge is `BASE64URL(SHA-256(verifier))`. Network verifies the verifier whenever the authorization carried a challenge, and requires S256 from every developer app.
-- `createRevocationStore(db?)` keeps each person's token cutoff from Network's `network.user.token_valid_after` event (Contracts 0.39.0: sign out everywhere, a password change or reset, a ban, staff ending someone's sessions). Subscribe to the event, pass each delivery to `store.apply(event)` (`'revoked'` means the cutoff moved: close that person's sockets, drop their cached sessions), and after `verifyUserToken()` refuse the token when `store.isRevoked(claims)`. That is Network's rule, `iat * 1000 < valid_after`. With a better-sqlite3 handle the cutoffs survive restarts; the store only ever moves a cutoff forward and ignores anything not from `network`.
+- `createPgRevocationStore(db)` keeps each person's token cutoff from Network's `network.user.token_valid_after` event (Contracts 0.39.0: sign out everywhere, a password change or reset, a ban, staff ending someone's sessions). Subscribe to the event, pass each delivery to `store.apply(event)` (`'revoked'` means the cutoff moved: close that person's sockets, drop their cached sessions), and after `verifyUserToken()` refuse the token when `store.isRevoked(claims)`. That is Network's rule, `iat * 1000 < valid_after`. The cutoffs live in the service's PostgreSQL (`revocationSchema()` for its migration; `await store.load()` at boot reads them into memory); the store only ever moves a cutoff forward and ignores anything not from `network`.
- `createSsoClient()` (`openvibe-sdk/sso`) is the site side of first-party sign-in, the layer Blog, Coupons, Deals, Host, News and Trade each carried a copy of. `/auth/login` sends the browser to the Network with a random `state` and a PKCE S256 challenge kept in short-lived httpOnly cookies on `/auth`; `/auth/callback` requires the state (constant-time compare) and exchanges the code server-side with the client secret and the verifier. `next` is a same-site path, this site's https origin or the Network's; anything else, including control characters and backslashes, goes home. Sessions are verified offline with `verifyUserToken` through the shared JWKS client (the last good keys survive a Network outage), so a service principal or a typed token (a realtime ticket, a FedCM assertion) is never a session. Cookies are host-only: `ov_token` (the access JWT, readable by the shared navbar), `ov_refresh` (httpOnly, `Path=/auth`) and `ov_sso_hint`.
- `createSsoClient()` (`openvibe-sdk/sso`) is the site side of first-party sign-in, the layer Blog, Coupons, Deals, Host, News and Trade each carried a copy of. `/auth/login` sends the browser to the Network with a random `state` and a PKCE S256 challenge kept in short-lived httpOnly cookies on `/auth`; `/auth/callback` requires the state (constant-time compare) and exchanges the code server-side with the client secret and the verifier. `next` is a same-site path, this site's https origin or the Network's; anything else, including control characters and backslashes, goes home. Sessions are verified offline with `verifyUserToken` through the shared JWKS client (the last good keys survive a Network outage), so a service principal or a typed token (a realtime ticket, a FedCM assertion) is never a session. Cookies are host-only: `ov_token` (the access JWT, readable by the shared navbar), `ov_refresh` (httpOnly, `Path=/auth`) and `ov_sso_hint`.
@@ -438,7 +438,7 @@ const thumbs = createQueue({ valkey, name: 'thumbs' });
@@ -438,7 +438,7 @@ const thumbs = createQueue({ valkey, name: 'thumbs' });
thumbs.process(async (job) => { … }, { concurrency: 4 });
thumbs.process(async (job) => { … }, { concurrency: 4 });
```
```
-Install the drivers the service uses: `pg` and `iovalkey` (production), `@electric-sql/pglite` (tests). `better-sqlite3` is only for a small app that keeps its outbox or inbox in SQLite (`createOutbox`/`createInbox`).
+Install the drivers the service uses: `pg` and `iovalkey` (production), `@electric-sql/pglite` (tests and small embedded apps). PostgreSQL is the only database: the better-sqlite3 outbox, inbox and revocation store were removed in 0.44.0.
### Browser without a bundler
### Browser without a bundler
@@ -523,7 +523,7 @@ Service implementations and their data (each service), the contracts (OpenVibe.C
@@ -523,7 +523,7 @@ Service implementations and their data (each service), the contracts (OpenVibe.C
## Depends on
## Depends on
-Nothing at runtime. `openvibe-contracts` v0.86.0 and `better-sqlite3` are devDependencies (type checks, the outbox and inbox tests). At run time the clients talk to OpenVibe.Network (tokens, registry, discovery) and the service each one wraps.
+Nothing at runtime. `openvibe-contracts` and `@electric-sql/pglite` are devDependencies (type checks, the database, outbox and inbox tests). At run time the clients talk to OpenVibe.Network (tokens, registry, discovery) and the service each one wraps.
---
---
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.43.0",
+ "version": "0.44.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.43.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.44.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, bot, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, bot, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);
@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.43.0';
+const SDK_VERSION = '0.44.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -5,6 +5,6 @@ export const {
@@ -5,6 +5,6 @@ export const {
createCodeVerifier, pkceChallenge, createPkcePair, createState, buildAuthorizeUrl, startAuthorization,
createCodeVerifier, pkceChallenge, createPkcePair, createState, buildAuthorizeUrl, startAuthorization,
readCallback, base64url, decodeUnverified, unverifiedClaims, createServiceTokenClient, verifyUserToken,
readCallback, base64url, decodeUnverified, unverifiedClaims, createServiceTokenClient, verifyUserToken,
verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys,
verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys,
- exchangeCode, refreshUserToken, createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER,
+ exchangeCode, refreshUserToken, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -2,8 +2,8 @@
@@ -2,8 +2,8 @@
import cjs from '../src/events.js';
import cjs from '../src/events.js';
export const {
export const {
- createEventsClient, verifyDelivery, signDelivery, parseDelivery, createOutbox, createInbox,
- createPgOutbox, createPgInbox, outboxSchema, inboxSchema, projectKey, appSource,
- createAppEvents, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders, createServiceOutbox,
+ createEventsClient, verifyDelivery, signDelivery, parseDelivery, createPgOutbox, createPgInbox,
+ outboxSchema, inboxSchema, projectKey, appSource, createAppEvents, verifyDeliveryV2,
+ signDeliveryV2, signDeliveryHeaders, createServiceOutbox,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -1,16 +1,15 @@
@@ -1,16 +1,15 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.43.0",
+ "version": "0.44.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.43.0",
+ "version": "0.44.0",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
- "better-sqlite3": "^11.10.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -20,7 +19,6 @@
@@ -20,7 +19,6 @@
},
},
"peerDependencies": {
"peerDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
- "better-sqlite3": ">=11",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -28,9 +26,6 @@
@@ -28,9 +26,6 @@
"@electric-sql/pglite": {
"@electric-sql/pglite": {
"optional": true
"optional": true
},
},
- "better-sqlite3": {
- "optional": true
- },
"iovalkey": {
"iovalkey": {
"optional": true
"optional": true
},
},
@@ -88,93 +83,6 @@
@@ -88,93 +83,6 @@
}
}
}
}
},
},
- "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==",
- "dev": true,
- "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"
- },
- "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==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
- "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",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "dev": true,
- "license": "ISC"
- },
"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",
@@ -203,32 +111,6 @@
@@ -203,32 +111,6 @@
}
}
}
}
},
},
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "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",
@@ -239,36 +121,6 @@
@@ -239,36 +121,6 @@
"node": ">=0.10"
"node": ">=0.10"
}
}
},
},
- "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==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=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==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "once": "^1.4.0"
- }
- },
- "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==",
- "dev": true,
- "license": "(MIT OR WTFPL)",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/fast-deep-equal": {
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -293,62 +145,6 @@
@@ -293,62 +145,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==",
- "dev": true,
- "license": "MIT"
- },
- "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==",
- "dev": true,
- "license": "MIT"
- },
- "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==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "dev": true,
- "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"
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true,
- "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==",
- "dev": true,
- "license": "ISC"
- },
"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",
@@ -391,36 +187,6 @@
@@ -391,36 +187,6 @@
"dev": true,
"dev": true,
"license": "MIT"
"license": "MIT"
},
},
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/ms": {
"node_modules/ms": {
"version": "2.1.3",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -428,36 +194,6 @@
@@ -428,36 +194,6 @@
"dev": true,
"dev": true,
"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==",
- "dev": true,
- "license": "MIT"
- },
- "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==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "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==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
"version": "0.129.0",
"version": "0.129.0",
"resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
@@ -615,76 +351,6 @@
@@ -615,76 +351,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.",
- "dev": true,
- "license": "MIT",
- "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/pump": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "dev": true,
- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
- "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==",
- "dev": true,
- "license": "MIT",
- "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",
@@ -718,87 +384,6 @@
@@ -718,87 +384,6 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true,
- "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"
- },
- "node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "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==",
- "dev": true,
- "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"
- },
- "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==",
- "dev": true,
- "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",
- "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",
@@ -816,83 +401,6 @@
@@ -816,83 +401,6 @@
"dev": true,
"dev": true,
"license": "MIT"
"license": "MIT"
},
},
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "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==",
- "dev": true,
- "license": "MIT",
- "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/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
- "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==",
- "dev": true,
- "license": "MIT"
- },
- "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==",
- "dev": true,
- "license": "ISC"
- },
"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",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.43.0",
+ "version": "0.44.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -297,14 +297,12 @@
@@ -297,14 +297,12 @@
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
- "better-sqlite3": "^11.10.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"peerDependencies": {
"peerDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
- "better-sqlite3": ">=11",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -317,9 +315,6 @@
@@ -317,9 +315,6 @@
},
},
"iovalkey": {
"iovalkey": {
"optional": true
"optional": true
- },
- "better-sqlite3": {
- "optional": true
}
}
}
}
}
}
@@ -10,6 +10,6 @@ const { verifyServiceToken } = require('./service');
@@ -10,6 +10,6 @@ const { verifyServiceToken } = require('./service');
const { createJwksClient, jwksClient, jwksStatus } = require('./jwks');
const { createJwksClient, jwksClient, jwksStatus } = require('./jwks');
const { createNetworkKeys } = require('./keys');
const { createNetworkKeys } = require('./keys');
const { exchangeCode, refreshUserToken } = require('./oauth');
const { exchangeCode, refreshUserToken } = require('./oauth');
-const { createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('./revocations');
+const { createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('./revocations');
-module.exports = { ...browser, createServiceTokenClient, verifyUserToken, verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys, exchangeCode, refreshUserToken, createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER };
+module.exports = { ...browser, createServiceTokenClient, verifyUserToken, verifyAppToken, verifyServiceToken, createJwksClient, jwksClient, jwksStatus, createNetworkKeys, exchangeCode, refreshUserToken, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER };
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
* tokens subscribes to the event, applies it here and asks isRevoked() after verifying a token:
* tokens subscribes to the event, applies it here and asks isRevoked() after verifying a token:
* Network's rule is `iat * 1000 < valid_after` → refuse.
* Network's rule is `iat * 1000 < valid_after` → refuse.
*
*
- * const cutoffs = createRevocationStore(db) // a better-sqlite3 handle; omit for memory only
+ * const cutoffs = createPgRevocationStore(db) // an openvibe-sdk/db handle; await cutoffs.load() at boot
* cutoffs.apply(envelope) → 'revoked' | 'unchanged' | 'ignored:<why>' (idempotent: keeps the later cutoff)
* cutoffs.apply(envelope) → 'revoked' | 'unchanged' | 'ignored:<why>' (idempotent: keeps the later cutoff)
* cutoffs.isRevoked(claims) → true when claims.iat is before claims.subject_id's cutoff
* cutoffs.isRevoked(claims) → true when claims.iat is before claims.subject_id's cutoff
* cutoffs.cutoffFor(subject) → ms (0 = none)
* cutoffs.cutoffFor(subject) → ms (0 = none)
@@ -22,67 +22,6 @@ const EVENT_TYPE = 'network.user.token_valid_after';
@@ -22,67 +22,6 @@ const EVENT_TYPE = 'network.user.token_valid_after';
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const TABLE_RE = /^[a-z_][a-z0-9_]{0,62}$/;
const TABLE_RE = /^[a-z_][a-z0-9_]{0,62}$/;
-function createRevocationStore(db = null, { table = 'ov_token_revocations', now = () => Date.now(), maxCache = 50000 } = {}) {
- if (!TABLE_RE.test(table)) throw new TypeError(`createRevocationStore: bad table name ${table}`);
- const cache = new Map();
- let q = null;
- function ready() {
- if (!db || q) return q;
- // floating-ok: better-sqlite3 db.exec is synchronous
- db.exec(`CREATE TABLE IF NOT EXISTS ${table} (
- subject_id TEXT PRIMARY KEY,
- valid_after_ms INTEGER NOT NULL,
- reason TEXT,
- updated_at INTEGER NOT NULL
- )`);
- q = {
- get: db.prepare(`SELECT valid_after_ms FROM ${table} WHERE subject_id = ?`),
- put: db.prepare(`INSERT INTO ${table} (subject_id, valid_after_ms, reason, updated_at) VALUES (?, ?, ?, ?)
- ON CONFLICT(subject_id) DO UPDATE SET valid_after_ms = excluded.valid_after_ms, reason = excluded.reason, updated_at = excluded.updated_at
- WHERE excluded.valid_after_ms > ${table}.valid_after_ms`),
- };
- return q;
- }
-
- function cutoffFor(subject) {
- if (!subject) return 0;
- if (cache.has(subject)) return cache.get(subject);
- const s = ready();
- const row = s ? s.get.get(subject) : null;
- const ms = row ? Number(row.valid_after_ms) || 0 : 0;
- if (cache.size >= maxCache) cache.clear();
- cache.set(subject, ms);
- return ms;
- }
-
- function record(subject, validAfterMs, reason = null) {
- if (!SUBJECT_RE.test(String(subject || '')) || !Number.isFinite(validAfterMs)) return false;
- if (!(validAfterMs > cutoffFor(subject))) return false;
- const s = ready();
- if (s) s.put.run(subject, validAfterMs, reason, now());
- cache.set(subject, validAfterMs);
- return true;
- }
-
- function apply(event) {
- if (!event || event.event_type !== EVENT_TYPE) return 'ignored:type';
- if (event.source !== 'network') return 'ignored:source';
- const p = event.payload && typeof event.payload === 'object' ? event.payload : {};
- const subject = p.subject && p.subject.type === 'user' ? p.subject.id : null;
- const ms = Date.parse(p.valid_after);
- if (!SUBJECT_RE.test(String(subject || '')) || !Number.isFinite(ms)) return 'ignored:payload';
- return record(subject, ms, typeof p.reason === 'string' ? p.reason.slice(0, 40) : null) ? 'revoked' : 'unchanged';
- }
-
- function isRevoked(claims) {
- if (!claims || typeof claims.iat !== 'number' || typeof claims.subject_id !== 'string') return false;
- return claims.iat * 1000 < cutoffFor(claims.subject_id);
- }
-
- return { apply, record, isRevoked, cutoffFor, EVENT_TYPE };
-}
-
-/** The PostgreSQL table createPgRevocationStore uses, for the service's migration. */
function revocationSchema(table = 'ov_token_revocations') {
function revocationSchema(table = 'ov_token_revocations') {
if (!TABLE_RE.test(table)) throw new TypeError(`revocationSchema: bad table name ${table}`);
if (!TABLE_RE.test(table)) throw new TypeError(`revocationSchema: bad table name ${table}`);
return `CREATE TABLE IF NOT EXISTS ${table} (
return `CREATE TABLE IF NOT EXISTS ${table} (
@@ -145,4 +84,4 @@ function createPgRevocationStore(db, { table = 'ov_token_revocations', now = ()
@@ -145,4 +84,4 @@ function createPgRevocationStore(db, { table = 'ov_token_revocations', now = ()
return { load, apply, record, isRevoked, cutoffFor, loaded: () => loaded, EVENT_TYPE };
return { load, apply, record, isRevoked, cutoffFor, loaded: () => loaded, EVENT_TYPE };
}
}
-module.exports = { createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER: EVENT_TYPE };
+module.exports = { createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER: EVENT_TYPE };
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.43.0';
+const SDK_VERSION = '0.44.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -374,11 +374,11 @@ function createAppEvents(client, { projectId, appId, onBehalfOf, baseUrl } = {})
@@ -374,11 +374,11 @@ function createAppEvents(client, { projectId, appId, onBehalfOf, baseUrl } = {})
};
};
}
}
-const { createOutbox, createInbox, createPgOutbox, createPgInbox, outboxSchema, inboxSchema } = require('./outbox');
+const { createPgOutbox, createPgInbox, outboxSchema, inboxSchema } = require('./outbox');
const { createServiceOutbox } = require('./service-outbox');
const { createServiceOutbox } = require('./service-outbox');
module.exports = {
module.exports = {
- createEventsClient, verifyDelivery, signDelivery, parseDelivery, createOutbox, createInbox,
+ createEventsClient, verifyDelivery, signDelivery, parseDelivery,
createPgOutbox, createPgInbox, outboxSchema, inboxSchema,
createPgOutbox, createPgInbox, outboxSchema, inboxSchema,
projectKey, appSource, createAppEvents, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders, createServiceOutbox,
projectKey, appSource, createAppEvents, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders, createServiceOutbox,
};
};
@@ -1,21 +1,19 @@
@@ -1,21 +1,19 @@
'use strict';
'use strict';
/**
/**
- * openvibe-sdk/events (server): transactional outbox and inbox (ADR-004), on the service's own database:
- * createOutbox/createInbox on better-sqlite3 (services not yet moved), createPgOutbox/createPgInbox on an
- * openvibe-sdk/db handle (PostgreSQL, ADR-035; below). No dependency: the caller passes its database handle.
+ * openvibe-sdk/events (server): transactional outbox and inbox (ADR-004) on the service's own PostgreSQL database
+ * (an openvibe-sdk/db handle, ADR-035): createPgOutbox / createPgInbox, with outboxSchema / inboxSchema for the
+ * service's migration. (The better-sqlite3 kits were removed in 0.44.0: every service runs on PostgreSQL.)
*
*
* const events = createEventsClient(client, { source: 'live' });
* const events = createEventsClient(client, { source: 'live' });
- * const outbox = createOutbox(db, { events });
- * outbox.ensureSchema();
- * db.transaction(() => {
- * db.prepare('UPDATE streams SET is_live = 1 WHERE id = ?').run(id);
- * outbox.enqueue({ event_type: 'live.stream.started', actor, subject, payload });
- * })(); // the event exists if and only if the change committed
+ * const outbox = createPgOutbox(db, { events });
+ * await db.tx(async (t) => {
+ * await t.exec(sql`UPDATE streams SET is_live = true WHERE id = ${id}`);
+ * await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject, payload });
+ * }); // the event exists if and only if the change committed
* outbox.start(); // relay: publishes due rows, marks them sent, backs off on failure
* outbox.start(); // relay: publishes due rows, marks them sent, backs off on failure
*
*
- * const inbox = createInbox(db);
- * inbox.ensureSchema();
- * inbox.once('network', event.event_id, () => { ...synchronous db writes... });
+ * const inbox = createPgInbox(db);
+ * await inbox.once('network', event.event_id, async (t) => { ...writes on t... });
*
*
* Delivery is at least once: a crash between publish and mark-sent republishes the same event_id,
* Delivery is at least once: a crash between publish and mark-sent republishes the same event_id,
* which OpenVibe.Events answers as a duplicate. A 4xx other than 401/408/409/425/429 is permanent
* which OpenVibe.Events answers as a duplicate. A 4xx other than 401/408/409/425/429 is permanent
@@ -49,178 +47,6 @@ function isPermanent(err) {
@@ -49,178 +47,6 @@ function isPermanent(err) {
return Number.isInteger(s) && s >= 400 && s < 500 && !RETRYABLE_4XX.has(s);
return Number.isInteger(s) && s >= 400 && s < 500 && !RETRYABLE_4XX.has(s);
}
}
-function createOutbox(db, {
- events, table = 'event_outbox', batchSize = 50, intervalMs = 1000,
- backoffMs = [1000, 5000, 30000, 120000, 600000], now = () => Date.now(), onError = null, allowOutsideTransaction = false,
-} = {}) {
- if (!db || typeof db.prepare !== 'function') throw new TypeError('a better-sqlite3 database is required');
- if (!events || typeof events.publish !== 'function' || typeof events.prepare !== 'function') {
- throw new TypeError('events must be an openvibe-sdk events client (createEventsClient)');
- }
- if (!TABLE_RE.test(table)) throw new TypeError('bad outbox table name');
- let stmts = null;
- let timer = null;
- let running = false;
-
- function ensureSchema() {
- // floating-ok: better-sqlite3 db.exec is synchronous
- db.exec(`CREATE TABLE IF NOT EXISTS ${table} (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- event_id TEXT NOT NULL UNIQUE,
- envelope TEXT NOT NULL,
- traceparent TEXT,
- created_at INTEGER NOT NULL,
- attempts INTEGER NOT NULL DEFAULT 0,
- next_attempt_at INTEGER NOT NULL DEFAULT 0,
- sent_at INTEGER,
- seq INTEGER,
- rejected_at INTEGER,
- last_error TEXT
- );
- CREATE INDEX IF NOT EXISTS idx_${table}_due ON ${table}(sent_at, rejected_at, next_attempt_at);`);
- stmts = null;
- }
-
- function q() {
- if (!stmts) {
- stmts = {
- insert: db.prepare(`INSERT INTO ${table} (event_id, envelope, traceparent, created_at, next_attempt_at) VALUES (?, ?, ?, ?, 0)`),
- due: db.prepare(`SELECT * FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL AND next_attempt_at <= ? ORDER BY id LIMIT ?`),
- sent: db.prepare(`UPDATE ${table} SET sent_at = ?, attempts = attempts + 1, last_error = NULL WHERE id = ?`),
- failed: db.prepare(`UPDATE ${table} SET attempts = attempts + 1, next_attempt_at = ?, last_error = ? WHERE id = ?`),
- rejected: db.prepare(`UPDATE ${table} SET rejected_at = ?, attempts = attempts + 1, last_error = ? WHERE id = ?`),
- pending: db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL`),
- rejectedCount: db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE rejected_at IS NOT NULL`),
- prune: db.prepare(`DELETE FROM ${table} WHERE sent_at IS NOT NULL AND sent_at < ?`),
- };
- }
- return stmts;
- }
-
- /** INSIDE the caller's transaction. Returns the complete envelope (event_id, timestamp, ...). */
- function enqueue(envelope, { traceparent } = {}) {
- if (!allowOutsideTransaction && !db.inTransaction) {
- throw new Error('outbox.enqueue() must run inside the transaction that makes the change');
- }
- const m = traceparent && TRACEPARENT_RE.exec(traceparent);
- const env = events.prepare(envelope, { traceId: m ? m[1] : undefined, now: now() });
- q().insert.run(env.event_id, JSON.stringify(env), m ? traceparent : null, now());
- return env;
- }
-
- const backoff = (attempts) => backoffMs[Math.min(attempts, backoffMs.length - 1)];
-
- function markFailure(row, err) {
- const message = String((err && err.message) || err).slice(0, 500);
- if (isPermanent(err)) q().rejected.run(now(), message, row.id);
- else q().failed.run(now() + backoff(row.attempts), message, row.id);
- if (onError) { try { onError(err, row); } catch { /* reporting must not break the relay */ } }
- }
-
- async function publishRows(rows) {
- const stats = { sent: 0, failed: 0, rejected: 0 };
- const envelopes = rows.map((r) => JSON.parse(r.envelope));
- const traceparent = rows[0].traceparent || undefined;
- try {
- await events.publish(rows.length === 1 ? envelopes[0] : envelopes, { traceparent });
- db.transaction(() => { for (const row of rows) q().sent.run(now(), row.id); })();
- stats.sent += rows.length;
- } catch (err) {
- if (rows.length > 1 && isPermanent(err)) {
- // One bad envelope rejects the whole batch: isolate it.
- for (const row of rows) {
- const s = await publishRows([row]);
- stats.sent += s.sent; stats.failed += s.failed; stats.rejected += s.rejected;
- }
- } else {
- // floating-ok: markFailure here is the better-sqlite3 one above (synchronous .run)
- for (const row of rows) markFailure(row, err);
- if (isPermanent(err)) stats.rejected += rows.length; else stats.failed += rows.length;
- }
- }
- return stats;
- }
-
- async function doFlush() {
- const total = { sent: 0, failed: 0, rejected: 0 };
- for (;;) {
- const rows = q().due.all(now(), batchSize);
- if (!rows.length) break;
- const s = await publishRows(rows);
- total.sent += s.sent; total.failed += s.failed; total.rejected += s.rejected;
- if (s.failed || rows.length < batchSize) break;
- }
- return total;
- }
-
- /** Publish due rows once (serialFlush: a call during a pass also covers rows committed since that pass claimed). */
- const flush = serialFlush(doFlush);
-
- function schedule(ms) {
- if (!running) return;
- clearTimeout(timer);
- timer = setTimeout(async () => {
- timer = null;
- try { await flush(); } catch (err) { if (onError) { try { onError(err); } catch { /* ignore */ } } }
- schedule(intervalMs);
- }, ms);
- if (timer.unref) timer.unref();
- }
-
- return {
- ensureSchema,
- enqueue,
- flush,
- start() { if (!running) { running = true; schedule(0); } },
- stop() { running = false; clearTimeout(timer); timer = null; return flush.idle(); },
- /** Wake the relay now (e.g. right after the transaction commits). */
- kick() { schedule(0); },
- pending: () => q().pending.get().n,
- rejected: () => q().rejectedCount.get().n,
- /** Delete sent rows older than `olderThanMs` (default 7 days). */
- prune(olderThanMs = 7 * 24 * 60 * 60 * 1000) { return q().prune.run(now() - olderThanMs).changes; },
- };
-}
-
-function createInbox(db, { table = 'idempotency_receipts', now = () => Date.now() } = {}) {
- if (!db || typeof db.prepare !== 'function') throw new TypeError('a better-sqlite3 database is required');
- if (!TABLE_RE.test(table)) throw new TypeError('bad inbox table name');
- let claim = null;
-
- function ensureSchema() {
- // floating-ok: better-sqlite3 db.exec is synchronous
- db.exec(`CREATE TABLE IF NOT EXISTS ${table} (
- consumer TEXT NOT NULL,
- event_id TEXT NOT NULL,
- processed_at INTEGER NOT NULL,
- PRIMARY KEY (consumer, event_id)
- )`);
- claim = null;
- }
-
- /** Run fn exactly once per (consumer, eventId), in one SQLite transaction with the receipt. */
- function once(consumer, eventId, fn) {
- if (!consumer || !eventId) throw new TypeError('consumer and eventId are required');
- if (!claim) claim = db.prepare(`INSERT OR IGNORE INTO ${table} (consumer, event_id, processed_at) VALUES (?, ?, ?)`);
- return db.transaction(() => {
- if (claim.run(String(consumer), String(eventId), now()).changes === 0) return { duplicate: true };
- const result = fn();
- if (result && typeof result.then === 'function') {
- throw new TypeError('inbox.once(): fn must be synchronous (it runs inside a SQLite transaction)');
- }
- return { duplicate: false, result };
- })();
- }
-
- function seen(consumer, eventId) {
- return Boolean(db.prepare(`SELECT 1 FROM ${table} WHERE consumer = ? AND event_id = ?`).get(String(consumer), String(eventId)));
- }
-
- return { ensureSchema, once, seen };
-}
-
-// ── PostgreSQL (ADR-035): the same outbox and inbox on an openvibe-sdk/db handle ─────────────────
-
/** The outbox table's DDL, for a service migration (the runtime role cannot create tables). */
/** The outbox table's DDL, for a service migration (the runtime role cannot create tables). */
function outboxSchema(table = 'event_outbox') {
function outboxSchema(table = 'event_outbox') {
if (!TABLE_RE.test(table)) throw new TypeError('bad outbox table name');
if (!TABLE_RE.test(table)) throw new TypeError('bad outbox table name');
@@ -382,4 +208,4 @@ function createPgInbox(db, { table = 'idempotency_receipts', now = () => Date.no
@@ -382,4 +208,4 @@ function createPgInbox(db, { table = 'idempotency_receipts', now = () => Date.no
};
};
}
}
-module.exports = { createOutbox, createInbox, createPgOutbox, createPgInbox, outboxSchema, inboxSchema, isPermanent };
+module.exports = { createPgOutbox, createPgInbox, outboxSchema, inboxSchema, isPermanent };
@@ -5,7 +5,8 @@ const crypto = require('node:crypto');
@@ -5,7 +5,8 @@ const crypto = require('node:crypto');
const { stubServer, send, run, waitFor } = require('./helpers');
const { stubServer, send, run, waitFor } = require('./helpers');
const { createClient, parseTraceparent } = require('../src/core');
const { createClient, parseTraceparent } = require('../src/core');
const { createServiceTokenClient } = require('../src/auth');
const { createServiceTokenClient } = require('../src/auth');
-const { createEventsClient, verifyDelivery, signDelivery, parseDelivery, createInbox, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders } = require('../src/events');
+const { createEventsClient, verifyDelivery, signDelivery, parseDelivery, createPgInbox, inboxSchema, verifyDeliveryV2, signDeliveryV2, signDeliveryHeaders } = require('../src/events');
+const { createDb } = require('../src/db');
const { subscribe } = require('../src/realtime');
const { subscribe } = require('../src/realtime');
const { createMockPlatform } = require('../src/testing');
const { createMockPlatform } = require('../src/testing');
@@ -293,18 +294,18 @@ run([
@@ -293,18 +294,18 @@ run([
['deliverEvents: signed deliveries to a local endpoint, retried in order, dead after max attempts', async () => {
['deliverEvents: signed deliveries to a local endpoint, retried in order, dead after max attempts', async () => {
const { platform, events } = setup();
const { platform, events } = setup();
- const Database = require('better-sqlite3');
- const inbox = createInbox(new Database(':memory:'));
- inbox.ensureSchema();
+ const idb = createDb({ pglite: true, service: 'sdk-test' });
+ await idb.query(inboxSchema());
+ const inbox = createPgInbox(idb);
let failNext = 1;
let failNext = 1;
let secret;
let secret;
const handled = [];
const handled = [];
- const srv = await stubServer((req, res, body) => {
+ const srv = await stubServer(async (req, res, body) => {
const d = parseDelivery(body, req.headers, secret, { requireV2: true });
const d = parseDelivery(body, req.headers, secret, { requireV2: true });
if (!d) return send(res, 401, { error: 'bad signature' });
if (!d) return send(res, 401, { error: 'bad signature' });
if (failNext > 0) { failNext--; return send(res, 503, { error: 'busy' }); }
if (failNext > 0) { failNext--; return send(res, 503, { error: 'busy' }); }
assert.ok(!('seq' in d) && !('seq' in JSON.parse(body)) && req.headers['x-openvibe-seq'] === undefined, 'a delivery carries no sequence number');
assert.ok(!('seq' in d) && !('seq' in JSON.parse(body)) && req.headers['x-openvibe-seq'] === undefined, 'a delivery carries no sequence number');
- const r = inbox.once('webhook', d.event.event_id, () => handled.push([d.event.subject.id, d.attempt, req.headers['x-openvibe-event-type']]));
+ const r = await inbox.once('webhook', d.event.event_id, async () => { handled.push([d.event.subject.id, d.attempt, req.headers['x-openvibe-event-type']]); });
return send(res, 200, { duplicate: r.duplicate });
return send(res, 200, { duplicate: r.duplicate });
});
});
platform.publishEvent({ event_type: 'media.vod.ready', source: 'media', actor, subject: { type: 'vod', id: '0' } }); // before the subscription: not delivered
platform.publishEvent({ event_type: 'media.vod.ready', source: 'media', actor, subject: { type: 'vod', id: '0' } }); // before the subscription: not delivered
@@ -339,6 +340,7 @@ run([
@@ -339,6 +340,7 @@ run([
await waitFor(() => handled.length === 3);
await waitFor(() => handled.length === 3);
await worker.stop();
await worker.stop();
await srv.close();
await srv.close();
+ await idb.close();
}],
}],
['iterate pages by cursor alone: the unchanged cursor or latest_cursor ends it', async () => {
['iterate pages by cursor alone: the unchanged cursor or latest_cursor ends it', async () => {
const asked = [];
const asked = [];
deleted file mode 100644
deleted file mode 100644
@@ -1,139 +0,0 @@
@@ -1,139 +0,0 @@
-'use strict';
-/** Outbox/inbox: enqueue only inside a transaction, relay publishes once, retries, rejects, dedupes. */
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const os = require('node:os');
-const path = require('node:path');
-const Database = require('better-sqlite3');
-const { run } = require('./helpers');
-const { createClient } = require('../src/core');
-const { createServiceTokenClient } = require('../src/auth');
-const { createEventsClient, createOutbox, createInbox } = require('../src/events');
-const { createMockPlatform } = require('../src/testing');
-
-const actor = { type: 'service', id: 'live' };
-const subject = { type: 'stream', id: '12', revision: 1 };
-
-function setup() {
- const platform = createMockPlatform({
- clients: { live: { secret: 'live-secret', grants: [['events.event.publish', 'openvibe.events']] } },
- });
- const tokens = createServiceTokenClient({ clientId: 'live', clientSecret: 'live-secret', fetch: platform.fetch });
- const client = createClient({ fetch: platform.fetch, tokenProvider: tokens, retries: 0 });
- const events = createEventsClient(client, { source: 'live' });
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-outbox-'));
- const db = new Database(path.join(dir, 'svc.db'));
- db.exec('CREATE TABLE streams (id INTEGER PRIMARY KEY, is_live INTEGER)');
- return { platform, events, db };
-}
-
-run([
- ['enqueue refuses to run outside a transaction', async () => {
- const { events, db } = setup();
- const outbox = createOutbox(db, { events });
- outbox.ensureSchema();
- assert.throws(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject }), /inside the transaction/);
- }],
- ['a rolled-back change leaves no event; a committed one is published once', async () => {
- const { platform, events, db } = setup();
- const outbox = createOutbox(db, { events });
- outbox.ensureSchema();
- assert.throws(() => db.transaction(() => {
- db.prepare('INSERT INTO streams (id, is_live) VALUES (1, 1)').run();
- outbox.enqueue({ event_type: 'live.stream.started', actor, subject });
- throw new Error('boom');
- })());
- assert.equal(outbox.pending(), 0);
- let env;
- db.transaction(() => {
- db.prepare('INSERT INTO streams (id, is_live) VALUES (2, 1)').run();
- env = outbox.enqueue({ event_type: 'live.stream.started', actor, subject, payload: { stream_id: 2 } }, { traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' });
- })();
- assert.match(env.event_id, /^evt_/);
- assert.equal(env.trace_id, '0af7651916cd43dd8448eb211c80319c');
- assert.equal(outbox.pending(), 1);
- const s = await outbox.flush();
- assert.deepEqual(s, { sent: 1, failed: 0, rejected: 0 });
- assert.equal(outbox.pending(), 0);
- assert.equal(platform.state.events.length, 1);
- assert.equal(platform.state.events[0].event.event_id, env.event_id);
- await outbox.flush();
- assert.equal(platform.state.events.length, 1, 'nothing republished');
- }],
- ['a transient failure backs off and retries; a batch publishes together', async () => {
- const { platform, events, db } = setup();
- let t = 1_000_000;
- let fail = true;
- const flaky = { prepare: events.prepare, publish: (...a) => (fail ? Promise.reject(Object.assign(new Error('down'), { status: 503 })) : events.publish(...a)) };
- const outbox = createOutbox(db, { events: flaky, now: () => t });
- outbox.ensureSchema();
- db.transaction(() => { for (let i = 0; i < 3; i++) outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: String(i) } }); })();
- assert.deepEqual(await outbox.flush(), { sent: 0, failed: 3, rejected: 0 });
- fail = false;
- assert.deepEqual(await outbox.flush(), { sent: 0, failed: 0, rejected: 0 }, 'not due yet');
- t += 1000;
- assert.deepEqual(await outbox.flush(), { sent: 3, failed: 0, rejected: 0 });
- assert.equal(platform.state.events.length, 3);
- }],
- ['a permanent 4xx rejects only the bad row', async () => {
- const { platform, events, db } = setup();
- const picky = {
- prepare: events.prepare,
- publish: (input, o) => {
- const list = Array.isArray(input) ? input : [input];
- if (list.some((e) => e.subject.id === 'bad')) return Promise.reject(Object.assign(new Error('events.type_not_allowed'), { status: 403 }));
- return events.publish(input, o);
- },
- };
- const outbox = createOutbox(db, { events: picky });
- outbox.ensureSchema();
- db.transaction(() => {
- outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'ok1' } });
- outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'bad' } });
- outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'ok2' } });
- })();
- assert.deepEqual(await outbox.flush(), { sent: 2, failed: 0, rejected: 1 });
- assert.equal(outbox.pending(), 0);
- assert.equal(outbox.rejected(), 1);
- assert.equal(platform.state.events.length, 2);
- }],
- ['a token-endpoint refusal is retried, never rejected', async () => {
- const { events, db } = setup();
- const noGrant = { prepare: events.prepare, publish: () => Promise.reject(Object.assign(new Error('unauthorized_client'), { status: 400, url: 'http://127.0.0.1:4000/oauth/token' })) };
- const outbox = createOutbox(db, { events: noGrant });
- outbox.ensureSchema();
- db.transaction(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject }))();
- assert.deepEqual(await outbox.flush(), { sent: 0, failed: 1, rejected: 0 });
- assert.equal(outbox.rejected(), 0);
- assert.equal(outbox.pending(), 1);
- }],
- ['a flush called during a pass also publishes what was committed after that pass claimed', async () => {
- const { events, db } = setup();
- let release; const gate = new Promise((r) => { release = r; });
- const sent = [];
- const slow = { prepare: events.prepare, publish: async (input, o) => { await gate; sent.push(input.subject.id); return await events.publish(input, o); } };
- const outbox = createOutbox(db, { events: slow });
- outbox.ensureSchema();
- db.transaction(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject }))();
- const first = outbox.flush();
- await new Promise((r) => setImmediate(r));
- db.transaction(() => outbox.enqueue({ event_type: 'live.stream.started', actor, subject: { type: 'stream', id: '13', revision: 1 } }))();
- const second = outbox.flush();
- assert.equal(outbox.flush(), second, 'calls during a pass share the next one');
- release();
- await first; await second;
- assert.deepEqual(sent.sort(), ['12', '13']);
- assert.equal(outbox.pending(), 0);
- }],
- ['inbox runs a handler exactly once and rolls back with it', async () => {
- const { db } = setup();
- const inbox = createInbox(db);
- inbox.ensureSchema();
- assert.throws(() => inbox.once('network', 'evt_1', () => { db.prepare('INSERT INTO streams (id, is_live) VALUES (9, 1)').run(); throw new Error('crash'); }));
- assert.equal(inbox.seen('network', 'evt_1'), false);
- assert.equal(db.prepare('SELECT COUNT(*) AS n FROM streams').get().n, 0);
- assert.deepEqual(inbox.once('network', 'evt_1', () => { db.prepare('INSERT INTO streams (id, is_live) VALUES (9, 1)').run(); return 'done'; }), { duplicate: false, result: 'done' });
- assert.deepEqual(inbox.once('network', 'evt_1', () => { throw new Error('must not run'); }), { duplicate: true });
- assert.throws(() => inbox.once('network', 'evt_2', async () => {}), /synchronous/);
- }],
-]);
@@ -81,6 +81,38 @@ function cases(label, open) {
@@ -81,6 +81,38 @@ function cases(label, open) {
assert.equal(await o2.rejected(), 1);
assert.equal(await o2.rejected(), 1);
} finally { await done(); }
} finally { await done(); }
}],
}],
+ [`${label}: a token-endpoint refusal is retried, never rejected`, async () => {
+ const { db, done } = await open();
+ try {
+ const { events } = eventsClient();
+ const noGrant = { prepare: events.prepare, publish: () => Promise.reject(Object.assign(new Error('unauthorized_client'), { status: 400, url: 'http://127.0.0.1:4000/oauth/token' })) };
+ const outbox = createPgOutbox(db, { events: noGrant });
+ await db.tx(async (t) => { await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'tok' } }); });
+ assert.deepEqual(await outbox.flush(), { sent: 0, failed: 1, rejected: 0 }, 'no grant yet says nothing about the event');
+ assert.equal(await outbox.rejected(), 0);
+ assert.equal(await outbox.pending(), 1);
+ } finally { await done(); }
+ }],
+ [`${label}: a flush during a pass also publishes what was committed after that pass claimed`, async () => {
+ const { db, done } = await open();
+ try {
+ const { events } = eventsClient();
+ let release; const gate = new Promise((r) => { release = r; });
+ const sent = [];
+ const slow = { prepare: events.prepare, publish: async (input, o) => { await gate; for (const e of [].concat(input)) sent.push(e.subject.id); return await events.publish(input, o); } };
+ const outbox = createPgOutbox(db, { events: slow });
+ await db.tx(async (t) => { await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: '12' } }); });
+ const first = outbox.flush();
+ await new Promise((r) => setTimeout(r, 20));
+ await db.tx(async (t) => { await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: '13' } }); });
+ const second = outbox.flush();
+ assert.equal(outbox.flush(), second, 'calls during a pass share the next one');
+ release();
+ await first; await second;
+ assert.deepEqual(sent.sort(), ['12', '13']);
+ assert.equal(await outbox.pending(), 0);
+ } finally { await done(); }
+ }],
[`${label}: the inbox runs a handler once, inside its transaction`, async () => {
[`${label}: the inbox runs a handler once, inside its transaction`, async () => {
const { db, done } = await open();
const { db, done } = await open();
try {
try {
@@ -1,38 +1,14 @@
@@ -1,38 +1,14 @@
'use strict';
'use strict';
-// openvibe-sdk/auth createRevocationStore: network.user.token_valid_after cutoffs (Contracts 0.39.0).
+// openvibe-sdk/auth createPgRevocationStore: network.user.token_valid_after cutoffs (Contracts 0.39.0).
const assert = require('assert');
const assert = require('assert');
-const Database = require('better-sqlite3');
-const { createRevocationStore, createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('../src/auth');
+const { createPgRevocationStore, revocationSchema, TOKEN_VALID_AFTER } = require('../src/auth');
const A = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
const A = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
const B = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4';
const B = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4';
const ev = (subject, iso, over = {}) => ({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', event_type: TOKEN_VALID_AFTER, version: 1, source: 'network', payload: { subject: { type: 'user', id: subject }, valid_after: iso, reason: 'signed_out_everywhere' }, ...over });
const ev = (subject, iso, over = {}) => ({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', event_type: TOKEN_VALID_AFTER, version: 1, source: 'network', payload: { subject: { type: 'user', id: subject }, valid_after: iso, reason: 'signed_out_everywhere' }, ...over });
const T = Date.parse('2026-09-24T20:00:00Z');
const T = Date.parse('2026-09-24T20:00:00Z');
-const db = new Database(':memory:');
-const s = createRevocationStore(db);
-assert.strictEqual(s.isRevoked({ subject_id: A, iat: T / 1000 - 10 }), false, 'nothing known');
-assert.strictEqual(s.apply(ev(A, new Date(T).toISOString())), 'revoked');
-assert.strictEqual(s.isRevoked({ subject_id: A, iat: T / 1000 - 1 }), true, 'issued before: refused');
-assert.strictEqual(s.isRevoked({ subject_id: A, iat: T / 1000 }), false, 'issued in the cutoff second: accepted (Network rule)');
-assert.strictEqual(s.isRevoked({ subject_id: B, iat: T / 1000 - 1 }), false, 'someone else');
-assert.strictEqual(s.isRevoked({ subject_id: A }), false, 'no iat, nothing to compare');
-assert.strictEqual(s.apply(ev(A, new Date(T - 60000).toISOString())), 'unchanged', 'never moves back');
-assert.strictEqual(s.apply(ev(A, new Date(T).toISOString())), 'unchanged', 'a redelivery');
-assert.strictEqual(s.apply(ev(A, new Date(T + 1).toISOString(), { source: 'live' })), 'ignored:source');
-assert.strictEqual(s.apply(ev('gst_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', new Date(T).toISOString())), 'ignored:payload');
-assert.strictEqual(s.apply({ ...ev(A, 'soon') }), 'ignored:payload');
-assert.strictEqual(s.apply({ event_type: 'network.module.updated' }), 'ignored:type');
-assert.strictEqual(s.cutoffFor(A), T);
-
-// Survives a restart (a new store on the same database); memory-only works without one.
-assert.strictEqual(createRevocationStore(db).cutoffFor(A), T);
-const mem = createRevocationStore();
-assert.strictEqual(mem.apply(ev(B, new Date(T).toISOString())), 'revoked');
-assert.strictEqual(mem.isRevoked({ subject_id: B, iat: T / 1000 - 5 }), true);
-assert.throws(() => createRevocationStore(db, { table: 'x; DROP TABLE y' }), /bad table name/);
-
-// createPgRevocationStore: the same rules on PostgreSQL (PGlite), cutoffs read into memory by load().
+// createPgRevocationStore on PostgreSQL (PGlite): cutoffs read into memory by load(), written through.
(async () => {
(async () => {
const { createDb } = require('../src/db');
const { createDb } = require('../src/db');
const pg = createDb({ pglite: true, service: 'sdk-test' });
const pg = createDb({ pglite: true, service: 'sdk-test' });
@@ -44,6 +20,12 @@ assert.throws(() => createRevocationStore(db, { table: 'x; DROP TABLE y' }), /ba
@@ -44,6 +20,12 @@ assert.throws(() => createRevocationStore(db, { table: 'x; DROP TABLE y' }), /ba
assert.strictEqual(p.isRevoked({ subject_id: A, iat: T / 1000 }), false);
assert.strictEqual(p.isRevoked({ subject_id: A, iat: T / 1000 }), false);
assert.strictEqual(await p.apply(ev(A, new Date(T - 60000).toISOString())), 'unchanged', 'never moves back');
assert.strictEqual(await p.apply(ev(A, new Date(T - 60000).toISOString())), 'unchanged', 'never moves back');
assert.strictEqual(await p.apply(ev(A, new Date(T + 1).toISOString(), { source: 'live' })), 'ignored:source');
assert.strictEqual(await p.apply(ev(A, new Date(T + 1).toISOString(), { source: 'live' })), 'ignored:source');
+ assert.strictEqual(p.isRevoked({ subject_id: B, iat: T / 1000 - 1 }), false, 'someone else');
+ assert.strictEqual(p.isRevoked({ subject_id: A }), false, 'no iat, nothing to compare');
+ assert.strictEqual(await p.apply(ev(A, new Date(T).toISOString())), 'unchanged', 'a redelivery');
+ assert.strictEqual(await p.apply(ev('gst_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', new Date(T).toISOString())), 'ignored:payload');
+ assert.strictEqual(await p.apply({ ...ev(A, 'soon') }), 'ignored:payload');
+ assert.strictEqual(await p.apply({ event_type: 'network.module.updated' }), 'ignored:type');
// Another process moved it further: record() refuses to go back and takes the stored value.
// Another process moved it further: record() refuses to go back and takes the stored value.
await pg.prepare('UPDATE token_revocations SET valid_after_ms = ? WHERE subject_id = ?').run(T + 5000, A);
await pg.prepare('UPDATE token_revocations SET valid_after_ms = ? WHERE subject_id = ?').run(T + 5000, A);
assert.strictEqual(await p.record(A, T + 1000), false);
assert.strictEqual(await p.record(A, T + 1000), false);
@@ -224,22 +224,7 @@ export interface ExchangeCodeOptions {
@@ -224,22 +224,7 @@ export interface ExchangeCodeOptions {
export declare function exchangeCode(opts: ExchangeCodeOptions): Promise<UserTokenResponse>;
export declare function exchangeCode(opts: ExchangeCodeOptions): Promise<UserTokenResponse>;
export declare function refreshUserToken(opts: { refreshToken: string; clientId: string; clientSecret: string; network?: string; tokenUrl?: string; fetch?: FetchLike; timeoutMs?: number }): Promise<UserTokenResponse>;
export declare function refreshUserToken(opts: { refreshToken: string; clientId: string; clientSecret: string; network?: string; tokenUrl?: string; fetch?: FetchLike; timeoutMs?: number }): Promise<UserTokenResponse>;
-/** Per-person token cutoffs from network.user.token_valid_after (Contracts 0.39.0). */
-export interface RevocationStore {
- /** Apply an envelope: 'revoked' when the cutoff moved, 'unchanged', or 'ignored:<why>'. */
- apply(event: unknown): 'revoked' | 'unchanged' | `ignored:${string}`;
- /** Keep the later cutoff; true when it moved forward. */
- record(subject: string, validAfterMs: number, reason?: string | null): boolean;
- /** True when claims.iat (seconds) is before claims.subject_id's cutoff. */
- isRevoked(claims: { iat?: number; subject_id?: string } | null | undefined): boolean;
- /** The cutoff in ms, 0 when none. */
- cutoffFor(subject: string): number;
- readonly EVENT_TYPE: 'network.user.token_valid_after';
-}
-export interface RevocationStoreOptions { table?: string; now?: () => number; maxCache?: number }
-/** A better-sqlite3 handle keeps cutoffs across restarts; without one they live in memory. */
-export declare function createRevocationStore(db?: unknown, opts?: RevocationStoreOptions): RevocationStore;
-/** The same cutoffs on PostgreSQL: load() reads them into memory; apply()/record() write through (async). */
+/** Per-person token cutoffs from network.user.token_valid_after (Contracts 0.39.0), on PostgreSQL: load() reads them into memory; apply()/record() write through (async). */
export interface PgRevocationStore {
export interface PgRevocationStore {
/** Read every stored cutoff into memory (at boot); resolves to how many. */
/** Read every stored cutoff into memory (at boot); resolves to how many. */
load(): Promise<number>;
load(): Promise<number>;
@@ -80,17 +80,5 @@ export interface CreateDbOptions {
@@ -80,17 +80,5 @@ export interface CreateDbOptions {
}
}
export function createDb(opts?: CreateDbOptions): Db;
export function createDb(opts?: CreateDbOptions): Db;
export declare class DbError extends Error { code?: string; detail?: string; constraint?: string; table?: string; column?: string; statement?: string; cause?: unknown }
export declare class DbError extends Error { code?: string; detail?: string; constraint?: string; table?: string; column?: string; statement?: string; cause?: unknown }
-export interface ImportTableOptions { from?: string; dropColumns?: string[]; where?: string; map?: (row: Row) => Row | null }
-export interface ImportReport { ok: boolean; tables: { table: string; source: string; rows: number; ms: number; checksum?: string }[]; problems: { table: string; problem: string }[] }
-/** The one-time SQLite → PostgreSQL move with verification (needs better-sqlite3). */
-export function importSqlite(o: { sqlite: string | object; db: Db; tables?: Record<string, ImportTableOptions>; only?: string[]; truncate?: boolean; verify?: boolean; log?: { log(msg: string): void } }): Promise<ImportReport>;
export const PARSERS: Record<number, (v: string) => unknown>;
export const PARSERS: Record<number, (v: string) => unknown>;
export const ISOLATION: Record<string, string>;
export const ISOLATION: Record<string, string>;
-/**
- * A service's scripts/migrate-to-postgres.js in one call: migrate (or --pglite), import into emptied tables, clean
- * text PostgreSQL refuses (reported), verify counts and checksums; resolves 0 when everything verified, else 1.
- */
-export function runSqliteMigration(opts: {
- service: string; sqlite: string; directUrl?: string; migrations: string;
- tables?: Record<string, ImportTableOptions>; argv?: string[]; out?: (line: string) => void;
-}): Promise<0 | 1>;
@@ -120,37 +120,7 @@ export interface ParseDeliveryOptions extends DeliveryV2Options {
@@ -120,37 +120,7 @@ export interface ParseDeliveryOptions extends DeliveryV2Options {
}
}
export declare function parseDelivery(rawBody: RawBody, headers: Record<string, any> | Headers, secret: string, opts?: ParseDeliveryOptions): { event: EventEnvelope; subscriptionId: string | null; attempt: number } | null;
export declare function parseDelivery(rawBody: RawBody, headers: Record<string, any> | Headers, secret: string, opts?: ParseDeliveryOptions): { event: EventEnvelope; subscriptionId: string | null; attempt: number } | null;
-/** The subset of a better-sqlite3 Database the outbox and inbox use. */
-export interface SqliteDatabase {
- prepare(sql: string): any;
- exec(sql: string): any;
- transaction<F extends (...args: any[]) => any>(fn: F): F;
- readonly inTransaction: boolean;
-}
export interface FlushStats { sent: number; failed: number; rejected: number; }
export interface FlushStats { sent: number; failed: number; rejected: number; }
-export interface Outbox {
- ensureSchema(): void;
- /** Inside the caller's transaction; returns the complete envelope. */
- enqueue(envelope: EventInput, opts?: { traceparent?: string }): EventEnvelope;
- flush(): Promise<FlushStats>;
- start(): void;
- stop(): Promise<unknown>;
- kick(): void;
- pending(): number;
- rejected(): number;
- prune(olderThanMs?: number): number;
-}
-export declare function createOutbox(db: SqliteDatabase, opts: {
- events: Pick<EventsClient, 'publish' | 'prepare'>;
- table?: string; batchSize?: number; intervalMs?: number; backoffMs?: number[];
- now?: () => number; onError?: (err: unknown, row?: unknown) => void; allowOutsideTransaction?: boolean;
-}): Outbox;
-export interface Inbox {
- ensureSchema(): void;
- once<T>(consumer: string, eventId: string, fn: () => T): { duplicate: true } | { duplicate: false; result: T };
- seen(consumer: string, eventId: string): boolean;
-}
-export declare function createInbox(db: SqliteDatabase, opts?: { table?: string; now?: () => number }): Inbox;
/** The PostgreSQL outbox (ADR-035) on an openvibe-sdk/db handle; several relays may share one table. */
/** The PostgreSQL outbox (ADR-035) on an openvibe-sdk/db handle; several relays may share one table. */
export interface PgOutbox {
export interface PgOutbox {