@fuzdev/fuz_app 0.87.0 → 0.88.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 (69) hide show
  1. package/dist/actions/action_rpc.js +1 -1
  2. package/dist/actions/register_action_ws.js +1 -1
  3. package/dist/auth/CLAUDE.md +15 -0
  4. package/dist/auth/account_actions.js +1 -1
  5. package/dist/auth/account_route_schema.d.ts +146 -0
  6. package/dist/auth/account_route_schema.d.ts.map +1 -0
  7. package/dist/auth/account_route_schema.js +141 -0
  8. package/dist/auth/account_routes.d.ts +0 -79
  9. package/dist/auth/account_routes.d.ts.map +1 -1
  10. package/dist/auth/account_routes.js +15 -110
  11. package/dist/auth/audit_log_route_schema.d.ts +32 -0
  12. package/dist/auth/audit_log_route_schema.d.ts.map +1 -0
  13. package/dist/auth/audit_log_route_schema.js +36 -0
  14. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  15. package/dist/auth/audit_log_routes.js +2 -12
  16. package/dist/auth/bearer_auth.js +1 -1
  17. package/dist/auth/bootstrap_route_schema.d.ts +85 -0
  18. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -0
  19. package/dist/auth/bootstrap_route_schema.js +56 -0
  20. package/dist/auth/bootstrap_routes.d.ts +0 -20
  21. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  22. package/dist/auth/bootstrap_routes.js +4 -35
  23. package/dist/auth/signup_route_schema.d.ts +53 -0
  24. package/dist/auth/signup_route_schema.d.ts.map +1 -0
  25. package/dist/auth/signup_route_schema.js +59 -0
  26. package/dist/auth/signup_routes.d.ts +0 -26
  27. package/dist/auth/signup_routes.d.ts.map +1 -1
  28. package/dist/auth/signup_routes.js +8 -40
  29. package/dist/http/CLAUDE.md +2 -1
  30. package/dist/http/client_ip.d.ts +15 -0
  31. package/dist/http/client_ip.d.ts.map +1 -0
  32. package/dist/http/client_ip.js +13 -0
  33. package/dist/http/proxy.d.ts +0 -7
  34. package/dist/http/proxy.d.ts.map +1 -1
  35. package/dist/http/proxy.js +0 -7
  36. package/dist/realtime/sse.d.ts +0 -2
  37. package/dist/realtime/sse.d.ts.map +1 -1
  38. package/dist/realtime/sse.js +1 -2
  39. package/dist/realtime/sse_constants.d.ts +17 -0
  40. package/dist/realtime/sse_constants.d.ts.map +1 -0
  41. package/dist/realtime/sse_constants.js +16 -0
  42. package/dist/testing/admin_integration.d.ts.map +1 -1
  43. package/dist/testing/admin_integration.js +1 -1
  44. package/dist/testing/app_server.d.ts +0 -15
  45. package/dist/testing/app_server.d.ts.map +1 -1
  46. package/dist/testing/app_server.js +1 -15
  47. package/dist/testing/audit_completeness.d.ts.map +1 -1
  48. package/dist/testing/audit_completeness.js +1 -1
  49. package/dist/testing/cross_backend/in_process_setup.d.ts +143 -0
  50. package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -0
  51. package/dist/testing/cross_backend/in_process_setup.js +166 -0
  52. package/dist/testing/cross_backend/setup.d.ts +31 -140
  53. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  54. package/dist/testing/cross_backend/setup.js +11 -171
  55. package/dist/testing/cross_backend/sse_round_trip.js +1 -1
  56. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/testing_reset_actions.js +2 -1
  58. package/dist/testing/integration.js +2 -2
  59. package/dist/testing/middleware.d.ts.map +1 -1
  60. package/dist/testing/middleware.js +2 -1
  61. package/dist/testing/sse_round_trip.d.ts +1 -1
  62. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  63. package/dist/testing/sse_round_trip.js +1 -1
  64. package/dist/testing/stubs.d.ts.map +1 -1
  65. package/dist/testing/stubs.js +7 -9
  66. package/dist/testing/test_credentials.d.ts +23 -0
  67. package/dist/testing/test_credentials.d.ts.map +1 -0
  68. package/dist/testing/test_credentials.js +22 -0
  69. package/package.json +2 -2
@@ -14,7 +14,7 @@
14
14
  import { z } from 'zod';
15
15
  import { DEV } from 'esm-env';
16
16
  import {} from '../http/route_spec.js';
17
- import { get_client_ip } from '../http/proxy.js';
17
+ import { get_client_ip } from '../http/client_ip.js';
18
18
  import { get_request_context, } from '../auth/request_context.js';
