openvibers/openvibe.host Commits
Host: site ids are sit_<ULID> (three-letter prefix, ADR-048 resource names); zero sites existed. (#27)
Changes
@@@ -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`.@@@ -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}$`)]));@@@ -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 });@@@ -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`;@@@ -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);
@@ -257,7 +257,7 @@ A Node service (`server/`, Express 4, better-sqlite3, port **4910**, service id
@@ -257,7 +257,7 @@ A Node service (`server/`, Express 4, better-sqlite3, port **4910**, service id
### Model
### 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.
- **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)).
- **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.
- **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`.
- **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`.
@@ -3,11 +3,11 @@
@@ -3,11 +3,11 @@
* Host's own identifiers: <prefix>_<ULID> (the contracts ULID, so they sort by creation time).
* 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)
* 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 { 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 ULID = '[0-9A-HJKMNP-TV-Z]{26}';
const RE = Object.fromEntries(Object.entries(PREFIX).map(([k, p]) => [k, new RegExp(`^${p}_${ULID}$`)]));
const RE = Object.fromEntries(Object.entries(PREFIX).map(([k, p]) => [k, new RegExp(`^${p}_${ULID}$`)]));
@@ -19,7 +19,7 @@ const { boot, check, done } = require('./stageb/boot');
@@ -19,7 +19,7 @@ const { boot, check, done } = require('./stageb/boot');
assert.strictEqual(project.role, 'owner');
assert.strictEqual(project.role, 'owner');
assert.strictEqual(project.quota.deploys_per_day, 50);
assert.strictEqual(project.quota.deploys_per_day, 50);
site = await t.site(alice, project.id, 'alice');
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.hostname, 'alice.openvibe.host');
assert.strictEqual(site.url, 'https://alice.openvibe.host');
assert.strictEqual(site.url, 'https://alice.openvibe.host');
const r = await t.api('GET', `/api/v1/sites/${site.id}`, { as: alice });
const r = await t.api('GET', `/api/v1/sites/${site.id}`, { as: alice });
@@ -204,7 +204,7 @@ const ORIGIN = 'https://openvibe.host';
@@ -204,7 +204,7 @@ const ORIGIN = 'https://openvibe.host';
assert.match(page.text, /Deploys in the last 24 h/);
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 } });
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);
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 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 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`;
const body = part('csrf', null, csrf) + part('strip', null, 'folder') + part('activate', null, '1') + part('files', 'site/index.html', 'DASH OK') + `--${boundary}--\r\n`;
@@ -96,7 +96,7 @@ const CUSTOM = 'www.widgets-example.org';
@@ -96,7 +96,7 @@ const CUSTOM = 'www.widgets-example.org';
assert.strictEqual(head.status, 200);
assert.strictEqual(head.status, 200);
assert.strictEqual(head.text, '');
assert.strictEqual(head.text, '');
for (const r of [sm, rb]) {
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.ok(!r.text.includes(secretish), `${secretish} in ${r.text}`);
}
}
assert.strictEqual(r.headers['set-cookie'], undefined);
assert.strictEqual(r.headers['set-cookie'], undefined);