@fuzdev/fuz_app 0.141.0 → 0.142.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 +10 -5
- package/dist/actions/connection_closer.d.ts +19 -2
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +25 -2
- package/dist/actions/perform_action.d.ts +10 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -9
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -2
- package/dist/auth/CLAUDE.md +33 -13
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +12 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +8 -0
- package/dist/auth/account_routes.d.ts +7 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +240 -187
- package/dist/auth/admin_action_specs.d.ts +6 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +6 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +14 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +85 -76
- package/dist/auth/cell_action_specs.d.ts +7 -3
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +9 -5
- package/dist/auth/cell_actions.d.ts +3 -3
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +67 -38
- package/dist/auth/cell_authorize.d.ts +69 -8
- package/dist/auth/cell_authorize.d.ts.map +1 -1
- package/dist/auth/cell_authorize.js +89 -9
- package/dist/auth/cell_field_action_specs.d.ts +5 -4
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +7 -7
- package/dist/auth/cell_field_actions.d.ts +7 -5
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +24 -22
- package/dist/auth/cell_grant_action_specs.d.ts +6 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +6 -0
- package/dist/auth/cell_grant_actions.d.ts +3 -1
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +16 -2
- package/dist/auth/cell_item_action_specs.d.ts +5 -4
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +7 -7
- package/dist/auth/cell_item_actions.d.ts +6 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +23 -22
- package/dist/auth/cell_relation_list_limit.d.ts +20 -0
- package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
- package/dist/auth/cell_relation_list_limit.js +27 -0
- package/dist/auth/cell_relation_visibility.d.ts +4 -3
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
- package/dist/auth/cell_relation_visibility.js +6 -5
- package/dist/auth/cell_steward.d.ts +45 -0
- package/dist/auth/cell_steward.d.ts.map +1 -0
- package/dist/auth/cell_steward.js +53 -0
- package/dist/auth/request_context.d.ts +4 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +6 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +6 -1
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +23 -16
- package/dist/auth/role_grant_offer_queries.d.ts +8 -6
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +16 -8
- package/dist/auth/signup_routes.d.ts +4 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +157 -134
- package/dist/auth/token_scope.d.ts +7 -1
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +8 -2
- package/dist/db/CLAUDE.md +32 -19
- package/dist/db/cell_ddl.d.ts +1 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +1 -2
- package/dist/db/cell_field_queries.d.ts +20 -18
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +28 -22
- package/dist/db/cell_item_queries.d.ts +20 -14
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +28 -18
- package/dist/db/cell_moderation.d.ts +67 -0
- package/dist/db/cell_moderation.d.ts.map +1 -0
- package/dist/db/cell_moderation.js +66 -0
- package/dist/db/cell_queries.d.ts +44 -14
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +68 -45
- package/dist/db/cell_view_sql.d.ts +102 -0
- package/dist/db/cell_view_sql.d.ts.map +1 -0
- package/dist/db/cell_view_sql.js +112 -0
- package/dist/db/db_pg.d.ts +1 -1
- package/dist/db/db_pg.js +1 -1
- package/dist/http/auth_shape.d.ts +5 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +5 -2
- package/dist/http/ip_canonical.d.ts +30 -3
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +50 -3
- package/dist/primitive_schemas.d.ts +10 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +11 -2
- package/dist/rate_limiter.d.ts +179 -12
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +352 -42
- package/dist/server/serve_fact_route.d.ts +8 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +15 -6
- package/dist/testing/CLAUDE.md +10 -3
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +37 -2
- package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +401 -5
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +145 -2
- package/dist/testing/cross_backend/expected_schema.json +0 -1
- package/dist/testing/cross_backend/login_security.d.ts +16 -0
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +154 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +24 -2
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +64 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
- package/dist/testing/schema_introspect.js +1 -1
- package/dist/testing/schema_parity.d.ts +4 -3
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/schema_parity.js +4 -3
- package/dist/testing/schema_ready_fixture.d.ts +2 -4
- package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
- package/dist/testing/schema_ready_fixture.js +2 -4
- package/dist/ui/auth_state.svelte.d.ts +10 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +22 -4
- package/package.json +1 -1
- package/dist/db/cell_history_ddl.d.ts +0 -38
- package/dist/db/cell_history_ddl.d.ts.map +0 -1
- package/dist/db/cell_history_ddl.js +0 -61
|
@@ -379,15 +379,17 @@ const query_grantor_holds_global_admin = async (deps, grantor_actor_id) => {
|
|
|
379
379
|
* inside a transaction — the caller's route spec should declare
|
|
380
380
|
* `transaction: true` (or wrap explicitly).
|
|
381
381
|
*
|
|
382
|
-
* Idempotent on race:
|
|
383
|
-
*
|
|
384
|
-
* duplicate or throwing.
|
|
382
|
+
* Idempotent on replay and on race: a call that observes the offer already
|
|
383
|
+
* accepted returns the existing role_grant (`created: false`) rather than
|
|
384
|
+
* creating a duplicate or throwing — while that grant is still held. A grant
|
|
385
|
+
* revoked since the accept, or gone, makes the offer terminal.
|
|
385
386
|
*
|
|
386
387
|
* Error map:
|
|
387
388
|
* - `RoleGrantOfferNotFoundError` — offer does not exist, or belongs to a
|
|
388
389
|
* different recipient (IDOR guard). The offer row is untouched.
|
|
389
390
|
* - `RoleGrantOfferAlreadyTerminalError` — offer is declined, retracted, or
|
|
390
|
-
* superseded
|
|
391
|
+
* superseded; or accepted, and its grant revoked, gone, or bound to another
|
|
392
|
+
* actor.
|
|
391
393
|
* - `RoleGrantOfferExpiredError` — offer is pending but past `expires_at`.
|
|
392
394
|
* - `RoleGrantOfferActorMismatchError` — actor-targeted offer, different actor.
|
|
393
395
|
* - `RoleGrantOfferBuiltinRefusedError` — a builtin-role offer that must not
|
|
@@ -404,7 +406,7 @@ const query_grantor_holds_global_admin = async (deps, grantor_actor_id) => {
|
|
|
404
406
|
* @mutates `role_grant_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple
|
|
405
407
|
* @mutates `audit_log` table - emits `role_grant_offer_accept` + `role_grant_create` + one `role_grant_offer_supersede` per sibling
|
|
406
408
|
* @throws RoleGrantOfferNotFoundError if the offer is missing or belongs to another recipient
|
|
407
|
-
* @throws RoleGrantOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
|
|
409
|
+
* @throws RoleGrantOfferAlreadyTerminalError if the offer is declined, retracted, or superseded, or accepted with its grant revoked, gone, or bound to another actor
|
|
408
410
|
* @throws RoleGrantOfferExpiredError if the offer is pending but past `expires_at`
|
|
409
411
|
* @throws RoleGrantOfferActorMismatchError if the offer is actor-targeted to a different actor
|
|
410
412
|
* @throws RoleGrantOfferBuiltinRefusedError if a builtin-role offer must not mint (see `BuiltinOfferRefusal`)
|
|
@@ -436,15 +438,21 @@ export const query_accept_offer = async (deps, input) => {
|
|
|
436
438
|
// it off so `locked` stays exactly `ROLE_GRANT_OFFER_COLUMNS`.
|
|
437
439
|
const { still_valid, ...locked } = locked_row;
|
|
438
440
|
if (locked.accepted_at) {
|
|
439
|
-
//
|
|
441
|
+
// Already accepted (a replay, or a race winner already committed);
|
|
442
|
+
// return the pre-existing role_grant.
|
|
440
443
|
// `role_grant_offer_role_grant_iff_accepted` CHECK guarantees resulting_role_grant_id is non-null.
|
|
441
|
-
const role_grant =
|
|
444
|
+
const role_grant = await query_role_grant_by_id(deps, locked.resulting_role_grant_id);
|
|
445
|
+
// The idempotent answer stands only while the grant it names does. A
|
|
446
|
+
// grant revoked since the accept (or gone) makes a replay terminal:
|
|
447
|
+
// answering OK with the revoked grant's id would tell the caller "you
|
|
448
|
+
// hold this role" when they don't.
|
|
449
|
+
//
|
|
442
450
|
// Multi-actor guard: two actors on the same recipient account may
|
|
443
451
|
// both race an account-grain offer — the loser must not silently
|
|
444
452
|
// receive the winner's role_grant (which would tell them "you got it"
|
|
445
453
|
// while the actor on the role_grant is someone else). Treat the offer
|
|
446
454
|
// as terminal for the loser.
|
|
447
|
-
if (role_grant.actor_id !== actor_id) {
|
|
455
|
+
if (!role_grant || role_grant.revoked_at !== null || role_grant.actor_id !== actor_id) {
|
|
448
456
|
throw new RoleGrantOfferAlreadyTerminalError(offer_id);
|
|
449
457
|
}
|
|
450
458
|
return {
|
|
@@ -41,9 +41,10 @@ export interface SignupRouteOptions extends AuthSessionRouteOptions {
|
|
|
41
41
|
* disable. Its own instance, not login's: signup failures bound username
|
|
42
42
|
* *enumeration* (the 403/409 split), not credential guessing, and an
|
|
43
43
|
* open-signup deployment lets any unauthenticated caller spend this budget
|
|
44
|
-
* — which must not cost anyone their login attempts.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* — which must not cost anyone their login attempts. Charged by every
|
|
45
|
+
* classified failure and by every successful open signup (an invite-gated
|
|
46
|
+
* success spends an invite instead); never refunded on success (see
|
|
47
|
+
* `RateLimiter.reset`). Mirrors the Rust spine's `signup_ip_rate_limiter`.
|
|
47
48
|
*/
|
|
48
49
|
signup_ip_rate_limiter: RateLimiter | null;
|
|
49
50
|
/** Rate limiter for signup attempts, keyed by submitted username. Pass `null` to disable. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGxE,OAAO,EAGN,KAAK,WAAW,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAQhD;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE;;;;;;;;;OASG;IACH,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3C,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CA2MjB,CAAC"}
|
|
@@ -14,7 +14,8 @@ import { query_invite_find_unclaimed_match_for_update, query_invite_claim_unscop
|
|
|
14
14
|
import { create_signup_route_shape } from './signup_route_schema.js';
|
|
15
15
|
import { get_route_input } from '../http/route_spec.js';
|
|
16
16
|
import { get_client_ip } from '../http/client_ip.js';
|
|
17
|
-
import {
|
|
17
|
+
import { rate_limit_ip_key } from '../http/ip_canonical.js';
|
|
18
|
+
import { rate_limit_exceeded_response, reserve_optional } from '../rate_limiter.js';
|
|
18
19
|
import { ERROR_NO_MATCHING_INVITE, ERROR_SIGNUP_CONFLICT } from '../http/error_schemas.js';
|
|
19
20
|
import { is_pg_unique_violation } from '../db/pg_error.js';
|
|
20
21
|
/**
|
|
@@ -62,148 +63,170 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
62
63
|
signup_account_rate_limited: signup_account_rate_limiter !== null
|
|
63
64
|
}),
|
|
64
65
|
handler: async (c, route) => {
|
|
65
|
-
// Per-IP rate limit
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// Per-IP rate limit (before any work). 429 stays fast. Reserved,
|
|
67
|
+
// not checked: the attempt counts against the cap until it settles
|
|
68
|
+
// below, so a concurrent burst can't all pass before any of it
|
|
69
|
+
// records — and the Argon2 hash every attempt pays is bounded with
|
|
70
|
+
// it. The `finally` releases whatever no branch recorded.
|
|
71
|
+
// keyed by the address's rate-limit key (an IPv6 client's /64)
|
|
72
|
+
const ip_key = signup_ip_rate_limiter ? rate_limit_ip_key(get_client_ip(c)) : null;
|
|
73
|
+
const ip_reserved = await reserve_optional(signup_ip_rate_limiter, ip_key, {
|
|
74
|
+
signal: c.req.raw.signal
|
|
75
|
+
});
|
|
76
|
+
if (!ip_reserved.ok) {
|
|
77
|
+
return rate_limit_exceeded_response(c, ip_reserved.retry_after);
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
let account_reservation = null;
|
|
80
|
+
try {
|
|
81
|
+
const { username, password: pw, email } = get_route_input(c);
|
|
82
|
+
// Per-account rate limit (after input parsing, before DB work).
|
|
83
|
+
// 429 stays fast — same precedent as login.
|
|
84
|
+
const account_key = username.toLowerCase();
|
|
85
|
+
const account_reserved = await reserve_optional(signup_account_rate_limiter, account_key, { signal: c.req.raw.signal });
|
|
86
|
+
if (!account_reserved.ok) {
|
|
87
|
+
return rate_limit_exceeded_response(c, account_reserved.retry_after);
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
account_reservation = account_reserved.reservation;
|
|
90
|
+
// Load the open-signup toggle fresh from the DB on every
|
|
91
|
+
// request — the authoritative source, so multiple server
|
|
92
|
+
// processes never serve a stale in-memory value. Its cost, and
|
|
93
|
+
// the hash below, are bounded per address by the per-IP
|
|
94
|
+
// reservation above: every classified failure and every
|
|
95
|
+
// successful open signup records it. What escapes is an
|
|
96
|
+
// invite-gated success (which spends an invite), a 5xx, and a
|
|
97
|
+
// request with no resolved client IP. The per-account limiter
|
|
98
|
+
// bounds retries of one username only.
|
|
99
|
+
const { open_signup } = await query_app_settings_load(route);
|
|
100
|
+
// Start the denial-time floor concurrently with failure work.
|
|
101
|
+
// Observed response time for 403 / 409 is `max(work, delay)`
|
|
102
|
+
// so the cheap `no_match` path (no Argon2, find returns
|
|
103
|
+
// nothing) and the expensive `signup_conflict` path (Argon2
|
|
104
|
+
// + tx + rollback) converge — closes the username-enumeration
|
|
105
|
+
// timing oracle. Mirrors login's `login_fail_delay`. Started
|
|
106
|
+
// after rate-limit checks so 429 stays fast.
|
|
107
|
+
const delay = signup_fail_delay(signup_fail_floor_ms, signup_fail_jitter_ms);
|
|
108
|
+
// Hash before the transaction so the connection isn't held
|
|
109
|
+
// across the ~100ms Argon2id. Paid on every attempt that gets
|
|
110
|
+
// here — bounded as described above.
|
|
111
|
+
const password_hash = await password.hash_password(pw);
|
|
112
|
+
// `invite` is assigned inside the tx by the FOR UPDATE find;
|
|
113
|
+
// captured at the outer scope so the failure-audit catch
|
|
114
|
+
// branch can still reference `invite.id` after the tx rolls
|
|
115
|
+
// back on PG unique violation.
|
|
116
|
+
let invite;
|
|
117
|
+
const emit_failure_audit = (reason) => {
|
|
118
|
+
deps.audit.emit(route, {
|
|
119
|
+
event_type: 'signup',
|
|
120
|
+
outcome: 'failure',
|
|
121
|
+
ip: get_client_ip(c),
|
|
122
|
+
metadata: {
|
|
123
|
+
username,
|
|
124
|
+
reason,
|
|
125
|
+
...(invite && { invite_id: invite.id }),
|
|
126
|
+
...(email != null && { email }),
|
|
127
|
+
...(open_signup && { open_signup: true })
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = await route.db.transaction(async (tx) => {
|
|
134
|
+
const tx_deps = { db: tx };
|
|
135
|
+
// Find + claim run inside the same transaction so the
|
|
136
|
+
// row lock makes them atomic. Concurrent signups for
|
|
137
|
+
// the same (username, email) tuple block on the lock
|
|
138
|
+
// and observe the post-commit state on retry — the
|
|
139
|
+
// loser's `find_for_update` returns no row (winner
|
|
140
|
+
// flipped `claimed_at`) and falls through to
|
|
141
|
+
// `ERROR_NO_MATCHING_INVITE`. No race window.
|
|
142
|
+
if (!open_signup) {
|
|
143
|
+
invite = await query_invite_find_unclaimed_match_for_update(tx_deps, email ?? null, username);
|
|
144
|
+
if (!invite) {
|
|
145
|
+
throw new NoMatchingInviteError();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const { account, actor } = await query_create_account_with_actor(tx_deps, {
|
|
149
|
+
username,
|
|
150
|
+
password_hash,
|
|
151
|
+
email
|
|
152
|
+
});
|
|
153
|
+
if (invite) {
|
|
154
|
+
// Guaranteed to succeed: FOR UPDATE held the row
|
|
155
|
+
// for the duration of the tx, so no concurrent
|
|
156
|
+
// claim could flip `claimed_at` between the find
|
|
157
|
+
// and this UPDATE.
|
|
158
|
+
await query_invite_claim_unscoped(tx_deps, invite.id, account.id);
|
|
159
|
+
}
|
|
160
|
+
await create_session_and_set_cookie({
|
|
161
|
+
keyring,
|
|
162
|
+
deps: tx_deps,
|
|
163
|
+
c,
|
|
164
|
+
account_id: account.id,
|
|
165
|
+
session_options
|
|
166
|
+
});
|
|
167
|
+
return { account, actor };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
if (e instanceof NoMatchingInviteError) {
|
|
172
|
+
ip_reserved.reservation.record();
|
|
173
|
+
account_reservation?.record();
|
|
174
|
+
emit_failure_audit('no_match');
|
|
175
|
+
await delay;
|
|
176
|
+
return c.json({ error: ERROR_NO_MATCHING_INVITE }, 403);
|
|
177
|
+
}
|
|
178
|
+
// Unique constraint violation: username or email already exists.
|
|
179
|
+
if (is_pg_unique_violation(e)) {
|
|
180
|
+
ip_reserved.reservation.record();
|
|
181
|
+
account_reservation?.record();
|
|
182
|
+
emit_failure_audit('signup_conflict');
|
|
183
|
+
await delay;
|
|
184
|
+
return c.json({ error: ERROR_SIGNUP_CONFLICT }, 409);
|
|
185
|
+
}
|
|
186
|
+
// Unclassified failure (e.g. session create error, Argon2
|
|
187
|
+
// fault on hash, DB outage mid-tx). Tx is rolled back so
|
|
188
|
+
// no account persists, but the *attempt* should leave a
|
|
189
|
+
// forensic trail — emit `outcome: 'failure'` with reason
|
|
190
|
+
// `internal_error` before rethrowing. 5xx responses are
|
|
191
|
+
// not floored: they aren't response-time-controlled
|
|
192
|
+
// enumeration oracles.
|
|
193
|
+
emit_failure_audit('internal_error');
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
// Clear the account-grain bucket only — the IP aggregate stands.
|
|
197
|
+
// This is the cheapest version of the spray path `RateLimiter.reset`
|
|
198
|
+
// describes: where login at least costs the attacker a credential,
|
|
199
|
+
// an open-signup deployment let an *unauthenticated* caller zero a
|
|
200
|
+
// shared IP budget by creating throwaway accounts, and the
|
|
201
|
+
// per-account limiter never applies to the attacker's own signups.
|
|
202
|
+
account_reservation?.release();
|
|
203
|
+
if (signup_account_rate_limiter)
|
|
204
|
+
signup_account_rate_limiter.reset(account_key);
|
|
205
|
+
// A successful *open* signup is charged to the IP bucket like a
|
|
206
|
+
// failure: with no invite to spend, account creation is otherwise
|
|
207
|
+
// unbounded from a single address — throwaway accounts, each one a
|
|
208
|
+
// session and a row. An invite-gated success is bounded by the
|
|
209
|
+
// invites an admin minted, so it records nothing (a class signing up
|
|
210
|
+
// on its invites from one NAT egress would otherwise run the bucket
|
|
211
|
+
// dry); the `finally` releases it.
|
|
212
|
+
if (open_signup)
|
|
213
|
+
ip_reserved.reservation.record();
|
|
106
214
|
deps.audit.emit(route, {
|
|
107
215
|
event_type: 'signup',
|
|
108
|
-
|
|
216
|
+
account_id: result.account.id,
|
|
109
217
|
ip: get_client_ip(c),
|
|
110
|
-
metadata: {
|
|
111
|
-
username,
|
|
112
|
-
reason,
|
|
113
|
-
...(invite && { invite_id: invite.id }),
|
|
114
|
-
...(email != null && { email }),
|
|
115
|
-
...(open_signup && { open_signup: true })
|
|
116
|
-
}
|
|
218
|
+
metadata: invite ? { invite_id: invite.id, username } : { open_signup: true, username }
|
|
117
219
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const tx_deps = { db: tx };
|
|
123
|
-
// Find + claim run inside the same transaction so the
|
|
124
|
-
// row lock makes them atomic. Concurrent signups for
|
|
125
|
-
// the same (username, email) tuple block on the lock
|
|
126
|
-
// and observe the post-commit state on retry — the
|
|
127
|
-
// loser's `find_for_update` returns no row (winner
|
|
128
|
-
// flipped `claimed_at`) and falls through to
|
|
129
|
-
// `ERROR_NO_MATCHING_INVITE`. No race window.
|
|
130
|
-
if (!open_signup) {
|
|
131
|
-
invite = await query_invite_find_unclaimed_match_for_update(tx_deps, email ?? null, username);
|
|
132
|
-
if (!invite) {
|
|
133
|
-
throw new NoMatchingInviteError();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const { account, actor } = await query_create_account_with_actor(tx_deps, {
|
|
137
|
-
username,
|
|
138
|
-
password_hash,
|
|
139
|
-
email
|
|
140
|
-
});
|
|
141
|
-
if (invite) {
|
|
142
|
-
// Guaranteed to succeed: FOR UPDATE held the row
|
|
143
|
-
// for the duration of the tx, so no concurrent
|
|
144
|
-
// claim could flip `claimed_at` between the find
|
|
145
|
-
// and this UPDATE.
|
|
146
|
-
await query_invite_claim_unscoped(tx_deps, invite.id, account.id);
|
|
147
|
-
}
|
|
148
|
-
await create_session_and_set_cookie({
|
|
149
|
-
keyring,
|
|
150
|
-
deps: tx_deps,
|
|
151
|
-
c,
|
|
152
|
-
account_id: account.id,
|
|
153
|
-
session_options
|
|
154
|
-
});
|
|
155
|
-
return { account, actor };
|
|
220
|
+
return c.json({
|
|
221
|
+
ok: true,
|
|
222
|
+
account: { id: result.account.id, username: result.account.username },
|
|
223
|
+
actor: { id: result.actor.id }
|
|
156
224
|
});
|
|
157
225
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
signup_ip_rate_limiter.record(ip);
|
|
162
|
-
if (signup_account_rate_limiter)
|
|
163
|
-
signup_account_rate_limiter.record(account_key);
|
|
164
|
-
emit_failure_audit('no_match');
|
|
165
|
-
await delay;
|
|
166
|
-
return c.json({ error: ERROR_NO_MATCHING_INVITE }, 403);
|
|
167
|
-
}
|
|
168
|
-
// Unique constraint violation: username or email already exists.
|
|
169
|
-
if (is_pg_unique_violation(e)) {
|
|
170
|
-
if (signup_ip_rate_limiter && ip)
|
|
171
|
-
signup_ip_rate_limiter.record(ip);
|
|
172
|
-
if (signup_account_rate_limiter)
|
|
173
|
-
signup_account_rate_limiter.record(account_key);
|
|
174
|
-
emit_failure_audit('signup_conflict');
|
|
175
|
-
await delay;
|
|
176
|
-
return c.json({ error: ERROR_SIGNUP_CONFLICT }, 409);
|
|
177
|
-
}
|
|
178
|
-
// Unclassified failure (e.g. session create error, Argon2
|
|
179
|
-
// fault on hash, DB outage mid-tx). Tx is rolled back so
|
|
180
|
-
// no account persists, but the *attempt* should leave a
|
|
181
|
-
// forensic trail — emit `outcome: 'failure'` with reason
|
|
182
|
-
// `internal_error` before rethrowing. 5xx responses are
|
|
183
|
-
// not floored: they aren't response-time-controlled
|
|
184
|
-
// enumeration oracles.
|
|
185
|
-
emit_failure_audit('internal_error');
|
|
186
|
-
throw e;
|
|
226
|
+
finally {
|
|
227
|
+
ip_reserved.reservation.release();
|
|
228
|
+
account_reservation?.release();
|
|
187
229
|
}
|
|
188
|
-
// Clear the account-grain bucket only — the IP aggregate stands.
|
|
189
|
-
// This is the cheapest version of the spray path `RateLimiter.reset`
|
|
190
|
-
// describes: where login at least costs the attacker a credential,
|
|
191
|
-
// an open-signup deployment let an *unauthenticated* caller zero a
|
|
192
|
-
// shared IP budget by creating throwaway accounts, and the
|
|
193
|
-
// per-account limiter never applies to the attacker's own signups.
|
|
194
|
-
if (signup_account_rate_limiter)
|
|
195
|
-
signup_account_rate_limiter.reset(account_key);
|
|
196
|
-
deps.audit.emit(route, {
|
|
197
|
-
event_type: 'signup',
|
|
198
|
-
account_id: result.account.id,
|
|
199
|
-
ip: get_client_ip(c),
|
|
200
|
-
metadata: invite ? { invite_id: invite.id, username } : { open_signup: true, username }
|
|
201
|
-
});
|
|
202
|
-
return c.json({
|
|
203
|
-
ok: true,
|
|
204
|
-
account: { id: result.account.id, username: result.account.username },
|
|
205
|
-
actor: { id: result.actor.id }
|
|
206
|
-
});
|
|
207
230
|
}
|
|
208
231
|
}
|
|
209
232
|
];
|
|
@@ -77,7 +77,13 @@ export type TokenScopeInput = z.infer<typeof TokenScopeInput>;
|
|
|
77
77
|
export declare const token_scope_full: () => TokenScope;
|
|
78
78
|
/** A narrowed scope over `methods`. */
|
|
79
79
|
export declare const token_scope_methods: (methods: Array<string>) => TokenScope;
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Build the stored scope from a mint-time input.
|
|
82
|
+
*
|
|
83
|
+
* Duplicate method names collapse into one, first occurrence kept: the set is
|
|
84
|
+
* what the scope admits, and the Rust spine stores a set, so both count the
|
|
85
|
+
* same methods (the `account_token_list` label reads `N methods`).
|
|
86
|
+
*/
|
|
81
87
|
export declare const token_scope_from_input: (input: TokenScopeInput) => TokenScope;
|
|
82
88
|
/**
|
|
83
89
|
* Does `scope` admit dispatching `method`?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UAC8B,CAAC;AAE/F;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,iFAMjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,YACA,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AAQtE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,KAAG,MAC3B,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,oBAAoB,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,MAAM,KAAG,oBAAoB,GAAG,IAUnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,OAAO,UAAU,EACjB,YAAY,oBAAoB,KAC9B,OAGkC,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,YAAY,MAAM,KAChB;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAGxD,CAAC;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAGrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IAU5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,UAAU,KAAG,MAQzD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,UAAU,GAAG,IAQtE,CAAC"}
|
package/dist/auth/token_scope.js
CHANGED
|
@@ -83,8 +83,14 @@ export const token_scope_methods = (methods) => ({
|
|
|
83
83
|
kind: 'methods',
|
|
84
84
|
methods
|
|
85
85
|
});
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Build the stored scope from a mint-time input.
|
|
88
|
+
*
|
|
89
|
+
* Duplicate method names collapse into one, first occurrence kept: the set is
|
|
90
|
+
* what the scope admits, and the Rust spine stores a set, so both count the
|
|
91
|
+
* same methods (the `account_token_list` label reads `N methods`).
|
|
92
|
+
*/
|
|
93
|
+
export const token_scope_from_input = (input) => input.kind === 'full' ? token_scope_full() : token_scope_methods([...new Set(input.methods)]);
|
|
88
94
|
/**
|
|
89
95
|
* Does `scope` admit dispatching `method`?
|
|
90
96
|
*
|
package/dist/db/CLAUDE.md
CHANGED
|
@@ -95,21 +95,33 @@ The wire schemas + RPC handlers + authz predicates for this layer live in
|
|
|
95
95
|
authorizer's verdict + the `cell_moderate` verb, never in `data`. Nullable
|
|
96
96
|
`created_by` / `updated_by` FKs to `actor` (NULL = system origin). GIN on `data`
|
|
97
97
|
/ `refs`; global partial-unique on `path`.
|
|
98
|
-
- **`cell_history_ddl.ts`** — dormant `cell_history` table
|
|
99
|
-
(`CELL_HISTORY_MIGRATION_NS`, namespace `fuz_cell_history`), FK → `cell.id`.
|
|
100
|
-
Ships present-but-unwritten; no snapshot lifecycle yet.
|
|
101
98
|
- **`cell_queries.ts`** — `query_cell_create / get / get_by_path / update /
|
|
102
99
|
delete / set_moderation`, `_list_by_kind / _list_by_creator`, the
|
|
103
100
|
generic `query_cell_list` (filter + SQL-side visibility predicate mirroring
|
|
104
|
-
`can_view_cell
|
|
105
|
-
`ref` filter by `cell.refs`, plus `root_id` /
|
|
106
|
-
per-root feed + mod queue),
|
|
107
|
-
`query_cell_load_many` (bulk id load, no visibility filter —
|
|
108
|
-
|
|
101
|
+
`can_view_cell`, moderation admission included; the `kind` filter narrows
|
|
102
|
+
by the `cell.kind` column, the `ref` filter by `cell.refs`, plus `root_id` /
|
|
103
|
+
`moderation` filters for the per-root feed + mod queue),
|
|
104
|
+
`query_cell_load_many` (bulk id load of full rows, no visibility filter —
|
|
105
|
+
the deep-clone walk), and `query_cell_view_load_many` (the
|
|
106
|
+
`CELL_VIEW_COLUMNS` view facts alone — feeds `filter_visible_target_ids`,
|
|
107
|
+
the in-memory relation filter the `cell_get` bundle and the clone walk
|
|
108
|
+
run). `query_cell_set_moderation` is the dedicated
|
|
109
109
|
`cell_moderate` write (`moderation` is absent from `CellUpdatePatch`, so an
|
|
110
110
|
author can't self-approve via `cell_update`). `cell.refs` derived from
|
|
111
111
|
`data` via `extract_refs` on create/update. `CellRow.grant_count` is a
|
|
112
112
|
derived projection (correlated subquery on `idx_cell_grant_cell`).
|
|
113
|
+
- **`cell_moderation.ts`** — the `cell.moderation` read rules in pure and
|
|
114
|
+
SQL form side by side (`cell_moderation_admits_audience` /
|
|
115
|
+
`_sql`: the allowlist unmoderated ‖ `'approved'`), composed by
|
|
116
|
+
`auth/cell_authorize.ts` and `query_cell_list` so the two can't drift; the
|
|
117
|
+
`CELL_MODERATION_*` literals. Twin of the Rust `fuz_cell::cell_moderation`.
|
|
118
|
+
- **`cell_view_sql.ts`** — `can_view_cell` in SQL: `can_view_cell_sql`
|
|
119
|
+
(admin ‖ owner ‖ moderation-admitted public or grant), the
|
|
120
|
+
`caller_role_grants_cte` its role-shaped grant arm reads, and `CellViewer`
|
|
121
|
+
(the caller it binds — `auth/cell_authorize.ts`'s `cell_viewer_from_auth`
|
|
122
|
+
builds one; `CELL_VIEWER_UNRESTRICTED` filters nothing). `query_cell_list`
|
|
123
|
+
embeds it beside its steward arm, and the relation lists apply it to the
|
|
124
|
+
far end before `LIMIT`. Twin of the Rust `fuz_cell::cell_view_sql`.
|
|
113
125
|
- **`cell_grant_queries.ts`** — resource-side ACL: `query_cell_grant_create`
|
|
114
126
|
(UPSERT on the partial unique index, actor- vs role-shaped principal),
|
|
115
127
|
`_get`, `_delete` (returns deleted row), `_list_for_cell`,
|
|
@@ -119,11 +131,14 @@ delete / set_moderation`, `_list_by_kind / _list_by_creator`, the
|
|
|
119
131
|
- **`cell_field_queries.ts`** — named relations (`(source_id, name) →
|
|
120
132
|
target_id`): `query_cell_field_set` (UPSERT), `_get`, `_delete`,
|
|
121
133
|
`_list_for_source` (`{limit, name_after}`), `_list_for_target`. Reads JOIN
|
|
122
|
-
cell with `deleted_at IS NULL` so dangling relations don't surface
|
|
134
|
+
cell with `deleted_at IS NULL` so dangling relations don't surface; the
|
|
135
|
+
list reads take a `CellViewer` and keep only edges whose far end it can
|
|
136
|
+
view, before `LIMIT`.
|
|
123
137
|
- **`cell_item_queries.ts`** — ordered children (`(parent_id, position) →
|
|
124
138
|
child_id`, fractional-index keyed): `query_cell_item_insert` (throws
|
|
125
139
|
`23505` on collision), `_get`, `_move`, `_delete`, `_list_for_parent`
|
|
126
|
-
(`{limit, position_after}`), `_list_for_child`. Same soft-delete JOIN
|
|
140
|
+
(`{limit, position_after}`), `_list_for_child`. Same soft-delete JOIN and
|
|
141
|
+
viewer filter.
|
|
127
142
|
- **`cell_audit_queries.ts`** — `query_audit_log_list_by_cell`: matches
|
|
128
143
|
`audit_log` rows whose `metadata` names the cell on any of six cell-domain
|
|
129
144
|
keys (`cell_id` / `source_id` / `new_id` / `parent_id` / `child_id` /
|
|
@@ -191,22 +206,20 @@ DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
|
|
|
191
206
|
FK dependency dictates order; consumers register via `migration_namespaces`
|
|
192
207
|
on `create_app_backend` (after the built-in `fuz_auth` namespace). The fact
|
|
193
208
|
namespace is FK-independent so its slot is free; the spine binary and every
|
|
194
|
-
consumer place it last
|
|
195
|
-
|
|
196
|
-
is assigned per-namespace — so only the cell→cell_history FK constrains it):
|
|
209
|
+
consumer place it last (cross-namespace order is immaterial to the
|
|
210
|
+
`schema_version` rows — `sequence` is assigned per-namespace):
|
|
197
211
|
|
|
198
212
|
```
|
|
199
213
|
auth_migration_ns (built-in, fuz_auth)
|
|
200
|
-
→ CELL_MIGRATION_NS
|
|
201
|
-
→
|
|
202
|
-
→ FACT_MIGRATION_NS (fuz_facts — no external FK; slot is free)
|
|
214
|
+
→ CELL_MIGRATION_NS (fuz_cell — FKs actor.id)
|
|
215
|
+
→ FACT_MIGRATION_NS (fuz_facts — no external FK; slot is free)
|
|
203
216
|
```
|
|
204
217
|
|
|
205
218
|
### Pre-stable migration policy
|
|
206
219
|
|
|
207
|
-
Scoped to the namespaces on this page (`fuz_cell` / `
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
Scoped to the namespaces on this page (`fuz_cell` / `fuz_facts`): each is a
|
|
221
|
+
single `full_*_schema` entry, so when it needs to change shape, **rewrite the
|
|
222
|
+
original in place** rather than appending a
|
|
210
223
|
follow-up — consumers re-bootstrap dev DBs on upgrade. The `fuz_auth`
|
|
211
224
|
namespace is the opposite — frozen and append-only, since consumers hold
|
|
212
225
|
deployed databases on it. See ../../../docs/migrations.md §Schema stability.
|
package/dist/db/cell_ddl.d.ts
CHANGED
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* **Single-migration shape**: `full_cell_schema` creates the canonical
|
|
31
31
|
* cell + cell_grant + cell_field + cell_item layout in one shot from the
|
|
32
|
-
* live exported constants.
|
|
33
|
-
* separate `fuz_cell_history` namespace (`cell_history_ddl.ts`).
|
|
32
|
+
* live exported constants.
|
|
34
33
|
*
|
|
35
34
|
* @module
|
|
36
35
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,WAAW,gkBAgBtB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAkBtC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,0iBAc5B,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAW5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,+RAO5B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAE5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,qSAO3B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAG3C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,uBAAuB;AACvB,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuB5C,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/db/cell_ddl.js
CHANGED
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* **Single-migration shape**: `full_cell_schema` creates the canonical
|
|
31
31
|
* cell + cell_grant + cell_field + cell_item layout in one shot from the
|
|
32
|
-
* live exported constants.
|
|
33
|
-
* separate `fuz_cell_history` namespace (`cell_history_ddl.ts`).
|
|
32
|
+
* live exported constants.
|
|
34
33
|
*
|
|
35
34
|
* @module
|
|
36
35
|
*/
|