@fuzdev/fuz_app 0.135.0 → 0.136.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.
Files changed (67) hide show
  1. package/dist/actions/CLAUDE.md +2 -2
  2. package/dist/actions/transports_ws_backend.d.ts +1 -1
  3. package/dist/actions/transports_ws_backend.js +1 -1
  4. package/dist/auth/CLAUDE.md +8 -2
  5. package/dist/auth/account_action_specs.d.ts +7 -0
  6. package/dist/auth/account_action_specs.d.ts.map +1 -1
  7. package/dist/auth/account_action_specs.js +11 -1
  8. package/dist/auth/audit_emitter.d.ts +4 -0
  9. package/dist/auth/audit_emitter.d.ts.map +1 -1
  10. package/dist/auth/audit_emitter.js +18 -2
  11. package/dist/auth/cell_action_specs.d.ts +10 -0
  12. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  13. package/dist/auth/cell_action_specs.js +17 -3
  14. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  15. package/dist/auth/role_grant_offer_action_specs.js +7 -2
  16. package/dist/auth/scope_kind_schema.d.ts +10 -0
  17. package/dist/auth/scope_kind_schema.d.ts.map +1 -1
  18. package/dist/auth/scope_kind_schema.js +11 -0
  19. package/dist/env/load.d.ts +1 -1
  20. package/dist/env/load.js +1 -1
  21. package/dist/http/CLAUDE.md +1 -1
  22. package/dist/http/body_limit.d.ts +27 -14
  23. package/dist/http/body_limit.d.ts.map +1 -1
  24. package/dist/http/body_limit.js +29 -16
  25. package/dist/http/db_routes.d.ts.map +1 -1
  26. package/dist/http/db_routes.js +5 -3
  27. package/dist/testing/CLAUDE.md +36 -19
  28. package/dist/testing/audit_drift_guard.d.ts +18 -5
  29. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  30. package/dist/testing/audit_drift_guard.js +24 -8
  31. package/dist/testing/cross_backend/body_size.d.ts +18 -5
  32. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  33. package/dist/testing/cross_backend/body_size.js +233 -34
  34. package/dist/testing/cross_backend/body_size_smuggling.d.ts +0 -12
  35. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  36. package/dist/testing/cross_backend/body_size_smuggling.js +92 -147
  37. package/dist/testing/cross_backend/capabilities.d.ts +3 -24
  38. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  39. package/dist/testing/cross_backend/capabilities.js +1 -4
  40. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  41. package/dist/testing/cross_backend/cell_crud.js +31 -2
  42. package/dist/testing/cross_backend/conformance_case.d.ts +3 -3
  43. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  44. package/dist/testing/cross_backend/credential_header_robustness.js +18 -54
  45. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  46. package/dist/testing/cross_backend/default_backend_configs.js +2 -8
  47. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  48. package/dist/testing/cross_backend/testing_backdoor.js +2 -0
  49. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
  50. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  51. package/dist/testing/cross_backend/testing_reset_actions.js +5 -0
  52. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  53. package/dist/testing/cross_backend/token_lifetime.js +34 -1
  54. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  55. package/dist/testing/cross_backend/ts_spine_backend_config.js +7 -14
  56. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/ws_outbound_overflow.js +65 -0
  58. package/dist/testing/schema_introspect.d.ts +24 -4
  59. package/dist/testing/schema_introspect.d.ts.map +1 -1
  60. package/dist/testing/schema_introspect.js +27 -4
  61. package/dist/testing/transports/raw_http.d.ts +78 -0
  62. package/dist/testing/transports/raw_http.d.ts.map +1 -0
  63. package/dist/testing/transports/raw_http.js +141 -0
  64. package/package.json +1 -1
  65. package/dist/env/resolve.d.ts +0 -163
  66. package/dist/env/resolve.d.ts.map +0 -1
  67. package/dist/env/resolve.js +0 -267