19
19
  import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
20
20
  import { compile_action_registry } from './compile_action_registry.js';
@@ -30,7 +30,7 @@ import { wait } from '@fuzdev/fuz_util/async.js';
30
30
  import { Logger } from '@fuzdev/fuz_util/log.js';
31
31
  import { get_request_context, require_request_context, } from '../auth/request_context.js';
32
32
  import { hash_session_token } from '../auth/session_queries.js';
33
- import { get_client_ip } from '../http/proxy.js';
33
+ import { get_client_ip } from '../http/client_ip.js';
34
34
  import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
35
35
  import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
36
36
  import { create_jsonrpc_error_response, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
@@ -121,6 +121,21 @@ they track the same config. Sample via `get_*`; `reset_*` are test-only.
121
121
  - `auth/audit_log_routes.ts` — optional `GET /audit/stream` (SSE); list/history are on the RPC surface.
122
122
  - `auth/auth_guard_resolver.ts` — `fuz_auth_guard_resolver` injected into `apply_route_specs` so the framework stays auth-agnostic.
123
123
 
124
+ **Hono-free route shapes.** Each cookie/SSE-coupled route module has a sibling
125
+ `*_route_schema.ts` (`account_route_schema.ts`, `signup_route_schema.ts`,
126
+ `audit_log_route_schema.ts`, `bootstrap_route_schema.ts`) holding the I/O
127
+ schemas **and** the route _shapes_ (`Omit<RouteSpec, 'handler'>` —
128
+ method/path/auth/io/errors, via `create_*_route_shapes(options)` or a static
129
+ `*_route_shape` const). The `create_*_route_specs` factories spread a shape and
130
+ attach the live (hono-coupled) handler; a cross-process surface builder spreads
131
+ the same shape with a stub handler (surface generation never runs handlers).
132
+ Single source of truth — the shape can't drift between the live route and the
133
+ attack surface — and a backend-spawning consumer assembling its surface imports
134
+ the shapes without dragging `hono/cookie` / `hono/streaming` (and the optional
135
+ `hono` peer) onto a Rust-only cross-process suite. Shared route-limit constants
136
+ (`DEFAULT_MAX_SESSIONS` / `_TOKENS`) live in `account_route_schema.ts` for the
137
+ same reason (the RPC `account_actions` reads `DEFAULT_MAX_TOKENS`).
138
+
124
139
  **`POST /login` timing floor.** Login 401s are floored to
125
140
  `DEFAULT_LOGIN_FAIL_FLOOR_MS` (250ms) + uniform jitter (±25ms) via
126
141
  `Promise.all(work, setTimeout)` so observed time is `max(work, delay)` and
@@ -27,7 +27,7 @@ import { to_session_account } from './account_schema.js';
27
27
  import { query_session_list_for_account, query_session_revoke_for_account, query_session_revoke_all_for_account, } from './session_queries.js';
28
28
  import { query_api_token_enforce_limit, query_api_token_list_for_account, query_create_api_token, query_revoke_api_token_for_account, } from './api_token_queries.js';
29
29
  import { generate_api_token } from './api_token.js';
30
- import { DEFAULT_MAX_TOKENS } from './account_routes.js';
30
+ import { DEFAULT_MAX_TOKENS } from './account_route_schema.js';
31
31
  import { account_verify_action_spec, account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec, } from './account_action_specs.js';
32
32
  /**
33
33
  * Create the self-service account RPC actions.
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Hono-free wire schemas + route shapes for the account REST routes.
3
+ *
4
+ * Split from `account_routes.ts` (whose handlers pull `hono/cookie` via
5
+ * `session_middleware`) so cross-process test suites can build the account
6
+ * route shapes — and assert on the `POST /login` / `GET /api/account/status`
7
+ * response shapes — without dragging the in-process Hono session handler, and
8
+ * its optional `hono` peer, onto a backend-spawning consumer. `account_routes.ts`
9
+ * imports these back and attaches the live handlers; single source of truth
10
+ * for the wire shape.
11
+ *
12
+ * @module
13
+ */
14
+ import { z } from 'zod';
15
+ import type { RouteSpec } from '../http/route_spec.js';
16
+ /** Input for `GET /api/account/status`. No parameters — caller is the subject. */
17
+ export declare const AccountStatusInput: z.ZodNull;
18
+ export type AccountStatusInput = z.infer<typeof AccountStatusInput>;
19
+ /** Output for `GET /api/account/status`. */
20
+ export declare const AccountStatusOutput: z.ZodObject<{
21
+ account: z.ZodObject<{
22
+ id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
23
+ username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
24
+ email: z.ZodNullable<z.ZodEmail>;
25
+ email_verified: z.ZodBoolean;
26
+ created_at: z.ZodString;
27
+ }, z.core.$strict>;
28
+ actor: z.ZodNullable<z.ZodObject<{
29
+ id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
30
+ name: z.ZodString;
31
+ }, z.core.$strict>>;
32
+ role_grants: z.ZodArray<z.ZodObject<{
33
+ id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
34
+ role: z.ZodString;
35
+ scope_kind: z.ZodNullable<z.ZodString>;
36
+ scope_id: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
37
+ created_at: z.ZodString;
38
+ expires_at: z.ZodNullable<z.ZodString>;
39
+ granted_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
40
+ }, z.core.$strict>>;
41
+ }, z.core.$strict>;
42
+ export type AccountStatusOutput = z.infer<typeof AccountStatusOutput>;
43
+ /** Error body for `GET /api/account/status` on the unauthenticated path. */
44
+ export declare const AccountStatusUnauthenticatedError: z.ZodObject<{
45
+ error: z.ZodLiteral<"authentication_required">;
46
+ bootstrap_available: z.ZodOptional<z.ZodBoolean>;
47
+ }, z.core.$loose>;
48
+ export type AccountStatusUnauthenticatedError = z.infer<typeof AccountStatusUnauthenticatedError>;
49
+ /** Input for `POST /login`. Accepts a username or email in the `username` field. */
50
+ export declare const LoginInput: z.ZodObject<{
51
+ username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
52
+ password: z.ZodString;
53
+ }, z.core.$strict>;
54
+ export type LoginInput = z.infer<typeof LoginInput>;
55
+ /** Output for `POST /login`. Session cookie is the operative side effect. */
56
+ export declare const LoginOutput: z.ZodObject<{
57
+ ok: z.ZodLiteral<true>;
58
+ }, z.core.$strict>;
59
+ export type LoginOutput = z.infer<typeof LoginOutput>;
60
+ /** Input for `POST /logout`. Session identity flows through the cookie. */
61
+ export declare const LogoutInput: z.ZodNull;
62
+ export type LogoutInput = z.infer<typeof LogoutInput>;
63
+ /** Output for `POST /logout`. Includes the revoked account's username for UI redraw. */
64
+ export declare const LogoutOutput: z.ZodObject<{
65
+ ok: z.ZodLiteral<true>;
66
+ username: z.ZodString;
67
+ }, z.core.$strict>;
68
+ export type LogoutOutput = z.infer<typeof LogoutOutput>;
69
+ /** Input for `POST /password`. `current_password` is minimally validated; `new_password` enforces the full policy. */
70
+ export declare const PasswordChangeInput: z.ZodObject<{
71
+ current_password: z.ZodString;
72
+ new_password: z.ZodString;
73
+ }, z.core.$strict>;
74
+ export type PasswordChangeInput = z.infer<typeof PasswordChangeInput>;
75
+ /** Output for `POST /password`. Counts are returned so the UI can summarize the revoke-all cascade. */
76
+ export declare const PasswordChangeOutput: z.ZodObject<{
77
+ ok: z.ZodLiteral<true>;
78
+ sessions_revoked: z.ZodNumber;
79
+ tokens_revoked: z.ZodNumber;
80
+ }, z.core.$strict>;
81
+ export type PasswordChangeOutput = z.infer<typeof PasswordChangeOutput>;
82
+ /** Default maximum sessions per account. */
83
+ export declare const DEFAULT_MAX_SESSIONS = 5;
84
+ /** Default maximum API tokens per account. */
85
+ export declare const DEFAULT_MAX_TOKENS = 10;
86
+ /**
87
+ * The `GET /api/account/status` route shape minus its handler — pure
88
+ * hono-free data. `create_account_status_route_spec` spreads this and
89
+ * attaches the live handler (which reads the account id off the request
90
+ * context); surface generation spreads it with a stub handler.
91
+ */
92
+ export declare const account_status_route_shape: {
93
+ method: "GET";
94
+ path: string;
95
+ auth: {
96
+ account: "none";
97
+ actor: "none";
98
+ };
99
+ description: string;
100
+ input: z.ZodNull;
101
+ output: z.ZodObject<{
102
+ account: z.ZodObject<{
103
+ id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
104
+ username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
105
+ email: z.ZodNullable<z.ZodEmail>;
106
+ email_verified: z.ZodBoolean;
107
+ created_at: z.ZodString;
108
+ }, z.core.$strict>;
109
+ actor: z.ZodNullable<z.ZodObject<{
110
+ id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
111
+ name: z.ZodString;
112
+ }, z.core.$strict>>;
113
+ role_grants: z.ZodArray<z.ZodObject<{
114
+ id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
115
+ role: z.ZodString;
116
+ scope_kind: z.ZodNullable<z.ZodString>;
117
+ scope_id: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
118
+ created_at: z.ZodString;
119
+ expires_at: z.ZodNullable<z.ZodString>;
120
+ granted_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
121
+ }, z.core.$strict>>;
122
+ }, z.core.$strict>;
123
+ errors: {
124
+ 401: z.ZodObject<{
125
+ error: z.ZodLiteral<"authentication_required">;
126
+ bootstrap_available: z.ZodOptional<z.ZodBoolean>;
127
+ }, z.core.$loose>;
128
+ };
129
+ };
130
+ /** Option inputs that shape the account route metadata (not its handlers). */
131
+ export interface AccountRouteShapeOptions {
132
+ /** Whether a per-account login rate limiter is wired — toggles `/password`'s `rate_limit`. */
133
+ login_account_rate_limited: boolean;
134
+ }
135
+ /**
136
+ * The four account route shapes (`/verify`, `/login`, `/logout`, `/password`)
137
+ * minus their handlers — pure hono-free data. `create_account_route_specs`
138
+ * spreads each and attaches the live handler; cross-process surface builders
139
+ * spread them with stub handlers. Single source of truth — the shapes can't
140
+ * drift between the live routes and the surface.
141
+ *
142
+ * Returns a fixed 4-tuple `[verify, login, logout, password]` so destructuring
143
+ * yields non-optional shapes under `noUncheckedIndexedAccess`.
144
+ */
145
+ export declare const create_account_route_shapes: (options: AccountRouteShapeOptions) => [Omit<RouteSpec, "handler">, Omit<RouteSpec, "handler">, Omit<RouteSpec, "handler">, Omit<RouteSpec, "handler">];
146
+ //# sourceMappingURL=account_route_schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAoD1B,CAAC"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Hono-free wire schemas + route shapes for the account REST routes.
3
+ *
4
+ * Split from `account_routes.ts` (whose handlers pull `hono/cookie` via
5
+ * `session_middleware`) so cross-process test suites can build the account
6
+ * route shapes — and assert on the `POST /login` / `GET /api/account/status`
7
+ * response shapes — without dragging the in-process Hono session handler, and
8
+ * its optional `hono` peer, onto a backend-spawning consumer. `account_routes.ts`
9
+ * imports these back and attaches the live handlers; single source of truth
10
+ * for the wire shape.
11
+ *
12
+ * @module
13
+ */
14
+ import { z } from 'zod';
15
+ import { ActorSummaryJson, RoleGrantSummaryJson, SessionAccountJson } from './account_schema.js';
16
+ import { UsernameProvided } from '../primitive_schemas.js';
17
+ import { Password, PasswordProvided } from './password.js';
18
+ import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
19
+ /** Input for `GET /api/account/status`. No parameters — caller is the subject. */
20
+ export const AccountStatusInput = z.null();
21
+ /** Output for `GET /api/account/status`. */
22
+ export const AccountStatusOutput = z.strictObject({
23
+ account: SessionAccountJson,
24
+ actor: ActorSummaryJson.nullable(),
25
+ role_grants: z.array(RoleGrantSummaryJson),
26
+ });
27
+ /** Error body for `GET /api/account/status` on the unauthenticated path. */
28
+ export const AccountStatusUnauthenticatedError = z.looseObject({
29
+ error: z.literal(ERROR_AUTHENTICATION_REQUIRED),
30
+ bootstrap_available: z.boolean().optional(),
31
+ });
32
+ /** Input for `POST /login`. Accepts a username or email in the `username` field. */
33
+ export const LoginInput = z.strictObject({
34
+ username: UsernameProvided,
35
+ password: PasswordProvided,
36
+ });
37
+ /** Output for `POST /login`. Session cookie is the operative side effect. */
38
+ export const LoginOutput = z.strictObject({
39
+ ok: z.literal(true),
40
+ });
41
+ /** Input for `POST /logout`. Session identity flows through the cookie. */
42
+ export const LogoutInput = z.null();
43
+ /** Output for `POST /logout`. Includes the revoked account's username for UI redraw. */
44
+ export const LogoutOutput = z.strictObject({
45
+ ok: z.literal(true),
46
+ username: z.string(),
47
+ });
48
+ /** Input for `POST /password`. `current_password` is minimally validated; `new_password` enforces the full policy. */
49
+ export const PasswordChangeInput = z.strictObject({
50
+ current_password: PasswordProvided,
51
+ new_password: Password,
52
+ });
53
+ /** Output for `POST /password`. Counts are returned so the UI can summarize the revoke-all cascade. */
54
+ export const PasswordChangeOutput = z.strictObject({
55
+ ok: z.literal(true),
56
+ sessions_revoked: z.number(),
57
+ tokens_revoked: z.number(),
58
+ });
59
+ /** Default maximum sessions per account. */
60
+ export const DEFAULT_MAX_SESSIONS = 5;
61
+ /** Default maximum API tokens per account. */
62
+ export const DEFAULT_MAX_TOKENS = 10;
63
+ /**
64
+ * The `GET /api/account/status` route shape minus its handler — pure
65
+ * hono-free data. `create_account_status_route_spec` spreads this and
66
+ * attaches the live handler (which reads the account id off the request
67
+ * context); surface generation spreads it with a stub handler.
68
+ */
69
+ export const account_status_route_shape = {
70
+ method: 'GET',
71
+ path: '/api/account/status',
72
+ auth: { account: 'none', actor: 'none' },
73
+ description: 'Current account info (unauthenticated: 401 with bootstrap status)',
74
+ input: AccountStatusInput,
75
+ output: AccountStatusOutput,
76
+ errors: {
77
+ 401: AccountStatusUnauthenticatedError,
78
+ },
79
+ };
80
+ /**
81
+ * The four account route shapes (`/verify`, `/login`, `/logout`, `/password`)
82
+ * minus their handlers — pure hono-free data. `create_account_route_specs`
83
+ * spreads each and attaches the live handler; cross-process surface builders
84
+ * spread them with stub handlers. Single source of truth — the shapes can't
85
+ * drift between the live routes and the surface.
86
+ *
87
+ * Returns a fixed 4-tuple `[verify, login, logout, password]` so destructuring
88
+ * yields non-optional shapes under `noUncheckedIndexedAccess`.
89
+ */
90
+ export const create_account_route_shapes = (options) => [
91
+ {
92
+ method: 'GET',
93
+ path: '/verify',
94
+ auth: { account: 'required', actor: 'none' },
95
+ description: 'Session-validity probe for nginx auth_request (empty body, 200 or 401)',
96
+ input: z.null(),
97
+ output: z.null(),
98
+ },
99
+ {
100
+ method: 'POST',
101
+ path: '/login',
102
+ auth: { account: 'none', actor: 'none' },
103
+ description: 'Exchange credentials for session',
104
+ input: LoginInput,
105
+ output: LoginOutput,
106
+ rate_limit: 'both',
107
+ errors: {
108
+ 400: z.looseObject({
109
+ error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
110
+ }),
111
+ 401: z.looseObject({ error: z.literal(ERROR_INVALID_CREDENTIALS) }),
112
+ },
113
+ },
114
+ {
115
+ method: 'POST',
116
+ path: '/logout',
117
+ // `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
118
+ // Logout is a session-bound operation; a bearer / daemon token holds no session
119
+ // to end, so the dispatcher rejects it (403 `credential_type_required`) rather than
120
+ // returning a misleading 200 + a phantom `logout` audit row for a no-op.
121
+ auth: { account: 'required', actor: 'none', credential_types: ['session'] },
122
+ description: 'Revoke current session and clear cookie',
123
+ input: LogoutInput,
124
+ output: LogoutOutput,
125
+ },
126
+ {
127
+ method: 'POST',
128
+ path: '/password',
129
+ // `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
130
+ auth: { account: 'required', actor: 'none', credential_types: ['session'] },
131
+ description: 'Change password (revokes all sessions and API tokens)',
132
+ input: PasswordChangeInput,
133
+ output: PasswordChangeOutput,
134
+ rate_limit: options.login_account_rate_limited ? 'both' : 'ip',
135
+ errors: {
136
+ 400: z.looseObject({
137
+ error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
138
+ }),
139
+ },
140
+ },
141
+ ];
@@ -21,53 +21,11 @@
21
21
  *
22
22
  * @module
23
23
  */
24
- import { z } from 'zod';
25
24
  import type { SessionOptions } from './session_cookie.js';
26
25
  import { type RouteSpec } from '../http/route_spec.js';
27
26
  import { type RateLimiter } from '../rate_limiter.js';
28
27
  import type { RouteFactoryDeps } from './deps.js';
29
28
  import type { ConnectionCloser } from '../actions/connection_closer.js';
30
- /** Input for `GET /api/account/status`. No parameters — caller is the subject. */
31
- export declare const AccountStatusInput: z.ZodNull;
32
- export type AccountStatusInput = z.infer<typeof AccountStatusInput>;
33
- /**
34
- * Output for `GET /api/account/status` on the authenticated path.
35
- *
36
- * `account` is always populated for authenticated callers. `actor` and
37
- * `role_grants` are populated when the caller's account has a unique actor or
38
- * the request supplies `?acting=<actor_id>`; on multi-actor accounts
39
- * without an `acting` query, `actor` is `null` and `role_grants` is empty so
40
- * the frontend can show a persona picker without a separate roundtrip.
41
- */
42
- export declare const AccountStatusOutput: z.ZodObject<{
43
- account: z.ZodObject<{
44
- id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
45
- username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
46
- email: z.ZodNullable<z.ZodEmail>;
47
- email_verified: z.ZodBoolean;
48
- created_at: z.ZodString;
49
- }, z.core.$strict>;
50
- actor: z.ZodNullable<z.ZodObject<{
51
- id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
52
- name: z.ZodString;
53
- }, z.core.$strict>>;
54
- role_grants: z.ZodArray<z.ZodObject<{
55
- id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
56
- role: z.ZodString;
57
- scope_kind: z.ZodNullable<z.ZodString>;
58
- scope_id: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
59
- created_at: z.ZodString;
60
- expires_at: z.ZodNullable<z.ZodString>;
61
- granted_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
62
- }, z.core.$strict>>;
63
- }, z.core.$strict>;
64
- export type AccountStatusOutput = z.infer<typeof AccountStatusOutput>;
65
- /** Error body for `GET /api/account/status` on the unauthenticated path. */
66
- export declare const AccountStatusUnauthenticatedError: z.ZodObject<{
67
- error: z.ZodLiteral<"authentication_required">;
68
- bootstrap_available: z.ZodOptional<z.ZodBoolean>;
69
- }, z.core.$loose>;
70
- export type AccountStatusUnauthenticatedError = z.infer<typeof AccountStatusUnauthenticatedError>;
71
29
  /**
72
30
  * Create the account status route spec.
73
31
  *
@@ -91,10 +49,6 @@ export interface AccountStatusOptions {
91
49
  available: boolean;
92
50
  };
93
51
  }
94
- /** Default maximum sessions per account. */
95
- export declare const DEFAULT_MAX_SESSIONS = 5;
96
- /** Default maximum API tokens per account. */
97
- export declare const DEFAULT_MAX_TOKENS = 10;
98
52
  /**
99
53
  * Default minimum wall-clock time (ms) for a login failure (401) response.
100
54
  *
@@ -154,39 +108,6 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
154
108
  */
155
109
  connection_closer?: ConnectionCloser | null;
156
110
  }
