ras-stack 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/conformance/index.d.ts +25 -0
- package/dist/conformance/index.js +125 -0
- package/dist/conformance/index.js.map +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -170,6 +170,15 @@ if (target.provider === 'postgres') {
|
|
|
170
170
|
|
|
171
171
|
The package does not make SQLite and PostgreSQL queries look identical. Applications keep driver-specific transaction and compatibility behavior while sharing target validation, pool defaults, numeric parsing, migrations, credential-safe display URLs, and shutdown.
|
|
172
172
|
|
|
173
|
+
Consumer tests can verify the real provider selection and provider-specific safety settings without sharing a schema or repository:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { assertDatabaseTargetConformance, assertSqliteConformance } from 'ras-stack/conformance'
|
|
177
|
+
|
|
178
|
+
await assertDatabaseTargetConformance(databaseTarget)
|
|
179
|
+
await assertSqliteConformance((name) => sqliteClient.pragma(name, { simple: true }))
|
|
180
|
+
```
|
|
181
|
+
|
|
173
182
|
## Realtime updates
|
|
174
183
|
|
|
175
184
|
Applications choose their channel names, authorize subscriptions, and define payloads. `ras-stack` handles Centrifugo's HTTP publication, signed tokens, and repeated browser lifecycle mechanics:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ConformanceError extends Error {
|
|
2
|
+
readonly scenario: string;
|
|
3
|
+
constructor(scenario: string, message: string, options?: {
|
|
4
|
+
cause?: unknown;
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export declare function assertMutationOriginConformance(guard: (request: Request) => void | Promise<void>, options?: {
|
|
8
|
+
trustForwardedHeaders?: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export type HealthHandlerFactory = (check: () => void | Promise<void>) => () => Response | Promise<Response>;
|
|
11
|
+
export declare function assertHealthHandlerConformance(createHandler: HealthHandlerFactory): Promise<void>;
|
|
12
|
+
export type SqlitePragmaReader = (name: 'journal_mode' | 'synchronous' | 'busy_timeout' | 'foreign_keys') => unknown;
|
|
13
|
+
export declare function assertSqliteConformance(readPragma: SqlitePragmaReader): Promise<void>;
|
|
14
|
+
export type DatabaseTarget = {
|
|
15
|
+
provider: 'sqlite';
|
|
16
|
+
file: string;
|
|
17
|
+
} | {
|
|
18
|
+
provider: 'postgres';
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
export type DatabaseTargetResolver = (options: {
|
|
22
|
+
databaseUrl?: string;
|
|
23
|
+
sqliteFile: string;
|
|
24
|
+
}) => DatabaseTarget;
|
|
25
|
+
export declare function assertDatabaseTargetConformance(resolve: DatabaseTargetResolver): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export class ConformanceError extends Error {
|
|
2
|
+
scenario;
|
|
3
|
+
constructor(scenario, message, options = {}) {
|
|
4
|
+
super(`${scenario}: ${message}`, options.cause === undefined ? undefined : { cause: options.cause });
|
|
5
|
+
this.scenario = scenario;
|
|
6
|
+
this.name = 'ConformanceError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function assertMutationOriginConformance(guard, options = {}) {
|
|
10
|
+
await accepted('same-origin request', () => guard(new Request('https://app.example/action', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { origin: 'https://app.example', 'sec-fetch-site': 'same-origin' },
|
|
13
|
+
})));
|
|
14
|
+
await rejected('cross-origin request', () => guard(new Request('https://app.example/action', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { origin: 'https://attacker.example', 'sec-fetch-site': 'cross-site' },
|
|
17
|
+
})));
|
|
18
|
+
await rejected('missing-origin request', () => guard(new Request('https://app.example/action', { method: 'POST' })));
|
|
19
|
+
if (!options.trustForwardedHeaders) {
|
|
20
|
+
await rejected('spoofed forwarded-origin request', () => guard(new Request('https://app.example/action', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
origin: 'https://attacker.example',
|
|
24
|
+
'x-forwarded-host': 'attacker.example',
|
|
25
|
+
'x-forwarded-proto': 'https',
|
|
26
|
+
},
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function assertHealthHandlerConformance(createHandler) {
|
|
31
|
+
const healthy = await createHandler(() => undefined)();
|
|
32
|
+
if (healthy.status !== 200)
|
|
33
|
+
throw new ConformanceError('healthy dependency', `expected status 200, received ${healthy.status}`);
|
|
34
|
+
const healthyBody = await responseBody(healthy, 'healthy dependency');
|
|
35
|
+
if (healthyBody.ok !== true)
|
|
36
|
+
throw new ConformanceError('healthy dependency', 'response body must contain ok: true');
|
|
37
|
+
const privateMessage = 'password=private-diagnostic';
|
|
38
|
+
const unavailable = await createHandler(() => Promise.reject(new Error(privateMessage)))();
|
|
39
|
+
if (unavailable.status !== 503) {
|
|
40
|
+
throw new ConformanceError('unavailable dependency', `expected status 503, received ${unavailable.status}`);
|
|
41
|
+
}
|
|
42
|
+
const unavailableText = await unavailable.text();
|
|
43
|
+
if (unavailableText.includes(privateMessage)) {
|
|
44
|
+
throw new ConformanceError('unavailable dependency', 'response exposed the private diagnostic message');
|
|
45
|
+
}
|
|
46
|
+
let unavailableBody;
|
|
47
|
+
try {
|
|
48
|
+
unavailableBody = JSON.parse(unavailableText);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new ConformanceError('unavailable dependency', 'response body must be JSON', { cause: error });
|
|
52
|
+
}
|
|
53
|
+
if (!unavailableBody || typeof unavailableBody !== 'object' || !('ok' in unavailableBody) || unavailableBody.ok !== false) {
|
|
54
|
+
throw new ConformanceError('unavailable dependency', 'response body must contain ok: false');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function assertSqliteConformance(readPragma) {
|
|
58
|
+
const values = {
|
|
59
|
+
journalMode: String(await readPragma('journal_mode')).toLowerCase(),
|
|
60
|
+
synchronous: Number(await readPragma('synchronous')),
|
|
61
|
+
busyTimeout: Number(await readPragma('busy_timeout')),
|
|
62
|
+
foreignKeys: Number(await readPragma('foreign_keys')),
|
|
63
|
+
};
|
|
64
|
+
if (values.journalMode !== 'wal' && values.journalMode !== 'memory') {
|
|
65
|
+
throw new ConformanceError('SQLite journal mode', `expected wal or memory, received ${values.journalMode}`);
|
|
66
|
+
}
|
|
67
|
+
if (values.synchronous !== 2) {
|
|
68
|
+
throw new ConformanceError('SQLite synchronous mode', `expected FULL (2), received ${values.synchronous}`);
|
|
69
|
+
}
|
|
70
|
+
if (values.busyTimeout !== 5000) {
|
|
71
|
+
throw new ConformanceError('SQLite busy timeout', `expected 5000, received ${values.busyTimeout}`);
|
|
72
|
+
}
|
|
73
|
+
if (values.foreignKeys !== 1) {
|
|
74
|
+
throw new ConformanceError('SQLite foreign keys', `expected enabled (1), received ${values.foreignKeys}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function assertDatabaseTargetConformance(resolve) {
|
|
78
|
+
const sqliteFile = '/data/application.sqlite';
|
|
79
|
+
const sqlite = resolve({ sqliteFile });
|
|
80
|
+
if (sqlite.provider !== 'sqlite' || sqlite.file !== sqliteFile) {
|
|
81
|
+
throw new ConformanceError('default database target', 'expected the configured SQLite file');
|
|
82
|
+
}
|
|
83
|
+
for (const url of ['postgres://user:secret@database/application', 'postgresql://user:secret@database/application']) {
|
|
84
|
+
const target = resolve({ databaseUrl: url, sqliteFile });
|
|
85
|
+
if (target.provider !== 'postgres' || target.url !== url) {
|
|
86
|
+
throw new ConformanceError('PostgreSQL database target', `expected the configured ${new URL(url).protocol} URL`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
resolve({ databaseUrl: 'https://database.example/application', sqliteFile });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw new ConformanceError('invalid database target', 'expected a non-PostgreSQL URL to be rejected');
|
|
96
|
+
}
|
|
97
|
+
async function accepted(scenario, work) {
|
|
98
|
+
try {
|
|
99
|
+
await work();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new ConformanceError(scenario, 'expected request to be accepted', { cause: error });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function rejected(scenario, work) {
|
|
106
|
+
try {
|
|
107
|
+
await work();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw new ConformanceError(scenario, 'expected request to be rejected');
|
|
113
|
+
}
|
|
114
|
+
async function responseBody(response, scenario) {
|
|
115
|
+
try {
|
|
116
|
+
const body = await response.json();
|
|
117
|
+
if (body && typeof body === 'object')
|
|
118
|
+
return body;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new ConformanceError(scenario, 'response body must be JSON', { cause: error });
|
|
122
|
+
}
|
|
123
|
+
throw new ConformanceError(scenario, 'response body must be an object');
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 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\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ras-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Composable full-stack primitives shared across Richard Solomou's applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"types": "./dist/database/postgres.d.ts",
|
|
60
60
|
"default": "./dist/database/postgres.js"
|
|
61
61
|
},
|
|
62
|
+
"./conformance": {
|
|
63
|
+
"types": "./dist/conformance/index.d.ts",
|
|
64
|
+
"default": "./dist/conformance/index.js"
|
|
65
|
+
},
|
|
62
66
|
"./policy": {
|
|
63
67
|
"types": "./dist/policy/index.d.ts",
|
|
64
68
|
"default": "./dist/policy/index.js"
|