@@ -1 +1 @@
1
- {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA6ExE,kEAAkE;AAClE,MAAM,WAAW,+BAAgC,SAAQ,wBAAwB;IAChF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAyEF,CAAC"}
1
+ {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA+ExE,kEAAkE;AAClE,MAAM,WAAW,+BAAgC,SAAQ,wBAAwB;IAChF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAyEF,CAAC"}
@@ -77,6 +77,8 @@ const NIL_UUID = '00000000-0000-0000-0000-000000000000';
77
77
  */
78
78
  const backdoor_methods = [
79
79
  { method: '_testing_reset', params: {} },
80
+ // The audit barrier — its input is void, so the call carries no params.
81
+ { method: '_testing_drain_effects', params: undefined },
80
82
  { method: '_testing_mint_session', params: { account_id: NIL_UUID, expires_in_seconds: -60 } },
81
83
  { method: '_testing_put_fact', params: { content: 'backdoor-probe' } },
82
84
  // An empty burst — valid input, so it clears the 400 phase; a refused
@@ -379,6 +379,11 @@ export declare const create_testing_flood_notifications_action: () => RpcAction;
379
379
  * shapes match by design). Optional `exclude_tables` drops documented
380
380
  * divergences from both sides before comparison.
381
381
  *
382
+ * A backend running on pglet (a TS backend config's `memory://` default)
383
+ * answers with an error when its schema holds an index: the snapshot exists
384
+ * to be compared, and pglet's index definitions omit the modifiers a
385
+ * comparison needs (`query_schema_snapshot`). Run the backend on PostgreSQL.
386
+ *
382
387
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
383
388
  */
384
389
  export declare const testing_schema_snapshot_action_spec: {
@@ -1 +1 @@
1
- {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"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"}
@@ -397,6 +397,11 @@ rpc_action(testing_flood_notifications_action_spec, ({ count, size }, ctx) => {
397
397
  * shapes match by design). Optional `exclude_tables` drops documented
398
398
  * divergences from both sides before comparison.
399
399
  *
400
+ * A backend running on pglet (a TS backend config's `memory://` default)
401
+ * answers with an error when its schema holds an index: the snapshot exists
402
+ * to be compared, and pglet's index definitions omit the modifiers a
403
+ * comparison needs (`query_schema_snapshot`). Run the backend on PostgreSQL.
404
+ *
400
405
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
401
406
  */
402
407
  export const testing_schema_snapshot_action_spec = {
@@ -1 +1 @@
1
- {"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAaD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IAsHF,CAAC"}
1
+ {"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkD9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAaD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IA6KF,CAAC"}
@@ -27,11 +27,16 @@ import '../assert_dev_env.js';
27
27
  * pin on the silently-ignoring-unknown-shapes hazard.
28
28
  * - **Out-of-range days are refused** — `days: 0` 400s identically, so the
29
29
  * two hand-rolled bounds checks can't drift.
30
+ * - **The `name` cap** — the same blind spot on another field of the same
31
+ * mint: a name of exactly `API_TOKEN_NAME_LENGTH_MAX` UTF-16 code units
32
+ * mints and reads back whole, and one unit past it is `invalid_params`,
33
+ * with an astral-plane name at each side of the boundary so the two spines
34
+ * count the same unit.
30
35
  *
31
36
  * @module
32
37
  */
33
38
  import { describe, test, assert } from 'vitest';
34
- import { account_token_create_action_spec, account_token_list_action_spec, TokenCreateOutput, TokenListOutput } from '../../auth/account_action_specs.js';
39
+ import { account_token_create_action_spec, account_token_list_action_spec, API_TOKEN_NAME_LENGTH_MAX, TokenCreateOutput, TokenListOutput } from '../../auth/account_action_specs.js';
35
40
  import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
36
41
  import { assert_iso8601_seconds } from './wire_shapes.js';
37
42
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
@@ -97,5 +102,33 @@ export const describe_token_lifetime_cross_tests = (options) => {
97
102
  assert.ok(!res.ok, 'days: 0 must be refused');
98
103
  assert.strictEqual(res.error?.code, -32602, `expected invalid_params: ${JSON.stringify(res.error)}`);
99
104
  });
105
+ test('a name at the length cap mints and reads back; one past it is invalid_params', async () => {
106
+ const fixture = await setup_test();
107
+ const astral = '\u{1F600}';
108
+ // the cap is odd: whole astral characters, plus one unit to land on it
109
+ const astral_at_cap = astral.repeat((API_TOKEN_NAME_LENGTH_MAX - 1) / 2) + 'a';
110
+ const mint = (name) => cross_rpc_call(fixture.transport, rpc_path, account_token_create_action_spec.method, { name, scope: { kind: 'full' }, lifetime: { kind: 'eternal' } }, fixture.create_session_headers());
111
+ const admitted = ['a'.repeat(API_TOKEN_NAME_LENGTH_MAX), astral_at_cap];
112
+ const minted_ids = [];
113
+ for (const name of admitted) {
114
+ assert.strictEqual(name.length, API_TOKEN_NAME_LENGTH_MAX);
115
+ minted_ids.push(expect_output(await mint(name), TokenCreateOutput).id);
116
+ }
117
+ const listed = expect_output(await cross_rpc_call(fixture.transport, rpc_path, account_token_list_action_spec.method, undefined, fixture.create_session_headers()), TokenListOutput);
118
+ for (const [index, id] of minted_ids.entries()) {
119
+ assert.strictEqual(listed.tokens.find((t) => t.id === id)?.name, admitted[index], 'a name at the cap must be stored whole');
120
+ }
121
+ for (const name of [
122
+ 'a'.repeat(API_TOKEN_NAME_LENGTH_MAX + 1),
123
+ // one astral character past the cap: two units over an even count
124
+ astral.repeat((API_TOKEN_NAME_LENGTH_MAX + 1) / 2),
125
+ astral_at_cap + 'a'
126
+ ]) {
127
+ const res = await mint(name);
128
+ assert.ok(!res.ok, `a ${name.length}-unit name must be refused`);
129
+ assert.strictEqual(res.error?.code, -32602, `expected invalid_params for a ${name.length}-unit name, got ${JSON.stringify(res.error)}`);
130
+ assert.strictEqual(error_reason(res), undefined, 'a schema refusal carries no reason');
131
+ }
132
+ });
100
133
  });
101
134
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAwDzD,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAiCF,CAAC"}
1
+ {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiDzD,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAiCF,CAAC"}
@@ -22,25 +22,18 @@ const ts_spine_capabilities = Object.freeze({
22
22
  });
23
23
  /**
24
24
  * Capabilities for the **Bun** spine binary — `ts_spine_capabilities` with
25
- * `oversized_reject_closes_connection: false` and
26
25
  * `ws_handshake_pipelining: false`.
27
26
  *
28
- * `Bun.serve` does not close the connection after an oversized-body `413`:
29
- * the response carries `Connection: close`, and Bun keeps the socket open,
30
- * skips the declared body, and serves the request that follows it (observed
31
- * on Bun 1.3.11). The next request is framed on `Content-Length`, so nothing
32
- * is desynchronized, but the smuggling suite's "the pipelined request is never
33
- * served" assertion does not hold; the flag puts Bun on the suite's no-desync
34
- * arm, which requires exactly the 413 and the reply to the pipelined request.
35
- * See `docs/security.md` §"Body Size Limiting".
36
- *
37
- * `Bun.serve` also answers `400` to an upgrade request that carries WebSocket
38
- * frames in the same TCP write, before any handler runs, so the WS suite's
39
- * frames-with-the-handshake case has nothing to drive there.
27
+ * `Bun.serve` answers `400` to an upgrade request that carries WebSocket
28
+ * frames in the same TCP write when the handler upgrades after an await that
29
+ * yields to the event loop — a timer, I/O; the spine's does, behind the auth
30
+ * middleware's database reads — so the WS suite's frames-with-the-handshake
31
+ * case has nothing to drive there (observed on Bun 1.4.3; an upgrade made
32
+ * synchronously in `fetch`, or after an await that settles in the same
33
+ * microtask run, takes the frames).
40
34
  */
41
35
  const ts_spine_bun_capabilities = Object.freeze({
42
36
  ...ts_spine_capabilities,
43
- oversized_reject_closes_connection: false,
44
37
  ws_handshake_pipelining: false
45
38
  });
46
39
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
@@ -1 +1 @@
1
- {"version":3,"file":"ws_outbound_overflow.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_outbound_overflow.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAiCzD,sDAAsD;AACtD,MAAM,WAAW,kCAAkC;IAClD,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,kCAAkC,KACzC,IA0LF,CAAC"}
1
+ {"version":3,"file":"ws_outbound_overflow.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_outbound_overflow.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAoFzD,sDAAsD;AACtD,MAAM,WAAW,kCAAkC;IAClD,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,kCAAkC,KACzC,IA0NF,CAAC"}
@@ -32,6 +32,13 @@ import '../assert_dev_env.js';
32
32
  * `invalid_params`, and the socket stays open.
33
33
  * - **no connection** — over HTTP RPC the action refuses with `invalid_params`
34
34
  * and reason `no_connection`, rather than answering as though it flooded.
35
+ * - **the action's limits** — over HTTP RPC a call at each of the action's
36
+ * limits (`count`, `size`, `count` × `size`) is refused for having no
37
+ * connection, and a call one past each is refused `invalid_params` with no
38
+ * reason. Both spines check the limits before they look for the connection
39
+ * and before they build a payload, so the boundaries are read off the
40
+ * refusal and no burst is ever allocated — which pins the two spines'
41
+ * limits equal.
35
42
  *
36
43
  * What the suite does **not** assert, because it is each implementation's own:
37
44
  * how many frames accepted before the overflow reach the client (the Rust
@@ -77,6 +84,50 @@ const LARGE_FITS = { count: 3, size: MIB };
77
84
  const OVERFLOWS = { count: 32, size: MIB };
78
85
  /** One mebibyte past the action's `count` × `size` limit. */
79
86
  const REFUSED = { count: 65, size: MIB };
87
+ /** The most notifications one flood call may ask for. */
88
+ const FLOOD_MAX_COUNT = 65_536;
89
+ /** The largest payload one flood call may ask for. */
90
+ const FLOOD_MAX_SIZE = 16 * MIB;
91
+ /** The most payload bytes (`count` × `size`) one flood call may ask for. */
92
+ const FLOOD_MAX_TOTAL_BYTES = 64 * MIB;
93
+ /**
94
+ * A `count` and `size`, each within its own limit, whose product is exactly
95
+ * one byte past `FLOOD_MAX_TOTAL_BYTES` (the factors of 2^26 + 1).
96
+ */
97
+ const FLOOD_ONE_BYTE_PAST_TOTAL = { count: 265, size: 253_241 };
98
+ /**
99
+ * Calls at and one past each of the flood action's limits. Each "past" call
100
+ * holds the other axis at zero, or both axes within their own limit, so one
101
+ * limit alone decides it.
102
+ */
103
+ const FLOOD_LIMIT_CASES = [
104
+ { name: 'count at its limit', params: { count: FLOOD_MAX_COUNT, size: 1 }, within: true },
105
+ { name: 'count past its limit', params: { count: FLOOD_MAX_COUNT + 1, size: 0 }, within: false },
106
+ { name: 'size at its limit', params: { count: 1, size: FLOOD_MAX_SIZE }, within: true },
107
+ { name: 'size past its limit', params: { count: 0, size: FLOOD_MAX_SIZE + 1 }, within: false },
108
+ {
109
+ name: 'count × size at its limit, both axes at theirs or under',
110
+ params: { count: FLOOD_MAX_TOTAL_BYTES / FLOOD_MAX_SIZE, size: FLOOD_MAX_SIZE },
111
+ within: true
112
+ },
113
+ {
114
+ name: 'count × size at its limit, count at its own',
115
+ params: { count: FLOOD_MAX_COUNT, size: FLOOD_MAX_TOTAL_BYTES / FLOOD_MAX_COUNT },
116
+ within: true
117
+ },
118
+ {
119
+ name: 'count × size past its limit, each axis within its own',
120
+ params: { count: FLOOD_MAX_COUNT, size: FLOOD_MAX_TOTAL_BYTES / FLOOD_MAX_COUNT + 1 },
121
+ within: false
122
+ },
123
+ {
124
+ // the smallest step past the product limit: the row above overshoots it by
125
+ // `FLOOD_MAX_COUNT` bytes, so alone it admits a limit that many bytes loose
126
+ name: 'count × size one byte past its limit',
127
+ params: FLOOD_ONE_BYTE_PAST_TOTAL,
128
+ within: false
129
+ }
130
+ ];
80
131
  /** How long the test waits for the megabyte burst and its response. */
81
132
  const LARGE_BURST_TIMEOUT_MS = 15_000;
82
133
  /** How long the test waits for the overflow close. */
@@ -207,5 +258,19 @@ export const describe_ws_outbound_overflow_cross_tests = (options) => {
207
258
  assert.strictEqual(res.error?.code, INVALID_PARAMS);
208
259
  assert.strictEqual(res.error?.data?.reason, REASON_NO_CONNECTION);
209
260
  });
261
+ test_if(rpc_path !== undefined, 'over HTTP a flood call at each limit is refused for no connection, one past it for its params', async () => {
262
+ const fixture = await setup_test();
263
+ const transport = fixture.fresh_transport({ origin: null });
264
+ // the hand-factored case is only "one byte past" for this limit
265
+ assert.strictEqual(FLOOD_ONE_BYTE_PAST_TOTAL.count * FLOOD_ONE_BYTE_PAST_TOTAL.size, FLOOD_MAX_TOTAL_BYTES + 1);
266
+ for (const { name, params, within } of FLOOD_LIMIT_CASES) {
267
+ const res = await cross_rpc_call(transport, rpc_path, FLOOD_METHOD, params, fixture.create_daemon_token_headers());
268
+ assert.strictEqual(res.ok, false, `${name}: expected a refusal over HTTP`);
269
+ assert.strictEqual(res.error?.code, INVALID_PARAMS, name);
270
+ // within the limits the call gets as far as the connection check;
271
+ // past one it is refused before that, with no reason
272
+ assert.strictEqual(res.error?.data?.reason, within ? REASON_NO_CONNECTION : undefined, `${name}: ${JSON.stringify(params)}`);
273
+ }
274
+ });
210
275
  });
211
276
  };
@@ -21,10 +21,14 @@ import './assert_dev_env.js';
21
21
  * Index and constraint definitions are compared as the engine renders them,
22
22
  * so both sides of a parity comparison need engines that render alike. pglet
23
23
  * leaves an index's modifiers out of `pg_indexes.indexdef` — per-key sort
24
- * direction, `NULLS` placement, operator classes, `NULLS NOT DISTINCT` — so a
25
- * pglet snapshot diffed against a PostgreSQL one reports every index that
26
- * uses one, with identical DDL on both sides. Take both snapshots on
27
- * PostgreSQL.
24
+ * direction, `NULLS` placement, operator classes, `NULLS NOT DISTINCT` — and
25
+ * that misleads a comparison in both directions: a pglet snapshot diffed
26
+ * against a PostgreSQL one reports every index that uses a modifier, with
27
+ * identical DDL on both sides (a false difference), and two pglet snapshots
28
+ * agree when the two schemas differ only in a modifier (a false agreement).
29
+ * So `query_schema_snapshot` throws on pglet when the snapshot holds an
30
+ * index, unless the caller passes `accept_pglet_index_definitions`. Take both
31
+ * snapshots of a parity comparison on PostgreSQL.
28
32
  *
29
33
  * Paired with `testing/schema_parity.ts` for comparison + assertion helpers.
30
34
  *
@@ -170,6 +174,17 @@ export interface QuerySchemaSnapshotOptions {
170
174
  * consumer's domain schema.
171
175
  */
172
176
  readonly exclude_tables?: ReadonlyArray<string>;
177
+ /**
178
+ * Take the snapshot on pglet even though it holds indexes. Default `false`:
179
+ * pglet's `pg_indexes.indexdef` omits index modifiers (sort direction,
180
+ * `NULLS` placement, operator classes, `NULLS NOT DISTINCT`), so the
181
+ * snapshot's index definitions cannot be compared with another engine's,
182
+ * and do not tell two pglet schemas apart when they differ only in a
183
+ * modifier. Pass `true` only where no index definition is compared —
184
+ * reading tables, columns, constraints, sequences or enums off the
185
+ * snapshot. No effect on PostgreSQL or PGlite.
186
+ */
187
+ readonly accept_pglet_index_definitions?: boolean;
173
188
  }
174
189
  /**
175
190
  * Introspect a live database into a deterministic `SchemaSnapshot`.
@@ -180,6 +195,11 @@ export interface QuerySchemaSnapshotOptions {
180
195
  * The `schema_version` migration tracker never appears in the `tables`
181
196
  * field — it's framework bookkeeping created by the migration runner,
182
197
  * identical across consumers, and would only add noise.
198
+ *
199
+ * @param db - the database to introspect
200
+ * @param options - schema, table excludes, and the pglet opt-in
201
+ * @returns the snapshot, every collection sorted
202
+ * @throws Error on pglet when the snapshot holds an index and `accept_pglet_index_definitions` is not set — its index definitions omit modifiers, so a comparison built on them is wrong in both directions
183
203
  */
184
204
  export declare const query_schema_snapshot: (db: Db, options?: QuerySchemaSnapshotOptions) => Promise<SchemaSnapshot>;
185
205
  //# sourceMappingURL=schema_introspect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema_introspect.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_introspect.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;iBAWzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,qCAAqC;AACrC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;iBAG3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;iBAOzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,gBAAgB,CAa9E,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AA8DD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GACjC,IAAI,EAAE,EACN,UAAS,0BAA+B,KACtC,OAAO,CAAC,cAAc,CAyHxB,CAAC"}
1
+ {"version":3,"file":"schema_introspect.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_introspect.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;iBAWzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,qCAAqC;AACrC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;iBAG3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;iBAOzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,gBAAgB,CAa9E,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;;;;OASG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,OAAO,CAAC;CAClD;AAiED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,GACjC,IAAI,EAAE,EACN,UAAS,0BAA+B,KACtC,OAAO,CAAC,cAAc,CA0IxB,CAAC"}
@@ -21,10 +21,14 @@ import './assert_dev_env.js';
21
21
  * Index and constraint definitions are compared as the engine renders them,
22
22
  * so both sides of a parity comparison need engines that render alike. pglet
23
23
  * leaves an index's modifiers out of `pg_indexes.indexdef` — per-key sort
24
- * direction, `NULLS` placement, operator classes, `NULLS NOT DISTINCT` — so a
25
- * pglet snapshot diffed against a PostgreSQL one reports every index that
26
- * uses one, with identical DDL on both sides. Take both snapshots on
27
- * PostgreSQL.
24
+ * direction, `NULLS` placement, operator classes, `NULLS NOT DISTINCT` — and
25
+ * that misleads a comparison in both directions: a pglet snapshot diffed
26
+ * against a PostgreSQL one reports every index that uses a modifier, with
27
+ * identical DDL on both sides (a false difference), and two pglet snapshots
28
+ * agree when the two schemas differ only in a modifier (a false agreement).
29
+ * So `query_schema_snapshot` throws on pglet when the snapshot holds an
30
+ * index, unless the caller passes `accept_pglet_index_definitions`. Take both
31
+ * snapshots of a parity comparison on PostgreSQL.
28
32
  *
29
33
  * Paired with `testing/schema_parity.ts` for comparison + assertion helpers.
30
34
  *
@@ -136,6 +140,8 @@ export const query_migration_tracker = async (db) => {
136
140
  }))