157
- /** Input for `POST /login`. Accepts a username or email in the `username` field. */
158
- export declare const LoginInput: z.ZodObject<{
159
- username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
160
- password: z.ZodString;
161
- }, z.core.$strict>;
162
- export type LoginInput = z.infer<typeof LoginInput>;
163
- /** Output for `POST /login`. The signed session cookie is the operative side effect. */
164
- export declare const LoginOutput: z.ZodObject<{
165
- ok: z.ZodLiteral<true>;
166
- }, z.core.$strict>;
167
- export type LoginOutput = z.infer<typeof LoginOutput>;
168
- /** Input for `POST /logout`. Session identity flows through the cookie. */
169
- export declare const LogoutInput: z.ZodNull;
170
- export type LogoutInput = z.infer<typeof LogoutInput>;
171
- /** Output for `POST /logout`. Includes the revoked account's username for UI redraw. */
172
- export declare const LogoutOutput: z.ZodObject<{
173
- ok: z.ZodLiteral<true>;
174
- username: z.ZodString;
175
- }, z.core.$strict>;
176
- export type LogoutOutput = z.infer<typeof LogoutOutput>;
177
- /** Input for `POST /password`. `current_password` is minimally validated; `new_password` enforces the full policy. */
178
- export declare const PasswordChangeInput: z.ZodObject<{
179
- current_password: z.ZodString;
180
- new_password: z.ZodString;
181
- }, z.core.$strict>;
182
- export type PasswordChangeInput = z.infer<typeof PasswordChangeInput>;
183
- /** Output for `POST /password`. Counts are returned so the UI can summarize the revoke-all cascade. */
184
- export declare const PasswordChangeOutput: z.ZodObject<{
185
- ok: z.ZodLiteral<true>;
186
- sessions_revoked: z.ZodNumber;
187
- tokens_revoked: z.ZodNumber;
188
- }, z.core.$strict>;
189
- export type PasswordChangeOutput = z.infer<typeof PasswordChangeOutput>;
190
111
  /**
191
112
  * Create account route specs for session-based auth.
192
113
  *
@@ -1 +1 @@
1
- {"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA4BxD,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAGtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CAkRjB,CAAC"}
@@ -21,41 +21,18 @@
21
21
  *
22
22
  * @module
23
23
  */
