@fuzdev/fuz_app 0.111.0 → 0.112.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 +15 -11
- package/dist/actions/action_rpc.d.ts +1 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +4 -6
- package/dist/actions/peer_ping.d.ts +8 -0
- package/dist/actions/peer_ping.d.ts.map +1 -1
- package/dist/actions/peer_ping.js +8 -0
- package/dist/actions/peer_request.d.ts +1 -1
- package/dist/actions/peer_request.js +1 -1
- package/dist/actions/perform_action.d.ts +16 -9
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -21
- package/dist/actions/register_action_ws.d.ts +1 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +88 -21
- package/dist/auth/CLAUDE.md +37 -7
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +10 -1
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +36 -16
- package/dist/auth/account_schema.d.ts +20 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -1
- package/dist/auth/actor_search_actions.d.ts +3 -3
- package/dist/auth/actor_search_actions.d.ts.map +1 -1
- package/dist/auth/actor_search_actions.js +10 -6
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -5
- package/dist/auth/app_settings_queries.d.ts.map +1 -1
- package/dist/auth/app_settings_queries.js +6 -4
- package/dist/auth/audit_log_queries.d.ts +6 -0
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +11 -5
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +3 -8
- package/dist/auth/cell_audit_actions.d.ts.map +1 -1
- package/dist/auth/cell_audit_actions.js +1 -1
- package/dist/auth/cell_field_actions.js +1 -1
- package/dist/auth/cell_grant_actions.js +1 -1
- package/dist/auth/cell_item_actions.js +1 -1
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +10 -8
- package/dist/auth/role_grant_offer_ddl.d.ts +11 -0
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +20 -2
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +29 -15
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +21 -7
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +9 -4
- package/dist/db/CLAUDE.md +21 -2
- package/dist/db/cell_audit_queries.js +2 -2
- package/dist/db/cell_field_queries.d.ts +1 -1
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +8 -6
- package/dist/db/cell_grant_queries.d.ts +1 -1
- package/dist/db/cell_grant_queries.d.ts.map +1 -1
- package/dist/db/cell_grant_queries.js +12 -10
- package/dist/db/cell_item_queries.d.ts +1 -1
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +9 -7
- package/dist/db/cell_queries.d.ts +3 -3
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +6 -4
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +9 -3
- package/dist/db/sql_columns.d.ts +80 -3
- package/dist/db/sql_columns.d.ts.map +1 -1
- package/dist/db/sql_columns.js +91 -4
- package/dist/http/CLAUDE.md +3 -1
- package/dist/http/jsonrpc_helpers.d.ts +13 -0
- package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.js +13 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -2
- package/dist/testing/CLAUDE.md +53 -27
- package/dist/testing/admin_integration.d.ts +0 -3
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -1
- package/dist/testing/adversarial_404.d.ts.map +1 -1
- package/dist/testing/adversarial_404.js +4 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +5 -4
- package/dist/testing/app_server.d.ts +4 -3
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -5
- package/dist/testing/attack_surface.d.ts +16 -0
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +36 -13
- package/dist/testing/audit_completeness.d.ts +0 -3
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +0 -1
- package/dist/testing/auth_apps.d.ts +9 -2
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +9 -2
- package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +7 -0
- package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
- package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.js +10 -4
- package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +4 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +1 -1
- package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +10 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
- package/dist/testing/cross_backend/origin.d.ts +4 -2
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
- package/dist/testing/cross_backend/query_shape.d.ts +6 -5
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/ready.d.ts +2 -2
- package/dist/testing/cross_backend/ready.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
- package/dist/testing/cross_backend/setup.d.ts +26 -6
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.d.ts +5 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +1 -5
- package/dist/testing/cross_backend/testing_backdoor.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +6 -0
- package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
- package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
- package/dist/testing/cross_backend/wire_shapes.js +41 -0
- package/dist/testing/data_exposure.d.ts +0 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +0 -1
- package/dist/testing/db.d.ts +44 -7
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +62 -27
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -2
- package/dist/testing/round_trip.d.ts +0 -3
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +0 -4
- package/dist/testing/rpc_attack_surface.js +2 -2
- package/dist/testing/rpc_round_trip.d.ts +0 -3
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +0 -1
- package/dist/testing/schema_generators.d.ts +15 -0
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +45 -11
- package/dist/testing/standard.d.ts +5 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +2 -7
- package/dist/timestamp.d.ts +58 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/dist/timestamp.js +57 -0
- package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +6 -2
- package/package.json +1 -1
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -102,8 +102,13 @@ doesn't carry `id`). Module direction: `role_grant_offer_queries.ts` imports
|
|
|
102
102
|
the revoke cascades in `role_grant_queries.ts` still _write_ offer rows, which
|
|
103
103
|
is exactly why the offer const and the shared CTE tail live in the DDL module.
|
|
104
104
|
Every const is pinned to the live schema by
|
|
105
|
-
`src/test/db/column_projections.db.test.ts`.
|
|
106
|
-
names
|
|
105
|
+
`src/test/db/column_projections.db.test.ts`. The Rust `fuz_auth` twin carries
|
|
106
|
+
the same nine consts (names and order), projected through
|
|
107
|
+
`fuz_db::qualify_columns` / `omit_columns`, decoded with compile-time
|
|
108
|
+
name→index (`fuz_db::col!`), and pinned by its own
|
|
109
|
+
`tests/columns.rs` registry; its narrow purpose rows (`AccountRow` is an
|
|
110
|
+
`id, username` pair) keep private per-shape lists checked as subsets of the
|
|
111
|
+
table const.
|
|
107
112
|
|
|
108
113
|
`_unscoped` suffix on `query_session_revoke_by_hash_unscoped` and
|
|
109
114
|
`query_invite_claim_unscoped` is the safety signal: SQL only checks row state,
|
|
@@ -295,7 +300,7 @@ Account-only — never loads actor or role_grants, never populates
|
|
|
295
300
|
`REQUEST_CONTEXT_KEY`.
|
|
296
301
|
|
|
297
302
|
**Authorization runs before input validation**, matching the dispatcher's
|
|
298
|
-
401 → authz → 403 → 400 phase order (see `http/CLAUDE.md` §Validation
|
|
303
|
+
401 → authz → 403 → 400 phase order — the 429 is dispatcher-only (see `http/CLAUDE.md` §Validation
|
|
299
304
|
pipeline). When the route's input declares `acting?: ActingActor` or its auth
|
|
300
305
|
requires role_grants, the authorization phase calls `resolve_acting_actor` over
|
|
301
306
|
the `acting` selector — the validated query value on GETs, read off the raw
|
|
@@ -320,6 +325,31 @@ path populates `REQUEST_CONTEXT_KEY`; it sets only `ACCOUNT_ID_KEY`,
|
|
|
320
325
|
`TEST_CONTEXT_PRESET_KEY` to bypass DB-backed actor resolution; production
|
|
321
326
|
code that reads `REQUEST_CONTEXT_KEY` is reading test escape-hatch state.
|
|
322
327
|
|
|
328
|
+
### Wire timestamp shape
|
|
329
|
+
|
|
330
|
+
Every timestamp on the wire is **second-precision UTC** —
|
|
331
|
+
`YYYY-MM-DDTHH:MM:SSZ`, exactly 20 characters, the shape both spines emit.
|
|
332
|
+
Timestamps read from a row get it from the SQL projection: each query module
|
|
333
|
+
builds an `iso8601_timestamp_expr` override over its table const and passes
|
|
334
|
+
it to `columns_sql` / `qualify_columns` (see `db/CLAUDE.md`), covering `account`, `actor`,
|
|
335
|
+
`role_grant`, `role_grant_offer`, `auth_session`, `api_token`, `invite`,
|
|
336
|
+
`app_settings`, `audit_log`, and the cell tables. Stamps minted in TS rather
|
|
337
|
+
than read back — the `expires_at` on `account_token_create`'s output — use
|
|
338
|
+
`to_iso8601_seconds` (`timestamp.ts`), so the mint and the later
|
|
339
|
+
`account_token_list` read agree byte for byte.
|
|
340
|
+
|
|
341
|
+
The `*Json` schemas type these as `z.string()`, which a millisecond stamp
|
|
342
|
+
also satisfies — so the shape is pinned behaviorally by
|
|
343
|
+
`assert_iso8601_seconds` (`testing/cross_backend/wire_shapes.ts`) inside the
|
|
344
|
+
shared suites, on both backends.
|
|
345
|
+
|
|
346
|
+
The alias costs one rule: **an `ORDER BY` in a projected SELECT qualifies
|
|
347
|
+
its column** (`ORDER BY auth_session.created_at`) — bare, Postgres resolves
|
|
348
|
+
it to the formatted output column, which ties rows written inside the same
|
|
349
|
+
second and drops the btree index. Qualified, ordering stays exact and
|
|
350
|
+
index-backed, matching the Rust twin (which aliases nothing, so its bare
|
|
351
|
+
`ORDER BY created_at` already means the column).
|
|
352
|
+
|
|
323
353
|
### Open-registry composition
|
|
324
354
|
|
|
325
355
|
Four open string registries — `RoleName`, `ScopeKindName`,
|
|
@@ -511,15 +541,15 @@ authenticate.
|
|
|
511
541
|
- `account_delete_action_spec` — mutation (soft delete); self-or-admin (`{account, actor}`, handler elevates to admin when `account_id` ≠ self); input `{account_id?}`; output `{ok, deleted}`. Tombstones account + actor(s), revokes sessions/tokens, emits `account_delete` + per-actor `actor_delete`. Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`) — both fail-loud with a failure-audit row.
|
|
512
542
|
- `account_purge_action_spec` — mutation (hard purge); keeper-only (`credential_types: ['daemon_token']` + `roles: ['keeper']`) + `confirm: true` + WARN; input `{account_id, confirm?}`; output `{ok, purged}`. Cascading delete; emits `account_purge` + per-actor `actor_purge` (identity snapshot survives in metadata since `audit_log` ids carry no FK). Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`).
|
|
513
543
|
- `account_undelete_action_spec` — mutation (reactivation, inverse of soft delete); admin-only (`roles: ['admin']` — no self path, a tombstoned account can't authenticate); input `{account_id}`; output `{ok, undeleted}`. Clears the `deleted_at` tombstone on the account + its actor(s), emits `account_undelete` + per-actor `actor_undelete`. Does not restore revoked sessions/tokens.
|
|
514
|
-
- `admin_session_list_action_spec` — read; input `
|
|
544
|
+
- `admin_session_list_action_spec` — read; input `{acting?}` (`.default({})`); output `{sessions}`.
|
|
515
545
|
- `admin_session_revoke_all_action_spec` — mutation; input `{account_id}`; output `{ok, count}`.
|
|
516
546
|
- `admin_token_revoke_all_action_spec` — mutation; input `{account_id}`; output `{ok, count}`.
|
|
517
|
-
- `audit_log_list_action_spec` — read; input `{event_type?, account_id?, limit?, offset?, since_seq?}`; output `{events}`.
|
|
547
|
+
- `audit_log_list_action_spec` — read; input `{event_type?, outcome?, account_id?, limit?, offset?, since_seq?}`; output `{events}`.
|
|
518
548
|
- `audit_log_role_grant_history_action_spec` — read; input `{limit?, offset?}`; output `{events}`.
|
|
519
549
|
- `invite_create_action_spec` — mutation; input `{email?, username?}`; output `{ok, invite}`.
|
|
520
|
-
- `invite_list_action_spec` — read; input `
|
|
550
|
+
- `invite_list_action_spec` — read; input `{acting?}` (`.default({})`); output `{invites}`.
|
|
521
551
|
- `invite_delete_action_spec` — mutation; input `{invite_id}`; output `{ok}`.
|
|
522
|
-
- `app_settings_get_action_spec` — read; input `
|
|
552
|
+
- `app_settings_get_action_spec` — read; input `{acting?}` (`.default({})`); output `{settings}`.
|
|
523
553
|
- `app_settings_update_action_spec` — mutation; input `{open_signup}`; output `{ok, settings}`.
|
|
524
554
|
|
|
525
555
|
Constants: `AUDIT_LOG_LIST_LIMIT_MAX = 200`, `ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT = 50`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAqBxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAqBxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAyBnD,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,iBAAiB,EACvB,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CAwKjB,CAAC"}
|
|
@@ -30,6 +30,7 @@ import { parse_stored_token_scope, token_scope_from_input, token_scope_label } f
|
|
|
30
30
|
import { token_lifetime_to_expires_at } from './token_lifetime.js';
|
|
31
31
|
import { generate_api_token } from './api_token.js';
|
|
32
32
|
import { DEFAULT_MAX_TOKENS } from './account_route_schema.js';
|
|
33
|
+
import { to_iso8601_seconds } from '../timestamp.js';
|
|
33
34
|
import { account_verify_action_spec, account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec } from './account_action_specs.js';
|
|
34
35
|
/**
|
|
35
36
|
* Create the self-service account RPC actions.
|
|
@@ -120,7 +121,15 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
120
121
|
credential_type: ctx.credential_type ?? undefined
|
|
121
122
|
}
|
|
122
123
|
});
|
|
123
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
token,
|
|
127
|
+
id,
|
|
128
|
+
name: input.name,
|
|
129
|
+
// the canonical wire shape, matching what `token_list` reads back
|
|
130
|
+
// through `api_token`'s ISO-8601 projection
|
|
131
|
+
expires_at: expires_at === null ? null : to_iso8601_seconds(expires_at)
|
|
132
|
+
};
|
|
124
133
|
};
|
|
125
134
|
const token_list_handler = async (_input, ctx) => {
|
|
126
135
|
const rows = await query_api_token_list_for_account(ctx, ctx.auth.account.id);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { assert_row } from '../db/assert_row.js';
|
|
10
|
-
import { columns_sql, omit_columns } from '../db/sql_columns.js';
|
|
10
|
+
import { columns_sql, iso8601_timestamp_column, iso8601_timestamp_expr, omit_columns } from '../db/sql_columns.js';
|
|
11
11
|
import { to_admin_account } from './account_schema.js';
|
|
12
12
|
import { ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT } from './admin_action_specs.js';
|
|
13
13
|
import { ROLE_GRANT_COLUMNS } from './role_grant_queries.js';
|
|
@@ -54,6 +54,18 @@ export const ACTOR_COLUMNS = [
|
|
|
54
54
|
'deleted_at',
|
|
55
55
|
'deleted_by'
|
|
56
56
|
];
|
|
57
|
+
/**
|
|
58
|
+
* The timestamp override both tables this module owns share (`ACTOR_COLUMNS`
|
|
59
|
+
* carries the same triple); every read here is on the bare table. Actor
|
|
60
|
+
* timestamps never reach the wire (the summaries carry `{id, name}` only —
|
|
61
|
+
* see `actor_lookup_action_specs.ts` on timing-oracle avoidance), but `Actor`
|
|
62
|
+
* declares them as strings, so the projection makes the row honest there too.
|
|
63
|
+
*/
|
|
64
|
+
const account_actor_expr = iso8601_timestamp_expr(ACCOUNT_COLUMNS, [
|
|
65
|
+
'created_at',
|
|
66
|
+
'updated_at',
|
|
67
|
+
'deleted_at'
|
|
68
|
+
])('');
|
|
57
69
|
/**
|
|
58
70
|
* Create a new account.
|
|
59
71
|
*
|
|
@@ -65,7 +77,7 @@ export const ACTOR_COLUMNS = [
|
|
|
65
77
|
export const query_create_account = async (deps, input) => {
|
|
66
78
|
const row = await deps.db.query_one(`INSERT INTO account (username, password_hash, email)
|
|
67
79
|
VALUES ($1, $2, $3)
|
|
68
|
-
RETURNING ${columns_sql(ACCOUNT_COLUMNS)}`, [input.username, input.password_hash, input.email ?? null]);
|
|
80
|
+
RETURNING ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)}`, [input.username, input.password_hash, input.email ?? null]);
|
|
69
81
|
return assert_row(row, 'INSERT INTO account');
|
|
70
82
|
};
|
|
71
83
|
/**
|
|
@@ -78,19 +90,19 @@ export const query_create_account = async (deps, input) => {
|
|
|
78
90
|
* soft-deleted rows too, uses `query_purge_account` directly.
|
|
79
91
|
*/
|
|
80
92
|
export const query_account_by_id = async (deps, id) => {
|
|
81
|
-
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS)} FROM account WHERE id = $1 AND deleted_at IS NULL`, [id]);
|
|
93
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account WHERE id = $1 AND deleted_at IS NULL`, [id]);
|
|
82
94
|
};
|
|
83
95
|
/**
|
|
84
96
|
* Find an account by username (case-insensitive).
|
|
85
97
|
*/
|
|
86
98
|
export const query_account_by_username = async (deps, username) => {
|
|
87
|
-
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS)} FROM account WHERE LOWER(username) = LOWER($1)`, [username]);
|
|
99
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account WHERE LOWER(username) = LOWER($1)`, [username]);
|
|
88
100
|
};
|
|
89
101
|
/**
|
|
90
102
|
* Find an account by email (case-insensitive).
|
|
91
103
|
*/
|
|
92
104
|
export const query_account_by_email = async (deps, email) => {
|
|
93
|
-
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS)} FROM account WHERE LOWER(email) = LOWER($1)`, [email]);
|
|
105
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account WHERE LOWER(email) = LOWER($1)`, [email]);
|
|
94
106
|
};
|
|
95
107
|
/**
|
|
96
108
|
* Find an account by username or email.
|
|
@@ -243,7 +255,7 @@ export const query_account_has_any = async (deps) => {
|
|
|
243
255
|
* @mutates `actor` table - inserts the new row
|
|
244
256
|
*/
|
|
245
257
|
export const query_create_actor = async (deps, account_id, name) => {
|
|
246
|
-
const row = await deps.db.query_one(`INSERT INTO actor (account_id, name) VALUES ($1, $2) RETURNING ${columns_sql(ACTOR_COLUMNS)}`, [account_id, name]);
|
|
258
|
+
const row = await deps.db.query_one(`INSERT INTO actor (account_id, name) VALUES ($1, $2) RETURNING ${columns_sql(ACTOR_COLUMNS, account_actor_expr)}`, [account_id, name]);
|
|
247
259
|
return assert_row(row, 'INSERT INTO actor');
|
|
248
260
|
};
|
|
249
261
|
/**
|
|
@@ -258,7 +270,7 @@ export const query_create_actor = async (deps, account_id, name) => {
|
|
|
258
270
|
* `query_actor_by_id` instead.
|
|
259
271
|
*/
|
|
260
272
|
export const query_actors_by_account = async (deps, account_id) => {
|
|
261
|
-
return deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS)} FROM actor WHERE account_id = $1 ORDER BY created_at ASC, id ASC`, [account_id]);
|
|
273
|
+
return deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE account_id = $1 ORDER BY actor.created_at ASC, actor.id ASC`, [account_id]);
|
|
262
274
|
};
|
|
263
275
|
/**
|
|
264
276
|
* List **active** (non-tombstoned) actors on an account, ordered by
|
|
@@ -272,15 +284,13 @@ export const query_actors_by_account = async (deps, account_id) => {
|
|
|
272
284
|
* tombstoned rows stay on the unfiltered `query_actors_by_account`.
|
|
273
285
|
*/
|
|
274
286
|
export const query_active_actors_by_account = async (deps, account_id) => {
|
|
275
|
-
return deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS)} FROM actor WHERE account_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC, id ASC`, [account_id]);
|
|
287
|
+
return deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE account_id = $1 AND deleted_at IS NULL ORDER BY actor.created_at ASC, actor.id ASC`, [account_id]);
|
|
276
288
|
};
|
|
277
289
|
/**
|
|
278
290
|
* Find an actor by id.
|
|
279
291
|
*/
|
|
280
292
|
export const query_actor_by_id = async (deps, id) => {
|
|
281
|
-
return deps.db.query_one(`SELECT ${columns_sql(ACTOR_COLUMNS)} FROM actor WHERE id = $1`, [
|
|
282
|
-
id
|
|
283
|
-
]);
|
|
293
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE id = $1`, [id]);
|
|
284
294
|
};
|
|
285
295
|
/**
|
|
286
296
|
* Create an account and its actor in a single operation.
|
|
@@ -302,6 +312,14 @@ export const query_create_account_with_actor = async (deps, input) => {
|
|
|
302
312
|
* active-grant summary the admin listing projects (`RoleGrantWithActorId`).
|
|
303
313
|
*/
|
|
304
314
|
const ADMIN_ROLE_GRANT_COLUMNS = omit_columns(ROLE_GRANT_COLUMNS, 'revoked_at', 'revoked_by', 'revoked_reason', 'source_offer_id');
|
|
315
|
+
/**
|
|
316
|
+
* The admin listing's active-grant summary timestamps — twin of the spine's
|
|
317
|
+
* `role_grant_summary_columns`. The read is on the bare `role_grant` table.
|
|
318
|
+
*/
|
|
319
|
+
const admin_role_grant_expr = iso8601_timestamp_expr(ADMIN_ROLE_GRANT_COLUMNS, [
|
|
320
|
+
'created_at',
|
|
321
|
+
'expires_at'
|
|
322
|
+
])('');
|
|
305
323
|
/**
|
|
306
324
|
* List accounts with their actors, active role_grants, and pending inbound
|
|
307
325
|
* role_grant offers for admin display.
|
|
@@ -327,21 +345,23 @@ export const query_admin_account_list = async (deps, options) => {
|
|
|
327
345
|
// `include_deleted` to surface tombstoned rows for reactivation.
|
|
328
346
|
const where = options?.include_deleted ? '' : 'WHERE deleted_at IS NULL';
|
|
329
347
|
const account_query = limit == null
|
|
330
|
-
? deps.db.query(`SELECT ${columns_sql(ACCOUNT_COLUMNS)} FROM account ${where} ORDER BY created_at OFFSET $1`, [offset])
|
|
331
|
-
: deps.db.query(`SELECT ${columns_sql(ACCOUNT_COLUMNS)} FROM account ${where} ORDER BY created_at LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
348
|
+
? deps.db.query(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account ${where} ORDER BY account.created_at OFFSET $1`, [offset])
|
|
349
|
+
: deps.db.query(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account ${where} ORDER BY account.created_at LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
332
350
|
const accounts = await account_query;
|
|
333
351
|
if (accounts.length === 0)
|
|
334
352
|
return [];
|
|
335
353
|
const account_ids = accounts.map((a) => a.id);
|
|
336
354
|
const [actors, role_grants, pending_offers] = await Promise.all([
|
|
337
|
-
deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS)} FROM actor WHERE account_id = ANY($1::uuid[])`, [account_ids]),
|
|
338
|
-
deps.db.query(`SELECT ${columns_sql(ADMIN_ROLE_GRANT_COLUMNS)}
|
|
355
|
+
deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE account_id = ANY($1::uuid[])`, [account_ids]),
|
|
356
|
+
deps.db.query(`SELECT ${columns_sql(ADMIN_ROLE_GRANT_COLUMNS, admin_role_grant_expr)}
|
|
339
357
|
FROM role_grant
|
|
340
358
|
WHERE actor_id IN (SELECT id FROM actor WHERE account_id = ANY($1::uuid[]))
|
|
341
359
|
AND revoked_at IS NULL
|
|
342
360
|
AND (expires_at IS NULL OR expires_at > NOW())`, [account_ids]),
|
|
343
361
|
deps.db.query(`SELECT po.id, po.to_account_id, po.from_actor_id, po.role, po.scope_kind, po.scope_id,
|
|
344
|
-
po
|
|
362
|
+
${iso8601_timestamp_column('po', 'created_at')} AS created_at,
|
|
363
|
+
${iso8601_timestamp_column('po', 'expires_at')} AS expires_at,
|
|
364
|
+
a.username AS from_username
|
|
345
365
|
FROM role_grant_offer po
|
|
346
366
|
JOIN actor act ON act.id = po.from_actor_id
|
|
347
367
|
JOIN account a ON a.id = act.account_id
|
|
@@ -97,6 +97,26 @@ export interface RoleGrant {
|
|
|
97
97
|
/** Offer that produced this role_grant (set by `query_accept_offer`). `null` for direct grants. */
|
|
98
98
|
source_offer_id: Uuid | null;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether a role grant is live, from its wire fields — not revoked, and not
|
|
102
|
+
* past `expires_at`.
|
|
103
|
+
*
|
|
104
|
+
* Server-side this is belt-and-suspenders: every `role_grant` a request
|
|
105
|
+
* context holds came through `query_role_grant_find_active_for_actor`, whose
|
|
106
|
+
* `revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())` is the
|
|
107
|
+
* authoritative gate on both spines (the Rust twin's in-memory
|
|
108
|
+
* `RoleGrantRow` carries no timestamps at all and re-checks nothing).
|
|
109
|
+
*
|
|
110
|
+
* `expires_at` arrives second-truncated, so the expiry check is
|
|
111
|
+
* `is_iso8601_seconds_live` (`timestamp.ts`): it admits through the end of
|
|
112
|
+
* the second the stamp names, so the predicate can never report a grant
|
|
113
|
+
* expired *before* the SQL gate would — a bare `expires_at > now` would deny
|
|
114
|
+
* an admitted grant for up to a second.
|
|
115
|
+
*
|
|
116
|
+
* @param p - the grant's revocation + expiry fields
|
|
117
|
+
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
118
|
+
* @returns `true` when the grant is neither revoked nor expired
|
|
119
|
+
*/
|
|
100
120
|
export declare const is_role_grant_active: (p: {
|
|
101
121
|
revoked_at?: string | null;
|
|
102
122
|
expires_at: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAG9C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"account_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ import { z } from 'zod';
|
|
|
20
20
|
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
21
21
|
import { Blake3Hash } from '@fuzdev/fuz_util/hash_schemas.ts';
|
|
22
22
|
import { Username, Email } from '../primitive_schemas.js';
|
|
23
|
+
import { is_iso8601_seconds_live } from '../timestamp.js';
|
|
23
24
|
/**
|
|
24
25
|
* Maximum length of the optional free-form `revoked_reason` attached to a
|
|
25
26
|
* revoked role_grant. Bounds the value at the schema layer so both the admin
|
|
@@ -27,7 +28,27 @@ import { Username, Email } from '../primitive_schemas.js';
|
|
|
27
28
|
* `role_grant_revoke` WS notification validate against the same ceiling.
|
|
28
29
|
*/
|
|
29
30
|
export const ROLE_GRANT_REVOKED_REASON_LENGTH_MAX = 500;
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Whether a role grant is live, from its wire fields — not revoked, and not
|
|
33
|
+
* past `expires_at`.
|
|
34
|
+
*
|
|
35
|
+
* Server-side this is belt-and-suspenders: every `role_grant` a request
|
|
36
|
+
* context holds came through `query_role_grant_find_active_for_actor`, whose
|
|
37
|
+
* `revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())` is the
|
|
38
|
+
* authoritative gate on both spines (the Rust twin's in-memory
|
|
39
|
+
* `RoleGrantRow` carries no timestamps at all and re-checks nothing).
|
|
40
|
+
*
|
|
41
|
+
* `expires_at` arrives second-truncated, so the expiry check is
|
|
42
|
+
* `is_iso8601_seconds_live` (`timestamp.ts`): it admits through the end of
|
|
43
|
+
* the second the stamp names, so the predicate can never report a grant
|
|
44
|
+
* expired *before* the SQL gate would — a bare `expires_at > now` would deny
|
|
45
|
+
* an admitted grant for up to a second.
|
|
46
|
+
*
|
|
47
|
+
* @param p - the grant's revocation + expiry fields
|
|
48
|
+
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
49
|
+
* @returns `true` when the grant is neither revoked nor expired
|
|
50
|
+
*/
|
|
51
|
+
export const is_role_grant_active = (p, now = new Date()) => !p.revoked_at && (!p.expires_at || is_iso8601_seconds_live(p.expires_at, now.getTime()));
|
|
31
52
|
/**
|
|
32
53
|
* A session's storage key — the blake3 hash of its raw token, branded so a
|
|
33
54
|
* bare `string` can't stand in for one. `Blake3Hash` (`hash_schemas.ts`) is
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - **Admin gate on empty `scope_ids`** — unbounded global search is
|
|
12
12
|
* admin-only. Non-admin callers without a `scope_ids` filter are
|
|
13
13
|
* rejected with `invalid_params` carrying `actor_search_scope_required`.
|
|
14
|
-
* The admin check is account-grain (any actor on the caller's
|
|
15
|
-
* holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
-
* doesn't load `auth.role_grants` for an in-memory check.
|
|
14
|
+
* The admin check is account-grain (any **active** actor on the caller's
|
|
15
|
+
* account holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
+
* posture doesn't load `auth.role_grants` for an in-memory check.
|
|
17
17
|
* - **Limit clamp** — input is bounded by `ACTOR_SEARCH_LIMIT_MAX` at
|
|
18
18
|
* the schema; the handler picks the default when omitted.
|
|
19
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor_search_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/actor_search_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAc9F,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,eAAO,MAAM,2BAA2B,GAAI,OAAO,qBAAqB,KAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"actor_search_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/actor_search_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAc9F,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,eAAO,MAAM,2BAA2B,GAAI,OAAO,qBAAqB,KAAG,KAAK,CAAC,SAAS,CAyCzF,CAAC"}
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - **Admin gate on empty `scope_ids`** — unbounded global search is
|
|
12
12
|
* admin-only. Non-admin callers without a `scope_ids` filter are
|
|
13
13
|
* rejected with `invalid_params` carrying `actor_search_scope_required`.
|
|
14
|
-
* The admin check is account-grain (any actor on the caller's
|
|
15
|
-
* holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
-
* doesn't load `auth.role_grants` for an in-memory check.
|
|
14
|
+
* The admin check is account-grain (any **active** actor on the caller's
|
|
15
|
+
* account holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
+
* posture doesn't load `auth.role_grants` for an in-memory check.
|
|
17
17
|
* - **Limit clamp** — input is bounded by `ACTOR_SEARCH_LIMIT_MAX` at
|
|
18
18
|
* the schema; the handler picks the default when omitted.
|
|
19
19
|
*
|
|
@@ -26,15 +26,19 @@
|
|
|
26
26
|
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
27
27
|
import { rpc_action } from '../actions/action_rpc.js';
|
|
28
28
|
import { query_actor_search } from './actor_search_queries.js';
|
|
29
|
-
import {
|
|
29
|
+
import { query_account_has_active_global_role } from './role_grant_queries.js';
|
|
30
30
|
import { ROLE_ADMIN } from './role_schema.js';
|
|
31
31
|
import { ACTOR_SEARCH_LIMIT_DEFAULT, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, actor_search_action_spec } from './actor_search_action_specs.js';
|
|
32
32
|
export const create_actor_search_actions = (_deps) => {
|
|
33
33
|
const handler = async (input, ctx) => {
|
|
34
34
|
if (!input.scope_ids || input.scope_ids.length === 0) {
|
|
35
35
|
// Unbounded global search is admin-only. Account-grain admin
|
|
36
|
-
// check — any actor on the caller's account holds the
|
|
37
|
-
|
|
36
|
+
// check — any *active* actor on the caller's account holds the
|
|
37
|
+
// role. The tombstone-aware query is what every admission gate
|
|
38
|
+
// reads: soft-delete leaves `role_grant` rows intact (undelete
|
|
39
|
+
// restores them), so the blind sibling would admit an account
|
|
40
|
+
// whose only admin grant hangs off a tombstoned actor.
|
|
41
|
+
const is_admin = await query_account_has_active_global_role(ctx, ctx.auth.account.id, ROLE_ADMIN);
|
|
38
42
|
if (!is_admin) {
|
|
39
43
|
throw jsonrpc_errors.invalid_params('scope_ids required for non-admin callers', {
|
|
40
44
|
reason: ERROR_ACTOR_SEARCH_SCOPE_REQUIRED
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api_token_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/api_token_queries.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,KAAK,UAAU,EAAmD,MAAM,kBAAkB,CAAC;AAEpG,yEAAyE;AACzE,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IACnD,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,0HAUoB,CAAC;
|
|
1
|
+
{"version":3,"file":"api_token_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/api_token_queries.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,KAAK,UAAU,EAAmD,MAAM,kBAAkB,CAAC;AAEpG,yEAAyE;AACzE,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IACnD,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,0HAUoB,CAAC;AAgBnD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,EAClB,MAAM,MAAM,EACZ,YAAY,MAAM,EAClB,OAAO,UAAU,EACjB,aAAa,IAAI,GAAG,IAAI,KACtB,OAAO,CAAC,QAAQ,CAelB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,iBAAiB,EACvB,WAAW,MAAM,EACjB,IAAI,MAAM,GAAG,SAAS,EACtB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,KAC/C,OAAO,CAAC,QAAQ,GAAG,SAAS,CAgC9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,IAAI,MAAM,EACV,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAYhB,CAAC"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { assert_row } from '../db/assert_row.js';
|
|
7
|
-
import { columns_sql, omit_columns } from '../db/sql_columns.js';
|
|
7
|
+
import { columns_sql, iso8601_timestamp_expr, omit_columns } from '../db/sql_columns.js';
|
|
8
8
|
import { hash_api_token } from './api_token.js';
|
|
9
9
|
import { parse_stored_token_scope, serialize_token_scope } from './token_scope.js';
|
|
10
10
|
/**
|
|
@@ -30,6 +30,12 @@ export const API_TOKEN_COLUMNS = [
|
|
|
30
30
|
* while the hash stays off it by construction.
|
|
31
31
|
*/
|
|
32
32
|
const API_TOKEN_CLIENT_COLUMNS = omit_columns(API_TOKEN_COLUMNS, 'token_hash');
|
|
33
|
+
/** The `api_token` timestamp override; every read here is on the bare table. */
|
|
34
|
+
const api_token_expr = iso8601_timestamp_expr(API_TOKEN_COLUMNS, [
|
|
35
|
+
'expires_at',
|
|
36
|
+
'last_used_at',
|
|
37
|
+
'created_at'
|
|
38
|
+
])('');
|
|
33
39
|
/**
|
|
34
40
|
* Store a new API token (the hash, not the raw token).
|
|
35
41
|
*
|
|
@@ -45,7 +51,7 @@ const API_TOKEN_CLIENT_COLUMNS = omit_columns(API_TOKEN_COLUMNS, 'token_hash');
|
|
|
45
51
|
export const query_create_api_token = async (deps, id, account_id, name, token_hash, scope, expires_at) => {
|
|
46
52
|
const row = await deps.db.query_one(`INSERT INTO api_token (id, account_id, name, token_hash, scope, expires_at)
|
|
47
53
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6)
|
|
48
|
-
RETURNING ${columns_sql(API_TOKEN_COLUMNS)}`, [
|
|
54
|
+
RETURNING ${columns_sql(API_TOKEN_COLUMNS, api_token_expr)}`, [
|
|
49
55
|
id,
|
|
50
56
|
account_id,
|
|
51
57
|
name,
|
|
@@ -72,7 +78,7 @@ export const query_create_api_token = async (deps, id, account_id, name, token_h
|
|
|
72
78
|
*/
|
|
73
79
|
export const query_validate_api_token = async (deps, raw_token, ip, pending_effects) => {
|
|
74
80
|
const token_hash = hash_api_token(raw_token);
|
|
75
|
-
const row = await deps.db.query_one(`SELECT ${columns_sql(API_TOKEN_COLUMNS)} FROM api_token
|
|
81
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(API_TOKEN_COLUMNS, api_token_expr)} FROM api_token
|
|
76
82
|
WHERE token_hash = $1
|
|
77
83
|
AND (expires_at IS NULL OR expires_at > NOW())`, [token_hash]);
|
|
78
84
|
if (!row)
|
|
@@ -130,8 +136,8 @@ export const query_revoke_api_token_for_account = async (deps, id, account_id) =
|
|
|
130
136
|
* so `token_hash` never rides the listing.
|
|
131
137
|
*/
|
|
132
138
|
export const query_api_token_list_for_account = async (deps, account_id) => {
|
|
133
|
-
return deps.db.query(`SELECT ${columns_sql(API_TOKEN_CLIENT_COLUMNS)}
|
|
134
|
-
FROM api_token WHERE account_id = $1 ORDER BY created_at DESC`, [account_id]);
|
|
139
|
+
return deps.db.query(`SELECT ${columns_sql(API_TOKEN_CLIENT_COLUMNS, api_token_expr)}
|
|
140
|
+
FROM api_token WHERE account_id = $1 ORDER BY api_token.created_at DESC`, [account_id]);
|
|
135
141
|
};
|
|
136
142
|
/**
|
|
137
143
|
* Enforce a per-account token limit by evicting the oldest tokens.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_settings_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/app_settings_queries.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"app_settings_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/app_settings_queries.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAOrD,OAAO,KAAK,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAEzF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,4DAA6D,CAAC;AAQ/F;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,WAAW,CAQlF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,KACb,OAAO,CAAC,2BAA2B,CAWrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,aAAa,OAAO,EACpB,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,CASrB,CAAC"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module
|
|
7
7
|
*/
|
|
8
|
-
import { columns_sql, omit_columns, qualify_columns } from '../db/sql_columns.js';
|
|
8
|
+
import { columns_sql, iso8601_timestamp_expr, omit_columns, qualify_columns } from '../db/sql_columns.js';
|
|
9
9
|
/**
|
|
10
10
|
* The full `app_settings` column set — the singleton `id` (always `1`) plus
|
|
11
11
|
* the settings the row type carries — drift-guarded like every other
|
|
@@ -16,6 +16,8 @@ import { columns_sql, omit_columns, qualify_columns } from '../db/sql_columns.js
|
|
|
16
16
|
export const APP_SETTINGS_COLUMNS = ['id', 'open_signup', 'updated_at', 'updated_by'];
|
|
17
17
|
/** `APP_SETTINGS_COLUMNS` minus the constant singleton `id` — the `AppSettings` row projection. */
|
|
18
18
|
const APP_SETTINGS_ROW_COLUMNS = omit_columns(APP_SETTINGS_COLUMNS, 'id');
|
|
19
|
+
/** The `app_settings` timestamp override, by row qualifier (`''` bare, `s` for the username JOIN). */
|
|
20
|
+
const app_settings_expr = iso8601_timestamp_expr(APP_SETTINGS_ROW_COLUMNS, ['updated_at']);
|
|
19
21
|
/**
|
|
20
22
|
* Load the current app settings.
|
|
21
23
|
*
|
|
@@ -24,7 +26,7 @@ const APP_SETTINGS_ROW_COLUMNS = omit_columns(APP_SETTINGS_COLUMNS, 'id');
|
|
|
24
26
|
* @throws Error if the singleton `app_settings` row is missing (migration drift — should not occur in practice)
|
|
25
27
|
*/
|
|
26
28
|
export const query_app_settings_load = async (deps) => {
|
|
27
|
-
const row = await deps.db.query_one(`SELECT ${columns_sql(APP_SETTINGS_ROW_COLUMNS)} FROM app_settings WHERE id = 1`);
|
|
29
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(APP_SETTINGS_ROW_COLUMNS, app_settings_expr(''))} FROM app_settings WHERE id = 1`);
|
|
28
30
|
if (!row) {
|
|
29
31
|
throw new Error('app_settings row not found — migration may not have run');
|
|
30
32
|
}
|
|
@@ -38,7 +40,7 @@ export const query_app_settings_load = async (deps) => {
|
|
|
38
40
|
* @throws Error if the singleton `app_settings` row is missing
|
|
39
41
|
*/
|
|
40
42
|
export const query_app_settings_load_with_username = async (deps) => {
|
|
41
|
-
const row = await deps.db.query_one(`SELECT ${qualify_columns(APP_SETTINGS_ROW_COLUMNS, 's')}, act.name AS updated_by_username
|
|
43
|
+
const row = await deps.db.query_one(`SELECT ${qualify_columns(APP_SETTINGS_ROW_COLUMNS, 's', app_settings_expr('s'))}, act.name AS updated_by_username
|
|
42
44
|
FROM app_settings s
|
|
43
45
|
LEFT JOIN actor act ON act.id = s.updated_by
|
|
44
46
|
WHERE s.id = 1`);
|
|
@@ -58,7 +60,7 @@ export const query_app_settings_load_with_username = async (deps) => {
|
|
|
58
60
|
* @throws Error if the singleton `app_settings` row is missing
|
|
59
61
|
*/
|
|
60
62
|
export const query_app_settings_update = async (deps, open_signup, actor_id) => {
|
|
61
|
-
const row = await deps.db.query_one(`UPDATE app_settings SET open_signup = $1, updated_at = NOW(), updated_by = $2 WHERE id = 1 RETURNING ${columns_sql(APP_SETTINGS_ROW_COLUMNS)}`, [open_signup, actor_id]);
|
|
63
|
+
const row = await deps.db.query_one(`UPDATE app_settings SET open_signup = $1, updated_at = NOW(), updated_by = $2 WHERE id = 1 RETURNING ${columns_sql(APP_SETTINGS_ROW_COLUMNS, app_settings_expr(''))}`, [open_signup, actor_id]);
|
|
62
64
|
if (!row) {
|
|
63
65
|
throw new Error('app_settings row not found — migration may not have run');
|
|
64
66
|
}
|
|
@@ -24,6 +24,12 @@ import { type AuditLogConfig, type AuditLogEvent, type AuditLogInput, type Audit
|
|
|
24
24
|
* per-cell timeline read in `db/cell_audit_queries.ts`.
|
|
25
25
|
*/
|
|
26
26
|
export declare const AUDIT_LOG_COLUMNS: readonly ["id", "seq", "event_type", "outcome", "actor_id", "account_id", "target_account_id", "target_actor_id", "ip", "created_at", "metadata"];
|
|
27
|
+
/**
|
|
28
|
+
* The `audit_log` timestamp override, by row qualifier (`''` bare, `al` for
|
|
29
|
+
* the username JOINs). Exported for the per-cell timeline read in
|
|
30
|
+
* `db/cell_audit_queries.ts`, which projects the same table.
|
|
31
|
+
*/
|
|
32
|
+
export declare const audit_log_expr: (alias: string) => import("../db/sql_columns.js").ColumnExpr;
|
|
27
33
|
/** Number of audit metadata validation failures observed since process start. */
|
|
28
34
|
export declare const get_audit_metadata_validation_failures: () => number;
|
|
29
35
|
/** Reset the counter — for tests only. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAGN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,yBAAyB,EAC9B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,mJAYyB,CAAC;
|
|
1
|
+
{"version":3,"file":"audit_log_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAGN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,EACnC,KAAK,yBAAyB,EAC9B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,mJAYyB,CAAC;AAExD;;;;GAIG;AACH,eAAO,MAAM,cAAc,8DAA4D,CAAC;AAaxF,iFAAiF;AACjF,eAAO,MAAM,sCAAsC,QAAO,MACvB,CAAC;AAEpC,0CAA0C;AAC1C,eAAO,MAAM,wCAAwC,QAAO,IAE3D,CAAC;AAYF,gFAAgF;AAChF,eAAO,MAAM,qCAAqC,QAAO,MACvB,CAAC;AAEnC,0CAA0C;AAC1C,eAAO,MAAM,uCAAuC,QAAO,IAE1D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,eAAe,GAAU,CAAC,SAAS,MAAM,EACrD,MAAM,SAAS,EACf,OAAO,aAAa,CAAC,CAAC,CAAC,EACvB,SAAQ,cAAyC,KAC/C,OAAO,CAAC,aAAa,CAsCvB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,UAAU,mBAAmB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CA0C9B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,GAC/C,MAAM,SAAS,EACf,UAAU,mBAAmB,KAC3B,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAwD/C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,cAA+B,EAC/B,eAAU,KACR,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAmB1C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,QAAQ,IAAI,KACV,OAAO,CAAC,MAAM,CAMhB,CAAC"}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import { assert_row } from '../db/assert_row.js';
|
|
15
|
-
import { columns_sql, qualify_columns } from '../db/sql_columns.js';
|
|
15
|
+
import { columns_sql, iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
|
|
16
16
|
import { AUDIT_LOG_DEFAULT_LIMIT, builtin_audit_log_config } from './audit_log_schema.js';
|
|
17
17
|
/**
|
|
18
18
|
* The full `audit_log` column set, named explicitly so a row read fails loud
|
|
@@ -37,6 +37,12 @@ export const AUDIT_LOG_COLUMNS = [
|
|
|
37
37
|
'created_at',
|
|
38
38
|
'metadata'
|
|
39
39
|
];
|
|
40
|
+
/**
|
|
41
|
+
* The `audit_log` timestamp override, by row qualifier (`''` bare, `al` for
|
|
42
|
+
* the username JOINs). Exported for the per-cell timeline read in
|
|
43
|
+
* `db/cell_audit_queries.ts`, which projects the same table.
|
|
44
|
+
*/
|
|
45
|
+
export const audit_log_expr = iso8601_timestamp_expr(AUDIT_LOG_COLUMNS, ['created_at']);
|
|
40
46
|
/**
|
|
41
47
|
* Process-wide counter for audit metadata validation failures. `query_audit_log`
|
|
42
48
|
* increments on `safeParse` mismatch and writes the row anyway (fail-open —
|
|
@@ -108,7 +114,7 @@ export const query_audit_log = async (deps, input, config = builtin_audit_log_co
|
|
|
108
114
|
}
|
|
109
115
|
const rows = await deps.db.query(`INSERT INTO audit_log (event_type, outcome, actor_id, account_id, target_account_id, target_actor_id, ip, metadata)
|
|
110
116
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
111
|
-
RETURNING ${columns_sql(AUDIT_LOG_COLUMNS)}`, [
|
|
117
|
+
RETURNING ${columns_sql(AUDIT_LOG_COLUMNS, audit_log_expr(''))}`, [
|
|
112
118
|
input.event_type,
|
|
113
119
|
input.outcome ?? 'success',
|
|
114
120
|
input.actor_id ?? null,
|
|
@@ -156,7 +162,7 @@ export const query_audit_log_list = async (deps, options) => {
|
|
|
156
162
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
157
163
|
const limit = options?.limit ?? AUDIT_LOG_DEFAULT_LIMIT;
|
|
158
164
|
const offset = options?.offset ?? 0;
|
|
159
|
-
return deps.db.query(`SELECT ${columns_sql(AUDIT_LOG_COLUMNS)} FROM audit_log ${where} ORDER BY seq DESC LIMIT $${param_index++} OFFSET $${param_index}`, [...params, limit, offset]);
|
|
165
|
+
return deps.db.query(`SELECT ${columns_sql(AUDIT_LOG_COLUMNS, audit_log_expr(''))} FROM audit_log ${where} ORDER BY seq DESC LIMIT $${param_index++} OFFSET $${param_index}`, [...params, limit, offset]);
|
|
160
166
|
};
|
|
161
167
|
/**
|
|
162
168
|
* List audit log entries with resolved usernames, newest first.
|
|
@@ -200,7 +206,7 @@ export const query_audit_log_list_with_usernames = async (deps, options) => {
|
|
|
200
206
|
// has no actor binding (admin password reset, session revoke, etc.).
|
|
201
207
|
// Under v1 1:1 the two branches resolve to the same username; the
|
|
202
208
|
// chain is forensic future-proofing for N:1 multi-actor.
|
|
203
|
-
return deps.db.query(`SELECT ${qualify_columns(AUDIT_LOG_COLUMNS, 'al')},
|
|
209
|
+
return deps.db.query(`SELECT ${qualify_columns(AUDIT_LOG_COLUMNS, 'al', audit_log_expr('al'))},
|
|
204
210
|
COALESCE(origin_act_acc.username, origin_acc.username) AS username,
|
|
205
211
|
COALESCE(target_act_acc.username, target_acc.username) AS target_username
|
|
206
212
|
FROM audit_log al
|
|
@@ -224,7 +230,7 @@ export const query_audit_log_list_role_grant_history = async (deps, limit = AUDI
|
|
|
224
230
|
// Same actor-chained JOIN as `query_audit_log_list_with_usernames` —
|
|
225
231
|
// see the comment there for rationale (forensic future-proofing for
|
|
226
232
|
// N:1 multi-actor; v1 1:1 picks the same username via either branch).
|
|
227
|
-
return deps.db.query(`SELECT ${qualify_columns(AUDIT_LOG_COLUMNS, 'al')},
|
|
233
|
+
return deps.db.query(`SELECT ${qualify_columns(AUDIT_LOG_COLUMNS, 'al', audit_log_expr('al'))},
|
|
228
234
|
COALESCE(origin_act_acc.username, origin_acc.username) AS username,
|
|
229
235
|
COALESCE(target_act_acc.username, target_acc.username) AS target_username
|
|
230
236
|
FROM audit_log al
|
|
@@ -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,EAAY,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAoCN,KAAK,QAAQ,EAEb,MAAM,wBAAwB,CAAC;AAChC,OAAO,EASN,KAAK,OAAO,EACZ,MAAM,uBAAuB,CAAC;AAyB/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,6XAA6X;IAC7X,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;
|
|
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,EAAY,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAoCN,KAAK,QAAQ,EAEb,MAAM,wBAAwB,CAAC;AAChC,OAAO,EASN,KAAK,OAAO,EACZ,MAAM,uBAAuB,CAAC;AAyB/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,6XAA6X;IAC7X,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;AA2BH,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB,GAAI,MAAM,cAAc,KAAG,KAAK,CAAC,SAAS,CAinBzE,CAAC"}
|