@fuzdev/fuz_app 0.107.0 → 0.109.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 +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +1 -1
- package/dist/auth/account_route_schema.d.ts +10 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +10 -1
- package/dist/auth/account_routes.d.ts +18 -4
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +32 -29
- package/dist/auth/api_token_queries.d.ts +4 -0
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +5 -1
- package/dist/auth/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- package/dist/auth/bearer_auth.d.ts +9 -6
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +12 -21
- package/dist/auth/bootstrap_routes.d.ts +12 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +26 -10
- package/dist/auth/middleware.d.ts +0 -3
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +11 -10
- package/dist/auth/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +45 -4
- package/dist/auth/session_queries.d.ts +13 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +14 -3
- package/dist/auth/signup_routes.d.ts +10 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -11
- package/dist/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- package/dist/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +13 -2
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +13 -2
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/rate_limiter.d.ts +36 -3
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +36 -3
- package/dist/server/app_server.d.ts +27 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +30 -20
- package/dist/server/app_server_context.d.ts +10 -2
- package/dist/server/app_server_context.d.ts.map +1 -1
- package/dist/testing/CLAUDE.md +42 -8
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +6 -4
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +9 -8
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +59 -0
- package/dist/testing/cross_backend/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- package/dist/testing/cross_backend/session_cap.d.ts +19 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/session_cap.js +93 -0
- package/dist/testing/middleware.d.ts +2 -5
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +5 -5
- package/dist/testing/rate_limiting.d.ts +10 -16
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +11 -95
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +3 -1
- package/dist/testing/testing_rate_limiter.d.ts +7 -1
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -1
- package/dist/testing/testing_rate_limiter.js +7 -1
- package/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- package/package.json +1 -1
|
@@ -24,6 +24,13 @@ import '../assert_dev_env.js';
|
|
|
24
24
|
* not `429`). A backend that ignored XFF and keyed on the (loopback) TCP
|
|
25
25
|
* peer would 429 the fresh-IP request too — so the `401` proves the limiter
|
|
26
26
|
* keys on the resolved `X-Forwarded-For` client IP.
|
|
27
|
+
* - **a success does not refund the per-IP budget** — the distributed-spray
|
|
28
|
+
* backstop. Failed guesses against *distinct* usernames from one forwarded
|
|
29
|
+
* IP, interleaved with the attacker's own successful logins, must still 429
|
|
30
|
+
* within a bounded total-request budget. Both spines used to clear the IP
|
|
31
|
+
* bucket on success, which made that budget unbounded: one credential (or,
|
|
32
|
+
* under open signup, none) bought unlimited guesses against arbitrary victims
|
|
33
|
+
* from a single address. Only the account-grain bucket is forgiven now.
|
|
27
34
|
*
|
|
28
35
|
* **Determinism without a limiter reset.** Limiter state is in-memory and the
|
|
29
36
|
* per-test `_testing_reset` wipes only the DB, never the buckets — so each
|
|
@@ -58,6 +65,7 @@ import { default_login_account_rate_limit, default_login_ip_rate_limit } from '.
|
|
|
58
65
|
const XFF_IP_LIMIT = '203.0.113.1';
|
|
59
66
|
const XFF_SEGREGATION_EXHAUST = '203.0.113.2';
|
|
60
67
|
const XFF_SEGREGATION_FRESH = '203.0.113.3';
|
|
68
|
+
const XFF_SPRAY = '203.0.113.4';
|
|
61
69
|
/** A non-routable, non-existent identifier — login resolves to the not-found path (records the bucket, returns 401). */
|
|
62
70
|
const PROBE_PASSWORD = 'login_security_wrong_password';
|
|
63
71
|
export const describe_login_security_cross_tests = (options) => {
|
|
@@ -93,6 +101,20 @@ export const describe_login_security_cross_tests = (options) => {
|
|
|
93
101
|
retry_after_header: res.headers.get('retry-after')
|
|
94
102
|
};
|
|
95
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* POST a login expected to *succeed*, on a fresh transport with the given
|
|
106
|
+
* `X-Forwarded-For`. The mirror of `attempt` — used to prove that a success
|
|
107
|
+
* does not refund the resolved IP's bucket.
|
|
108
|
+
*/
|
|
109
|
+
const succeed = async (fixture, forwarded_for, username, password) => {
|
|
110
|
+
const res = await fixture.fresh_transport()(login_path, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'content-type': 'application/json', 'x-forwarded-for': forwarded_for },
|
|
113
|
+
body: JSON.stringify({ username, password })
|
|
114
|
+
});
|
|
115
|
+
await res.text();
|
|
116
|
+
return res.status;
|
|
117
|
+
};
|
|
96
118
|
describe('login rate limiting + trusted-proxy parity', () => {
|
|
97
119
|
test(`per-IP login limit fires 429 + Retry-After after ${ip_limit} attempts from one forwarded IP`, async () => {
|
|
98
120
|
const fixture = await setup_test();
|
|
@@ -126,5 +148,42 @@ export const describe_login_security_cross_tests = (options) => {
|
|
|
126
148
|
const fresh = await attempt(fixture, XFF_SEGREGATION_FRESH, 'probe_xff');
|
|
127
149
|
assert.strictEqual(fresh.status, 401, "a fresh forwarded IP must not inherit another IP's limit");
|
|
128
150
|
});
|
|
151
|
+
test('a success does not refund the per-IP budget (distributed-spray backstop)', async () => {
|
|
152
|
+
const fixture = await setup_test();
|
|
153
|
+
// The attacker's own account. Minting rides the setup path from
|
|
154
|
+
// loopback with no XFF, so it lands on a different bucket than
|
|
155
|
+
// XFF_SPRAY and can't confound the count.
|
|
156
|
+
const attacker = await fixture.create_account({
|
|
157
|
+
username: 'spray_attacker',
|
|
158
|
+
password_value: 'spray_attacker_password'
|
|
159
|
+
});
|
|
160
|
+
let budget = 0;
|
|
161
|
+
let limited = false;
|
|
162
|
+
for (let i = 0; i < ip_limit * 3; i++) {
|
|
163
|
+
// Their own successful login — the move that used to zero the
|
|
164
|
+
// whole IP window — then one guess against a fresh victim
|
|
165
|
+
// username, so no per-account bucket accumulates and only the
|
|
166
|
+
// IP aggregate can stop this.
|
|
167
|
+
const own = await succeed(fixture, XFF_SPRAY, attacker.account.username, 'spray_attacker_password');
|
|
168
|
+
budget++;
|
|
169
|
+
// Either leg may trip: the aggregate governs the *address*, so
|
|
170
|
+
// once it fills even the attacker's own login is refused. That
|
|
171
|
+
// is the property — no move from this IP buys more attempts.
|
|
172
|
+
if (own === 429) {
|
|
173
|
+
limited = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
assert.strictEqual(own, 200, "the attacker's own login must succeed");
|
|
177
|
+
const guess = await attempt(fixture, XFF_SPRAY, `spray_victim_${i}`);
|
|
178
|
+
budget++;
|
|
179
|
+
if (guess.status === 429) {
|
|
180
|
+
limited = true;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
assert.strictEqual(guess.status, 401, 'a guess against an unknown user must 401');
|
|
184
|
+
}
|
|
185
|
+
assert(limited, 'the per-IP aggregate must eventually 429 the spray');
|
|
186
|
+
assert(budget <= ip_limit * 2 + 2, `interleaved successes must not extend the per-IP budget (spent ${budget})`);
|
|
187
|
+
});
|
|
129
188
|
});
|
|
130
189
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { RpcPathCrossSuiteOptions } from './setup.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the query-shape parity suite. The standard RPC-dispatched
|
|
5
|
+
* cross-suite shape (`setup_test` / `capabilities` / `rpc_path`); aliases the
|
|
6
|
+
* shared `RpcPathCrossSuiteOptions` rather than minting a duplicate.
|
|
7
|
+
*/
|
|
8
|
+
export type QueryShapeCrossTestOptions = RpcPathCrossSuiteOptions;
|
|
9
|
+
export declare const describe_query_shape_cross_tests: (options: QueryShapeCrossTestOptions) => void;
|
|
10
|
+
//# sourceMappingURL=query_shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/query_shape.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;GAIG;AACH,MAAM,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAQlE,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IAwHtF,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend query-string shape parity suite.
|
|
4
|
+
*
|
|
5
|
+
* The TS pipeline reads queries through Hono's `c.req.query()` — a duplicated
|
|
6
|
+
* key resolves to its **first** occurrence — and only routes that declare a
|
|
7
|
+
* `z.strictObject` query schema 400 unknown keys, in the pipeline's
|
|
8
|
+
* params → query → 401 → authz → 403 phase order. A derived-serde axum
|
|
9
|
+
* extractor diverges on every axis (a plain-text pre-handler "duplicate
|
|
10
|
+
* field" 400 observable anonymously, silently ignored unknown keys, query
|
|
11
|
+
* rejection ahead of the 401), so each spine surface that reads a query is
|
|
12
|
+
* pinned here on both backends:
|
|
13
|
+
*
|
|
14
|
+
* - **`GET /api/account/status`** — NO query schema: a duplicated `acting`
|
|
15
|
+
* reads first-wins (the persona resolves), unknown keys are ignored, and an
|
|
16
|
+
* anonymous caller with junk query hears 401, never a query 400.
|
|
17
|
+
* - **the JSON-RPC GET endpoint** — `method` / `id` / `params` read
|
|
18
|
+
* first-wins; a duplicated `method` must dispatch the first one on every
|
|
19
|
+
* backend (a last-wins reader dispatches a different method per backend on
|
|
20
|
+
* the same request).
|
|
21
|
+
* - **`GET /api/facts/:hash`** (gated on `capabilities.fact_serving`) —
|
|
22
|
+
* strict query schema: unknown keys and a malformed `acting` are 400
|
|
23
|
+
* `invalid_query_params` even for an anonymous caller (query precedes the
|
|
24
|
+
* 401 guard), while a duplicated `acting` takes the first occurrence.
|
|
25
|
+
*
|
|
26
|
+
* The per-surface in-process TS pins live beside each module's own tests
|
|
27
|
+
* (`account_status.test.ts`, `action_rpc.test.ts`,
|
|
28
|
+
* `serve_fact_route.db.test.ts`); this suite is the cross-impl gate, so it is
|
|
29
|
+
* cross-process only — like `login_security.ts` and `testing_backdoor.ts`.
|
|
30
|
+
*
|
|
31
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
32
|
+
* cross-backend suites.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { describe, test, assert } from 'vitest';
|
|
37
|
+
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
38
|
+
import { ERROR_INVALID_QUERY_PARAMS } from '../../http/error_schemas.js';
|
|
39
|
+
import { test_if } from './capabilities.js';
|
|
40
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
41
|
+
/** The status route every spine serves (bundled into the account family). */
|
|
42
|
+
const STATUS_PATH = '/api/account/status';
|
|
43
|
+
/** A well-formed blake3 hash that references no stored fact. */
|
|
44
|
+
const MISSING_HASH = `blake3:${'a'.repeat(64)}`;
|
|
45
|
+
export const describe_query_shape_cross_tests = (options) => {
|
|
46
|
+
const { setup_test, capabilities } = options;
|
|
47
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
48
|
+
describe('query-shape parity (duplicate keys, unknown keys, phase order)', () => {
|
|
49
|
+
test_if(capabilities.account_status, 'status: duplicated acting takes the first occurrence (persona resolves)', async () => {
|
|
50
|
+
const fixture = await setup_test();
|
|
51
|
+
// First occurrence is the keeper's real actor; the second is a
|
|
52
|
+
// mismatching-but-well-formed uuid. First-wins resolves the real
|
|
53
|
+
// persona; last-wins would fall through to `actor: null`, and a
|
|
54
|
+
// derived-serde extractor would 400 before the handler ran.
|
|
55
|
+
const nil = '00000000-0000-0000-0000-000000000000';
|
|
56
|
+
const res = await fixture.transport(`${STATUS_PATH}?acting=${fixture.actor.id}&acting=${nil}`, { method: 'GET', headers: fixture.create_session_headers() });
|
|
57
|
+
assert.strictEqual(res.status, 200);
|
|
58
|
+
const body = (await res.json());
|
|
59
|
+
assert.strictEqual(body.actor?.id, fixture.actor.id, 'first acting occurrence must win');
|
|
60
|
+
});
|
|
61
|
+
test_if(capabilities.account_status, 'status: anonymous with duplicated/unknown query keys → 401, never a query 400', async () => {
|
|
62
|
+
const fixture = await setup_test();
|
|
63
|
+
const anon = fixture.fresh_transport();
|
|
64
|
+
// The status route has no query schema — an anonymous prober with a
|
|
65
|
+
// junk query must hear the same 401 as a clean one (a pre-handler
|
|
66
|
+
// query reject here is a backend fingerprint).
|
|
67
|
+
const dup = await anon(`${STATUS_PATH}?acting=a&acting=b`, { method: 'GET' });
|
|
68
|
+
assert.strictEqual(dup.status, 401);
|
|
69
|
+
const unknown = await anon(`${STATUS_PATH}?foo=1&bar=2`, { method: 'GET' });
|
|
70
|
+
assert.strictEqual(unknown.status, 401);
|
|
71
|
+
});
|
|
72
|
+
test_if(capabilities.account_status, 'status: unknown query keys are ignored for an authed caller (no strict schema)', async () => {
|
|
73
|
+
const fixture = await setup_test();
|
|
74
|
+
const res = await fixture.transport(`${STATUS_PATH}?foo=1`, {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: fixture.create_session_headers()
|
|
77
|
+
});
|
|
78
|
+
assert.strictEqual(res.status, 200);
|
|
79
|
+
});
|
|
80
|
+
test('rpc GET: duplicated method and id keys take the first occurrence', async () => {
|
|
81
|
+
const fixture = await setup_test();
|
|
82
|
+
const method = account_verify_action_spec.method;
|
|
83
|
+
// First-wins dispatches `account_verify` with id 7; a last-wins
|
|
84
|
+
// reader dispatches the missing method (method_not_found) with id 8.
|
|
85
|
+
const res = await fixture.transport(`${rpc_path}?method=${method}&method=definitely_missing_method&id=7&id=8`, { method: 'GET', headers: fixture.create_session_headers() });
|
|
86
|
+
assert.strictEqual(res.status, 200);
|
|
87
|
+
const body = (await res.json());
|
|
88
|
+
assert.strictEqual(body.id, 7, 'first id occurrence must win');
|
|
89
|
+
assert.ok(body.result !== undefined, 'first method occurrence must dispatch');
|
|
90
|
+
});
|
|
91
|
+
test_if(capabilities.fact_serving, 'bare-hash facts: unknown query key → 400 invalid_query_params, even anonymous', async () => {
|
|
92
|
+
const fixture = await setup_test();
|
|
93
|
+
// Authed admin (the keeper): the strict query schema refuses the
|
|
94
|
+
// unknown key before the handler can 404 the missing fact.
|
|
95
|
+
const as_admin = await fixture.transport(`/api/facts/${MISSING_HASH}?foo=1`, {
|
|
96
|
+
method: 'GET',
|
|
97
|
+
headers: fixture.create_session_headers()
|
|
98
|
+
});
|
|
99
|
+
assert.strictEqual(as_admin.status, 400);
|
|
100
|
+
const body = (await as_admin.json());
|
|
101
|
+
assert.strictEqual(body.error, ERROR_INVALID_QUERY_PARAMS);
|
|
102
|
+
// Anonymous: query validation precedes the 401 guard (phase
|
|
103
|
+
// order), with the clean anonymous request as the 401 control.
|
|
104
|
+
const anon = fixture.fresh_transport();
|
|
105
|
+
const anon_bad = await anon(`/api/facts/${MISSING_HASH}?foo=1`, { method: 'GET' });
|
|
106
|
+
assert.strictEqual(anon_bad.status, 400);
|
|
107
|
+
const anon_clean = await anon(`/api/facts/${MISSING_HASH}`, { method: 'GET' });
|
|
108
|
+
assert.strictEqual(anon_clean.status, 401);
|
|
109
|
+
});
|
|
110
|
+
test_if(capabilities.fact_serving, 'bare-hash facts: duplicated acting takes the first occurrence; malformed acting → 400', async () => {
|
|
111
|
+
const fixture = await setup_test();
|
|
112
|
+
// First occurrence is the keeper's real actor; the second is
|
|
113
|
+
// garbage. First-wins passes the query phase and reaches the
|
|
114
|
+
// handler's masked 404 (the hash references nothing) — a
|
|
115
|
+
// derived-serde extractor would 400 the duplication instead.
|
|
116
|
+
const dup = await fixture.transport(`/api/facts/${MISSING_HASH}?acting=${fixture.actor.id}&acting=not-a-uuid`, { method: 'GET', headers: fixture.create_session_headers() });
|
|
117
|
+
assert.strictEqual(dup.status, 404, 'duplicated acting must resolve first-wins');
|
|
118
|
+
// A malformed single acting is refused at the query phase.
|
|
119
|
+
const bad = await fixture.transport(`/api/facts/${MISSING_HASH}?acting=not-a-uuid`, {
|
|
120
|
+
method: 'GET',
|
|
121
|
+
headers: fixture.create_session_headers()
|
|
122
|
+
});
|
|
123
|
+
assert.strictEqual(bad.status, 400);
|
|
124
|
+
const body = (await bad.json());
|
|
125
|
+
assert.strictEqual(body.error, ERROR_INVALID_QUERY_PARAMS);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { SetupTest } from './setup.js';
|
|
3
|
+
/** Options for the session-cap parity suite. */
|
|
4
|
+
export interface SessionCapCrossTestOptions {
|
|
5
|
+
/** Per-test fixture producer (cross-process only — see the module doc). */
|
|
6
|
+
readonly setup_test: SetupTest;
|
|
7
|
+
/**
|
|
8
|
+
* The cap both spines enforce. Defaults to `DEFAULT_MAX_SESSIONS` — the TS
|
|
9
|
+
* route default and the Rust `const`. A consumer that overrides
|
|
10
|
+
* `max_sessions` on the TS side passes its value here.
|
|
11
|
+
*/
|
|
12
|
+
readonly max_sessions?: number;
|
|
13
|
+
/** REST login route path. Default `/api/account/login`. */
|
|
14
|
+
readonly login_path?: string;
|
|
15
|
+
/** REST account-status route path. Default `/api/account/status`. */
|
|
16
|
+
readonly status_path?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const describe_session_cap_cross_tests: (options: SessionCapCrossTestOptions) => void;
|
|
19
|
+
//# sourceMappingURL=session_cap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/session_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwD9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,gDAAgD;AAChD,MAAM,WAAW,0BAA0B;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IA6DtF,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for the **per-account concurrent-session cap**
|
|
4
|
+
* over real HTTP.
|
|
5
|
+
*
|
|
6
|
+
* The cap is a shipped control on the TS spine (`query_session_enforce_limit`,
|
|
7
|
+
* `DEFAULT_MAX_SESSIONS`) that the Rust spine went without — its session INSERT
|
|
8
|
+
* carried no eviction, so every Rust-backed deployment served unlimited
|
|
9
|
+
* concurrent sessions per account. That divergence survived because nothing crossed the
|
|
10
|
+
* wire on it — the TS half is pinned by in-process tests
|
|
11
|
+
* (`session_middleware.lifecycle.db.test.ts`, `session_token_limits.integration.db.test.ts`)
|
|
12
|
+
* whose Rust counterparts simply didn't exist to fail. This suite is the pin
|
|
13
|
+
* that runs on *both*, so the next spine to lose the cap fails here rather than
|
|
14
|
+
* in production. Two properties:
|
|
15
|
+
*
|
|
16
|
+
* - **the cap bounds concurrent sessions, by eviction not refusal** — logging in
|
|
17
|
+
* `max_sessions + 1` times succeeds every time (a login is never denied for
|
|
18
|
+
* being the one over the line), and the newest cookie resolves afterward.
|
|
19
|
+
* - **the evicted session is the oldest** — the first login's cookie no longer
|
|
20
|
+
* authenticates once the cap is exceeded. A spine with no cap authenticates it
|
|
21
|
+
* fine, which is exactly the defect this catches.
|
|
22
|
+
*
|
|
23
|
+
* **Why `max_sessions + 1` logins and not `max_sessions`.** The starting session
|
|
24
|
+
* count isn't zero and isn't guaranteed equal across impls — `create_account`
|
|
25
|
+
* seeds its own session on the Rust cradle. Overshooting by one guarantees at
|
|
26
|
+
* least one eviction whatever the baseline is, and the *oldest* login is the
|
|
27
|
+
* first to go either way. Asserting on the first and last sessions rather than
|
|
28
|
+
* on a row count keeps the case wire-observable (no DB channel) and
|
|
29
|
+
* baseline-independent.
|
|
30
|
+
*
|
|
31
|
+
* Both surfaces are flat REST (`POST /api/account/login`, `GET /api/account/status`)
|
|
32
|
+
* on every spine, so this is an imperative suite (not a `conformance_table` row)
|
|
33
|
+
* — the sibling of `cookie_attributes.ts` / `origin.ts` / `login_security.ts`.
|
|
34
|
+
* **Cross-process only**, and load-bearingly so: each login is held on its own
|
|
35
|
+
* `fresh_transport` and identified by that transport's cookie jar, which
|
|
36
|
+
* in-process is a jar-less passthrough. That also keeps the suite free of a
|
|
37
|
+
* fourth `Set-Cookie` parser — reading the raw header is `cookie_attributes.ts`'s
|
|
38
|
+
* job, and this case only needs "does this login still authenticate".
|
|
39
|
+
*
|
|
40
|
+
* What it does **not** pin: concurrent creators. Two simultaneous logins can
|
|
41
|
+
* each evict against a stale count under Read Committed and both commit above
|
|
42
|
+
* the cap — see `query_session_enforce_limit`'s race note. That needs a
|
|
43
|
+
* barrier-based test against one backend, not a parity case.
|
|
44
|
+
*
|
|
45
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
46
|
+
* cross-backend suites.
|
|
47
|
+
*
|
|
48
|
+
* @module
|
|
49
|
+
*/
|
|
50
|
+
import { describe, test, assert } from 'vitest';
|
|
51
|
+
import { DEFAULT_MAX_SESSIONS } from '../../auth/account_route_schema.js';
|
|
52
|
+
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
53
|
+
export const describe_session_cap_cross_tests = (options) => {
|
|
54
|
+
const { setup_test } = options;
|
|
55
|
+
const max_sessions = options.max_sessions ?? DEFAULT_MAX_SESSIONS;
|
|
56
|
+
const login_path = options.login_path ?? '/api/account/login';
|
|
57
|
+
const status_path = options.status_path ?? '/api/account/status';
|
|
58
|
+
// Fresh-keeper-per-test wipes the DB between tests, so a literal username
|
|
59
|
+
// never collides (see `setup.ts`).
|
|
60
|
+
const username = 'session_cap_user';
|
|
61
|
+
describe('per-account session cap parity', () => {
|
|
62
|
+
test(`logging in ${max_sessions + 1} times evicts the oldest session (newest cookie resolves, first does not)`, async () => {
|
|
63
|
+
const fixture = await setup_test();
|
|
64
|
+
await fixture.create_account({ username, password_value: DEFAULT_TEST_PASSWORD });
|
|
65
|
+
// One transport per login, each with its own empty jar, so each ends up
|
|
66
|
+
// holding exactly that login's session cookie and re-sends it on any
|
|
67
|
+
// later request. That's why this suite is cross-process only: in-process
|
|
68
|
+
// `fresh_transport` is a jar-less passthrough and every session would
|
|
69
|
+
// look alike. Held oldest-first.
|
|
70
|
+
const sessions = [];
|
|
71
|
+
for (let i = 0; i < max_sessions + 1; i++) {
|
|
72
|
+
const transport = fixture.fresh_transport();
|
|
73
|
+
const res = await transport(login_path, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'content-type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD })
|
|
77
|
+
});
|
|
78
|
+
assert.strictEqual(res.status, 200, `login ${i + 1}/${max_sessions + 1} must succeed — the cap evicts, it never refuses`);
|
|
79
|
+
assert.ok(transport.cookies().length > 0, `login ${i + 1} must set a session cookie on its jar`);
|
|
80
|
+
sessions.push(transport);
|
|
81
|
+
}
|
|
82
|
+
/** `GET /status` on one login's transport: 200 authenticated, 401 not. */
|
|
83
|
+
const status_on = async (transport) => {
|
|
84
|
+
const res = await transport(status_path, { method: 'GET' });
|
|
85
|
+
await res.text();
|
|
86
|
+
return res.status;
|
|
87
|
+
};
|
|
88
|
+
assert.strictEqual(await status_on(sessions[sessions.length - 1]), 200, 'the newest session must still resolve — the cap must evict, not refuse');
|
|
89
|
+
assert.strictEqual(await status_on(sessions[0]), 401, `the oldest session must be evicted once the account exceeds ${max_sessions} sessions ` +
|
|
90
|
+
'(a spine with no cap authenticates this cookie)');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
@@ -11,7 +11,6 @@ import './assert_dev_env.js';
|
|
|
11
11
|
import { vi } from 'vitest';
|
|
12
12
|
import { Hono } from 'hono';
|
|
13
13
|
import type { z } from 'zod';
|
|
14
|
-
import type { RateLimiter } from '../rate_limiter.js';
|
|
15
14
|
import { type RequestContext } from '../auth/request_context.js';
|
|
16
15
|
/** Mock configuration for bearer auth middleware test setup. */
|
|
17
16
|
export interface BearerAuthTestOptions {
|
|
@@ -80,7 +79,7 @@ export declare const TEST_CLIENT_IP = "127.0.0.1";
|
|
|
80
79
|
* The route handler at `/api/test` returns the resolved context in the response body,
|
|
81
80
|
* enabling assertions on `REQUEST_CONTEXT_KEY` and `CREDENTIAL_TYPE_KEY`.
|
|
82
81
|
*/
|
|
83
|
-
export declare const create_bearer_auth_test_app: (tc: BearerAuthTestOptions
|
|
82
|
+
export declare const create_bearer_auth_test_app: (tc: BearerAuthTestOptions) => {
|
|
84
83
|
app: Hono;
|
|
85
84
|
mocks: BearerAuthMocks;
|
|
86
85
|
};
|
|
@@ -89,7 +88,7 @@ export declare const create_bearer_auth_test_app: (tc: BearerAuthTestOptions, ip
|
|
|
89
88
|
*
|
|
90
89
|
* Generates one `test()` per case inside a `describe()` block.
|
|
91
90
|
*/
|
|
92
|
-
export declare const describe_bearer_auth_cases: (suite_name: string, cases: Array<BearerAuthTestCase
|
|
91
|
+
export declare const describe_bearer_auth_cases: (suite_name: string, cases: Array<BearerAuthTestCase>) => void;
|
|
93
92
|
/** Path used by the echo route in `create_test_middleware_stack_app`. */
|
|
94
93
|
export declare const TEST_MIDDLEWARE_PATH = "/api/test";
|
|
95
94
|
/** Options for `create_test_middleware_stack_app`. */
|
|
@@ -100,8 +99,6 @@ export interface TestMiddlewareStackOptions {
|
|
|
100
99
|
allowed_origins?: string;
|
|
101
100
|
/** Connection IP or factory. @default first trusted proxy */
|
|
102
101
|
connection_ip?: string | (() => string | undefined);
|
|
103
|
-
/** Rate limiter for bearer auth. @default `null` */
|
|
104
|
-
ip_rate_limiter?: RateLimiter | null;
|
|
105
102
|
}
|
|
106
103
|
/** Return type of `create_test_middleware_stack_app`. */
|
|
107
104
|
export interface TestMiddlewareStackApp {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,EAAE,EAA0B,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAe7B,OAAO,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,EAAE,EAA0B,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAe7B,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AA2BtF,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACrC,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oEAAoE;IACpE,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,4CAA4C;IAC5C,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,0CAA0C;IAC1C,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,+DAA+D;IAC/D,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,gFAAgF;IAChF,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAmB,SAAQ,qBAAqB;IAChE,+EAA+E;IAC/E,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9C,2FAA2F;IAC3F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mGAAmG;IACnG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mGAAmG;IACnG,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,uEAAuE;IACvE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD;AAID,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,eAAe,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1C,qBAAqB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAChD,2BAA2B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACtD,0BAA0B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACrD;AAKD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GAAI,IAAI,qBAAqB,KAAG,eAoCpE,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,cAAc,cAAc,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,IAAI,qBAAqB,KACvB;IAAE,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAwDrC,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,GACtC,YAAY,MAAM,EAClB,OAAO,KAAK,CAAC,kBAAkB,CAAC,KAC9B,IAyEF,CAAC;AAIF,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD,sDAAsD;AACtD,MAAM,WAAW,0BAA0B;IAC1C,iDAAiD;IACjD,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;CACpD;AAED,yDAAyD;AACzD,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,IAAI,CAAC;IACV,aAAa,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,eAAe,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1C,qBAAqB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAChD,2BAA2B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACtD,0BAA0B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACrD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,GAC5C,UAAU,0BAA0B,KAClC,sBAwDF,CAAC"}
|
|
@@ -91,9 +91,9 @@ export const TEST_CLIENT_IP = '127.0.0.1';
|
|
|
91
91
|
* The route handler at `/api/test` returns the resolved context in the response body,
|
|
92
92
|
* enabling assertions on `REQUEST_CONTEXT_KEY` and `CREDENTIAL_TYPE_KEY`.
|
|
93
93
|
*/
|
|
94
|
-
export const create_bearer_auth_test_app = (tc
|
|
94
|
+
export const create_bearer_auth_test_app = (tc) => {
|
|
95
95
|
const mocks = create_bearer_auth_mocks(tc);
|
|
96
|
-
const bearer_middleware = create_bearer_auth_middleware(STUB_DEPS,
|
|
96
|
+
const bearer_middleware = create_bearer_auth_middleware(STUB_DEPS, new Logger('test', { level: 'off' }));
|
|
97
97
|
const app = new Hono();
|
|
98
98
|
// inject pre-existing session identity if the test case specifies one.
|
|
99
99
|
// `pre_context` simulates the session middleware having authenticated
|
|
@@ -144,11 +144,11 @@ export const create_bearer_auth_test_app = (tc, ip_rate_limiter = null) => {
|
|
|
144
144
|
*
|
|
145
145
|
* Generates one `test()` per case inside a `describe()` block.
|
|
146
146
|
*/
|
|
147
|
-
export const describe_bearer_auth_cases = (suite_name, cases
|
|
147
|
+
export const describe_bearer_auth_cases = (suite_name, cases) => {
|
|
148
148
|
describe(suite_name, () => {
|
|
149
149
|
for (const tc of cases) {
|
|
150
150
|
test(tc.name, async () => {
|
|
151
|
-
const { app, mocks } = create_bearer_auth_test_app(tc
|
|
151
|
+
const { app, mocks } = create_bearer_auth_test_app(tc);
|
|
152
152
|
const res = await app.request('/api/test', {
|
|
153
153
|
method: 'GET',
|
|
154
154
|
headers: tc.headers
|
|
@@ -227,7 +227,7 @@ export const create_test_middleware_stack_app = (options) => {
|
|
|
227
227
|
});
|
|
228
228
|
const allowed_patterns = parse_allowed_origins(allowed_origins_str);
|
|
229
229
|
const origin_mw = verify_request_source(allowed_patterns);
|
|
230
|
-
const bearer_mw = create_bearer_auth_middleware(STUB_DEPS,
|
|
230
|
+
const bearer_mw = create_bearer_auth_middleware(STUB_DEPS, new Logger('test', { level: 'off' }));
|
|
231
231
|
const app = new Hono();
|
|
232
232
|
app.use('*', proxy_mw);
|
|
233
233
|
app.use('/api/*', origin_mw);
|
|
@@ -4,7 +4,7 @@ import type { AppServerContext } from '../server/app_server_context.js';
|
|
|
4
4
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
5
5
|
import { type SuiteAppOptions } from './app_server.js';
|
|
6
6
|
import { type DbFactory } from './db.js';
|
|
7
|
-
import {
|
|
7
|
+
import type { RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
8
8
|
/**
|
|
9
9
|
* Configuration for `describe_rate_limiting_tests`.
|
|
10
10
|
*/
|
|
@@ -25,37 +25,31 @@ export interface RateLimitingTestOptions {
|
|
|
25
25
|
*/
|
|
26
26
|
max_attempts?: number;
|
|
27
27
|
/**
|
|
28
|
-
* RPC endpoint specs
|
|
29
|
-
*
|
|
30
|
-
* action. Hard-fails via `require_rpc_endpoint_path` on setup.
|
|
28
|
+
* RPC endpoint specs mounted on the test app. Optional — both suites below
|
|
29
|
+
* exercise REST auth routes, so nothing here needs an RPC path.
|
|
31
30
|
*
|
|
32
31
|
* Accepts either an array (eager) or a factory
|
|
33
32
|
* `(ctx: AppServerContext) => Array<RpcEndpointSpec>` — the factory form
|
|
34
33
|
* is required when action handlers must close over the per-test
|
|
35
|
-
* `ctx.deps`.
|
|
36
|
-
* endpoint `path` regardless of ctx — it is invoked once at setup with
|
|
37
|
-
* a stub ctx for path lookup and again per-test by `create_app_server`
|
|
38
|
-
* for live dispatch.
|
|
34
|
+
* `ctx.deps`.
|
|
39
35
|
*/
|
|
40
|
-
rpc_endpoints
|
|
36
|
+
rpc_endpoints?: RpcEndpointsSuiteOption;
|
|
41
37
|
}
|
|
42
38
|
/**
|
|
43
39
|
* Standard rate limiting integration test suite.
|
|
44
40
|
*
|
|
45
|
-
* Creates
|
|
41
|
+
* Creates 2 test groups:
|
|
46
42
|
* 1. IP rate limiting on login — fires `max_attempts + 1` login requests,
|
|
47
43
|
* verifies the last returns 429 with a valid `RateLimitError` body.
|
|
48
44
|
* 2. Per-account rate limiting on login — fires `max_attempts + 1` login
|
|
49
45
|
* requests with the same username, verifies the last returns 429.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* There is deliberately no bearer-auth group: the bearer path carries no rate
|
|
48
|
+
* limiter on either spine, because an API token's entropy — not throttling —
|
|
49
|
+
* is what bounds guessing it. See `auth/bearer_auth.ts`.
|
|
52
50
|
*
|
|
53
51
|
* Each test group asserts that required routes exist, failing with a descriptive
|
|
54
52
|
* message if the consumer's route specs are misconfigured.
|
|
55
|
-
*
|
|
56
|
-
* @throws Error at setup time when `options.rpc_endpoints` is empty — the
|
|
57
|
-
* bearer-auth rate-limit test probes via the `account_verify` RPC action,
|
|
58
|
-
* so the suite hard-fails via `require_rpc_endpoint_path`.
|
|
59
53
|
*/
|
|
60
54
|
export declare const describe_rate_limiting_tests: (options: RateLimitingTestOptions) => void;
|
|
61
55
|
//# sourceMappingURL=rate_limiting.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate_limiting.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rate_limiting.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA0B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAEjB,OAAO,
|
|
1
|
+
{"version":3,"file":"rate_limiting.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rate_limiting.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA0B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAIhE;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,uBAAuB,CAAC;CACxC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAqK/E,CAAC"}
|