@formancy/server 0.4.0 → 0.6.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/dist/index.d.mts CHANGED
@@ -1,7 +1,54 @@
1
- import { Scanner, ServerOptionsSources, Storage } from "@formancy/server-core";
2
- import { FastifyInstance } from "fastify";
1
+ import { Completer, Scanner, ServerOptionsSources, Storage } from "@formancy/server-core";
2
+ import { FastifyBaseLogger, FastifyInstance } from "fastify";
3
+ import { FastifyRateLimitStoreCtor } from "@fastify/rate-limit";
3
4
  import postgres from "postgres";
4
5
  import { Readable } from "node:stream";
6
+ //#region src/model-settings.d.ts
7
+ /**
8
+ * Which model the environment names for the builders: none, or a provider, its key and
9
+ * the model.
10
+ *
11
+ * A function of the environment rather than lines in `main.ts`, as `fileStoreSettings` is:
12
+ * the rules are where a deployment goes wrong, and `main.ts` is a composition root no test
13
+ * imports. Constructing the adapter stays in `main.ts`; this only says which
14
+ * ([0165](../../../docs/decisions/0165-a-deployments-model-is-asked-through-its-server.md)).
15
+ */
16
+ export declare const MODEL_PROVIDERS: readonly ["anthropic", "openai", "xai"];
17
+ /** Anthropic's Claude, OpenAI's models, or xAI's Grok — each through its own key. */
18
+ type ModelProvider = (typeof MODEL_PROVIDERS)[number];
19
+ type ModelSettings = {
20
+ readonly kind: 'none';
21
+ } | {
22
+ readonly kind: 'configured';
23
+ readonly provider: ModelProvider;
24
+ readonly apiKey: string;
25
+ readonly model: string;
26
+ };
27
+ type Environment = Readonly<Record<string, string | undefined>>;
28
+ /**
29
+ * Absent — all three unset or blank — is a supported state, and the default: this
30
+ * deployment has no model, the server says so to the admin, and no prompt pane is drawn.
31
+ *
32
+ * Throws on anything in between, so the server refuses to start rather than starting
33
+ * without the model the operator thinks they configured: a key or a model with no
34
+ * provider, a provider formancy has no adapter for, or a provider without its key or
35
+ * its model. Throws too when the environment would add headers to every request the
36
+ * provider's SDK sends. **No provider has a default model.** A model id goes stale when
37
+ * the provider retires it, and then every turn fails as a model that could not be
38
+ * reached, after an upgrade nobody made; and which model runs is what the operator pays
39
+ * for. Neither is this file's to decide. The message never repeats a value, because the
40
+ * key is one.
41
+ */
42
+ export declare function modelSettings(env: Environment): ModelSettings;
43
+ //#endregion
44
+ //#region src/routes/model.d.ts
45
+ /** The deployment's model: which provider, which model, and the adapter that asks it. */
46
+ interface DeploymentModel {
47
+ readonly provider: ModelProvider;
48
+ readonly model: string;
49
+ readonly completer: Completer;
50
+ }
51
+ //#endregion
5
52
  //#region src/file-store.d.ts
6
53
  /**
7
54
  * Where uploaded bytes live.
@@ -46,6 +93,40 @@ export declare function createLocalFileStore(root: string): FileStore;
46
93
  */
47
94
  export declare function sha256(bytes: Buffer): string;
48
95
  //#endregion
