openvibers/openvibe.host Commits

Host: site ids are sit_<ULID> (three-letter prefix, ADR-048 resource names); zero sites existed. (#27)

OP OpenVibers committed 7 Oct 2026 · 4d4ee8d3eb · 5 files, +6 −6

**Changes (file:line)**
- `server/ids.js:6` — header comment `site_ site` → `sit_  site` (alignment kept).
- `server/ids.js:10` — `PREFIX.site` `'site'` → `'sit'`; `RE`/`newId`/`isId` build from `PREFIX`, so `isId('site', …)` now matches `^sit_<ULID>$` and new ids are `sit_…`. This is the only place the prefix is recognized or built.
- `test/host-lifecycle.test.js:22` — `assert.match(site.id, /^site_/)` → `/^sit_/`.
- `test/host-secrets-dashboard.test.js:207` — id capture regex `\/sites\/(site_[0-9A-Z]+)` → `(sit_[0-9A-Z]+)`.
- `test/host-sitemap.test.js:99` — secretish list `'site_'` → `'sit_'`.
- `README.md:260` — `site_<ULID>` → `sit_<ULID>`, plus the one-line note that the prefix is `sit_` since 2026-10-07 (ADR-048 three-letter rule), renamed from `site_` with nothing converted.

Left unchanged (not the id prefix): `host_sites`/`host_site_sources`/`host_site_config`, `site_id` columns, `siteUrl`, `site_config.*` error codes, `network.site_copy`, `/sites/:id` routes (generic params validated through `isId`).

**Migration:** no CHECK constraint or migration logic mentions the prefix, so no new migration. The only occurrences are two SQL comments in the applied `migrations/0001_initial.sql:44` (`-- site_<ULID>`) and `:142` (`-- prj_… or site_…`); per "never edit an applied one" I left them, and a comment cannot be altered by a new migration.

**STATUS.md/json:** `STATUS.json` `updated` is already `2026-10-07` (and that field is owned by the Shared D41 docs-status script), so no edit was needed there.

**Tests** — `nice -n 15 ov test --no-cache test/host-lifecycle.test.js test/host-secrets-dashboard.test.js test/host-sitemap.test.js test/host-isolation.test.js` → **4 passed in 9 s** (all four; `ov` reported "5 changed files since 49fcd465; 4 of 61 test files cover it"). Not committed or pushed.

Changes

ddiff --git a/README.md b/README.md
iindex e2571d7..17de289 100644
---- a/README.md
++++ b/README.md
@@@ -257,7 +257,7 @@ A Node service (`server/`, Express 4, better-sqlite3, port **4910**, service id
### Model
- **Projects** (`prj_<ULID>`) have an owner (a `usr_` subject), an environment (`production` or `sandbox`), members with a role (`owner` > `maintainer` > `deployer`), and optionally the `network_project_id` of the OpenVibe.Network project (ADR-014: Network owns projects; until it has them, Host creates one for its owner). Every Host row and every stored object is keyed by the project id.
--- **Sites** (`site_<ULID>`) have a name (one DNS label, 3–40 characters, reserved names refused) that is their default host, `<name>.openvibe.host`, and a pointer to the active deploy. A deleted site's name is held for 30 days so nobody else can serve content on links that still point there.
++- **Sites** (`sit_<ULID>`) have a name (one DNS label, 3–40 characters, reserved names refused) that is their default host, `<name>.openvibe.host`, and a pointer to the active deploy. A deleted site's name is held for 30 days so nobody else can serve content on links that still point there. (The id prefix is `sit_` since 2026-10-07, ADR-048's three-letter rule; it was `site_`, renamed with no conversion because production held no sites.)
- **Deploys** (`dpl_<ULID>`) are immutable artifacts: a manifest (`host.deploy-manifest@1`: path, sha256, size and content type of every file, file count, total bytes) with its own sha256, file rows, and the upload/validation log. Database triggers refuse any update of a deploy's artifact columns or file rows. The bytes are content-addressed per project: `<HOST_STORAGE_DIR>/projects/<prj_…>/<aa>/<sha256>`, with `HOST_OBJECT_STORE=media` writing them through to OpenVibe.Media as well ([Storage](#storage)).
- **Domains**: every site has its default domain. A custom domain is added as `pending` and becomes `verified` (and served) once `_openvibe-host.<hostname>` has the TXT record `openvibe-host-verification=<token>`. A verified name belongs to one site. OpenVibe domains (every domain in the released service manifests, `openvibe.<tld>`, the sites domain itself) can never be claimed. The worker re-checks pending domains (they fail after `HOST_DOMAIN_PENDING_DAYS`) and re-checks verified ones daily: a TXT record gone for `HOST_DOMAIN_LAPSE_DAYS` lapses the domain and it stops being served.
- **Quotas** per project, enforced by Host: storage bytes (objects stored once per project, so re-uploading unchanged files costs nothing), deploys per rolling 24 hours (checked before the body is read; failed uploads count), files per deploy, bytes per file, sites, custom domains. A deploy's own size before deduplication may not exceed the storage quota. Defaults come from `HOST_QUOTA_*` / `HOST_SANDBOX_QUOTA_*`; staff override them per project. Sandbox projects get smaller quotas, no custom domains, and `X-Robots-Tag: noindex`.
ddiff --git a/server/ids.js b/server/ids.js
iindex d6049ce..51655c0 100644
---- a/server/ids.js
++++ b/server/ids.js
@@@ -3,11 +3,11 @@
* Host's own identifiers: <prefix>_<ULID> (the contracts ULID, so they sort by creation time).
*
* prj_ project (until OpenVibe.Network has projects; then it references network_project_id)
-- * site_ site dpl_ deploy dom_ domain
++ * sit_ site dpl_ deploy dom_ domain
*/
const { ids } = require('openvibe-contracts');
--const PREFIX = { project: 'prj', site: 'site', deploy: 'dpl', domain: 'dom' };
++const PREFIX = { project: 'prj', site: 'sit', deploy: 'dpl', domain: 'dom' };
const ULID = '[0-9A-HJKMNP-TV-Z]{26}';
const RE = Object.fromEntries(Object.entries(PREFIX).map(([k, p]) => [k, new RegExp(`^${p}_${ULID}$`)]));
ddiff --git a/test/host-lifecycle.test.js b/test/host-lifecycle.test.js
iindex 2200bd2..f909cfa 100644
---- a/test/host-lifecycle.test.js
++++ b/test/host-lifecycle.test.js
@@@ -19,7 +19,7 @@ const { boot, check, done } = require('./stageb/boot');
assert.strictEqual(project.role, 'owner');
assert.strictEqual(project.quota.deploys_per_day, 50);
site = await t.site(alice, project.id, 'alice');
-- assert.match(site.id, /^site_/);
++ assert.match(site.id, /^sit_/);
assert.strictEqual(site.hostname, 'alice.openvibe.host');
assert.strictEqual(site.url, 'https://alice.openvibe.host');
const r = await t.api('GET', `/api/v1/sites/${site.id}`, { as: alice });
ddiff --git a/test/host-secrets-dashboard.test.js b/test/host-secrets-dashboard.test.js
iindex 3a99714..3650733 100644
---- a/test/host-secrets-dashboard.test.js
++++ b/test/host-secrets-dashboard.test.js
@@@ -204,7 +204,7 @@ const ORIGIN = 'https://openvibe.host';
assert.match(page.text, /Deploys in the last 24 h/);
const s = await t.api('POST', `/projects/${projectId}/sites`, { session: alice, form: { csrf, name: 'dash-site' }, headers: { origin: ORIGIN } });
assert.strictEqual(s.status, 303, s.text);
-- siteId = s.headers.location.match(/\/sites\/(site_[0-9A-Z]+)/)[1];
++ siteId = s.headers.location.match(/\/sites\/(sit_[0-9A-Z]+)/)[1];
const boundary = 'dashBOUNDARY';
const part = (name, filename, content) => `--${boundary}\r\nContent-Disposition: form-data; name="${name}"${filename ? `; filename="${filename}"` : ''}\r\n\r\n${content}\r\n`;
const body = part('csrf', null, csrf) + part('strip', null, 'folder') + part('activate', null, '1') + part('files', 'site/index.html', 'DASH OK') + `--${boundary}--\r\n`;
ddiff --git a/test/host-sitemap.test.js b/test/host-sitemap.test.js
iindex c8824dd..d47a71d 100644
---- a/test/host-sitemap.test.js
++++ b/test/host-sitemap.test.js
@@@ -96,7 +96,7 @@ const CUSTOM = 'www.widgets-example.org';
assert.strictEqual(head.status, 200);
assert.strictEqual(head.text, '');
for (const r of [sm, rb]) {
-- for (const secretish of [t.SECRET, '_openvibe-host', 'openvibe-host-verification', 'prj_', 'site_', 'dpl_', 'dom_']) {
++ for (const secretish of [t.SECRET, '_openvibe-host', 'openvibe-host-verification', 'prj_', 'sit_', 'dpl_', 'dom_']) {
assert.ok(!r.text.includes(secretish), `${secretish} in ${r.text}`);
}
assert.strictEqual(r.headers['set-cookie'], undefined);