@formancy/data-server 0.0.0-stage → 0.1.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/LICENSE.md +156 -0
- package/NOTICE +9 -0
- package/README.md +619 -2
- package/dist/drivers-DtJsZ_HW.mjs +3328 -0
- package/dist/drivers-DtJsZ_HW.mjs.map +1 -0
- package/dist/identity-keys-cli.d.mts +1 -0
- package/dist/identity-keys-cli.mjs +69 -0
- package/dist/identity-keys-cli.mjs.map +1 -0
- package/dist/identity-settings-BlQY9gHy.mjs +165 -0
- package/dist/identity-settings-BlQY9gHy.mjs.map +1 -0
- package/dist/index.d.mts +733 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/main.d.mts +1 -0
- package/dist/main.mjs +189 -0
- package/dist/main.mjs.map +1 -0
- package/dist/secrets-DhKHG-Y9.mjs +689 -0
- package/dist/secrets-DhKHG-Y9.mjs.map +1 -0
- package/package.json +52 -3
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import { DatabaseAdapter, DatabaseKind, DiscoveryScope, DriftReport, FormBindings, FormPolicy, GenerationNote, GenerationRequest, LookupAdapter, MetadataSnapshot, PresentationConflict, PresentationOverrides, ReassignedKey, RecordAdapter } from "@formancy/data-core";
|
|
2
|
+
import { FastifyBaseLogger, FastifyInstance, FastifyRequest } from "fastify";
|
|
3
|
+
import { FormSchema } from "@formancy/spec";
|
|
4
|
+
import "jose";
|
|
5
|
+
//#region src/config-store.d.ts
|
|
6
|
+
type PublishOutcome = {
|
|
7
|
+
ok: true;
|
|
8
|
+
version: number;
|
|
9
|
+
} |
|
|
10
|
+
/** Somebody published first. `current` is what they published, so the caller can rebase and retry. */
|
|
11
|
+
{
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: 'conflict';
|
|
14
|
+
current: number | null;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Where published form bundles live: outside every customer's business tables.
|
|
18
|
+
*
|
|
19
|
+
* A port because a second implementation is already planned — a shared store
|
|
20
|
+
* with compare-and-swap for more than one server instance (plan section 5) —
|
|
21
|
+
* and `config-store.test.ts` is written against this interface so that store
|
|
22
|
+
* can run the same cases. Today there is one, on a directory.
|
|
23
|
+
*
|
|
24
|
+
* Versions are immutable once published, as formancy's are
|
|
25
|
+
* (formancy.ai 0025): publishing never edits, it adds the next number.
|
|
26
|
+
*/
|
|
27
|
+
interface ConfigurationStore {
|
|
28
|
+
/** The newest published version, or `null` before the first. */
|
|
29
|
+
latest(id: string): Promise<number | null>;
|
|
30
|
+
/** Every published version, ascending; `[]` for an id never published (0030). */
|
|
31
|
+
versions(id: string): Promise<number[]>;
|
|
32
|
+
/**
|
|
33
|
+
* One published version, or `undefined` if it does not exist. A version
|
|
34
|
+
* that exists and does not parse -- edited by hand, or damaged -- throws
|
|
35
|
+
* `UnparsableVersionError`, so a caller can tell it from a read that
|
|
36
|
+
* failed.
|
|
37
|
+
*/
|
|
38
|
+
read(id: string, version: number): Promise<unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Publish the next version, only if the newest is still `expectedBase`.
|
|
41
|
+
* Two administrators publishing from the same base: one wins, the other is
|
|
42
|
+
* told, and nothing is overwritten.
|
|
43
|
+
*/
|
|
44
|
+
publish(id: string, expectedBase: number | null, value: unknown): Promise<PublishOutcome>;
|
|
45
|
+
/** Every id with at least one version, in codepoint order. */
|
|
46
|
+
list(): Promise<string[]>;
|
|
47
|
+
}
|
|
48
|
+
/** What `read` throws for a version that exists and does not parse. Its message names the file. */
|
|
49
|
+
export declare class UnparsableVersionError extends Error {
|
|
50
|
+
name: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A configuration store on a directory — a persistent volume in a container.
|
|
54
|
+
*
|
|
55
|
+
* Layout: `<root>/<id>/<version>.json`, pretty-printed, one file per version,
|
|
56
|
+
* so an export is a copy of the directory and a review is a diff.
|
|
57
|
+
*
|
|
58
|
+
* **Compare-and-swap without a lock.** A version is written to a temporary file,
|
|
59
|
+
* flushed, and then hard-linked to its final name. `link` fails if the name
|
|
60
|
+
* exists, atomically, so of two writers that both read version 3 as the newest,
|
|
61
|
+
* exactly one creates 4.json and the other gets a conflict. Nobody ever sees a
|
|
62
|
+
* half-written version, because the final name only appears once the bytes are
|
|
63
|
+
* on disk. A crash leaves a temporary file that nothing reads.
|
|
64
|
+
*
|
|
65
|
+
* On a local filesystem — ext4, xfs, NTFS — `link` is atomic. Over NFS it is
|
|
66
|
+
* atomic on the server but the error a client sees after a retransmission can
|
|
67
|
+
* be wrong, which is why the plan limits this store to a single-instance pilot.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createFileConfigurationStore(root: string): ConfigurationStore;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/secrets.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* Where a secret comes from: injected, so a test can supply an environment and
|
|
74
|
+
* a file system without touching the process's own.
|
|
75
|
+
*/
|
|
76
|
+
interface SecretSource {
|
|
77
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
78
|
+
readFile(path: string): Promise<string>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The value behind a secret reference.
|
|
82
|
+
*
|
|
83
|
+
* Configuration names a secret, never contains one (plan section 3: "a
|
|
84
|
+
* server-side secret reference"). Two forms: `env:NAME`, read from the
|
|
85
|
+
* environment, and `file:/absolute/path`, read from a file — which is how
|
|
86
|
+
* Docker and Kubernetes hand secrets to a container.
|
|
87
|
+
*
|
|
88
|
+
* **No error message ever contains the reference's value**, only its form. The
|
|
89
|
+
* commonest mistake is pasting the secret itself where its reference belongs,
|
|
90
|
+
* and an error that echoed the input would print the secret into a log.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveSecret(reference: string, source?: SecretSource): Promise<string>;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/connections.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* One database the deployment allows forms to bind to, as the operator wrote
|
|
97
|
+
* it. The password is a secret reference, never a password (0014): this file is
|
|
98
|
+
* reviewable configuration, and a secret in it would be a secret in review.
|
|
99
|
+
*/
|
|
100
|
+
interface ConnectionConfig {
|
|
101
|
+
/** The name forms use. Lower case, as configuration ids are (0013). */
|
|
102
|
+
id: string;
|
|
103
|
+
kind: DatabaseKind;
|
|
104
|
+
host: string;
|
|
105
|
+
port: number;
|
|
106
|
+
database: string;
|
|
107
|
+
user: string;
|
|
108
|
+
/** `env:NAME` or `file:/absolute/path`. */
|
|
109
|
+
password: string;
|
|
110
|
+
/** The schemas an administrator approved. Discovery reads nothing outside them. */
|
|
111
|
+
schemas: string[];
|
|
112
|
+
/**
|
|
113
|
+
* TLS. On by default, and certificate checking with it: a deployment that
|
|
114
|
+
* turns either off says so here, in a file somebody reviews.
|
|
115
|
+
*/
|
|
116
|
+
tls?: {
|
|
117
|
+
enabled?: boolean;
|
|
118
|
+
trustServerCertificate?: boolean;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** What the server holds for one open connection: the three ports. */
|
|
122
|
+
interface OpenConnection {
|
|
123
|
+
adapter: DatabaseAdapter;
|
|
124
|
+
lookups: LookupAdapter;
|
|
125
|
+
records: RecordAdapter;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Opens a connection of one kind. Supplied by the composition root, which
|
|
129
|
+
* imports the drivers; nothing here does, so the registry is tested with a
|
|
130
|
+
* fake and the server's core depends on no driver.
|
|
131
|
+
*/
|
|
132
|
+
type ConnectionFactory = (config: ConnectionConfig, password: string) => Promise<OpenConnection>;
|
|
133
|
+
/**
|
|
134
|
+
* The allowlist, checked: every problem at once, so an operator fixes the file
|
|
135
|
+
* in one pass rather than one restart per mistake.
|
|
136
|
+
*/
|
|
137
|
+
export declare function parseConnections(document: unknown): {
|
|
138
|
+
ok: true;
|
|
139
|
+
connections: ConnectionConfig[];
|
|
140
|
+
} | {
|
|
141
|
+
ok: false;
|
|
142
|
+
problems: string[];
|
|
143
|
+
};
|
|
144
|
+
interface ConnectionRegistry {
|
|
145
|
+
/** The ids a form may bind to. */
|
|
146
|
+
ids(): string[];
|
|
147
|
+
/** The approved scope of one connection, or `undefined` if the deployment does not allow it. */
|
|
148
|
+
scope(id: string): DiscoveryScope | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* The connection's ports, opened on first use and shared after. A connection
|
|
151
|
+
* that failed to open is tried again on the next call, not cached as broken:
|
|
152
|
+
* a database restarting is not a reason to restart the server.
|
|
153
|
+
*/
|
|
154
|
+
open(id: string): Promise<OpenConnection | undefined>;
|
|
155
|
+
/** Closes every open connection. Safe to call twice. */
|
|
156
|
+
close(): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The allowlisted connections, opened lazily through the composition root's
|
|
160
|
+
* factories. A form names a connection; this is the only place that name
|
|
161
|
+
* becomes a database, so a form cannot reach a database the operator did not
|
|
162
|
+
* list.
|
|
163
|
+
*/
|
|
164
|
+
export declare function createConnectionRegistry(connections: readonly ConnectionConfig[], factories: Readonly<Partial<Record<DatabaseKind, ConnectionFactory>>>, secrets?: SecretSource): ConnectionRegistry;
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/identity-keys.d.ts
|
|
167
|
+
/**
|
|
168
|
+
* The keys a host identity token is verified with (0014, 0059): one shared
|
|
169
|
+
* secret, one public key, or a JSON Web Key Set read from a file, each of its
|
|
170
|
+
* keys selected by the kid a token names.
|
|
171
|
+
*
|
|
172
|
+
* A `KeySet` is opaque. Its constructor refuses every caller but the
|
|
173
|
+
* builders below, which check what they are given, so a set the verifier
|
|
174
|
+
* meets is always one that `readKeySet`, `KeySet.fromSecret` or
|
|
175
|
+
* `KeySet.fromPem` made: one algorithm, public keys or long secrets, every kid
|
|
176
|
+
* distinct.
|
|
177
|
+
*
|
|
178
|
+
* Nothing a sentence here says comes from the file. JSON.parse's own message
|
|
179
|
+
* quotes the text around the error, and the text may be a secret, so a file
|
|
180
|
+
* that does not parse gets one fixed sentence; every other sentence names a
|
|
181
|
+
* key by its kid, when that is in the alphabet below and could not be a
|
|
182
|
+
* secret, or by its place in the file, and names a member, never its value.
|
|
183
|
+
*
|
|
184
|
+
* The class, its prototype and every set are frozen, and a set is verified
|
|
185
|
+
* with only through `verifyWithKeySet`, which index.ts does not export: code
|
|
186
|
+
* that reaches the class can replace none of its methods, and no caller can
|
|
187
|
+
* verify under a set with looser options than identity.ts passes.
|
|
188
|
+
*/
|
|
189
|
+
declare const KEY_SET_ALGORITHMS: readonly ["RS256", "ES256", "EdDSA", "HS256"];
|
|
190
|
+
type KeySetAlgorithm = (typeof KEY_SET_ALGORITHMS)[number];
|
|
191
|
+
/** The member that left a key out of force. Never its value. */
|
|
192
|
+
type LeftOutBecause = 'kty' | 'crv' | 'use' | 'key_ops' | 'alg';
|
|
193
|
+
/** A key the file holds that cannot verify the pinned algorithm: by its kid, or by its place in the file (`#3`). */
|
|
194
|
+
interface LeftOutKey {
|
|
195
|
+
readonly key: string;
|
|
196
|
+
readonly because: LeftOutBecause;
|
|
197
|
+
}
|
|
198
|
+
/** What changed between two sets, by kid; `replaced` is a kid whose key is not the same key. */
|
|
199
|
+
interface KeySetChange {
|
|
200
|
+
readonly added: readonly string[];
|
|
201
|
+
readonly removed: readonly string[];
|
|
202
|
+
readonly replaced: readonly string[];
|
|
203
|
+
}
|
|
204
|
+
/** A sentence saying why a set was refused. It holds no text of the file, so it may be logged. */
|
|
205
|
+
export declare class KeySetRefusal extends Error {
|
|
206
|
+
readonly name = "KeySetRefusal";
|
|
207
|
+
}
|
|
208
|
+
export declare class KeySet {
|
|
209
|
+
#private;
|
|
210
|
+
private constructor();
|
|
211
|
+
/**
|
|
212
|
+
* A JSON Web Key Set, `{"keys":[…]}`, read for one algorithm. Throws a
|
|
213
|
+
* `KeySetRefusal` for an algorithm not one of the four, a file that does
|
|
214
|
+
* not parse, an entry that is not an object, any key holding a private
|
|
215
|
+
* member, a key in force without a kid in the alphabet, with another's kid
|
|
216
|
+
* or with one that could be a secret, one that does not import, an RSA key
|
|
217
|
+
* under 2048 bits or with an exponent under 65537 or even, an Ed25519 point
|
|
218
|
+
* of small order, a secret under 32 bytes, and a file with no key in force.
|
|
219
|
+
* A key that cannot verify the algorithm is left out and named.
|
|
220
|
+
*/
|
|
221
|
+
static read(text: string, algorithm: KeySetAlgorithm): KeySet;
|
|
222
|
+
/** `FORMANCY_DATA_IDENTITY_SECRET`'s key: HS256 over the secret's UTF-8 bytes, at least 32 of them. */
|
|
223
|
+
static fromSecret(secret: string): KeySet;
|
|
224
|
+
/** `FORMANCY_DATA_IDENTITY_PUBLIC_KEY_FILE`'s key: an SPKI PEM, imported by jose as it always was, for RS256, ES256 or EdDSA and never HS256. */
|
|
225
|
+
static fromPem(pem: string, algorithm: string): Promise<KeySet>;
|
|
226
|
+
/** Whether `value` is a set the builders made. A brand check, so neither a look-alike nor an object made from the prototype passes. */
|
|
227
|
+
static is(value: unknown): value is KeySet;
|
|
228
|
+
get algorithm(): KeySetAlgorithm;
|
|
229
|
+
/** The kids in force, in file order; none for a one-key setting, which ignores kids. */
|
|
230
|
+
get kids(): readonly string[];
|
|
231
|
+
get leftOut(): readonly LeftOutKey[];
|
|
232
|
+
/** The type of each key in force, `public` or `secret`: what a test can ask of a key without being handed it. */
|
|
233
|
+
get keyTypes(): readonly string[];
|
|
234
|
+
/** What `next` adds, removes and replaces of this set's kids. A key is replaced when its kid stays and its key does not. */
|
|
235
|
+
compare(next: KeySet): KeySetChange;
|
|
236
|
+
/**
|
|
237
|
+
* A one-key setting's key as a key set file holding it under `kid`: what
|
|
238
|
+
* `identity-keys-cli.mjs convert` writes. The secret's key is the base64url
|
|
239
|
+
* of the bytes this set verifies with, not the secret's text, which is also
|
|
240
|
+
* valid base64url and another key (M2). Read back before it is returned.
|
|
241
|
+
* This is key material; nothing logs it.
|
|
242
|
+
*/
|
|
243
|
+
document(kid: string): Promise<string>;
|
|
244
|
+
}
|
|
245
|
+
/** The one function that checks a key set file's text; see `KeySet.read`. */
|
|
246
|
+
export declare function readKeySet(text: string, algorithm: KeySetAlgorithm): KeySet;
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/identity.d.ts
|
|
249
|
+
/**
|
|
250
|
+
* Who is asking, as the host application vouched for it.
|
|
251
|
+
*
|
|
252
|
+
* Built only from a token whose signature verified. Nothing in a request body,
|
|
253
|
+
* query or header other than that token contributes to it — a tenant named in a
|
|
254
|
+
* request is a claim by the browser, and the browser is the party policy exists
|
|
255
|
+
* to constrain.
|
|
256
|
+
*/
|
|
257
|
+
interface HostIdentity {
|
|
258
|
+
actor: {
|
|
259
|
+
id: string;
|
|
260
|
+
roles: string[];
|
|
261
|
+
};
|
|
262
|
+
attributes: Record<string, string>;
|
|
263
|
+
}
|
|
264
|
+
type IdentityOutcome = {
|
|
265
|
+
ok: true;
|
|
266
|
+
identity: HostIdentity;
|
|
267
|
+
} | {
|
|
268
|
+
ok: false;
|
|
269
|
+
reason: string;
|
|
270
|
+
};
|
|
271
|
+
type IdentityVerifier = (token: string) => Promise<IdentityOutcome>;
|
|
272
|
+
/** A setting that names one key: a shared secret, or a public key's PEM. Its tokens' kid is ignored. */
|
|
273
|
+
type OneKey = {
|
|
274
|
+
kind: 'secret';
|
|
275
|
+
secret: string;
|
|
276
|
+
} | {
|
|
277
|
+
kind: 'public-key';
|
|
278
|
+
pem: string;
|
|
279
|
+
algorithm: 'RS256' | 'ES256' | 'EdDSA';
|
|
280
|
+
};
|
|
281
|
+
interface IdentityOptions {
|
|
282
|
+
/**
|
|
283
|
+
* How tokens are signed. A shared secret (HS256), a public key the host
|
|
284
|
+
* signs against with its private one, or a key set read from a file and
|
|
285
|
+
* read again on SIGHUP, whose `keys` is asked on every token. Never a URL,
|
|
286
|
+
* and never a key or a URL a token names: an outbound request from inside
|
|
287
|
+
* a customer's network, and a key the token chooses (0014, 0059).
|
|
288
|
+
*/
|
|
289
|
+
key: OneKey | {
|
|
290
|
+
kind: 'key-set';
|
|
291
|
+
keys: () => KeySet;
|
|
292
|
+
};
|
|
293
|
+
issuer: string;
|
|
294
|
+
audience: string;
|
|
295
|
+
/** Attribute name to claim name, e.g. `{ tenant: 'tid' }`. Only these claims are read. */
|
|
296
|
+
attributes: Readonly<Record<string, string>>;
|
|
297
|
+
/** The claim holding the actor's roles. Default `roles`. */
|
|
298
|
+
rolesClaim?: string;
|
|
299
|
+
/** Seconds of clock skew tolerated on `exp` and `nbf`. Default 30. */
|
|
300
|
+
clockToleranceSeconds?: number;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* A verifier for the host application's tokens, built once at startup.
|
|
304
|
+
*
|
|
305
|
+
* Fails at construction on a configuration that would accept forgeries — a
|
|
306
|
+
* short secret, a key that does not parse, a key set no builder checked —
|
|
307
|
+
* rather than on the first request. The algorithm is pinned per key kind, and
|
|
308
|
+
* for a whole key set, so a token cannot choose `none` or swap an asymmetric
|
|
309
|
+
* key for a shared one.
|
|
310
|
+
*
|
|
311
|
+
* A key set's `keys` is asked once per token, so a set read again replaces
|
|
312
|
+
* the one in force between tokens, and a verification that has begun
|
|
313
|
+
* finishes on the set it began with.
|
|
314
|
+
*/
|
|
315
|
+
export declare function createIdentityVerifier(options: IdentityOptions): Promise<IdentityVerifier>;
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/audit.d.ts
|
|
318
|
+
/** What every event says, on either plane. */
|
|
319
|
+
interface AuditEventBase {
|
|
320
|
+
at: string;
|
|
321
|
+
/**
|
|
322
|
+
* The verified host actor, or `null` for a request refused before its token
|
|
323
|
+
* was read: no token or a token refused, over the rate limit, or a body
|
|
324
|
+
* Fastify refused (400, 413, 415, a client gone mid-body), whatever token it
|
|
325
|
+
* carried, since each plane authenticates after the body is read (0033).
|
|
326
|
+
*/
|
|
327
|
+
actor: string | null;
|
|
328
|
+
status: number;
|
|
329
|
+
/** `ok` below 400; otherwise the stable code the response carried, or `http-NNN` when it carried none. */
|
|
330
|
+
outcome: string;
|
|
331
|
+
}
|
|
332
|
+
type RuntimeOperation = 'form-list' | 'form' | 'read' | 'list' | 'create' | 'update' | 'delete' | 'lookup-query' | 'lookup-resolve';
|
|
333
|
+
type AdminOperation = 'connection-list' | 'connection-test' | 'discovery' | 'proposal' | 'publish' | 'version-list' | 'version-latest' | 'version-read' | 'drift' | 'regeneration' | 'restore' | 'import';
|
|
334
|
+
/**
|
|
335
|
+
* One operational audit event of the runtime plane: who asked for what,
|
|
336
|
+
* against which published version, and how it ended (plan section 12, 0023).
|
|
337
|
+
*
|
|
338
|
+
* Never a value. Not an answer, not a field, not a record: an audit log that
|
|
339
|
+
* copied business data would be a second copy of the customer's database with
|
|
340
|
+
* none of its permissions. The record is named by a keyed hash, or not at all.
|
|
341
|
+
*/
|
|
342
|
+
interface RuntimeAuditEvent extends AuditEventBase {
|
|
343
|
+
plane: 'runtime';
|
|
344
|
+
operation: RuntimeOperation;
|
|
345
|
+
/** The form addressed, when it has a form id's shape (`auditedForm`); otherwise `null`. */
|
|
346
|
+
form: string | null;
|
|
347
|
+
/** The published version the request was served from, when it got that far. */
|
|
348
|
+
formVersion: number | null;
|
|
349
|
+
/**
|
|
350
|
+
* A keyed hash of the record token, or `null`. Keyed, because a record token
|
|
351
|
+
* spells its key and a key is often small — customer 7 — so a plain hash
|
|
352
|
+
* could be reversed by trying every number. Without an audit key the server
|
|
353
|
+
* cannot make one that resists that, so it records nothing rather than a
|
|
354
|
+
* reference that only looks redacted.
|
|
355
|
+
*/
|
|
356
|
+
record: string | null;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* One operational audit event of the administrator's plane (0033): who
|
|
360
|
+
* connected, discovered, proposed, published, read, compared, regenerated,
|
|
361
|
+
* restored or imported, against which connection and which version, and how
|
|
362
|
+
* it ended.
|
|
363
|
+
*
|
|
364
|
+
* Names and numbers only. Never the bundle, the policy or a role it names,
|
|
365
|
+
* a snapshot, a generation request, a drift report or any message.
|
|
366
|
+
*/
|
|
367
|
+
interface AdminAuditEvent extends AuditEventBase {
|
|
368
|
+
plane: 'admin';
|
|
369
|
+
operation: AdminOperation;
|
|
370
|
+
/** An allowlisted connection's name, never an address (0019): one the allowlist does not know is `null`. */
|
|
371
|
+
connection: string | null;
|
|
372
|
+
/** The form addressed, when it has a form id's shape (`auditedForm`); otherwise `null`. */
|
|
373
|
+
form: string | null;
|
|
374
|
+
/** The version read, compared, regenerated from, or written. */
|
|
375
|
+
formVersion: number | null;
|
|
376
|
+
/** Publish, restore and import: the base the administrator said they edited, or saw newest. */
|
|
377
|
+
expectedBase: number | null;
|
|
378
|
+
/** Restore only: the version the new one copies, which nothing on disk records. */
|
|
379
|
+
restoredFrom: number | null;
|
|
380
|
+
}
|
|
381
|
+
/** Both planes, told apart by `plane`. A typed sink narrows on it before reading a plane's own fields. */
|
|
382
|
+
type AuditEvent = RuntimeAuditEvent | AdminAuditEvent;
|
|
383
|
+
/**
|
|
384
|
+
* Where events go. A port: the default writes a structured log line; a
|
|
385
|
+
* deployment that needs a durable trail supplies its own.
|
|
386
|
+
*
|
|
387
|
+
* A sink that throws does not fail the request — by the time the event exists,
|
|
388
|
+
* the write it describes has committed or not, and refusing to answer would
|
|
389
|
+
* only hide which. The failure is logged instead. That is the limitation 0023
|
|
390
|
+
* writes down: this is an operational trail, not evidence.
|
|
391
|
+
*/
|
|
392
|
+
type AuditSink = (event: AuditEvent) => void | Promise<void>;
|
|
393
|
+
/** Events as structured log lines, one per request. */
|
|
394
|
+
export declare function logAuditSink(log: FastifyBaseLogger): AuditSink;
|
|
395
|
+
/** The keyed hash a record is named by: the first 32 hex characters of HMAC-SHA-256. */
|
|
396
|
+
export declare function recordReference(key: string | undefined, token: string | undefined): string | null;
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/routes/admin.d.ts
|
|
399
|
+
interface AdminOptions {
|
|
400
|
+
registry: ConnectionRegistry;
|
|
401
|
+
store: ConfigurationStore;
|
|
402
|
+
/** Roles in the host's token that may connect, discover, propose and publish. */
|
|
403
|
+
adminRoles: readonly string[];
|
|
404
|
+
authenticate: (request: FastifyRequest) => Promise<HostIdentity | undefined>;
|
|
405
|
+
/**
|
|
406
|
+
* The operational trail (0023, 0033): one event per request, refusals
|
|
407
|
+
* included. Required, unlike the runtime's: a plane that publishes and
|
|
408
|
+
* restores forms does not run without saying who did.
|
|
409
|
+
*/
|
|
410
|
+
audit: {
|
|
411
|
+
sink: AuditSink;
|
|
412
|
+
now?: () => string;
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/bundle.d.ts
|
|
417
|
+
/**
|
|
418
|
+
* Everything one published version of a form needs at runtime, as one
|
|
419
|
+
* immutable document in the configuration store (0013).
|
|
420
|
+
*
|
|
421
|
+
* The four concerns plan section 9 keeps apart travel together here and stay
|
|
422
|
+
* separate inside: the portable formancy document, the database bindings, the
|
|
423
|
+
* access policy, and the snapshot the bindings were generated from — which the
|
|
424
|
+
* request planner reads column types from and drift review compares against.
|
|
425
|
+
*/
|
|
426
|
+
interface BundleParts {
|
|
427
|
+
/** The connection the form is bound to, by the name the deployment's allowlist gives it. */
|
|
428
|
+
connection: string;
|
|
429
|
+
form: FormSchema;
|
|
430
|
+
bindings: FormBindings;
|
|
431
|
+
policy: FormPolicy;
|
|
432
|
+
snapshot: MetadataSnapshot;
|
|
433
|
+
}
|
|
434
|
+
/** Published before 0030: served as it is; it kept no base, so a regeneration cannot carry its presentation. */
|
|
435
|
+
interface BundleV1 extends BundleParts {
|
|
436
|
+
format: 1;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Since 0030. `form` is `applyPresentation(base, presentation, bindings)`;
|
|
440
|
+
* `base` and `bindings` are `generateForm(snapshot, generation)`'s, which is
|
|
441
|
+
* checked at publish (`generatedProblems`) and not on read.
|
|
442
|
+
*/
|
|
443
|
+
interface BundleV2 extends BundleParts {
|
|
444
|
+
format: 2;
|
|
445
|
+
generation: GenerationRequest;
|
|
446
|
+
base: FormSchema;
|
|
447
|
+
presentation: PresentationOverrides;
|
|
448
|
+
}
|
|
449
|
+
type PublishedBundle = BundleV1 | BundleV2;
|
|
450
|
+
type BundleValidation = {
|
|
451
|
+
ok: true;
|
|
452
|
+
bundle: PublishedBundle;
|
|
453
|
+
} | {
|
|
454
|
+
ok: false;
|
|
455
|
+
problems: string[];
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* Whether a document can be served as a published form, with every reason it
|
|
459
|
+
* cannot.
|
|
460
|
+
*
|
|
461
|
+
* Run on publish and again on every read from the store, because the store is
|
|
462
|
+
* files on a volume and a file can be edited by hand (0013). Each check is one
|
|
463
|
+
* a hand edit could break silently:
|
|
464
|
+
*
|
|
465
|
+
* - the form must be one formancy's own validator accepts, in the spec
|
|
466
|
+
* version the generator writes (`servedSchema`, 0042) -- the validator
|
|
467
|
+
* alone accepts every version its release speaks;
|
|
468
|
+
* - the snapshot must still hash to its own fingerprint — an edited column
|
|
469
|
+
* type would otherwise reach the planner, which trusts it;
|
|
470
|
+
* - the bindings must have been generated from that snapshot;
|
|
471
|
+
* - the bindings must be a version this release reads — one published before
|
|
472
|
+
* 0027 cannot say which operation a field is written on, and is republished;
|
|
473
|
+
* - every bound field must exist in the form, and the policy must fit the
|
|
474
|
+
* bindings and the snapshot (`policyProblems`), so a rule nobody enforces
|
|
475
|
+
* cannot be published;
|
|
476
|
+
* - in format 2 (0030), the form must be its base with its presentation
|
|
477
|
+
* applied, and the generation request the one this server would store and
|
|
478
|
+
* the one its base and bindings say they came from (`bundle-format2.ts`).
|
|
479
|
+
*
|
|
480
|
+
* Format 1 is still read, so every version published before 0030 is served.
|
|
481
|
+
*
|
|
482
|
+
* It never throws. A part of another shape than a check reads -- a hand
|
|
483
|
+
* edit's `null` where the bindings' fields belong -- is one more reason the
|
|
484
|
+
* version is not served: thrown, it would reach the server's default 500,
|
|
485
|
+
* which answers with the exception's message, and the menu of forms (0045)
|
|
486
|
+
* would have to tell it from a store that cannot be read.
|
|
487
|
+
*/
|
|
488
|
+
export declare function validateBundle(document: unknown): BundleValidation;
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/routes/runtime.d.ts
|
|
491
|
+
interface RuntimeOptions {
|
|
492
|
+
registry: ConnectionRegistry;
|
|
493
|
+
store: ConfigurationStore;
|
|
494
|
+
authenticate: (request: FastifyRequest) => Promise<HostIdentity | undefined>;
|
|
495
|
+
/**
|
|
496
|
+
* The operational trail (0023): one event per request, outcomes included.
|
|
497
|
+
* `key` names records by a keyed hash; without it, no record is named.
|
|
498
|
+
*/
|
|
499
|
+
audit?: {
|
|
500
|
+
sink: AuditSink;
|
|
501
|
+
key?: string;
|
|
502
|
+
now?: () => string;
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* How long a lookup statement may run before the server stops it, the
|
|
506
|
+
* membership checks inside a create or an update included (0061).
|
|
507
|
+
*/
|
|
508
|
+
lookupDeadlineMs: number;
|
|
509
|
+
}
|
|
510
|
+
declare module 'fastify' {
|
|
511
|
+
interface FastifyRequest {
|
|
512
|
+
/** `repeated`: answered with an earlier sending's answer, and nothing asked of the database (0031). */
|
|
513
|
+
auditTrail?: {
|
|
514
|
+
formVersion: number | null;
|
|
515
|
+
record?: string;
|
|
516
|
+
repeated?: boolean;
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The audit name of each runtime route, keyed `METHOD url`; a route without
|
|
522
|
+
* one stops the server, or never answers (0033). `admin-audit.test.ts` sends
|
|
523
|
+
* every route the router holds and fails on an entry no route has. Exported
|
|
524
|
+
* from the package: the host page's suite reads the runtime plane's routes
|
|
525
|
+
* here rather than keeping a list of its own (0047).
|
|
526
|
+
*/
|
|
527
|
+
export declare const ROUTE_OPERATIONS: Readonly<Record<string, RuntimeOperation>>;
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/app.d.ts
|
|
530
|
+
interface DataServerOptions {
|
|
531
|
+
verifyIdentity: IdentityVerifier;
|
|
532
|
+
/**
|
|
533
|
+
* Fastify's logger: `true` for standard output, or a stream to write its
|
|
534
|
+
* lines to. Off by default in tests; the composition root turns it on.
|
|
535
|
+
*/
|
|
536
|
+
logger?: boolean | {
|
|
537
|
+
stream: {
|
|
538
|
+
write: (line: string) => void;
|
|
539
|
+
};
|
|
540
|
+
};
|
|
541
|
+
/**
|
|
542
|
+
* Requests per client address per window, on every route but `/health`.
|
|
543
|
+
*
|
|
544
|
+
* Default 600 a minute (`DEFAULT_RATE_LIMIT`). Generous on purpose: a lookup
|
|
545
|
+
* is a request per pause in typing. What it is for is the other case:
|
|
546
|
+
* somebody replaying tokens at the verifier as fast as the network allows.
|
|
547
|
+
* The runnable server reads `FORMANCY_DATA_RATE_LIMIT` for it (main.ts).
|
|
548
|
+
* Fastify's `trustProxy` is not offered, so behind a reverse proxy every
|
|
549
|
+
* client shares the proxy's address and one bucket: the setting raises that
|
|
550
|
+
* bucket, it does not split it.
|
|
551
|
+
*/
|
|
552
|
+
rateLimit?: {
|
|
553
|
+
max: number;
|
|
554
|
+
timeWindowMs: number;
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* The administrator's plane: connections, discovery, proposals, publication
|
|
558
|
+
* and drift. Absent, none of those routes exist — a server without a store
|
|
559
|
+
* answers 404 for them rather than pretending. Its audit sink is required:
|
|
560
|
+
* every request that reaches one of its routes is an event (0033).
|
|
561
|
+
*/
|
|
562
|
+
admin?: {
|
|
563
|
+
registry: ConnectionRegistry;
|
|
564
|
+
store: ConfigurationStore;
|
|
565
|
+
adminRoles: readonly string[];
|
|
566
|
+
audit: AdminOptions['audit'];
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* The runtime plane: published forms, their records and their lookups, for
|
|
570
|
+
* the host application's people. Absent, none of those routes exist.
|
|
571
|
+
*
|
|
572
|
+
* `origins`: the origins a page may call these routes from, each exactly
|
|
573
|
+
* as a browser sends `Origin` -- `https://app.example`, or plain HTTP on
|
|
574
|
+
* localhost, 127.0.0.1 or [::1] -- checked as `FORMANCY_DATA_CORS_ORIGINS`
|
|
575
|
+
* is, so a wildcard, a pattern or `null` stops the server here (0060).
|
|
576
|
+
* Empty or absent, nothing is offered across origins. The administrator's
|
|
577
|
+
* plane has no such list: it answers no other origin.
|
|
578
|
+
*
|
|
579
|
+
* `lookupDeadlineMs`: how long one lookup statement may run, counted from
|
|
580
|
+
* when the adapter is asked for it -- so a wait for a pooled connection
|
|
581
|
+
* counts, and reading the form and opening its connection do not -- before
|
|
582
|
+
* the server stops it: a search is then answered `503 lookup-timeout`, and
|
|
583
|
+
* a resolve, or a membership check inside a write, `503 unavailable`
|
|
584
|
+
* (0061). A whole number from 1 to 2147483647, or the server is not built.
|
|
585
|
+
* Default `DEFAULT_LOOKUP_DEADLINE_MS`, derived from the published
|
|
586
|
+
* performance figures; the runnable server reads
|
|
587
|
+
* `FORMANCY_DATA_LOOKUP_DEADLINE_MS` for it (main.ts).
|
|
588
|
+
*/
|
|
589
|
+
runtime?: {
|
|
590
|
+
registry: ConnectionRegistry;
|
|
591
|
+
store: ConfigurationStore;
|
|
592
|
+
audit?: NonNullable<RuntimeOptions['audit']>;
|
|
593
|
+
origins?: readonly string[];
|
|
594
|
+
lookupDeadlineMs?: number;
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
declare module 'fastify' {
|
|
598
|
+
interface FastifyRequest {
|
|
599
|
+
identity?: HostIdentity;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The HTTP surface, as far as it exists.
|
|
604
|
+
*
|
|
605
|
+
* The administrator's plane is registered when the server is given a store,
|
|
606
|
+
* a connection registry and the roles that administer. The record and lookup
|
|
607
|
+
* routes arrive with the operations they call: a route that answered before
|
|
608
|
+
* the adapters behind it could would be a documented-but-inert endpoint, which
|
|
609
|
+
* is the failure this repository refuses.
|
|
610
|
+
*
|
|
611
|
+
* `GET /v1/whoami` is for the person wiring a host application in: it returns
|
|
612
|
+
* exactly the identity the server derived from the host's token, so a wrong
|
|
613
|
+
* issuer, audience or claim mapping is visible before any form depends on it.
|
|
614
|
+
*/
|
|
615
|
+
export declare function createDataServer(options: DataServerOptions): Promise<FastifyInstance>;
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/identity-file.d.ts
|
|
618
|
+
/**
|
|
619
|
+
* What one read again of a key set file did, which the log line is built from
|
|
620
|
+
* (identity-settings.ts). `problem` is readKeySet's sentence, so it holds no
|
|
621
|
+
* text of the file.
|
|
622
|
+
*/
|
|
623
|
+
type KeySetReadAgain = {
|
|
624
|
+
read: true;
|
|
625
|
+
changed: true;
|
|
626
|
+
kids: readonly string[];
|
|
627
|
+
added: readonly string[];
|
|
628
|
+
removed: readonly string[];
|
|
629
|
+
replaced: readonly string[];
|
|
630
|
+
leftOut: readonly LeftOutKey[];
|
|
631
|
+
} | {
|
|
632
|
+
read: true;
|
|
633
|
+
changed: false;
|
|
634
|
+
kids: readonly string[];
|
|
635
|
+
leftOut: readonly LeftOutKey[];
|
|
636
|
+
} | {
|
|
637
|
+
read: false;
|
|
638
|
+
kids: readonly string[];
|
|
639
|
+
problem: string;
|
|
640
|
+
};
|
|
641
|
+
interface KeySetFile {
|
|
642
|
+
/** The set in force. The verifier asks for it on every token. */
|
|
643
|
+
readonly current: () => KeySet;
|
|
644
|
+
/**
|
|
645
|
+
* Reads the file again. A set readKeySet accepts replaces the one in force
|
|
646
|
+
* at once; anything else leaves it as it was. Reads run one at a time, in
|
|
647
|
+
* the order they were asked for, and none ever rejects.
|
|
648
|
+
*/
|
|
649
|
+
readonly readAgain: () => Promise<KeySetReadAgain>;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* The key set in `path`, read now -- a refused set throws, which stops the
|
|
653
|
+
* server at start -- and read again whenever `readAgain` is called: on SIGHUP
|
|
654
|
+
* in the running server (main.ts), and by an embedder wherever it chooses.
|
|
655
|
+
*
|
|
656
|
+
* A read that fails keeps the set in force, a key removed from the file
|
|
657
|
+
* included, because a typo or a half-written file must not refuse every
|
|
658
|
+
* token; the outcome says so, and the same file then stops the next start
|
|
659
|
+
* until it is fixed. `read` is injected for the tests; the server and the
|
|
660
|
+
* tool read through readKeySetText.
|
|
661
|
+
*/
|
|
662
|
+
export declare function openKeySetFile(path: string, algorithm: KeySetAlgorithm, read?: (path: string) => Promise<string>): Promise<KeySetFile>;
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/routes/admin-evolution.d.ts
|
|
665
|
+
/**
|
|
666
|
+
* What `POST /v1/forms/:id/regenerations` answers: the newest version
|
|
667
|
+
* regenerated from the database as it is now, with its presentation carried
|
|
668
|
+
* (0030). Nothing is written; this is a draft to review and publish.
|
|
669
|
+
*/
|
|
670
|
+
interface Regeneration {
|
|
671
|
+
/** The version regenerated from; publish with it as `expectedBase`. */
|
|
672
|
+
version: number;
|
|
673
|
+
/** `diffSnapshots(published snapshot, now, published bindings, published policy)`. */
|
|
674
|
+
drift: DriftReport;
|
|
675
|
+
/** The request as used: lookups the database no longer allows are left out. */
|
|
676
|
+
generation: GenerationRequest;
|
|
677
|
+
base: FormSchema;
|
|
678
|
+
bindings: FormBindings;
|
|
679
|
+
notes: GenerationNote[];
|
|
680
|
+
snapshot: MetadataSnapshot;
|
|
681
|
+
/** The published policy, unchanged. */
|
|
682
|
+
policy: FormPolicy;
|
|
683
|
+
/** What that policy does not fit in the new bindings and snapshot. */
|
|
684
|
+
policyProblems: string[];
|
|
685
|
+
/** The published presentation, carried to the new base. */
|
|
686
|
+
presentation: PresentationOverrides;
|
|
687
|
+
/** The new base with it applied. */
|
|
688
|
+
form: FormSchema;
|
|
689
|
+
conflicts: PresentationConflict[];
|
|
690
|
+
/** Lookups the runtime would refuse now, each with the sentence that refuses it. */
|
|
691
|
+
lookupsDropped: Array<{
|
|
692
|
+
foreignKey: string;
|
|
693
|
+
message: string;
|
|
694
|
+
}>;
|
|
695
|
+
/**
|
|
696
|
+
* Keys that now stand for another column or lookup: the grants written for
|
|
697
|
+
* them need confirming. A publish whose policy grants on one is refused,
|
|
698
|
+
* 422 `keys-reassigned`, unless its body lists it, as given here, in
|
|
699
|
+
* `keysConfirmed` (0039).
|
|
700
|
+
*/
|
|
701
|
+
keysReassigned: ReassignedKey[];
|
|
702
|
+
}
|
|
703
|
+
/** What `POST /v1/forms/:id/restorations` answers: the version written, the one it copies, and the drift that allowed it. */
|
|
704
|
+
interface Restoration {
|
|
705
|
+
version: number;
|
|
706
|
+
restoredFrom: number;
|
|
707
|
+
drift: DriftReport;
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/routes/admin-promotion.d.ts
|
|
711
|
+
/**
|
|
712
|
+
* What `POST /v1/forms/:id/imports` answers (0062): the version written, and
|
|
713
|
+
* the drift between the snapshot the bundle carries and this deployment's
|
|
714
|
+
* database that allowed it -- changes to review or note, never one that
|
|
715
|
+
* blocks.
|
|
716
|
+
*/
|
|
717
|
+
interface Importation {
|
|
718
|
+
version: number;
|
|
719
|
+
drift: DriftReport;
|
|
720
|
+
}
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/drivers.d.ts
|
|
723
|
+
/**
|
|
724
|
+
* The drivers, one factory per engine. Each adapter package opens its own
|
|
725
|
+
* connections (`connectPostgres`, `connectSqlServer`), so a connection and the
|
|
726
|
+
* code that binds values on it always come from one copy of the driver — the
|
|
727
|
+
* server imports no driver at all, and the end-to-end suite is what found why
|
|
728
|
+
* that matters.
|
|
729
|
+
*/
|
|
730
|
+
export declare const DRIVER_FACTORIES: Readonly<Record<DatabaseKind, ConnectionFactory>>;
|
|
731
|
+
//#endregion
|
|
732
|
+
export type { AdminAuditEvent, AuditEvent, AuditSink, BundleV1, BundleV2, BundleValidation, ConfigurationStore, ConnectionConfig, ConnectionFactory, ConnectionRegistry, DataServerOptions, HostIdentity, IdentityOptions, IdentityOutcome, IdentityVerifier, Importation, KeySetAlgorithm, KeySetChange, KeySetFile, KeySetReadAgain, LeftOutKey, OneKey, OpenConnection, PublishOutcome, PublishedBundle, Regeneration, Restoration, RuntimeAuditEvent, RuntimeOperation, SecretSource };
|
|
733
|
+
//# sourceMappingURL=index.d.mts.map
|