96
+ //#region src/server-log.d.ts
97
+ /**
98
+ * The server's log: a JSON line for every request, one for every error that answered one, and
99
+ * one for each thing a route or a background worker has to say
100
+ * ([0168](../../../docs/decisions/0168-the-log-is-built-from-a-list-of-fields.md)).
101
+ *
102
+ * **What keeps everything else out is construction, not a filter over text.** A line is made
103
+ * from the fields `LOG_FIELDS` names, each kept only when its value is of that field's kind,
104
+ * and from nothing else. A key a call hands over that is not on the list is never read, so a
105
+ * body, a header, a query string, an answer, a password, a draft's key or a file's name has no
106
+ * field to go in. The words of a call are never written at all — pino's message argument,
107
+ * Fastify's `error.message`, its `Route GET:/the/raw/url not found` — so neither are the
108
+ * parameters a database error quotes: a line says what happened with an `event` from
109
+ * `LOG_EVENTS`, and a thrown error is written as its kind and its code.
110
+ *
111
+ * Its own logger rather than pino's options, because pino's rule for what to keep would be
112
+ * three hooks — a log formatter, a bindings formatter and a method hook to drop the message —
113
+ * whose order is pino's internals; here it is one function, `written`, and a list.
114
+ */
115
+ /** Most severe first: `fatal` writes the least, `trace` the most. The names are pino's. */
116
+ declare const LOG_LEVELS: readonly ["fatal", "error", "warn", "info", "debug", "trace"];
117
+ type LogLevel = (typeof LOG_LEVELS)[number];
118
+ /** Where lines go: standard output in the server, an array in a test. */
119
+ interface LogSink {
120
+ write(line: string): unknown;
121
+ }
122
+ /** What `createApp` is told to log: where, and from which level up. */
123
+ interface ServerLogSettings {
124
+ readonly sink: LogSink;
125
+ readonly level: LogLevel;
126
+ }
127
+ /** What a background worker complains through. */
128
+ type WorkerLog = Pick<FastifyBaseLogger, 'error'>;
129
+ //#endregion
49
130
  //#region src/headers.d.ts
50
131
  /**
51
132
  * The header a client declares what it was looking at with.
@@ -56,6 +137,11 @@ export declare function sha256(bytes: Buffer): string;
56
137
  * the other one.
57
138
  */
58
139
  export declare const SCHEMA_HASH_HEADER = "x-formancy-schema-hash";
140
+ /**
141
+ * The token a response is sent with: handed out with the form, and with a draft, naming the id
142
+ * the response is stored under (0169). Required of an anonymous submission.
143
+ */
144
+ export declare const SUBMISSION_TOKEN_HEADER = "x-formancy-submission-token";
59
145
  //#endregion
60
146
  //#region src/app.d.ts