137
141
  };
138
142
  };
143
+ /** The marker pglet's `version()` carries. */
144
+ const PGLET_VERSION_MARKER = '(pglet)';
139
145
  const sort_keys = (record) => {
140
146
  const sorted = {};
141
147
  for (const key of Object.keys(record).sort()) {
@@ -170,6 +176,11 @@ const contype_to_kind = (contype) => {
170
176
  * The `schema_version` migration tracker never appears in the `tables`
171
177
  * field — it's framework bookkeeping created by the migration runner,
172
178
  * identical across consumers, and would only add noise.
179
+ *
180
+ * @param db - the database to introspect
181
+ * @param options - schema, table excludes, and the pglet opt-in
182
+ * @returns the snapshot, every collection sorted
183
+ * @throws Error on pglet when the snapshot holds an index and `accept_pglet_index_definitions` is not set — its index definitions omit modifiers, so a comparison built on them is wrong in both directions
173
184
  */
174
185
  export const query_schema_snapshot = async (db, options = {}) => {
175
186
  const schema = options.schema ?? 'public';
@@ -260,6 +271,18 @@ export const query_schema_snapshot = async (db, options = {}) => {
260
271
  for (const row of enum_rows) {
261
272
  (enums[row.enum_name] ??= { labels: [] }).labels.push(row.label);
262
273
  }
274
+ if (!options.accept_pglet_index_definitions &&
275
+ Object.values(tables).some((table) => table.indexes.length > 0)) {
276
+ const [version_row] = await db.query(`SELECT version() AS version`);
277
+ if (version_row?.version.includes(PGLET_VERSION_MARKER)) {
278
+ throw new Error('query_schema_snapshot: this database is pglet, whose pg_indexes.indexdef omits index ' +
279
+ 'modifiers (sort direction, NULLS placement, operator classes, NULLS NOT DISTINCT), so ' +
280
+ "the snapshot's index definitions report false differences against PostgreSQL and false " +
281
+ 'agreement between two pglet schemas. Take the snapshot on PostgreSQL (a TS backend ' +
282
+ 'config defaults to memory://, which is pglet), or pass ' +
283
+ '{accept_pglet_index_definitions: true} when no index definition is compared.');
284
+ }
285
+ }
263
286
  return {
264
287
  tables: sort_keys(tables),
265
288
  sequences: sort_keys(sequences),
@@ -0,0 +1,78 @@
1
+ import '../assert_dev_env.js';
2
+ /** What a raw exchange read back, and how it ended. */
3
+ export interface RawHttpExchange {
4
+ /** Every byte the server sent, `latin1`-decoded. */
5
+ readonly received: string;
6
+ /**
7
+ * `true` when the connection ended before the exchange stopped reading —
8
+ * the server closed it, or it never opened (a refused or unresolvable
9
+ * address reads as closed, with nothing received).
10
+ */
11
+ readonly closed_by_server: boolean;
12
+ }
13
+ /** Options for `raw_http_exchange`. */
14
+ export interface RawHttpExchangeOptions {
15
+ /** How long to read before giving up on the server. */
16
+ readonly read_timeout_ms: number;
17
+ /**
18
+ * When the exchange stops reading. `'close'` reads until the server closes
19
+ * the connection; `'response'` stops as soon as one whole response has
20
+ * arrived (`raw_http_parse_response`), for a server that answers and then
21
+ * keeps the connection open. Either way the read timeout bounds it.
22
+ *
23
+ * `'response'` stops at the first response the bytes frame, so it is for a
24
+ * final response carrying `Content-Length` or chunked framing: a `1xx`
25
+ * interim response ends the read, and so does the head of a response
26
+ * delimited by the connection closing, before its body.
27
+ *
28
+ * @default 'close'
29
+ */
30
+ readonly until?: 'close' | 'response';
31
+ }
32
+ /** One parsed HTTP/1.1 response. */
33
+ export interface RawHttpResponse {
34
+ readonly status: number;
35
+ /** Response headers, names lowercased; a repeated name keeps its last value. */
36
+ readonly headers: Readonly<Record<string, string>>;
37
+ /** The response body, with any chunked framing removed. */
38
+ readonly body: string;
39
+ }
40
+ /**
41
+ * Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
42
+ * what the server sends back until it closes the connection, a whole response
43
+ * has arrived (`until: 'response'`), or `read_timeout_ms` elapses.
44
+ *
45
+ * Write errors are swallowed: a server that refuses a request and closes
46
+ * mid-upload surfaces as `EPIPE` / `ECONNRESET` on the unfinished write, and
47
+ * what a caller asserts on is what was read back. A write error also ends the
48
+ * socket, so bytes the server sent just before it can go unread — a caller
49
+ * that needs to read a refusal reliably sends nothing the server will leave
50
+ * unread.
51
+ *
52
+ * @param base_url - the server's base URL; only its host and port are used
53
+ * @param request_bytes - the bytes to send, `latin1`-encoded
54
+ */
55
+ export declare const raw_http_exchange: (base_url: string, request_bytes: string, options: RawHttpExchangeOptions) => Promise<RawHttpExchange>;
56
+ /**
57
+ * Count HTTP response status lines in a raw byte stream. Deliberately
58
+ * **unanchored** (no `^`/`m`): a second pipelined response is concatenated
59
+ * straight after the first response's body, which carries no trailing newline,
60
+ * so a line-anchored match would miss it — and missing a smuggled second
61
+ * response is a silent false negative. It counts exactly the status lines as
62
+ * long as no response header or body contains the literal `HTTP/1.x NNN`.
63
+ */
64
+ export declare const raw_http_count_responses: (raw: string) => number;
65
+ /** The status code of the first HTTP response in a raw byte stream, or `null` if none. */
66
+ export declare const raw_http_first_status: (raw: string) => number | null;
67
+ /**
68
+ * Parse the first response in a raw byte stream, once all of it has arrived.
69
+ *
70
+ * The body is delimited by `Transfer-Encoding: chunked` or `Content-Length`.
71
+ * A response carrying neither is delimited by the connection closing, which
72
+ * the bytes alone don't show, so its body is whatever followed the head.
73
+ *
74
+ * @param raw - the bytes read so far, `latin1`-decoded
75
+ * @returns the response, or `null` when the head or the framed body is incomplete
76
+ */
77
+ export declare const raw_http_parse_response: (raw: string) => RawHttpResponse | null;
78
+ //# sourceMappingURL=raw_http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw_http.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/raw_http.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CACtC;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,SAAS,sBAAsB,KAC7B,OAAO,CAAC,eAAe,CAwBvB,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,MACN,CAAC;AAElD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAG5D,CAAC;AAyBF;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,KAAG,eAAe,GAAG,IAyBvE,CAAC"}
@@ -0,0 +1,141 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Raw-socket HTTP/1.1 exchanges, for tests that need to control exactly which
4
+ * bytes reach the server.
5
+ *
6
+ * `fetch` frames a request itself and sends all of it, so some server
7
+ * behaviors can't be reached through it: what a server does with two requests
8
+ * in one write, with duplicate or malformed headers, with a request head whose
9
+ * body never follows, or with a chunked body that is never terminated. This
10
+ * writes caller-built bytes over `node:net` and hands back what the server
11
+ * sent, plus just enough parsing to read a status or one whole response.
12
+ *
13
+ * Everything is `latin1`-decoded, so a string's length is its byte length.
14
+ *
15
+ * Plain TCP only: an `https:` URL is not spoken to over TLS, and the host must
16
+ * be a name or an IPv4 address — a bracketed IPv6 literal does not connect.
17
+ *
18
+ * @module
19
+ */
20
+ import { connect } from 'node:net';
21
+ /**
22
+ * Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
23
+ * what the server sends back until it closes the connection, a whole response
24
+ * has arrived (`until: 'response'`), or `read_timeout_ms` elapses.
25
+ *
26
+ * Write errors are swallowed: a server that refuses a request and closes
27
+ * mid-upload surfaces as `EPIPE` / `ECONNRESET` on the unfinished write, and
28
+ * what a caller asserts on is what was read back. A write error also ends the
29
+ * socket, so bytes the server sent just before it can go unread — a caller
30
+ * that needs to read a refusal reliably sends nothing the server will leave
31
+ * unread.
32
+ *
33
+ * @param base_url - the server's base URL; only its host and port are used
34
+ * @param request_bytes - the bytes to send, `latin1`-encoded
35
+ */
36
+ export const raw_http_exchange = (base_url, request_bytes, options) => new Promise((resolve) => {
37
+ const { read_timeout_ms, until = 'close' } = options;
38
+ const url = new URL(base_url);
39
+ const port = Number(url.port) || (url.protocol === 'https:' ? 443 : 80);
40
+ const socket = connect({ host: url.hostname, port });
41
+ let received = '';
42
+ let settled = false;
43
+ const finish = (closed_by_server) => {
44
+ if (settled)
45
+ return;
46
+ settled = true;
47
+ clearTimeout(timer);
48
+ socket.destroy();
49
+ resolve({ received, closed_by_server });
50
+ };
51
+ const timer = setTimeout(() => finish(false), read_timeout_ms);
52
+ socket.setEncoding('latin1');
53
+ socket.on('connect', () => socket.write(request_bytes, 'latin1'));
54
+ socket.on('data', (chunk) => {
55
+ received += chunk;
56
+ if (until === 'response' && raw_http_parse_response(received))
57
+ finish(false);
58
+ });
59
+ socket.on('error', () => { }); // EPIPE/ECONNRESET on a mid-write close is expected
60
+ socket.on('close', () => finish(true));
61
+ });
62
+ /**
63
+ * Count HTTP response status lines in a raw byte stream. Deliberately
64
+ * **unanchored** (no `^`/`m`): a second pipelined response is concatenated
65
+ * straight after the first response's body, which carries no trailing newline,
66
+ * so a line-anchored match would miss it — and missing a smuggled second
67
+ * response is a silent false negative. It counts exactly the status lines as
68
+ * long as no response header or body contains the literal `HTTP/1.x NNN`.
69
+ */
70
+ export const raw_http_count_responses = (raw) => (raw.match(/HTTP\/1\.[01] \d{3}/g) ?? []).length;
71
+ /** The status code of the first HTTP response in a raw byte stream, or `null` if none. */
72
+ export const raw_http_first_status = (raw) => {
73
+ const match = /HTTP\/1\.[01] (\d{3})/.exec(raw);
74
+ return match ? Number(match[1]) : null;
75
+ };
76
+ /**
77
+ * Remove chunked framing from a body.
78
+ *
79
+ * @returns the decoded body, or `null` when the terminating chunk hasn't arrived
80
+ */
81
+ const decode_chunked = (raw) => {
82
+ let body = '';
83
+ let offset = 0;
84
+ for (;;) {
85
+ const line_end = raw.indexOf('\r\n', offset);
86
+ if (line_end === -1)
87
+ return null;
88
+ // a chunk-size line may carry `;extension`s after the hex size
89
+ const size = Number.parseInt(raw.slice(offset, line_end), 16);
90
+ if (!Number.isFinite(size))
91
+ return null;
92
+ const data_start = line_end + 2;
93
+ // the last chunk is followed by optional trailers and an empty line
94
+ if (size === 0)
95
+ return raw.includes('\r\n\r\n', line_end) ? body : null;
96
+ if (raw.length < data_start + size + 2)
97
+ return null;
98
+ body += raw.slice(data_start, data_start + size);
99
+ offset = data_start + size + 2;
100
+ }
101
+ };
102
+ /**
103
+ * Parse the first response in a raw byte stream, once all of it has arrived.
104
+ *
105
+ * The body is delimited by `Transfer-Encoding: chunked` or `Content-Length`.
106
+ * A response carrying neither is delimited by the connection closing, which
107
+ * the bytes alone don't show, so its body is whatever followed the head.
108
+ *
109
+ * @param raw - the bytes read so far, `latin1`-decoded
110
+ * @returns the response, or `null` when the head or the framed body is incomplete
111
+ */
112
+ export const raw_http_parse_response = (raw) => {
113
+ const head_end = raw.indexOf('\r\n\r\n');
114
+ if (head_end === -1)
115
+ return null;
116
+ const [status_line, ...header_lines] = raw.slice(0, head_end).split('\r\n');
117
+ const status_match = /^HTTP\/1\.[01] (\d{3})/.exec(status_line);
118
+ if (!status_match)
119
+ return null;
120
+ const headers = {};
121
+ for (const line of header_lines) {
122
+ const colon = line.indexOf(':');
123
+ if (colon <= 0)
124
+ continue;
125
+ headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
126
+ }
127
+ const status = Number(status_match[1]);
128
+ const rest = raw.slice(head_end + 4);
129
+ if (headers['transfer-encoding']?.toLowerCase().includes('chunked')) {
130
+ const body = decode_chunked(rest);
131
+ return body === null ? null : { status, headers, body };
132
+ }
133
+ const content_length = headers['content-length'];
134
+ if (content_length !== undefined) {
135
+ const length = Number(content_length);
136
+ if (!Number.isSafeInteger(length) || length < 0 || rest.length < length)
137
+ return null;
138
+ return { status, headers, body: rest.slice(0, length) };
139
+ }
140
+ return { status, headers, body: rest };
141
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.135.0",
3
+ "version": "0.136.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",