@fuzdev/fuz_app 0.85.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +4 -2
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +8 -4
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -3
- package/dist/hono_context.d.ts +9 -7
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/http/CLAUDE.md +14 -3
- package/dist/http/pending_effects.d.ts +43 -0
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +53 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +14 -9
- package/dist/testing/CLAUDE.md +31 -3
- package/dist/testing/cross_backend/capabilities.d.ts +20 -7
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.d.ts +6 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_backdoor.js +126 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +12 -3
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +40 -9
- package/dist/testing/cross_backend/testing_server_core.d.ts +13 -2
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_core.js +19 -7
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +5 -2
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
- package/dist/testing/mock_fs.d.ts +1 -0
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +1 -6
- package/dist/testing/surface_invariants.d.ts +34 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +49 -1
- package/dist/testing/ws_round_trip.d.ts +1 -0
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +1 -38
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -506,8 +506,10 @@ Per-message side-effect queues: `pending_effects` (eager) drains via
|
|
|
506
506
|
handlers via `emit_after_commit`) drains via `flush_post_commit_effects`.
|
|
507
507
|
Both flush in the same `try/finally` that releases the request controller,
|
|
508
508
|
so fire-and-forget audit / notification effects pushed by the handler
|
|
509
|
-
complete (or reject visibly) before the next message dispatches.
|
|
510
|
-
|
|
509
|
+
complete (or reject visibly) before the next message dispatches. The
|
|
510
|
+
deferred queue is **discarded on rollback** before it reaches that flush (a
|
|
511
|
+
rolled-back message fires no post-commit effect). See `http/CLAUDE.md`
|
|
512
|
+
§Pending Effects.
|
|
511
513
|
|
|
512
514
|
**Lifecycle hooks.** `on_socket_open({ws, connection_id, identity, notify, signal})`
|
|
513
515
|
fires after `transport.add_connection` but before the first message;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAGpC,OAAO,EAEN,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAY5B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAA+B,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE7E;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,MAAM,EAAE,SAAS,CAAC;IAClB,mGAAmG;IACnG,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,yDAAyD;IACzD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uEAAuE;IACvE,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,MAAM,EAAE,WAAW,CAAC;IACpB,sFAAsF;IACtF,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,uDAAuD;IACvD,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAC,eAAe,EAAE,cAAc,GAAG,IAAI,CAAA;KAAC,CAAC;CAClD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,gGAAgG;IAChG,EAAE,EAAE,EAAE,CAAC;IACP;;;OAGG;IACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;OAGG;IACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3C,uEAAuE;IACvE,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC5B;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,OAAO,kBAAkB,EACzB,MAAM,iBAAiB,KACrB,OAAO,CAAC,mBAAmB,CA6J7B,CAAC;AAoFF;;;GAGG;AACH,eAAO,MAAM,iCAAiC,GAC7C,IAAI,gBAAgB,EACpB,QAAQ,mBAAmB,KACzB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,gBAAgB,CAAA;CAAC,GAAG,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAC,CAK5F,CAAC"}
|
|
@@ -41,6 +41,7 @@ import { DEV } from 'esm-env';
|
|
|
41
41
|
import { apply_authorization_phase, has_any_scoped_role, } from '../auth/request_context.js';
|
|
42
42
|
import {} from '../hono_context.js';
|
|
43
43
|
import { is_void_schema } from '../http/schema_helpers.js';
|
|
44
|
+
import { dispatch_with_post_commit_rollback } from '../http/pending_effects.js';
|
|
44
45
|
import { JSONRPC_VERSION, } from '../http/jsonrpc.js';
|
|
45
46
|
import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, http_status_to_jsonrpc_error_code, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
|
|
46
47
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, } from '../http/error_schemas.js';
|
|
@@ -159,11 +160,14 @@ export const perform_action = async (input, deps) => {
|
|
|
159
160
|
}
|
|
160
161
|
return { kind: 'ok', result: output };
|
|
161
162
|
};
|
|
163
|
+
// Dispatch — transaction for mutations, pool for reads. Wrapped so a thrown
|
|
164
|
+
// handler discards the post-commit effects it queued (`emit_after_commit`):
|
|
165
|
+
// its transaction rolled back, so those effects must not announce state that
|
|
166
|
+
// never committed. The eager `pending_effects` queue survives rollback
|
|
167
|
+
// (attempt audits). See `dispatch_with_post_commit_rollback` (canonical
|
|
168
|
+
// contract) and docs/security.md §"Post-commit WS fan-out".
|
|
162
169
|
try {
|
|
163
|
-
|
|
164
|
-
return await db.transaction((tx) => execute(tx));
|
|
165
|
-
}
|
|
166
|
-
return await execute(db);
|
|
170
|
+
return await dispatch_with_post_commit_rollback(post_commit_effects, () => use_transaction ? db.transaction((tx) => execute(tx)) : execute(db));
|
|
167
171
|
}
|
|
168
172
|
catch (err) {
|
|
169
173
|
// Duck-type check: Error with numeric `code` signals a JSON-RPC error.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA0B1B,OAAO,EAA4C,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAiC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AA6BD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,EAAE,6BAavC,CAAC;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IAC/C,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA0B1B,OAAO,EAA4C,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AACpG,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAEhD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAiC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAC,EACrE,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,EACnC,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AA6BD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,EAAE,6BAavC,CAAC;AAIF;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG;IAC/C,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAodjB,CAAC"}
|
|
@@ -248,9 +248,10 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
|
|
|
248
248
|
}));
|
|
249
249
|
// Audit events are written in-transaction by query_accept_offer; wire
|
|
250
250
|
// them through `audit.notify` post-commit so SSE/WS broadcasts fire.
|
|
251
|
-
// WS notifications
|
|
252
|
-
// grantor sees "accepted" and each superseded grantor sees
|
|
253
|
-
//
|
|
251
|
+
// WS notifications ride the same deferred post-commit thunk so the
|
|
252
|
+
// grantor sees "accepted" and each superseded grantor sees "supersede"
|
|
253
|
+
// only once the accept has durably committed — and never if it rolls
|
|
254
|
+
// back (the dispatch site discards `post_commit_effects` on rollback).
|
|
254
255
|
emit_after_commit(ctx, () => {
|
|
255
256
|
fan_out_audit_events(result.audit_events, audit);
|
|
256
257
|
if (notification_sender && grantor_account_id) {
|
package/dist/hono_context.d.ts
CHANGED
|
@@ -99,13 +99,15 @@ declare module 'hono' {
|
|
|
99
99
|
*/
|
|
100
100
|
pending_effects: Array<Promise<void>>;
|
|
101
101
|
/**
|
|
102
|
-
* Post-commit thunks pushed via `emit_after_commit(ctx, fn)`. The
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
102
|
+
* Post-commit thunks pushed via `emit_after_commit(ctx, fn)`. The flush
|
|
103
|
+
* middleware invokes each thunk after the handler returns — never inline
|
|
104
|
+
* — so notifications (WS sends, etc.) cannot fire mid-transaction; the
|
|
105
|
+
* queue is also discarded when the handler's transaction rolls back, so a
|
|
106
|
+
* thunk never fires for state that never committed. Full contract (the
|
|
107
|
+
* eager-`pending_effects` contrast, the discard mechanism, the flush
|
|
108
|
+
* safety net) lives on `dispatch_with_post_commit_rollback` /
|
|
109
|
+
* `emit_after_commit` in `http/pending_effects.ts`. Producers do not push
|
|
110
|
+
* raw thunks directly. Initialized by `create_app_server`; in test mode
|
|
109
111
|
* (`await_pending_effects: true`), every thunk completes before the
|
|
110
112
|
* response returns.
|
|
111
113
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAO9D;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,mDAInB,CAAC;AAEX,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAE7D,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;;;WAMG;QACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC;;;;;;;WAOG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC
|
|
1
|
+
{"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAO9D;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,mDAInB,CAAC;AAEX,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAE7D,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;;;WAMG;QACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC;;;;;;;WAOG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC;;;;;;;;;;;;WAYG;QACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD;;;;WAIG;QACH,mBAAmB,EAAE,OAAO,CAAC;KAC7B;CACD"}
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -29,7 +29,7 @@ effects, see ../../../docs/architecture.md.
|
|
|
29
29
|
- `http/jsonrpc_helpers.ts` — message builders, type guards, input/result normalizers.
|
|
30
30
|
- `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status + surface route specs.
|
|
31
31
|
- `http/db_routes.ts` — generic keeper-only table browser route specs (public schema).
|
|
32
|
-
- `http/pending_effects.ts` — `emit_after_commit` + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
|
|
32
|
+
- `http/pending_effects.ts` — `emit_after_commit` + `dispatch_with_post_commit_rollback` (the shared rollback-discard wrapper both dispatch sites use) + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
|
|
33
33
|
|
|
34
34
|
## Route Spec System
|
|
35
35
|
|
|
@@ -427,7 +427,7 @@ this work that's already started"; thunk pushers say "run this after the
|
|
|
427
427
|
handler returns" — and burying both behind one field made
|
|
428
428
|
`c.var.pending_effects.push(x)` ambiguous at the call site. Splitting turns the field name into the contract.
|
|
429
429
|
|
|
430
|
-
### Why `emit_after_commit` defers
|
|
430
|
+
### Why `emit_after_commit` defers — and discards on rollback
|
|
431
431
|
|
|
432
432
|
The thunk shape is **load-bearing for correctness**. Pushing
|
|
433
433
|
`Promise.resolve().then(fn)` onto an eager queue — what `emit_after_commit`
|
|
@@ -437,6 +437,17 @@ would leak a notification for state that never landed. The thunk defers
|
|
|
437
437
|
the work to flush time; the `try/finally` in the flush middleware runs
|
|
438
438
|
after the handler (and any wrapping transaction) returns.
|
|
439
439
|
|
|
440
|
+
Deferral alone isn't enough: the flush runs whether the handler returned or
|
|
441
|
+
threw, so a thrown handler (rolling back its transaction) would still fire the
|
|
442
|
+
thunk it queued. So the contract is **two-sided**: both dispatch sites
|
|
443
|
+
(`http/route_spec.ts`, `actions/perform_action.ts`) wrap their handler in the
|
|
444
|
+
shared `dispatch_with_post_commit_rollback` helper, which discards
|
|
445
|
+
`post_commit_effects` on a handler throw — `emit_after_commit` thus reads as
|
|
446
|
+
**"run iff the wrapping transaction commits."** The eager `pending_effects`
|
|
447
|
+
queue is the deliberate opposite (survives rollback — attempt audits). The full
|
|
448
|
+
two-sided contract + Rust-twin parity note live on the helper's TSDoc in
|
|
449
|
+
`http/pending_effects.ts`.
|
|
450
|
+
|
|
440
451
|
```typescript
|
|
441
452
|
emit_after_commit(ctx, () => notification_sender.send_to_account(account_id, msg));
|
|
442
453
|
```
|
|
@@ -449,7 +460,7 @@ and any side effect that must run only after the transaction commits.
|
|
|
449
460
|
|
|
450
461
|
- **The flush owns the safety net.** `flush_post_commit_effects` wraps every thunk in `try/catch` and routes errors through `ctx.log.error`, so one failing send cannot starve sibling effects in the same batch nor corrupt the already-committed response. Per-thunk `try/catch` inside `emit_after_commit` would skip directly-pushed thunks (e.g. tests); centralizing the wrap in the flush closes that gap.
|
|
451
462
|
- **Test mode (`await_pending_effects: true`) flushes both queues.** Eager: `await flush_pending_effects(pending_effects, log)`. Deferred: `await flush_post_commit_effects(post_commit_effects, log)`. Both complete before the response returns. Production mode wraps the same helpers in `void ...` and threads `on_effect_error` into `flush_pending_effects`'s `on_rejection` callback for fan-out.
|
|
452
|
-
- **
|
|
463
|
+
- **Drain at the outer flush; discard at the dispatch site.** The successful-path _drain_ of both queues stays at the outer flush middleware (`server/app_server.ts`) + the per-message WS flush — adjacent, one location, no per-wrapper drain timing. What the dispatch sites (route-spec wrapper / `perform_action`) own is the _rollback discard_, via the shared `dispatch_with_post_commit_rollback` helper: on a handler throw they truncate `post_commit_effects` before the outer flush ever sees it, so the flush only drains effects from a committed handler. The eager `pending_effects` queue is never truncated — it drains regardless (attempt audits survive rollback).
|
|
453
464
|
- Structurally satisfied by both `RouteContext` (HTTP) and `ActionContext` (RPC + WS) — they share the `{log, post_commit_effects}` shape, which is why this helper lives in `http/` rather than `actions/` or `auth/`.
|
|
454
465
|
|
|
455
466
|
WS sends are **not** wrapped by `create_validated_broadcaster` (that only
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
* returns. Used for WS sends and any work that must observe a committed
|
|
18
18
|
* transaction.
|
|
19
19
|
*
|
|
20
|
+
* **Discard on rollback.** A `post_commit_effects` thunk is discarded if the
|
|
21
|
+
* handler's transaction rolls back (a thrown handler) — it must not announce
|
|
22
|
+
* state that never committed. Both dispatch sites enforce this by wrapping
|
|
23
|
+
* their handler in `dispatch_with_post_commit_rollback` (see its TSDoc below
|
|
24
|
+
* for the full two-sided contract and the Rust-twin parity note). The eager
|
|
25
|
+
* `pending_effects` queue is the deliberate opposite: its pool writes run
|
|
26
|
+
* outside the transaction and intentionally **survive** rollback (attempt
|
|
27
|
+
* audits).
|
|
28
|
+
*
|
|
20
29
|
* The split exists because the two shapes encode different contracts:
|
|
21
30
|
* eager pushers are saying "wait for this work that's already started";
|
|
22
31
|
* thunk pushers are saying "run this after the handler returns." Burying
|
|
@@ -55,11 +64,45 @@ export interface EmitAfterCommitContext {
|
|
|
55
64
|
* The flush owns the per-thunk `try/catch` + `log.error` so any
|
|
56
65
|
* directly-pushed thunk (tests included) cannot escape the safety net.
|
|
57
66
|
*
|
|
67
|
+
* Deferral is only half the contract: a queued thunk is also **discarded if
|
|
68
|
+
* the handler's transaction rolls back** — via `dispatch_with_post_commit_rollback`
|
|
69
|
+
* (see its TSDoc). So `fn` runs iff the wrapping transaction commits, never for
|
|
70
|
+
* state that rolled back. Rollback-resilient writes (attempt audits that must
|
|
71
|
+
* land even when the handler fails) belong on the eager `pending_effects` queue
|
|
72
|
+
* instead.
|
|
73
|
+
*
|
|
58
74
|
* @param ctx - context carrying `log` and the `post_commit_effects` queue
|
|
59
75
|
* @param fn - side effect to run after commit; may return `void` or `Promise<void>`
|
|
60
76
|
* @mutates `ctx.post_commit_effects` - appends `fn` verbatim
|
|
61
77
|
*/
|
|
62
78
|
export declare const emit_after_commit: (ctx: EmitAfterCommitContext, fn: () => void | Promise<void>) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Run a handler dispatch (the handler plus its wrapping `db.transaction`),
|
|
81
|
+
* discarding any `post_commit_effects` it queued via `emit_after_commit` if it
|
|
82
|
+
* throws. A thrown handler rolls back its transaction, so firing those deferred
|
|
83
|
+
* effects would announce state that never committed. On any throw the queue is
|
|
84
|
+
* truncated back to its pre-dispatch depth — **not** cleared, so entries a
|
|
85
|
+
* surrounding scope pre-seeded survive — and the error re-thrown unchanged.
|
|
86
|
+
*
|
|
87
|
+
* The eager `pending_effects` queue is deliberately never touched here: its
|
|
88
|
+
* pool writes run outside the transaction and intentionally survive rollback
|
|
89
|
+
* (attempt audits). `emit_after_commit` thus reads as "run iff the wrapping
|
|
90
|
+
* transaction commits."
|
|
91
|
+
*
|
|
92
|
+
* Both dispatch sites — the REST route wrapper (`http/route_spec.ts`) and the
|
|
93
|
+
* action dispatcher (`actions/perform_action.ts`, the RPC + WS path) — wrap
|
|
94
|
+
* their handler call in this so the discard contract lives in one place. The
|
|
95
|
+
* Rust `fuz_actions` spine pins the same contract.
|
|
96
|
+
*
|
|
97
|
+
* `post_commit_effects` is `undefined` when a handler runs without the
|
|
98
|
+
* app-server pending-effects middleware (bare route / dispatch harnesses):
|
|
99
|
+
* absent ⇒ nothing queued ⇒ nothing to discard.
|
|
100
|
+
*
|
|
101
|
+
* @param post_commit_effects - the deferred queue to truncate on throw, or `undefined`
|
|
102
|
+
* @param dispatch - invokes the handler (and any wrapping transaction)
|
|
103
|
+
* @mutates `post_commit_effects` - truncated to its pre-dispatch depth on throw
|
|
104
|
+
*/
|
|
105
|
+
export declare const dispatch_with_post_commit_rollback: <T>(post_commit_effects: Array<() => void | Promise<void>> | undefined, dispatch: () => T | Promise<T>) => Promise<Awaited<T>>;
|
|
63
106
|
/**
|
|
64
107
|
* Drain an eager `pending_effects` queue: `Promise.allSettled` the
|
|
65
108
|
* in-flight handles, route every rejection through `log.error`, and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,KAAK,sBAAsB,EAC3B,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAC5B,IAEF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,kCAAkC,GAAU,CAAC,EACzD,qBAAqB,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EAClE,UAAU,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,KAC5B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAQpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EACrC,KAAK,MAAM,EACX,eAAe,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KACtC,OAAO,CAAC,IAAI,CASd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,aAAa,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAClD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAiBd,CAAC"}
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
* returns. Used for WS sends and any work that must observe a committed
|
|
18
18
|
* transaction.
|
|
19
19
|
*
|
|
20
|
+
* **Discard on rollback.** A `post_commit_effects` thunk is discarded if the
|
|
21
|
+
* handler's transaction rolls back (a thrown handler) — it must not announce
|
|
22
|
+
* state that never committed. Both dispatch sites enforce this by wrapping
|
|
23
|
+
* their handler in `dispatch_with_post_commit_rollback` (see its TSDoc below
|
|
24
|
+
* for the full two-sided contract and the Rust-twin parity note). The eager
|
|
25
|
+
* `pending_effects` queue is the deliberate opposite: its pool writes run
|
|
26
|
+
* outside the transaction and intentionally **survive** rollback (attempt
|
|
27
|
+
* audits).
|
|
28
|
+
*
|
|
20
29
|
* The split exists because the two shapes encode different contracts:
|
|
21
30
|
* eager pushers are saying "wait for this work that's already started";
|
|
22
31
|
* thunk pushers are saying "run this after the handler returns." Burying
|
|
@@ -45,6 +54,13 @@
|
|
|
45
54
|
* The flush owns the per-thunk `try/catch` + `log.error` so any
|
|
46
55
|
* directly-pushed thunk (tests included) cannot escape the safety net.
|
|
47
56
|
*
|
|
57
|
+
* Deferral is only half the contract: a queued thunk is also **discarded if
|
|
58
|
+
* the handler's transaction rolls back** — via `dispatch_with_post_commit_rollback`
|
|
59
|
+
* (see its TSDoc). So `fn` runs iff the wrapping transaction commits, never for
|
|
60
|
+
* state that rolled back. Rollback-resilient writes (attempt audits that must
|
|
61
|
+
* land even when the handler fails) belong on the eager `pending_effects` queue
|
|
62
|
+
* instead.
|
|
63
|
+
*
|
|
48
64
|
* @param ctx - context carrying `log` and the `post_commit_effects` queue
|
|
49
65
|
* @param fn - side effect to run after commit; may return `void` or `Promise<void>`
|
|
50
66
|
* @mutates `ctx.post_commit_effects` - appends `fn` verbatim
|
|
@@ -52,6 +68,43 @@
|
|
|
52
68
|
export const emit_after_commit = (ctx, fn) => {
|
|
53
69
|
ctx.post_commit_effects.push(fn);
|
|
54
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Run a handler dispatch (the handler plus its wrapping `db.transaction`),
|
|
73
|
+
* discarding any `post_commit_effects` it queued via `emit_after_commit` if it
|
|
74
|
+
* throws. A thrown handler rolls back its transaction, so firing those deferred
|
|
75
|
+
* effects would announce state that never committed. On any throw the queue is
|
|
76
|
+
* truncated back to its pre-dispatch depth — **not** cleared, so entries a
|
|
77
|
+
* surrounding scope pre-seeded survive — and the error re-thrown unchanged.
|
|
78
|
+
*
|
|
79
|
+
* The eager `pending_effects` queue is deliberately never touched here: its
|
|
80
|
+
* pool writes run outside the transaction and intentionally survive rollback
|
|
81
|
+
* (attempt audits). `emit_after_commit` thus reads as "run iff the wrapping
|
|
82
|
+
* transaction commits."
|
|
83
|
+
*
|
|
84
|
+
* Both dispatch sites — the REST route wrapper (`http/route_spec.ts`) and the
|
|
85
|
+
* action dispatcher (`actions/perform_action.ts`, the RPC + WS path) — wrap
|
|
86
|
+
* their handler call in this so the discard contract lives in one place. The
|
|
87
|
+
* Rust `fuz_actions` spine pins the same contract.
|
|
88
|
+
*
|
|
89
|
+
* `post_commit_effects` is `undefined` when a handler runs without the
|
|
90
|
+
* app-server pending-effects middleware (bare route / dispatch harnesses):
|
|
91
|
+
* absent ⇒ nothing queued ⇒ nothing to discard.
|
|
92
|
+
*
|
|
93
|
+
* @param post_commit_effects - the deferred queue to truncate on throw, or `undefined`
|
|
94
|
+
* @param dispatch - invokes the handler (and any wrapping transaction)
|
|
95
|
+
* @mutates `post_commit_effects` - truncated to its pre-dispatch depth on throw
|
|
96
|
+
*/
|
|
97
|
+
export const dispatch_with_post_commit_rollback = async (post_commit_effects, dispatch) => {
|
|
98
|
+
const depth = post_commit_effects?.length ?? 0;
|
|
99
|
+
try {
|
|
100
|
+
return await dispatch();
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (post_commit_effects)
|
|
104
|
+
post_commit_effects.length = depth;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
55
108
|
/**
|
|
56
109
|
* Drain an eager `pending_effects` queue: `Promise.allSettled` the
|
|
57
110
|
* in-flight handles, route every rejection through `log.error`, and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAW,IAAI,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACpE,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAKjB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"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"}
|
package/dist/http/route_spec.js
CHANGED
|
@@ -16,6 +16,7 @@ import { DEV } from 'esm-env';
|
|
|
16
16
|
import { ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_ROUTE_PARAMS, ERROR_INVALID_QUERY_PARAMS, } from './error_schemas.js';
|
|
17
17
|
import { ThrownJsonrpcError, jsonrpc_error_code_to_http_status, jsonrpc_error_code_to_name, } from './jsonrpc_errors.js';
|
|
18
18
|
import { is_null_schema, merge_error_schemas } from './schema_helpers.js';
|
|
19
|
+
import { dispatch_with_post_commit_rollback } from './pending_effects.js';
|
|
19
20
|
import { assert_route_auth_acting_biconditional } from './auth_shape.js';
|
|
20
21
|
export function get_route_input(c, _schema) {
|
|
21
22
|
return c.get('validated_input');
|
|
@@ -321,17 +322,21 @@ export const apply_route_specs = (app, specs, resolve_auth_guards, log, db, auth
|
|
|
321
322
|
// Step 1: adapt RouteHandler → Handler (construct RouteContext, call spec.handler)
|
|
322
323
|
const use_transaction = spec.transaction ?? spec.method !== 'GET';
|
|
323
324
|
const inner = spec.handler;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
// Discard post-commit effects a handler queued (`emit_after_commit`) if it
|
|
326
|
+
// throws — a thrown handler rolls back its wrapping transaction, so firing
|
|
327
|
+
// those effects would announce state that never committed. The eager
|
|
328
|
+
// `pending_effects` queue (attempt audits, run outside the transaction) is
|
|
329
|
+
// untouched. Shared with the action dispatcher (actions/perform_action.ts)
|
|
330
|
+
// via `dispatch_with_post_commit_rollback`.
|
|
331
|
+
let handler = (c) => {
|
|
332
|
+
const route_context = {
|
|
327
333
|
pending_effects: c.var.pending_effects,
|
|
328
334
|
post_commit_effects: c.var.post_commit_effects,
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
db,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
335
|
+
};
|
|
336
|
+
return dispatch_with_post_commit_rollback(c.var.post_commit_effects, () => use_transaction
|
|
337
|
+
? db.transaction(async (tx) => inner(c, { db: tx, ...route_context }))
|
|
338
|
+
: inner(c, { db, ...route_context }));
|
|
339
|
+
};
|
|
335
340
|
// Step 2: output validation
|
|
336
341
|
handler = wrap_output_validation(handler, spec.output, merged_errors, log);
|
|
337
342
|
// Step 3: error catch layer
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -13,9 +13,13 @@ testing-patterns. This file is a reference index for the helpers themselves.
|
|
|
13
13
|
|
|
14
14
|
## Production guard — always the first import
|
|
15
15
|
|
|
16
|
-
Every module here starts with `import './assert_dev_env.js';`
|
|
17
|
-
from `esm-env` and throws if false, preventing production-bundle
|
|
18
|
-
|
|
16
|
+
Every runtime-reachable module here starts with `import './assert_dev_env.js';`
|
|
17
|
+
— reads `DEV` from `esm-env` and throws if false, preventing production-bundle
|
|
18
|
+
inclusion. Make this the first line in new modules. Enforced by
|
|
19
|
+
`src/test/testing/assert_dev_env_coverage.test.ts`, which fails if any module
|
|
20
|
+
omits the guard. The sole exemption is `cross_backend/make_cross_backend_project.ts`
|
|
21
|
+
— a vitest-project factory consumed by consumers' `vite.config.ts` at config
|
|
22
|
+
time (never runtime), where a throwing guard would break `vite build`.
|
|
19
23
|
|
|
20
24
|
## Stubs, factories, mocks
|
|
21
25
|
|
|
@@ -274,6 +278,7 @@ RPC / WS structural invariants (options-free, apply over `surface.rpc_endpoints`
|
|
|
274
278
|
* `assert_ws_method_descriptions_present` — every WS method on every endpoint has a non-empty `description`.
|
|
275
279
|
* `assert_ws_endpoints_include_protocol_actions` — every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`).
|
|
276
280
|
* `assert_ws_notifications_have_null_auth` — WS method `kind === 'remote_notification' ⟺ auth === null`; guards against drift between spec union and surface emitter.
|
|
281
|
+
* `assert_no_testing_methods` — no `_testing_*` backdoor action (`TESTING_METHOD_PREFIX`) appears as an RPC or WS method on the declared surface. The test-binary actions are live-mounted only; this guards against a future wiring change folding `create_testing_actions(...)` into the surface-generating registry.
|
|
277
282
|
|
|
278
283
|
Per-endpoint duplicate method names and the auth-shape biconditional are
|
|
279
284
|
already enforced at startup by `compile_action_registry` (see
|
|
@@ -1208,6 +1213,29 @@ spine bootstrap (auth + cell + cell_history + fact) and is regenerated +
|
|
|
1208
1213
|
drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
|
|
1209
1214
|
(`UPDATE_SCHEMA_READY=1`, then `gro format`).
|
|
1210
1215
|
|
|
1216
|
+
### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
|
|
1217
|
+
|
|
1218
|
+
`describe_testing_backdoor_cross_tests({setup_test, capabilities, rpc_path?})` —
|
|
1219
|
+
the negative-credential parity suite for the `_testing_*` backdoor actions.
|
|
1220
|
+
For each of `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
|
|
1221
|
+
`_testing_schema_snapshot` (the three privileged writes plus the schema-dump
|
|
1222
|
+
read) it fires three principals over real HTTP: **anonymous** → 401, **session** →
|
|
1223
|
+
403 `credential_type_required`, **bearer** → 403 `credential_type_required` —
|
|
1224
|
+
proving the daemon-token gate that fences each backdoor action holds end-to-end
|
|
1225
|
+
on the real dispatcher (the spec-derived `describe_rpc_attack_surface_tests`
|
|
1226
|
+
never enumerates them because they're off the declared surface). Each method is
|
|
1227
|
+
sent with **valid** params so the session/bearer cases clear the 400
|
|
1228
|
+
input-validation phase and reach the 403 credential gate (order is
|
|
1229
|
+
401 → 400 → 403); the handler never runs. Cited property: `docs/security.md`
|
|
1230
|
+
§Test Backdoor Actions. Cross-process only (the `_testing_*` actions are
|
|
1231
|
+
mounted on the spawned binary, not the in-process app — like the ws/sse
|
|
1232
|
+
suites); ungated, since every cross backend that uses
|
|
1233
|
+
`default_cross_process_setup` mounts them. fuz_app's own wiring is
|
|
1234
|
+
`src/test/cross_backend/testing_backdoor.cross.test.ts`. The complement is the
|
|
1235
|
+
in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`,
|
|
1236
|
+
asserting each spec declares `credential_types: ['daemon_token']`) + the
|
|
1237
|
+
`assert_no_testing_methods` surface invariant.
|
|
1238
|
+
|
|
1211
1239
|
### Building a TS test-server binary — `testing_server_core.ts` + adapters
|
|
1212
1240
|
|
|
1213
1241
|
The reusable shape for standing up a **spawnable TS** cross-process test
|
|
@@ -7,19 +7,32 @@ import '../assert_dev_env.js';
|
|
|
7
7
|
export interface BackendCapabilities {
|
|
8
8
|
/**
|
|
9
9
|
* Bearer token auth (`Authorization: Bearer <token>`) is wired through
|
|
10
|
-
* the backend's middleware stack.
|
|
11
|
-
*
|
|
10
|
+
* the backend's middleware stack.
|
|
11
|
+
*
|
|
12
|
+
* **Declared for backend-shape documentation, not gating.** No suite reads
|
|
13
|
+
* this flag — the bearer-token cases in `describe_standard_integration_tests`
|
|
14
|
+
* / `describe_rate_limiting_tests` run unconditionally (every spine wires
|
|
15
|
+
* bearer auth). Fold into a typed capability taxonomy if these gain real
|
|
16
|
+
* gating readers.
|
|
12
17
|
*/
|
|
13
18
|
readonly bearer_auth: boolean;
|
|
14
19
|
/**
|
|
15
|
-
* Trusted-proxy XFF parsing is wired (`X-Forwarded-For` etc.).
|
|
16
|
-
*
|
|
17
|
-
*
|
|
20
|
+
* Trusted-proxy XFF parsing is wired (`X-Forwarded-For` etc.).
|
|
21
|
+
*
|
|
22
|
+
* **Declared for backend-shape documentation, not gating.** No suite reads
|
|
23
|
+
* this flag (there is no cross-process proxy-resolution suite); it records
|
|
24
|
+
* the proxy-default difference between the TS family (`false`) and the Rust
|
|
25
|
+
* family (`true`). Fold into a typed capability taxonomy if it gains real
|
|
26
|
+
* gating readers.
|
|
18
27
|
*/
|
|
19
28
|
readonly trusted_proxy: boolean;
|
|
20
29
|
/**
|
|
21
|
-
* Per-account login rate limiting is wired.
|
|
22
|
-
*
|
|
30
|
+
* Per-account login rate limiting is wired.
|
|
31
|
+
*
|
|
32
|
+
* **Declared for backend-shape documentation, not gating.** No suite reads
|
|
33
|
+
* this flag; the `describe_rate_limiting_tests` per-account cases are
|
|
34
|
+
* in-process-only and don't cross a process boundary. Fold into a typed
|
|
35
|
+
* capability taxonomy if it gains real gating readers.
|
|
23
36
|
*/
|
|
24
37
|
readonly login_rate_limit: boolean;
|
|
25
38
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { CellCrossTestOptions } from './cell_cross_helpers.js';
|
|
3
|
+
/** Options for the testing-backdoor negative-credential suite. */
|
|
4
|
+
export type TestingBackdoorCrossTestOptions = CellCrossTestOptions;
|
|
5
|
+
export declare const describe_testing_backdoor_cross_tests: (options: TestingBackdoorCrossTestOptions) => void;
|
|
6
|
+
//# sourceMappingURL=testing_backdoor.d.ts.map
|
|
@@ -0,0 +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;AAyD9B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,yBAAyB,CAAC;AAgElE,kEAAkE;AAClE,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CAAC;AAEnE,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAiCF,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend negative-credential suite for the `_testing_*` backdoor
|
|
4
|
+
* actions.
|
|
5
|
+
*
|
|
6
|
+
* `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
|
|
7
|
+
* `_testing_schema_snapshot` are privileged test-binary actions the
|
|
8
|
+
* production wire never exposes — three direct DB writes (full auth wipe,
|
|
9
|
+
* forged session row, raw fact insert) plus a full-schema introspection read
|
|
10
|
+
* (the highest info-leak of the set were the gate to break). Their only
|
|
11
|
+
* structural fence is the **daemon-token** credential gate on
|
|
12
|
+
* each spec's `auth` axis. A test binary live-mounts them on its RPC
|
|
13
|
+
* endpoint but keeps them off the declared surface — so the spec-derived
|
|
14
|
+
* `describe_rpc_attack_surface_tests` never enumerates them, and nothing
|
|
15
|
+
* else fires them with a non-daemon credential to prove the gate holds
|
|
16
|
+
* end-to-end. This suite does, against each impl's real auth resolution.
|
|
17
|
+
*
|
|
18
|
+
* For every backdoor method, three principals:
|
|
19
|
+
*
|
|
20
|
+
* - **anonymous** (no credential) → `401` (pre-validation auth refuses an
|
|
21
|
+
* account-less caller before anything else).
|
|
22
|
+
* - **session** (the keeper's browser-context cookie) → `403`
|
|
23
|
+
* `credential_type_required` — a session cookie, even one carrying the
|
|
24
|
+
* keeper role, tops out below the daemon-token channel.
|
|
25
|
+
* - **bearer** (the keeper's api-token, non-browser context) → `403`
|
|
26
|
+
* `credential_type_required` — same ceiling; an api token cannot reach
|
|
27
|
+
* keeper operations.
|
|
28
|
+
*
|
|
29
|
+
* Each method is sent with **valid** params so the session/bearer cases
|
|
30
|
+
* clear the dispatcher's input-validation (400) phase and actually reach the
|
|
31
|
+
* post-authorization credential gate (the order is 401 → 400 → 403); the
|
|
32
|
+
* handler never runs (the gate refuses first), so the writes never execute.
|
|
33
|
+
*
|
|
34
|
+
* Complements the spec-level gate check (which pins that each spec *declares*
|
|
35
|
+
* `credential_types: ['daemon_token']`) and the surface-absence invariant
|
|
36
|
+
* (`assert_no_testing_methods`) — this one pins the runtime 401/403 behavior
|
|
37
|
+
* on both impls. Cited property: `security.md` §Test Backdoor Actions
|
|
38
|
+
* (daemon-token-gated, off-surface, DEV-excluded).
|
|
39
|
+
*
|
|
40
|
+
* Cross-process only — the `_testing_*` actions are mounted on the spawned
|
|
41
|
+
* binary, not the in-process app — like the ws/sse suites. Wire from a
|
|
42
|
+
* `*.cross.test.ts`. Requires the standard `_testing_*` actions mounted (the
|
|
43
|
+
* same precondition `default_cross_process_setup` already imposes for its
|
|
44
|
+
* per-test `_testing_reset`); ungated, since every cross backend mounts them.
|
|
45
|
+
*
|
|
46
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
47
|
+
* cross-backend suites.
|
|
48
|
+
*
|
|
49
|
+
* @module
|
|
50
|
+
*/
|
|
51
|
+
import { describe, test, assert } from 'vitest';
|
|
52
|
+
import { ERROR_CREDENTIAL_TYPE_REQUIRED } from '../../http/error_schemas.js';
|
|
53
|
+
import { rpc_call } from '../rpc_helpers.js';
|
|
54
|
+
import { SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
55
|
+
/** A well-formed UUID that never names a real row. */
|
|
56
|
+
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
57
|
+
/**
|
|
58
|
+
* The backdoor methods + a **valid** params payload each (so the
|
|
59
|
+
* session/bearer cases reach the 403 credential gate rather than a 400 on
|
|
60
|
+
* input validation). The handlers never run — the gate refuses first.
|
|
61
|
+
*/
|
|
62
|
+
const backdoor_methods = [
|
|
63
|
+
{ method: '_testing_reset', params: {} },
|
|
64
|
+
{ method: '_testing_mint_session', params: { account_id: NIL_UUID, expires_in_seconds: -60 } },
|
|
65
|
+
{ method: '_testing_put_fact', params: { content: 'backdoor-probe' } },
|
|
66
|
+
// The schema-dump read — `exclude_tables` is optional, so `{}` is valid
|
|
67
|
+
// and clears the 400 phase like the writes above.
|
|
68
|
+
{ method: '_testing_schema_snapshot', params: {} },
|
|
69
|
+
];
|
|
70
|
+
const principals = [
|
|
71
|
+
{
|
|
72
|
+
name: 'anonymous',
|
|
73
|
+
status: 401,
|
|
74
|
+
// Fresh jar so the keeper cookie (cross-process) can't leak in.
|
|
75
|
+
resolve: (f) => ({ transport: f.fresh_transport(), headers: {} }),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'session',
|
|
79
|
+
status: 403,
|
|
80
|
+
reason: ERROR_CREDENTIAL_TYPE_REQUIRED,
|
|
81
|
+
resolve: (f) => ({ transport: f.transport, headers: f.create_session_headers() }),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'bearer',
|
|
85
|
+
status: 403,
|
|
86
|
+
reason: ERROR_CREDENTIAL_TYPE_REQUIRED,
|
|
87
|
+
// Bearer is discarded in a browser context, so suppress Origin (empty
|
|
88
|
+
// jar + no Origin) — the credential must actually resolve so the refusal
|
|
89
|
+
// lands on the credential-type gate, not on bearer-discard (→ 401).
|
|
90
|
+
resolve: (f) => ({
|
|
91
|
+
transport: f.fresh_transport({ origin: null }),
|
|
92
|
+
headers: f.create_bearer_headers(),
|
|
93
|
+
suppress_default_origin: true,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
export const describe_testing_backdoor_cross_tests = (options) => {
|
|
98
|
+
const { setup_test } = options;
|
|
99
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
100
|
+
describe('testing backdoor credential gate parity', () => {
|
|
101
|
+
for (const { method, params } of backdoor_methods) {
|
|
102
|
+
for (const principal of principals) {
|
|
103
|
+
test(`${method} rejects ${principal.name} → ${principal.status}`, async () => {
|
|
104
|
+
const fixture = await setup_test();
|
|
105
|
+
const { transport, headers, suppress_default_origin } = principal.resolve(fixture);
|
|
106
|
+
const res = await rpc_call({
|
|
107
|
+
app: transport,
|
|
108
|
+
path: rpc_path,
|
|
109
|
+
method,
|
|
110
|
+
params,
|
|
111
|
+
headers,
|
|
112
|
+
...(suppress_default_origin && { suppress_default_origin: true }),
|
|
113
|
+
});
|
|
114
|
+
const label = `${method} ${principal.name}`;
|
|
115
|
+
assert.ok(!res.ok, `${label}: expected denial (${principal.status}) but the call succeeded`);
|
|
116
|
+
assert.strictEqual(res.status, principal.status, `${label}: status`);
|
|
117
|
+
// `!res.ok` narrows `res` to the error variant for `res.error`.
|
|
118
|
+
if (principal.reason !== undefined && !res.ok) {
|
|
119
|
+
const reason = res.error.data?.reason;
|
|
120
|
+
assert.strictEqual(reason, principal.reason, `${label}: error.data.reason`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
@@ -183,9 +183,18 @@ export declare const testing_drain_effects_action_spec: {
|
|
|
183
183
|
* `_testing_mint_session` — mint an expired-by-construction server-side
|
|
184
184
|
* session for an existing account and return its signed cookie value.
|
|
185
185
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
186
|
+
* `expires_in_seconds` is **constrained negative** (`z.number().int().negative()`)
|
|
187
|
+
* so the action is structurally incapable of minting a *usable* session: it
|
|
188
|
+
* can only produce an already-backdated, already-dead `auth_session` row. The
|
|
189
|
+
* daemon-token gate + loopback binding already fence the backdoor, but the
|
|
190
|
+
* negative constraint is the make-impossible-states floor — even a misuse
|
|
191
|
+
* can't forge a valid session for an arbitrary `account_id`. The Rust mirror
|
|
192
|
+
* (`fuz_testing::create_testing_mint_session_action_spec`) enforces the same
|
|
193
|
+
* floor.
|
|
194
|
+
*
|
|
195
|
+
* The minted `auth_session` row's `expires_at` is backdated while the
|
|
196
|
+
* returned cookie's own signed payload stays valid (future). Cross-process
|
|
197
|
+
* auth resolution therefore passes the
|
|
189
198
|
* cookie-payload gate (`parse_session`) and is refused by the authoritative
|
|
190
199
|
* DB-row gate (`query_session_get_valid` — `WHERE expires_at > NOW()`) —
|
|
191
200
|
* the gate the in-process payload-expiry tests never reach and the one that
|
|
@@ -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":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAIvE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,gBAAgB,CAAC;AAkCvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACiB,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAqIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
|
|
@@ -68,6 +68,20 @@ import { auth_integration_truncate_tables } from '../db.js';
|
|
|
68
68
|
import { query_schema_snapshot, SchemaSnapshot } from '../schema_introspect.js';
|
|
69
69
|
import { query_create_actor } from '../../auth/account_queries.js';
|
|
70
70
|
import { create_test_account_with_credentials, mint_test_session, DEFAULT_TEST_PASSWORD, } from '../app_server.js';
|
|
71
|
+
/**
|
|
72
|
+
* Shared `auth` axis for every `_testing_*` action: keeper-only via the
|
|
73
|
+
* daemon-token credential, no acting actor. This is the entire structural
|
|
74
|
+
* fence on the backdoor surface (these actions run direct DB writes the
|
|
75
|
+
* production wire never exposes), so all five specs reference this one const
|
|
76
|
+
* rather than re-declaring it — a single source of truth the gate test
|
|
77
|
+
* (`testing_actions_auth.test.ts`) pins. Mirrors the Rust `DAEMON_TOKEN_ONLY`
|
|
78
|
+
* / shared `AuthSpec` in `fuz_testing`.
|
|
79
|
+
*/
|
|
80
|
+
const TESTING_ACTION_AUTH = {
|
|
81
|
+
account: 'required',
|
|
82
|
+
actor: 'none',
|
|
83
|
+
credential_types: ['daemon_token'],
|
|
84
|
+
};
|
|
71
85
|
/** Output shape for an individual seeded account (keeper or extra). */
|
|
72
86
|
const SeededAccountShape = z.strictObject({
|
|
73
87
|
account: z.strictObject({ id: Uuid, username: z.string() }),
|
|
@@ -104,7 +118,7 @@ export const testing_reset_action_spec = {
|
|
|
104
118
|
method: '_testing_reset',
|
|
105
119
|
kind: 'request_response',
|
|
106
120
|
initiator: 'frontend',
|
|
107
|
-
auth:
|
|
121
|
+
auth: TESTING_ACTION_AUTH,
|
|
108
122
|
side_effects: true,
|
|
109
123
|
input: z.strictObject({
|
|
110
124
|
extra_keeper_roles: z.array(z.string()).optional(),
|
|
@@ -154,7 +168,7 @@ export const testing_drain_effects_action_spec = {
|
|
|
154
168
|
method: '_testing_drain_effects',
|
|
155
169
|
kind: 'request_response',
|
|
156
170
|
initiator: 'frontend',
|
|
157
|
-
auth:
|
|
171
|
+
auth: TESTING_ACTION_AUTH,
|
|
158
172
|
side_effects: false,
|
|
159
173
|
input: z.void(),
|
|
160
174
|
output: z.strictObject({ ok: z.boolean() }),
|
|
@@ -165,9 +179,18 @@ export const testing_drain_effects_action_spec = {
|
|
|
165
179
|
* `_testing_mint_session` — mint an expired-by-construction server-side
|
|
166
180
|
* session for an existing account and return its signed cookie value.
|
|
167
181
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
182
|
+
* `expires_in_seconds` is **constrained negative** (`z.number().int().negative()`)
|
|
183
|
+
* so the action is structurally incapable of minting a *usable* session: it
|
|
184
|
+
* can only produce an already-backdated, already-dead `auth_session` row. The
|
|
185
|
+
* daemon-token gate + loopback binding already fence the backdoor, but the
|
|
186
|
+
* negative constraint is the make-impossible-states floor — even a misuse
|
|
187
|
+
* can't forge a valid session for an arbitrary `account_id`. The Rust mirror
|
|
188
|
+
* (`fuz_testing::create_testing_mint_session_action_spec`) enforces the same
|
|
189
|
+
* floor.
|
|
190
|
+
*
|
|
191
|
+
* The minted `auth_session` row's `expires_at` is backdated while the
|
|
192
|
+
* returned cookie's own signed payload stays valid (future). Cross-process
|
|
193
|
+
* auth resolution therefore passes the
|
|
171
194
|
* cookie-payload gate (`parse_session`) and is refused by the authoritative
|
|
172
195
|
* DB-row gate (`query_session_get_valid` — `WHERE expires_at > NOW()`) —
|
|
173
196
|
* the gate the in-process payload-expiry tests never reach and the one that
|
|
@@ -186,11 +209,19 @@ export const testing_mint_session_action_spec = {
|
|
|
186
209
|
method: '_testing_mint_session',
|
|
187
210
|
kind: 'request_response',
|
|
188
211
|
initiator: 'frontend',
|
|
189
|
-
auth:
|
|
212
|
+
auth: TESTING_ACTION_AUTH,
|
|
190
213
|
side_effects: true,
|
|
191
214
|
input: z.strictObject({
|
|
192
215
|
account_id: Uuid,
|
|
193
|
-
expires_in_seconds: z
|
|
216
|
+
expires_in_seconds: z
|
|
217
|
+
.number()
|
|
218
|
+
.int()
|
|
219
|
+
.negative()
|
|
220
|
+
.meta({
|
|
221
|
+
description: 'Seconds to offset the session row from NOW(). Constrained negative so this ' +
|
|
222
|
+
'backdoor can ONLY mint an already-expired (backdated) row — never a usable ' +
|
|
223
|
+
'session for an arbitrary account. Its sole use is the expired-session gate.',
|
|
224
|
+
}),
|
|
194
225
|
}),
|
|
195
226
|
output: z.strictObject({ session_cookie: z.string() }),
|
|
196
227
|
async: true,
|
|
@@ -223,7 +254,7 @@ export const testing_put_fact_action_spec = {
|
|
|
223
254
|
method: '_testing_put_fact',
|
|
224
255
|
kind: 'request_response',
|
|
225
256
|
initiator: 'frontend',
|
|
226
|
-
auth:
|
|
257
|
+
auth: TESTING_ACTION_AUTH,
|
|
227
258
|
side_effects: true,
|
|
228
259
|
input: z.strictObject({
|
|
229
260
|
content: z.string(),
|
|
@@ -248,7 +279,7 @@ export const testing_schema_snapshot_action_spec = {
|
|
|
248
279
|
method: '_testing_schema_snapshot',
|
|
249
280
|
kind: 'request_response',
|
|
250
281
|
initiator: 'frontend',
|
|
251
|
-
auth:
|
|
282
|
+
auth: TESTING_ACTION_AUTH,
|
|
252
283
|
side_effects: false,
|
|
253
284
|
input: z.strictObject({ exclude_tables: z.array(z.string()).optional() }),
|
|
254
285
|
output: SchemaSnapshot,
|
|
@@ -127,14 +127,25 @@ export interface StartTestingServerOptions {
|
|
|
127
127
|
/** Optional logger; defaults to a `[daemon_name]`-namespaced `Logger`. */
|
|
128
128
|
log?: LoggerType;
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Loopback bind hosts — the only ones the test binary may serve on. It ships
|
|
132
|
+
* deterministic dev secrets (fixed cookie keys + bootstrap token in
|
|
133
|
+
* `default_secrets.ts`), so binding any network-reachable interface would let
|
|
134
|
+
* anyone who knows those fixed keys forge cookies against it. An allowlist
|
|
135
|
+
* (not an `0.0.0.0`/`::` blocklist) closes the gap a concrete LAN/public
|
|
136
|
+
* interface IP — e.g. `--host 192.168.1.50` — would otherwise slip through.
|
|
137
|
+
* Covers `localhost`, the IPv4 loopback `127.0.0.0/8`, and IPv6 `::1`.
|
|
138
|
+
*/
|
|
139
|
+
export declare const is_loopback_host: (host: string) => boolean;
|
|
130
140
|
/**
|
|
131
141
|
* Boot a test-mode server using the supplied runtime adapter.
|
|
132
142
|
*
|
|
133
143
|
* Mirrors a production `start_server` at the surface level — stale-daemon
|
|
134
144
|
* check, daemon-info write, bind, graceful drain — but the app is the
|
|
135
145
|
* caller's no-domain (or domain) {@link StartTestingServerOptions.build_app}
|
|
136
|
-
* and the runtime boundary is the {@link TestingServerAdapter}. Refuses
|
|
137
|
-
* bind
|
|
146
|
+
* and the runtime boundary is the {@link TestingServerAdapter}. Refuses any
|
|
147
|
+
* non-loopback bind host (the test binary must stay on loopback — see
|
|
148
|
+
* `is_loopback_host`).
|
|
138
149
|
*/
|
|
139
150
|
export declare const start_testing_server: (options: StartTestingServerOptions) => Promise<void>;
|
|
140
151
|
//# sourceMappingURL=testing_server_core.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing_server_core.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_core.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG1E,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,iBAAiB,EAAE,gBAAgB,CAAC;IACpC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,OAAO,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,mFAAmF;IACnF,iBAAiB,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,iBAAiB,CAAC;IACpD,8EAA8E;IAC9E,KAAK,EAAE,CAAC,OAAO,EAAE;QAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,KAAK,WAAW,CAAC;IACxF,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,yBAAyB,EAAE,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAClE,oEAAoE;IACpE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,GAAG,EAAE,IAAI,CAAC;IACV,qEAAqE;IACrE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,uEAAuE;IACvE,eAAe,CAAC,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChE;AAED,gDAAgD;AAChD,MAAM,WAAW,yBAAyB;IACzC,+CAA+C;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,GAAG,CAAC,EAAE,UAAU,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"testing_server_core.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_server_core.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG1E,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,iBAAiB,EAAE,gBAAgB,CAAC;IACpC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,OAAO,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,mFAAmF;IACnF,iBAAiB,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,iBAAiB,CAAC;IACpD,8EAA8E;IAC9E,KAAK,EAAE,CAAC,OAAO,EAAE;QAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,KAAK,WAAW,CAAC;IACxF,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,yBAAyB,EAAE,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAClE,oEAAoE;IACpE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,GAAG,EAAE,IAAI,CAAC;IACV,qEAAqE;IACrE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,uEAAuE;IACvE,eAAe,CAAC,EAAE,CAAC,iBAAiB,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChE;AAED,gDAAgD;AAChD,MAAM,WAAW,yBAAyB;IACzC,+CAA+C;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,GAAG,CAAC,EAAE,UAAU,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,OAG/C,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAU,SAAS,yBAAyB,KAAG,OAAO,CAAC,IAAI,CA4D3F,CAAC"}
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
2
|
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
3
3
|
import { write_daemon_info, read_daemon_info, is_daemon_running } from '../../cli/daemon.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Loopback bind hosts — the only ones the test binary may serve on. It ships
|
|
6
|
+
* deterministic dev secrets (fixed cookie keys + bootstrap token in
|
|
7
|
+
* `default_secrets.ts`), so binding any network-reachable interface would let
|
|
8
|
+
* anyone who knows those fixed keys forge cookies against it. An allowlist
|
|
9
|
+
* (not an `0.0.0.0`/`::` blocklist) closes the gap a concrete LAN/public
|
|
10
|
+
* interface IP — e.g. `--host 192.168.1.50` — would otherwise slip through.
|
|
11
|
+
* Covers `localhost`, the IPv4 loopback `127.0.0.0/8`, and IPv6 `::1`.
|
|
12
|
+
*/
|
|
13
|
+
export const is_loopback_host = (host) => {
|
|
14
|
+
const h = host.replace(/^\[(.*)\]$/, '$1'); // unwrap an `[::1]`-style IPv6 literal
|
|
15
|
+
return h === 'localhost' || h === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
16
|
+
};
|
|
6
17
|
/**
|
|
7
18
|
* Boot a test-mode server using the supplied runtime adapter.
|
|
8
19
|
*
|
|
9
20
|
* Mirrors a production `start_server` at the surface level — stale-daemon
|
|
10
21
|
* check, daemon-info write, bind, graceful drain — but the app is the
|
|
11
22
|
* caller's no-domain (or domain) {@link StartTestingServerOptions.build_app}
|
|
12
|
-
* and the runtime boundary is the {@link TestingServerAdapter}. Refuses
|
|
13
|
-
* bind
|
|
23
|
+
* and the runtime boundary is the {@link TestingServerAdapter}. Refuses any
|
|
24
|
+
* non-loopback bind host (the test binary must stay on loopback — see
|
|
25
|
+
* `is_loopback_host`).
|
|
14
26
|
*/
|
|
15
27
|
export const start_testing_server = async (options) => {
|
|
16
28
|
const { adapter, daemon_name, host, port, app_version, build_app } = options;
|
|
17
29
|
const log = options.log ?? new Logger(`[${daemon_name}]`);
|
|
18
30
|
const { runtime } = adapter;
|
|
19
|
-
if (
|
|
20
|
-
log.error(`FATAL: binding to '${host}' exposes the test binary
|
|
21
|
-
`Use --host localhost (default)
|
|
31
|
+
if (!is_loopback_host(host)) {
|
|
32
|
+
log.error(`FATAL: binding to '${host}' exposes the test binary (which ships deterministic ` +
|
|
33
|
+
`dev secrets) beyond loopback. Use --host localhost (default), 127.0.0.1, or ::1 instead.`);
|
|
22
34
|
adapter.exit(1);
|
|
23
35
|
}
|
|
24
36
|
const stale = await read_daemon_info(runtime, daemon_name);
|
|
@@ -23,8 +23,11 @@ export declare const TS_SPINE_DIR_ENV = "FUZ_TESTING_TS_SPINE_DIR";
|
|
|
23
23
|
* Audit-log SSE stream path the TS spine binary serves (it wires
|
|
24
24
|
* `audit_log_sse`). Matches `SPINE_SSE_PATH` in `testing/cross_backend/default_spine_surface.ts`
|
|
25
25
|
* and the cross-process SSE suite's default. Scoped to the TS configs
|
|
26
|
-
* (which advertise `capabilities.sse: true`) — the
|
|
27
|
-
*
|
|
26
|
+
* (which advertise `capabilities.sse: true`) — the shared
|
|
27
|
+
* `ts_default_capabilities` stays `sse: false` because not every consumer
|
|
28
|
+
* wires `audit_log_sse`, so the default stays honest for backends that don't
|
|
29
|
+
* serve the stream. (The Rust `testing_spine_stub` does serve it and opts in
|
|
30
|
+
* via `rust_spine_stub_backend_config`.)
|
|
28
31
|
*/
|
|
29
32
|
export declare const TS_SPINE_SSE_PATH = "/api/admin/audit/stream";
|
|
30
33
|
/** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
|
|
@@ -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,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D
|
|
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,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAS3D,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,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aA6BF,CAAC"}
|
|
@@ -7,8 +7,11 @@ export const TS_SPINE_DIR_ENV = 'FUZ_TESTING_TS_SPINE_DIR';
|
|
|
7
7
|
* Audit-log SSE stream path the TS spine binary serves (it wires
|
|
8
8
|
* `audit_log_sse`). Matches `SPINE_SSE_PATH` in `testing/cross_backend/default_spine_surface.ts`
|
|
9
9
|
* and the cross-process SSE suite's default. Scoped to the TS configs
|
|
10
|
-
* (which advertise `capabilities.sse: true`) — the
|
|
11
|
-
*
|
|
10
|
+
* (which advertise `capabilities.sse: true`) — the shared
|
|
11
|
+
* `ts_default_capabilities` stays `sse: false` because not every consumer
|
|
12
|
+
* wires `audit_log_sse`, so the default stays honest for backends that don't
|
|
13
|
+
* serve the stream. (The Rust `testing_spine_stub` does serve it and opts in
|
|
14
|
+
* via `rust_spine_stub_backend_config`.)
|
|
12
15
|
*/
|
|
13
16
|
export const TS_SPINE_SSE_PATH = '/api/admin/audit/stream';
|
|
14
17
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock_fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/mock_fs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,MAAM;IACtB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,gBAAe,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,KAAG,MAsB3E,CAAC"}
|
|
1
|
+
{"version":3,"file":"mock_fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/mock_fs.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;GAKG;AAEH,MAAM,WAAW,MAAM;IACtB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,gBAAe,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,KAAG,MAsB3E,CAAC"}
|
package/dist/testing/mock_fs.js
CHANGED
|
@@ -142,6 +142,38 @@ export declare const assert_ws_endpoints_include_protocol_actions: (surface: App
|
|
|
142
142
|
* surface mocks the shape incorrectly.
|
|
143
143
|
*/
|
|
144
144
|
export declare const assert_ws_notifications_have_null_auth: (surface: AppSurface) => void;
|
|
145
|
+
/**
|
|
146
|
+
* Reserved method-name prefix for the daemon-token-gated test-backdoor
|
|
147
|
+
* actions (`_testing_reset`, `_testing_mint_session`, `_testing_put_fact`,
|
|
148
|
+
* `_testing_drain_effects`, `_testing_schema_snapshot`). Test binaries
|
|
149
|
+
* live-mount these on their RPC endpoint but they must never appear on a
|
|
150
|
+
* **declared** surface.
|
|
151
|
+
*/
|
|
152
|
+
export declare const TESTING_METHOD_PREFIX = "_testing_";
|
|
153
|
+
/**
|
|
154
|
+
* No `_testing_*` backdoor action ever appears as a method on the declared
|
|
155
|
+
* surface (RPC or WS).
|
|
156
|
+
*
|
|
157
|
+
* The test-backdoor actions (`_testing_reset` et al.) are daemon-token-gated
|
|
158
|
+
* privileged actions a consumer's test binary appends to its live RPC
|
|
159
|
+
* endpoint at assembly time — but they are deliberately excluded from
|
|
160
|
+
* surface generation (`spine_rpc_endpoints` and every consumer's
|
|
161
|
+
* `create_*_app_surface_spec` omit them) so the published attack surface,
|
|
162
|
+
* the committed `*_attack_surface.json` snapshot, and codegen never carry
|
|
163
|
+
* a backdoor. This invariant is the structural guard against a future
|
|
164
|
+
* wiring change that folds `create_testing_actions(...)` into the *declared*
|
|
165
|
+
* registry instead of the live-only append — the surface stays the
|
|
166
|
+
* authoritative "what the server exposes" map only if a backdoor can never
|
|
167
|
+
* hide in it.
|
|
168
|
+
*
|
|
169
|
+
* Pairs with the wire-level negative-credential check
|
|
170
|
+
* (`describe_testing_backdoor_cross_tests`, cross-process) and the
|
|
171
|
+
* spec-level gate check: this one pins absence-from-surface, those pin
|
|
172
|
+
* the daemon-token gate and the 401/403 behavior.
|
|
173
|
+
*
|
|
174
|
+
* @throws AssertionError naming the offending endpoint + method.
|
|
175
|
+
*/
|
|
176
|
+
export declare const assert_no_testing_methods: (surface: AppSurface) => void;
|
|
145
177
|
/**
|
|
146
178
|
* Configuration for security policy invariants.
|
|
147
179
|
*
|
|
@@ -282,7 +314,8 @@ export declare const assert_surface_invariants: (surface: AppSurface) => void;
|
|
|
282
314
|
* `actions/CLAUDE.md` §Registry compile) — these assertions cover only
|
|
283
315
|
* the contract-surface concerns that a runtime registration check
|
|
284
316
|
* cannot: empty descriptions, missing protocol-action spread on WS
|
|
285
|
-
* endpoints,
|
|
317
|
+
* endpoints, kind ⇔ auth drift on WS methods, and a `_testing_*`
|
|
318
|
+
* backdoor action leaking onto the declared surface.
|
|
286
319
|
*
|
|
287
320
|
* @throws AssertionError on the first invariant violation; the message
|
|
288
321
|
* names the offending endpoint, method, and field.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
|
|
@@ -452,6 +452,52 @@ export const assert_ws_notifications_have_null_auth = (surface) => {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
};
|
|
455
|
+
/**
|
|
456
|
+
* Reserved method-name prefix for the daemon-token-gated test-backdoor
|
|
457
|
+
* actions (`_testing_reset`, `_testing_mint_session`, `_testing_put_fact`,
|
|
458
|
+
* `_testing_drain_effects`, `_testing_schema_snapshot`). Test binaries
|
|
459
|
+
* live-mount these on their RPC endpoint but they must never appear on a
|
|
460
|
+
* **declared** surface.
|
|
461
|
+
*/
|
|
462
|
+
export const TESTING_METHOD_PREFIX = '_testing_';
|
|
463
|
+
/**
|
|
464
|
+
* No `_testing_*` backdoor action ever appears as a method on the declared
|
|
465
|
+
* surface (RPC or WS).
|
|
466
|
+
*
|
|
467
|
+
* The test-backdoor actions (`_testing_reset` et al.) are daemon-token-gated
|
|
468
|
+
* privileged actions a consumer's test binary appends to its live RPC
|
|
469
|
+
* endpoint at assembly time — but they are deliberately excluded from
|
|
470
|
+
* surface generation (`spine_rpc_endpoints` and every consumer's
|
|
471
|
+
* `create_*_app_surface_spec` omit them) so the published attack surface,
|
|
472
|
+
* the committed `*_attack_surface.json` snapshot, and codegen never carry
|
|
473
|
+
* a backdoor. This invariant is the structural guard against a future
|
|
474
|
+
* wiring change that folds `create_testing_actions(...)` into the *declared*
|
|
475
|
+
* registry instead of the live-only append — the surface stays the
|
|
476
|
+
* authoritative "what the server exposes" map only if a backdoor can never
|
|
477
|
+
* hide in it.
|
|
478
|
+
*
|
|
479
|
+
* Pairs with the wire-level negative-credential check
|
|
480
|
+
* (`describe_testing_backdoor_cross_tests`, cross-process) and the
|
|
481
|
+
* spec-level gate check: this one pins absence-from-surface, those pin
|
|
482
|
+
* the daemon-token gate and the 401/403 behavior.
|
|
483
|
+
*
|
|
484
|
+
* @throws AssertionError naming the offending endpoint + method.
|
|
485
|
+
*/
|
|
486
|
+
export const assert_no_testing_methods = (surface) => {
|
|
487
|
+
for (const ep of surface.rpc_endpoints) {
|
|
488
|
+
for (const method of ep.methods) {
|
|
489
|
+
assert.ok(!method.name.startsWith(TESTING_METHOD_PREFIX), `RPC endpoint '${ep.path}' exposes test-backdoor method '${method.name}' on the ` +
|
|
490
|
+
`declared surface — '${TESTING_METHOD_PREFIX}*' actions must be live-mounted only, ` +
|
|
491
|
+
`never folded into the surface-generating registry`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const ep of surface.ws_endpoints) {
|
|
495
|
+
for (const method of ep.methods) {
|
|
496
|
+
assert.ok(!method.name.startsWith(TESTING_METHOD_PREFIX), `WS endpoint '${ep.path}' exposes test-backdoor method '${method.name}' on the ` +
|
|
497
|
+
`declared surface — '${TESTING_METHOD_PREFIX}*' actions must be live-mounted only`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
455
501
|
/** Default patterns for sensitive REST routes that should be rate-limited. */
|
|
456
502
|
const DEFAULT_SENSITIVE_PATTERNS = [
|
|
457
503
|
/\/login$/,
|
|
@@ -637,7 +683,8 @@ export const assert_surface_invariants = (surface) => {
|
|
|
637
683
|
* `actions/CLAUDE.md` §Registry compile) — these assertions cover only
|
|
638
684
|
* the contract-surface concerns that a runtime registration check
|
|
639
685
|
* cannot: empty descriptions, missing protocol-action spread on WS
|
|
640
|
-
* endpoints,
|
|
686
|
+
* endpoints, kind ⇔ auth drift on WS methods, and a `_testing_*`
|
|
687
|
+
* backdoor action leaking onto the declared surface.
|
|
641
688
|
*
|
|
642
689
|
* @throws AssertionError on the first invariant violation; the message
|
|
643
690
|
* names the offending endpoint, method, and field.
|
|
@@ -647,6 +694,7 @@ export const assert_rpc_ws_surface_invariants = (surface) => {
|
|
|
647
694
|
assert_ws_method_descriptions_present(surface);
|
|
648
695
|
assert_ws_endpoints_include_protocol_actions(surface);
|
|
649
696
|
assert_ws_notifications_have_null_auth(surface);
|
|
697
|
+
assert_no_testing_methods(surface);
|
|
650
698
|
};
|
|
651
699
|
/**
|
|
652
700
|
* Run security policy invariants. Configurable with sensible defaults.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,41 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* In-process test helpers for WebSocket JSON-RPC round-trips.
|
|
3
|
-
*
|
|
4
|
-
* Drives `register_action_ws` without an HTTP server. Consumers supply
|
|
5
|
-
* specs + handlers; the harness constructs real `WSContext` instances
|
|
6
|
-
* backed by test-owned `send`/`close` pairs, fakes the authenticated
|
|
7
|
-
* Hono context (`request_context`, credential type, session id, api
|
|
8
|
-
* token id), and exposes a `connect()` factory returning a `WsClient`
|
|
9
|
-
* per connection.
|
|
10
|
-
*
|
|
11
|
-
* Three layers are exported:
|
|
12
|
-
*
|
|
13
|
-
* - **Primitives** (`create_fake_ws`, `create_fake_hono_context`,
|
|
14
|
-
* `create_stub_upgrade`, `MinimalActionEnvironment`,
|
|
15
|
-
* `dispatch_ws_message`) — used by fuz_app's own dispatcher tests
|
|
16
|
-
* and by consumers wiring tight one-off tests.
|
|
17
|
-
* - **Harness** (`create_ws_test_harness`, `keeper_identity`) — the
|
|
18
|
-
* high-level driver. Give it specs + handlers, get back
|
|
19
|
-
* `{transport, connect()}`. `connect()` is async and resolves after
|
|
20
|
-
* `on_socket_open` completes, so broadcasts sent immediately after
|
|
21
|
-
* `await harness.connect()` reach the client. Returns a `WsClient`
|
|
22
|
-
* (shared interface — see `transports/ws_client.ts`); the same
|
|
23
|
-
* interface is implemented by `transports/ws_transport.ts` for
|
|
24
|
-
* cross-process tests.
|
|
25
|
-
* - **Broadcast wiring** — `build_broadcast_api` for wiring a typed
|
|
26
|
-
* broadcast API against the harness's transport. Wire-frame types
|
|
27
|
-
* + predicates (`is_notification`, `is_response_for`,
|
|
28
|
-
* `JsonrpcNotificationFrame`, ...) live in `transports/ws_client.ts`
|
|
29
|
-
* so both in-process and cross-process drivers reference one source.
|
|
30
|
-
*
|
|
31
|
-
* Hono's wire upgrade is skipped — the Node test runtime has no
|
|
32
|
-
* `@hono/node-ws` adapter — but the full dispatch path is exercised
|
|
33
|
-
* (per-action auth, input validation, `ctx.notify` back to the
|
|
34
|
-
* originating socket, broadcast via `BackendWebsocketTransport`, and
|
|
35
|
-
* close-on-revoke).
|
|
36
|
-
*
|
|
37
|
-
* @module
|
|
38
|
-
*/
|
|
1
|
+
import './assert_dev_env.js';
|
|
39
2
|
import { WSContext, createWSMessageEvent, } from 'hono/ws';
|
|
40
3
|
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
41
4
|
import { create_uuid } from '@fuzdev/fuz_util/id.js';
|