openvibers/openvibe.shared Commits

4.112.0: x-unit seconds in config schemas (#290)

OP OpenVibers committed 7 days ago · 2497e05339 · 7 files, +21 −10

* 4.112.0: x-unit seconds for an integer owner setting

* Merge main (4.111.1) into 4.112.0

# Conflicts:
#	CHANGELOG.md
#	README.md
#	STATUS.json
#	package-lock.json
#	package.json

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 834b91b..d650ccc 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,12 @@
# Changelog
++## 4.112.0 — 2026-11-05
++
++- **`openvibe-shared/config`: `x-unit: "seconds"` is an owner-setting unit.** An integer property may now be
++ annotated `seconds` (alongside `nano_usd`, `ms` and `bps`), and `summary()` carries the annotation unchanged so a
++ settings form can show the input in seconds with the label "s". An unknown unit is still refused when the store is
++ created, naming the unit.
++
## 4.111.1 — 2026-11-05
- **The tab icon is the Pair on every site.** `theme-loader.js` repaints the favicon on each theme apply
ddiff --git a/README.md b/README.md
iindex f513727..fd22c1a 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.111.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.112.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
@@@ -344,7 +344,7 @@ settings.sealed; settings.kek; // false / null without a KEK
marker with its four-character `hint`, `set_at`, `set_by`, `bound_to`) and `schema`, the JSON
Schema to draw the form from; a secret property's `default`, `examples`, `enum` and `const` are
removed from that copy. The annotations are `title`, `description`, `x-group`, `x-env`, `x-danger`,
-- `x-bind` and `x-unit` (`nano_usd`, `ms`, `bps`).
++ `x-bind` and `x-unit` (`nano_usd`, `ms`, `bps`, `seconds`).
- **Hooks.** `onChange({ namespace, keys, actor, revision })` runs after every successful activation
(never for a proposal or the seed), and a throwing hook is logged and never refuses the revision.
`requireFresh(req, { namespace, keys })`, an `adminRoutes` option, runs before a secret write, a
ddiff --git a/STATUS.json b/STATUS.json
iindex bfe1039..0941cfb 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.111.1",
++ "version": "4.112.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@@ -27,7 +27,8 @@
"a CONFIRMATION_REQUESTED notification type: an agent owner is told when a sensitive-capability confirmation awaits them",
"the Dock pops out into a window of its own (Document Picture-in-Picture): the same panel, the same tabs, the page's pill saying where the Dock went",
"the Dock accepts a refreshing token (string, sync or async) and an optional read-only scope set: the extension hides approve/decline and the chat composer while keeping reads",
-- "vibes-pay.js: OpenVibe.vibes.pay(payUrl) opens the Vibes Pay sheet (popup on a computer, full page on a phone), resolves { status } from an origin-checked postMessage and binds <button data-ov-vibes-pay> in the Vibes accent, both themes (D12)"
++ "vibes-pay.js: OpenVibe.vibes.pay(payUrl) opens the Vibes Pay sheet (popup on a computer, full page on a phone), resolves { status } from an origin-checked postMessage and binds <button data-ov-vibes-pay> in the Vibes accent, both themes (D12)",
++ "openvibe-shared/config accepts x-unit seconds for an integer owner setting, so an owner form can label the input in seconds"
],
-- "updated": "2026-11-04"
++ "updated": "2026-11-05"
}
ddiff --git a/config-core.js b/config-core.js
iindex f2f06a3..8da644e 100644
---- a/config-core.js
++++ b/config-core.js
@@@ -265,7 +265,7 @@ const KEYWORDS = new Set(['type', 'enum', 'const', 'properties', 'required', 'ad
'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']);
const ANNOTATIONS = new Set(['$schema', '$id', '$comment', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', 'format']);
const TYPES = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'];
--const UNITS = new Set(['nano_usd', 'ms', 'bps']);
++const UNITS = new Set(['nano_usd', 'ms', 'bps', 'seconds']);
const ENV_NAME = /^[A-Z][A-Z0-9_]{0,127}$/;
const patterns = new WeakMap();
ddiff --git a/package-lock.json b/package-lock.json
iindex 45295c5..9e81fb6 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.111.1",
++ "version": "4.112.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.111.1",
++ "version": "4.112.0",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 3ba85a7..82e9eef 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.111.1",
++ "version": "4.112.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/config-secrets.test.js b/test/config-secrets.test.js
iindex 09ad6b5..75c9b1a 100644
---- a/test/config-secrets.test.js
++++ b/test/config-secrets.test.js
@@@ -363,7 +363,10 @@ const noSecret = (text, values, what) => {
assert.strictEqual(bindLeakSummary.secrets.bound_key.bound_to, null);
const enumSecret = await createConfigStore({ db, service: 'live', namespace: 'live.enumsecret', schema: { type: 'object', additionalProperties: false, properties: { api_key: { type: 'string', enum: ['ENUM-MADE-UP-SECRET-7'] } } }, classify, defaults: {}, keks: keks1, log });
await assert.rejects(enumSecret.setSecret('api_key', 'another value', { actor: USER }), (e) => e.code === 'config.invalid' && !JSON.stringify(e).includes('ENUM-MADE-UP-SECRET-7'));
-- await assert.rejects(createConfigStore({ db, service: 'live', namespace: 'live.badunit', schema: { type: 'object', properties: { x: { type: 'integer', 'x-unit': 'seconds' } } }, keks: keks1, log }), /x-unit/);
++ // `seconds` is an owner-setting unit (an integer the form shows in seconds, labelled "s"); anything else is refused.
++ const seconds = await createConfigStore({ db, service: 'live', namespace: 'live.secondsunit', schema: { type: 'object', properties: { x: { type: 'integer', 'x-unit': 'seconds' } } }, keks: keks1, log });
++ assert.strictEqual((await seconds.summary()).schema.properties.x['x-unit'], 'seconds');
++ await assert.rejects(createConfigStore({ db, service: 'live', namespace: 'live.badunit', schema: { type: 'object', properties: { x: { type: 'integer', 'x-unit': 'fortnights' } } }, keks: keks1, log }), /x-unit/);
await assert.rejects(createConfigStore({ db, service: 'live', namespace: 'live.badenv', schema: { type: 'object', properties: { x: { type: 'string', 'x-env': 'lower-case' } } }, keks: keks1, log }), /x-env/);
await assert.rejects(createConfigStore({ db, service: 'live', namespace: 'live.baddanger', schema: { type: 'object', properties: { x: { type: 'boolean', 'x-danger': 'yes' } } }, keks: keks1, log }), /x-danger/);