24
- import { z } from 'zod';
25
24
  import { clear_session_cookie, create_session_and_set_cookie } from './session_middleware.js';
26
- import { ActorSummaryJson, RoleGrantSummaryJson, SessionAccountJson, to_session_account, } from './account_schema.js';
27
- import { UsernameProvided } from '../primitive_schemas.js';
25
+ import { RoleGrantSummaryJson, to_session_account } from './account_schema.js';
26
+ import { account_status_route_shape, create_account_route_shapes, DEFAULT_MAX_SESSIONS, } from './account_route_schema.js';
28
27
  import { hash_session_token, query_session_revoke_all_for_account, query_session_revoke_by_hash_unscoped, } from './session_queries.js';
29
28
  import { query_account_by_username_or_email, query_update_account_password, } from './account_queries.js';
30
29
  import { query_revoke_all_api_tokens_for_account } from './api_token_queries.js';
31
30
  import { build_account_context, build_request_context, get_request_context, require_request_context, resolve_acting_actor, } from './request_context.js';
32
31
  import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
33
32
  import { get_route_input } from '../http/route_spec.js';
34
- import { get_client_ip } from '../http/proxy.js';
33
+ import { get_client_ip } from '../http/client_ip.js';
35
34
  import { rate_limit_exceeded_response } from '../rate_limiter.js';
