@cruba/crubal-host 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,138 @@
1
- # Temporary Holding Version
1
+ # @cruba/crubal-host
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The reference CRUBAL host (docs/design/host.md): what the specification hands the host, and nothing a
4
+ business requires. It runs owners' stores on `@cruba/crubal-postgres`, proves who a caller is, resolves
5
+ secrets by name, acts through adapters, signs each owner's chain, installs published packages, publishes
6
+ views and runs schedules, and serves one small HTTP surface. It owns no model: everything goes through
7
+ `PgStore` and `PgBorder`, and what it keeps of its own (owners' keys, API keys, identity links) lives in
8
+ its own tables.
9
+
10
+ ## Operations
11
+
12
+ Request bodies over `bodyLimit` (1 MiB) are refused with 413 before anything reads them; `cors` lists the
13
+ browser origins that get the headers and their preflight; one structured log line per request (owner,
14
+ caller, route, status, ms) goes to `log` or the console. Credentials' secrets beyond the connected table come
15
+ from a `SecretStore` (the environment, a function, a chain; a vault is one more implementation). Views with
16
+ `fresh: write` publish at the commit; a view with a delay (`fresh: 5m`) is published by the `/project` loop
17
+ once that much has passed since its last publish. The container is the first deployment shape:
18
+ `Dockerfile`, `compose.yml` (Postgres and the host, migrated on start) and `smoke.sh`, which brings it up,
19
+ checks `/health`, makes an owner and runs this suite against it. Environment: `CRUBAL_PG_URL`,
20
+ `CRUBAL_HOST_DOMAIN`, `CRUBAL_HOST_KEY`, `CRUBAL_HOST_CORS`, `CRUBAL_HOST_BODY_LIMIT`.
21
+
22
+ ## What it does
23
+
24
+ - **Identity.** An API key, or a token an identity provider issued: `oidcVerifier({issuer, audience, jwks |
25
+ secret, provider, identity})` checks a JWT against the issuer's JWKS (or a shared secret for HS256), its
26
+ issuer, audience and expiry, and maps its subject through the host's identity links
27
+ (`linkIdentity(provider, subject, owner, caller)`) to a caller. Supabase Auth is one such issuer. A
28
+ request signed by another owner (RFC 9421 message signatures over the method, target, body digest and
29
+ date, with the domain's Ed25519 key) resolves to the caller `<domain>:owner`, that owner acting as itself,
30
+ its key fetched from its well-known file.
31
+ - **Owners and keys.** `createOwner(domain)` makes an owner with an Ed25519 key; every entry the owner's
32
+ store writes is signed with it, and the public half is in the host's well-known file, so an export
33
+ verifies anywhere. `issueKey(owner, caller)` issues an API key for a caller record (printed once, kept
34
+ hashed); `whoIs(authorization)` resolves an API key, or any bearer token a configured verifier knows (an
35
+ OIDC-shaped adapter goes here), to `{owner, caller}`. The store sees only the caller ID.
36
+ - **Credentials.** `secret(owner, name)` resolves a contract's credential name: a connection made in the
37
+ host (`host.credentials`: a pasted key or password, or an OAuth grant through
38
+ `GET /o/<owner>/connect/<name>` → the provider → `/oauth/callback`), refreshed through the provider when
39
+ expired and marked `expired` when it can't be; else a configured source or the environment
40
+ (`CRUBAL_SECRET_<OWNER>_<NAME>`). The fact of a connection (scheme, scopes, status, when) is readable at
41
+ `GET /o/<owner>/connections`; the material never leaves the host's table, never enters a package or the
42
+ log. A platform puts a vault behind the same interface.
43
+ - **Adapters.** A send's act is an adapter's: the built-in `http` adapter POSTs the payload to the
44
+ contract's `host:url` with the credential the contract names, sent as its `auth` scheme says
45
+ (`apikey`/`oauth2` as a bearer, `basic`), and brings the answer back as intake; a named adapter per
46
+ contract replaces it. A missing secret or a 401 is a `failed` run with reason `credential`.
47
+ - **Packages.** `install(owner, bundle)` fetches the publisher's keys from its well-known file and hands
48
+ the store a verified bundle.
49
+ - **The loops.** `project()` publishes every owner's stale rows (or `fresh: "write"` does it after each
50
+ commit); `tick(now)` runs every scheduled contract's due ticks; `retry(now)` tries failed runs again
51
+ once their contract's `retry` interval has passed, from the kept payload, and notices the owner of a run
52
+ dead after its last attempt. All three are behind `POST /project`, `POST /tick` and `POST /retry` for a
53
+ scheduler, guarded by the host key.
54
+ - **Terms.** A package's `terms` (12.5.1) become `GET /o/<owner>/robots.txt` (the training bots disallowed
55
+ under `train no`, everything under `search no`), `GET /o/<owner>/terms.json`, and `X-Robots-Tag` /
56
+ `X-Crubal-Terms` headers on every view page.
57
+ - **Emergency access** (14.13). With `emergency` terms configured (approvers, longest, the approvers'
58
+ dedicated keys) and published in the well-known file, `POST /o/<owner>/emergency` behind the host key
59
+ makes an edge only when the approvals are enough, each a signature over the edge with an approver's key,
60
+ none the requester's, the mode view or control, no grant, and `until` within the longest; it is written
61
+ witnessed into the owner's log and the owner is noticed. Every record read under it is a witnessed `read`
62
+ write. `POST /o/<owner>/emergency/<edge>/end` ends it for the owner, except a legal order's, whose attempt
63
+ is recorded. A witnessed entry is never redacted.
64
+
65
+ ## The reference index
66
+
67
+ `GET /o/<owner>/v/<view>?page=&<filter>=<bucket>,<bucket>&q=` serves a view's page from the index
68
+ (`src/indexer.ts`, docs/design/index-and-forms.md): the index pulls the view's projection stream from the
69
+ store into its own `index_rows`, idempotently by (view, audience, seq), truncating on a `reset`, and
70
+ serves pages in the view's order from the `sort` and `pin` the store put on each row, counts per bucket
71
+ over the rows the *other* picks leave, full-text search over the row's text slots, partition by the caller's
72
+ edges, role-marked slots dropped for a caller without the role, and the `X-Robots-Tag` header
73
+ (`noindex` past page one or off a single `index` bucket). It reads nothing but the stream and can be
74
+ dropped and rebuilt from it.
75
+
76
+ ## The HTTP surface
77
+
78
+ ```
79
+ GET /.well-known/crubal.json
80
+ GET /o/<owner>/v/<view>?page=&<filter>=&q= a view's page, counts and indexability, from the index
81
+ GET /o/<owner>/robots.txt · GET /o/<owner>/terms.json
82
+ GET /o/<owner>/connections · GET|POST|DELETE /o/<owner>/connect/<name> · GET /oauth/callback owner only
83
+ POST /o/<owner>/emergency (platform) · POST /o/<owner>/emergency/<edge>/end (owner or platform)
84
+ POST /o/<owner>/run/<contract> { record, answers? } a form run lands the answers; a send acts
85
+ POST /o/<owner>/receive/<contract>?record=&run= an answer (or an arrival with ?level=&of=)
86
+ GET /o/<owner>/records/<id>[?full=1] the values the caller may see
87
+ GET /o/<owner>/forms/<contract>?record= the form as data for this caller
88
+ GET /o/<owner>/stream/<contract>?cursor=view=seq · POST …/ack { cursor }
89
+ GET /o/<owner>/v/<view>?…&shape=<name> (a further shape the view names: crubal:jsonld, crubal:portable)
90
+ GET /o/<owner>/offer the offers this caller may read (7.8 `to`: public, link token, followers, named parties)
91
+ GET /o/<owner>/offer/<contract>[?link=] one offer as a border wrapper: records with sources, mode, terms, the signed entries
92
+ POST /o/<owner>/offer/<contract>/link the owner mints the link token (kept across republishing)
93
+ POST /o/<owner>/offer/<contract>/unpublish the token stops working, nothing is deleted
94
+ POST /o/<owner>/take/<contract> the owner takes: a signed fetch of the presenter's offer, the wrapper verified against its keys, each record taken into ours (bound by the presenter's id)
95
+ POST /o/<owner>/inbox/<contract> a presenter's pushed stream entries, signed; they land in the index under the presenter's name
96
+ POST /push · POST /expire loops behind the host key: push `when: entry` streams and acknowledge; sweep taken values past their terms
97
+ GET /o/<domain>:<id>/export · POST /o/<domain>:<id>/erase/<record> a hosted person (14.1), signed in to the domain as that caller: their own chain, their own records only
98
+ POST /o/<owner>/rotate a new key signs from now; the well-known file lists the old with until and the new with from (11.7)
99
+ POST /o/<owner>/install { package, version? } install by name from the publisher's well-known file (11.7): the bundle fetched where the file says, what it extends installed first, each verified against the publisher's keys; { bundle } installs one handed in
100
+ GET /health the database reachable and every chain's head (503 when not)
101
+ POST /drain a loop behind the host key: pending instructions applied at a rate ({ owner?, rate? })
102
+ GET /o/<owner>/review (open items by key) · POST /o/<owner>/review { key, decision, … }
103
+ POST /o/<owner>/install { bundle } owner only
104
+ GET /o/<owner>/export owner only: the log and the intake
105
+ POST /o/<owner>/erase/<record> owner only
106
+ POST /project · POST /tick · POST /retry the loops; X-Host-Key
107
+ ```
108
+
109
+ A route with no caller is `public`; what a caller may run, edit or see is the store's access (chapter 14),
110
+ never the host's. The owner is a caller whose ID is the owner itself.
111
+
112
+ ## Running it
113
+
114
+ `CRUBAL_PG_URL=… CRUBAL_HOST_DOMAIN=… CRUBAL_HOST_KEY=…`, then `node bin/crubal-host.ts migrate`,
115
+ `owner create <domain>`, `key issue <owner> <caller>`, `serve --port 8787`. Everything runs and is tested
116
+ locally (`CRUBAL_PG_URL=postgres:///postgres npm test`): the test starts the server on a free port, makes
117
+ an owner and keys, installs a signed package through the API, runs a form and a send with an adapter whose
118
+ answer closes the run, reads the record as staff, verifies the exported log against the well-known file,
119
+ erases as the owner, and drives the loops with the host key.
120
+
121
+ ## Not yet here
122
+
123
+ The `system` record a connection writes into the owner's data (it needs the party package's kinds, which
124
+ wait with the vocabularies), and a deployment: the host runs and is tested locally only.
125
+
126
+ ## The platform surface (14.8, 14.9; 2026-10-16)
127
+
128
+ - **Hosted callers.** A person proven by an identity token is `person:<domain:id>` at every other owner on
129
+ the host, with no signature; owners and their admins grant them edges within the grantor chain. A system
130
+ (API key) never crosses owners by itself; another host's callers keep the signed path.
131
+ - **Acting for a person.** `POST /o/<domain:id>/acting-for {system: "<owner>:<id>", items?, until?,
132
+ gated?}` by the person; `GET` lists, `DELETE /…/<id>` revokes. A run or receive carrying `for:
133
+ "<domain:id>"` from that system runs as the person, capped to the items, landing as suggestions while
134
+ gated; the entry names `caller` and `for`.
135
+ - **The platform index.** `GET /whoami`: the caller's live edges across every owner, grouped by owner with
136
+ the mode, relations and grants they add up to. `GET /whoami/rows?relation=<name>[&view=…&limit=…]`: the
137
+ index rows they stand in that relation to, across owners. `bench/platform.ts` measures both over ten
138
+ owners and a million records.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // crubal-host: run the reference host over a Postgres store. CRUBAL_PG_URL names the database;
3
+ // CRUBAL_HOST_DOMAIN the host's domain; CRUBAL_HOST_KEY guards the loops.
4
+ // crubal-host migrate the store's tables and the host's
5
+ // crubal-host owner create <domain> an owner, with the key the host signs its chain with
6
+ // crubal-host key issue <owner> <caller> an API key for a caller (printed once)
7
+ // crubal-host serve [--port 8787] the HTTP surface (CRUBAL_HOST_OF=domain=url,… maps publishers not yet at https://<domain>)
8
+ import { connect } from "@cruba/crubal-postgres";
9
+ import { Host } from "../src/host.js";
10
+ import { serve } from "../src/server.js";
11
+ const [cmd, a, b, c] = process.argv.slice(2);
12
+ const flag = (name) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : undefined; };
13
+ const pool = connect();
14
+ const host = new Host(pool, {
15
+ domain: process.env.CRUBAL_HOST_DOMAIN ?? "localhost",
16
+ ...(process.env.CRUBAL_HOST_KEY ? { hostKey: process.env.CRUBAL_HOST_KEY } : {}),
17
+ ...(process.env.CRUBAL_HOST_CORS ? { cors: process.env.CRUBAL_HOST_CORS.split(",").map((s) => s.trim()) } : {}),
18
+ ...(process.env.CRUBAL_HOST_BODY_LIMIT ? { bodyLimit: Number(process.env.CRUBAL_HOST_BODY_LIMIT) } : {}),
19
+ // CRUBAL_HOST_OF maps a domain to where its host answers, for a gate run against a publisher that isn't
20
+ // at https://<domain> yet: "cashierfu.com=http://localhost:3100/h,other.example=http://…".
21
+ ...(process.env.CRUBAL_HOST_OF ? { hostOf: hostOfFrom(process.env.CRUBAL_HOST_OF) } : {}),
22
+ fresh: "write",
23
+ });
24
+ function hostOfFrom(spec) {
25
+ const map = new Map(spec.split(",").map((pair) => pair.trim()).filter(Boolean).map((pair) => { const i = pair.indexOf("="); return [pair.slice(0, i).trim(), pair.slice(i + 1).trim()]; }));
26
+ return (domain) => map.get(domain) ?? `https://${domain}`;
27
+ }
28
+ try {
29
+ if (cmd === "migrate") {
30
+ await host.migrate();
31
+ console.log("migrated");
32
+ }
33
+ else if (cmd === "owner" && a === "create" && b) {
34
+ console.log(JSON.stringify(await host.createOwner(b)));
35
+ }
36
+ else if (cmd === "key" && a === "issue" && b && c) {
37
+ console.log(await host.issueKey(b, c));
38
+ }
39
+ else if (cmd === "serve") {
40
+ const port = Number(flag("--port") ?? 8787);
41
+ serve(host).listen(port, () => console.log(`crubal-host on :${port}`));
42
+ }
43
+ else {
44
+ console.log("usage: crubal-host migrate | owner create <domain> | key issue <owner> <caller> | serve [--port 8787]");
45
+ process.exit(2);
46
+ }
47
+ }
48
+ finally {
49
+ if (cmd !== "serve")
50
+ await pool.end();
51
+ }
52
+ //# sourceMappingURL=crubal-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crubal-host.js","sourceRoot":"","sources":["../../bin/crubal-host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import type { JsonObject } from "@cruba/crubal";
2
+ import type { Pool } from "@cruba/crubal-postgres";
3
+ /** The key material is sealed with (AES-256-GCM); a row that holds this shape is ciphertext. */
4
+ type Sealed = {
5
+ $sealed: 1;
6
+ iv: string;
7
+ tag: string;
8
+ data: string;
9
+ };
10
+ /** A 32-byte key from what the environment holds: 64 hex characters, 44 base64 characters, or any text hashed. */
11
+ export declare function secretsKeyFrom(raw: string | undefined): Buffer | undefined;
12
+ export type Connection = {
13
+ owner: string;
14
+ name: string;
15
+ scheme: string;
16
+ scopes: string[];
17
+ status: "connected" | "expired";
18
+ expires?: string;
19
+ connected: string;
20
+ };
21
+ export type OAuthProvider = {
22
+ authorizeUrl: string;
23
+ tokenUrl: string;
24
+ clientId: string;
25
+ clientSecret: string;
26
+ /** The redirect the provider sends the code to; the host's connect route. */
27
+ redirectUri: string;
28
+ };
29
+ export declare class Credentials {
30
+ readonly pool: Pool;
31
+ readonly now: () => string;
32
+ /** The key the material is sealed with at rest; without one no secret is stored. */
33
+ readonly key?: Buffer;
34
+ constructor(pool: Pool, now: () => string, key?: Buffer);
35
+ /** Seal material for the table (AES-256-GCM, a fresh nonce each time). */
36
+ seal(material: JsonObject): Sealed;
37
+ /** The material back from the table: sealed rows opened with the key; a row from before sealing read as it is. */
38
+ open(stored: unknown): JsonObject;
39
+ /** Seal every row stored before sealing existed; run once at migration. Returns how many it sealed. */
40
+ sealAll(): Promise<number>;
41
+ /** Connect a credential: the material under the name, sealed, with its scheme, scopes and expiry. */
42
+ connect(owner: string, name: string, scheme: string, material: JsonObject, scopes?: string[], expires?: string): Promise<Connection>;
43
+ disconnect(owner: string, name: string): Promise<void>;
44
+ /** The fact of a connection, never its material: what a `system` record carries and a view may show. */
45
+ connection(owner: string, name: string): Promise<Connection | undefined>;
46
+ connections(owner: string): Promise<Connection[]>;
47
+ /**
48
+ * The secret an adapter sends: the key or password as stored, or an OAuth access token, refreshed first
49
+ * when it has expired and a refresh token is held. Undefined when nothing is connected under the name.
50
+ */
51
+ secret(owner: string, name: string, provider?: OAuthProvider): Promise<string | undefined>;
52
+ /** Where to send a person to connect an OAuth account; the state names the owner and the credential. */
53
+ authorizeUrl(owner: string, name: string, provider: OAuthProvider, scopes: string[]): string;
54
+ /** The code the provider sent back becomes the connection. */
55
+ exchangeCode(owner: string, name: string, code: string, provider: OAuthProvider, scopes: string[], fetchFn?: typeof fetch): Promise<Connection>;
56
+ private refresh;
57
+ }
58
+ export {};
@@ -0,0 +1,161 @@
1
+ // credentials.ts: what stands behind a contract's `credential` name (docs/design/host.md, "Outbound"). A
2
+ // credential is per owner and name, holds scheme-shaped material, is connected by a host flow (OAuth's
3
+ // consent, or a pasted key), refreshed and rotated by the host, and never enters a package or the log. The
4
+ // reference host keeps the material in its own table; a platform puts a vault behind the same interface.
5
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
6
+ const isSealed = (v) => typeof v === "object" && v !== null && v.$sealed === 1;
7
+ /** A 32-byte key from what the environment holds: 64 hex characters, 44 base64 characters, or any text hashed. */
8
+ export function secretsKeyFrom(raw) {
9
+ if (!raw)
10
+ return undefined;
11
+ const t = raw.trim();
12
+ if (/^[0-9a-fA-F]{64}$/.test(t))
13
+ return Buffer.from(t, "hex");
14
+ if (/^[A-Za-z0-9+/=_-]{43,44}$/.test(t))
15
+ return Buffer.from(t.replace(/-/g, "+").replace(/_/g, "/"), "base64");
16
+ return createHash("sha256").update(t).digest();
17
+ }
18
+ export class Credentials {
19
+ pool;
20
+ now;
21
+ /** The key the material is sealed with at rest; without one no secret is stored. */
22
+ key;
23
+ constructor(pool, now, key) {
24
+ this.pool = pool;
25
+ this.now = now;
26
+ const k = key ?? secretsKeyFrom(process.env.CRUBAL_SECRETS_KEY);
27
+ if (k)
28
+ this.key = k;
29
+ }
30
+ /** Seal material for the table (AES-256-GCM, a fresh nonce each time). */
31
+ seal(material) {
32
+ if (!this.key)
33
+ throw new Error("a secrets key is required to store a secret (CRUBAL_SECRETS_KEY); none is set");
34
+ const iv = randomBytes(12);
35
+ const cipher = createCipheriv("aes-256-gcm", this.key, iv);
36
+ const data = Buffer.concat([cipher.update(JSON.stringify(material), "utf8"), cipher.final()]);
37
+ return { $sealed: 1, iv: iv.toString("base64"), tag: cipher.getAuthTag().toString("base64"), data: data.toString("base64") };
38
+ }
39
+ /** The material back from the table: sealed rows opened with the key; a row from before sealing read as it is. */
40
+ open(stored) {
41
+ if (!isSealed(stored))
42
+ return (stored ?? {});
43
+ if (!this.key)
44
+ throw new Error("a secrets key is required to read a sealed secret (CRUBAL_SECRETS_KEY); none is set");
45
+ const decipher = createDecipheriv("aes-256-gcm", this.key, Buffer.from(stored.iv, "base64"));
46
+ decipher.setAuthTag(Buffer.from(stored.tag, "base64"));
47
+ const text = Buffer.concat([decipher.update(Buffer.from(stored.data, "base64")), decipher.final()]).toString("utf8");
48
+ return JSON.parse(text);
49
+ }
50
+ /** Seal every row stored before sealing existed; run once at migration. Returns how many it sealed. */
51
+ async sealAll() {
52
+ if (!this.key)
53
+ return 0;
54
+ const { rows } = await this.pool.query("select owner, name, material from host_credentials");
55
+ let n = 0;
56
+ for (const r of rows) {
57
+ if (isSealed(r.material))
58
+ continue;
59
+ await this.pool.query("update host_credentials set material = $3 where owner = $1 and name = $2", [r.owner, r.name, this.seal((r.material ?? {}))]);
60
+ n++;
61
+ }
62
+ return n;
63
+ }
64
+ /** Connect a credential: the material under the name, sealed, with its scheme, scopes and expiry. */
65
+ async connect(owner, name, scheme, material, scopes = [], expires) {
66
+ await this.pool.query(`insert into host_credentials (owner, name, scheme, material, scopes, expires, connected) values ($1, $2, $3, $4, $5, $6, $7)
67
+ on conflict (owner, name) do update set scheme = excluded.scheme, material = excluded.material, scopes = excluded.scopes, expires = excluded.expires, connected = excluded.connected`, [owner, name, scheme, this.seal(material), scopes, expires ?? null, this.now()]);
68
+ return (await this.connection(owner, name));
69
+ }
70
+ async disconnect(owner, name) {
71
+ await this.pool.query("delete from host_credentials where owner = $1 and name = $2", [owner, name]);
72
+ }
73
+ /** The fact of a connection, never its material: what a `system` record carries and a view may show. */
74
+ async connection(owner, name) {
75
+ const { rows } = await this.pool.query("select owner, name, scheme, scopes, expires, connected from host_credentials where owner = $1 and name = $2", [owner, name]);
76
+ const r = rows[0];
77
+ if (!r)
78
+ return undefined;
79
+ const expires = r.expires ? String(r.expires) : undefined;
80
+ return { owner, name, scheme: String(r.scheme), scopes: r.scopes ?? [], status: expires && expires <= this.now() ? "expired" : "connected", ...(expires ? { expires } : {}), connected: String(r.connected) };
81
+ }
82
+ async connections(owner) {
83
+ const { rows } = await this.pool.query("select name from host_credentials where owner = $1 order by connected", [owner]);
84
+ const out = [];
85
+ for (const r of rows) {
86
+ const c = await this.connection(owner, String(r.name));
87
+ if (c)
88
+ out.push(c);
89
+ }
90
+ return out;
91
+ }
92
+ /**
93
+ * The secret an adapter sends: the key or password as stored, or an OAuth access token, refreshed first
94
+ * when it has expired and a refresh token is held. Undefined when nothing is connected under the name.
95
+ */
96
+ async secret(owner, name, provider) {
97
+ const { rows } = await this.pool.query("select scheme, material, expires from host_credentials where owner = $1 and name = $2", [owner, name]);
98
+ const r = rows[0];
99
+ if (!r)
100
+ return undefined;
101
+ const material = this.open(r.material);
102
+ if (r.scheme === "oauth2") {
103
+ const expired = r.expires ? String(r.expires) <= this.now() : false;
104
+ if (expired && typeof material.refresh_token === "string" && provider) {
105
+ const fresh = await this.refresh(owner, name, material.refresh_token, provider);
106
+ return fresh;
107
+ }
108
+ if (expired)
109
+ return undefined;
110
+ return typeof material.access_token === "string" ? material.access_token : undefined;
111
+ }
112
+ if (r.scheme === "basic")
113
+ return `${material.username ?? ""}:${material.password ?? ""}`;
114
+ return typeof material.key === "string" ? material.key : typeof material.secret === "string" ? material.secret : undefined;
115
+ }
116
+ /** Where to send a person to connect an OAuth account; the state names the owner and the credential. */
117
+ authorizeUrl(owner, name, provider, scopes) {
118
+ const u = new URL(provider.authorizeUrl);
119
+ u.searchParams.set("response_type", "code");
120
+ u.searchParams.set("client_id", provider.clientId);
121
+ u.searchParams.set("redirect_uri", provider.redirectUri);
122
+ u.searchParams.set("scope", scopes.join(" "));
123
+ u.searchParams.set("state", Buffer.from(JSON.stringify({ owner, name })).toString("base64url"));
124
+ return u.toString();
125
+ }
126
+ /** The code the provider sent back becomes the connection. */
127
+ async exchangeCode(owner, name, code, provider, scopes, fetchFn = fetch) {
128
+ const res = await fetchFn(provider.tokenUrl, {
129
+ method: "POST",
130
+ headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString("base64")}` },
131
+ body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: provider.redirectUri }).toString(),
132
+ });
133
+ if (!res.ok)
134
+ throw new Error(`the token endpoint answered ${res.status}`);
135
+ const tok = (await res.json());
136
+ const expires = tok.expires_in ? new Date(Date.parse(this.now()) + tok.expires_in * 1000).toISOString().replace(/\.\d{3}Z$/, "Z") : undefined;
137
+ return this.connect(owner, name, "oauth2", { access_token: tok.access_token, ...(tok.refresh_token ? { refresh_token: tok.refresh_token } : {}) }, tok.scope ? tok.scope.split(" ") : scopes, expires);
138
+ }
139
+ async refresh(owner, name, refreshToken, provider, fetchFn = fetch) {
140
+ // The scopes the connection was granted ride along: some providers (eBay) require them on a refresh.
141
+ const { rows: held } = await this.pool.query("select scopes from host_credentials where owner = $1 and name = $2", [owner, name]);
142
+ const scopes = held[0]?.scopes ?? [];
143
+ const res = await fetchFn(provider.tokenUrl, {
144
+ method: "POST",
145
+ headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString("base64")}` },
146
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, ...(scopes.length ? { scope: scopes.join(" ") } : {}) }).toString(),
147
+ });
148
+ if (!res.ok) {
149
+ // A connection that can't be refreshed is expired: the fact is visible, the secret gone.
150
+ await this.pool.query("update host_credentials set expires = $3 where owner = $1 and name = $2", [owner, name, this.now()]);
151
+ return undefined;
152
+ }
153
+ const tok = (await res.json());
154
+ const expires = tok.expires_in ? new Date(Date.parse(this.now()) + tok.expires_in * 1000).toISOString().replace(/\.\d{3}Z$/, "Z") : undefined;
155
+ const { rows } = await this.pool.query("select material, scopes from host_credentials where owner = $1 and name = $2", [owner, name]);
156
+ const was = rows[0] ?? {};
157
+ await this.connect(owner, name, "oauth2", { ...this.open(was.material), access_token: tok.access_token, ...(tok.refresh_token ? { refresh_token: tok.refresh_token } : {}) }, was.scopes ?? [], expires);
158
+ return tok.access_token;
159
+ }
160
+ }
161
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { JsonObject } from "@cruba/crubal";
2
+ import type { PgStore } from "@cruba/crubal-postgres";
3
+ export type EmergencyTerms = {
4
+ /** How many approvers an edge needs. */
5
+ approvers: number;
6
+ /** The longest an edge may last, ISO 8601 (`PT24H`). */
7
+ longest: string;
8
+ /** How owners are told. */
9
+ notify?: string;
10
+ /** The approvers' dedicated keys, by approver ID: Ed25519 public halves. */
11
+ keys: {
12
+ [approver: string]: {
13
+ alg: string;
14
+ public: string;
15
+ };
16
+ };
17
+ };
18
+ export type EmergencyRequest = {
19
+ edge: string;
20
+ who: string;
21
+ mode: "view" | "control";
22
+ on: string;
23
+ grants?: string[];
24
+ until: string;
25
+ kind: "legal" | "incident";
26
+ reason: string;
27
+ reference: string;
28
+ /** Each approval: the approver and their signature over the edge's hash. */
29
+ approvals: {
30
+ by: string;
31
+ sig: string;
32
+ }[];
33
+ notify?: "now" | {
34
+ delayUntil: string;
35
+ order: string;
36
+ };
37
+ };
38
+ /** What an approver signs: the edge without its approvals and notice. */
39
+ export declare function edgeHash(r: EmergencyRequest): string;
40
+ /** Check a request against the terms. Returns the reasons it is refused; none means it may be made. */
41
+ export declare function checkEmergency(r: EmergencyRequest, terms: EmergencyTerms | undefined, now: string): string[];
42
+ /** Make the edge in the owner's log: witnessed, by platform, its approvals carried. */
43
+ export declare function makeEmergencyEdge(store: PgStore, r: EmergencyRequest, terms: EmergencyTerms | undefined, now: string): Promise<{
44
+ edge: JsonObject;
45
+ entry: string;
46
+ }>;
47
+ /** Log a read under an emergency edge (14.13): record by record, witnessed. */
48
+ export declare function logEmergencyRead(store: PgStore, who: string, recordId: string, edge: string, now: string): Promise<void>;
49
+ /** End an emergency edge: the owner may, unless it cites a legal order; the attempt is recorded either way. */
50
+ export declare function endEmergencyEdge(store: PgStore, edgeId: string, by: string, now: string, isOwner: boolean): Promise<{
51
+ ended: boolean;
52
+ reason?: string;
53
+ }>;
@@ -0,0 +1,91 @@
1
+ // emergency.ts: emergency access (spec 14.13), the mechanics a platform enforces. An emergency edge is
2
+ // created only under published terms, with the approvals they require, each a signature with a key kept
3
+ // for this alone, none by the requester; it is view or a narrow control, never admin, never grant, never an
4
+ // owner power; it lasts no longer than the terms allow; it is written witnessed, into the owner's log; every
5
+ // read under it is logged; the owner is told. The people and the policy are the platform's.
6
+ import { createPublicKey, verify as edVerify } from "node:crypto";
7
+ import { serialize } from "@cruba/crubal";
8
+ import { sha256 } from "@cruba/crubal";
9
+ const durationMs = (d) => {
10
+ const m = d.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?)?$/);
11
+ return m ? (Number(m[1] ?? 0) * 24 * 60 + Number(m[2] ?? 0) * 60 + Number(m[3] ?? 0)) * 60_000 : Number.NaN;
12
+ };
13
+ /** What an approver signs: the edge without its approvals and notice. */
14
+ export function edgeHash(r) {
15
+ const { approvals: _a, notify: _n, ...rest } = r;
16
+ return sha256(serialize(rest));
17
+ }
18
+ /** Check a request against the terms. Returns the reasons it is refused; none means it may be made. */
19
+ export function checkEmergency(r, terms, now) {
20
+ const why = [];
21
+ if (!terms)
22
+ return ["the platform publishes no emergency terms, so it can make no emergency edge"];
23
+ if (r.mode !== "view" && r.mode !== "control")
24
+ why.push("an emergency edge is view or control, never admin");
25
+ if ((r.grants ?? []).some((g) => /^grant\b/.test(g) || g === "grant"))
26
+ why.push("an emergency edge never carries grant");
27
+ if (r.kind !== "legal" && r.kind !== "incident")
28
+ why.push("the kind is legal or incident");
29
+ if (!r.reason || !r.reference)
30
+ why.push("a stated reason and reference are required");
31
+ const approvers = new Set(r.approvals.map((a) => a.by));
32
+ if (approvers.size < terms.approvers)
33
+ why.push(`${terms.approvers} approvers are required, ${approvers.size} approved`);
34
+ if (approvers.has(r.who))
35
+ why.push("the requester may not approve");
36
+ const hash = edgeHash(r);
37
+ for (const a of r.approvals) {
38
+ const k = terms.keys[a.by];
39
+ if (!k) {
40
+ why.push(`${a.by} holds no emergency key`);
41
+ continue;
42
+ }
43
+ try {
44
+ const key = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: k.public }, format: "jwk" });
45
+ if (!edVerify(null, Buffer.from(hash, "hex"), key, Buffer.from(a.sig, "base64url")))
46
+ why.push(`${a.by}'s approval doesn't verify`);
47
+ }
48
+ catch {
49
+ why.push(`${a.by}'s approval doesn't verify`);
50
+ }
51
+ }
52
+ const longest = durationMs(terms.longest);
53
+ if (Number.isNaN(Date.parse(r.until)) || Date.parse(r.until) - Date.parse(now) > longest)
54
+ why.push(`until is later than the terms allow (${terms.longest})`);
55
+ if (Date.parse(r.until) <= Date.parse(now))
56
+ why.push("until is already past");
57
+ return why;
58
+ }
59
+ /** Make the edge in the owner's log: witnessed, by platform, its approvals carried. */
60
+ export async function makeEmergencyEdge(store, r, terms, now) {
61
+ const why = checkEmergency(r, terms, now);
62
+ if (why.length)
63
+ throw new Error(`the emergency edge is refused: ${why.join("; ")}`);
64
+ const edge = {
65
+ edge: r.edge, who: r.who, relation: "emergency", mode: r.mode, on: r.on, grants: r.grants ?? [], until: r.until, by: "platform",
66
+ emergency: { kind: r.kind, reason: r.reason, reference: r.reference, approvals: r.approvals, notify: (r.notify ?? "now"), at: now },
67
+ };
68
+ const out = await store.commitFrom((d) => { d.edges.set(r.edge, edge); return { by: { person: r.who }, out: null }; }, now, { witnessed: true });
69
+ if (!out.entry)
70
+ throw new Error("the edge wrote nothing");
71
+ return { edge, entry: String(out.entry.log) };
72
+ }
73
+ /** Log a read under an emergency edge (14.13): record by record, witnessed. */
74
+ export async function logEmergencyRead(store, who, recordId, edge, now) {
75
+ await store.commitFrom(() => ({ by: { person: who }, extra: [{ read: recordId, edge }], out: null }), now, { witnessed: true });
76
+ }
77
+ /** End an emergency edge: the owner may, unless it cites a legal order; the attempt is recorded either way. */
78
+ export async function endEmergencyEdge(store, edgeId, by, now, isOwner) {
79
+ const state = await store.state();
80
+ const edge = state.edges.get(edgeId);
81
+ if (!edge || edge.relation !== "emergency")
82
+ throw new Error(`no emergency edge ${edgeId}`);
83
+ const legal = edge.emergency?.kind === "legal";
84
+ if (isOwner && legal) {
85
+ await store.commitFrom(() => ({ by: { person: by }, extra: [{ review: { key: "emergency", edge: edgeId, attempted: "end", by, reason: "a legal order's edge is ended by the order" } }], out: null }), now, { witnessed: true });
86
+ return { ended: false, reason: "a legal order's edge is the order's matter; the attempt is recorded" };
87
+ }
88
+ await store.commitFrom((d) => { d.edges.delete(edgeId); return { by: { person: by }, out: null }; }, now, { witnessed: true });
89
+ return { ended: true };
90
+ }
91
+ //# sourceMappingURL=emergency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emergency.js","sourceRoot":"","sources":["../../src/emergency.ts"],"names":[],"mappings":"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"}