@fuzdev/fuz_app 0.86.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.
- package/dist/actions/action_rpc.js +1 -1
- package/dist/actions/register_action_ws.js +1 -1
- package/dist/auth/CLAUDE.md +15 -0
- package/dist/auth/account_actions.js +1 -1
- package/dist/auth/account_route_schema.d.ts +146 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -0
- package/dist/auth/account_route_schema.js +141 -0
- package/dist/auth/account_routes.d.ts +0 -79
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +15 -110
- package/dist/auth/audit_log_route_schema.d.ts +32 -0
- package/dist/auth/audit_log_route_schema.d.ts.map +1 -0
- package/dist/auth/audit_log_route_schema.js +36 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +2 -12
- package/dist/auth/bearer_auth.js +1 -1
- package/dist/auth/bootstrap_route_schema.d.ts +85 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -0
- package/dist/auth/bootstrap_route_schema.js +56 -0
- package/dist/auth/bootstrap_routes.d.ts +0 -20
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +4 -35
- package/dist/auth/signup_route_schema.d.ts +53 -0
- package/dist/auth/signup_route_schema.d.ts.map +1 -0
- package/dist/auth/signup_route_schema.js +59 -0
- package/dist/auth/signup_routes.d.ts +0 -26
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +8 -40
- package/dist/http/CLAUDE.md +2 -1
- package/dist/http/client_ip.d.ts +15 -0
- package/dist/http/client_ip.d.ts.map +1 -0
- package/dist/http/client_ip.js +13 -0
- package/dist/http/proxy.d.ts +0 -7
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +0 -7
- package/dist/realtime/sse.d.ts +0 -2
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse.js +1 -2
- package/dist/realtime/sse_constants.d.ts +17 -0
- package/dist/realtime/sse_constants.d.ts.map +1 -0
- package/dist/realtime/sse_constants.js +16 -0
- package/dist/testing/CLAUDE.md +6 -3
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +1 -1
- package/dist/testing/app_server.d.ts +0 -15
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +1 -15
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +1 -1
- package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -5
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +1 -1
- package/dist/testing/cross_backend/actor_lookup.d.ts +5 -5
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_search.d.ts +3 -3
- package/dist/testing/cross_backend/actor_search.d.ts.map +1 -1
- package/dist/testing/cross_backend/app_settings.d.ts +3 -3
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +10 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -0
- package/dist/testing/cross_backend/body_size.js +137 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +10 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -0
- package/dist/testing/cross_backend/body_size_smuggling.js +138 -0
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts +0 -11
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +1 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +1 -1
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +8 -6
- package/dist/testing/cross_backend/fact_serving.d.ts +2 -3
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.d.ts +143 -0
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/in_process_setup.js +166 -0
- package/dist/testing/cross_backend/origin.d.ts +5 -5
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/ready.d.ts +2 -7
- package/dist/testing/cross_backend/ready.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.d.ts +54 -135
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +11 -171
- package/dist/testing/cross_backend/sse_round_trip.js +1 -1
- package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -2
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +2 -1
- package/dist/testing/integration.js +2 -2
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +2 -1
- package/dist/testing/sse_round_trip.d.ts +1 -1
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +7 -9
- package/dist/testing/test_credentials.d.ts +23 -0
- package/dist/testing/test_credentials.d.ts.map +1 -0
- package/dist/testing/test_credentials.js +22 -0
- 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/
|
|
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/
|
|
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';
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -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 './
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,
|
|
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 {
|
|
27
|
-
import {
|
|
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/
|
|
33
|
+
import { get_client_ip } from '../http/client_ip.js';
|
|
35
34
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
36
|
-
import {
|
|
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
|
-
|
|
74
|
-
path: options?.path ??
|
|
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
|
-
//
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|