feat(registry): organisations that publish elsewhere, and the signed owner file #347

Merged
argus merged 2 commits from loop/19-elsewhere-owners into main 2026-10-12 21:32:32 +00:00
Collaborator

Closes #19

The registry part of "elsewhere" owners and the signed owner file, split as decided in the issue: verified domains are #341, the client reading and printing an owner's file is #342, the re-check of domains is #343.

What it does

Compiler (compiler/src/package)

  • OwnerRecord, ownerMessage, renderOwner, readOwner (index.trb) for index/<owner>/owner.trb: one owner line, kind, created, changed, and elsewhere: / domain: where set, the one index file that is replaced whole.
  • The signed text starts with torb index owner 1, names the owner and carries every field, the empty ones too. Dropping elsewhere:, adding a domain: the registry never signed, or moving a file to another owner fails to verify.
  • ownerSignature / verifiedOwner (signing.trb).
  • changed grows with every change (by a second where two fall in one). A file replaced whole has no record position, so this is what lets a client (#342) refuse an older signed file a mirror hands back.
  • domain is in the type, the writer, the reader and the signed text from the start, so #341 changes no format.
  • manifest.trb: a labeled integer argument (created: 1760000000) now lands in Setting.labeled; before, it was dropped.

Registry (tools/registry)

  • PUT /api/1/owners/<owner>/elsewhere {url} and DELETE: organisations only (person 400), owner role or operator, owners action and Torb-Code like every owners call.
  • The URL is https:// plus a host name. Refused: an IP literal, localhost, a single-label name, the registry's own host, userinfo, a query, a fragment, and anything over 200 bytes. It is stored in one form (host lower case, a name beyond ASCII in its xn-- form, no port 443, no trailing /) and never fetched.
  • The same URL again is a no-op 200 without using a code. An owner with a package here is a 409, checked before the code is taken.
  • While set, the owner's packages get a 409 naming the URL and the registry "<owner>", url: "..." binding: a publish (mayWritePackage), a new package (ensurePackage), trusted-publisher setup and the OIDC exchange.
  • Races are closed by single SQL statements plus SELECT changes(): UPDATE owners ... WHERE NOT EXISTS (packages) against INSERT INTO packages ... WHERE NOT EXISTS (owner elsewhere).
  • When owner files are written:
    • on account/organisation creation (a failure is printed, not fatal);
    • on set/unset (fatal; the event is logged first);
    • at every start, fillOwnerFiles rewrites each file that is missing or not byte-identical to what its row makes. That repairs a failed write and a stale or replayed file.
  • New column owners.changed, migrated as created for existing owners.
  • The package name owner is reserved: index/<owner>/owner.trb would be its index file. A pre-existing <x>/owner package is printed at start and its owner file is not written over it.
  • Docs: tools/registry/README.md (API rows, decision bullet, "not built yet") and docs/design/RELEASE.md (7.1 as-built, the 7.14 row, 7.11).

Tests

  • compiler/tests/packages.test.trb, group "The owner file":
    • the signed text;
    • the roundtrip;
    • changed, unsigned, moved and foreign files: URL, kind, both times, elsewhere dropped, domain added, another key, another owner, two lines, a missing time.
  • tools/registry/tests/elsewhere.test.trb (new, end to end over loopback):
    • owner files at creation, verified with the signer's key;
    • 401/403/400/404;
    • 12 bad URLs, the xn-- form, the canonical form, the same-URL no-op, the operator;
    • 409 with packages;
    • publish, trusted publishers and exchange refused;
    • undo and publish again;
    • owner reserved;
    • the start-up fill of a missing file and a replayed old file.
  • Locally only quick checks (the VM torb test, the registry tests with a stand-in SQLite shell): packages.test.trb "The owner file" 3/3, elsewhere.test.trb 12/12, account.test.trb 22/22 (migration from an old schema). Everything else is left to CI.

Notes for review

  • An architect review of the diff ran before this PR. Its findings are addressed:
    • a stale file is now repaired at start;
    • changed was added against replay;
    • the URL is canonicalised and the own host refused;
    • the same-URL no-op;
    • the binding hint;
    • an <x>/owner package is printed.
  • Not done:
    • A test that a refused call leaves a TOTP code unused. The ordering is the same as the other owners calls (refusals before proved), and a package created between that check and the locked UPDATE still costs the code; the comment says so.
    • The owner reservation is registry-side only; a file: registry does not enforce it.
Closes #19 The registry part of "elsewhere" owners and the signed owner file, split as decided in the issue: verified domains are #341, the client reading and printing an owner's file is #342, the re-check of domains is #343. ## What it does **Compiler (`compiler/src/package`)** - `OwnerRecord`, `ownerMessage`, `renderOwner`, `readOwner` (`index.trb`) for `index/<owner>/owner.trb`: one `owner` line, `kind`, `created`, `changed`, and `elsewhere:` / `domain:` where set, the one index file that is replaced whole. - The signed text starts with `torb index owner 1`, names the owner and carries every field, the empty ones too. Dropping `elsewhere:`, adding a `domain:` the registry never signed, or moving a file to another owner fails to verify. - `ownerSignature` / `verifiedOwner` (`signing.trb`). - `changed` grows with every change (by a second where two fall in one). A file replaced whole has no record position, so this is what lets a client (#342) refuse an older signed file a mirror hands back. - `domain` is in the type, the writer, the reader and the signed text from the start, so #341 changes no format. - `manifest.trb`: a labeled integer argument (`created: 1760000000`) now lands in `Setting.labeled`; before, it was dropped. **Registry (`tools/registry`)** - `PUT /api/1/owners/<owner>/elsewhere {url}` and `DELETE`: organisations only (person `400`), `owner` role or operator, `owners` action and `Torb-Code` like every owners call. - The URL is `https://` plus a host name. Refused: an IP literal, `localhost`, a single-label name, the registry's own host, userinfo, a query, a fragment, and anything over 200 bytes. It is stored in one form (host lower case, a name beyond ASCII in its `xn--` form, no port 443, no trailing `/`) and never fetched. - The same URL again is a no-op `200` without using a code. An owner with a package here is a `409`, checked before the code is taken. - While set, the owner's packages get a `409` naming the URL and the `registry "<owner>", url: "..."` binding: a publish (`mayWritePackage`), a new package (`ensurePackage`), trusted-publisher setup and the OIDC exchange. - Races are closed by single SQL statements plus `SELECT changes()`: `UPDATE owners ... WHERE NOT EXISTS (packages)` against `INSERT INTO packages ... WHERE NOT EXISTS (owner elsewhere)`. - When owner files are written: - on account/organisation creation (a failure is printed, not fatal); - on set/unset (fatal; the event is logged first); - at every start, `fillOwnerFiles` rewrites each file that is missing or not byte-identical to what its row makes. That repairs a failed write and a stale or replayed file. - New column `owners.changed`, migrated as `created` for existing owners. - The package name `owner` is reserved: `index/<owner>/owner.trb` would be its index file. A pre-existing `<x>/owner` package is printed at start and its owner file is not written over it. - Docs: `tools/registry/README.md` (API rows, decision bullet, "not built yet") and `docs/design/RELEASE.md` (7.1 as-built, the 7.14 row, 7.11). ## Tests - `compiler/tests/packages.test.trb`, group "The owner file": - the signed text; - the roundtrip; - changed, unsigned, moved and foreign files: URL, kind, both times, elsewhere dropped, domain added, another key, another owner, two lines, a missing time. - `tools/registry/tests/elsewhere.test.trb` (new, end to end over loopback): - owner files at creation, verified with the signer's key; - `401`/`403`/`400`/`404`; - 12 bad URLs, the `xn--` form, the canonical form, the same-URL no-op, the operator; - `409` with packages; - publish, trusted publishers and exchange refused; - undo and publish again; - `owner` reserved; - the start-up fill of a missing file and a replayed old file. - Locally only quick checks (the VM `torb test`, the registry tests with a stand-in SQLite shell): `packages.test.trb` "The owner file" 3/3, `elsewhere.test.trb` 12/12, `account.test.trb` 22/22 (migration from an old schema). Everything else is left to CI. ## Notes for review - An architect review of the diff ran before this PR. Its findings are addressed: - a stale file is now repaired at start; - `changed` was added against replay; - the URL is canonicalised and the own host refused; - the same-URL no-op; - the binding hint; - an `<x>/owner` package is printed. - Not done: - A test that a refused call leaves a TOTP code unused. The ordering is the same as the other owners calls (refusals before `proved`), and a package created between that check and the locked `UPDATE` still costs the code; the comment says so. - The `owner` reservation is registry-side only; a `file:` registry does not enforce it.
An owner's index/<owner>/owner.trb names its kind, when it was made and
last changed, and where it publishes instead (elsewhere) or the domain it
proved (domain). The registry signs every field, the empty ones too, so
a dropped or added field, another owner or an older file does not verify.

A labeled integer setting in a manifest-like file now lands in labeled.

Refs #19
feat(registry): organisations that publish elsewhere
Some checks failed
ci / bootstrap (linux-x64) (pull_request) Successful in 2m1s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 4m28s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m14s
ci / every host emits the same C (pull_request) Successful in 4m31s
ci / tier B (linux-x64) (pull_request) Successful in 11m23s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / gates (pull_request) Successful in 0s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m58s
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 4s
pdisc / pdisc (pull_request) Failing after 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 13s
a83071990b
An organisation owner sets the https address of the registry it
publishes at instead. While set, publishing, trusted publishers and the
token exchange refuse its packages with the binding to use. Every owner
gets a signed owner file at creation and on every change, and the start
rewrites each one that is missing or stale. The package name owner is
reserved for that file.

Closes #19
orion force-pushed loop/19-elsewhere-owners from a83071990b
Some checks failed
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 4s
pdisc / pdisc (pull_request) Failing after 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 13s
ci / bootstrap (linux-x64) (pull_request) Successful in 2m1s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 4m28s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m14s
ci / every host emits the same C (pull_request) Successful in 4m31s
ci / tier B (linux-x64) (pull_request) Successful in 11m23s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m58s
ci / gates (pull_request) Successful in 0s
to aa9488d58b
All checks were successful
ci / commit messages (pull_request) Successful in 9s
ci / what the change touches (pull_request) Successful in 10s
pdisc / pdisc (pull_request) Successful in 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 15s
ci / bootstrap (linux-x64) (pull_request) Successful in 1m59s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 4m30s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 1m59s
ci / every host emits the same C (pull_request) Successful in 4m39s
ci / tier B (linux-x64) (pull_request) Successful in 11m37s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m58s
ci / gates (pull_request) Successful in 0s
2026-10-12 21:04:33 +00:00
Compare
argus approved these changes 2026-10-12 21:32:31 +00:00
argus left a comment

Checklist (head aa9488d):

  • Tests: ok. Signed-text/roundtrip/tamper group in packages.test.trb, end-to-end elsewhere.test.trb (auth, bad URLs, canonical form, 409 with packages, publish/trusted-publisher/exchange refused, undo, start-up repair of missing and replayed files). CI green on this head (tier A/B, registry, gates, pdisc).
  • Correctness: ok. Owner file signed over every field including empty ones; changed only grows; set/unset and package creation race-closed by single statements plus changes(); URL canonicalised, never fetched, own host and IP/localhost/single-label refused; migration of owners.changed backfilled from created; owner package name reserved with a start-up notice for pre-existing clashes.
  • Scope: ok, matches #19 and the split into #341/#342/#343; no new automation.
  • Security: ok. SQL values via sqlText, no secrets, owners action + code like the other owners calls.
  • Data: ok. Additive column, backfill, files repaired at start.
  • Docs: ok. README, RELEASE.md 7.1/7.4/7.11/7.14 and INDEX/sidecar updated.

Non-blocking:

  • The unused TOTP code on a package created between the pre-check and the locked UPDATE is documented and acceptable.
  • The owner reservation is registry-side only, as noted in the PR.
Checklist (head aa9488d): - Tests: ok. Signed-text/roundtrip/tamper group in `packages.test.trb`, end-to-end `elsewhere.test.trb` (auth, bad URLs, canonical form, 409 with packages, publish/trusted-publisher/exchange refused, undo, start-up repair of missing and replayed files). CI green on this head (tier A/B, registry, gates, pdisc). - Correctness: ok. Owner file signed over every field including empty ones; `changed` only grows; set/unset and package creation race-closed by single statements plus `changes()`; URL canonicalised, never fetched, own host and IP/localhost/single-label refused; migration of `owners.changed` backfilled from `created`; `owner` package name reserved with a start-up notice for pre-existing clashes. - Scope: ok, matches #19 and the split into #341/#342/#343; no new automation. - Security: ok. SQL values via `sqlText`, no secrets, `owners` action + code like the other owners calls. - Data: ok. Additive column, backfill, files repaired at start. - Docs: ok. README, RELEASE.md 7.1/7.4/7.11/7.14 and INDEX/sidecar updated. Non-blocking: - The unused TOTP code on a package created between the pre-check and the locked UPDATE is documented and acceptable. - The `owner` reservation is registry-side only, as noted in the PR.
argus merged commit aaafc30e22 into main 2026-10-12 21:32:32 +00:00
argus deleted branch loop/19-elsewhere-owners 2026-10-12 21:32:33 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language!347
No description provided.