ras-stack 0.37.0 → 0.38.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/cli.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { runAssetsCli } from './build/cli.js';
3
+ import { runInitCli } from './policy/init-cli.js';
3
4
  import { runPolicyCli } from './policy/cli.js';
4
5
  import { runPreviewCli } from './preview/cli.js';
5
6
  import { runRealtimeCli } from './runtime/dev-cli.js';
6
7
  declare const commands: {
7
8
  readonly assets: typeof runAssetsCli;
9
+ readonly init: typeof runInitCli;
8
10
  readonly policy: typeof runPolicyCli;
9
11
  readonly preview: typeof runPreviewCli;
10
12
  readonly realtime: typeof runRealtimeCli;
package/dist/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { runAssetsCli } from './build/cli.js';
3
+ import { runInitCli } from './policy/init-cli.js';
3
4
  import { runPolicyCli } from './policy/cli.js';
4
5
  import { runPreviewCli } from './preview/cli.js';
5
6
  import { runRealtimeCli } from './runtime/dev-cli.js';
6
7
  const commands = {
7
8
  assets: runAssetsCli,
9
+ init: runInitCli,
8
10
  policy: runPolicyCli,
9
11
  preview: runPreviewCli,
10
12
  realtime: runRealtimeCli,
@@ -18,7 +20,7 @@ export function parseRasCommand(arguments_) {
18
20
  export async function runRasCli(arguments_) {
19
21
  const parsed = parseRasCommand(arguments_);
20
22
  if (!parsed) {
21
- console.error('usage: ras <assets|policy|preview|realtime> [arguments]');
23
+ console.error('usage: ras <assets|init|policy|preview|realtime> [arguments]');
22
24
  process.exitCode = 2;
23
25
  return;
24
26
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,MAAM,QAAQ,GAAG;IACf,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,QAAQ,EAAE,cAAc;CAChB,CAAA;AAIV,MAAM,UAAU,eAAe,CAAC,UAAoB;IAClD,MAAM,CAAC,OAAO,EAAE,GAAG,aAAa,CAAC,GAAG,UAAU,CAAA;IAC9C,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACpE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,OAAqB,EAAE,CAAA;AACrE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAoB;IAClD,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAA;QACxE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED,IAAI,OAAO,IAAI,CAAC,IAAI;IAAE,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["#!/usr/bin/env node\nimport { runAssetsCli } from './build/cli.js'\nimport { runPolicyCli } from './policy/cli.js'\nimport { runPreviewCli } from './preview/cli.js'\nimport { runRealtimeCli } from './runtime/dev-cli.js'\n\nconst commands = {\n assets: runAssetsCli,\n policy: runPolicyCli,\n preview: runPreviewCli,\n realtime: runRealtimeCli,\n} as const\n\nexport type RasCommand = keyof typeof commands\n\nexport function parseRasCommand(arguments_: string[]): { arguments: string[]; command: RasCommand } | undefined {\n const [command, ...argumentsRest] = arguments_\n if (!(command && Object.hasOwn(commands, command))) return undefined\n return { arguments: argumentsRest, command: command as RasCommand }\n}\n\nexport async function runRasCli(arguments_: string[]): Promise<void> {\n const parsed = parseRasCommand(arguments_)\n if (!parsed) {\n console.error('usage: ras <assets|policy|preview|realtime> [arguments]')\n process.exitCode = 2\n return\n }\n await commands[parsed.command](parsed.arguments)\n}\n\nif (import.meta.main) await runRasCli(process.argv.slice(2))\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,MAAM,QAAQ,GAAG;IACf,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,QAAQ,EAAE,cAAc;CAChB,CAAA;AAIV,MAAM,UAAU,eAAe,CAAC,UAAoB;IAClD,MAAM,CAAC,OAAO,EAAE,GAAG,aAAa,CAAC,GAAG,UAAU,CAAA;IAC9C,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACpE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,OAAqB,EAAE,CAAA;AACrE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAoB;IAClD,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,8DAA8D,CAAC,CAAA;QAC7E,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAM;IACR,CAAC;IACD,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED,IAAI,OAAO,IAAI,CAAC,IAAI;IAAE,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["#!/usr/bin/env node\nimport { runAssetsCli } from './build/cli.js'\nimport { runInitCli } from './policy/init-cli.js'\nimport { runPolicyCli } from './policy/cli.js'\nimport { runPreviewCli } from './preview/cli.js'\nimport { runRealtimeCli } from './runtime/dev-cli.js'\n\nconst commands = {\n assets: runAssetsCli,\n init: runInitCli,\n policy: runPolicyCli,\n preview: runPreviewCli,\n realtime: runRealtimeCli,\n} as const\n\nexport type RasCommand = keyof typeof commands\n\nexport function parseRasCommand(arguments_: string[]): { arguments: string[]; command: RasCommand } | undefined {\n const [command, ...argumentsRest] = arguments_\n if (!(command && Object.hasOwn(commands, command))) return undefined\n return { arguments: argumentsRest, command: command as RasCommand }\n}\n\nexport async function runRasCli(arguments_: string[]): Promise<void> {\n const parsed = parseRasCommand(arguments_)\n if (!parsed) {\n console.error('usage: ras <assets|init|policy|preview|realtime> [arguments]')\n process.exitCode = 2\n return\n }\n await commands[parsed.command](parsed.arguments)\n}\n\nif (import.meta.main) await runRasCli(process.argv.slice(2))\n"]}
@@ -18,6 +18,35 @@ export type RealtimeTokenConformanceOptions = {
18
18
  now?: number;
19
19
  };
20
20
  export declare function assertRealtimeTokenConformance(sign: RealtimeTokenSigner, options: RealtimeTokenConformanceOptions): Promise<void>;
21
+ export type RateLimitStoreProbe = {
22
+ increment: (key: string, windowSeconds: number, now: number) => Promise<{
23
+ count: number;
24
+ resetAt: number;
25
+ }> | {
26
+ count: number;
27
+ resetAt: number;
28
+ };
29
+ };
30
+ export declare function assertRateLimitStoreConformance(store: RateLimitStoreProbe, options?: {
31
+ now?: number;
32
+ }): Promise<void>;
33
+ export type AuthSecretProvider = (environment: NodeJS.ProcessEnv) => string | Promise<string>;
34
+ export declare function assertAuthSecretConformance(secret: AuthSecretProvider, options?: {
35
+ environmentKey?: string;
36
+ }): Promise<void>;
37
+ export type RealtimePublisherProbe = {
38
+ publish: (channel: string, data: unknown) => boolean;
39
+ close: () => Promise<void>;
40
+ };
41
+ export declare function assertRealtimePublisherConformance(create: () => RealtimePublisherProbe, options?: {
42
+ probes?: number;
43
+ }): Promise<void>;
44
+ export type SmtpConfigReader = (environment: NodeJS.ProcessEnv) => {
45
+ host: string;
46
+ port: number;
47
+ from: string;
48
+ } | undefined;
49
+ export declare function assertSmtpConfigConformance(read: SmtpConfigReader): void;
21
50
  export type DatabaseTarget = {
22
51
  provider: 'sqlite';
23
52
  file: string;
@@ -142,6 +142,94 @@ async function verifyHmac(signed, signature, secret) {
142
142
  .replaceAll('=', '');
143
143
  return expected === signature;
144
144
  }
145
+ // The package cannot reach a consumer's PostgreSQL, so the store contract travels to wherever the real database is.
146
+ export async function assertRateLimitStoreConformance(store, options = {}) {
147
+ const now = options.now ?? Math.floor(Date.now() / 1000);
148
+ const window = 60;
149
+ const key = `conformance:${crypto.randomUUID()}`;
150
+ const first = await store.increment(key, window, now);
151
+ if (first.count !== 1)
152
+ throw new ConformanceError('rate limit store', `expected a new key to start at 1, received ${first.count}`);
153
+ if (first.resetAt !== now + window) {
154
+ throw new ConformanceError('rate limit store', `expected the window to end at ${now + window}, received ${first.resetAt}`);
155
+ }
156
+ const second = await store.increment(key, window, now + 1);
157
+ if (second.count !== 2)
158
+ throw new ConformanceError('rate limit store', `expected the second request to count 2, received ${second.count}`);
159
+ if (second.resetAt !== first.resetAt) {
160
+ throw new ConformanceError('rate limit store', 'a request inside the window must not extend it');
161
+ }
162
+ const other = await store.increment(`conformance:${crypto.randomUUID()}`, window, now);
163
+ if (other.count !== 1)
164
+ throw new ConformanceError('rate limit store', 'one key consumed another key budget');
165
+ const expired = await store.increment(key, window, first.resetAt);
166
+ if (expired.count !== 1)
167
+ throw new ConformanceError('rate limit store', 'an elapsed window must start a new count');
168
+ if (expired.resetAt !== first.resetAt + window) {
169
+ throw new ConformanceError('rate limit store', 'an elapsed window must start a new window');
170
+ }
171
+ }
172
+ // A secret that changes between restarts signs out every session, and a short one is guessable. Neither shows up
173
+ // until it is already in production, so the contract is checked against the provider an application wired.
174
+ export async function assertAuthSecretConformance(secret, options = {}) {
175
+ const key = options.environmentKey ?? 'AUTH_SECRET';
176
+ const configured = 'configured-secret-value-that-is-long-enough';
177
+ const first = await secret({});
178
+ if (typeof first !== 'string' || !first.trim())
179
+ throw new ConformanceError('auth secret', 'provider returned no secret');
180
+ if (first.length < 32)
181
+ throw new ConformanceError('auth secret', `expected at least 32 characters, received ${first.length}`);
182
+ if (new Set(first).size < 8)
183
+ throw new ConformanceError('auth secret', 'secret does not look randomly generated');
184
+ if ((await secret({})) !== first) {
185
+ throw new ConformanceError('auth secret', 'secret changed between calls, which signs out every session on restart');
186
+ }
187
+ if ((await secret({ [key]: configured })) !== configured) {
188
+ throw new ConformanceError('auth secret', `${key} must take precedence over a generated secret`);
189
+ }
190
+ }
191
+ // A publisher that accepts every channel turns a burst into unbounded memory, and one that accepts work after close
192
+ // loses publications during shutdown. Both appear only under load.
193
+ export async function assertRealtimePublisherConformance(create, options = {}) {
194
+ const probes = options.probes ?? 5_000;
195
+ const bounded = create();
196
+ let admitted = 0;
197
+ for (let attempt = 0; attempt < probes; attempt++)
198
+ if (bounded.publish(`conformance-${attempt}`, { attempt }))
199
+ admitted += 1;
200
+ if (admitted === probes)
201
+ throw new ConformanceError('realtime publisher', `accepted all ${probes} channels without a capacity limit`);
202
+ if (admitted === 0)
203
+ throw new ConformanceError('realtime publisher', 'rejected every publication');
204
+ await bounded.close();
205
+ const closed = create();
206
+ await closed.close();
207
+ if (closed.publish('conformance', {}))
208
+ throw new ConformanceError('realtime publisher', 'accepted work after it was closed');
209
+ }
210
+ // Half-configured SMTP is the failure that reaches production, because nothing sends mail until something needs to.
211
+ export function assertSmtpConfigConformance(read) {
212
+ if (read({}) !== undefined)
213
+ throw new ConformanceError('SMTP configuration', 'expected no configuration when nothing is set');
214
+ const configured = read({ SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com' });
215
+ if (!configured)
216
+ throw new ConformanceError('SMTP configuration', 'expected a configuration from a host and sender');
217
+ if (configured.port !== 587)
218
+ throw new ConformanceError('SMTP configuration', `expected the default port 587, received ${configured.port}`);
219
+ rejects(read, { SMTP_HOST: 'smtp.example' }, 'a host without a sender');
220
+ rejects(read, { EMAIL_FROM: 'app@example.com' }, 'a sender without a host');
221
+ rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_PORT: '70000' }, 'a port outside the valid range');
222
+ rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_USER: 'user' }, 'a user without a password');
223
+ }
224
+ function rejects(read, environment, scenario) {
225
+ try {
226
+ read(environment);
227
+ }
228
+ catch {
229
+ return;
230
+ }
231
+ throw new ConformanceError('SMTP configuration', `expected ${scenario} to be rejected`);
232
+ }
145
233
  export function assertDatabaseTargetConformance(resolve) {
146
234
  const sqliteFile = '/data/application.sqlite';
147
235
  const sqlite = resolve({ sqliteFile });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"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","sourcesContent":["export class ConformanceError extends Error {\n constructor(\n readonly scenario: string,\n message: string,\n options: { cause?: unknown } = {},\n ) {\n super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'ConformanceError'\n }\n}\n\nexport async function assertMutationOriginConformance(\n guard: (request: Request) => void | Promise<void>,\n options: { trustForwardedHeaders?: boolean } = {},\n) {\n await accepted('same-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },\n }),\n ),\n )\n await rejected('cross-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },\n }),\n ),\n )\n await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })))\n if (!options.trustForwardedHeaders) {\n await rejected('spoofed forwarded-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: {\n origin: 'https://attacker.example',\n 'x-forwarded-host': 'attacker.example',\n 'x-forwarded-proto': 'https',\n },\n }),\n ),\n )\n }\n}\n\nexport type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>\n\nexport async function assertHealthHandlerConformance(createHandler: HealthHandlerFactory) {\n const healthy = await createHandler(() => undefined)()\n if (healthy.status !== 200) throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`)\n const healthyBody = await responseBody(healthy, 'healthy dependency')\n if (healthyBody.ok !== true) throw new ConformanceError('healthy dependency', 'response body must contain ok: true')\n\n const privateMessage = 'password=private-diagnostic'\n const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))()\n if (unavailable.status !== 503) {\n throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`)\n }\n const unavailableText = await unavailable.text()\n if (unavailableText.includes(privateMessage)) {\n throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message')\n }\n let unavailableBody: unknown\n try {\n unavailableBody = JSON.parse(unavailableText)\n } catch (error) {\n throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error })\n }\n if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {\n throw new ConformanceError('unavailable dependency', 'response body must contain ok: false')\n }\n}\n\nexport type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown\n\nexport async function assertSqliteConformance(readPragma: SqlitePragmaReader) {\n const values = {\n journalMode: String(await readPragma('journal_mode')).toLowerCase(),\n synchronous: Number(await readPragma('synchronous')),\n busyTimeout: Number(await readPragma('busy_timeout')),\n foreignKeys: Number(await readPragma('foreign_keys')),\n }\n if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {\n throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`)\n }\n if (values.synchronous !== 2) {\n throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`)\n }\n if (values.busyTimeout !== 5000) {\n throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`)\n }\n if (values.foreignKeys !== 1) {\n throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`)\n }\n}\n\nexport type RealtimeTokenSigner = (subject: string, claims: Record<string, unknown>) => string | Promise<string>\n\nexport type RealtimeTokenConformanceOptions = { secret: string; maxTtlSeconds?: number; now?: number }\n\nexport async function assertRealtimeTokenConformance(sign: RealtimeTokenSigner, options: RealtimeTokenConformanceOptions) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const maxTtlSeconds = options.maxTtlSeconds ?? 60 * 60\n const token = await sign('person-123', { channel: 'room:1' })\n const { header, payload, signed, signature } = decodeToken(token)\n\n if (header.alg !== 'HS256') {\n throw new ConformanceError('realtime token algorithm', `expected HS256, received ${JSON.stringify(header.alg)}`)\n }\n if (payload.sub !== 'person-123') {\n throw new ConformanceError('realtime token subject', 'token must bind the subject it was signed for')\n }\n if (payload.channel !== 'room:1') {\n throw new ConformanceError('realtime token claims', 'token must carry the claims it was signed with')\n }\n if (typeof payload.exp !== 'number') {\n throw new ConformanceError('realtime token expiry', 'token must expire')\n }\n if (payload.exp <= now) {\n throw new ConformanceError('realtime token expiry', 'token expired before it was issued')\n }\n if (payload.exp - now > maxTtlSeconds) {\n throw new ConformanceError('realtime token expiry', `token outlives the ${maxTtlSeconds} second maximum`)\n }\n if (!(await verifyHmac(signed, signature, options.secret))) {\n throw new ConformanceError('realtime token signature', 'token is not signed with the shared Centrifugo secret')\n }\n\n const other = decodeToken(await sign('person-456', { channel: 'room:1' })).payload\n if (other.sub === payload.sub) {\n throw new ConformanceError('realtime token subject', 'every subject received the same identity')\n }\n}\n\nfunction decodeToken(token: string) {\n const segments = token.split('.')\n if (segments.length !== 3) throw new ConformanceError('realtime token format', 'expected a three-segment JWT')\n const [header, claims, signature] = segments as [string, string, string]\n return {\n header: decodeSegment(header, 'header'),\n payload: decodeSegment(claims, 'payload'),\n signed: `${header}.${claims}`,\n signature,\n }\n}\n\nfunction decodeSegment(segment: string, name: string): Record<string, unknown> {\n try {\n const padded = segment\n .replaceAll('-', '+')\n .replaceAll('_', '/')\n .padEnd(Math.ceil(segment.length / 4) * 4, '=')\n const value: unknown = JSON.parse(atob(padded))\n if (value && typeof value === 'object') return value as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError('realtime token format', `${name} is not base64url JSON`, { cause: error })\n }\n throw new ConformanceError('realtime token format', `${name} is not an object`)\n}\n\nasync function verifyHmac(signed: string, signature: string, secret: string) {\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(signed)))\n const expected = btoa(String.fromCharCode(...digest))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '')\n return expected === signature\n}\n\nexport type DatabaseTarget = { provider: 'sqlite'; file: string } | { provider: 'postgres'; url: string }\n\nexport type DatabaseTargetResolver = (options: { databaseUrl?: string; sqliteFile: string }) => DatabaseTarget\n\nexport function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver) {\n const sqliteFile = '/data/application.sqlite'\n const sqlite = resolve({ sqliteFile })\n if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {\n throw new ConformanceError('default database target', 'expected the configured SQLite file')\n }\n\n for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {\n const target = resolve({ databaseUrl: url, sqliteFile })\n if (target.provider !== 'postgres' || target.url !== url) {\n throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`)\n }\n }\n\n try {\n resolve({ databaseUrl: 'https://database.example/application', sqliteFile })\n } catch {\n return\n }\n throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected')\n}\n\nexport function assertPostHogBrowserConformance(options: Record<string, unknown>) {\n if (typeof options.api_host !== 'string' || !options.api_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'api_host must be configured')\n }\n if (typeof options.ui_host !== 'string' || !options.ui_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'ui_host must be configured')\n }\n if (typeof options.defaults !== 'string' || !options.defaults.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'SDK defaults must be pinned')\n }\n if (!options.capture_exceptions) {\n throw new ConformanceError('PostHog browser initialization', 'exception autocapture must be enabled')\n }\n if (options.capture_pageview !== 'history_change') {\n throw new ConformanceError('PostHog browser initialization', 'SPA pageviews must follow history changes')\n }\n if (options.person_profiles !== 'identified_only') {\n throw new ConformanceError('PostHog browser initialization', 'person profiles must be limited to identified users')\n }\n const recording = options.session_recording\n if (!recording || typeof recording !== 'object' || !('maskAllInputs' in recording) || recording.maskAllInputs !== true) {\n throw new ConformanceError('PostHog browser initialization', 'session replay must mask all inputs by default')\n }\n}\n\ntype PostHogContextParser = (\n request: Request,\n options?: { authenticatedDistinctId?: string; allowAnonymousDistinctId?: boolean },\n) => { distinctId?: string; sessionId?: string; properties: { $session_id?: string } }\n\nexport function assertPostHogRequestConformance(parse: PostHogContextParser) {\n const matched = parse(postHogRequest('person-123', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (matched.distinctId !== 'person-123' || matched.sessionId !== 'session-456' || matched.properties.$session_id !== 'session-456') {\n throw new ConformanceError('authenticated PostHog request', 'expected verified identity and session propagation')\n }\n const spoofed = parse(postHogRequest('attacker', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (spoofed.distinctId !== undefined) {\n throw new ConformanceError('spoofed PostHog request', 'unverified distinct id was trusted')\n }\n const malformed = parse(postHogRequest('person-123', 'x'.repeat(129)), { authenticatedDistinctId: 'person-123' })\n if (malformed.sessionId !== undefined || malformed.properties.$session_id !== undefined) {\n throw new ConformanceError('malformed PostHog request', 'unbounded session id was propagated')\n }\n}\n\nfunction postHogRequest(distinctId: string, sessionId: string) {\n return new Request('https://app.example/action', {\n headers: { 'x-posthog-distinct-id': distinctId, 'x-posthog-session-id': sessionId },\n })\n}\n\nasync function accepted(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch (error) {\n throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error })\n }\n}\n\nasync function rejected(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch {\n return\n }\n throw new ConformanceError(scenario, 'expected request to be rejected')\n}\n\nasync function responseBody(response: Response, scenario: string): Promise<Record<string, unknown>> {\n try {\n const body: unknown = await response.json()\n if (body && typeof body === 'object') return body as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError(scenario, 'response body must be JSON', { cause: error })\n }\n throw new ConformanceError(scenario, 'response body must be an object')\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"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","sourcesContent":["export class ConformanceError extends Error {\n constructor(\n readonly scenario: string,\n message: string,\n options: { cause?: unknown } = {},\n ) {\n super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'ConformanceError'\n }\n}\n\nexport async function assertMutationOriginConformance(\n guard: (request: Request) => void | Promise<void>,\n options: { trustForwardedHeaders?: boolean } = {},\n) {\n await accepted('same-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },\n }),\n ),\n )\n await rejected('cross-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },\n }),\n ),\n )\n await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })))\n if (!options.trustForwardedHeaders) {\n await rejected('spoofed forwarded-origin request', () =>\n guard(\n new Request('https://app.example/action', {\n method: 'POST',\n headers: {\n origin: 'https://attacker.example',\n 'x-forwarded-host': 'attacker.example',\n 'x-forwarded-proto': 'https',\n },\n }),\n ),\n )\n }\n}\n\nexport type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>\n\nexport async function assertHealthHandlerConformance(createHandler: HealthHandlerFactory) {\n const healthy = await createHandler(() => undefined)()\n if (healthy.status !== 200) throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`)\n const healthyBody = await responseBody(healthy, 'healthy dependency')\n if (healthyBody.ok !== true) throw new ConformanceError('healthy dependency', 'response body must contain ok: true')\n\n const privateMessage = 'password=private-diagnostic'\n const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))()\n if (unavailable.status !== 503) {\n throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`)\n }\n const unavailableText = await unavailable.text()\n if (unavailableText.includes(privateMessage)) {\n throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message')\n }\n let unavailableBody: unknown\n try {\n unavailableBody = JSON.parse(unavailableText)\n } catch (error) {\n throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error })\n }\n if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {\n throw new ConformanceError('unavailable dependency', 'response body must contain ok: false')\n }\n}\n\nexport type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown\n\nexport async function assertSqliteConformance(readPragma: SqlitePragmaReader) {\n const values = {\n journalMode: String(await readPragma('journal_mode')).toLowerCase(),\n synchronous: Number(await readPragma('synchronous')),\n busyTimeout: Number(await readPragma('busy_timeout')),\n foreignKeys: Number(await readPragma('foreign_keys')),\n }\n if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {\n throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`)\n }\n if (values.synchronous !== 2) {\n throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`)\n }\n if (values.busyTimeout !== 5000) {\n throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`)\n }\n if (values.foreignKeys !== 1) {\n throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`)\n }\n}\n\nexport type RealtimeTokenSigner = (subject: string, claims: Record<string, unknown>) => string | Promise<string>\n\nexport type RealtimeTokenConformanceOptions = { secret: string; maxTtlSeconds?: number; now?: number }\n\nexport async function assertRealtimeTokenConformance(sign: RealtimeTokenSigner, options: RealtimeTokenConformanceOptions) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const maxTtlSeconds = options.maxTtlSeconds ?? 60 * 60\n const token = await sign('person-123', { channel: 'room:1' })\n const { header, payload, signed, signature } = decodeToken(token)\n\n if (header.alg !== 'HS256') {\n throw new ConformanceError('realtime token algorithm', `expected HS256, received ${JSON.stringify(header.alg)}`)\n }\n if (payload.sub !== 'person-123') {\n throw new ConformanceError('realtime token subject', 'token must bind the subject it was signed for')\n }\n if (payload.channel !== 'room:1') {\n throw new ConformanceError('realtime token claims', 'token must carry the claims it was signed with')\n }\n if (typeof payload.exp !== 'number') {\n throw new ConformanceError('realtime token expiry', 'token must expire')\n }\n if (payload.exp <= now) {\n throw new ConformanceError('realtime token expiry', 'token expired before it was issued')\n }\n if (payload.exp - now > maxTtlSeconds) {\n throw new ConformanceError('realtime token expiry', `token outlives the ${maxTtlSeconds} second maximum`)\n }\n if (!(await verifyHmac(signed, signature, options.secret))) {\n throw new ConformanceError('realtime token signature', 'token is not signed with the shared Centrifugo secret')\n }\n\n const other = decodeToken(await sign('person-456', { channel: 'room:1' })).payload\n if (other.sub === payload.sub) {\n throw new ConformanceError('realtime token subject', 'every subject received the same identity')\n }\n}\n\nfunction decodeToken(token: string) {\n const segments = token.split('.')\n if (segments.length !== 3) throw new ConformanceError('realtime token format', 'expected a three-segment JWT')\n const [header, claims, signature] = segments as [string, string, string]\n return {\n header: decodeSegment(header, 'header'),\n payload: decodeSegment(claims, 'payload'),\n signed: `${header}.${claims}`,\n signature,\n }\n}\n\nfunction decodeSegment(segment: string, name: string): Record<string, unknown> {\n try {\n const padded = segment\n .replaceAll('-', '+')\n .replaceAll('_', '/')\n .padEnd(Math.ceil(segment.length / 4) * 4, '=')\n const value: unknown = JSON.parse(atob(padded))\n if (value && typeof value === 'object') return value as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError('realtime token format', `${name} is not base64url JSON`, { cause: error })\n }\n throw new ConformanceError('realtime token format', `${name} is not an object`)\n}\n\nasync function verifyHmac(signed: string, signature: string, secret: string) {\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(signed)))\n const expected = btoa(String.fromCharCode(...digest))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '')\n return expected === signature\n}\n\nexport type RateLimitStoreProbe = {\n increment: (\n key: string,\n windowSeconds: number,\n now: number,\n ) => Promise<{ count: number; resetAt: number }> | { count: number; resetAt: number }\n}\n\n// The package cannot reach a consumer's PostgreSQL, so the store contract travels to wherever the real database is.\nexport async function assertRateLimitStoreConformance(store: RateLimitStoreProbe, options: { now?: number } = {}) {\n const now = options.now ?? Math.floor(Date.now() / 1000)\n const window = 60\n const key = `conformance:${crypto.randomUUID()}`\n\n const first = await store.increment(key, window, now)\n if (first.count !== 1) throw new ConformanceError('rate limit store', `expected a new key to start at 1, received ${first.count}`)\n if (first.resetAt !== now + window) {\n throw new ConformanceError('rate limit store', `expected the window to end at ${now + window}, received ${first.resetAt}`)\n }\n\n const second = await store.increment(key, window, now + 1)\n if (second.count !== 2) throw new ConformanceError('rate limit store', `expected the second request to count 2, received ${second.count}`)\n if (second.resetAt !== first.resetAt) {\n throw new ConformanceError('rate limit store', 'a request inside the window must not extend it')\n }\n\n const other = await store.increment(`conformance:${crypto.randomUUID()}`, window, now)\n if (other.count !== 1) throw new ConformanceError('rate limit store', 'one key consumed another key budget')\n\n const expired = await store.increment(key, window, first.resetAt)\n if (expired.count !== 1) throw new ConformanceError('rate limit store', 'an elapsed window must start a new count')\n if (expired.resetAt !== first.resetAt + window) {\n throw new ConformanceError('rate limit store', 'an elapsed window must start a new window')\n }\n}\n\nexport type AuthSecretProvider = (environment: NodeJS.ProcessEnv) => string | Promise<string>\n\n// A secret that changes between restarts signs out every session, and a short one is guessable. Neither shows up\n// until it is already in production, so the contract is checked against the provider an application wired.\nexport async function assertAuthSecretConformance(secret: AuthSecretProvider, options: { environmentKey?: string } = {}) {\n const key = options.environmentKey ?? 'AUTH_SECRET'\n const configured = 'configured-secret-value-that-is-long-enough'\n\n const first = await secret({})\n if (typeof first !== 'string' || !first.trim()) throw new ConformanceError('auth secret', 'provider returned no secret')\n if (first.length < 32) throw new ConformanceError('auth secret', `expected at least 32 characters, received ${first.length}`)\n if (new Set(first).size < 8) throw new ConformanceError('auth secret', 'secret does not look randomly generated')\n\n if ((await secret({})) !== first) {\n throw new ConformanceError('auth secret', 'secret changed between calls, which signs out every session on restart')\n }\n if ((await secret({ [key]: configured })) !== configured) {\n throw new ConformanceError('auth secret', `${key} must take precedence over a generated secret`)\n }\n}\n\nexport type RealtimePublisherProbe = { publish: (channel: string, data: unknown) => boolean; close: () => Promise<void> }\n\n// A publisher that accepts every channel turns a burst into unbounded memory, and one that accepts work after close\n// loses publications during shutdown. Both appear only under load.\nexport async function assertRealtimePublisherConformance(create: () => RealtimePublisherProbe, options: { probes?: number } = {}) {\n const probes = options.probes ?? 5_000\n const bounded = create()\n let admitted = 0\n for (let attempt = 0; attempt < probes; attempt++) if (bounded.publish(`conformance-${attempt}`, { attempt })) admitted += 1\n if (admitted === probes) throw new ConformanceError('realtime publisher', `accepted all ${probes} channels without a capacity limit`)\n if (admitted === 0) throw new ConformanceError('realtime publisher', 'rejected every publication')\n await bounded.close()\n\n const closed = create()\n await closed.close()\n if (closed.publish('conformance', {})) throw new ConformanceError('realtime publisher', 'accepted work after it was closed')\n}\n\nexport type SmtpConfigReader = (environment: NodeJS.ProcessEnv) => { host: string; port: number; from: string } | undefined\n\n// Half-configured SMTP is the failure that reaches production, because nothing sends mail until something needs to.\nexport function assertSmtpConfigConformance(read: SmtpConfigReader) {\n if (read({}) !== undefined) throw new ConformanceError('SMTP configuration', 'expected no configuration when nothing is set')\n\n const configured = read({ SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com' })\n if (!configured) throw new ConformanceError('SMTP configuration', 'expected a configuration from a host and sender')\n if (configured.port !== 587)\n throw new ConformanceError('SMTP configuration', `expected the default port 587, received ${configured.port}`)\n\n rejects(read, { SMTP_HOST: 'smtp.example' }, 'a host without a sender')\n rejects(read, { EMAIL_FROM: 'app@example.com' }, 'a sender without a host')\n rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_PORT: '70000' }, 'a port outside the valid range')\n rejects(read, { SMTP_HOST: 'smtp.example', EMAIL_FROM: 'app@example.com', SMTP_USER: 'user' }, 'a user without a password')\n}\n\nfunction rejects(read: SmtpConfigReader, environment: NodeJS.ProcessEnv, scenario: string) {\n try {\n read(environment)\n } catch {\n return\n }\n throw new ConformanceError('SMTP configuration', `expected ${scenario} to be rejected`)\n}\n\nexport type DatabaseTarget = { provider: 'sqlite'; file: string } | { provider: 'postgres'; url: string }\n\nexport type DatabaseTargetResolver = (options: { databaseUrl?: string; sqliteFile: string }) => DatabaseTarget\n\nexport function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver) {\n const sqliteFile = '/data/application.sqlite'\n const sqlite = resolve({ sqliteFile })\n if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {\n throw new ConformanceError('default database target', 'expected the configured SQLite file')\n }\n\n for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {\n const target = resolve({ databaseUrl: url, sqliteFile })\n if (target.provider !== 'postgres' || target.url !== url) {\n throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`)\n }\n }\n\n try {\n resolve({ databaseUrl: 'https://database.example/application', sqliteFile })\n } catch {\n return\n }\n throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected')\n}\n\nexport function assertPostHogBrowserConformance(options: Record<string, unknown>) {\n if (typeof options.api_host !== 'string' || !options.api_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'api_host must be configured')\n }\n if (typeof options.ui_host !== 'string' || !options.ui_host.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'ui_host must be configured')\n }\n if (typeof options.defaults !== 'string' || !options.defaults.trim()) {\n throw new ConformanceError('PostHog browser initialization', 'SDK defaults must be pinned')\n }\n if (!options.capture_exceptions) {\n throw new ConformanceError('PostHog browser initialization', 'exception autocapture must be enabled')\n }\n if (options.capture_pageview !== 'history_change') {\n throw new ConformanceError('PostHog browser initialization', 'SPA pageviews must follow history changes')\n }\n if (options.person_profiles !== 'identified_only') {\n throw new ConformanceError('PostHog browser initialization', 'person profiles must be limited to identified users')\n }\n const recording = options.session_recording\n if (!recording || typeof recording !== 'object' || !('maskAllInputs' in recording) || recording.maskAllInputs !== true) {\n throw new ConformanceError('PostHog browser initialization', 'session replay must mask all inputs by default')\n }\n}\n\ntype PostHogContextParser = (\n request: Request,\n options?: { authenticatedDistinctId?: string; allowAnonymousDistinctId?: boolean },\n) => { distinctId?: string; sessionId?: string; properties: { $session_id?: string } }\n\nexport function assertPostHogRequestConformance(parse: PostHogContextParser) {\n const matched = parse(postHogRequest('person-123', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (matched.distinctId !== 'person-123' || matched.sessionId !== 'session-456' || matched.properties.$session_id !== 'session-456') {\n throw new ConformanceError('authenticated PostHog request', 'expected verified identity and session propagation')\n }\n const spoofed = parse(postHogRequest('attacker', 'session-456'), { authenticatedDistinctId: 'person-123' })\n if (spoofed.distinctId !== undefined) {\n throw new ConformanceError('spoofed PostHog request', 'unverified distinct id was trusted')\n }\n const malformed = parse(postHogRequest('person-123', 'x'.repeat(129)), { authenticatedDistinctId: 'person-123' })\n if (malformed.sessionId !== undefined || malformed.properties.$session_id !== undefined) {\n throw new ConformanceError('malformed PostHog request', 'unbounded session id was propagated')\n }\n}\n\nfunction postHogRequest(distinctId: string, sessionId: string) {\n return new Request('https://app.example/action', {\n headers: { 'x-posthog-distinct-id': distinctId, 'x-posthog-session-id': sessionId },\n })\n}\n\nasync function accepted(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch (error) {\n throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error })\n }\n}\n\nasync function rejected(scenario: string, work: () => void | Promise<void>) {\n try {\n await work()\n } catch {\n return\n }\n throw new ConformanceError(scenario, 'expected request to be rejected')\n}\n\nasync function responseBody(response: Response, scenario: string): Promise<Record<string, unknown>> {\n try {\n const body: unknown = await response.json()\n if (body && typeof body === 'object') return body as Record<string, unknown>\n } catch (error) {\n throw new ConformanceError(scenario, 'response body must be JSON', { cause: error })\n }\n throw new ConformanceError(scenario, 'response body must be an object')\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
2
2
  import postgres, { type Sql } from 'postgres';
3
+ import { type RateLimitStore } from '../server/rate-limit.js';
3
4
  export type PostgresClientOptions = NonNullable<Parameters<typeof postgres>[1]>;
4
5
  export type OpenDrizzlePostgresOptions<TSchema extends Record<string, unknown>> = {
5
6
  url: string;
@@ -13,4 +14,5 @@ export type DrizzlePostgresConnection<TSchema extends Record<string, unknown>> =
13
14
  export declare function openDrizzlePostgres<TSchema extends Record<string, unknown>>(options: OpenDrizzlePostgresOptions<TSchema>): DrizzlePostgresConnection<TSchema>;
14
15
  export declare function migrateDrizzlePostgres<TSchema extends Record<string, unknown>>(connection: DrizzlePostgresConnection<TSchema>, migrationsFolder: string): Promise<void>;
15
16
  export declare function closeDrizzlePostgres(connection: Pick<DrizzlePostgresConnection<Record<string, unknown>>, 'client'>, timeout?: number): Promise<void>;
17
+ export declare function postgresRateLimitStore(client: Sql, table?: string): RateLimitStore;
16
18
  export declare function redactedPostgresUrl(source: string): string;
@@ -1,6 +1,7 @@
1
1
  import { drizzle } from 'drizzle-orm/postgres-js';
2
2
  import { migrate } from 'drizzle-orm/postgres-js/migrator';
3
3
  import postgres, {} from 'postgres';
4
+ import { rateLimitTable } from '../server/rate-limit.js';
4
5
  export function openDrizzlePostgres(options) {
5
6
  const client = postgres(options.url, {
6
7
  max: 10,
@@ -20,6 +21,25 @@ export async function migrateDrizzlePostgres(connection, migrationsFolder) {
20
21
  export async function closeDrizzlePostgres(connection, timeout = 5) {
21
22
  await connection.client.end({ timeout });
22
23
  }
24
+ // One statement so concurrent replicas cannot both read a stale count and write the same increment.
25
+ export function postgresRateLimitStore(client, table = 'rate_limit') {
26
+ const name = client(rateLimitTable(table));
27
+ return {
28
+ async increment(key, windowSeconds, now) {
29
+ const resetAt = now + windowSeconds;
30
+ const [row] = await client `
31
+ insert into ${name} (key, count, reset_at) values (${key}, 1, ${resetAt})
32
+ on conflict (key) do update set
33
+ count = case when ${name}.reset_at <= ${now} then 1 else ${name}.count + 1 end,
34
+ reset_at = case when ${name}.reset_at <= ${now} then ${resetAt} else ${name}.reset_at end
35
+ returning count, reset_at
36
+ `;
37
+ if (!row)
38
+ throw new Error('rate limit increment returned no row');
39
+ return { count: row.count, resetAt: row.reset_at };
40
+ },
41
+ };
42
+ }
23
43
  export function redactedPostgresUrl(source) {
24
44
  const url = new URL(source);
25
45
  url.username = '';
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/database/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA2B,MAAM,yBAAyB,CAAA;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAA;AAC1D,OAAO,QAAQ,EAAE,EAAY,MAAM,UAAU,CAAA;AAe7C,MAAM,UAAU,mBAAmB,CACjC,OAA4C;IAE5C,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,GAAG,EAAE,CAAC,SAAS;QACzB,KAAK,EAAE;YACL,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;YAC1F,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;SAChG;QACD,GAAG,OAAO,CAAC,MAAM;KAClB,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA8C,EAC9C,gBAAwB;IAExB,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,UAA8E,EAAE,OAAO,GAAG,CAAC;IACpI,MAAM,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3B,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAA;IACjB,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAA;IACjB,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { migrate } from 'drizzle-orm/postgres-js/migrator'\nimport postgres, { type Sql } from 'postgres'\n\nexport type PostgresClientOptions = NonNullable<Parameters<typeof postgres>[1]>\n\nexport type OpenDrizzlePostgresOptions<TSchema extends Record<string, unknown>> = {\n url: string\n schema: TSchema\n client?: PostgresClientOptions\n}\n\nexport type DrizzlePostgresConnection<TSchema extends Record<string, unknown>> = {\n database: PostgresJsDatabase<TSchema>\n client: Sql\n}\n\nexport function openDrizzlePostgres<TSchema extends Record<string, unknown>>(\n options: OpenDrizzlePostgresOptions<TSchema>,\n): DrizzlePostgresConnection<TSchema> {\n const client = postgres(options.url, {\n max: 10,\n onnotice: () => undefined,\n types: {\n bigint: { to: 20, from: [20], serialize: (value: number) => String(value), parse: Number },\n numeric: { to: 1700, from: [1700], serialize: (value: number) => String(value), parse: Number },\n },\n ...options.client,\n })\n const database = drizzle(client, { schema: options.schema })\n return { database, client }\n}\n\nexport async function migrateDrizzlePostgres<TSchema extends Record<string, unknown>>(\n connection: DrizzlePostgresConnection<TSchema>,\n migrationsFolder: string,\n) {\n await migrate(connection.database, { migrationsFolder })\n}\n\nexport async function closeDrizzlePostgres(connection: Pick<DrizzlePostgresConnection<Record<string, unknown>>, 'client'>, timeout = 5) {\n await connection.client.end({ timeout })\n}\n\nexport function redactedPostgresUrl(source: string) {\n const url = new URL(source)\n url.username = ''\n url.password = ''\n return url.toString()\n}\n"]}
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/database/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA2B,MAAM,yBAAyB,CAAA;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAA;AAC1D,OAAO,QAAQ,EAAE,EAAY,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAuB,MAAM,yBAAyB,CAAA;AAe7E,MAAM,UAAU,mBAAmB,CACjC,OAA4C;IAE5C,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,GAAG,EAAE,CAAC,SAAS;QACzB,KAAK,EAAE;YACL,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;YAC1F,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;SAChG;QACD,GAAG,OAAO,CAAC,MAAM;KAClB,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA8C,EAC9C,gBAAwB;IAExB,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,UAA8E,EAAE,OAAO,GAAG,CAAC;IACpI,MAAM,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;AAC1C,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,sBAAsB,CAAC,MAAW,EAAE,KAAK,GAAG,YAAY;IACtE,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC1C,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG;YACrC,MAAM,OAAO,GAAG,GAAG,GAAG,aAAa,CAAA;YACnC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,MAAM,CAAuC;sBACjD,IAAI,mCAAmC,GAAG,QAAQ,OAAO;;8BAEjD,IAAI,gBAAgB,GAAG,gBAAgB,IAAI;iCACxC,IAAI,gBAAgB,GAAG,SAAS,OAAO,SAAS,IAAI;;OAE9E,CAAA;YACD,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;YACjE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAA;QACpD,CAAC;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3B,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAA;IACjB,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAA;IACjB,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { migrate } from 'drizzle-orm/postgres-js/migrator'\nimport postgres, { type Sql } from 'postgres'\nimport { rateLimitTable, type RateLimitStore } from '../server/rate-limit.js'\n\nexport type PostgresClientOptions = NonNullable<Parameters<typeof postgres>[1]>\n\nexport type OpenDrizzlePostgresOptions<TSchema extends Record<string, unknown>> = {\n url: string\n schema: TSchema\n client?: PostgresClientOptions\n}\n\nexport type DrizzlePostgresConnection<TSchema extends Record<string, unknown>> = {\n database: PostgresJsDatabase<TSchema>\n client: Sql\n}\n\nexport function openDrizzlePostgres<TSchema extends Record<string, unknown>>(\n options: OpenDrizzlePostgresOptions<TSchema>,\n): DrizzlePostgresConnection<TSchema> {\n const client = postgres(options.url, {\n max: 10,\n onnotice: () => undefined,\n types: {\n bigint: { to: 20, from: [20], serialize: (value: number) => String(value), parse: Number },\n numeric: { to: 1700, from: [1700], serialize: (value: number) => String(value), parse: Number },\n },\n ...options.client,\n })\n const database = drizzle(client, { schema: options.schema })\n return { database, client }\n}\n\nexport async function migrateDrizzlePostgres<TSchema extends Record<string, unknown>>(\n connection: DrizzlePostgresConnection<TSchema>,\n migrationsFolder: string,\n) {\n await migrate(connection.database, { migrationsFolder })\n}\n\nexport async function closeDrizzlePostgres(connection: Pick<DrizzlePostgresConnection<Record<string, unknown>>, 'client'>, timeout = 5) {\n await connection.client.end({ timeout })\n}\n\n// One statement so concurrent replicas cannot both read a stale count and write the same increment.\nexport function postgresRateLimitStore(client: Sql, table = 'rate_limit'): RateLimitStore {\n const name = client(rateLimitTable(table))\n return {\n async increment(key, windowSeconds, now) {\n const resetAt = now + windowSeconds\n const [row] = await client<{ count: number; reset_at: number }[]>`\n insert into ${name} (key, count, reset_at) values (${key}, 1, ${resetAt})\n on conflict (key) do update set\n count = case when ${name}.reset_at <= ${now} then 1 else ${name}.count + 1 end,\n reset_at = case when ${name}.reset_at <= ${now} then ${resetAt} else ${name}.reset_at end\n returning count, reset_at\n `\n if (!row) throw new Error('rate limit increment returned no row')\n return { count: row.count, resetAt: row.reset_at }\n },\n }\n}\n\nexport function redactedPostgresUrl(source: string) {\n const url = new URL(source)\n url.username = ''\n url.password = ''\n return url.toString()\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
3
+ import { type RateLimitStore } from '../server/rate-limit.js';
3
4
  export type SqlitePragmas = {
4
5
  journalMode?: 'DELETE' | 'TRUNCATE' | 'PERSIST' | 'MEMORY' | 'WAL' | 'OFF';
5
6
  synchronous?: 'OFF' | 'NORMAL' | 'FULL' | 'EXTRA';
@@ -23,3 +24,4 @@ export declare function openDrizzleSqlite<TSchema extends Record<string, unknown
23
24
  export declare function closeDrizzleSqlite(database: {
24
25
  $client: Database.Database;
25
26
  }): void;
27
+ export declare function sqliteRateLimitStore(client: Database.Database, table?: string): RateLimitStore;
@@ -3,6 +3,7 @@ import { drizzle } from 'drizzle-orm/better-sqlite3';
3
3
  import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
4
4
  import { mkdirSync } from 'node:fs';
5
5
  import path from 'node:path';
6
+ import { rateLimitTable } from '../server/rate-limit.js';
6
7
  export function openSqliteClient(file, options = {}) {
7
8
  if (file !== ':memory:')
8
9
  mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
@@ -46,4 +47,24 @@ export function openDrizzleSqlite(options) {
46
47
  export function closeDrizzleSqlite(database) {
47
48
  database.$client.close();
48
49
  }
50
+ // One statement so concurrent requests cannot both read a stale count and write the same increment.
51
+ export function sqliteRateLimitStore(client, table = 'rate_limit') {
52
+ const name = rateLimitTable(table);
53
+ const statement = client.prepare(`
54
+ insert into ${name} (key, count, reset_at) values (?, 1, ?)
55
+ on conflict(key) do update set
56
+ count = case when ${name}.reset_at <= ? then 1 else ${name}.count + 1 end,
57
+ reset_at = case when ${name}.reset_at <= ? then ? else ${name}.reset_at end
58
+ returning count, reset_at
59
+ `);
60
+ return {
61
+ increment(key, windowSeconds, now) {
62
+ const resetAt = now + windowSeconds;
63
+ const row = statement.get(key, resetAt, now, now, resetAt);
64
+ if (!row)
65
+ throw new Error('rate limit increment returned no row');
66
+ return { count: row.count, resetAt: row.reset_at };
67
+ },
68
+ };
69
+ }
49
70
  //# sourceMappingURL=sqlite.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/database/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AACrC,OAAO,EAAE,OAAO,EAA8B,MAAM,4BAA4B,CAAA;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,qCAAqC,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,IAAI,MAAM,WAAW,CAAA;AAc5B,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,OAAO,GAAsB,EAAE;IAC5E,IAAI,IAAI,KAAK,UAAU;QAAE,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACzF,MAAM,MAAM,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACnD,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK;YAAE,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;QACvE,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAyB,EAAE,OAAO,GAAkB,EAAE;IACpF,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,KAAK,CAAA;IAChD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,MAAM,CAAA;IACjD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAA;IAC/C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAA;IAC/C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;IACpH,MAAM,CAAC,MAAM,CAAC,kBAAkB,WAAW,EAAE,CAAC,CAAA;IAC9C,MAAM,CAAC,MAAM,CAAC,iBAAiB,WAAW,EAAE,CAAC,CAAA;IAC7C,MAAM,CAAC,MAAM,CAAC,kBAAkB,WAAW,EAAE,CAAC,CAAA;IAC9C,MAAM,CAAC,MAAM,CAAC,kBAAkB,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;IAC7D,OAAO,MAAM,CAAA;AACf,CAAC;AAQD,MAAM,UAAU,iBAAiB,CAC/B,OAA0C;IAE1C,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;QAC5D,IAAI,OAAO,CAAC,gBAAgB;YAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAA;QAC/F,OAAO,QAAQ,CAAA;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAwC;IACzE,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;AAC1B,CAAC","sourcesContent":["import Database from 'better-sqlite3'\nimport { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'\nimport { migrate } from 'drizzle-orm/better-sqlite3/migrator'\nimport { mkdirSync } from 'node:fs'\nimport path from 'node:path'\n\nexport type SqlitePragmas = {\n journalMode?: 'DELETE' | 'TRUNCATE' | 'PERSIST' | 'MEMORY' | 'WAL' | 'OFF'\n synchronous?: 'OFF' | 'NORMAL' | 'FULL' | 'EXTRA'\n busyTimeout?: number\n foreignKeys?: boolean\n}\n\nexport type OpenSqliteOptions = {\n database?: Database.Options\n pragmas?: SqlitePragmas | false\n}\n\nexport function openSqliteClient(file: string, options: OpenSqliteOptions = {}): Database.Database {\n if (file !== ':memory:') mkdirSync(path.dirname(path.resolve(file)), { recursive: true })\n const client = new Database(file, options.database)\n try {\n if (options.pragmas !== false) configureSqlite(client, options.pragmas)\n return client\n } catch (error) {\n client.close()\n throw error\n }\n}\n\nexport function configureSqlite(client: Database.Database, pragmas: SqlitePragmas = {}): Database.Database {\n const journalMode = pragmas.journalMode ?? 'WAL'\n const synchronous = pragmas.synchronous ?? 'FULL'\n const busyTimeout = pragmas.busyTimeout ?? 5000\n const foreignKeys = pragmas.foreignKeys ?? true\n if (!Number.isInteger(busyTimeout) || busyTimeout < 0) throw new Error('busyTimeout must be a non-negative integer')\n client.pragma(`journal_mode = ${journalMode}`)\n client.pragma(`synchronous = ${synchronous}`)\n client.pragma(`busy_timeout = ${busyTimeout}`)\n client.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`)\n return client\n}\n\nexport type OpenDrizzleSqliteOptions<TSchema extends Record<string, unknown>> = OpenSqliteOptions & {\n file: string\n schema: TSchema\n migrationsFolder?: string\n}\n\nexport function openDrizzleSqlite<TSchema extends Record<string, unknown>>(\n options: OpenDrizzleSqliteOptions<TSchema>,\n): BetterSQLite3Database<TSchema> & { $client: Database.Database } {\n const client = openSqliteClient(options.file, options)\n try {\n const database = drizzle({ client, schema: options.schema })\n if (options.migrationsFolder) migrate(database, { migrationsFolder: options.migrationsFolder })\n return database\n } catch (error) {\n client.close()\n throw error\n }\n}\n\nexport function closeDrizzleSqlite(database: { $client: Database.Database }) {\n database.$client.close()\n}\n"]}
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/database/sqlite.ts"],"names":[],"mappings":"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","sourcesContent":["import Database from 'better-sqlite3'\nimport { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'\nimport { migrate } from 'drizzle-orm/better-sqlite3/migrator'\nimport { mkdirSync } from 'node:fs'\nimport path from 'node:path'\nimport { rateLimitTable, type RateLimitStore } from '../server/rate-limit.js'\n\nexport type SqlitePragmas = {\n journalMode?: 'DELETE' | 'TRUNCATE' | 'PERSIST' | 'MEMORY' | 'WAL' | 'OFF'\n synchronous?: 'OFF' | 'NORMAL' | 'FULL' | 'EXTRA'\n busyTimeout?: number\n foreignKeys?: boolean\n}\n\nexport type OpenSqliteOptions = {\n database?: Database.Options\n pragmas?: SqlitePragmas | false\n}\n\nexport function openSqliteClient(file: string, options: OpenSqliteOptions = {}): Database.Database {\n if (file !== ':memory:') mkdirSync(path.dirname(path.resolve(file)), { recursive: true })\n const client = new Database(file, options.database)\n try {\n if (options.pragmas !== false) configureSqlite(client, options.pragmas)\n return client\n } catch (error) {\n client.close()\n throw error\n }\n}\n\nexport function configureSqlite(client: Database.Database, pragmas: SqlitePragmas = {}): Database.Database {\n const journalMode = pragmas.journalMode ?? 'WAL'\n const synchronous = pragmas.synchronous ?? 'FULL'\n const busyTimeout = pragmas.busyTimeout ?? 5000\n const foreignKeys = pragmas.foreignKeys ?? true\n if (!Number.isInteger(busyTimeout) || busyTimeout < 0) throw new Error('busyTimeout must be a non-negative integer')\n client.pragma(`journal_mode = ${journalMode}`)\n client.pragma(`synchronous = ${synchronous}`)\n client.pragma(`busy_timeout = ${busyTimeout}`)\n client.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`)\n return client\n}\n\nexport type OpenDrizzleSqliteOptions<TSchema extends Record<string, unknown>> = OpenSqliteOptions & {\n file: string\n schema: TSchema\n migrationsFolder?: string\n}\n\nexport function openDrizzleSqlite<TSchema extends Record<string, unknown>>(\n options: OpenDrizzleSqliteOptions<TSchema>,\n): BetterSQLite3Database<TSchema> & { $client: Database.Database } {\n const client = openSqliteClient(options.file, options)\n try {\n const database = drizzle({ client, schema: options.schema })\n if (options.migrationsFolder) migrate(database, { migrationsFolder: options.migrationsFolder })\n return database\n } catch (error) {\n client.close()\n throw error\n }\n}\n\nexport function closeDrizzleSqlite(database: { $client: Database.Database }) {\n database.$client.close()\n}\n\n// One statement so concurrent requests cannot both read a stale count and write the same increment.\nexport function sqliteRateLimitStore(client: Database.Database, table = 'rate_limit'): RateLimitStore {\n const name = rateLimitTable(table)\n const statement = client.prepare<[string, number, number, number, number], { count: number; reset_at: number }>(`\n insert into ${name} (key, count, reset_at) values (?, 1, ?)\n on conflict(key) do update set\n count = case when ${name}.reset_at <= ? then 1 else ${name}.count + 1 end,\n reset_at = case when ${name}.reset_at <= ? then ? else ${name}.reset_at end\n returning count, reset_at\n `)\n return {\n increment(key, windowSeconds, now) {\n const resetAt = now + windowSeconds\n const row = statement.get(key, resetAt, now, now, resetAt)\n if (!row) throw new Error('rate limit increment returned no row')\n return { count: row.count, resetAt: row.reset_at }\n },\n }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import { type TypeScriptPreset } from './init.js';
2
+ export type InitPrompts = {
3
+ confirm: (options: {
4
+ message: string;
5
+ default: boolean;
6
+ }) => Promise<boolean>;
7
+ select: (options: {
8
+ message: string;
9
+ choices: readonly TypeScriptPreset[];
10
+ }) => Promise<TypeScriptPreset>;
11
+ };
12
+ export declare function runInitCli(arguments_: string[], prompts?: InitPrompts): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import confirm from '@inquirer/confirm';
2
+ import select from '@inquirer/select';
3
+ import { applyRepositoryInit, INIT_STEPS, planRepositoryInit } from './init.js';
4
+ const PRESETS = ['library', 'bundler', 'node-bundler', 'browser', 'tanstack'];
5
+ const consolePrompts = {
6
+ confirm: (options) => confirm(options),
7
+ select: (options) => select({ message: options.message, choices: options.choices.map((value) => ({ value })) }),
8
+ };
9
+ export async function runInitCli(arguments_, prompts = consolePrompts) {
10
+ const unknown = arguments_.find((argument) => argument !== '--yes' && argument !== '--dry-run');
11
+ if (unknown) {
12
+ console.error('usage: ras init [--yes] [--dry-run]');
13
+ process.exitCode = 2;
14
+ return;
15
+ }
16
+ const accepted = arguments_.includes('--yes');
17
+ const dryRun = arguments_.includes('--dry-run');
18
+ if (!accepted && prompts === consolePrompts && !process.stdin.isTTY) {
19
+ console.error('ras init asks questions and needs a terminal. Pass --yes to accept every step instead.');
20
+ process.exitCode = 2;
21
+ return;
22
+ }
23
+ try {
24
+ const steps = [];
25
+ let preset = 'library';
26
+ for (const step of INIT_STEPS) {
27
+ // Each step is offered in turn so a repository can adopt one part without the rest.
28
+ // oxlint-disable-next-line no-await-in-loop
29
+ if (!accepted && !(await prompts.confirm({ message: `${step.title}? (${step.detail})`, default: true })))
30
+ continue;
31
+ steps.push(step.name);
32
+ // oxlint-disable-next-line no-await-in-loop
33
+ if (step.name === 'typescript' && !accepted)
34
+ preset = await prompts.select({ message: 'TypeScript preset', choices: PRESETS });
35
+ }
36
+ if (steps.length === 0) {
37
+ console.log('Nothing selected.');
38
+ return;
39
+ }
40
+ const planned = await planRepositoryInit(process.cwd(), { steps, typescriptPreset: preset });
41
+ const writable = accepted ? planned : await confirmOverwrites(prompts, planned);
42
+ if (dryRun) {
43
+ for (const file of writable)
44
+ console.log(`${file.existing === undefined ? 'create' : 'replace'} ${file.path}`);
45
+ console.log('Nothing written because --dry-run was requested.');
46
+ return;
47
+ }
48
+ for (const path of await applyRepositoryInit(process.cwd(), writable))
49
+ console.log(`wrote ${path}`);
50
+ if (steps.includes('policy'))
51
+ console.log('Run ras policy check to confirm the generated files stay in step.');
52
+ }
53
+ catch (error) {
54
+ // Ctrl-C during a question is a decision to stop, not a crash worth a stack trace.
55
+ if (error.name !== 'ExitPromptError')
56
+ throw error;
57
+ console.error('Nothing written.');
58
+ process.exitCode = 130;
59
+ }
60
+ }
61
+ async function confirmOverwrites(prompts, planned) {
62
+ const writable = [];
63
+ for (const file of planned) {
64
+ if (file.existing === undefined || file.existing === file.contents) {
65
+ writable.push(file);
66
+ continue;
67
+ }
68
+ // Replacing is the destructive branch, so it is asked one file at a time and defaults to keeping what is there.
69
+ // oxlint-disable-next-line no-await-in-loop
70
+ if (await prompts.confirm({ message: `${file.path} already exists. Replace it?`, default: false }))
71
+ writable.push(file);
72
+ }
73
+ return writable;
74
+ }
75
+ //# sourceMappingURL=init-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init-cli.js","sourceRoot":"","sources":["../../src/policy/init-cli.ts"],"names":[],"mappings":"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","sourcesContent":["import confirm from '@inquirer/confirm'\nimport select from '@inquirer/select'\nimport { applyRepositoryInit, INIT_STEPS, planRepositoryInit, type InitStepName, type PlannedFile, type TypeScriptPreset } from './init.js'\n\nconst PRESETS: readonly TypeScriptPreset[] = ['library', 'bundler', 'node-bundler', 'browser', 'tanstack']\n\nexport type InitPrompts = {\n confirm: (options: { message: string; default: boolean }) => Promise<boolean>\n select: (options: { message: string; choices: readonly TypeScriptPreset[] }) => Promise<TypeScriptPreset>\n}\n\nconst consolePrompts: InitPrompts = {\n confirm: (options) => confirm(options),\n select: (options) => select({ message: options.message, choices: options.choices.map((value) => ({ value })) }),\n}\n\nexport async function runInitCli(arguments_: string[], prompts: InitPrompts = consolePrompts): Promise<void> {\n const unknown = arguments_.find((argument) => argument !== '--yes' && argument !== '--dry-run')\n if (unknown) {\n console.error('usage: ras init [--yes] [--dry-run]')\n process.exitCode = 2\n return\n }\n const accepted = arguments_.includes('--yes')\n const dryRun = arguments_.includes('--dry-run')\n if (!accepted && prompts === consolePrompts && !process.stdin.isTTY) {\n console.error('ras init asks questions and needs a terminal. Pass --yes to accept every step instead.')\n process.exitCode = 2\n return\n }\n\n try {\n const steps: InitStepName[] = []\n let preset: TypeScriptPreset = 'library'\n for (const step of INIT_STEPS) {\n // Each step is offered in turn so a repository can adopt one part without the rest.\n // oxlint-disable-next-line no-await-in-loop\n if (!accepted && !(await prompts.confirm({ message: `${step.title}? (${step.detail})`, default: true }))) continue\n steps.push(step.name)\n // oxlint-disable-next-line no-await-in-loop\n if (step.name === 'typescript' && !accepted) preset = await prompts.select({ message: 'TypeScript preset', choices: PRESETS })\n }\n if (steps.length === 0) {\n console.log('Nothing selected.')\n return\n }\n\n const planned = await planRepositoryInit(process.cwd(), { steps, typescriptPreset: preset })\n const writable = accepted ? planned : await confirmOverwrites(prompts, planned)\n\n if (dryRun) {\n for (const file of writable) console.log(`${file.existing === undefined ? 'create' : 'replace'} ${file.path}`)\n console.log('Nothing written because --dry-run was requested.')\n return\n }\n\n for (const path of await applyRepositoryInit(process.cwd(), writable)) console.log(`wrote ${path}`)\n if (steps.includes('policy')) console.log('Run ras policy check to confirm the generated files stay in step.')\n } catch (error) {\n // Ctrl-C during a question is a decision to stop, not a crash worth a stack trace.\n if ((error as { name?: string }).name !== 'ExitPromptError') throw error\n console.error('Nothing written.')\n process.exitCode = 130\n }\n}\n\nasync function confirmOverwrites(prompts: InitPrompts, planned: readonly PlannedFile[]) {\n const writable: PlannedFile[] = []\n for (const file of planned) {\n if (file.existing === undefined || file.existing === file.contents) {\n writable.push(file)\n continue\n }\n // Replacing is the destructive branch, so it is asked one file at a time and defaults to keeping what is there.\n // oxlint-disable-next-line no-await-in-loop\n if (await prompts.confirm({ message: `${file.path} already exists. Replace it?`, default: false })) writable.push(file)\n }\n return writable\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { type AdoptionPolicy } from './index.js';
2
+ export type InitStepName = 'policy' | 'toolchain' | 'typescript' | 'oxlint' | 'workflow' | 'justfile';
3
+ export type InitStep = {
4
+ name: InitStepName;
5
+ title: string;
6
+ detail: string;
7
+ };
8
+ export type TypeScriptPreset = 'library' | 'bundler' | 'node-bundler' | 'browser' | 'tanstack';
9
+ export type InitAnswers = {
10
+ steps: readonly InitStepName[];
11
+ typescriptPreset?: TypeScriptPreset;
12
+ };
13
+ export type PlannedFile = {
14
+ path: string;
15
+ contents: string;
16
+ existing?: string;
17
+ };
18
+ export declare const INIT_ADOPTION: AdoptionPolicy;
19
+ export declare const INIT_STEPS: readonly InitStep[];
20
+ export declare function planRepositoryInit(root: string, answers: InitAnswers, release?: string): Promise<PlannedFile[]>;
21
+ export declare function applyRepositoryInit(root: string, files: readonly PlannedFile[]): Promise<string[]>;
@@ -0,0 +1,130 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { renderedPolicyFiles } from './index.js';
4
+ // Adoption values a generated repository starts from. `init.test.ts` holds these to this repository's own policy.
5
+ export const INIT_ADOPTION = {
6
+ node: '>=24 <25',
7
+ pnpm: '11.15.0',
8
+ just: '1.58.0',
9
+ };
10
+ export const INIT_STEPS = [
11
+ {
12
+ name: 'policy',
13
+ title: 'Repository policy',
14
+ detail: 'ras-stack.policy.json plus the Changesets, Dependabot, and pnpm files it generates',
15
+ },
16
+ { name: 'toolchain', title: 'Toolchain versions', detail: 'engines.node and packageManager in package.json' },
17
+ { name: 'typescript', title: 'TypeScript configuration', detail: 'tsconfig.json extending a shared preset' },
18
+ { name: 'oxlint', title: 'Lint configuration', detail: '.oxlintrc.json extending the shared rules' },
19
+ { name: 'workflow', title: 'CI workflow', detail: '.github/workflows/ci.yml calling the shared check workflow' },
20
+ { name: 'justfile', title: 'Command runner', detail: 'justfile wrapping the package scripts' },
21
+ ];
22
+ export async function planRepositoryInit(root, answers, release) {
23
+ const version = release ?? (await rasStackVersion());
24
+ const selected = new Set(answers.steps);
25
+ const planned = [];
26
+ if (selected.has('policy')) {
27
+ const policy = {
28
+ changesets: true,
29
+ dependabot: true,
30
+ pnpm: {},
31
+ adoption: { ...INIT_ADOPTION, minimumRasStackVersion: version, minimumWorkflowRasStackVersion: version },
32
+ };
33
+ planned.push({ path: 'ras-stack.policy.json', contents: `${JSON.stringify(policy, null, 2)}\n` });
34
+ for (const [path, contents] of await renderedPolicyFiles(root, policy))
35
+ planned.push({ path, contents });
36
+ }
37
+ if (selected.has('toolchain')) {
38
+ const manifest = await readJson(join(root, 'package.json'));
39
+ if (!manifest)
40
+ throw new Error('package.json is required before the toolchain can be configured');
41
+ const engines = manifest.engines && typeof manifest.engines === 'object' ? manifest.engines : {};
42
+ const configured = { ...manifest, engines: { ...engines, node: INIT_ADOPTION.node }, packageManager: `pnpm@${INIT_ADOPTION.pnpm}` };
43
+ planned.push({ path: 'package.json', contents: `${JSON.stringify(configured, null, 2)}\n` });
44
+ }
45
+ if (selected.has('typescript')) {
46
+ const preset = answers.typescriptPreset ?? 'library';
47
+ planned.push({
48
+ path: 'tsconfig.json',
49
+ contents: `${JSON.stringify({ extends: `./node_modules/ras-stack/config/typescript/${preset}.json`, compilerOptions: { types: ['node'] }, include: ['src/**/*.ts'] }, null, 2)}\n`,
50
+ });
51
+ }
52
+ if (selected.has('oxlint')) {
53
+ planned.push({
54
+ path: '.oxlintrc.json',
55
+ contents: `${JSON.stringify({ extends: ['./node_modules/ras-stack/config/oxlint.json'] }, null, 2)}\n`,
56
+ });
57
+ }
58
+ if (selected.has('workflow'))
59
+ planned.push({ path: '.github/workflows/ci.yml', contents: workflow(version) });
60
+ if (selected.has('justfile'))
61
+ planned.push({ path: 'justfile', contents: JUSTFILE });
62
+ await Promise.all(planned.map(async (file) => {
63
+ const existing = await readFile(join(root, file.path), 'utf8').catch(() => undefined);
64
+ if (existing !== undefined)
65
+ file.existing = existing;
66
+ }));
67
+ return planned;
68
+ }
69
+ export async function applyRepositoryInit(root, files) {
70
+ await Promise.all(files.map(async (file) => {
71
+ const absolute = join(root, file.path);
72
+ await mkdir(dirname(absolute), { recursive: true });
73
+ await writeFile(absolute, file.contents);
74
+ }));
75
+ return files.map((file) => file.path);
76
+ }
77
+ function workflow(version) {
78
+ return `name: CI
79
+
80
+ on:
81
+ pull_request:
82
+ push:
83
+ branches: [main]
84
+
85
+ permissions:
86
+ contents: read
87
+
88
+ jobs:
89
+ check:
90
+ uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v${version}
91
+ with:
92
+ command: just check
93
+ just-version: '${INIT_ADOPTION.just}'
94
+ `;
95
+ }
96
+ const JUSTFILE = `default:
97
+ @just --list
98
+
99
+ install:
100
+ corepack enable
101
+ pnpm install
102
+
103
+ format:
104
+ pnpm format
105
+
106
+ lint:
107
+ pnpm lint
108
+
109
+ typecheck:
110
+ pnpm typecheck
111
+
112
+ test *args:
113
+ pnpm exec vitest run {{ args }}
114
+
115
+ check:
116
+ pnpm check
117
+ `;
118
+ async function readJson(path) {
119
+ const source = await readFile(path, 'utf8').catch(() => undefined);
120
+ return source === undefined ? undefined : JSON.parse(source);
121
+ }
122
+ // The generated workflow pins the release doing the generating, so a new repository starts on a version that exists.
123
+ async function rasStackVersion() {
124
+ const manifest = await readJson(new URL('../../package.json', import.meta.url));
125
+ const version = manifest?.version;
126
+ if (typeof version !== 'string')
127
+ throw new Error('ras-stack version is unavailable');
128
+ return version;
129
+ }
130
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/policy/init.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { renderedPolicyFiles, type AdoptionPolicy } from './index.js'\n\nexport type InitStepName = 'policy' | 'toolchain' | 'typescript' | 'oxlint' | 'workflow' | 'justfile'\n\nexport type InitStep = { name: InitStepName; title: string; detail: string }\n\nexport type TypeScriptPreset = 'library' | 'bundler' | 'node-bundler' | 'browser' | 'tanstack'\n\nexport type InitAnswers = { steps: readonly InitStepName[]; typescriptPreset?: TypeScriptPreset }\n\nexport type PlannedFile = { path: string; contents: string; existing?: string }\n\n// Adoption values a generated repository starts from. `init.test.ts` holds these to this repository's own policy.\nexport const INIT_ADOPTION: AdoptionPolicy = {\n node: '>=24 <25',\n pnpm: '11.15.0',\n just: '1.58.0',\n}\n\nexport const INIT_STEPS: readonly InitStep[] = [\n {\n name: 'policy',\n title: 'Repository policy',\n detail: 'ras-stack.policy.json plus the Changesets, Dependabot, and pnpm files it generates',\n },\n { name: 'toolchain', title: 'Toolchain versions', detail: 'engines.node and packageManager in package.json' },\n { name: 'typescript', title: 'TypeScript configuration', detail: 'tsconfig.json extending a shared preset' },\n { name: 'oxlint', title: 'Lint configuration', detail: '.oxlintrc.json extending the shared rules' },\n { name: 'workflow', title: 'CI workflow', detail: '.github/workflows/ci.yml calling the shared check workflow' },\n { name: 'justfile', title: 'Command runner', detail: 'justfile wrapping the package scripts' },\n]\n\nexport async function planRepositoryInit(root: string, answers: InitAnswers, release?: string): Promise<PlannedFile[]> {\n const version = release ?? (await rasStackVersion())\n const selected = new Set(answers.steps)\n const planned: PlannedFile[] = []\n\n if (selected.has('policy')) {\n const policy = {\n changesets: true,\n dependabot: true,\n pnpm: {},\n adoption: { ...INIT_ADOPTION, minimumRasStackVersion: version, minimumWorkflowRasStackVersion: version },\n }\n planned.push({ path: 'ras-stack.policy.json', contents: `${JSON.stringify(policy, null, 2)}\\n` })\n for (const [path, contents] of await renderedPolicyFiles(root, policy)) planned.push({ path, contents })\n }\n\n if (selected.has('toolchain')) {\n const manifest = await readJson(join(root, 'package.json'))\n if (!manifest) throw new Error('package.json is required before the toolchain can be configured')\n const engines = manifest.engines && typeof manifest.engines === 'object' ? manifest.engines : {}\n const configured = { ...manifest, engines: { ...engines, node: INIT_ADOPTION.node }, packageManager: `pnpm@${INIT_ADOPTION.pnpm}` }\n planned.push({ path: 'package.json', contents: `${JSON.stringify(configured, null, 2)}\\n` })\n }\n\n if (selected.has('typescript')) {\n const preset = answers.typescriptPreset ?? 'library'\n planned.push({\n path: 'tsconfig.json',\n contents: `${JSON.stringify({ extends: `./node_modules/ras-stack/config/typescript/${preset}.json`, compilerOptions: { types: ['node'] }, include: ['src/**/*.ts'] }, null, 2)}\\n`,\n })\n }\n\n if (selected.has('oxlint')) {\n planned.push({\n path: '.oxlintrc.json',\n contents: `${JSON.stringify({ extends: ['./node_modules/ras-stack/config/oxlint.json'] }, null, 2)}\\n`,\n })\n }\n\n if (selected.has('workflow')) planned.push({ path: '.github/workflows/ci.yml', contents: workflow(version) })\n if (selected.has('justfile')) planned.push({ path: 'justfile', contents: JUSTFILE })\n\n await Promise.all(\n planned.map(async (file) => {\n const existing = await readFile(join(root, file.path), 'utf8').catch(() => undefined)\n if (existing !== undefined) file.existing = existing\n }),\n )\n return planned\n}\n\nexport async function applyRepositoryInit(root: string, files: readonly PlannedFile[]) {\n await Promise.all(\n files.map(async (file) => {\n const absolute = join(root, file.path)\n await mkdir(dirname(absolute), { recursive: true })\n await writeFile(absolute, file.contents)\n }),\n )\n return files.map((file) => file.path)\n}\n\nfunction workflow(version: string) {\n return `name: CI\n\non:\n pull_request:\n push:\n branches: [main]\n\npermissions:\n contents: read\n\njobs:\n check:\n uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v${version}\n with:\n command: just check\n just-version: '${INIT_ADOPTION.just}'\n`\n}\n\nconst JUSTFILE = `default:\n @just --list\n\ninstall:\n corepack enable\n pnpm install\n\nformat:\n pnpm format\n\nlint:\n pnpm lint\n\ntypecheck:\n pnpm typecheck\n\ntest *args:\n pnpm exec vitest run {{ args }}\n\ncheck:\n pnpm check\n`\n\nasync function readJson(path: string | URL) {\n const source = await readFile(path, 'utf8').catch(() => undefined)\n return source === undefined ? undefined : (JSON.parse(source) as Record<string, unknown>)\n}\n\n// The generated workflow pins the release doing the generating, so a new repository starts on a version that exists.\nasync function rasStackVersion() {\n const manifest = await readJson(new URL('../../package.json', import.meta.url))\n const version = manifest?.version\n if (typeof version !== 'string') throw new Error('ras-stack version is unavailable')\n return version\n}\n"]}
@@ -4,6 +4,8 @@ export { errorHasCode, infrastructureDiagnostic, infrastructureFailure, Infrastr
4
4
  export type { InfrastructureFailure } from './errors.js';
5
5
  export { databaseHealthFailure, healthResponse } from './health.js';
6
6
  export type { HealthResponseOptions } from './health.js';
7
+ export { createRateLimit, forwardedClientAddress, memoryRateLimitStore, rateLimitTable } from './rate-limit.js';
8
+ export type { RateLimitCounter, RateLimitDecision, RateLimitOptions, RateLimitStore } from './rate-limit.js';
7
9
  export { createRpc } from './rpc.js';
8
10
  export type { RpcErrorContext, RpcLogger, RpcOptions } from './rpc.js';
9
11
  export { clearGlobalSingleton, globalAsyncSingleton, globalSingleton, peekGlobalSingleton } from './singleton.js';
@@ -1,6 +1,7 @@
1
1
  export { canonicalRedirect } from './canonical-host.js';
2
2
  export { errorHasCode, infrastructureDiagnostic, infrastructureFailure, InfrastructureError, safeInfrastructureError } from './errors.js';
3
3
  export { databaseHealthFailure, healthResponse } from './health.js';
4
+ export { createRateLimit, forwardedClientAddress, memoryRateLimitStore, rateLimitTable } from './rate-limit.js';
4
5
  export { createRpc } from './rpc.js';
5
6
  export { clearGlobalSingleton, globalAsyncSingleton, globalSingleton, peekGlobalSingleton } from './singleton.js';
6
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,EAAE,YAAY,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAEzI,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEpC,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA","sourcesContent":["export { canonicalRedirect } from './canonical-host.js'\nexport type { CanonicalRedirectOptions } from './canonical-host.js'\nexport { errorHasCode, infrastructureDiagnostic, infrastructureFailure, InfrastructureError, safeInfrastructureError } from './errors.js'\nexport type { InfrastructureFailure } from './errors.js'\nexport { databaseHealthFailure, healthResponse } from './health.js'\nexport type { HealthResponseOptions } from './health.js'\nexport { createRpc } from './rpc.js'\nexport type { RpcErrorContext, RpcLogger, RpcOptions } from './rpc.js'\nexport { clearGlobalSingleton, globalAsyncSingleton, globalSingleton, peekGlobalSingleton } from './singleton.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,EAAE,YAAY,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAEzI,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAE/G,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEpC,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA","sourcesContent":["export { canonicalRedirect } from './canonical-host.js'\nexport type { CanonicalRedirectOptions } from './canonical-host.js'\nexport { errorHasCode, infrastructureDiagnostic, infrastructureFailure, InfrastructureError, safeInfrastructureError } from './errors.js'\nexport type { InfrastructureFailure } from './errors.js'\nexport { databaseHealthFailure, healthResponse } from './health.js'\nexport type { HealthResponseOptions } from './health.js'\nexport { createRateLimit, forwardedClientAddress, memoryRateLimitStore, rateLimitTable } from './rate-limit.js'\nexport type { RateLimitCounter, RateLimitDecision, RateLimitOptions, RateLimitStore } from './rate-limit.js'\nexport { createRpc } from './rpc.js'\nexport type { RpcErrorContext, RpcLogger, RpcOptions } from './rpc.js'\nexport { clearGlobalSingleton, globalAsyncSingleton, globalSingleton, peekGlobalSingleton } from './singleton.js'\n"]}
@@ -0,0 +1,30 @@
1
+ import type { RateLimitRule } from '../auth/settings.js';
2
+ export type RateLimitCounter = {
3
+ count: number;
4
+ resetAt: number;
5
+ };
6
+ export type RateLimitStore = {
7
+ increment: (key: string, windowSeconds: number, now: number) => Promise<RateLimitCounter> | RateLimitCounter;
8
+ };
9
+ export type RateLimitDecision = {
10
+ key: string;
11
+ remaining: number;
12
+ resetAt: number;
13
+ };
14
+ export type RateLimitOptions = {
15
+ store: RateLimitStore;
16
+ rule: RateLimitRule;
17
+ identify: (request: Request) => string | undefined;
18
+ scope?: string;
19
+ onUnidentified?: 'reject' | 'allow';
20
+ onUnavailable?: 'reject' | 'allow';
21
+ now?: () => number;
22
+ };
23
+ export declare function createRateLimit(options: RateLimitOptions): (request: Request) => Promise<RateLimitDecision | undefined>;
24
+ export declare function memoryRateLimitStore(options?: {
25
+ maxKeys?: number;
26
+ }): RateLimitStore;
27
+ export declare function rateLimitTable(name: string): string;
28
+ export declare function forwardedClientAddress(request: Request, options?: {
29
+ trustForwardedHeaders?: boolean;
30
+ }): string | undefined;
@@ -0,0 +1,73 @@
1
+ import { InfrastructureError } from './errors.js';
2
+ export function createRateLimit(options) {
3
+ const scope = options.scope ?? 'default';
4
+ const { window, max } = options.rule;
5
+ if (!Number.isSafeInteger(window) || window < 1)
6
+ throw new RangeError('rate limit window must be a positive integer');
7
+ if (!Number.isSafeInteger(max) || max < 1)
8
+ throw new RangeError('rate limit max must be a positive integer');
9
+ return async function limit(request) {
10
+ const now = Math.floor((options.now?.() ?? Date.now()) / 1000);
11
+ const identity = options.identify(request);
12
+ if (!identity) {
13
+ if ((options.onUnidentified ?? 'reject') === 'allow')
14
+ return undefined;
15
+ throw rejection('unidentified client rejected', 403, window);
16
+ }
17
+ const key = `${scope}:${identity}`;
18
+ let counter;
19
+ try {
20
+ counter = await options.store.increment(key, window, now);
21
+ }
22
+ catch (error) {
23
+ if ((options.onUnavailable ?? 'reject') === 'allow')
24
+ return undefined;
25
+ // A store outage is infrastructure rather than a client fault, so it classifies with everything else that is retryable.
26
+ throw new InfrastructureError('rate_limit_unavailable', 'rate limit is temporarily unavailable', { cause: error, retryable: true });
27
+ }
28
+ if (counter.count > max)
29
+ throw rejection('rate limit exceeded', 429, Math.max(counter.resetAt - now, 1));
30
+ return { key, remaining: max - counter.count, resetAt: counter.resetAt };
31
+ };
32
+ }
33
+ // Counters live in one process, so this is for development and tests; replicas each enforce their own budget.
34
+ export function memoryRateLimitStore(options = {}) {
35
+ const maxKeys = options.maxKeys ?? 10_000;
36
+ const counters = new Map();
37
+ return {
38
+ increment(key, windowSeconds, now) {
39
+ const existing = counters.get(key);
40
+ if (existing && existing.resetAt > now) {
41
+ existing.count += 1;
42
+ return existing;
43
+ }
44
+ if (counters.size >= maxKeys) {
45
+ for (const [candidate, counter] of counters)
46
+ if (counter.resetAt <= now)
47
+ counters.delete(candidate);
48
+ // Expiring nothing under a burst of distinct keys would otherwise grow the map without a bound.
49
+ while (counters.size >= maxKeys)
50
+ counters.delete(counters.keys().next().value);
51
+ }
52
+ const counter = { count: 1, resetAt: now + windowSeconds };
53
+ counters.set(key, counter);
54
+ return counter;
55
+ },
56
+ };
57
+ }
58
+ // Stores interpolate the table into SQL, and only better-sqlite3 lacks an identifier escape, so the name is validated for both.
59
+ export function rateLimitTable(name) {
60
+ if (!/^[a-z_][a-z\d_]*$/i.test(name))
61
+ throw new Error('rate limit table must be an unquoted SQL identifier');
62
+ return name;
63
+ }
64
+ // A forwarded address is only as trustworthy as the proxy that set it, so reading it stays opt-in.
65
+ export function forwardedClientAddress(request, options = {}) {
66
+ if (!options.trustForwardedHeaders)
67
+ return undefined;
68
+ return request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || undefined;
69
+ }
70
+ function rejection(message, status, retryAfterSeconds) {
71
+ return new Response(message, { status, headers: { 'retry-after': String(retryAfterSeconds) } });
72
+ }
73
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/server/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAoBjD,MAAM,UAAU,eAAe,CAAC,OAAyB;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,SAAS,CAAA;IACxC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAA;IACpC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,8CAA8C,CAAC,CAAA;IACrH,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,2CAA2C,CAAC,CAAA;IAE5G,OAAO,KAAK,UAAU,KAAK,CAAC,OAAgB;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAA;QAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,QAAQ,CAAC,KAAK,OAAO;gBAAE,OAAO,SAAS,CAAA;YACtE,MAAM,SAAS,CAAC,8BAA8B,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;QAC9D,CAAC;QAED,MAAM,GAAG,GAAG,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAA;QAClC,IAAI,OAAyB,CAAA;QAC7B,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QAC3D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,QAAQ,CAAC,KAAK,OAAO;gBAAE,OAAO,SAAS,CAAA;YACrE,wHAAwH;YACxH,MAAM,IAAI,mBAAmB,CAAC,wBAAwB,EAAE,uCAAuC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACrI,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,GAAG,GAAG;YAAE,MAAM,SAAS,CAAC,qBAAqB,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,CAAA;QACxG,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1E,CAAC,CAAA;AACH,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,oBAAoB,CAAC,OAAO,GAAyB,EAAE;IACrE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAA;IACzC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAA;IACpD,OAAO;QACL,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG;YAC/B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAClC,IAAI,QAAQ,IAAI,QAAQ,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC;gBACvC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAA;gBACnB,OAAO,QAAQ,CAAA;YACjB,CAAC;YACD,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC7B,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,QAAQ;oBAAE,IAAI,OAAO,CAAC,OAAO,IAAI,GAAG;wBAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;gBACnG,gGAAgG;gBAChG,OAAO,QAAQ,CAAC,IAAI,IAAI,OAAO;oBAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAM,CAAC,CAAA;YACjF,CAAC;YACD,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,aAAa,EAAE,CAAA;YAC1D,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;YAC1B,OAAO,OAAO,CAAA;QAChB,CAAC;KACF,CAAA;AACH,CAAC;AAED,gIAAgI;AAChI,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;IAC5G,OAAO,IAAI,CAAA;AACb,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,sBAAsB,CAAC,OAAgB,EAAE,OAAO,GAAwC,EAAE;IACxG,IAAI,CAAC,OAAO,CAAC,qBAAqB;QAAE,OAAO,SAAS,CAAA;IACpD,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAA;AACnF,CAAC;AAED,SAAS,SAAS,CAAC,OAAe,EAAE,MAAc,EAAE,iBAAyB;IAC3E,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC,CAAA;AACjG,CAAC","sourcesContent":["import type { RateLimitRule } from '../auth/settings.js'\nimport { InfrastructureError } from './errors.js'\n\nexport type RateLimitCounter = { count: number; resetAt: number }\n\nexport type RateLimitStore = {\n increment: (key: string, windowSeconds: number, now: number) => Promise<RateLimitCounter> | RateLimitCounter\n}\n\nexport type RateLimitDecision = { key: string; remaining: number; resetAt: number }\n\nexport type RateLimitOptions = {\n store: RateLimitStore\n rule: RateLimitRule\n identify: (request: Request) => string | undefined\n scope?: string\n onUnidentified?: 'reject' | 'allow'\n onUnavailable?: 'reject' | 'allow'\n now?: () => number\n}\n\nexport function createRateLimit(options: RateLimitOptions) {\n const scope = options.scope ?? 'default'\n const { window, max } = options.rule\n if (!Number.isSafeInteger(window) || window < 1) throw new RangeError('rate limit window must be a positive integer')\n if (!Number.isSafeInteger(max) || max < 1) throw new RangeError('rate limit max must be a positive integer')\n\n return async function limit(request: Request): Promise<RateLimitDecision | undefined> {\n const now = Math.floor((options.now?.() ?? Date.now()) / 1000)\n const identity = options.identify(request)\n if (!identity) {\n if ((options.onUnidentified ?? 'reject') === 'allow') return undefined\n throw rejection('unidentified client rejected', 403, window)\n }\n\n const key = `${scope}:${identity}`\n let counter: RateLimitCounter\n try {\n counter = await options.store.increment(key, window, now)\n } catch (error) {\n if ((options.onUnavailable ?? 'reject') === 'allow') return undefined\n // A store outage is infrastructure rather than a client fault, so it classifies with everything else that is retryable.\n throw new InfrastructureError('rate_limit_unavailable', 'rate limit is temporarily unavailable', { cause: error, retryable: true })\n }\n\n if (counter.count > max) throw rejection('rate limit exceeded', 429, Math.max(counter.resetAt - now, 1))\n return { key, remaining: max - counter.count, resetAt: counter.resetAt }\n }\n}\n\n// Counters live in one process, so this is for development and tests; replicas each enforce their own budget.\nexport function memoryRateLimitStore(options: { maxKeys?: number } = {}): RateLimitStore {\n const maxKeys = options.maxKeys ?? 10_000\n const counters = new Map<string, RateLimitCounter>()\n return {\n increment(key, windowSeconds, now) {\n const existing = counters.get(key)\n if (existing && existing.resetAt > now) {\n existing.count += 1\n return existing\n }\n if (counters.size >= maxKeys) {\n for (const [candidate, counter] of counters) if (counter.resetAt <= now) counters.delete(candidate)\n // Expiring nothing under a burst of distinct keys would otherwise grow the map without a bound.\n while (counters.size >= maxKeys) counters.delete(counters.keys().next().value!)\n }\n const counter = { count: 1, resetAt: now + windowSeconds }\n counters.set(key, counter)\n return counter\n },\n }\n}\n\n// Stores interpolate the table into SQL, and only better-sqlite3 lacks an identifier escape, so the name is validated for both.\nexport function rateLimitTable(name: string) {\n if (!/^[a-z_][a-z\\d_]*$/i.test(name)) throw new Error('rate limit table must be an unquoted SQL identifier')\n return name\n}\n\n// A forwarded address is only as trustworthy as the proxy that set it, so reading it stays opt-in.\nexport function forwardedClientAddress(request: Request, options: { trustForwardedHeaders?: boolean } = {}) {\n if (!options.trustForwardedHeaders) return undefined\n return request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || undefined\n}\n\nfunction rejection(message: string, status: number, retryAfterSeconds: number) {\n return new Response(message, { status, headers: { 'retry-after': String(retryAfterSeconds) } })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -152,6 +152,8 @@
152
152
  "version-packages": "changeset version"
153
153
  },
154
154
  "dependencies": {
155
+ "@inquirer/confirm": "^6.1.1",
156
+ "@inquirer/select": "^5.2.1",
155
157
  "yaml": "^2.8.2"
156
158
  },
157
159
  "devDependencies": {