@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/README.md +8 -3
- package/dist/completers-C-E2b7Jm.mjs +3111 -0
- package/dist/completers-C-E2b7Jm.mjs.map +1 -0
- package/dist/index.d.mts +180 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/main.mjs +297 -46
- package/dist/main.mjs.map +1 -1
- package/package.json +8 -4
- package/dist/file-collector-CGSJAtGO.mjs +0 -1545
- package/dist/file-collector-CGSJAtGO.mjs.map +0 -1
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
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":"
|
|
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
|
|
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 };
|