@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,128 @@
1
+ -- The host's own schema (docs/design/host.md): owners and their keys, identities and API keys. Nothing of
2
+ -- an owner's data lives here; that is the store's. Private keys are here only in the reference host's
3
+ -- simplest form; a platform keeps them in a key store or a KMS and leaves `private` empty.
4
+
5
+ create table if not exists host_owners (
6
+ owner text primary key,
7
+ public text not null,
8
+ private text,
9
+ created text not null
10
+ );
11
+
12
+ create table if not exists host_api_keys (
13
+ key_hash text primary key,
14
+ owner text not null,
15
+ caller text not null,
16
+ label text,
17
+ created text not null
18
+ );
19
+
20
+ -- The reference index (docs/design/index-and-forms.md): the current rows per view and audience, applied from
21
+ -- the projection stream, and where it is in each stream. Searchable over the row's text slots.
22
+ create table if not exists index_rows (
23
+ owner text not null,
24
+ view text not null,
25
+ version text not null,
26
+ audience text not null,
27
+ record text not null,
28
+ row jsonb not null,
29
+ buckets jsonb not null default '{}',
30
+ marks jsonb,
31
+ ancestors text[] not null default '{}',
32
+ sort jsonb not null default '[]',
33
+ pin integer,
34
+ seq bigint not null,
35
+ search tsvector,
36
+ primary key (owner, view, version, audience, record)
37
+ );
38
+ create index if not exists index_rows_search on index_rows using gin (search);
39
+ -- Picks are containments on the buckets document; the first pages of a view in its stored order come off this index.
40
+ create index if not exists index_rows_buckets on index_rows using gin (buckets);
41
+ create index if not exists index_rows_order on index_rows (owner, view, version, audience, pin, (sort ->> 0), (sort ->> 1), (sort ->> 2), record);
42
+ -- The platform index (14.8, 14.10): rows reached by an edge on an ancestor, across owners.
43
+ create index if not exists index_rows_ancestors on index_rows using gin (ancestors);
44
+ create index if not exists index_rows_record on index_rows (record);
45
+
46
+ create table if not exists index_cursors (
47
+ owner text not null,
48
+ view text not null,
49
+ version text not null,
50
+ audience text not null,
51
+ seq bigint not null default 0,
52
+ primary key (owner, view, version, audience)
53
+ );
54
+
55
+ -- Credentials behind a contract's name (docs/design/host.md, "Outbound"). The reference host keeps the
56
+ -- material here; a platform keeps it in a vault behind the same interface and leaves `material` empty.
57
+ create table if not exists host_credentials (
58
+ owner text not null,
59
+ name text not null,
60
+ scheme text not null,
61
+ material jsonb not null,
62
+ scopes text[] not null default '{}',
63
+ expires text,
64
+ connected text not null,
65
+ primary key (owner, name)
66
+ );
67
+
68
+ create table if not exists host_identities (
69
+ provider text not null,
70
+ subject text not null,
71
+ owner text not null,
72
+ caller text not null,
73
+ primary key (provider, subject)
74
+ );
75
+
76
+ -- Link offers (7.8 `to: link`): one unguessable token per offer; unpublishing revokes, republishing keeps it.
77
+ create table if not exists host_offer_links (
78
+ owner text not null,
79
+ contract text not null,
80
+ token text not null,
81
+ revoked boolean not null default false,
82
+ primary key (owner, contract)
83
+ );
84
+
85
+ -- Retired signing keys (11.7): each with the span it signed in, so a rotated key still verifies.
86
+ create table if not exists host_keys (
87
+ owner text not null,
88
+ public text not null,
89
+ "from" text,
90
+ until text not null,
91
+ primary key (owner, public)
92
+ );
93
+
94
+ -- A system acting for a hosted person (14.9): what the person allowed, narrowed to items, until when, gated
95
+ -- until they lift it. Revoking keeps the row with when. Identity-adjacent, so the host's, like API keys.
96
+ create table if not exists host_acting_for (
97
+ id bigint generated always as identity primary key,
98
+ system_owner text not null,
99
+ system text not null,
100
+ person text not null,
101
+ items text[] not null default '{}',
102
+ until text,
103
+ gated boolean not null default true,
104
+ created text not null,
105
+ revoked text
106
+ );
107
+ create index if not exists host_acting_for_person on host_acting_for (person);
108
+
109
+ -- Packages an owner publishes from this host (11.5, 11.7): each version's signed bundle and its text, listed in
110
+ -- the well-known file and served at /packages/<owner>/<name>/<version>/. A published version never changes.
111
+ create table if not exists host_published (
112
+ owner text not null,
113
+ package text not null,
114
+ version text not null,
115
+ hash text not null,
116
+ json jsonb not null,
117
+ text text not null,
118
+ published text not null,
119
+ primary key (owner, package, version)
120
+ );
121
+
122
+ -- When each view with a delay (12.6 fresh: 5m) was last published by the loop.
123
+ create table if not exists host_publish (
124
+ owner text not null,
125
+ view text not null,
126
+ at text not null,
127
+ primary key (owner, view)
128
+ );
@@ -0,0 +1,21 @@
1
+ export type SecretStore = {
2
+ get(owner: string, name: string): Promise<string | undefined>;
3
+ /** Optional: a store that can be written to (the table can; the environment can't). */
4
+ set?(owner: string, name: string, value: string): Promise<void>;
5
+ };
6
+ /** Secrets from the environment: `CRUBAL_SECRET_<OWNER>_<NAME>`. */
7
+ export declare class EnvSecrets implements SecretStore {
8
+ get(owner: string, name: string): Promise<string | undefined>;
9
+ }
10
+ /** Secrets from a function the host was configured with (a vault client, a file). */
11
+ export declare class FunctionSecrets implements SecretStore {
12
+ private readonly fn;
13
+ constructor(fn: (owner: string, name: string) => string | undefined | Promise<string | undefined>);
14
+ get(owner: string, name: string): Promise<string | undefined>;
15
+ }
16
+ /** Several stores asked in order; the first answer wins. */
17
+ export declare class ChainedSecrets implements SecretStore {
18
+ private readonly stores;
19
+ constructor(stores: SecretStore[]);
20
+ get(owner: string, name: string): Promise<string | undefined>;
21
+ }
@@ -0,0 +1,36 @@
1
+ // secrets.ts: where credentials' secrets live (docs/design/host.md "What is kept where"). The host asks a
2
+ // SecretStore by (owner, name); the environment serves one owner, the credentials table serves many, and a
3
+ // vault is another implementation of the same two calls. Adapters never see names, only the material.
4
+ const envName = (s) => s.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
5
+ /** Secrets from the environment: `CRUBAL_SECRET_<OWNER>_<NAME>`. */
6
+ export class EnvSecrets {
7
+ async get(owner, name) {
8
+ return process.env[`CRUBAL_SECRET_${envName(owner)}_${envName(name)}`];
9
+ }
10
+ }
11
+ /** Secrets from a function the host was configured with (a vault client, a file). */
12
+ export class FunctionSecrets {
13
+ fn;
14
+ constructor(fn) {
15
+ this.fn = fn;
16
+ }
17
+ async get(owner, name) {
18
+ return this.fn(owner, name);
19
+ }
20
+ }
21
+ /** Several stores asked in order; the first answer wins. */
22
+ export class ChainedSecrets {
23
+ stores;
24
+ constructor(stores) {
25
+ this.stores = stores;
26
+ }
27
+ async get(owner, name) {
28
+ for (const s of this.stores) {
29
+ const v = await s.get(owner, name);
30
+ if (v !== undefined)
31
+ return v;
32
+ }
33
+ return undefined;
34
+ }
35
+ }
36
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/secrets.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,2GAA2G;AAC3G,sGAAsG;AAQtG,MAAM,OAAO,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;AAE7E,oEAAoE;AACpE,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,GAAG,CAAC,KAAa,EAAE,IAAY;QACnC,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;CACF;AAED,qFAAqF;AACrF,MAAM,OAAO,eAAe;IACT,EAAE,CAAoF;IACvG,YAAY,EAAqF;QAC/F,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAa,EAAE,IAAY;QACnC,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;CACF;AAED,4DAA4D;AAC5D,MAAM,OAAO,cAAc;IACR,MAAM,CAAgB;IACvC,YAAY,MAAqB;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAa,EAAE,IAAY;QACnC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,KAAK,SAAS;gBAAE,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,41 @@
1
+ import { type Server } from "node:http";
2
+ import type { Host } from "./host.ts";
3
+ /**
4
+ * A request as the host sees it, whatever carried it: the node server below, or a route in an app that runs
5
+ * the host in process. `url` is the path and query as node gives it; the origin a signature covers comes
6
+ * from the `host` and `x-forwarded-proto` headers; the body is already read, once, so a digest can cover it.
7
+ */
8
+ export type HostRequest = {
9
+ method: string;
10
+ url: string;
11
+ headers: {
12
+ [h: string]: string | string[] | undefined;
13
+ };
14
+ body: string;
15
+ };
16
+ /** What a request's log line and CORS need, set as the request is understood. */
17
+ export type Seen = {
18
+ owner?: string;
19
+ caller?: string;
20
+ route?: string;
21
+ };
22
+ export type HostReply = {
23
+ status: number;
24
+ headers: {
25
+ [h: string]: string;
26
+ };
27
+ body: string;
28
+ seen: Seen;
29
+ };
30
+ /**
31
+ * Answer one request. Every route is here, owner-scoped, the same for every owner; errors become a JSON
32
+ * body with their status. Transports call this: `serve` for node, an app's route for in-process hosting.
33
+ */
34
+ export declare function handle(host: Host, req: HostRequest): Promise<HostReply>;
35
+ /** One structured line per request, when it has been answered. */
36
+ export declare function logLine(host: Host, req: {
37
+ method: string;
38
+ url: string;
39
+ }, reply: HostReply, started: number): void;
40
+ /** The node server: reads each request, hands it to `handle`, writes the reply, logs the line. */
41
+ export declare function serve(host: Host): Server;