61
147
  interface AppOptions {
@@ -97,21 +183,26 @@ interface AppOptions {
97
183
  /**
98
184
  * Anonymous submissions allowed per IP per minute. Off in tests by setting
99
185
  * it high; a real deployment should leave the default.
100
- *
101
- * NOTE: @fastify/rate-limit's default store is in-memory and therefore
102
- * PER PROCESS. Behind more than one replica this counts a fraction of the
103
- * traffic and silently permits N times the limit. A multi-replica
104
- * deployment must supply a shared store.
105
186
  */
106
187
  submissionRateLimit?: {
107
188
  max: number;
108
189
  timeWindowMs: number;
109
190
  };
191
+ /** Where every limit counts; absent, each process counts its own. `main.ts` passes the
192
+ * database every replica shares, through `createPostgresRateLimitStore` (0170). */
193
+ rateLimitStore?: FastifyRateLimitStoreCtor;
110
194
  /** Login attempts per IP per minute. Defaults to 10. */
111
195
  loginRateLimit?: {
112
196
  max: number;
113
197
  timeWindowMs: number;
114
198
  };
199
+ /**
200
+ * The reverse proxies, as addresses and CIDR ranges, whose X-Forwarded-For
201
+ * names the client every limit above counts. Absent trusts none, so behind a
202
+ * proxy all respondents share its one budget; `trust-proxy.ts` says why there
203
+ * is no hop count and no `true`.
204
+ */
205
+ trustProxy?: string[];
115
206
  /**
116
207
  * Largest accepted request body, in bytes. A structural cap BEFORE parsing:
117
208
  * the JSON parser should never be handed something enormous in the first
@@ -128,11 +219,29 @@ interface AppOptions {
128
219
  /**
129
220
  * Largest file this deployment will accept, whatever a form says. A form
130
221
  * author sets the per-field limit; this is the operator's ceiling over all
131
- * of them, because the disk is theirs. Defaults to 10 MB.
222
+ * of them, because the disk is theirs. Defaults to 10 MB; a whole number no
223
+ * larger than `files.size` holds, or `createApp` throws (`upload-settings.ts`).
132
224
  */
133
225
  maxFileBytes?: number;
134
226
  /** Asked about every upload's bytes before they are kept; absent, none is asked (0131). */
135
227
  scanner?: Scanner;
228
+ /**
229
+ * The deployment's model, which the builders ask through `/model/complete` under the
230
+ * briefing this server writes. Absent, there is none: `/model` names no provider and no
231
+ * model, and the admin draws no prompt pane (0165, 0183).
232
+ */
233
+ model?: DeploymentModel;
234
+ /** Model requests per session per minute. Defaults to 10: a run of three turns, three times. */
235
+ modelRateLimit?: {
236
+ max: number;
237
+ timeWindowMs: number;
238
+ };
239
+ /**
240
+ * Where the server's log goes and from which level: a line per request and per error, each
241
+ * built from a list of fields (`server-log.ts`, 0168). Absent, there is none — `main.ts`
242
+ * passes one unless `FORMANCY_LOG_LEVEL` is `off`; a host embedding the app chooses (0115).
243
+ */
244
+ log?: ServerLogSettings;
136
245
  }
137
246
  /**
138
247
  * The HTTP surface, split into two planes.
@@ -155,6 +264,56 @@ export declare function createApp(storage: Storage, options: AppOptions): Promis
155
264
  /** The Storage port over Postgres — the mirror of server-core's in-memory one. */
156
265
  export declare function createPostgresStorage(sql: postgres.Sql): Storage;
157
266
  //#endregion
267
+ //#region src/postgres-rate-limits.d.ts
268
+ interface PostgresRateLimitOptions {
269
+ /** {@link COUNT_TIMEOUT_MS} unless a test needs a slow database to be slow sooner. */
270
+ readonly timeoutMs?: number;
271
+ /** {@link SWEEP_EVERY_MS} unless a test needs to see a sweep. */
272
+ readonly sweepEveryMs?: number;
273
+ /**
274
+ * The server's log (0168), where the counter says that it stopped answering, that it answers
275
+ * again and that a sweep failed — each an event from `LOG_EVENTS`, by what was thrown and never
276
+ * its words, which name the database's address — and where its connections' notices go.
277
+ * Without one it says nothing, as `createApp` without one keeps no log: a host embedding both
278
+ * decides.
279
+ */
280
+ readonly log?: FastifyBaseLogger;
281
+ }
282
+ /** The counter: a store for `createApp`, and its connections, for whoever made it to end. */
283
+ interface PostgresRateLimits {
284
+ /** `@fastify/rate-limit`'s store, which `createApp` takes as `rateLimitStore`. */
285
+ readonly store: FastifyRateLimitStoreCtor;
286
+ /** Closes the counter's connections, once the app that counts in them is closed. */
287
+ readonly end: () => Promise<void>;
288
+ }
289
+ /**
290
+ * `@fastify/rate-limit`'s store over the PostgreSQL every replica already shares (0170).
291
+ *
292
+ * The plugin's own store keeps its counts in the process, so behind N replicas each limit
293
+ * allowed N times what it said. This one keeps them in `rate_limit_counters`, one row per
294
+ * route and client, and counts with one statement: the upsert takes the row's lock, so two
295
+ * replicas counting the same client at once are counted one after the other and neither
296
+ * increment is lost.
297
+ *
298
+ * A window starts at a key's first request and lasts the route's `timeWindow`, as the plugin's
299
+ * own stores do it, on the database's clock — one clock for every replica, where each
300
+ * process's own would end the window at a different moment on each.
301
+ *
302
+ * It opens its own connections from the database's address rather than taking a pool, because
303
+ * which connections it counts on is what keeps a counter that cannot answer from stopping
304
+ * everything else: handed the pool storage queries on, it would count correctly and bring that
305
+ * outage back. A count that does not come back within the bound is abandoned: one still waiting
306
+ * here to be sent is dropped unsent, and one already sent is ended by the database once it runs,
307
+ * by the `statement_timeout` of these connections, with no cancel request — a connection more —
308
+ * needed for it. The error the plugin is handed says nothing about the database, because a route
309
+ * that refuses without a count sends it to whoever asked; what each route does then is its own
310
+ * declaration (`rate-limits.ts`).
311
+ *
312
+ * The plugin's in-memory store is the second implementation of the same interface, and what
313
+ * `createApp` counts in when it is given none.
314
+ */
315
+ export declare function createPostgresRateLimitStore(databaseUrl: string, options?: PostgresRateLimitOptions): PostgresRateLimits;
316
+ //#endregion
158
317
  //#region src/db.d.ts
159
318
  /**
160
319
  * Bootstrap DDL for the thin slice — drizzle-kit migrations arrive with the
@@ -191,7 +350,19 @@ interface CollectorHandle {
191
350
  export declare function startFileCollector(storage: Storage, store: FileStore, options?: {
192
351
  intervalMs?: number;
193
352
  afterHours?: number;
353
+ log?: WorkerLog;
194
354
  }): CollectorHandle;
195
355
  //#endregion
196
- export type { CollectorHandle, FileStore };
356
+ //#region src/completers.d.ts
357
+ /**
358
+ * The adapter for the provider the operator named (0165): Anthropic through its own SDK,
359
+ * OpenAI and xAI through OpenAI's, at each one's base URL.
360
+ *
361
+ * `fetch` is for the tests, which check where each provider's requests go.
362
+ */
363
+ export declare function createCompleter(settings: Extract<ModelSettings, {
364
+ kind: 'configured';
365
+ }>, fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>): Completer;
366
+ //#endregion
367
+ export type { CollectorHandle, DeploymentModel, FileStore, ModelProvider, ModelSettings, PostgresRateLimitOptions, PostgresRateLimits };
197
368
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/file-store.ts","../src/headers.ts","../src/app.ts","../src/postgres-storage.ts","../src/db.ts","../src/file-collector.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UAoBiB;;EAEf,IAAI,aAAa,OAAO,SAAS;;EAEjC,KAAK,cAAc,QAAQ;;EAE3B,OAAO,cAAc;;EAErB,OAAO,cAAc;;;;;;;;;;;wBAYP,qBAAqB,eAAe;;;;;;;;wBA0DpC,OAAO,OAAO;;;;;;;;;;;qBC1FjB;;;UC6CI;;EAEf;;;;;;EAMA;IAAmB;IAAe;;;;;;;;;;;EAUlC;;;;;;;;;;;;;;EAcA,iBAAiB;;;;;;;;;;EAUjB;IAAwB;IAAa;;;EAErC;IAAmB;IAAa;;;;;;;EAMhC;;;;;;;EAOA,YAAY;;;;;;EAMZ;;EAEA,UAAU;;;;;;;;;;;;;;;;;wBAkBU,UAAU,SAAS,SAAS,SAAS,aAAa,QAAQ;;;;wBC/HhE,sBAAsB,KAAK,SAAS,MAAM;;;;;;;;wBCqIpC,gBAAgB,KAAK,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;UCrHzC;EACf;;wBAGc,mBACd,SAAS,SACT,OAAO,WACP;EAAW;EAAqB;IAC/B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/model-settings.ts","../src/routes/model.ts","../src/file-store.ts","../src/server-log.ts","../src/headers.ts","../src/app.ts","../src/postgres-storage.ts","../src/postgres-rate-limits.ts","../src/db.ts","../src/file-collector.ts","../src/completers.ts"],"mappings":";;;;;;;;;;;;;;;qBASa;;KAGD,wBAAwB;KAExB;WACG;;WAEA;WACA,UAAU;WACV;WACA;;KAGV,cAAc,SAAS;;;;;;;;;;;;;;;wBA8BZ,cAAc,KAAK,cAAc;;;;UCrChC;WACN,UAAU;WACV;WACA,WAAW;;;;;;;;;;;;;;;;;;UCCL;;EAEf,IAAI,aAAa,OAAO,SAAS;;EAEjC,KAAK,cAAc,QAAQ;;EAE3B,OAAO,cAAc;;EAErB,OAAO,cAAc;;;;;;;;;;;wBAYP,qBAAqB,eAAe;;;;;;;;wBA0DpC,OAAO,OAAO;;;;;;;;;;;;;;;;;;;;;;cCzEjB;KACD,mBAAmB;;UAKd;EACf,MAAM;;;UAIS;WACN,MAAM;WACN,OAAO;;;KAoRN,YAAY,KAAK;;;;;;;;;;;qBClThB;;;;;qBASA;;;UCgCI;;EAEf;;;;;;EAMA;IAAmB;IAAe;;;;;;;;;;;EAUlC;;;;;;;;;;;;;;EAcA,iBAAiB;;;;;EAKjB;IAAwB;IAAa;;;;EAGrC,iBAAiB;;EAEjB;IAAmB;IAAa;;;;;;;;EAOhC;;;;;;EAMA;;;;;;;EAOA,YAAY;;;;;;;EAOZ;;EAEA,UAAU;;;;;;EAMV,QAAQ;;EAER;IAAmB;IAAa;;;;;;;EAMhC,MAAM;;;;;;;;;;;;;;;;;wBAkBc,UAAU,SAAS,SAAS,SAAS,aAAa,QAAQ;;;;wBC9IhE,sBAAsB,KAAK,SAAS,MAAM;;;UC2BzC;;WAEN;;WAEA;;;;;;;;WAQA,MAAM;;;UAIA;;WAEN,OAAO;;WAEP,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA6BN,6BACd,qBACA,UAAS,2BACR;;;;;;;;wBC8GmB,gBAAgB,KAAK,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;UC5KzC;EACf;;wBAGc,mBACd,SAAS,SACT,OAAO,WACP;EAAW;EAAqB;EAAqB,MAAM;IAC1D;;;;;;;;;wBCxBa,gBACd,UAAU,QAAQ;EAAiB;IACnC,SAAS,gBAAgB,MAAM,SAAS,OAAO,gBAAgB,QAAQ,YACtE"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as bootstrapSchema, i as createPostgresStorage, n as createLocalFileStore, o as createApp, r as sha256, s as SCHEMA_HASH_HEADER, t as startFileCollector } from "./file-collector-CGSJAtGO.mjs";
2
- export { SCHEMA_HASH_HEADER, bootstrapSchema, createApp, createLocalFileStore, createPostgresStorage, sha256, startFileCollector };
1
+ import { _ as SUBMISSION_TOKEN_HEADER, a as createLocalFileStore, c as createPostgresStorage, g as SCHEMA_HASH_HEADER, i as startFileCollector, l as bootstrapSchema, n as MODEL_PROVIDERS, o as sha256, r as modelSettings, s as createPostgresRateLimitStore, t as createCompleter, u as createApp } from "./completers-C-E2b7Jm.mjs";
2
+ export { MODEL_PROVIDERS, SCHEMA_HASH_HEADER, SUBMISSION_TOKEN_HEADER, bootstrapSchema, createApp, createCompleter, createLocalFileStore, createPostgresRateLimitStore, createPostgresStorage, modelSettings, sha256, startFileCollector };