@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.
@@ -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