@fuzdev/fuz_app 0.62.0 → 0.64.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 +139 -24
- package/dist/actions/action_rpc.d.ts +10 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +1 -1
- package/dist/actions/action_spec.d.ts +1 -1
- package/dist/actions/action_spec.js +1 -1
- package/dist/actions/connection_closer.d.ts +68 -0
- package/dist/actions/connection_closer.d.ts.map +1 -0
- package/dist/actions/connection_closer.js +41 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +1 -0
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +23 -2
- package/dist/actions/register_ws_endpoint.d.ts +11 -9
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -5
- package/dist/actions/transports_ws_auth_guard.d.ts +24 -8
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +23 -7
- package/dist/actions/ws_endpoint_spec.d.ts +119 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
- package/dist/actions/ws_endpoint_spec.js +13 -0
- package/dist/auth/CLAUDE.md +124 -39
- package/dist/auth/account_action_specs.d.ts +7 -1
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +11 -4
- package/dist/auth/account_actions.d.ts +13 -0
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +40 -5
- package/dist/auth/account_routes.d.ts +12 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +63 -12
- package/dist/auth/account_schema.d.ts +5 -5
- package/dist/auth/account_schema.js +2 -2
- package/dist/auth/actor_lookup_actions.d.ts +1 -1
- package/dist/auth/actor_lookup_actions.js +1 -1
- package/dist/auth/actor_lookup_queries.d.ts +1 -1
- package/dist/auth/actor_lookup_queries.js +1 -1
- package/dist/auth/actor_search_action_specs.d.ts +1 -1
- package/dist/auth/actor_search_action_specs.js +1 -1
- package/dist/auth/actor_search_actions.d.ts +1 -1
- package/dist/auth/actor_search_actions.js +1 -1
- package/dist/auth/actor_search_queries.d.ts +1 -1
- package/dist/auth/actor_search_queries.js +1 -1
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/admin_actions.d.ts +11 -0
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +25 -0
- package/dist/auth/all_action_spec_registries.d.ts +2 -2
- package/dist/auth/all_action_spec_registries.js +2 -2
- package/dist/auth/audit_emitter.d.ts +56 -12
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +38 -12
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.js +1 -1
- package/dist/auth/audit_log_schema.d.ts +30 -3
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +21 -3
- package/dist/auth/bootstrap_routes.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts +2 -2
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/signup_routes.d.ts +1 -1
- package/dist/auth/standard_rpc_actions.d.ts +1 -0
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +1 -0
- package/dist/env/update_env_variable.js +1 -1
- package/dist/http/CLAUDE.md +42 -26
- package/dist/http/ip_canonical.d.ts +99 -0
- package/dist/http/ip_canonical.d.ts.map +1 -0
- package/dist/http/ip_canonical.js +191 -0
- package/dist/http/origin.d.ts +13 -5
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +13 -31
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/http/proxy.d.ts +13 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +15 -23
- package/dist/http/surface.d.ts +50 -0
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +27 -1
- package/dist/primitive_schemas.d.ts +20 -4
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +25 -4
- package/dist/realtime/sse_auth_guard.d.ts +16 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +15 -3
- package/dist/server/app_backend.d.ts +66 -19
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +57 -34
- package/dist/server/app_server.d.ts +60 -0
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +95 -2
- package/dist/server/startup.d.ts.map +1 -1
- package/dist/server/startup.js +12 -0
- package/dist/testing/CLAUDE.md +91 -71
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +4 -5
- package/dist/testing/adversarial_headers.d.ts +6 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -5
- package/dist/testing/app_server.d.ts +33 -32
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +4 -13
- package/dist/testing/attack_surface.d.ts +8 -7
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +12 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +20 -6
- package/dist/testing/audit_drift_guard.d.ts +116 -0
- package/dist/testing/audit_drift_guard.d.ts.map +1 -0
- package/dist/testing/audit_drift_guard.js +134 -0
- package/dist/testing/connection_closer_helpers.d.ts +44 -0
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
- package/dist/testing/connection_closer_helpers.js +48 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +7 -9
- package/dist/testing/rate_limiting.js +4 -4
- package/dist/testing/rpc_helpers.d.ts +2 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +6 -8
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +12 -6
- package/dist/testing/stubs.d.ts +11 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -0
- package/dist/testing/surface_invariants.d.ts +66 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +103 -1
- package/dist/ui/CLAUDE.md +13 -18
- package/dist/ui/SurfaceExplorer.svelte +161 -2
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/keyed_async_slot.svelte.d.ts +1 -1
- package/dist/ui/keyed_async_slot.svelte.js +1 -1
- package/package.json +1 -1
|
@@ -90,22 +90,26 @@ export const account_session_list_action_spec = {
|
|
|
90
90
|
async: true,
|
|
91
91
|
description: 'List auth sessions for the current account.',
|
|
92
92
|
};
|
|
93
|
+
// `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
|
|
94
|
+
// A leaked bearer can otherwise compose `account_session_list` + N×revoke to
|
|
95
|
+
// reach the same effect as `account_session_revoke_all`.
|
|
93
96
|
export const account_session_revoke_action_spec = {
|
|
94
97
|
method: 'account_session_revoke',
|
|
95
98
|
kind: 'request_response',
|
|
96
99
|
initiator: 'frontend',
|
|
97
|
-
auth: { account: 'required', actor: 'none' },
|
|
100
|
+
auth: { account: 'required', actor: 'none', credential_types: ['session'] },
|
|
98
101
|
side_effects: true,
|
|
99
102
|
input: SessionRevokeInput,
|
|
100
103
|
output: SessionRevokeOutput,
|
|
101
104
|
async: true,
|
|
102
105
|
description: 'Revoke a single auth session for the current account (IDOR-guarded).',
|
|
103
106
|
};
|
|
107
|
+
// `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
|
|
104
108
|
export const account_session_revoke_all_action_spec = {
|
|
105
109
|
method: 'account_session_revoke_all',
|
|
106
110
|
kind: 'request_response',
|
|
107
111
|
initiator: 'frontend',
|
|
108
|
-
auth: { account: 'required', actor: 'none' },
|
|
112
|
+
auth: { account: 'required', actor: 'none', credential_types: ['session'] },
|
|
109
113
|
side_effects: true,
|
|
110
114
|
input: SessionRevokeAllInput,
|
|
111
115
|
output: SessionRevokeAllOutput,
|
|
@@ -113,6 +117,8 @@ export const account_session_revoke_all_action_spec = {
|
|
|
113
117
|
description: 'Revoke every auth session for the current account.',
|
|
114
118
|
};
|
|
115
119
|
/**
|
|
120
|
+
* `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
|
|
121
|
+
*
|
|
116
122
|
* `rate_limit: 'account'` bounds the burn rate of API-token creates. The
|
|
117
123
|
* outstanding-token count is already capped by `max_tokens` (via
|
|
118
124
|
* `query_api_token_enforce_limit`), but the per-account *rate* of churn
|
|
@@ -124,7 +130,7 @@ export const account_token_create_action_spec = {
|
|
|
124
130
|
method: 'account_token_create',
|
|
125
131
|
kind: 'request_response',
|
|
126
132
|
initiator: 'frontend',
|
|
127
|
-
auth: { account: 'required', actor: 'none' },
|
|
133
|
+
auth: { account: 'required', actor: 'none', credential_types: ['session'] },
|
|
128
134
|
side_effects: true,
|
|
129
135
|
input: TokenCreateInput,
|
|
130
136
|
output: TokenCreateOutput,
|
|
@@ -143,11 +149,12 @@ export const account_token_list_action_spec = {
|
|
|
143
149
|
async: true,
|
|
144
150
|
description: 'List API tokens for the current account. Hashes are never returned.',
|
|
145
151
|
};
|
|
152
|
+
// `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
|
|
146
153
|
export const account_token_revoke_action_spec = {
|
|
147
154
|
method: 'account_token_revoke',
|
|
148
155
|
kind: 'request_response',
|
|
149
156
|
initiator: 'frontend',
|
|
150
|
-
auth: { account: 'required', actor: 'none' },
|
|
157
|
+
auth: { account: 'required', actor: 'none', credential_types: ['session'] },
|
|
151
158
|
side_effects: true,
|
|
152
159
|
input: TokenRevokeInput,
|
|
153
160
|
output: TokenRevokeOutput,
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
import { type RpcAction } from '../actions/action_rpc.js';
|
|
26
|
+
import type { ConnectionCloser } from '../actions/connection_closer.js';
|
|
26
27
|
import type { RouteFactoryDeps } from './deps.js';
|
|
27
28
|
/** Options for `create_account_actions`. */
|
|
28
29
|
export interface AccountActionOptions {
|
|
@@ -33,6 +34,18 @@ export interface AccountActionOptions {
|
|
|
33
34
|
* `DEFAULT_MAX_TOKENS`; pass `null` to disable the cap.
|
|
34
35
|
*/
|
|
35
36
|
max_tokens?: number | null;
|
|
37
|
+
/**
|
|
38
|
+
* Live-connection closer — when set, `account_session_revoke` /
|
|
39
|
+
* `_session_revoke_all` / `account_token_revoke` handlers eagerly close
|
|
40
|
+
* affected WebSocket sockets BEFORE emitting the corresponding audit
|
|
41
|
+
* event. Closes the audit-failure-leaks-WS surface: the listener-based
|
|
42
|
+
* close (`transports_ws_auth_guard`) only fires after the audit INSERT
|
|
43
|
+
* succeeds, so a DB error would leave live sockets stale. `BackendWebsocketTransport`
|
|
44
|
+
* satisfies this interface structurally; consumers pass their transport
|
|
45
|
+
* instance directly. When absent, only the listener-based close runs.
|
|
46
|
+
* Mirrors `zzz_server`'s handler-side `close_sockets_for_*` calls.
|
|
47
|
+
*/
|
|
48
|
+
connection_closer?: ConnectionCloser | null;
|
|
36
49
|
}
|
|
37
50
|
/**
|
|
38
51
|
* Create the self-service account RPC actions.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAqC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAqC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5F,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAetE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAwBhD,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,CAAC,EAC7C,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CAyIjB,CAAC"}
|
|
@@ -39,7 +39,7 @@ import { account_verify_action_spec, account_session_list_action_spec, account_s
|
|
|
39
39
|
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
40
40
|
*/
|
|
41
41
|
export const create_account_actions = (deps, options = {}) => {
|
|
42
|
-
const { max_tokens = DEFAULT_MAX_TOKENS } = options;
|
|
42
|
+
const { max_tokens = DEFAULT_MAX_TOKENS, connection_closer = null } = options;
|
|
43
43
|
const verify_handler = (_input, ctx) => {
|
|
44
44
|
return to_session_account(ctx.auth.account);
|
|
45
45
|
};
|
|
@@ -49,22 +49,49 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
49
49
|
};
|
|
50
50
|
const session_revoke_handler = async (input, ctx) => {
|
|
51
51
|
const revoked = await query_session_revoke_for_account(ctx, input.session_id, ctx.auth.account.id);
|
|
52
|
+
// Handler-side belt+suspenders: close the live WS socket bound to this
|
|
53
|
+
// session BEFORE the audit emit, so revocation lands even if the audit
|
|
54
|
+
// INSERT fails. The real ordering invariant is "before the transaction
|
|
55
|
+
// commits": this handler runs inside the dispatcher's transaction
|
|
56
|
+
// (side_effects: true), so any throw between this close and the return
|
|
57
|
+
// would roll back the DB revoke while leaving the socket severed. That
|
|
58
|
+
// is benign — the session is still valid, the client reconnects — but
|
|
59
|
+
// don't introduce a throw here without acknowledging the trade.
|
|
60
|
+
// Only fire on success — failure carries an attacker-guessable
|
|
61
|
+
// session_id and the listener-based close already ignores failure
|
|
62
|
+
// outcomes for the same reason. Idempotent — the audit listener runs a
|
|
63
|
+
// second close on success but matches no sockets the second time.
|
|
64
|
+
if (revoked && connection_closer) {
|
|
65
|
+
connection_closer.close_sockets_for_session(input.session_id);
|
|
66
|
+
}
|
|
52
67
|
deps.audit.emit(ctx, {
|
|
53
68
|
event_type: 'session_revoke',
|
|
54
69
|
outcome: revoked ? 'success' : 'failure',
|
|
55
70
|
account_id: ctx.auth.account.id,
|
|
56
71
|
ip: ctx.client_ip,
|
|
57
|
-
|
|
72
|
+
// `credential_type` defense in depth — see `docs/security.md` §Credential-channel gating.
|
|
73
|
+
metadata: { session_id: input.session_id, credential_type: ctx.credential_type ?? undefined },
|
|
58
74
|
});
|
|
59
75
|
return { ok: true, revoked };
|
|
60
76
|
};
|
|
61
77
|
const session_revoke_all_handler = async (_input, ctx) => {
|
|
62
78
|
const count = await query_session_revoke_all_for_account(ctx, ctx.auth.account.id);
|
|
79
|
+
// Handler-side belt+suspenders — see session_revoke_handler comment.
|
|
80
|
+
// Close fires regardless of `count` (today `count >= 1` always — the
|
|
81
|
+
// caller is using the session they're revoking; future bearer / daemon-
|
|
82
|
+
// token-credentialed callers may hit `count: 0`). Symmetric with the
|
|
83
|
+
// admin revoke-all handlers in `admin_actions.ts`, where `count: 0` is
|
|
84
|
+
// a real outcome (target account had no live sessions/tokens) and the
|
|
85
|
+
// eager close still fires to scrub sockets that the audit listener
|
|
86
|
+
// would otherwise miss when the INSERT fails. Idempotent at all counts.
|
|
87
|
+
if (connection_closer) {
|
|
88
|
+
connection_closer.close_sockets_for_account(ctx.auth.account.id);
|
|
89
|
+
}
|
|
63
90
|
deps.audit.emit(ctx, {
|
|
64
91
|
event_type: 'session_revoke_all',
|
|
65
92
|
account_id: ctx.auth.account.id,
|
|
66
93
|
ip: ctx.client_ip,
|
|
67
|
-
metadata: { count },
|
|
94
|
+
metadata: { count, credential_type: ctx.credential_type ?? undefined },
|
|
68
95
|
});
|
|
69
96
|
return { ok: true, count };
|
|
70
97
|
};
|
|
@@ -78,7 +105,11 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
78
105
|
event_type: 'token_create',
|
|
79
106
|
account_id: ctx.auth.account.id,
|
|
80
107
|
ip: ctx.client_ip,
|
|
81
|
-
metadata: {
|
|
108
|
+
metadata: {
|
|
109
|
+
token_id: id,
|
|
110
|
+
name: input.name,
|
|
111
|
+
credential_type: ctx.credential_type ?? undefined,
|
|
112
|
+
},
|
|
82
113
|
});
|
|
83
114
|
return { ok: true, token, id, name: input.name };
|
|
84
115
|
};
|
|
@@ -88,12 +119,16 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
88
119
|
};
|
|
89
120
|
const token_revoke_handler = async (input, ctx) => {
|
|
90
121
|
const revoked = await query_revoke_api_token_for_account(ctx, input.token_id, ctx.auth.account.id);
|
|
122
|
+
// Handler-side belt+suspenders — see session_revoke_handler comment.
|
|
123
|
+
if (revoked && connection_closer) {
|
|
124
|
+
connection_closer.close_sockets_for_token(input.token_id);
|
|
125
|
+
}
|
|
91
126
|
deps.audit.emit(ctx, {
|
|
92
127
|
event_type: 'token_revoke',
|
|
93
128
|
outcome: revoked ? 'success' : 'failure',
|
|
94
129
|
account_id: ctx.auth.account.id,
|
|
95
130
|
ip: ctx.client_ip,
|
|
96
|
-
metadata: { token_id: input.token_id },
|
|
131
|
+
metadata: { token_id: input.token_id, credential_type: ctx.credential_type ?? undefined },
|
|
97
132
|
});
|
|
98
133
|
return { ok: true, revoked };
|
|
99
134
|
};
|
|
@@ -26,6 +26,7 @@ import type { SessionOptions } from './session_cookie.js';
|
|
|
26
26
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
27
27
|
import { type RateLimiter } from '../rate_limiter.js';
|
|
28
28
|
import type { RouteFactoryDeps } from './deps.js';
|
|
29
|
+
import type { ConnectionCloser } from '../actions/connection_closer.js';
|
|
29
30
|
/** Input for `GET /api/account/status`. No parameters — caller is the subject. */
|
|
30
31
|
export declare const AccountStatusInput: z.ZodNull;
|
|
31
32
|
export type AccountStatusInput = z.infer<typeof AccountStatusInput>;
|
|
@@ -41,7 +42,7 @@ export type AccountStatusInput = z.infer<typeof AccountStatusInput>;
|
|
|
41
42
|
export declare const AccountStatusOutput: z.ZodObject<{
|
|
42
43
|
account: z.ZodObject<{
|
|
43
44
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
44
|
-
username: z.ZodString
|
|
45
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
45
46
|
email: z.ZodNullable<z.ZodEmail>;
|
|
46
47
|
email_verified: z.ZodBoolean;
|
|
47
48
|
created_at: z.ZodString;
|
|
@@ -143,10 +144,19 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
|
143
144
|
* jitter while keeping the floor. Default `DEFAULT_LOGIN_FAIL_JITTER_MS`.
|
|
144
145
|
*/
|
|
145
146
|
login_fail_jitter_ms?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Live-connection closer — when set, the `logout` and `password` handlers
|
|
149
|
+
* eagerly close affected WebSocket sockets for the account BEFORE
|
|
150
|
+
* emitting the corresponding audit event. Mirrors the self-service
|
|
151
|
+
* action surface (see `AccountActionOptions.connection_closer`). When
|
|
152
|
+
* absent, only the listener-based close (`transports_ws_auth_guard` on
|
|
153
|
+
* `audit.on_event_chain`) runs.
|
|
154
|
+
*/
|
|
155
|
+
connection_closer?: ConnectionCloser | null;
|
|
146
156
|
}
|
|
147
157
|
/** Input for `POST /login`. Accepts a username or email in the `username` field. */
|
|
148
158
|
export declare const LoginInput: z.ZodObject<{
|
|
149
|
-
username: z.ZodString
|
|
159
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
150
160
|
password: z.ZodString;
|
|
151
161
|
}, z.core.$strict>;
|
|
152
162
|
export type LoginInput = z.infer<typeof LoginInput>;
|
|
@@ -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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA2BxD,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,7 +29,7 @@ import { hash_session_token, query_session_revoke_all_for_account, query_session
|
|
|
29
29
|
import { query_account_by_username_or_email, query_update_account_password, } from './account_queries.js';
|
|
30
30
|
import { query_revoke_all_api_tokens_for_account } from './api_token_queries.js';
|
|
31
31
|
import { build_account_context, build_request_context, get_request_context, require_request_context, resolve_acting_actor, } from './request_context.js';
|
|
32
|
-
import { ACCOUNT_ID_KEY } from '../hono_context.js';
|
|
32
|
+
import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
33
33
|
import { get_route_input } from '../http/route_spec.js';
|
|
34
34
|
import { get_client_ip } from '../http/proxy.js';
|
|
35
35
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
@@ -217,7 +217,7 @@ export const PasswordChangeOutput = z.strictObject({
|
|
|
217
217
|
*/
|
|
218
218
|
export const create_account_route_specs = (deps, options) => {
|
|
219
219
|
const { keyring, password } = deps;
|
|
220
|
-
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, } = options;
|
|
220
|
+
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;
|
|
221
221
|
return [
|
|
222
222
|
{
|
|
223
223
|
method: 'GET',
|
|
@@ -254,8 +254,12 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
254
254
|
return rate_limit_exceeded_response(c, check.retry_after);
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
// `UsernameProvided` canonicalizes via `.trim().toLowerCase()` at
|
|
258
|
+
// parse time — the validated value lands canonical in
|
|
259
|
+
// `c.var.validated_input`, so the per-account rate-limit key,
|
|
260
|
+
// DB lookup, and audit metadata see one form. See
|
|
261
|
+
// `primitive_schemas.ts` for the schema-layer canonicalization.
|
|
262
|
+
const { username, password: pw } = get_route_input(c);
|
|
259
263
|
// DB lookup first so we can key the per-account rate limit by a canonical value
|
|
260
264
|
// (account.id) rather than the submitted identifier. Otherwise an attacker could
|
|
261
265
|
// alternate between username and email to double the per-account bucket.
|
|
@@ -339,6 +343,21 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
339
343
|
if (session_token) {
|
|
340
344
|
const token_hash = hash_session_token(session_token);
|
|
341
345
|
await query_session_revoke_by_hash_unscoped(route, token_hash);
|
|
346
|
+
// Handler-side belt+suspenders: close the live WS bound to
|
|
347
|
+
// this session BEFORE the audit emit so revocation lands
|
|
348
|
+
// even if the audit INSERT fails. Same transaction-commit
|
|
349
|
+
// trade as `password` / RPC `session_revoke` below — a
|
|
350
|
+
// throw between this close and the response rolls back the
|
|
351
|
+
// DB revoke while leaving the socket severed; benign
|
|
352
|
+
// (client reconnects, session still valid) but don't
|
|
353
|
+
// introduce a throw here without acknowledging the trade.
|
|
354
|
+
// The audit listener (`create_ws_logout_closer`) runs an
|
|
355
|
+
// account-wide close on the logout event afterward —
|
|
356
|
+
// broader than this targeted close, but both layers are
|
|
357
|
+
// idempotent. Mirrors `zzz_server::account::logout_inner`.
|
|
358
|
+
if (connection_closer) {
|
|
359
|
+
connection_closer.close_sockets_for_session(token_hash);
|
|
360
|
+
}
|
|
342
361
|
}
|
|
343
362
|
clear_session_cookie(c, session_options);
|
|
344
363
|
// Account-grain operation — no `actor_id` (which actor was
|
|
@@ -355,7 +374,8 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
355
374
|
{
|
|
356
375
|
method: 'POST',
|
|
357
376
|
path: '/password',
|
|
358
|
-
|
|
377
|
+
// `credential_types: ['session']` — see `docs/security.md` §Credential-channel gating.
|
|
378
|
+
auth: { account: 'required', actor: 'none', credential_types: ['session'] },
|
|
359
379
|
description: 'Change password (revokes all sessions and API tokens)',
|
|
360
380
|
input: PasswordChangeInput,
|
|
361
381
|
output: PasswordChangeOutput,
|
|
@@ -376,6 +396,8 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
376
396
|
}
|
|
377
397
|
const ctx = require_request_context(c);
|
|
378
398
|
const { current_password, new_password } = get_route_input(c);
|
|
399
|
+
// Defense in depth — see `docs/security.md` §Credential-channel gating.
|
|
400
|
+
const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? undefined;
|
|
379
401
|
// per-account rate limit check (after context resolution, before argon2 work)
|
|
380
402
|
if (login_account_rate_limiter) {
|
|
381
403
|
const check = login_account_rate_limiter.check(ctx.account.id);
|
|
@@ -394,14 +416,14 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
394
416
|
outcome: 'failure',
|
|
395
417
|
account_id: ctx.account.id,
|
|
396
418
|
ip: get_client_ip(c),
|
|
419
|
+
metadata: { credential_type },
|
|
397
420
|
});
|
|
398
421
|
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
399
422
|
}
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
login_account_rate_limiter.reset(ctx.account.id);
|
|
423
|
+
// Verify succeeded — do the throw-y operations FIRST so a fault
|
|
424
|
+
// (Argon2 OOM, native binding error, DB outage on the UPDATE)
|
|
425
|
+
// can't wipe the rate-limit history of a caller observing 500s.
|
|
426
|
+
// Resets happen below, after both calls have settled.
|
|
405
427
|
const new_hash = await password.hash_password(new_password);
|
|
406
428
|
// Conditional UPDATE keyed on the verified hash: closes the
|
|
407
429
|
// verify-write race with a concurrent password change that
|
|
@@ -409,6 +431,19 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
409
431
|
// operation — `updated_by` stays null (the per-request actor is
|
|
410
432
|
// incidental; password is account-level state).
|
|
411
433
|
const updated = await query_update_account_password(route, ctx.account.id, new_hash, null, ctx.account.password_hash);
|
|
434
|
+
// Verify-success contract — the caller proved knowledge, so wipe
|
|
435
|
+
// their failure history. The race-loser branch below re-records
|
|
436
|
+
// one on top of the wiped slate so net cost stays 1 (mirrors the
|
|
437
|
+
// verify-fail branch above's `record`-from-prior+1 outcome when
|
|
438
|
+
// prior was 0; for prior > 0 the race-loser pays exactly 1,
|
|
439
|
+
// matching the OLD pre-S1 behavior). Deferring from "after
|
|
440
|
+
// verify" to "after UPDATE settled" is what closes the S1
|
|
441
|
+
// bypass — a throw between reset and the UPDATE could have
|
|
442
|
+
// wiped an attacker's budget.
|
|
443
|
+
if (ip_rate_limiter && ip)
|
|
444
|
+
ip_rate_limiter.reset(ip);
|
|
445
|
+
if (login_account_rate_limiter)
|
|
446
|
+
login_account_rate_limiter.reset(ctx.account.id);
|
|
412
447
|
if (!updated) {
|
|
413
448
|
// A concurrent password change committed first — our
|
|
414
449
|
// `current_password` was correct at read-time but the row's
|
|
@@ -426,13 +461,29 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
426
461
|
outcome: 'failure',
|
|
427
462
|
account_id: ctx.account.id,
|
|
428
463
|
ip: get_client_ip(c),
|
|
429
|
-
metadata: { reason: 'concurrent_change' },
|
|
464
|
+
metadata: { reason: 'concurrent_change', credential_type },
|
|
430
465
|
});
|
|
431
466
|
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
432
467
|
}
|
|
433
468
|
// revoke all sessions and API tokens (force re-auth everywhere)
|
|
434
469
|
const sessions_revoked = await query_session_revoke_all_for_account(route, ctx.account.id);
|
|
435
470
|
const tokens_revoked = await query_revoke_all_api_tokens_for_account(route, ctx.account.id);
|
|
471
|
+
// Handler-side belt+suspenders — close every live WS socket on
|
|
472
|
+
// this account BEFORE the audit emit so the revoke-all cascade
|
|
473
|
+
// lands even if the audit INSERT fails. The real ordering
|
|
474
|
+
// invariant is "before the transaction commits": this route
|
|
475
|
+
// runs with the default `transaction: true`, so a throw between
|
|
476
|
+
// this close and the response would roll back the password
|
|
477
|
+
// update + session/token revokes while leaving sockets severed.
|
|
478
|
+
// Benign — affected clients reconnect with their still-valid
|
|
479
|
+
// session — but don't introduce a throw here without
|
|
480
|
+
// acknowledging the trade. Listener-based close
|
|
481
|
+
// (`transports_ws_auth_guard` on the `password_change` event)
|
|
482
|
+
// runs the same close afterward; idempotent on the second pass.
|
|
483
|
+
// Mirrors `zzz_server::account::password_inner`.
|
|
484
|
+
if (connection_closer) {
|
|
485
|
+
connection_closer.close_sockets_for_account(ctx.account.id);
|
|
486
|
+
}
|
|
436
487
|
clear_session_cookie(c, session_options);
|
|
437
488
|
// Account-grain operation — no `actor_id`. The password is
|
|
438
489
|
// account-level state; which per-request actor was resolved
|
|
@@ -442,7 +493,7 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
442
493
|
event_type: 'password_change',
|
|
443
494
|
account_id: ctx.account.id,
|
|
444
495
|
ip: get_client_ip(c),
|
|
445
|
-
metadata: { sessions_revoked, tokens_revoked },
|
|
496
|
+
metadata: { sessions_revoked, tokens_revoked, credential_type },
|
|
446
497
|
});
|
|
447
498
|
return c.json({ ok: true, sessions_revoked, tokens_revoked });
|
|
448
499
|
},
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* `Account`, `Actor`, `RoleGrant`, `AuthSession`, and `ApiToken`.
|
|
6
6
|
*
|
|
7
7
|
* Identifier primitives (`Username`, `UsernameProvided`, `Email`) live
|
|
8
|
-
* in
|
|
8
|
+
* in `primitive_schemas.ts` — they're general validator shapes that
|
|
9
9
|
* don't depend on the auth domain. The auth-shape request-contract
|
|
10
|
-
* primitive `ActingActor` lives in
|
|
10
|
+
* primitive `ActingActor` lives in `http/auth_shape.ts` next to
|
|
11
11
|
* `RouteAuth` (the two pair: `auth.actor !== 'none'` ⟺ input declares
|
|
12
12
|
* `acting?: ActingActor`).
|
|
13
13
|
*
|
|
@@ -106,7 +106,7 @@ export interface ApiToken {
|
|
|
106
106
|
/** Zod schema for `SessionAccount` — account without sensitive fields. */
|
|
107
107
|
export declare const SessionAccountJson: z.ZodObject<{
|
|
108
108
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
109
|
-
username: z.ZodString
|
|
109
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
110
110
|
email: z.ZodNullable<z.ZodEmail>;
|
|
111
111
|
email_verified: z.ZodBoolean;
|
|
112
112
|
created_at: z.ZodString;
|
|
@@ -152,7 +152,7 @@ export type ActorSummaryJson = z.infer<typeof ActorSummaryJson>;
|
|
|
152
152
|
/** Zod schema for admin-facing account data — extends `SessionAccountJson` with audit fields. */
|
|
153
153
|
export declare const AdminAccountJson: z.ZodObject<{
|
|
154
154
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
155
|
-
username: z.ZodString
|
|
155
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
156
156
|
email: z.ZodNullable<z.ZodEmail>;
|
|
157
157
|
email_verified: z.ZodBoolean;
|
|
158
158
|
created_at: z.ZodString;
|
|
@@ -188,7 +188,7 @@ export type PendingOfferSummaryJson = z.infer<typeof PendingOfferSummaryJson>;
|
|
|
188
188
|
export declare const AdminAccountEntryJson: z.ZodObject<{
|
|
189
189
|
account: z.ZodObject<{
|
|
190
190
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
191
|
-
username: z.ZodString
|
|
191
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
192
192
|
email: z.ZodNullable<z.ZodEmail>;
|
|
193
193
|
email_verified: z.ZodBoolean;
|
|
194
194
|
created_at: z.ZodString;
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* `Account`, `Actor`, `RoleGrant`, `AuthSession`, and `ApiToken`.
|
|
6
6
|
*
|
|
7
7
|
* Identifier primitives (`Username`, `UsernameProvided`, `Email`) live
|
|
8
|
-
* in
|
|
8
|
+
* in `primitive_schemas.ts` — they're general validator shapes that
|
|
9
9
|
* don't depend on the auth domain. The auth-shape request-contract
|
|
10
|
-
* primitive `ActingActor` lives in
|
|
10
|
+
* primitive `ActingActor` lives in `http/auth_shape.ts` next to
|
|
11
11
|
* `RouteAuth` (the two pair: `auth.actor !== 'none'` ⟺ input declares
|
|
12
12
|
* `acting?: ActingActor`).
|
|
13
13
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure read — no audit, no side effects. Auth (`account: 'required'`) +
|
|
5
5
|
* rate-limit (`account`-grain) enforced at the spec layer; see
|
|
6
|
-
*
|
|
6
|
+
* `auth/actor_lookup_action_specs.ts` for the info-leak audit.
|
|
7
7
|
*
|
|
8
8
|
* `display_name` is omitted (not `null`) when `actor.name` is blank,
|
|
9
9
|
* matching the wire shape `display_name?` so the typed client sees an
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure read — no audit, no side effects. Auth (`account: 'required'`) +
|
|
5
5
|
* rate-limit (`account`-grain) enforced at the spec layer; see
|
|
6
|
-
*
|
|
6
|
+
* `auth/actor_lookup_action_specs.ts` for the info-leak audit.
|
|
7
7
|
*
|
|
8
8
|
* `display_name` is omitted (not `null`) when `actor.name` is blank,
|
|
9
9
|
* matching the wire shape `display_name?` so the typed client sees an
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* The inner join still resolves one row per actor — `actor.account_id`
|
|
11
11
|
* is `NOT NULL` so every actor has exactly one account.
|
|
12
12
|
*
|
|
13
|
-
* Info-leak posture (see `actor_lookup_action_specs.ts` §
|
|
13
|
+
* Info-leak posture (see `actor_lookup_action_specs.ts` §audit):
|
|
14
14
|
*
|
|
15
15
|
* - Row shape **omits** `account_id` — the join is control-plane,
|
|
16
16
|
* not wire-visible.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* The inner join still resolves one row per actor — `actor.account_id`
|
|
11
11
|
* is `NOT NULL` so every actor has exactly one account.
|
|
12
12
|
*
|
|
13
|
-
* Info-leak posture (see `actor_lookup_action_specs.ts` §
|
|
13
|
+
* Info-leak posture (see `actor_lookup_action_specs.ts` §audit):
|
|
14
14
|
*
|
|
15
15
|
* - Row shape **omits** `account_id` — the join is control-plane,
|
|
16
16
|
* not wire-visible.
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* ## Wire shape — info-leak audit
|
|
46
46
|
*
|
|
47
47
|
* Output `{actors: [{id, username, display_name?}]}` is identical to
|
|
48
|
-
* `actor_lookup`'s — see
|
|
48
|
+
* `actor_lookup`'s — see `auth/actor_lookup_action_specs.ts` for the full
|
|
49
49
|
* field-by-field audit. Same omissions (`account_id`, email,
|
|
50
50
|
* timestamps, role / role_grants / session state), same `display_name`
|
|
51
51
|
* omitted-not-null contract, same response-order-unspecified rule.
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* ## Wire shape — info-leak audit
|
|
46
46
|
*
|
|
47
47
|
* Output `{actors: [{id, username, display_name?}]}` is identical to
|
|
48
|
-
* `actor_lookup`'s — see
|
|
48
|
+
* `actor_lookup`'s — see `auth/actor_lookup_action_specs.ts` for the full
|
|
49
49
|
* field-by-field audit. Same omissions (`account_id`, email,
|
|
50
50
|
* timestamps, role / role_grants / session state), same `display_name`
|
|
51
51
|
* omitted-not-null contract, same response-order-unspecified rule.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure read — no audit, no side effects. Auth (`account: 'required'`,
|
|
5
5
|
* `actor: 'none'`) + rate-limit (`account`-grain) enforced at the spec
|
|
6
|
-
* layer; see
|
|
6
|
+
* layer; see `auth/actor_search_action_specs.ts` for the info-leak audit
|
|
7
7
|
* and threat model.
|
|
8
8
|
*
|
|
9
9
|
* The handler adds two checks the spec layer can't express:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure read — no audit, no side effects. Auth (`account: 'required'`,
|
|
5
5
|
* `actor: 'none'`) + rate-limit (`account`-grain) enforced at the spec
|
|
6
|
-
* layer; see
|
|
6
|
+
* layer; see `auth/actor_search_action_specs.ts` for the info-leak audit
|
|
7
7
|
* and threat model.
|
|
8
8
|
*
|
|
9
9
|
* The handler adds two checks the spec layer can't express:
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* gates), no role_grant join — every actor with a matching prefix is
|
|
30
30
|
* returned.
|
|
31
31
|
*
|
|
32
|
-
* ## Info-leak posture (see `actor_search_action_specs.ts` §
|
|
32
|
+
* ## Info-leak posture (see `actor_search_action_specs.ts` §audit)
|
|
33
33
|
*
|
|
34
34
|
* - Row shape **omits** `account_id` — the join is control-plane, not
|
|
35
35
|
* wire-visible. Identical to `actor_lookup_queries.ts`.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* gates), no role_grant join — every actor with a matching prefix is
|
|
30
30
|
* returned.
|
|
31
31
|
*
|
|
32
|
-
* ## Info-leak posture (see `actor_search_action_specs.ts` §
|
|
32
|
+
* ## Info-leak posture (see `actor_search_action_specs.ts` §audit)
|
|
33
33
|
*
|
|
34
34
|
* - Row shape **omits** `account_id` — the join is control-plane, not
|
|
35
35
|
* wire-visible. Identical to `actor_lookup_queries.ts`.
|
|
@@ -35,7 +35,7 @@ export declare const AdminAccountListOutput: z.ZodObject<{
|
|
|
35
35
|
accounts: z.ZodArray<z.ZodObject<{
|
|
36
36
|
account: z.ZodObject<{
|
|
37
37
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
38
|
-
username: z.ZodString
|
|
38
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
39
39
|
email: z.ZodNullable<z.ZodEmail>;
|
|
40
40
|
email_verified: z.ZodBoolean;
|
|
41
41
|
created_at: z.ZodString;
|
|
@@ -183,7 +183,7 @@ export type AuditLogRoleGrantHistoryOutput = z.infer<typeof AuditLogRoleGrantHis
|
|
|
183
183
|
/** Input for `invite_create`. At least one of `email` / `username` must be provided. */
|
|
184
184
|
export declare const InviteCreateInput: z.ZodObject<{
|
|
185
185
|
email: z.ZodOptional<z.ZodNullable<z.ZodEmail>>;
|
|
186
|
-
username: z.ZodOptional<z.ZodNullable<z.ZodString
|
|
186
|
+
username: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
|
|
187
187
|
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
188
188
|
}, z.core.$strict>;
|
|
189
189
|
export type InviteCreateInput = z.infer<typeof InviteCreateInput>;
|
|
@@ -193,7 +193,7 @@ export declare const InviteCreateOutput: z.ZodObject<{
|
|
|
193
193
|
invite: z.ZodObject<{
|
|
194
194
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
195
195
|
email: z.ZodNullable<z.ZodEmail>;
|
|
196
|
-
username: z.ZodNullable<z.ZodString
|
|
196
|
+
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
197
197
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
198
198
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
199
199
|
created_at: z.ZodString;
|
|
@@ -211,7 +211,7 @@ export declare const InviteListOutput: z.ZodObject<{
|
|
|
211
211
|
invites: z.ZodArray<z.ZodObject<{
|
|
212
212
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
213
213
|
email: z.ZodNullable<z.ZodEmail>;
|
|
214
|
-
username: z.ZodNullable<z.ZodString
|
|
214
|
+
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
215
215
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
216
216
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
217
217
|
created_at: z.ZodString;
|
|
@@ -289,7 +289,7 @@ export declare const admin_account_list_action_spec: {
|
|
|
289
289
|
accounts: z.ZodArray<z.ZodObject<{
|
|
290
290
|
account: z.ZodObject<{
|
|
291
291
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
292
|
-
username: z.ZodString
|
|
292
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
293
293
|
email: z.ZodNullable<z.ZodEmail>;
|
|
294
294
|
email_verified: z.ZodBoolean;
|
|
295
295
|
created_at: z.ZodString;
|
|
@@ -512,7 +512,7 @@ export declare const invite_create_action_spec: {
|
|
|
512
512
|
side_effects: true;
|
|
513
513
|
input: z.ZodObject<{
|
|
514
514
|
email: z.ZodOptional<z.ZodNullable<z.ZodEmail>>;
|
|
515
|
-
username: z.ZodOptional<z.ZodNullable<z.ZodString
|
|
515
|
+
username: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
|
|
516
516
|
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
517
517
|
}, z.core.$strict>;
|
|
518
518
|
output: z.ZodObject<{
|
|
@@ -520,7 +520,7 @@ export declare const invite_create_action_spec: {
|
|
|
520
520
|
invite: z.ZodObject<{
|
|
521
521
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
522
522
|
email: z.ZodNullable<z.ZodEmail>;
|
|
523
|
-
username: z.ZodNullable<z.ZodString
|
|
523
|
+
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
524
524
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
525
525
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
526
526
|
created_at: z.ZodString;
|
|
@@ -554,7 +554,7 @@ export declare const invite_list_action_spec: {
|
|
|
554
554
|
invites: z.ZodArray<z.ZodObject<{
|
|
555
555
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
556
556
|
email: z.ZodNullable<z.ZodEmail>;
|
|
557
|
-
username: z.ZodNullable<z.ZodString
|
|
557
|
+
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
558
558
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
559
559
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
560
560
|
created_at: z.ZodString;
|