@fuzdev/fuz_app 0.108.0 → 0.110.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/README.md +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +7 -6
- package/dist/auth/account_action_specs.d.ts +18 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +14 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +6 -2
- package/dist/auth/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- package/dist/auth/bootstrap_account.d.ts +7 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +9 -4
- package/dist/auth/bootstrap_routes.d.ts +12 -4
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +26 -7
- package/dist/auth/daemon_token_middleware.d.ts +7 -69
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +8 -104
- package/dist/auth/deps.d.ts +9 -5
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.js +1 -1
- package/dist/auth/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +25 -8
- package/dist/auth/request_context.d.ts +1 -3
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +2 -5
- package/dist/auth/session_cookie.d.ts +18 -36
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +16 -26
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +1 -4
- package/dist/auth/session_queries.d.ts +14 -30
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +15 -45
- package/dist/auth/token_lifetime.d.ts +51 -0
- package/dist/auth/token_lifetime.d.ts.map +1 -0
- package/dist/auth/token_lifetime.js +54 -0
- package/dist/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- package/dist/dev/setup.d.ts +5 -9
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +18 -21
- package/dist/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +12 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -1
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +27 -1
- package/dist/runtime/deps.d.ts +38 -4
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/fs.d.ts +24 -5
- package/dist/runtime/fs.d.ts.map +1 -1
- package/dist/runtime/fs.js +38 -7
- package/dist/runtime/mock.d.ts +7 -0
- package/dist/runtime/mock.d.ts.map +1 -1
- package/dist/runtime/mock.js +41 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -1
- package/dist/runtime/secure_file.d.ts +71 -0
- package/dist/runtime/secure_file.d.ts.map +1 -0
- package/dist/runtime/secure_file.js +125 -0
- package/dist/server/app_backend.d.ts +6 -5
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +2 -3
- package/dist/testing/CLAUDE.md +36 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +10 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +13 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +10 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +34 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -1
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +2 -1
- package/dist/testing/cross_backend/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
- package/dist/testing/cross_backend/token_lifetime.js +99 -0
- package/dist/testing/cross_backend/token_scope_surface.js +1 -1
- package/dist/testing/daemon_token_rotation.d.ts +83 -0
- package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
- package/dist/testing/daemon_token_rotation.js +99 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -4
- package/dist/ui/AccountSessions.svelte +0 -5
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +7 -4
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +0 -5
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ export declare const clear_session_cookie: <T>(c: Context, options: SessionOptio
|
|
|
35
35
|
*
|
|
36
36
|
* @param keyring - key ring for cookie verification
|
|
37
37
|
* @param options - session configuration
|
|
38
|
-
* @mutates Hono context - sets `options.context_key` and may
|
|
38
|
+
* @mutates Hono context - sets `options.context_key` and may clear the session cookie
|
|
39
39
|
*/
|
|
40
40
|
export declare const create_session_middleware: <TIdentity>(keyring: Keyring, options: SessionOptions<TIdentity>) => MiddlewareHandler;
|
|
41
41
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,
|
|
1
|
+
{"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAcF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,IAAI,CAad,CAAC"}
|
|
@@ -49,7 +49,7 @@ export const clear_session_cookie = (c, options) => {
|
|
|
49
49
|
*
|
|
50
50
|
* @param keyring - key ring for cookie verification
|
|
51
51
|
* @param options - session configuration
|
|
52
|
-
* @mutates Hono context - sets `options.context_key` and may
|
|
52
|
+
* @mutates Hono context - sets `options.context_key` and may clear the session cookie
|
|
53
53
|
*/
|
|
54
54
|
export const create_session_middleware = (keyring, options) => {
|
|
55
55
|
return async (c, next) => {
|
|
@@ -60,9 +60,6 @@ export const create_session_middleware = (keyring, options) => {
|
|
|
60
60
|
if (result.action === 'clear') {
|
|
61
61
|
clear_session_cookie(c, options);
|
|
62
62
|
}
|
|
63
|
-
else if (result.action === 'refresh' && result.new_signed_value) {
|
|
64
|
-
set_session_cookie(c, result.new_signed_value, options);
|
|
65
|
-
}
|
|
66
63
|
await next();
|
|
67
64
|
};
|
|
68
65
|
};
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
10
9
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
11
10
|
import type { AuthSession, SessionId } from './account_schema.js';
|
|
12
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Session lifetime in milliseconds (30 days).
|
|
13
|
+
*
|
|
14
|
+
* An **absolute** cap: `expires_at` is set once at mint and never extended —
|
|
15
|
+
* there is deliberately no touch/renewal query on either spine (a sliding
|
|
16
|
+
* window renews a leaked cookie forever; see `docs/security.md` §Session
|
|
17
|
+
* Security). The cookie's `SESSION_AGE_MAX` mirrors this value.
|
|
18
|
+
*/
|
|
13
19
|
export declare const AUTH_SESSION_LIFETIME_MS: number;
|
|
14
|
-
/** Extend session when it has less than this remaining (1 day in ms). */
|
|
15
|
-
export declare const AUTH_SESSION_EXTEND_THRESHOLD_MS: number;
|
|
16
20
|
/**
|
|
17
21
|
* Hash a session token to its storage key using blake3.
|
|
18
22
|
*
|
|
@@ -46,16 +50,6 @@ export declare const query_create_session: (deps: QueryDeps, token_hash: string,
|
|
|
46
50
|
* @param token_hash - blake3 hash of the session token
|
|
47
51
|
*/
|
|
48
52
|
export declare const query_session_get_valid: (deps: QueryDeps, token_hash: string) => Promise<AuthSession | undefined>;
|
|
49
|
-
/**
|
|
50
|
-
* Update `last_seen_at` and optionally extend expiry for a session.
|
|
51
|
-
*
|
|
52
|
-
* Extends if less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remaining.
|
|
53
|
-
*
|
|
54
|
-
* @param deps - query dependencies
|
|
55
|
-
* @param token_hash - blake3 hash of the session token
|
|
56
|
-
* @mutates `auth_session` row - updates `last_seen_at` and conditionally `expires_at`
|
|
57
|
-
*/
|
|
58
|
-
export declare const query_session_touch: (deps: QueryDeps, token_hash: string) => Promise<void>;
|
|
59
53
|
/**
|
|
60
54
|
* Revoke (delete) a session by its token hash, with no account scoping.
|
|
61
55
|
*
|
|
@@ -137,9 +131,14 @@ export declare const query_session_enforce_limit: (deps: QueryDeps, account_id:
|
|
|
137
131
|
/**
|
|
138
132
|
* List all active sessions across all accounts with usernames.
|
|
139
133
|
*
|
|
134
|
+
* Ordered by `created_at DESC` (newest session first), matching the
|
|
135
|
+
* per-account listing. `last_seen_at` is still selected for the wire shape
|
|
136
|
+
* but is decorative — nothing updates it post-mint (it always equals
|
|
137
|
+
* `created_at`); it is slated for removal on its own twin migration.
|
|
138
|
+
*
|
|
140
139
|
* @param deps - query dependencies
|
|
141
140
|
* @param limit - maximum entries to return
|
|
142
|
-
* @returns active sessions joined with account usernames, newest
|
|
141
|
+
* @returns active sessions joined with account usernames, newest first
|
|
143
142
|
*/
|
|
144
143
|
export declare const query_session_list_all_active: (deps: QueryDeps, limit?: number) => Promise<Array<AuthSession & {
|
|
145
144
|
username: string;
|
|
@@ -151,19 +150,4 @@ export declare const query_session_list_all_active: (deps: QueryDeps, limit?: nu
|
|
|
151
150
|
* @mutates `auth_session` table - deletes every row past `expires_at`
|
|
152
151
|
*/
|
|
153
152
|
export declare const query_session_cleanup_expired: (deps: QueryDeps) => Promise<number>;
|
|
154
|
-
/**
|
|
155
|
-
* Touch a session without blocking the caller.
|
|
156
|
-
*
|
|
157
|
-
* Errors are logged to console — session touching never breaks request flows.
|
|
158
|
-
* Pass `pending_effects` (from `c.var.pending_effects`) to register
|
|
159
|
-
* the promise for test flushing.
|
|
160
|
-
*
|
|
161
|
-
* @param deps - query dependencies
|
|
162
|
-
* @param token_hash - blake3 hash of the session token
|
|
163
|
-
* @param pending_effects - optional array to register the effect for later awaiting
|
|
164
|
-
* @param log - the logger instance
|
|
165
|
-
* @returns the settled promise (callers may ignore it — fire-and-forget semantics preserved)
|
|
166
|
-
* @mutates `pending_effects` - pushes the in-flight settled promise when provided
|
|
167
|
-
*/
|
|
168
|
-
export declare const session_touch_fire_and_forget: (deps: QueryDeps, token_hash: string, pending_effects: Array<Promise<void>> | undefined, log: Logger) => Promise<void>;
|
|
169
153
|
//# sourceMappingURL=session_queries.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
|
|
10
10
|
import { generate_random_base64url } from '../crypto.js';
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Session lifetime in milliseconds (30 days).
|
|
13
|
+
*
|
|
14
|
+
* An **absolute** cap: `expires_at` is set once at mint and never extended —
|
|
15
|
+
* there is deliberately no touch/renewal query on either spine (a sliding
|
|
16
|
+
* window renews a leaked cookie forever; see `docs/security.md` §Session
|
|
17
|
+
* Security). The cookie's `SESSION_AGE_MAX` mirrors this value.
|
|
18
|
+
*/
|
|
12
19
|
export const AUTH_SESSION_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13
|
-
/** Extend session when it has less than this remaining (1 day in ms). */
|
|
14
|
-
export const AUTH_SESSION_EXTEND_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
15
20
|
/**
|
|
16
21
|
* Hash a session token to its storage key using blake3.
|
|
17
22
|
*
|
|
@@ -57,25 +62,6 @@ export const query_create_session = async (deps, token_hash, account_id, expires
|
|
|
57
62
|
export const query_session_get_valid = async (deps, token_hash) => {
|
|
58
63
|
return deps.db.query_one(`SELECT * FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
|
|
59
64
|
};
|
|
60
|
-
/**
|
|
61
|
-
* Update `last_seen_at` and optionally extend expiry for a session.
|
|
62
|
-
*
|
|
63
|
-
* Extends if less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remaining.
|
|
64
|
-
*
|
|
65
|
-
* @param deps - query dependencies
|
|
66
|
-
* @param token_hash - blake3 hash of the session token
|
|
67
|
-
* @mutates `auth_session` row - updates `last_seen_at` and conditionally `expires_at`
|
|
68
|
-
*/
|
|
69
|
-
export const query_session_touch = async (deps, token_hash) => {
|
|
70
|
-
const new_expires = new Date(Date.now() + AUTH_SESSION_LIFETIME_MS);
|
|
71
|
-
await deps.db.query(`UPDATE auth_session
|
|
72
|
-
SET last_seen_at = NOW(),
|
|
73
|
-
expires_at = CASE
|
|
74
|
-
WHEN expires_at - NOW() < INTERVAL '1 day' THEN $2::timestamptz
|
|
75
|
-
ELSE expires_at
|
|
76
|
-
END
|
|
77
|
-
WHERE id = $1`, [token_hash, new_expires.toISOString()]);
|
|
78
|
-
};
|
|
79
65
|
/**
|
|
80
66
|
* Revoke (delete) a session by its token hash, with no account scoping.
|
|
81
67
|
*
|
|
@@ -176,16 +162,21 @@ export const query_session_enforce_limit = async (deps, account_id, max_sessions
|
|
|
176
162
|
/**
|
|
177
163
|
* List all active sessions across all accounts with usernames.
|
|
178
164
|
*
|
|
165
|
+
* Ordered by `created_at DESC` (newest session first), matching the
|
|
166
|
+
* per-account listing. `last_seen_at` is still selected for the wire shape
|
|
167
|
+
* but is decorative — nothing updates it post-mint (it always equals
|
|
168
|
+
* `created_at`); it is slated for removal on its own twin migration.
|
|
169
|
+
*
|
|
179
170
|
* @param deps - query dependencies
|
|
180
171
|
* @param limit - maximum entries to return
|
|
181
|
-
* @returns active sessions joined with account usernames, newest
|
|
172
|
+
* @returns active sessions joined with account usernames, newest first
|
|
182
173
|
*/
|
|
183
174
|
export const query_session_list_all_active = async (deps, limit = 200) => {
|
|
184
175
|
return deps.db.query(`SELECT s.id, s.account_id, s.created_at, s.expires_at, s.last_seen_at, a.username
|
|
185
176
|
FROM auth_session s
|
|
186
177
|
JOIN account a ON a.id = s.account_id
|
|
187
178
|
WHERE s.expires_at > NOW()
|
|
188
|
-
ORDER BY s.
|
|
179
|
+
ORDER BY s.created_at DESC, s.id DESC LIMIT $1`, [limit]);
|
|
189
180
|
};
|
|
190
181
|
/**
|
|
191
182
|
* Delete expired sessions.
|
|
@@ -197,24 +188,3 @@ export const query_session_cleanup_expired = async (deps) => {
|
|
|
197
188
|
const rows = await deps.db.query(`DELETE FROM auth_session WHERE expires_at <= NOW() RETURNING id`);
|
|
198
189
|
return rows.length;
|
|
199
190
|
};
|
|
200
|
-
/**
|
|
201
|
-
* Touch a session without blocking the caller.
|
|
202
|
-
*
|
|
203
|
-
* Errors are logged to console — session touching never breaks request flows.
|
|
204
|
-
* Pass `pending_effects` (from `c.var.pending_effects`) to register
|
|
205
|
-
* the promise for test flushing.
|
|
206
|
-
*
|
|
207
|
-
* @param deps - query dependencies
|
|
208
|
-
* @param token_hash - blake3 hash of the session token
|
|
209
|
-
* @param pending_effects - optional array to register the effect for later awaiting
|
|
210
|
-
* @param log - the logger instance
|
|
211
|
-
* @returns the settled promise (callers may ignore it — fire-and-forget semantics preserved)
|
|
212
|
-
* @mutates `pending_effects` - pushes the in-flight settled promise when provided
|
|
213
|
-
*/
|
|
214
|
-
export const session_touch_fire_and_forget = (deps, token_hash, pending_effects, log) => {
|
|
215
|
-
const p = query_session_touch(deps, token_hash).catch((err) => {
|
|
216
|
-
log.error('Session touch failed:', err);
|
|
217
|
-
});
|
|
218
|
-
pending_effects?.push(p);
|
|
219
|
-
return p;
|
|
220
|
-
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API token lifetime — the temporal axis of a minted credential.
|
|
3
|
+
*
|
|
4
|
+
* `lifetime` is **required** on `account_token_create`, mirroring `scope`:
|
|
5
|
+
* there is deliberately no default. A caller who wants a never-expiring token
|
|
6
|
+
* spells `{kind: 'eternal'}`, and the request records that they did — an
|
|
7
|
+
* omitted lifetime is a validation error upstream, not an eternal token.
|
|
8
|
+
* `expires_at IS NULL` therefore means "deliberately eternal", never "minted
|
|
9
|
+
* before the policy existed".
|
|
10
|
+
*
|
|
11
|
+
* Lifetime is deliberately **not** part of the scope document — `TokenScope`
|
|
12
|
+
* is strict on both spines and sits on the hot resolve path. The expiry lives
|
|
13
|
+
* in the indexed `api_token.expires_at` column where the SQL validation gate
|
|
14
|
+
* already enforces it (`expires_at IS NULL OR expires_at > NOW()`).
|
|
15
|
+
*
|
|
16
|
+
* A framework-level ceiling (`max_token_ttl_days`) is deferred until `fuzf`
|
|
17
|
+
* has an expiry story (an expiry-aware read + a 401 hint); see
|
|
18
|
+
* `docs/security.md` §API tokens.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/**
|
|
24
|
+
* Upper bound on `ttl.days` — a sanity cap (100 years), not a policy ceiling.
|
|
25
|
+
* Shared with the Rust twin so out-of-range requests 400 identically on both
|
|
26
|
+
* spines.
|
|
27
|
+
*/
|
|
28
|
+
export declare const TOKEN_TTL_DAYS_MAX = 36500;
|
|
29
|
+
/**
|
|
30
|
+
* Caller-provided lifetime for `account_token_create`.
|
|
31
|
+
*
|
|
32
|
+
* `eternal` mints a never-expiring token (`expires_at` NULL); `ttl` bounds it
|
|
33
|
+
* to `days` from mint time. `eternal` is deliberately the first variant so
|
|
34
|
+
* schema-driven test generators produce it by default.
|
|
35
|
+
*/
|
|
36
|
+
export declare const TokenLifetimeInput: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
37
|
+
kind: z.ZodLiteral<"eternal">;
|
|
38
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"ttl">;
|
|
40
|
+
days: z.ZodNumber;
|
|
41
|
+
}, z.core.$strict>], "kind">;
|
|
42
|
+
export type TokenLifetimeInput = z.infer<typeof TokenLifetimeInput>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a lifetime input to the `expires_at` value the mint query stores.
|
|
45
|
+
*
|
|
46
|
+
* @param lifetime - the validated lifetime input
|
|
47
|
+
* @param now - mint time (injectable for tests; defaults to the current time)
|
|
48
|
+
* @returns the expiry `Date`, or `null` for an eternal token
|
|
49
|
+
*/
|
|
50
|
+
export declare const token_lifetime_to_expires_at: (lifetime: TokenLifetimeInput, now?: Date) => Date | null;
|
|
51
|
+
//# sourceMappingURL=token_lifetime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_lifetime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;;4BAM7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAU,kBAAkB,EAC5B,MAAK,IAAiB,KACpB,IAAI,GAAG,IAGT,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API token lifetime — the temporal axis of a minted credential.
|
|
3
|
+
*
|
|
4
|
+
* `lifetime` is **required** on `account_token_create`, mirroring `scope`:
|
|
5
|
+
* there is deliberately no default. A caller who wants a never-expiring token
|
|
6
|
+
* spells `{kind: 'eternal'}`, and the request records that they did — an
|
|
7
|
+
* omitted lifetime is a validation error upstream, not an eternal token.
|
|
8
|
+
* `expires_at IS NULL` therefore means "deliberately eternal", never "minted
|
|
9
|
+
* before the policy existed".
|
|
10
|
+
*
|
|
11
|
+
* Lifetime is deliberately **not** part of the scope document — `TokenScope`
|
|
12
|
+
* is strict on both spines and sits on the hot resolve path. The expiry lives
|
|
13
|
+
* in the indexed `api_token.expires_at` column where the SQL validation gate
|
|
14
|
+
* already enforces it (`expires_at IS NULL OR expires_at > NOW()`).
|
|
15
|
+
*
|
|
16
|
+
* A framework-level ceiling (`max_token_ttl_days`) is deferred until `fuzf`
|
|
17
|
+
* has an expiry story (an expiry-aware read + a 401 hint); see
|
|
18
|
+
* `docs/security.md` §API tokens.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/**
|
|
24
|
+
* Upper bound on `ttl.days` — a sanity cap (100 years), not a policy ceiling.
|
|
25
|
+
* Shared with the Rust twin so out-of-range requests 400 identically on both
|
|
26
|
+
* spines.
|
|
27
|
+
*/
|
|
28
|
+
export const TOKEN_TTL_DAYS_MAX = 36_500;
|
|
29
|
+
/**
|
|
30
|
+
* Caller-provided lifetime for `account_token_create`.
|
|
31
|
+
*
|
|
32
|
+
* `eternal` mints a never-expiring token (`expires_at` NULL); `ttl` bounds it
|
|
33
|
+
* to `days` from mint time. `eternal` is deliberately the first variant so
|
|
34
|
+
* schema-driven test generators produce it by default.
|
|
35
|
+
*/
|
|
36
|
+
export const TokenLifetimeInput = z.discriminatedUnion('kind', [
|
|
37
|
+
z.strictObject({ kind: z.literal('eternal') }),
|
|
38
|
+
z.strictObject({
|
|
39
|
+
kind: z.literal('ttl'),
|
|
40
|
+
days: z.number().int().min(1).max(TOKEN_TTL_DAYS_MAX)
|
|
41
|
+
})
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a lifetime input to the `expires_at` value the mint query stores.
|
|
45
|
+
*
|
|
46
|
+
* @param lifetime - the validated lifetime input
|
|
47
|
+
* @param now - mint time (injectable for tests; defaults to the current time)
|
|
48
|
+
* @returns the expiry `Date`, or `null` for an eternal token
|
|
49
|
+
*/
|
|
50
|
+
export const token_lifetime_to_expires_at = (lifetime, now = new Date()) => {
|
|
51
|
+
if (lifetime.kind === 'eternal')
|
|
52
|
+
return null;
|
|
53
|
+
return new Date(now.getTime() + lifetime.days * 86_400_000);
|
|
54
|
+
};
|
package/dist/db/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ the code.
|
|
|
25
25
|
`baseline`), `Migration` / `MigrationNamespace` / `MigrationError`.
|
|
26
26
|
- `query_deps.ts` — `QueryDeps = {db}`, the first param to every `query_*`.
|
|
27
27
|
- `assert_row.ts` — `assert_row(row, context)` for INSERT … RETURNING.
|
|
28
|
-
- `pg_error.ts` — `is_pg_unique_violation` (Postgres `23505`).
|
|
28
|
+
- `pg_error.ts` — `pg_error_code` (SQLSTATE extractor for `pg`/PGlite errors), `is_pg_unique_violation` (Postgres `23505`).
|
|
29
29
|
- `sql_identifier.ts` — `assert_valid_sql_identifier`.
|
|
30
30
|
- `status.ts` — CLI DB status utility.
|
|
31
31
|
- `schema_ready.ts` — `/ready` deploy-gate core: `query_public_columns`
|
|
@@ -95,7 +95,8 @@ child_id`, fractional-index keyed): `query_cell_item_insert` (throws
|
|
|
95
95
|
- **`cell_audit_queries.ts`** — `query_audit_log_list_by_cell`: matches
|
|
96
96
|
`audit_log` rows whose `metadata` names the cell on any of six cell-domain
|
|
97
97
|
keys (`cell_id` / `source_id` / `new_id` / `parent_id` / `child_id` /
|
|
98
|
-
`target_id`), bitmap-OR over the
|
|
98
|
+
`target_id`), bitmap-OR over `idx_audit_log_metadata` (the `jsonb_path_ops`
|
|
99
|
+
GIN the `audit_log_metadata_gin_index` auth migration ships).
|
|
99
100
|
|
|
100
101
|
## Fact layer
|
|
101
102
|
|
|
@@ -155,6 +156,9 @@ auth_migration_ns (built-in, fuz_auth)
|
|
|
155
156
|
|
|
156
157
|
### Pre-stable migration policy
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
Scoped to the namespaces on this page (`fuz_cell` / `fuz_cell_history` /
|
|
160
|
+
`fuz_facts`): each is a single `full_*_schema` entry, so when it needs to
|
|
161
|
+
change shape, **rewrite the original in place** rather than appending a
|
|
162
|
+
follow-up — consumers re-bootstrap dev DBs on upgrade. The `fuz_auth`
|
|
163
|
+
namespace is the opposite — frozen and append-only, since consumers hold
|
|
164
|
+
deployed databases on it. See ../../../docs/migrations.md §Schema stability.
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* `new_id` (clone), `source_id` / `target_id` (cell_field events),
|
|
6
6
|
* `parent_id` / `child_id` (cell_item events).
|
|
7
7
|
*
|
|
8
|
-
* Each `metadata @> '{...}'::jsonb` clause hits
|
|
9
|
-
* `audit_log.metadata
|
|
8
|
+
* Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
|
|
9
|
+
* (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
|
|
10
|
+
* `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
|
|
11
|
+
* scans together.
|
|
10
12
|
*
|
|
11
13
|
* @module
|
|
12
14
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* `new_id` (clone), `source_id` / `target_id` (cell_field events),
|
|
6
6
|
* `parent_id` / `child_id` (cell_item events).
|
|
7
7
|
*
|
|
8
|
-
* Each `metadata @> '{...}'::jsonb` clause hits
|
|
9
|
-
* `audit_log.metadata
|
|
8
|
+
* Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
|
|
9
|
+
* (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
|
|
10
|
+
* `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
|
|
11
|
+
* scans together.
|
|
10
12
|
*
|
|
11
13
|
* @module
|
|
12
14
|
*/
|
package/dist/db/pg_error.d.ts
CHANGED
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Extract the error-code string from a caught error, for SQLSTATE checks.
|
|
11
|
+
*
|
|
12
|
+
* Other error families also carry string `code`s (Node FS errors:
|
|
13
|
+
* `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
|
|
14
|
+
* values, never against truthiness.
|
|
15
|
+
*
|
|
16
|
+
* @param error - the caught error
|
|
17
|
+
* @returns the `code` string, or `null` when the value isn't an `Error`
|
|
18
|
+
* or carries no string `code`
|
|
19
|
+
*/
|
|
20
|
+
export declare const pg_error_code: (error: unknown) => string | null;
|
|
9
21
|
/**
|
|
10
22
|
* Check if an error is a PostgreSQL unique constraint violation (error code 23505).
|
|
11
23
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,
|
|
1
|
+
{"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAGhD,CAAC;AAET;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAA2C,CAAC"}
|
package/dist/db/pg_error.js
CHANGED
|
@@ -6,10 +6,24 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Extract the error-code string from a caught error, for SQLSTATE checks.
|
|
11
|
+
*
|
|
12
|
+
* Other error families also carry string `code`s (Node FS errors:
|
|
13
|
+
* `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
|
|
14
|
+
* values, never against truthiness.
|
|
15
|
+
*
|
|
16
|
+
* @param error - the caught error
|
|
17
|
+
* @returns the `code` string, or `null` when the value isn't an `Error`
|
|
18
|
+
* or carries no string `code`
|
|
19
|
+
*/
|
|
20
|
+
export const pg_error_code = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string'
|
|
21
|
+
? error.code
|
|
22
|
+
: null;
|
|
9
23
|
/**
|
|
10
24
|
* Check if an error is a PostgreSQL unique constraint violation (error code 23505).
|
|
11
25
|
*
|
|
12
26
|
* @param error - the caught error
|
|
13
27
|
* @returns `true` if the error is a unique constraint violation
|
|
14
28
|
*/
|
|
15
|
-
export const is_pg_unique_violation = (error) => error
|
|
29
|
+
export const is_pg_unique_violation = (error) => pg_error_code(error) === '23505';
|
package/dist/dev/setup.d.ts
CHANGED
|
@@ -57,16 +57,12 @@ export interface SetupEnvOptions {
|
|
|
57
57
|
* Replaces `^KEY=$` (empty value) patterns in the env file.
|
|
58
58
|
*/
|
|
59
59
|
replacements?: Record<string, () => Promise<string>>;
|
|
60
|
-
/** Optional callback to set file permissions (e.g. `Deno.chmod`). */
|
|
61
|
-
set_permissions?: (path: string, mode: number) => Promise<void>;
|
|
62
60
|
log?: SetupLogger;
|
|
63
61
|
}
|
|
64
62
|
/** Options for `setup_bootstrap_token`. */
|
|
65
63
|
export interface SetupBootstrapTokenOptions {
|
|
66
64
|
/** State directory override. Defaults to `~/.{app_name}`. */
|
|
67
65
|
state_dir?: string;
|
|
68
|
-
/** Optional callback to set file/directory permissions. */
|
|
69
|
-
set_permissions?: (path: string, mode: number) => Promise<void>;
|
|
70
66
|
log?: SetupLogger;
|
|
71
67
|
}
|
|
72
68
|
/** Options for `create_database`. */
|
|
@@ -119,9 +115,9 @@ export declare const read_env_var: (deps: Pick<FsReadDeps, "read_text_file">, en
|
|
|
119
115
|
* @param deps - file read, write, and command capabilities
|
|
120
116
|
* @param env_path - path for the env file (e.g. `.env.development`)
|
|
121
117
|
* @param example_path - path to the example template
|
|
122
|
-
* @param options - extra replacements,
|
|
118
|
+
* @param options - extra replacements, logger
|
|
123
119
|
* @returns result indicating whether the file was created or updated
|
|
124
|
-
* @mutates filesystem - writes `env_path` (creating from `example_path` if missing)
|
|
120
|
+
* @mutates filesystem - writes `env_path` atomically at mode `0o600` (creating from `example_path` if missing)
|
|
125
121
|
*/
|
|
126
122
|
export declare const setup_env_file: (deps: FsReadDeps & FsWriteDeps & CommandDeps, env_path: string, example_path: string, options?: SetupEnvOptions) => Promise<SetupEnvResult>;
|
|
127
123
|
/**
|
|
@@ -132,9 +128,9 @@ export declare const setup_env_file: (deps: FsReadDeps & FsWriteDeps & CommandDe
|
|
|
132
128
|
*
|
|
133
129
|
* @param deps - file, command, and env capabilities
|
|
134
130
|
* @param app_name - application name (used for default state directory)
|
|
135
|
-
* @param options - state_dir override,
|
|
131
|
+
* @param options - state_dir override, logger
|
|
136
132
|
* @returns result indicating whether a token was created
|
|
137
|
-
* @mutates filesystem - creates state directory and writes the token file
|
|
133
|
+
* @mutates filesystem - creates the state directory (`0o700`) and writes the token file atomically at `0o600`
|
|
138
134
|
*/
|
|
139
135
|
export declare const setup_bootstrap_token: (deps: FsReadDeps & FsWriteDeps & CommandDeps & EnvDeps, app_name: string, options?: SetupBootstrapTokenOptions) => Promise<SetupTokenResult>;
|
|
140
136
|
/**
|
|
@@ -142,7 +138,7 @@ export declare const setup_bootstrap_token: (deps: FsReadDeps & FsWriteDeps & Co
|
|
|
142
138
|
*
|
|
143
139
|
* @param deps - file, command, env, and remove capabilities
|
|
144
140
|
* @param app_name - application name
|
|
145
|
-
* @param options - state_dir override,
|
|
141
|
+
* @param options - state_dir override, logger
|
|
146
142
|
* @returns result from creating the new token
|
|
147
143
|
* @mutates filesystem - removes the existing token file (if any) then writes a fresh one
|
|
148
144
|
*/
|
package/dist/dev/setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/dev/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,UAAU,EACV,YAAY,EACZ,WAAW,EACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/dev/setup.ts"],"names":[],"mappings":"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"}
|
package/dist/dev/setup.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
11
|
import { load_env_file } from '../env/dotenv.js';
|
|
12
|
+
import { write_file_atomic } from '../runtime/fs.js';
|
|
12
13
|
import { query_account_by_username, query_actors_by_account, query_create_account_with_actor } from '../auth/account_queries.js';
|
|
13
14
|
import { query_create_role_grant } from '../auth/role_grant_queries.js';
|
|
14
15
|
/** Default logger using bracket format. */
|
|
@@ -77,13 +78,12 @@ export const read_env_var = async (deps, env_path, name) => {
|
|
|
77
78
|
* @param deps - file read, write, and command capabilities
|
|
78
79
|
* @param env_path - path for the env file (e.g. `.env.development`)
|
|
79
80
|
* @param example_path - path to the example template
|
|
80
|
-
* @param options - extra replacements,
|
|
81
|
+
* @param options - extra replacements, logger
|
|
81
82
|
* @returns result indicating whether the file was created or updated
|
|
82
|
-
* @mutates filesystem - writes `env_path` (creating from `example_path` if missing)
|
|
83
|
+
* @mutates filesystem - writes `env_path` atomically at mode `0o600` (creating from `example_path` if missing)
|
|
83
84
|
*/
|
|
84
85
|
export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
85
86
|
const log = options?.log ?? default_setup_logger;
|
|
86
|
-
const set_permissions = options?.set_permissions;
|
|
87
87
|
// build the full replacement map (SECRET_FUZ_COOKIE_KEYS + extras)
|
|
88
88
|
const replacements = {
|
|
89
89
|
SECRET_FUZ_COOKIE_KEYS: () => generate_random_key(deps),
|
|
@@ -104,9 +104,10 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
if (changed) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
// Atomic replace at 0600 — the backfill just wrote secrets into the
|
|
108
|
+
// file, so publishing a fresh owner-only inode also repairs a
|
|
109
|
+
// hand-made permissive .env in the same step the old chmod covered.
|
|
110
|
+
await write_file_atomic(deps, env_path, content, { mode: 0o600 });
|
|
110
111
|
}
|
|
111
112
|
else {
|
|
112
113
|
log.skip(`${env_path} already configured`);
|
|
@@ -122,9 +123,7 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
|
122
123
|
content = content.replace(pattern, `${key}=${value}`);
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
|
-
await deps
|
|
126
|
-
if (set_permissions)
|
|
127
|
-
await set_permissions(env_path, 0o600);
|
|
126
|
+
await write_file_atomic(deps, env_path, content, { mode: 0o600 });
|
|
128
127
|
log.ok(`Created ${env_path} with generated secrets`);
|
|
129
128
|
return { created: true, updated: true, path: env_path };
|
|
130
129
|
};
|
|
@@ -136,13 +135,12 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
|
136
135
|
*
|
|
137
136
|
* @param deps - file, command, and env capabilities
|
|
138
137
|
* @param app_name - application name (used for default state directory)
|
|
139
|
-
* @param options - state_dir override,
|
|
138
|
+
* @param options - state_dir override, logger
|
|
140
139
|
* @returns result indicating whether a token was created
|
|
141
|
-
* @mutates filesystem - creates state directory and writes the token file
|
|
140
|
+
* @mutates filesystem - creates the state directory (`0o700`) and writes the token file atomically at `0o600`
|
|
142
141
|
*/
|
|
143
142
|
export const setup_bootstrap_token = async (deps, app_name, options) => {
|
|
144
143
|
const log = options?.log ?? default_setup_logger;
|
|
145
|
-
const set_permissions = options?.set_permissions;
|
|
146
144
|
const home = deps.env_get('HOME');
|
|
147
145
|
if (!home) {
|
|
148
146
|
log.skip('$HOME not set, skipping bootstrap token');
|
|
@@ -155,13 +153,13 @@ export const setup_bootstrap_token = async (deps, app_name, options) => {
|
|
|
155
153
|
log.skip(`~/.${app_name}/secret_bootstrap_token already exists`);
|
|
156
154
|
return { created: false, token_path };
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// Modes apply at creation — an already-existing state dir keeps its mode.
|
|
157
|
+
// The token itself is always freshly created here (the stat above skipped
|
|
158
|
+
// existing files), so its 0600 holds unconditionally; the server-side
|
|
159
|
+
// `read_secure_file` refuses anything laxer at read time.
|
|
160
|
+
await deps.mkdir(state_dir, { recursive: true, mode: 0o700 });
|
|
161
161
|
const key = await generate_random_key(deps);
|
|
162
|
-
await deps
|
|
163
|
-
if (set_permissions)
|
|
164
|
-
await set_permissions(token_path, 0o600);
|
|
162
|
+
await write_file_atomic(deps, token_path, key + '\n', { mode: 0o600 });
|
|
165
163
|
log.ok(`Created ~/.${app_name}/secret_bootstrap_token (one-shot, deleted after first use)`);
|
|
166
164
|
return { created: true, token_path };
|
|
167
165
|
};
|
|
@@ -170,13 +168,12 @@ export const setup_bootstrap_token = async (deps, app_name, options) => {
|
|
|
170
168
|
*
|
|
171
169
|
* @param deps - file, command, env, and remove capabilities
|
|
172
170
|
* @param app_name - application name
|
|
173
|
-
* @param options - state_dir override,
|
|
171
|
+
* @param options - state_dir override, logger
|
|
174
172
|
* @returns result from creating the new token
|
|
175
173
|
* @mutates filesystem - removes the existing token file (if any) then writes a fresh one
|
|
176
174
|
*/
|
|
177
175
|
export const reset_bootstrap_token = async (deps, app_name, options) => {
|
|
178
176
|
const log = options?.log ?? default_setup_logger;
|
|
179
|
-
const set_permissions = options?.set_permissions;
|
|
180
177
|
const home = deps.env_get('HOME');
|
|
181
178
|
if (!home) {
|
|
182
179
|
log.skip('$HOME not set');
|
|
@@ -189,7 +186,7 @@ export const reset_bootstrap_token = async (deps, app_name, options) => {
|
|
|
189
186
|
await deps.remove(token_path);
|
|
190
187
|
log.ok('Removed existing bootstrap token');
|
|
191
188
|
}
|
|
192
|
-
return setup_bootstrap_token(deps, app_name, { state_dir,
|
|
189
|
+
return setup_bootstrap_token(deps, app_name, { state_dir, log });
|
|
193
190
|
};
|
|
194
191
|
// === Database helpers ===
|
|
195
192
|
/**
|