@cruba/crubal-host 0.0.0-stage → 0.3.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.
@@ -0,0 +1,308 @@
1
+ import { type SecretStore } from "./secrets.ts";
2
+ import type { Json, JsonObject } from "@cruba/crubal";
3
+ import { type Contract, type Keys } from "@cruba/crubal";
4
+ import { type Adapter, PgBorder } from "@cruba/crubal-postgres";
5
+ import { type Pool } from "@cruba/crubal-postgres";
6
+ import { PgStore } from "@cruba/crubal-postgres";
7
+ import { type Connection, Credentials, type OAuthProvider } from "./credentials.ts";
8
+ import type { EmergencyTerms } from "./emergency.ts";
9
+ /**
10
+ * Who is calling, as the host proved it: an owner's caller, and how (`key`: an API key the host issued, a
11
+ * system; `identity`: a token the identity provider issued, a person; `signature`: another owner's domain
12
+ * key). A person proven by identity is vouched for across every owner this host holds (14.8).
13
+ */
14
+ export type Caller = {
15
+ owner: string;
16
+ caller: string;
17
+ via?: "key" | "identity" | "signature";
18
+ };
19
+ /** A system acting for a hosted person (14.9): what the person allowed, as the host keeps it. */
20
+ export type ActingFor = {
21
+ id: number;
22
+ systemOwner: string;
23
+ system: string;
24
+ person: string;
25
+ items: string[];
26
+ until?: string;
27
+ gated: boolean;
28
+ created: string;
29
+ revoked?: string;
30
+ };
31
+ export type HostOptions = {
32
+ /** The host's own domain, for its well-known file. */
33
+ domain: string;
34
+ /** A key the loops' routes (`/project`, `/tick`) require. */
35
+ hostKey?: string;
36
+ /** Verifiers for bearer tokens that aren't API keys (an OIDC-shaped adapter); each answers a caller or null. */
37
+ verifiers?: ((token: string) => Promise<Caller | null> | Caller | null)[];
38
+ /** Secrets by (owner, name); the environment (`CRUBAL_SECRET_<OWNER>_<NAME>`) otherwise. */
39
+ secrets?: (owner: string, name: string) => string | undefined;
40
+ /** Where credentials' secrets live beyond the connected table: a vault, a file; the environment otherwise. */
41
+ secretStore?: SecretStore;
42
+ /** Request bodies larger than this are refused with 413 (default 1 MiB). */
43
+ bodyLimit?: number;
44
+ /** Origins allowed to call from a browser; none otherwise. */
45
+ cors?: string[];
46
+ /** One structured line per request; the console as JSON otherwise, nothing when `false`. */
47
+ log?: ((line: JsonObject) => void) | false;
48
+ /** Adapters by contract name, beyond the built-in `http` one. */
49
+ adapters?: {
50
+ [contract: string]: Adapter;
51
+ };
52
+ /** A publisher's keys from its well-known file; `fetch` of `https://<domain>/.well-known/crubal.json` otherwise. */
53
+ resolveKeys?: (domain: string) => Promise<Keys>;
54
+ /** The clock (6.3). */
55
+ now?: () => string;
56
+ /** `write` publishes views right after each commit; else the loops do. */
57
+ fresh?: "write" | "asked";
58
+ /** Install unsigned bundles (development only). */
59
+ allowUnsigned?: boolean;
60
+ /** Whether a store may load an owner's whole state on a request path; the default refuses (brief 18, the scale rule). */
61
+ wholeState?: "allow" | "refuse";
62
+ /** How many owners' stores the host keeps at once (least recently used go); 256 by default. */
63
+ storeCache?: number;
64
+ /** Emergency terms (14.13), published in the well-known file, when the platform has them. */
65
+ emergency?: EmergencyTerms;
66
+ /** OAuth providers by credential name, for the connect flow and refresh. */
67
+ oauth?: {
68
+ [name: string]: OAuthProvider;
69
+ };
70
+ /** The owner's notice of an emergency edge or a dead run; the console otherwise. */
71
+ notify?: (owner: string, notice: JsonObject) => void | Promise<void>;
72
+ /** Another owner's host by domain: its base URL; `https://<domain>` otherwise. */
73
+ hostOf?: (domain: string) => string;
74
+ /** A JSON document by URL (a well-known file, a bundle); `fetch` otherwise. */
75
+ fetchJson?: (url: string) => Promise<JsonObject>;
76
+ /** The public base URL this host serves its well-known file and published packages from; `https://<domain>` otherwise. */
77
+ publicBase?: string;
78
+ /** The id prefix an arrival takes per level (`organization: "Z"`, `location: "K"`); the level's first letter otherwise. */
79
+ idPrefixes?: {
80
+ [level: string]: string;
81
+ };
82
+ /** The key connection material is sealed with at rest; `CRUBAL_SECRETS_KEY` in the environment otherwise. Without one no secret is stored. */
83
+ secretsKey?: string;
84
+ /** The id of an owner's root record, which connections sit within and whose admin manages them; `ROOT` otherwise. */
85
+ rootRecord?: string;
86
+ };
87
+ export declare const hashKey: (key: string) => string;
88
+ export declare class Host {
89
+ readonly pool: Pool;
90
+ readonly o: HostOptions;
91
+ readonly credentials: Credentials;
92
+ constructor(pool: Pool, o: HostOptions);
93
+ now(): string;
94
+ /** The store's tables and the host's. */
95
+ migrate(): Promise<void>;
96
+ /**
97
+ * Connect a service for an owner: the material sealed in the credentials store, and a `system` record of
98
+ * the fact within the owner's root record, written as the host's action naming the person. The person,
99
+ * when given, must hold admin on the root or the `connections` item; the owner's own key needs none.
100
+ */
101
+ connect(owner: string, name: string, c: {
102
+ scheme: string;
103
+ material: JsonObject;
104
+ scopes?: string[];
105
+ expires?: string;
106
+ person?: string;
107
+ values?: JsonObject;
108
+ }): Promise<{
109
+ connection: Connection;
110
+ record: string;
111
+ }>;
112
+ /** Revoke a connection: the material gone, the record marked revoked. */
113
+ disconnect(owner: string, name: string, person?: string): Promise<{
114
+ record?: string;
115
+ }>;
116
+ /** Is a service connected for the owner: material held and not expired, and the record not revoked? */
117
+ connected(owner: string, name: string): Promise<boolean>;
118
+ private requireConnections;
119
+ private systemFor;
120
+ /** The system record for a service, within ROOT, made or updated; the host's action names the person. */
121
+ private writeSystem;
122
+ /** Make an owner: a domain or a hosted person, with an Ed25519 key the host signs its chain with. */
123
+ createOwner(owner: string): Promise<{
124
+ owner: string;
125
+ public: string;
126
+ }>;
127
+ /**
128
+ * Rotate an owner's key (8.5, 11.7): a new key signs from now; the well-known file lists the old with
129
+ * `until` and the new with `from`, so the chain verifies across the rotation.
130
+ */
131
+ rotateKey(owner: string, now?: string): Promise<{
132
+ owner: string;
133
+ public: string;
134
+ retired: string;
135
+ }>;
136
+ /** The signer for a hosted person (14.1): their key, made on first use. */
137
+ private hostedSigner;
138
+ owners(): Promise<{
139
+ owner: string;
140
+ public: string;
141
+ }[]>;
142
+ /** The owners a page at a time, for the loops: a host of thousands never holds them all at once. */
143
+ private eachOwner;
144
+ private keyOf;
145
+ /** Issue an API key for a caller of an owner; the raw key is returned once and only its hash is kept. */
146
+ issueKey(owner: string, caller: string, label?: string): Promise<string>;
147
+ revokeKey(raw: string): Promise<void>;
148
+ /** Link an identity provider's subject to a caller, for the OIDC verifier. */
149
+ linkIdentity(provider: string, subject: string, owner: string, caller: string): Promise<void>;
150
+ /** Remove an identity link (a person deleting themselves): the subject signs in as nobody from now on. */
151
+ unlinkIdentity(provider: string, subject: string): Promise<boolean>;
152
+ /** Every identity link that names a caller, for export and delete. */
153
+ identitiesOf(owner: string, caller: string): Promise<{
154
+ provider: string;
155
+ subject: string;
156
+ }[]>;
157
+ identity(provider: string, subject: string): Promise<Caller | null>;
158
+ /**
159
+ * Who is calling when the request is signed by another owner (RFC 9421 with its domain key): the signer's
160
+ * domain, as a caller of the owner addressed. The signer's key comes from its well-known file.
161
+ */
162
+ whoSigned(owner: string, req: {
163
+ method: string;
164
+ url: string;
165
+ body?: string;
166
+ headers: {
167
+ [h: string]: string | string[] | undefined;
168
+ };
169
+ }): Promise<Caller | null>;
170
+ /** Who is calling: an API key, a token a verifier knows, or nobody. The store sees only the caller ID. */
171
+ whoIs(authorization: string | undefined): Promise<Caller | null>;
172
+ /** Does this host hold the owner (a domain, or a hosted person `domain:id`)? */
173
+ hosts(owner: string): Promise<boolean>;
174
+ /**
175
+ * The caller as an owner on this host sees them (14.8): their own ID at the owner that hosts them, or
176
+ * themselves as that owner when they address their own chain; elsewhere on the host, a person proven by
177
+ * identity is `person:<domain:id>`, vouched for without a signature; a system or a stranger is nobody.
178
+ */
179
+ callerFor(owner: string, who: Caller | null): Promise<string | undefined>;
180
+ /** A person lets a system act for them: narrowed to items, until a moment, gated until they lift it. */
181
+ allowActing(person: string, systemOwner: string, system: string, o?: {
182
+ items?: string[];
183
+ until?: string;
184
+ gated?: boolean;
185
+ }): Promise<ActingFor>;
186
+ /** The person revokes it: nothing is deleted, the row keeps when. */
187
+ revokeActing(person: string, id: number): Promise<boolean>;
188
+ actingFor(person: string): Promise<ActingFor[]>;
189
+ /** The live permission for this system to act for this person now, or none. */
190
+ mayActFor(who: Caller, person: string, now?: string): Promise<ActingFor | null>;
191
+ /**
192
+ * Every live edge a caller holds across every owner on this host, grouped by owner: who they are to
193
+ * each. A hosted person is named by their bare ID at home and `person:<domain:id>` elsewhere.
194
+ */
195
+ whoami(who: Caller, now?: string): Promise<JsonObject>;
196
+ /**
197
+ * The rows the caller stands in a relation to, across every owner on this host (14.10, 12.6): the index
198
+ * rows whose record, or an ancestor, is a live edge of theirs with that relation. The feed's primitive.
199
+ */
200
+ rowsFor(who: Caller, relation: string, o?: {
201
+ view?: string;
202
+ limit?: number;
203
+ now?: string;
204
+ }): Promise<JsonObject[]>;
205
+ /** A credential's secret: connected under the name (refreshed when OAuth and expired), a configured source, or the environment. */
206
+ secret(owner: string, name: string): Promise<string | undefined>;
207
+ /** Health (docs/design/completion.md O): the database reachable, and every owner's chain head. */
208
+ health(): Promise<JsonObject>;
209
+ /** An owner's store, signing with the owner's key when the host holds one. */
210
+ /** One store per owner for the life of the host, so its definitions and state caches outlive a call (G6). */
211
+ storeFor(owner: string): Promise<PgStore>;
212
+ private readonly stores;
213
+ /** Drop an owner's kept store: after a key rotation (its signer changes) or a rewrite of its history. */
214
+ forgetStore(owner: string): void;
215
+ /**
216
+ * The built-in `http` adapter: a contract with the host's part `host:url` is POSTed its payload as JSON,
217
+ * with the credential the contract names resolved to a secret and sent as its `auth` scheme says.
218
+ */
219
+ httpAdapter(owner: string): Adapter;
220
+ /** Another owner's host, by domain. */
221
+ hostOf(domain: string): string;
222
+ /** A request to another owner's host, signed with this owner's domain key (RFC 9421). */
223
+ signedFetch(owner: string, method: string, url: string, body?: string): Promise<Response>;
224
+ /** May this caller read an offer (7.8 `to`)? Public to anyone; link by token; followers and named parties by edge or signature. */
225
+ mayTake(owner: string, c: Contract, callerId: string | undefined, link: string | undefined): Promise<boolean>;
226
+ /** Publish an offer by link (7.8): the token, minted once and kept across republishing. */
227
+ offerLink(owner: string, contract: string): Promise<string>;
228
+ /** Unpublish a link offer: the token stops working, nothing is deleted. */
229
+ unpublishOffer(owner: string, contract: string): Promise<void>;
230
+ /**
231
+ * A take across hosts (7.8, 8.5.1): fetch the presenter's offer with a signed request, verify the wrapper
232
+ * against the presenter's keys, then take each offered record into ours: the one bound to it, or a new
233
+ * record bound from now on. Each take is one entry naming the presenter's.
234
+ */
235
+ take(owner: string, takeName: string, at?: string): Promise<JsonObject>;
236
+ /**
237
+ * Push streams (12.6.2): every stream contract with `when: entry` sends its entries since the cursor to
238
+ * the party it names (its adapter, else a signed POST to that host's inbox), and the acknowledgement
239
+ * moves the cursor.
240
+ */
241
+ push(now?: string, owner?: string): Promise<JsonObject[]>;
242
+ /** Taken values past their terms (7.8), for every owner or one. */
243
+ expire(now?: string, owner?: string): Promise<JsonObject[]>;
244
+ /** An owner's border over its own definitions, with the adapters. */
245
+ borderFor(owner: string, store?: PgStore): Promise<PgBorder>;
246
+ resolveKeys(domain: string): Promise<Keys>;
247
+ /**
248
+ * Install into one owner what another owner on this host already holds (brief 1: a person's chain takes
249
+ * the person package from the platform's own log): the bundle and what it extends, in order, each skipped
250
+ * when the target already has that version. The source's bundles were verified when it installed them.
251
+ */
252
+ installFromOwner(target: string, source: string, pkg: string, version?: string, at?: string): Promise<JsonObject[]>;
253
+ /** Install a published version for an owner: the publisher's keys from its well-known file, then the store. */
254
+ install(owner: string, bundle: JsonObject): Promise<JsonObject>;
255
+ /** A JSON document by URL. */
256
+ fetchJson(url: string): Promise<JsonObject>;
257
+ /** A publisher's well-known file (11.7), by its domain. */
258
+ wellKnownOf(domain: string): Promise<JsonObject>;
259
+ /**
260
+ * Install a package by name from its publisher's well-known file (11.7, release.md): the version asked
261
+ * for or the latest, its bundle fetched from where the file says, what it extends installed first
262
+ * (from their own publishers), every bundle verified against its publisher's published keys.
263
+ */
264
+ installFrom(owner: string, pkg: string, version?: string, seen?: Set<string>): Promise<JsonObject[]>;
265
+ /** Where this host serves what it publishes: the public base URL (`https://<domain>` otherwise). */
266
+ publicBase(): string;
267
+ /** The URL of a published version's bundle; the text sits beside it as package.crubal. */
268
+ bundleUrl(owner: string, pkg: string, version: string): string;
269
+ /**
270
+ * Publish a package version for an owner this host holds the key of: the text becomes a bundle signed
271
+ * with that key, versioned against the owner's last published version of the package (so unchanged
272
+ * members keep their versions and a breaking change needs its recipe), kept here and listed in the
273
+ * well-known file. A version already published is returned as it is; a published version never changes.
274
+ */
275
+ publishPackage(owner: string, source: string, o?: {
276
+ date?: string;
277
+ records?: Json[];
278
+ }): Promise<{
279
+ bundle: JsonObject;
280
+ changes: {
281
+ member: string;
282
+ kind: string;
283
+ }[];
284
+ already: boolean;
285
+ }>;
286
+ /** One published version: its signed bundle and its text, or null. */
287
+ publishedBundle(owner: string, pkg: string, version: string): Promise<{
288
+ bundle: JsonObject;
289
+ text: string;
290
+ } | null>;
291
+ /** What the well-known file lists: every published package with its versions and where each bundle is. */
292
+ publishedPackages(owner?: string): Promise<JsonObject[]>;
293
+ wellKnown(): Promise<JsonObject>;
294
+ notice(owner: string, notice: JsonObject): Promise<void>;
295
+ /** Publish every owner's stale rows. */
296
+ project(owner?: string, now?: string): Promise<{
297
+ [owner: string]: number;
298
+ }>;
299
+ /** Drain pending instructions (completion.md P) for every owner or one, at a rate per owner. */
300
+ drain(rate?: number, owner?: string, now?: string): Promise<JsonObject[]>;
301
+ /**
302
+ * Try failed runs again when their contract's retry interval has passed (7.5): a run still open after a
303
+ * failure, whose last attempt is `every` behind `now`. A run dead after its last attempt is noticed.
304
+ */
305
+ retry(now?: string, owner?: string): Promise<JsonObject[]>;
306
+ /** Run every scheduled contract's due ticks, for one owner or all. */
307
+ tick(now?: string, owner?: string): Promise<JsonObject[]>;
308
+ }