@fuzdev/fuz_app 0.38.1 → 0.40.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/auth/CLAUDE.md +124 -36
- package/dist/auth/account_actions.d.ts +5 -3
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +5 -6
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +7 -7
- package/dist/auth/admin_action_specs.d.ts +6 -138
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +4 -2
- package/dist/auth/admin_actions.d.ts +4 -3
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +8 -9
- package/dist/auth/audit_log_queries.d.ts +32 -20
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +52 -40
- package/dist/auth/audit_log_schema.d.ts +105 -84
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +84 -12
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +3 -3
- package/dist/auth/cleanup.d.ts +9 -1
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +2 -2
- package/dist/auth/deps.d.ts +13 -1
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/permit_offer_actions.d.ts +16 -2
- package/dist/auth/permit_offer_actions.d.ts.map +1 -1
- package/dist/auth/permit_offer_actions.js +26 -8
- package/dist/auth/role_schema.d.ts +10 -1
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +10 -1
- package/dist/auth/self_service_role_actions.d.ts +136 -0
- package/dist/auth/self_service_role_actions.d.ts.map +1 -0
- package/dist/auth/self_service_role_actions.js +198 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +2 -2
- package/dist/auth/standard_rpc_actions.d.ts +1 -1
- package/dist/auth/standard_rpc_actions.js +1 -1
- package/dist/http/jsonrpc_errors.d.ts +27 -75
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +16 -9
- package/dist/server/app_backend.d.ts +26 -7
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +29 -7
- package/dist/server/app_server.d.ts +6 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +16 -29
- package/dist/ui/AdminAccounts.svelte +19 -0
- package/dist/ui/AdminAccounts.svelte.d.ts +2 -17
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +23 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts +2 -17
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +11 -0
- package/dist/ui/PermitOfferHistory.svelte +11 -5
- package/dist/ui/PermitOfferHistory.svelte.d.ts +7 -1
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferInbox.svelte +12 -7
- package/dist/ui/PermitOfferInbox.svelte.d.ts +8 -3
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.d.ts +16 -1
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +12 -1
- package/dist/ui/format_scope.d.ts +45 -0
- package/dist/ui/format_scope.d.ts.map +1 -0
- package/dist/ui/format_scope.js +34 -0
- package/dist/ui/ui_format.d.ts +2 -3
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-service role grant/revoke RPC actions.
|
|
3
|
+
*
|
|
4
|
+
* Two static `request_response` actions — `self_service_role_grant` and
|
|
5
|
+
* `self_service_role_revoke` — that take `{role}` as input and toggle a
|
|
6
|
+
* permit on the caller for an allowlisted role. Idempotent in both
|
|
7
|
+
* directions: re-granting an already-held role returns `granted: false`;
|
|
8
|
+
* revoking a role the caller doesn't hold returns `revoked: false`.
|
|
9
|
+
*
|
|
10
|
+
* The factory takes an `eligible_roles` allowlist (validated against the
|
|
11
|
+
* supplied `roles.role_options` at factory time so typos surface at startup
|
|
12
|
+
* instead of at first call). Roles outside the allowlist are rejected
|
|
13
|
+
* with `forbidden` + reason `role_not_self_service_eligible`.
|
|
14
|
+
*
|
|
15
|
+
* Audit metadata carries `self_service: true` so admin reviewers can
|
|
16
|
+
* distinguish self-toggled permits from admin grants/offers. The
|
|
17
|
+
* `permit_grant` / `permit_revoke` metadata schemas declare
|
|
18
|
+
* `self_service: z.boolean().optional()` explicitly, so the field is
|
|
19
|
+
* part of the documented schema surface and is round-trip-validated by
|
|
20
|
+
* `query_audit_log`.
|
|
21
|
+
*
|
|
22
|
+
* Static method names — `role` lives in the input, not the method name —
|
|
23
|
+
* so specs are codegen-compatible (`satisfies RequestResponseActionSpec`)
|
|
24
|
+
* and the surface stays constant as consumers add eligible roles. Mirrors
|
|
25
|
+
* the existing `permit_offer_create({role})` precedent rather than
|
|
26
|
+
* generating per-role methods.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { rpc_action } from '../actions/action_rpc.js';
|
|
32
|
+
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
33
|
+
import { Uuid } from '../uuid.js';
|
|
34
|
+
import { RoleName } from './role_schema.js';
|
|
35
|
+
import { query_grant_permit, query_permit_find_active_for_actor, query_permit_has_role, query_revoke_permit, } from './permit_queries.js';
|
|
36
|
+
import { audit_log_fire_and_forget } from './audit_log_queries.js';
|
|
37
|
+
/** Error reason — caller asked to self-toggle a role outside the configured allowlist. */
|
|
38
|
+
export const ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE = 'role_not_self_service_eligible';
|
|
39
|
+
// -- Input/output schemas ---------------------------------------------------
|
|
40
|
+
/** Input for `self_service_role_grant`. */
|
|
41
|
+
export const SelfServiceRoleGrantInput = z.strictObject({
|
|
42
|
+
role: RoleName.meta({ description: 'Role to self-grant. Must be in the configured allowlist.' }),
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Output for `self_service_role_grant`. `granted` is `false` on idempotent
|
|
46
|
+
* re-grant (caller already held the role globally); `permit_id` is set on
|
|
47
|
+
* new grants only.
|
|
48
|
+
*/
|
|
49
|
+
export const SelfServiceRoleGrantOutput = z.strictObject({
|
|
50
|
+
ok: z.literal(true),
|
|
51
|
+
granted: z.boolean(),
|
|
52
|
+
permit_id: Uuid.optional(),
|
|
53
|
+
});
|
|
54
|
+
/** Input for `self_service_role_revoke`. */
|
|
55
|
+
export const SelfServiceRoleRevokeInput = z.strictObject({
|
|
56
|
+
role: RoleName.meta({ description: 'Role to self-revoke. Must be in the configured allowlist.' }),
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Output for `self_service_role_revoke`. `revoked` is `false` when the
|
|
60
|
+
* caller held no active global permit for the role (idempotent).
|
|
61
|
+
*/
|
|
62
|
+
export const SelfServiceRoleRevokeOutput = z.strictObject({
|
|
63
|
+
ok: z.literal(true),
|
|
64
|
+
revoked: z.boolean(),
|
|
65
|
+
});
|
|
66
|
+
// -- Action specs -----------------------------------------------------------
|
|
67
|
+
export const self_service_role_grant_action_spec = {
|
|
68
|
+
method: 'self_service_role_grant',
|
|
69
|
+
kind: 'request_response',
|
|
70
|
+
initiator: 'frontend',
|
|
71
|
+
auth: 'authenticated',
|
|
72
|
+
side_effects: true,
|
|
73
|
+
input: SelfServiceRoleGrantInput,
|
|
74
|
+
output: SelfServiceRoleGrantOutput,
|
|
75
|
+
async: true,
|
|
76
|
+
description: 'Self-grant an active permit for an allowlisted role. Idempotent — already-granted callers receive `granted: false`.',
|
|
77
|
+
};
|
|
78
|
+
export const self_service_role_revoke_action_spec = {
|
|
79
|
+
method: 'self_service_role_revoke',
|
|
80
|
+
kind: 'request_response',
|
|
81
|
+
initiator: 'frontend',
|
|
82
|
+
auth: 'authenticated',
|
|
83
|
+
side_effects: true,
|
|
84
|
+
input: SelfServiceRoleRevokeInput,
|
|
85
|
+
output: SelfServiceRoleRevokeOutput,
|
|
86
|
+
async: true,
|
|
87
|
+
description: 'Self-revoke an active global permit for an allowlisted role. Idempotent — callers without an active permit receive `revoked: false`.',
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* All self-service role action specs — a codegen-ready registry. Method
|
|
91
|
+
* names are static, so consumer typed-client codegen picks them up the
|
|
92
|
+
* same way it picks up `account_*_action_specs`.
|
|
93
|
+
*/
|
|
94
|
+
export const all_self_service_role_action_specs = [
|
|
95
|
+
self_service_role_grant_action_spec,
|
|
96
|
+
self_service_role_revoke_action_spec,
|
|
97
|
+
];
|
|
98
|
+
const require_request_auth = (auth) => {
|
|
99
|
+
if (!auth)
|
|
100
|
+
throw new Error('unreachable: action auth guard did not enforce authentication');
|
|
101
|
+
return auth;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Build the self-service role grant/revoke RPC actions.
|
|
105
|
+
*
|
|
106
|
+
* @param deps - `SelfServiceRoleActionDeps` slice of `AppDeps` (`log`, `on_audit_event`, optional `audit_log_config`)
|
|
107
|
+
* @param options - eligible-role allowlist plus optional role schema for typo-checking
|
|
108
|
+
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
109
|
+
*/
|
|
110
|
+
export const create_self_service_role_actions = (deps, options) => {
|
|
111
|
+
const eligible = new Set(options.eligible_roles);
|
|
112
|
+
if (options.roles) {
|
|
113
|
+
const role_options = options.roles.role_options;
|
|
114
|
+
for (const r of eligible) {
|
|
115
|
+
if (!role_options.has(r)) {
|
|
116
|
+
throw new Error(`create_self_service_role_actions: eligible_roles entry "${r}" is not registered in roles.role_options — typo or missing call to create_role_schema`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const reject_if_ineligible = (role) => {
|
|
121
|
+
if (!eligible.has(role)) {
|
|
122
|
+
throw jsonrpc_errors.forbidden('role not eligible for self-service', {
|
|
123
|
+
reason: ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const grant_handler = async (input, ctx) => {
|
|
128
|
+
const auth = require_request_auth(ctx.auth);
|
|
129
|
+
reject_if_ineligible(input.role);
|
|
130
|
+
// Pre-check for idempotent re-grant. `query_grant_permit` is itself
|
|
131
|
+
// idempotent (returns the existing permit instead of inserting), but
|
|
132
|
+
// it doesn't signal "already existed" vs "newly inserted" — so we
|
|
133
|
+
// peek first. The TOCTOU window is benign for self-service: two
|
|
134
|
+
// concurrent grants both observe "no permit", both call
|
|
135
|
+
// `query_grant_permit`, and one collapses onto the other inside the
|
|
136
|
+
// query's `ON CONFLICT DO NOTHING`. Worst case both responses report
|
|
137
|
+
// `granted: true`; the DB still ends up with exactly one permit.
|
|
138
|
+
const already = await query_permit_has_role(ctx, auth.actor.id, input.role);
|
|
139
|
+
if (already) {
|
|
140
|
+
return { ok: true, granted: false };
|
|
141
|
+
}
|
|
142
|
+
const permit = await query_grant_permit(ctx, {
|
|
143
|
+
actor_id: auth.actor.id,
|
|
144
|
+
role: input.role,
|
|
145
|
+
scope_id: null,
|
|
146
|
+
expires_at: null,
|
|
147
|
+
granted_by: auth.actor.id,
|
|
148
|
+
});
|
|
149
|
+
void audit_log_fire_and_forget(ctx, {
|
|
150
|
+
event_type: 'permit_grant',
|
|
151
|
+
actor_id: auth.actor.id,
|
|
152
|
+
account_id: auth.account.id,
|
|
153
|
+
ip: ctx.client_ip,
|
|
154
|
+
metadata: {
|
|
155
|
+
role: permit.role,
|
|
156
|
+
permit_id: permit.id,
|
|
157
|
+
scope_id: permit.scope_id,
|
|
158
|
+
self_service: true,
|
|
159
|
+
},
|
|
160
|
+
}, deps);
|
|
161
|
+
return { ok: true, granted: true, permit_id: permit.id };
|
|
162
|
+
};
|
|
163
|
+
const revoke_handler = async (input, ctx) => {
|
|
164
|
+
const auth = require_request_auth(ctx.auth);
|
|
165
|
+
reject_if_ineligible(input.role);
|
|
166
|
+
// Find an active global permit for this (actor, role). No dedicated
|
|
167
|
+
// query exists, but `query_permit_find_active_for_actor` returns the
|
|
168
|
+
// short list of every active permit and we filter in JS — fewer
|
|
169
|
+
// round-trips than a new helper for a one-call-per-revoke path.
|
|
170
|
+
const active = await query_permit_find_active_for_actor(ctx, auth.actor.id);
|
|
171
|
+
const target = active.find((p) => p.role === input.role && p.scope_id === null);
|
|
172
|
+
if (!target) {
|
|
173
|
+
return { ok: true, revoked: false };
|
|
174
|
+
}
|
|
175
|
+
const result = await query_revoke_permit(ctx, target.id, auth.actor.id, auth.actor.id);
|
|
176
|
+
if (!result) {
|
|
177
|
+
// Raced with another revoker — treat as already revoked.
|
|
178
|
+
return { ok: true, revoked: false };
|
|
179
|
+
}
|
|
180
|
+
void audit_log_fire_and_forget(ctx, {
|
|
181
|
+
event_type: 'permit_revoke',
|
|
182
|
+
actor_id: auth.actor.id,
|
|
183
|
+
account_id: auth.account.id,
|
|
184
|
+
ip: ctx.client_ip,
|
|
185
|
+
metadata: {
|
|
186
|
+
role: result.role,
|
|
187
|
+
permit_id: result.id,
|
|
188
|
+
scope_id: result.scope_id,
|
|
189
|
+
self_service: true,
|
|
190
|
+
},
|
|
191
|
+
}, deps);
|
|
192
|
+
return { ok: true, revoked: true };
|
|
193
|
+
};
|
|
194
|
+
return [
|
|
195
|
+
rpc_action(self_service_role_grant_action_spec, grant_handler),
|
|
196
|
+
rpc_action(self_service_role_revoke_action_spec, revoke_handler),
|
|
197
|
+
];
|
|
198
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAStB,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;AAQhD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,yFAAyF;IACzF,YAAY,EAAE,WAAW,CAAC;CAC1B;AAID,0FAA0F;AAC1F,eAAO,MAAM,WAAW;;;;kBAItB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,8EAA8E;AAC9E,eAAO,MAAM,YAAY;;kBAEvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAStB,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;AAQhD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,yFAAyF;IACzF,YAAY,EAAE,WAAW,CAAC;CAC1B;AAID,0FAA0F;AAC1F,eAAO,MAAM,WAAW;;;;kBAItB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,8EAA8E;AAC9E,eAAO,MAAM,YAAY;;kBAEvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CA2HjB,CAAC"}
|
|
@@ -38,7 +38,7 @@ export const SignupOutput = z.strictObject({
|
|
|
38
38
|
* @returns route specs (not yet applied to Hono)
|
|
39
39
|
*/
|
|
40
40
|
export const create_signup_route_specs = (deps, options) => {
|
|
41
|
-
const { keyring, password
|
|
41
|
+
const { keyring, password } = deps;
|
|
42
42
|
const { session_options, ip_rate_limiter, signup_account_rate_limiter, app_settings } = options;
|
|
43
43
|
return [
|
|
44
44
|
{
|
|
@@ -144,7 +144,7 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
144
144
|
account_id: result.id,
|
|
145
145
|
ip: get_client_ip(c),
|
|
146
146
|
metadata: invite ? { invite_id: invite.id, username } : { open_signup: true, username },
|
|
147
|
-
}, deps
|
|
147
|
+
}, deps);
|
|
148
148
|
return c.json({ ok: true });
|
|
149
149
|
},
|
|
150
150
|
},
|
|
@@ -49,7 +49,7 @@ export type StandardRpcActionsDeps = PermitOfferActionDeps;
|
|
|
49
49
|
* and `create_account_actions(deps, {max_tokens})`. The shared `roles`
|
|
50
50
|
* option flows to admin + permit-offer.
|
|
51
51
|
*
|
|
52
|
-
* @param deps -
|
|
52
|
+
* @param deps - `StandardRpcActionsDeps` (`log`, `on_audit_event`, optional `audit_log_config` from `AppDeps`; optional `notification_sender` for WS fan-out)
|
|
53
53
|
* @param options - role schema, optional app-settings ref, permit-offer config, account config
|
|
54
54
|
* @returns RPC actions to pass as `rpc_endpoints` or spread into `create_rpc_endpoint`
|
|
55
55
|
*/
|
|
@@ -28,7 +28,7 @@ import { create_account_actions } from './account_actions.js';
|
|
|
28
28
|
* and `create_account_actions(deps, {max_tokens})`. The shared `roles`
|
|
29
29
|
* option flows to admin + permit-offer.
|
|
30
30
|
*
|
|
31
|
-
* @param deps -
|
|
31
|
+
* @param deps - `StandardRpcActionsDeps` (`log`, `on_audit_event`, optional `audit_log_config` from `AppDeps`; optional `notification_sender` for WS fan-out)
|
|
32
32
|
* @param options - role schema, optional app-settings ref, permit-offer config, account config
|
|
33
33
|
* @returns RPC actions to pass as `rpc_endpoints` or spread into `create_rpc_endpoint`
|
|
34
34
|
*/
|
|
@@ -27,71 +27,22 @@ export type JsonrpcErrorName = 'parse_error' | 'invalid_request' | 'method_not_f
|
|
|
27
27
|
* Extensible — consumers add domain-specific codes to their own objects
|
|
28
28
|
* by casting `as JsonrpcErrorCode`. Application codes use the -32000 to
|
|
29
29
|
* -32099 range reserved by the JSON-RPC spec.
|
|
30
|
+
*
|
|
31
|
+
* Frozen with `Object.freeze` to convert accidental mutation (test
|
|
32
|
+
* cross-contamination, cast escapes) into loud TypeErrors. Spread into
|
|
33
|
+
* a fresh object to extend.
|
|
30
34
|
*/
|
|
31
|
-
export declare const JSONRPC_ERROR_CODES:
|
|
32
|
-
readonly parse_error: JsonrpcErrorCode;
|
|
33
|
-
readonly invalid_request: JsonrpcErrorCode;
|
|
34
|
-
readonly method_not_found: JsonrpcErrorCode;
|
|
35
|
-
readonly invalid_params: JsonrpcErrorCode;
|
|
36
|
-
readonly internal_error: JsonrpcErrorCode;
|
|
37
|
-
/**
|
|
38
|
-
* Same as HTTP 401 "unauthorized", but correctly named.
|
|
39
|
-
*
|
|
40
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status#client_error_responses
|
|
41
|
-
*/
|
|
42
|
-
readonly unauthenticated: JsonrpcErrorCode;
|
|
43
|
-
/**
|
|
44
|
-
* Named to match HTTP 403 — avoids confusion with 401 which
|
|
45
|
-
* is incorrectly named "unauthorized" in HTTP.
|
|
46
|
-
*/
|
|
47
|
-
readonly forbidden: JsonrpcErrorCode;
|
|
48
|
-
readonly not_found: JsonrpcErrorCode;
|
|
49
|
-
readonly conflict: JsonrpcErrorCode;
|
|
50
|
-
/**
|
|
51
|
-
* Application-level validation failures (business logic).
|
|
52
|
-
* Use `invalid_params` (-32602) for schema/parsing failures.
|
|
53
|
-
*/
|
|
54
|
-
readonly validation_error: JsonrpcErrorCode;
|
|
55
|
-
readonly rate_limited: JsonrpcErrorCode;
|
|
56
|
-
readonly service_unavailable: JsonrpcErrorCode;
|
|
57
|
-
readonly timeout: JsonrpcErrorCode;
|
|
58
|
-
/**
|
|
59
|
-
* Client-side backpressure — an outbound buffer (e.g. `FrontendWebsocketClient`'s
|
|
60
|
-
* disconnected request queue) refused a new request because it was full.
|
|
61
|
-
* Distinct from `rate_limited`, which signals a server-side policy.
|
|
62
|
-
*/
|
|
63
|
-
readonly queue_overflow: JsonrpcErrorCode;
|
|
64
|
-
/**
|
|
65
|
-
* Caller-initiated cancellation (e.g. `AbortSignal` fired). Cooperative,
|
|
66
|
-
* not a failure — the request did not complete because the caller asked
|
|
67
|
-
* for it to stop.
|
|
68
|
-
*/
|
|
69
|
-
readonly request_cancelled: JsonrpcErrorCode;
|
|
70
|
-
};
|
|
35
|
+
export declare const JSONRPC_ERROR_CODES: Readonly<Record<JsonrpcErrorName, JsonrpcErrorCode>>;
|
|
71
36
|
/**
|
|
72
37
|
* Named constructors for `JsonrpcErrorObject` values.
|
|
73
38
|
*
|
|
74
39
|
* Each function creates a JSON-RPC error object with the correct
|
|
75
40
|
* code and a sensible default message. Used by the catch layer in
|
|
76
41
|
* `apply_route_specs` to build response bodies.
|
|
42
|
+
*
|
|
43
|
+
* Frozen so tests must compose new objects rather than monkey-patch.
|
|
77
44
|
*/
|
|
78
|
-
export declare const jsonrpc_error_messages:
|
|
79
|
-
readonly parse_error: (data?: unknown) => JsonrpcErrorObject;
|
|
80
|
-
readonly invalid_request: (data?: unknown) => JsonrpcErrorObject;
|
|
81
|
-
readonly method_not_found: (method?: string, data?: unknown) => JsonrpcErrorObject;
|
|
82
|
-
readonly invalid_params: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
83
|
-
readonly internal_error: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
84
|
-
readonly unauthenticated: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
85
|
-
readonly forbidden: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
86
|
-
readonly not_found: (resource?: string, data?: unknown) => JsonrpcErrorObject;
|
|
87
|
-
readonly conflict: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
88
|
-
readonly validation_error: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
89
|
-
readonly rate_limited: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
90
|
-
readonly service_unavailable: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
91
|
-
readonly timeout: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
92
|
-
readonly queue_overflow: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
93
|
-
readonly request_cancelled: (message?: string, data?: unknown) => JsonrpcErrorObject;
|
|
94
|
-
};
|
|
45
|
+
export declare const jsonrpc_error_messages: Readonly<Record<JsonrpcErrorName, (...args: Array<any>) => JsonrpcErrorObject>>;
|
|
95
46
|
/**
|
|
96
47
|
* Error class carrying a JSON-RPC error code — thrown by handlers,
|
|
97
48
|
* caught by `apply_route_specs` and mapped to HTTP status + JSON-RPC error response.
|
|
@@ -109,29 +60,30 @@ export declare class ThrownJsonrpcError extends Error {
|
|
|
109
60
|
* Usage: `throw jsonrpc_errors.not_found('user')` or `throw jsonrpc_errors.forbidden()`.
|
|
110
61
|
*/
|
|
111
62
|
export declare const jsonrpc_errors: {
|
|
112
|
-
readonly parse_error: (
|
|
113
|
-
readonly invalid_request: (
|
|
114
|
-
readonly method_not_found: (
|
|
115
|
-
readonly invalid_params: (
|
|
116
|
-
readonly internal_error: (
|
|
117
|
-
readonly unauthenticated: (
|
|
118
|
-
readonly forbidden: (
|
|
119
|
-
readonly not_found: (
|
|
120
|
-
readonly conflict: (
|
|
121
|
-
readonly validation_error: (
|
|
122
|
-
readonly rate_limited: (
|
|
123
|
-
readonly service_unavailable: (
|
|
124
|
-
readonly timeout: (
|
|
125
|
-
readonly queue_overflow: (
|
|
126
|
-
readonly request_cancelled: (
|
|
63
|
+
readonly parse_error: (...args: any[]) => ThrownJsonrpcError;
|
|
64
|
+
readonly invalid_request: (...args: any[]) => ThrownJsonrpcError;
|
|
65
|
+
readonly method_not_found: (...args: any[]) => ThrownJsonrpcError;
|
|
66
|
+
readonly invalid_params: (...args: any[]) => ThrownJsonrpcError;
|
|
67
|
+
readonly internal_error: (...args: any[]) => ThrownJsonrpcError;
|
|
68
|
+
readonly unauthenticated: (...args: any[]) => ThrownJsonrpcError;
|
|
69
|
+
readonly forbidden: (...args: any[]) => ThrownJsonrpcError;
|
|
70
|
+
readonly not_found: (...args: any[]) => ThrownJsonrpcError;
|
|
71
|
+
readonly conflict: (...args: any[]) => ThrownJsonrpcError;
|
|
72
|
+
readonly validation_error: (...args: any[]) => ThrownJsonrpcError;
|
|
73
|
+
readonly rate_limited: (...args: any[]) => ThrownJsonrpcError;
|
|
74
|
+
readonly service_unavailable: (...args: any[]) => ThrownJsonrpcError;
|
|
75
|
+
readonly timeout: (...args: any[]) => ThrownJsonrpcError;
|
|
76
|
+
readonly queue_overflow: (...args: any[]) => ThrownJsonrpcError;
|
|
77
|
+
readonly request_cancelled: (...args: any[]) => ThrownJsonrpcError;
|
|
127
78
|
};
|
|
128
79
|
/**
|
|
129
80
|
* Maps JSON-RPC error codes to HTTP status codes.
|
|
130
81
|
*
|
|
131
82
|
* Extensible — consumers with domain-specific error codes can spread
|
|
132
|
-
* this into their own mapping object.
|
|
83
|
+
* this into their own mapping object. Frozen so the source can't be
|
|
84
|
+
* accidentally mutated; spread copies are mutable.
|
|
133
85
|
*/
|
|
134
|
-
export declare const JSONRPC_ERROR_CODE_TO_HTTP_STATUS: Record<number, number
|
|
86
|
+
export declare const JSONRPC_ERROR_CODE_TO_HTTP_STATUS: Readonly<Record<number, number>>;
|
|
135
87
|
/**
|
|
136
88
|
* Maps HTTP status codes to JSON-RPC error codes (reverse mapping).
|
|
137
89
|
*
|
|
@@ -139,7 +91,7 @@ export declare const JSONRPC_ERROR_CODE_TO_HTTP_STATUS: Record<number, number>;
|
|
|
139
91
|
* and invalid_request both map to 400), the last one wins. Use for
|
|
140
92
|
* best-effort HTTP → JSON-RPC translation.
|
|
141
93
|
*/
|
|
142
|
-
export declare const HTTP_STATUS_TO_JSONRPC_ERROR_CODE: Record<number, JsonrpcErrorCode
|
|
94
|
+
export declare const HTTP_STATUS_TO_JSONRPC_ERROR_CODE: Readonly<Record<number, JsonrpcErrorCode>>;
|
|
143
95
|
/**
|
|
144
96
|
* Map a JSON-RPC error code to an HTTP status code.
|
|
145
97
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonrpc_errors.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/jsonrpc_errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAMN,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,MAAM,cAAc,CAAC;AAEtB,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GACzB,aAAa,GACb,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,WAAW,GACX,WAAW,GACX,UAAU,GACV,kBAAkB,GAClB,cAAc,GACd,qBAAqB,GACrB,SAAS,GACT,gBAAgB,GAChB,mBAAmB,CAAC;AAEvB
|
|
1
|
+
{"version":3,"file":"jsonrpc_errors.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/jsonrpc_errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAMN,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,MAAM,cAAc,CAAC;AAEtB,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GACzB,aAAa,GACb,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,WAAW,GACX,WAAW,GACX,UAAU,GACV,kBAAkB,GAClB,cAAc,GACd,qBAAqB,GACrB,SAAS,GACT,gBAAgB,GAChB,mBAAmB,CAAC;AAEvB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,EA0C1B,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAmG7B,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,kBAAkB,CAAC,CAAC,CAAC;AAEtF;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,OAAO,CAAC;gBAEH,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,YAAY;CAK3F;AAWD;;;;GAIG;AACH,eAAO,MAAM,cAAc;8CAXQ,kBAAkB;kDAAlB,kBAAkB;mDAAlB,kBAAkB;iDAAlB,kBAAkB;iDAAlB,kBAAkB;kDAAlB,kBAAkB;4CAAlB,kBAAkB;4CAAlB,kBAAkB;2CAAlB,kBAAkB;mDAAlB,kBAAkB;+CAAlB,kBAAkB;sDAAlB,kBAAkB;0CAAlB,kBAAkB;iDAAlB,kBAAkB;oDAAlB,kBAAkB;CA2BqC,CAAC;AAI3F;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB7E,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAQvF,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,gBAAgB,KAAG,MAClB,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,GAAI,QAAQ,MAAM,KAAG,gBACa,CAAC"}
|
|
@@ -25,8 +25,12 @@ export const UNKNOWN_ERROR_MESSAGE = 'unknown error';
|
|
|
25
25
|
* Extensible — consumers add domain-specific codes to their own objects
|
|
26
26
|
* by casting `as JsonrpcErrorCode`. Application codes use the -32000 to
|
|
27
27
|
* -32099 range reserved by the JSON-RPC spec.
|
|
28
|
+
*
|
|
29
|
+
* Frozen with `Object.freeze` to convert accidental mutation (test
|
|
30
|
+
* cross-contamination, cast escapes) into loud TypeErrors. Spread into
|
|
31
|
+
* a fresh object to extend.
|
|
28
32
|
*/
|
|
29
|
-
export const JSONRPC_ERROR_CODES = {
|
|
33
|
+
export const JSONRPC_ERROR_CODES = Object.freeze({
|
|
30
34
|
// Standard JSON-RPC errors — values from jsonrpc.ts
|
|
31
35
|
parse_error: JSONRPC_PARSE_ERROR,
|
|
32
36
|
invalid_request: JSONRPC_INVALID_REQUEST,
|
|
@@ -67,15 +71,17 @@ export const JSONRPC_ERROR_CODES = {
|
|
|
67
71
|
* for it to stop.
|
|
68
72
|
*/
|
|
69
73
|
request_cancelled: -32010,
|
|
70
|
-
};
|
|
74
|
+
});
|
|
71
75
|
/**
|
|
72
76
|
* Named constructors for `JsonrpcErrorObject` values.
|
|
73
77
|
*
|
|
74
78
|
* Each function creates a JSON-RPC error object with the correct
|
|
75
79
|
* code and a sensible default message. Used by the catch layer in
|
|
76
80
|
* `apply_route_specs` to build response bodies.
|
|
81
|
+
*
|
|
82
|
+
* Frozen so tests must compose new objects rather than monkey-patch.
|
|
77
83
|
*/
|
|
78
|
-
export const jsonrpc_error_messages = {
|
|
84
|
+
export const jsonrpc_error_messages = Object.freeze({
|
|
79
85
|
parse_error: (data) => ({
|
|
80
86
|
code: JSONRPC_ERROR_CODES.parse_error,
|
|
81
87
|
message: 'parse error',
|
|
@@ -151,7 +157,7 @@ export const jsonrpc_error_messages = {
|
|
|
151
157
|
message,
|
|
152
158
|
data,
|
|
153
159
|
}),
|
|
154
|
-
};
|
|
160
|
+
});
|
|
155
161
|
/**
|
|
156
162
|
* Error class carrying a JSON-RPC error code — thrown by handlers,
|
|
157
163
|
* caught by `apply_route_specs` and mapped to HTTP status + JSON-RPC error response.
|
|
@@ -198,9 +204,10 @@ export const jsonrpc_errors = {
|
|
|
198
204
|
* Maps JSON-RPC error codes to HTTP status codes.
|
|
199
205
|
*
|
|
200
206
|
* Extensible — consumers with domain-specific error codes can spread
|
|
201
|
-
* this into their own mapping object.
|
|
207
|
+
* this into their own mapping object. Frozen so the source can't be
|
|
208
|
+
* accidentally mutated; spread copies are mutable.
|
|
202
209
|
*/
|
|
203
|
-
export const JSONRPC_ERROR_CODE_TO_HTTP_STATUS = {
|
|
210
|
+
export const JSONRPC_ERROR_CODE_TO_HTTP_STATUS = Object.freeze({
|
|
204
211
|
[-32700]: 400, // parse_error
|
|
205
212
|
[-32600]: 400, // invalid_request
|
|
206
213
|
[-32601]: 404, // method_not_found
|
|
@@ -218,7 +225,7 @@ export const JSONRPC_ERROR_CODE_TO_HTTP_STATUS = {
|
|
|
218
225
|
[-32007]: 503, // service_unavailable
|
|
219
226
|
[-32008]: 504, // timeout
|
|
220
227
|
[-32010]: 499, // request_cancelled (nginx "client closed request")
|
|
221
|
-
};
|
|
228
|
+
});
|
|
222
229
|
/**
|
|
223
230
|
* Maps HTTP status codes to JSON-RPC error codes (reverse mapping).
|
|
224
231
|
*
|
|
@@ -226,10 +233,10 @@ export const JSONRPC_ERROR_CODE_TO_HTTP_STATUS = {
|
|
|
226
233
|
* and invalid_request both map to 400), the last one wins. Use for
|
|
227
234
|
* best-effort HTTP → JSON-RPC translation.
|
|
228
235
|
*/
|
|
229
|
-
export const HTTP_STATUS_TO_JSONRPC_ERROR_CODE = Object.fromEntries(Object.entries(JSONRPC_ERROR_CODE_TO_HTTP_STATUS).map(([code, status]) => [
|
|
236
|
+
export const HTTP_STATUS_TO_JSONRPC_ERROR_CODE = Object.freeze(Object.fromEntries(Object.entries(JSONRPC_ERROR_CODE_TO_HTTP_STATUS).map(([code, status]) => [
|
|
230
237
|
status,
|
|
231
238
|
Number(code),
|
|
232
|
-
]));
|
|
239
|
+
])));
|
|
233
240
|
/**
|
|
234
241
|
* Map a JSON-RPC error code to an HTTP status code.
|
|
235
242
|
*
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
14
14
|
import type { AppDeps } from '../auth/deps.js';
|
|
15
|
-
import type { AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
15
|
+
import type { AuditLogConfig, AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
16
16
|
import type { DbType } from '../db/db.js';
|
|
17
17
|
import type { Keyring } from '../auth/keyring.js';
|
|
18
18
|
import type { PasswordHashDeps } from '../auth/password.js';
|
|
19
19
|
import type { StatResult } from '../runtime/deps.js';
|
|
20
|
-
import { type MigrationResult } from '../db/migrate.js';
|
|
20
|
+
import { type MigrationNamespace, type MigrationResult } from '../db/migrate.js';
|
|
21
21
|
/**
|
|
22
22
|
* Result of `create_app_backend()` — database metadata + deps bundle.
|
|
23
23
|
*
|
|
@@ -28,7 +28,7 @@ export interface AppBackend {
|
|
|
28
28
|
deps: AppDeps;
|
|
29
29
|
db_type: DbType;
|
|
30
30
|
db_name: string;
|
|
31
|
-
/** Migration results from `create_app_backend`
|
|
31
|
+
/** Migration results from `create_app_backend` — auth migrations plus any consumer namespaces passed via `migration_namespaces`. */
|
|
32
32
|
readonly migration_results: ReadonlyArray<MigrationResult>;
|
|
33
33
|
/** Close the database connection. Bound to the actual driver. */
|
|
34
34
|
close: () => Promise<void>;
|
|
@@ -60,15 +60,34 @@ export interface CreateAppBackendOptions {
|
|
|
60
60
|
* to all route factories automatically. Defaults to a noop.
|
|
61
61
|
*/
|
|
62
62
|
on_audit_event?: (event: AuditLogEvent) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Audit-log config for consumer event-type extensions. Built once at
|
|
65
|
+
* startup via `create_audit_log_config({extra_events})` and threaded
|
|
66
|
+
* through `AppDeps.audit_log_config` to every fuz_app emit site so
|
|
67
|
+
* consumer handlers cannot silently fall back to the builtin config.
|
|
68
|
+
* Omit to use `BUILTIN_AUDIT_LOG_CONFIG` (no extra events).
|
|
69
|
+
*/
|
|
70
|
+
audit_log_config?: AuditLogConfig;
|
|
71
|
+
/**
|
|
72
|
+
* Additional migration namespaces to run after the builtin auth namespace.
|
|
73
|
+
* Each namespace's own `schema_version` row tracks progress; order is
|
|
74
|
+
* append-only so forward-only guarantees hold per-namespace.
|
|
75
|
+
*
|
|
76
|
+
* The reserved `'fuz_auth'` namespace is rejected at startup. Omit for no
|
|
77
|
+
* extra namespaces. This is the only place to splice consumer migrations
|
|
78
|
+
* — DB init belongs to the backend lifecycle, not server assembly.
|
|
79
|
+
*/
|
|
80
|
+
migration_namespaces?: ReadonlyArray<MigrationNamespace>;
|
|
63
81
|
}
|
|
64
82
|
/**
|
|
65
83
|
* Initialize the backend: database + auth migrations + deps.
|
|
66
84
|
*
|
|
67
|
-
* Calls `create_db` → `run_migrations` (auth namespace
|
|
68
|
-
*
|
|
85
|
+
* Calls `create_db` → `run_migrations` (auth namespace, then any
|
|
86
|
+
* `migration_namespaces` from options in order) and bundles the result
|
|
87
|
+
* with the provided keyring and password deps.
|
|
69
88
|
*
|
|
70
|
-
* @param options - keyring, password deps,
|
|
71
|
-
* @returns app backend with deps, database metadata, and migration results
|
|
89
|
+
* @param options - keyring, password deps, optional database URL, and optional `migration_namespaces`
|
|
90
|
+
* @returns app backend with deps, database metadata, and combined migration results
|
|
72
91
|
*/
|
|
73
92
|
export declare const create_app_backend: (options: CreateAppBackendOptions) => Promise<AppBackend>;
|
|
74
93
|
//# sourceMappingURL=app_backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAE/C,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/
|
|
1
|
+
{"version":3,"file":"app_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAE/C,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAC,cAAc,EAAE,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/E,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAiB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAI/F;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,oIAAoI;IACpI,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC3D,iEAAiE;IACjE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,+DAA+D;IAC/D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,2BAA2B;IAC3B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAU,SAAS,uBAAuB,KAAG,OAAO,CAAC,UAAU,CAoC7F,CAAC"}
|
|
@@ -12,28 +12,50 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
14
14
|
import { run_migrations } from '../db/migrate.js';
|
|
15
|
-
import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
15
|
+
import { AUTH_MIGRATION_NS, AUTH_MIGRATION_NAMESPACE } from '../auth/migrations.js';
|
|
16
16
|
import { create_db } from '../db/create_db.js';
|
|
17
17
|
/**
|
|
18
18
|
* Initialize the backend: database + auth migrations + deps.
|
|
19
19
|
*
|
|
20
|
-
* Calls `create_db` → `run_migrations` (auth namespace
|
|
21
|
-
*
|
|
20
|
+
* Calls `create_db` → `run_migrations` (auth namespace, then any
|
|
21
|
+
* `migration_namespaces` from options in order) and bundles the result
|
|
22
|
+
* with the provided keyring and password deps.
|
|
22
23
|
*
|
|
23
|
-
* @param options - keyring, password deps,
|
|
24
|
-
* @returns app backend with deps, database metadata, and migration results
|
|
24
|
+
* @param options - keyring, password deps, optional database URL, and optional `migration_namespaces`
|
|
25
|
+
* @returns app backend with deps, database metadata, and combined migration results
|
|
25
26
|
*/
|
|
26
27
|
export const create_app_backend = async (options) => {
|
|
27
28
|
const { database_url, keyring, password, stat, read_text_file, delete_file } = options;
|
|
28
29
|
const log = options.log ?? new Logger('server');
|
|
29
30
|
const on_audit_event = options.on_audit_event ?? (() => { }); // eslint-disable-line @typescript-eslint/no-empty-function
|
|
31
|
+
const { audit_log_config } = options;
|
|
30
32
|
const { db, close, db_type, db_name } = await create_db(database_url);
|
|
31
|
-
|
|
33
|
+
if (options.migration_namespaces?.length) {
|
|
34
|
+
for (const ns of options.migration_namespaces) {
|
|
35
|
+
if (ns.namespace === AUTH_MIGRATION_NAMESPACE) {
|
|
36
|
+
throw new Error(`Migration namespace "${AUTH_MIGRATION_NAMESPACE}" is reserved by fuz_app — choose a different namespace`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const migration_results = await run_migrations(db, [
|
|
41
|
+
AUTH_MIGRATION_NS,
|
|
42
|
+
...(options.migration_namespaces ?? []),
|
|
43
|
+
]);
|
|
32
44
|
return {
|
|
33
45
|
db_type,
|
|
34
46
|
db_name,
|
|
35
47
|
migration_results,
|
|
36
48
|
close,
|
|
37
|
-
deps: {
|
|
49
|
+
deps: {
|
|
50
|
+
keyring,
|
|
51
|
+
password,
|
|
52
|
+
db,
|
|
53
|
+
stat,
|
|
54
|
+
read_text_file,
|
|
55
|
+
delete_file,
|
|
56
|
+
log,
|
|
57
|
+
on_audit_event,
|
|
58
|
+
audit_log_config,
|
|
59
|
+
},
|
|
38
60
|
};
|
|
39
61
|
};
|
|
@@ -17,7 +17,7 @@ import { type AuditLogSse } from '../realtime/sse_auth_guard.js';
|
|
|
17
17
|
import type { AppSettings } from '../auth/app_settings_schema.js';
|
|
18
18
|
import { type RateLimiter } from '../rate_limiter.js';
|
|
19
19
|
import type { DaemonTokenState } from '../auth/daemon_token.js';
|
|
20
|
-
import
|
|
20
|
+
import type { MigrationResult } from '../db/migrate.js';
|
|
21
21
|
import type { AppDeps } from '../auth/deps.js';
|
|
22
22
|
import type { AppBackend } from './app_backend.js';
|
|
23
23
|
import '../hono_context.js';
|
|
@@ -104,8 +104,6 @@ export interface AppServerOptions {
|
|
|
104
104
|
* Default: auto-created (authenticated).
|
|
105
105
|
*/
|
|
106
106
|
surface_route?: false;
|
|
107
|
-
/** Consumer migration namespaces — run after auth migrations during init. */
|
|
108
|
-
migration_namespaces?: Array<MigrationNamespace>;
|
|
109
107
|
/**
|
|
110
108
|
* Build route specs from the initialized backend.
|
|
111
109
|
* Called after all middleware is ready.
|
|
@@ -191,7 +189,7 @@ export interface AppServer {
|
|
|
191
189
|
bootstrap_status: BootstrapStatus;
|
|
192
190
|
/** Global app settings (mutable ref — mutated by settings admin route). */
|
|
193
191
|
app_settings: AppSettings;
|
|
194
|
-
/**
|
|
192
|
+
/** Migration results from `create_app_backend` (auth + any `migration_namespaces` passed there). */
|
|
195
193
|
migration_results: ReadonlyArray<MigrationResult>;
|
|
196
194
|
/** Factory-managed audit log SSE. `null` when `audit_log_sse` option is not set. */
|
|
197
195
|
audit_sse: AuditLogSse | null;
|
|
@@ -203,9 +201,10 @@ export declare const DEFAULT_MAX_BODY_SIZE: number;
|
|
|
203
201
|
/**
|
|
204
202
|
* Create a fully assembled Hono app with auth, middleware, and routes.
|
|
205
203
|
*
|
|
206
|
-
* Handles the
|
|
207
|
-
*
|
|
208
|
-
*
|
|
204
|
+
* Handles the assembly lifecycle: proxy middleware → auth middleware →
|
|
205
|
+
* bootstrap status → route specs → surface generation → Hono app assembly →
|
|
206
|
+
* static serving. Database migrations belong to the backend lifecycle —
|
|
207
|
+
* pass `migration_namespaces` to `create_app_backend`.
|
|
209
208
|
*
|
|
210
209
|
* @param options - server configuration
|
|
211
210
|
* @returns assembled Hono app, backend, surface build, and bootstrap status
|