36
- import { Password, PasswordProvided } from './password.js';
37
- import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
38
- /** Input for `GET /api/account/status`. No parameters — caller is the subject. */
39
- export const AccountStatusInput = z.null();
40
- /**
41
- * Output for `GET /api/account/status` on the authenticated path.
42
- *
43
- * `account` is always populated for authenticated callers. `actor` and
44
- * `role_grants` are populated when the caller's account has a unique actor or
45
- * the request supplies `?acting=<actor_id>`; on multi-actor accounts
46
- * without an `acting` query, `actor` is `null` and `role_grants` is empty so
47
- * the frontend can show a persona picker without a separate roundtrip.
48
- */
49
- export const AccountStatusOutput = z.strictObject({
50
- account: SessionAccountJson,
51
- actor: ActorSummaryJson.nullable(),
52
- role_grants: z.array(RoleGrantSummaryJson),
53
- });
54
- /** Error body for `GET /api/account/status` on the unauthenticated path. */
55
- export const AccountStatusUnauthenticatedError = z.looseObject({
56
- error: z.literal(ERROR_AUTHENTICATION_REQUIRED),
57
- bootstrap_available: z.boolean().optional(),
58
- });
35
+ import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS } from '../http/error_schemas.js';
59
36
  /**
60
37
  * Create the account status route spec.
61
38
  *
@@ -70,15 +47,8 @@ export const AccountStatusUnauthenticatedError = z.looseObject({
70
47
  * @returns a single account status route spec
71
48
  */
