@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
|
@@ -12,7 +12,8 @@ import { bootstrap_account } from './bootstrap_account.js';
|
|
|
12
12
|
import { bootstrap_route_shape } from './bootstrap_route_schema.js';
|
|
13
13
|
import { get_route_input } from '../http/route_spec.js';
|
|
14
14
|
import { get_client_ip } from '../http/client_ip.js';
|
|
15
|
-
import {
|
|
15
|
+
import { rate_limit_ip_key } from '../http/ip_canonical.js';
|
|
16
|
+
import { rate_limit_exceeded_response, reserve_optional } from '../rate_limiter.js';
|
|
16
17
|
import { ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING } from '../http/error_schemas.js';
|
|
17
18
|
/**
|
|
18
19
|
* Check bootstrap availability at startup.
|
|
@@ -76,88 +77,96 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
76
77
|
if (!bootstrap_status.available || token_path === null) {
|
|
77
78
|
return c.json({ error: ERROR_ALREADY_BOOTSTRAPPED }, 403);
|
|
78
79
|
}
|
|
79
|
-
// Per-IP rate limit
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
// Per-IP rate limit (before any token/DB work). Reserved, not
|
|
81
|
+
// checked: the attempt counts against the cap until it settles, so
|
|
82
|
+
// a concurrent burst of token guesses can't all pass before any of
|
|
83
|
+
// it records. The `finally` releases it unless a failure recorded it.
|
|
84
|
+
// keyed by the address's rate-limit key (an IPv6 client's /64)
|
|
85
|
+
const ip_key = bootstrap_ip_rate_limiter ? rate_limit_ip_key(get_client_ip(c)) : null;
|
|
86
|
+
const ip_reserved = await reserve_optional(bootstrap_ip_rate_limiter, ip_key, {
|
|
87
|
+
signal: c.req.raw.signal
|
|
88
|
+
});
|
|
89
|
+
if (!ip_reserved.ok) {
|
|
90
|
+
return rate_limit_exceeded_response(c, ip_reserved.retry_after);
|
|
86
91
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
try {
|
|
93
|
+
const input = get_route_input(c);
|
|
94
|
+
// `transaction: false` makes `route.db` the pool. `bootstrap_account`
|
|
95
|
+
// manages its own transaction internally.
|
|
96
|
+
const result = await bootstrap_account({
|
|
97
|
+
db: route.db,
|
|
98
|
+
token_path,
|
|
99
|
+
read_secure_file: deps.read_secure_file,
|
|
100
|
+
delete_file: deps.delete_file,
|
|
101
|
+
password: deps.password,
|
|
102
|
+
log: deps.log
|
|
103
|
+
}, input.token, input);
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
// An unreadable token file closes the window. Bootstrap cannot
|
|
106
|
+
// succeed without one, and `check_bootstrap_status` already reads
|
|
107
|
+
// an unreadable file as unavailable at startup — so leaving the
|
|
108
|
+
// flag set means the two disagree, and every later request takes
|
|
109
|
+
// this leg and writes another audit row. The limiter would bound
|
|
110
|
+
// that channel; closing it ends it, and stops `/status`
|
|
111
|
+
// advertising a window that can't be walked through. Twin of the
|
|
112
|
+
// Rust spine's `bootstrap_handler`, which does the same.
|
|
113
|
+
if (result.error === ERROR_TOKEN_FILE_MISSING)
|
|
114
|
+
bootstrap_status.available = false;
|
|
115
|
+
ip_reserved.reservation.record();
|
|
116
|
+
deps.audit.emit(route, {
|
|
117
|
+
event_type: 'bootstrap',
|
|
118
|
+
outcome: 'failure',
|
|
119
|
+
ip: get_client_ip(c),
|
|
120
|
+
metadata: { error: result.error }
|
|
121
|
+
});
|
|
122
|
+
return c.json({ error: result.error }, result.status);
|
|
123
|
+
}
|
|
124
|
+
// Successful bootstrap — update state immediately. Nothing is
|
|
125
|
+
// reset: bootstrap has no account-grain bucket to forgive (it
|
|
126
|
+
// predates any account), and the IP bucket is never refunded on
|
|
127
|
+
// success (see `RateLimiter.reset`). So this is the one auth
|
|
128
|
+
// surface where a success clears nothing at all. Bootstrap is
|
|
129
|
+
// one-shot, so at most `max_attempts - 1` residual entries
|
|
130
|
+
// survive — and they sit on this route's own limiter, not
|
|
131
|
+
// login's, so a fumbled token can't spend the operator's login
|
|
132
|
+
// budget on a deployment where their account is the only one.
|
|
133
|
+
bootstrap_status.available = false;
|
|
134
|
+
await create_session_and_set_cookie({
|
|
135
|
+
keyring,
|
|
136
|
+
deps: { db: route.db },
|
|
137
|
+
c,
|
|
138
|
+
account_id: result.account.id,
|
|
139
|
+
session_options
|
|
140
|
+
});
|
|
141
|
+
if (on_bootstrap) {
|
|
142
|
+
try {
|
|
143
|
+
await on_bootstrap(result, c);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
deps.log.error(`on_bootstrap callback failed: ${to_error_message(err)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
111
149
|
deps.audit.emit(route, {
|
|
112
150
|
event_type: 'bootstrap',
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
actor_id: result.actor.id,
|
|
152
|
+
account_id: result.account.id,
|
|
153
|
+
ip: get_client_ip(c)
|
|
116
154
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// success (see `RateLimiter.reset`). So this is the one auth
|
|
123
|
-
// surface where a success clears nothing at all. Bootstrap is
|
|
124
|
-
// one-shot, so at most `max_attempts - 1` residual entries
|
|
125
|
-
// survive — and they sit on this route's own limiter, not
|
|
126
|
-
// login's, so a fumbled token can't spend the operator's login
|
|
127
|
-
// budget on a deployment where their account is the only one.
|
|
128
|
-
bootstrap_status.available = false;
|
|
129
|
-
await create_session_and_set_cookie({
|
|
130
|
-
keyring,
|
|
131
|
-
deps: { db: route.db },
|
|
132
|
-
c,
|
|
133
|
-
account_id: result.account.id,
|
|
134
|
-
session_options
|
|
135
|
-
});
|
|
136
|
-
if (on_bootstrap) {
|
|
137
|
-
try {
|
|
138
|
-
await on_bootstrap(result, c);
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
deps.log.error(`on_bootstrap callback failed: ${to_error_message(err)}`);
|
|
155
|
+
// CRITICAL: If token file deletion failed, throw to force operator attention.
|
|
156
|
+
// All success work (session, on_bootstrap, audit) has completed above.
|
|
157
|
+
// The error response alerts the operator to delete the token file manually.
|
|
158
|
+
if (!result.token_file_deleted) {
|
|
159
|
+
throw new Error(`Bootstrap succeeded but token file was not deleted at ${token_path}. Delete it manually and log in.`);
|
|
142
160
|
}
|
|
161
|
+
return c.json({
|
|
162
|
+
ok: true,
|
|
163
|
+
account: { id: result.account.id, username: result.account.username },
|
|
164
|
+
actor: { id: result.actor.id }
|
|
165
|
+
});
|
|
143
166
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
actor_id: result.actor.id,
|
|
147
|
-
account_id: result.account.id,
|
|
148
|
-
ip: get_client_ip(c)
|
|
149
|
-
});
|
|
150
|
-
// CRITICAL: If token file deletion failed, throw to force operator attention.
|
|
151
|
-
// All success work (session, on_bootstrap, audit) has completed above.
|
|
152
|
-
// The error response alerts the operator to delete the token file manually.
|
|
153
|
-
if (!result.token_file_deleted) {
|
|
154
|
-
throw new Error(`Bootstrap succeeded but token file was not deleted at ${token_path}. Delete it manually and log in.`);
|
|
167
|
+
finally {
|
|
168
|
+
ip_reserved.reservation.release();
|
|
155
169
|
}
|
|
156
|
-
return c.json({
|
|
157
|
-
ok: true,
|
|
158
|
-
account: { id: result.account.id, username: result.account.username },
|
|
159
|
-
actor: { id: result.actor.id }
|
|
160
|
-
});
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
];
|
|
@@ -252,7 +252,9 @@ export type CellGetInput = z.infer<typeof CellGetInput>;
|
|
|
252
252
|
* Output for `cell_get`. Bundles relation arrays (`fields` + `items`)
|
|
253
253
|
* server-side via JOINs so the common "show this cell with its
|
|
254
254
|
* children" flow needs one round-trip. Targets are filtered to those the
|
|
255
|
-
* caller may view (strict target-visibility)
|
|
255
|
+
* caller may view (strict target-visibility); `*_truncated` reports the
|
|
256
|
+
* relation's size before that filter, so it can be `true` while fewer than
|
|
257
|
+
* `CELL_RELATIONS_BUNDLE_LIMIT` edges are returned. Per-relation `LIMIT
|
|
256
258
|
* CELL_RELATIONS_BUNDLE_LIMIT`; clients paginate via
|
|
257
259
|
* `cell_item_list({parent_id, position_after})` /
|
|
258
260
|
* `cell_field_list({source_id})` when truncated.
|
|
@@ -501,8 +503,10 @@ export type CellModerationDecision = z.infer<typeof CellModerationDecision>;
|
|
|
501
503
|
* Input for `cell_moderate` — the `pending → approved | rejected` transition,
|
|
502
504
|
* gated on moderation authority over the **governing root** (admin / root
|
|
503
505
|
* owner in v1), not the contribution (which the author manages, and could
|
|
504
|
-
* otherwise self-approve). 404 when the
|
|
505
|
-
*
|
|
506
|
+
* otherwise self-approve). 404 when the caller neither views the target nor
|
|
507
|
+
* stewards it (a root manager reads a pending or rejected contribution through
|
|
508
|
+
* the steward arm); 403 when the caller reaches it but isn't a root manager
|
|
509
|
+
* (the author lands here).
|
|
506
510
|
*/
|
|
507
511
|
export declare const CellModerateInput: z.ZodObject<{
|
|
508
512
|
cell_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_action_specs.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"cell_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_action_specs.ts"],"names":[],"mappings":"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"}
|
|
@@ -179,7 +179,7 @@ export const CellJson = z.strictObject({
|
|
|
179
179
|
description: 'Capability / identity tag — a write-once top-level column (peer to `visibility` / `path`), not a field inside `data`. `null` for a typeless cell. The discriminator a creation authorizer gates on; fixed at birth.'
|
|
180
180
|
}),
|
|
181
181
|
visibility: CellVisibility.meta({
|
|
182
|
-
description: "Access-control tag. `'public'` admits everyone (including unauthenticated visitors); `'private'` (default) admits admin / owner / `cell_grant`-admitted callers. Top-level column, not inside `data`."
|
|
182
|
+
description: "Access-control tag. `'public'` admits everyone (including unauthenticated visitors); `'private'` (default) admits admin / owner / `cell_grant`-admitted callers. Past admin and owner, either admits only while `moderation` is `null` or `'approved'`. Top-level column, not inside `data`."
|
|
183
183
|
}),
|
|
184
184
|
refs: z.array(FactHashSchema).nullable(),
|
|
185
185
|
parent_id: Uuid.nullable().meta({
|
|
@@ -247,7 +247,9 @@ export const CellGetInput = z
|
|
|
247
247
|
* Output for `cell_get`. Bundles relation arrays (`fields` + `items`)
|
|
248
248
|
* server-side via JOINs so the common "show this cell with its
|
|
249
249
|
* children" flow needs one round-trip. Targets are filtered to those the
|
|
250
|
-
* caller may view (strict target-visibility)
|
|
250
|
+
* caller may view (strict target-visibility); `*_truncated` reports the
|
|
251
|
+
* relation's size before that filter, so it can be `true` while fewer than
|
|
252
|
+
* `CELL_RELATIONS_BUNDLE_LIMIT` edges are returned. Per-relation `LIMIT
|
|
251
253
|
* CELL_RELATIONS_BUNDLE_LIMIT`; clients paginate via
|
|
252
254
|
* `cell_item_list({parent_id, position_after})` /
|
|
253
255
|
* `cell_field_list({source_id})` when truncated.
|
|
@@ -385,8 +387,10 @@ export const CellModerationDecision = z.enum(['approved', 'rejected']);
|
|
|
385
387
|
* Input for `cell_moderate` — the `pending → approved | rejected` transition,
|
|
386
388
|
* gated on moderation authority over the **governing root** (admin / root
|
|
387
389
|
* owner in v1), not the contribution (which the author manages, and could
|
|
388
|
-
* otherwise self-approve). 404 when the
|
|
389
|
-
*
|
|
390
|
+
* otherwise self-approve). 404 when the caller neither views the target nor
|
|
391
|
+
* stewards it (a root manager reads a pending or rejected contribution through
|
|
392
|
+
* the steward arm); 403 when the caller reaches it but isn't a root manager
|
|
393
|
+
* (the author lands here).
|
|
390
394
|
*/
|
|
391
395
|
export const CellModerateInput = z.strictObject({
|
|
392
396
|
cell_id: Uuid.meta({ description: 'Contribution to moderate.' }),
|
|
@@ -477,7 +481,7 @@ export const cell_moderate_action_spec = {
|
|
|
477
481
|
output: CellModerateOutput,
|
|
478
482
|
async: true,
|
|
479
483
|
rate_limit: 'account',
|
|
480
|
-
description: 'Moderate a gated contribution (`pending → approved | rejected`). Gated on moderation authority over the governing root (admin / root owner), not the contribution — so the author cannot self-approve. Approval flips `moderation → approved` + `visibility → public`; rejection sets `moderation → rejected`. 404 on
|
|
484
|
+
description: 'Moderate a gated contribution (`pending → approved | rejected`). Gated on moderation authority over the governing root (admin / root owner), not the contribution — so the author cannot self-approve. Approval flips `moderation → approved` + `visibility → public`; rejection sets `moderation → rejected`. 404 on a target the caller neither views nor stewards (a root manager reads a pending or rejected contribution), 403 for a viewable target whose caller is not a root manager.'
|
|
481
485
|
};
|
|
482
486
|
/**
|
|
483
487
|
* All cell-layer action specs — composed by app registries. Bundles the
|
|
@@ -60,7 +60,7 @@ export interface CellCreateAuthorizeInput {
|
|
|
60
60
|
parent_id: Uuid | null;
|
|
61
61
|
/** The governing root of the directory subtree, resolved by the handler from the parent (`parent.root_id ?? parent.id`). `null` for a root creation. */
|
|
62
62
|
root_id: Uuid | null;
|
|
63
|
-
/** The governing root's `data` — the handler reads it in-tx
|
|
63
|
+
/** The governing root's `data` — the handler reads it in-tx and hands it over, so a directory-aware authorizer resolves `root.data.policy[kind]` **without a DB read of its own** (pure predicate; reading in-tx avoids the single-connection PGlite deadlock a separate handle would hit). Non-null whenever `root_id` is — a missing or soft-deleted root answers `cell_not_found` before the authorizer runs — and `null` for a root creation. */
|
|
64
64
|
root_data: CellData | null;
|
|
65
65
|
/** Target scope — designed-in for M2 space-scoping; **always `null` in v1** (cells carry no scope column). */
|
|
66
66
|
scope_id: Uuid | null;
|
|
@@ -85,8 +85,8 @@ export type CellCreateVerdict = {
|
|
|
85
85
|
* contributions; answers "may *this actor* create *this kind* here?" and
|
|
86
86
|
* returns a `CellCreateVerdict`. Runs in `cell_create` after `validate_data`
|
|
87
87
|
* and after the handler resolves `root_id` from the parent (an unviewable
|
|
88
|
-
* parent already 404-masks before this runs).
|
|
89
|
-
* (all consumers untouched). Async-capable (DB / policy calls) — a DB-backed
|
|
88
|
+
* parent or a missing or soft-deleted root already 404-masks before this runs).
|
|
89
|
+
* Omitted = today's open create (all consumers untouched). Async-capable (DB / policy calls) — a DB-backed
|
|
90
90
|
* impl closes over its own `db` (the create handler stays unaware).
|
|
91
91
|
*/
|
|
92
92
|
export type CellCreateAuthorize = (auth: RequestActorContext, input: CellCreateAuthorizeInput) => CellCreateVerdict | Promise<CellCreateVerdict>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,
|
|
1
|
+
{"version":3,"file":"cell_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAEN,KAAK,mBAAmB,EAExB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAoCN,KAAK,QAAQ,EAEb,MAAM,wBAAwB,CAAC;AAChC,OAAO,EASN,KAAK,OAAO,EACZ,MAAM,uBAAuB,CAAC;AAkC/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACxC,uFAAuF;IACvF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,6HAA6H;IAC7H,IAAI,EAAE,QAAQ,CAAC;IACf,wHAAwH;IACxH,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,wJAAwJ;IACxJ,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,qbAAqb;IACrb,SAAS,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,8GAA8G;IAC9G,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAC;AAEjG;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,CACjC,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,wBAAwB,KAC3B,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,GAAG,iBAAiB,GAAG;IAChD,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,QAAQ,CAAC;IAC7C,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;CACvC,CAAC;AA6BF,eAAO,MAAM,YAAY,GAAI,KAAK,OAAO,KAAG,QAgB1C,CAAC;AAkDH,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB,GAAI,MAAM,cAAc,KAAG,KAAK,CAAC,SAAS,CA8pBzE,CAAC"}
|
|
@@ -49,8 +49,10 @@ import { query_cell_create, query_cell_get, query_cell_get_by_path, query_cell_u
|
|
|
49
49
|
import { query_cell_grant_list_for_cell, query_cell_grants_for_caller_in_cells } from '../db/cell_grant_queries.js';
|
|
50
50
|
import { query_cell_field_list_for_source, query_cell_field_set } from '../db/cell_field_queries.js';
|
|
51
51
|
import { query_cell_item_insert, query_cell_item_list_for_parent } from '../db/cell_item_queries.js';
|
|
52
|
-
import { can_view_cell, can_edit_cell, can_manage_cell } from './cell_authorize.js';
|
|
52
|
+
import { can_view_cell, can_edit_cell, can_manage_cell, cell_viewer_from_auth } from './cell_authorize.js';
|
|
53
53
|
import { filter_visible_target_ids } from './cell_relation_visibility.js';
|
|
54
|
+
import { CELL_VIEWER_UNRESTRICTED } from '../db/cell_view_sql.js';
|
|
55
|
+
import { cell_steward_admits } from './cell_steward.js';
|
|
54
56
|
import { to_grant_json } from './cell_grant_actions.js';
|
|
55
57
|
import { to_field_json } from './cell_field_actions.js';
|
|
56
58
|
import { to_item_json } from './cell_item_actions.js';
|
|
@@ -110,6 +112,25 @@ const emit_cell_audit = (ctx, event_type, row, deps, auth) => {
|
|
|
110
112
|
}
|
|
111
113
|
});
|
|
112
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* Gate a loaded cell on `can_view_cell` or, when that denies, the steward arm
|
|
117
|
+
* (`cell_steward_admits` — the governing root's manager reading a pending or
|
|
118
|
+
* rejected contribution), 404-masking a denial.
|
|
119
|
+
*
|
|
120
|
+
* The read gate of the surfaces a moderator works from: `cell_get` and
|
|
121
|
+
* `cell_moderate`'s view step. It is **not** for `cell_clone`'s source, a
|
|
122
|
+
* contribution's parent, the relation endpoint gates, or the grant paths —
|
|
123
|
+
* those keep `can_view_cell` alone (`auth/cell_steward.ts` says why), so a
|
|
124
|
+
* later sweep must not route them here. The steward check runs only after the
|
|
125
|
+
* view check denies, so an admitted read costs no root load.
|
|
126
|
+
*/
|
|
127
|
+
const admit_viewable_or_stewarded_cell = async (deps, auth, row, grants) => {
|
|
128
|
+
if (can_view_cell(auth, row, grants))
|
|
129
|
+
return;
|
|
130
|
+
if (await cell_steward_admits(deps, auth, row))
|
|
131
|
+
return;
|
|
132
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
133
|
+
};
|
|
113
134
|
/** Create the six generic cell RPC actions. */
|
|
114
135
|
export const create_cell_actions = (deps) => {
|
|
115
136
|
const { validate_data, authorize_create } = deps;
|
|
@@ -161,11 +182,15 @@ export const create_cell_actions = (deps) => {
|
|
|
161
182
|
// not an authorizer is mounted — `root_id` is written regardless, and an
|
|
162
183
|
// unviewable parent 404-masks the attempt (the same IDOR mask the read
|
|
163
184
|
// path uses; never reveals a hidden container). A root creation
|
|
164
|
-
// (`parent_id` null/absent) has `root_id = null`.
|
|
165
|
-
// **in-tx
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
185
|
+
// (`parent_id` null/absent) has `root_id = null`. The root is read
|
|
186
|
+
// **in-tx**, reusing the parent row when the parent *is* the root, else
|
|
187
|
+
// one read by `root_id` — so the authorizer stays pure + the read rides
|
|
188
|
+
// the handler's connection (no separate-handle deadlock on the
|
|
189
|
+
// single-connection in-memory drivers). A missing or soft-deleted root
|
|
190
|
+
// answers `cell_not_found` like a missing parent (soft delete doesn't
|
|
191
|
+
// cascade, so a live parent can sit under a tombstoned root): nothing is
|
|
192
|
+
// written pointing at the tombstone, and an authorizer never sees a root
|
|
193
|
+
// id without its data.
|
|
169
194
|
const parent_id = input.parent_id ?? null;
|
|
170
195
|
let root_id = null;
|
|
171
196
|
let root_data = null;
|
|
@@ -178,11 +203,14 @@ export const create_cell_actions = (deps) => {
|
|
|
178
203
|
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
179
204
|
}
|
|
180
205
|
root_id = parent.root_id ?? parent.id;
|
|
181
|
-
if (
|
|
182
|
-
root_data =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
if (root_id === parent.id) {
|
|
207
|
+
root_data = parent.data;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const root = await query_cell_get(ctx, root_id);
|
|
211
|
+
if (!root)
|
|
212
|
+
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
213
|
+
root_data = root.data;
|
|
186
214
|
}
|
|
187
215
|
}
|
|
188
216
|
// Parent-aware creation authorizer (opt-in). Runs after shape validation
|
|
@@ -272,22 +300,23 @@ export const create_cell_actions = (deps) => {
|
|
|
272
300
|
// `row.id` and have no inter-dependency. Bundle fetches one over the
|
|
273
301
|
// cap so we can detect truncation without a separate count query.
|
|
274
302
|
// Skip the grant fetch for unauthenticated callers — no grant can
|
|
275
|
-
// admit a null auth, so the predicate either
|
|
276
|
-
//
|
|
303
|
+
// admit a null auth, so the predicate either admits via the public
|
|
304
|
+
// arm or returns false either way.
|
|
277
305
|
const [grants, fields, items] = await Promise.all([
|
|
278
306
|
auth ? query_cell_grant_list_for_cell(ctx, row.id) : Promise.resolve(null),
|
|
279
|
-
query_cell_field_list_for_source(ctx, row.id, {
|
|
307
|
+
query_cell_field_list_for_source(ctx, CELL_VIEWER_UNRESTRICTED, row.id, {
|
|
280
308
|
limit: CELL_RELATIONS_BUNDLE_LIMIT + 1
|
|
281
309
|
}),
|
|
282
|
-
query_cell_item_list_for_parent(ctx, row.id, {
|
|
310
|
+
query_cell_item_list_for_parent(ctx, CELL_VIEWER_UNRESTRICTED, row.id, {
|
|
283
311
|
limit: CELL_RELATIONS_BUNDLE_LIMIT + 1
|
|
284
312
|
})
|
|
285
313
|
]);
|
|
286
314
|
// 404 covers both "no such cell" and "exists but caller can't view"
|
|
287
|
-
// — same response code so private-cell existence doesn't leak.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
315
|
+
// — same response code so private-cell existence doesn't leak. The
|
|
316
|
+
// governing root's manager also reads a pending or rejected
|
|
317
|
+
// contribution here (the steward arm); `can_edit` / `can_grant` stay on
|
|
318
|
+
// the base predicates, so a steward reads `false` / `false`.
|
|
319
|
+
await admit_viewable_or_stewarded_cell(ctx, auth, row, grants);
|
|
291
320
|
const can_edit = can_edit_cell(auth, row, grants);
|
|
292
321
|
// `can_grant` gates the share UI — managing grants is a manage-tier
|
|
293
322
|
// affordance (admin / owner), so it tracks `can_manage_cell` rather
|
|
@@ -404,14 +433,15 @@ export const create_cell_actions = (deps) => {
|
|
|
404
433
|
const moderate_handler = async (input, ctx) => {
|
|
405
434
|
const auth = ctx.auth;
|
|
406
435
|
const existing = await query_cell_get(ctx, input.cell_id);
|
|
407
|
-
// 404 covers miss + unviewable so a pending
|
|
408
|
-
//
|
|
436
|
+
// 404 covers miss + unviewable so a pending contribution doesn't leak
|
|
437
|
+
// existence to a non-viewer; the governing root's manager stewards it,
|
|
438
|
+
// which is how a non-admin root owner reaches the root check below. An
|
|
439
|
+
// approved contribution its author narrowed to private is not
|
|
440
|
+
// stewarded, so its root owner gets the 404 there.
|
|
409
441
|
if (!existing)
|
|
410
442
|
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
411
443
|
const grants = await query_cell_grant_list_for_cell(ctx, existing.id);
|
|
412
|
-
|
|
413
|
-
throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
|
|
414
|
-
}
|
|
444
|
+
await admit_viewable_or_stewarded_cell(ctx, auth, existing, grants);
|
|
415
445
|
// Only a contribution (with a governing root) is moderatable.
|
|
416
446
|
if (existing.root_id === null) {
|
|
417
447
|
throw jsonrpc_errors.invalid_params('cell is not a contribution (no governing root to moderate under)', { reason: ERROR_CELL_NOT_A_CONTRIBUTION });
|
|
@@ -526,9 +556,9 @@ export const create_cell_actions = (deps) => {
|
|
|
526
556
|
// don't bloat the transaction. The fetch sees the world as of
|
|
527
557
|
// pre-transaction; the inserts inside the transaction commit
|
|
528
558
|
// atomically together.
|
|
529
|
-
const source_fields = await query_cell_field_list_for_source(ctx, source.id);
|
|
559
|
+
const source_fields = await query_cell_field_list_for_source(ctx, CELL_VIEWER_UNRESTRICTED, source.id);
|
|
530
560
|
const source_items = deep
|
|
531
|
-
? await query_cell_item_list_for_parent(ctx, source.id)
|
|
561
|
+
? await query_cell_item_list_for_parent(ctx, CELL_VIEWER_UNRESTRICTED, source.id)
|
|
532
562
|
: [];
|
|
533
563
|
// In deep mode, pre-resolve which children are viewable so we can
|
|
534
564
|
// skip un-viewable ones silently (strict per-target view filter, D8).
|
|
@@ -614,7 +644,7 @@ export const create_cell_actions = (deps) => {
|
|
|
614
644
|
// `cell_item_insert` enforce. Non-viewable children are skipped
|
|
615
645
|
// silently (no count surfaced — see the deep-walk note on the
|
|
616
646
|
// hidden-child-count leak).
|
|
617
|
-
const shallow_items = await query_cell_item_list_for_parent(ctx, source.id);
|
|
647
|
+
const shallow_items = await query_cell_item_list_for_parent(ctx, CELL_VIEWER_UNRESTRICTED, source.id);
|
|
618
648
|
const shallow_children_visible = await filter_visible_target_ids(ctx, auth, shallow_items.map((i) => i.child_id));
|
|
619
649
|
for (const item of shallow_items) {
|
|
620
650
|
if (!shallow_children_visible.has(item.child_id))
|
|
@@ -663,13 +693,12 @@ export const create_cell_actions = (deps) => {
|
|
|
663
693
|
reason: ERROR_CELL_LIST_SHARED_WITH_REQUIRES_AUTH
|
|
664
694
|
});
|
|
665
695
|
}
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
const
|
|
671
|
-
const
|
|
672
|
-
const caller_actor_id = auth?.actor?.id ?? null;
|
|
696
|
+
// The caller the SQL view predicate binds — the facts `can_view_cell`
|
|
697
|
+
// reads, its active role grants projected into parallel arrays for the
|
|
698
|
+
// `cell_grant` role-shaped EXISTS (NULL `scope_id`s pass through as
|
|
699
|
+
// global-scope role_grants).
|
|
700
|
+
const viewer = cell_viewer_from_auth(auth);
|
|
701
|
+
const caller_actor_id = viewer.actor_id;
|
|
673
702
|
const rows = await query_cell_list(ctx, {
|
|
674
703
|
ids: input.ids,
|
|
675
704
|
kind: input.kind,
|
|
@@ -680,10 +709,10 @@ export const create_cell_actions = (deps) => {
|
|
|
680
709
|
root_id: input.root_id,
|
|
681
710
|
moderation: input.moderation,
|
|
682
711
|
viewer_actor_id: caller_actor_id,
|
|
683
|
-
viewer_is_admin:
|
|
712
|
+
viewer_is_admin: viewer.is_admin,
|
|
684
713
|
caller_actor_id,
|
|
685
|
-
caller_role_grant_roles: role_grant_roles,
|
|
686
|
-
caller_role_grant_scope_ids: role_grant_scope_ids,
|
|
714
|
+
caller_role_grant_roles: viewer.role_grant_roles,
|
|
715
|
+
caller_role_grant_scope_ids: viewer.role_grant_scope_ids,
|
|
687
716
|
shared_with_caller_only: input.shared_with === 'me',
|
|
688
717
|
order_by: input.order_by,
|
|
689
718
|
order_direction: input.order_direction,
|
|
@@ -694,7 +723,7 @@ export const create_cell_actions = (deps) => {
|
|
|
694
723
|
});
|
|
695
724
|
let cell_grants;
|
|
696
725
|
if (input.shared_with === 'me' && rows.length > 0 && caller_actor_id !== null) {
|
|
697
|
-
const grant_rows = await query_cell_grants_for_caller_in_cells(ctx, rows.map((r) => r.id), caller_actor_id, role_grant_roles, role_grant_scope_ids);
|
|
726
|
+
const grant_rows = await query_cell_grants_for_caller_in_cells(ctx, rows.map((r) => r.id), caller_actor_id, viewer.role_grant_roles, viewer.role_grant_scope_ids);
|
|
698
727
|
cell_grants = {};
|
|
699
728
|
for (const g of grant_rows) {
|
|
700
729
|
(cell_grants[g.cell_id] ??= []).push(to_grant_json(g));
|