72
49
  export const create_account_status_route_spec = (options) => ({
73
- method: 'GET',
74
- path: options?.path ?? '/api/account/status',
75
- auth: { account: 'none', actor: 'none' },
76
- description: 'Current account info (unauthenticated: 401 with bootstrap status)',
77
- input: AccountStatusInput,
78
- output: AccountStatusOutput,
79
- errors: {
80
- 401: AccountStatusUnauthenticatedError,
81
- },
50
+ ...account_status_route_shape,
51
+ path: options?.path ?? account_status_route_shape.path,
82
52
  handler: async (c, route) => {
83
53
  const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
84
54
  if (!account_id) {
@@ -147,10 +117,6 @@ export const create_account_status_route_spec = (options) => ({
147
117
  });
148
118
  },
149
119
  });
150
- /** Default maximum sessions per account. */
151
- export const DEFAULT_MAX_SESSIONS = 5;
152
- /** Default maximum API tokens per account. */
153
- export const DEFAULT_MAX_TOKENS = 10;
154
120
  /**
155
121
  * Default minimum wall-clock time (ms) for a login failure (401) response.
156
122
  *
@@ -176,34 +142,8 @@ const login_fail_delay = (floor_ms, jitter_ms) => {
176
142
  const jitter = jitter_ms > 0 ? Math.floor(Math.random() * (jitter_ms * 2 + 1)) - jitter_ms : 0;
177
143
  return new Promise((resolve) => setTimeout(resolve, floor_ms + jitter));
178
144
  };
179
- // -- Input/output schemas ---------------------------------------------------
180
- /** Input for `POST /login`. Accepts a username or email in the `username` field. */
181
- export const LoginInput = z.strictObject({
182
- username: UsernameProvided,
183
- password: PasswordProvided,
184
- });
185
- /** Output for `POST /login`. The signed session cookie is the operative side effect. */
186
- export const LoginOutput = z.strictObject({
187
- ok: z.literal(true),
188
- });
189
- /** Input for `POST /logout`. Session identity flows through the cookie. */
190
- export const LogoutInput = z.null();
191
- /** Output for `POST /logout`. Includes the revoked account's username for UI redraw. */
192
- export const LogoutOutput = z.strictObject({
193
- ok: z.literal(true),
194
- username: z.string(),
195
- });
196
- /** Input for `POST /password`. `current_password` is minimally validated; `new_password` enforces the full policy. */
197
- export const PasswordChangeInput = z.strictObject({
198
- current_password: PasswordProvided,
199
- new_password: Password,
200
- });
201
- /** Output for `POST /password`. Counts are returned so the UI can summarize the revoke-all cascade. */
202
- export const PasswordChangeOutput = z.strictObject({
203
- ok: z.literal(true),
204
- sessions_revoked: z.number(),
205
- tokens_revoked: z.number(),
206
- });
145
+ // `create_account_route_specs` spreads each shape and attaches the live
146
+ // handler below.
207
147
  /**
208
148
  * Create account route specs for session-based auth.
209
149
  *
@@ -218,33 +158,19 @@ export const PasswordChangeOutput = z.strictObject({
218
158
  export const create_account_route_specs = (deps, options) => {
219
159
  const { keyring, password } = deps;
220
160
  const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, connection_closer = null, } = options;
161
+ const [verify_shape, login_shape, logout_shape, password_shape] = create_account_route_shapes({
162
+ login_account_rate_limited: login_account_rate_limiter !== null,
163
+ });
221
164
  return [
222
165
  {
223
- method: 'GET',
224
- path: '/verify',
225
- auth: { account: 'required', actor: 'none' },
226
- description: 'Session-validity probe for nginx auth_request (empty body, 200 or 401)',
227
- input: z.null(),
228
- output: z.null(),
166
+ ...verify_shape,
229
167
  handler: (c) => {
230
168
  require_request_context(c);
231
169
  return c.body(null, 200);
232
170
  },
233
171
  },
234
172
  {
235
- method: 'POST',
236
- path: '/login',
237
- auth: { account: 'none', actor: 'none' },
238
- description: 'Exchange credentials for session',
239
- input: LoginInput,
240
- output: LoginOutput,
241
- rate_limit: 'both',
242
- errors: {
243
- 400: z.looseObject({
244
- error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
245
- }),
246
- 401: z.looseObject({ error: z.literal(ERROR_INVALID_CREDENTIALS) }),
247
- },
173
+ ...login_shape,
248
174
  handler: async (c, route) => {
249
175
  // Per-IP rate limit check (before any DB/password work)
250
176
  const ip = ip_rate_limiter ? get_client_ip(c) : null;
@@ -331,16 +257,7 @@ export const create_account_route_specs = (deps, options) => {
331
257
  },
332
258
  },
333
259
  {
334
- method: 'POST',
335
- path: '/logout',
336
- // `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
337
- // Logout is a session-bound operation; a bearer / daemon token holds no session
338
- // to end, so the dispatcher rejects it (403 `credential_type_required`) rather than
339
- // returning a misleading 200 + a phantom `logout` audit row for a no-op.
340
- auth: { account: 'required', actor: 'none', credential_types: ['session'] },
341
- description: 'Revoke current session and clear cookie',
342
- input: LogoutInput,
343
- output: LogoutOutput,
260
+ ...logout_shape,
344
261
  handler: async (c, route) => {
345
262
  const ctx = require_request_context(c);
346
263
  const session_token = c.get(session_options.context_key) ?? null;
@@ -377,19 +294,7 @@ export const create_account_route_specs = (deps, options) => {
377
294
  },
378
295
  },
379
296
  {
380
- method: 'POST',
381
- path: '/password',
382
- // `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
383
- auth: { account: 'required', actor: 'none', credential_types: ['session'] },
384
- description: 'Change password (revokes all sessions and API tokens)',
385
- input: PasswordChangeInput,
386
- output: PasswordChangeOutput,
387
- rate_limit: login_account_rate_limiter ? 'both' : 'ip',
388
- errors: {
389
- 400: z.looseObject({
390
- error: z.enum([ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY]),
391
- }),
392
- },
297
+ ...password_shape,
393
298
  handler: async (c, route) => {
394
299
  // per-IP rate limit check (before argon2 work)
395
300
  const ip = ip_rate_limiter ? get_client_ip(c) : null;