@fuzdev/fuz_app 0.59.0 → 0.60.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 +5 -5
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +2 -2
- package/dist/actions/action_event_helpers.d.ts +3 -3
- package/dist/actions/action_event_helpers.js +8 -8
- package/dist/actions/action_event_types.d.ts +3 -3
- package/dist/actions/action_event_types.js +3 -3
- package/dist/actions/transports_ws_auth_guard.d.ts +2 -2
- package/dist/actions/transports_ws_auth_guard.js +3 -3
- package/dist/auth/CLAUDE.md +157 -15
- package/dist/auth/actor_lookup_action_specs.d.ts +127 -0
- package/dist/auth/actor_lookup_action_specs.d.ts.map +1 -0
- package/dist/auth/actor_lookup_action_specs.js +93 -0
- package/dist/auth/actor_lookup_actions.d.ts +19 -0
- package/dist/auth/actor_lookup_actions.d.ts.map +1 -0
- package/dist/auth/actor_lookup_actions.js +32 -0
- package/dist/auth/actor_lookup_queries.d.ts +44 -0
- package/dist/auth/actor_lookup_queries.d.ts.map +1 -0
- package/dist/auth/actor_lookup_queries.js +42 -0
- package/dist/auth/actor_search_action_specs.d.ts +166 -0
- package/dist/auth/actor_search_action_specs.d.ts.map +1 -0
- package/dist/auth/actor_search_action_specs.js +139 -0
- package/dist/auth/actor_search_actions.d.ts +31 -0
- package/dist/auth/actor_search_actions.d.ts.map +1 -0
- package/dist/auth/actor_search_actions.js +61 -0
- package/dist/auth/actor_search_queries.d.ts +75 -0
- package/dist/auth/actor_search_queries.d.ts.map +1 -0
- package/dist/auth/actor_search_queries.js +91 -0
- package/dist/auth/admin_actions.js +2 -2
- package/dist/auth/all_action_spec_registries.d.ts +55 -0
- package/dist/auth/all_action_spec_registries.d.ts.map +1 -0
- package/dist/auth/all_action_spec_registries.js +59 -0
- package/dist/auth/audit_emitter.d.ts +1 -1
- package/dist/auth/audit_emitter.js +2 -2
- package/dist/auth/audit_log_queries.d.ts +1 -1
- package/dist/auth/audit_log_queries.js +3 -3
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.js +1 -1
- package/dist/auth/audit_log_schema.d.ts +5 -5
- package/dist/auth/audit_log_schema.js +7 -7
- package/dist/auth/auth_ddl.d.ts +7 -0
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +8 -0
- package/dist/auth/credential_type_schema.d.ts +1 -1
- package/dist/auth/credential_type_schema.js +3 -3
- package/dist/auth/grant_path_schema.d.ts +1 -1
- package/dist/auth/grant_path_schema.js +3 -3
- package/dist/auth/migrations.d.ts +4 -4
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +7 -6
- package/dist/auth/role_grant_offer_actions.js +2 -2
- package/dist/auth/role_grant_offer_notifications.d.ts +2 -2
- package/dist/auth/role_grant_offer_notifications.js +2 -2
- package/dist/auth/role_grant_queries.d.ts +21 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +31 -0
- package/dist/auth/role_schema.d.ts +2 -2
- package/dist/auth/role_schema.js +3 -3
- package/dist/auth/self_service_role_actions.d.ts +1 -1
- package/dist/auth/self_service_role_actions.js +2 -2
- package/dist/auth/session_cookie.d.ts +1 -1
- package/dist/auth/session_cookie.js +1 -1
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.js +5 -5
- package/dist/rate_limiter.d.ts +5 -5
- package/dist/rate_limiter.js +6 -6
- package/dist/realtime/sse_auth_guard.d.ts +3 -3
- package/dist/realtime/sse_auth_guard.js +4 -4
- package/dist/server/app_backend.d.ts +3 -3
- package/dist/server/app_backend.js +4 -4
- package/dist/server/app_server.d.ts +1 -1
- package/dist/server/app_server.js +10 -10
- package/dist/testing/CLAUDE.md +22 -12
- package/dist/testing/admin_integration.js +4 -4
- package/dist/testing/app_server.d.ts +1 -1
- package/dist/testing/app_server.js +2 -2
- package/dist/testing/attack_surface.d.ts +4 -4
- package/dist/testing/attack_surface.js +6 -6
- package/dist/testing/audit_completeness.js +4 -4
- package/dist/testing/data_exposure.d.ts +2 -2
- package/dist/testing/data_exposure.js +7 -7
- package/dist/testing/db.d.ts +8 -8
- package/dist/testing/db.js +11 -11
- package/dist/testing/integration.js +4 -4
- package/dist/testing/integration_helpers.d.ts +6 -6
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/rate_limiting.js +4 -4
- package/dist/testing/round_trip.js +2 -2
- package/dist/testing/rpc_round_trip.js +2 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +23 -2
- package/dist/testing/sse_round_trip.js +2 -2
- package/dist/testing/surface_invariants.d.ts +4 -4
- package/dist/testing/surface_invariants.js +5 -5
- package/package.json +1 -1
package/dist/auth/migrations.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* To add a migration in the pre-stable phase, prefer extending an existing
|
|
19
19
|
* entry's body (consumers will re-bootstrap on upgrade). If you do append
|
|
20
|
-
* a new entry to `
|
|
20
|
+
* a new entry to `auth_migrations`, the runner will apply it on existing
|
|
21
21
|
* tracker rows — the same shape that will become mandatory once the
|
|
22
22
|
* schema stabilizes:
|
|
23
23
|
*
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*
|
|
37
37
|
* @module
|
|
38
38
|
*/
|
|
39
|
-
import { ACCOUNT_SCHEMA, ACCOUNT_EMAIL_INDEX, ACCOUNT_USERNAME_CI_INDEX, ACTOR_SCHEMA, ACTOR_INDEX, ROLE_GRANT_SCHEMA, ROLE_GRANT_INDEXES, AUTH_SESSION_SCHEMA, AUTH_SESSION_INDEXES, API_TOKEN_SCHEMA, API_TOKEN_INDEX, BOOTSTRAP_LOCK_SCHEMA, BOOTSTRAP_LOCK_SEED, INVITE_SCHEMA, INVITE_INDEXES, APP_SETTINGS_SCHEMA, APP_SETTINGS_SEED, } from './auth_ddl.js';
|
|
39
|
+
import { ACCOUNT_SCHEMA, ACCOUNT_EMAIL_INDEX, ACCOUNT_USERNAME_CI_INDEX, ACTOR_SCHEMA, ACTOR_INDEX, ACTOR_NAME_LOWER_INDEX, ROLE_GRANT_SCHEMA, ROLE_GRANT_INDEXES, AUTH_SESSION_SCHEMA, AUTH_SESSION_INDEXES, API_TOKEN_SCHEMA, API_TOKEN_INDEX, BOOTSTRAP_LOCK_SCHEMA, BOOTSTRAP_LOCK_SEED, INVITE_SCHEMA, INVITE_INDEXES, APP_SETTINGS_SCHEMA, APP_SETTINGS_SEED, } from './auth_ddl.js';
|
|
40
40
|
import { AUDIT_LOG_SCHEMA, AUDIT_LOG_INDEXES } from './audit_log_ddl.js';
|
|
41
41
|
import { ROLE_GRANT_OFFER_SCHEMA, ROLE_GRANT_OFFER_PENDING_UNIQUE_INDEX, ROLE_GRANT_OFFER_INBOX_INDEX, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, } from './role_grant_offer_ddl.js';
|
|
42
42
|
/** Namespace identifier for fuz_app auth migrations. */
|
|
@@ -48,7 +48,7 @@ export const AUTH_MIGRATION_NAMESPACE = 'fuz_auth';
|
|
|
48
48
|
* as `ReadonlyArray<string>` (not a literal tuple) so `.includes()` accepts
|
|
49
49
|
* any consumer-supplied namespace string without a cast.
|
|
50
50
|
*/
|
|
51
|
-
export const
|
|
51
|
+
export const reserved_migration_namespaces = [AUTH_MIGRATION_NAMESPACE];
|
|
52
52
|
/**
|
|
53
53
|
* Auth schema migrations in order.
|
|
54
54
|
*
|
|
@@ -69,7 +69,7 @@ export const RESERVED_MIGRATION_NAMESPACES = [AUTH_MIGRATION_NAMESPACE];
|
|
|
69
69
|
* (registry-membership validation against `create_scope_kind_schema`);
|
|
70
70
|
* v2 may add INSERT-time `(role, scope_kind)` enforcement.
|
|
71
71
|
*/
|
|
72
|
-
export const
|
|
72
|
+
export const auth_migrations = [
|
|
73
73
|
// v0: full auth schema — all IF NOT EXISTS, safe for existing databases
|
|
74
74
|
{
|
|
75
75
|
name: 'full_auth_schema',
|
|
@@ -79,6 +79,7 @@ export const AUTH_MIGRATIONS = [
|
|
|
79
79
|
await db.query(ACCOUNT_USERNAME_CI_INDEX);
|
|
80
80
|
await db.query(ACTOR_SCHEMA);
|
|
81
81
|
await db.query(ACTOR_INDEX);
|
|
82
|
+
await db.query(ACTOR_NAME_LOWER_INDEX);
|
|
82
83
|
await db.query(ROLE_GRANT_SCHEMA);
|
|
83
84
|
for (const sql of ROLE_GRANT_INDEXES) {
|
|
84
85
|
await db.query(sql);
|
|
@@ -150,7 +151,7 @@ export const AUTH_MIGRATIONS = [
|
|
|
150
151
|
},
|
|
151
152
|
];
|
|
152
153
|
/** Pre-composed migration namespace for auth tables. */
|
|
153
|
-
export const
|
|
154
|
+
export const auth_migration_ns = {
|
|
154
155
|
namespace: AUTH_MIGRATION_NAMESPACE,
|
|
155
|
-
migrations:
|
|
156
|
+
migrations: auth_migrations,
|
|
156
157
|
};
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
import { rpc_action, } from '../actions/action_rpc.js';
|
|
41
41
|
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
42
42
|
import { emit_after_commit } from '../http/pending_effects.js';
|
|
43
|
-
import {
|
|
43
|
+
import { builtin_role_specs_by_name, ROLE_ADMIN, role_has_grant_path, } from './role_schema.js';
|
|
44
44
|
import { GRANT_PATH_ADMIN } from './grant_path_schema.js';
|
|
45
45
|
import { ROLE_GRANT_OFFER_DEFAULT_TTL_MS, to_role_grant_offer_json, } from './role_grant_offer_schema.js';
|
|
46
46
|
import { query_role_grant_offer_create, query_role_grant_offer_decline, query_role_grant_offer_retract, query_role_grant_offer_list, query_role_grant_offer_history_for_account, query_accept_offer, RoleGrantOfferActorAccountMismatchError, RoleGrantOfferActorMismatchError, RoleGrantOfferAlreadyTerminalError, RoleGrantOfferExpiredError, RoleGrantOfferNotFoundError, RoleGrantOfferSelfTargetError, } from './role_grant_offer_queries.js';
|
|
@@ -108,7 +108,7 @@ export const authorize_admin_or_holder = async (auth, input, _deps, _ctx) => {
|
|
|
108
108
|
*/
|
|
109
109
|
export const create_role_grant_offer_actions = (deps, options = {}) => {
|
|
110
110
|
const { log, audit, notification_sender = null } = deps;
|
|
111
|
-
const role_specs = options.roles?.role_specs ??
|
|
111
|
+
const role_specs = options.roles?.role_specs ?? builtin_role_specs_by_name;
|
|
112
112
|
const default_ttl_ms = options.default_ttl_ms ?? ROLE_GRANT_OFFER_DEFAULT_TTL_MS;
|
|
113
113
|
const authorize = options.authorize ?? default_authorize;
|
|
114
114
|
// Four denial paths (admin-grant-path gate, authorize, self-target,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `NotificationSender.send_to_account` argument), not in the payload.
|
|
24
24
|
*
|
|
25
25
|
* The specs surface as `EventSpec`s via `create_action_event_spec` — callers
|
|
26
|
-
* append `
|
|
26
|
+
* append `role_grant_offer_notification_specs` to their `event_specs` on
|
|
27
27
|
* `create_app_server` so the surface reflects them and DEV-mode broadcast
|
|
28
28
|
* validation catches payload drift.
|
|
29
29
|
*
|
|
@@ -376,7 +376,7 @@ export declare const role_grant_revoke_notification_spec: {
|
|
|
376
376
|
* Pass to `create_app_server`'s `event_specs` so the attack surface reflects
|
|
377
377
|
* them and DEV-mode `create_validated_broadcaster` catches payload drift.
|
|
378
378
|
*/
|
|
379
|
-
export declare const
|
|
379
|
+
export declare const role_grant_offer_notification_specs: Array<EventSpec>;
|
|
380
380
|
export declare const build_role_grant_offer_received_notification: (params: RoleGrantOfferReceivedParams) => JsonrpcNotification;
|
|
381
381
|
export declare const build_role_grant_offer_retracted_notification: (params: RoleGrantOfferRetractedParams) => JsonrpcNotification;
|
|
382
382
|
export declare const build_role_grant_offer_accepted_notification: (params: RoleGrantOfferAcceptedParams) => JsonrpcNotification;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `NotificationSender.send_to_account` argument), not in the payload.
|
|
24
24
|
*
|
|
25
25
|
* The specs surface as `EventSpec`s via `create_action_event_spec` — callers
|
|
26
|
-
* append `
|
|
26
|
+
* append `role_grant_offer_notification_specs` to their `event_specs` on
|
|
27
27
|
* `create_app_server` so the surface reflects them and DEV-mode broadcast
|
|
28
28
|
* validation catches payload drift.
|
|
29
29
|
*
|
|
@@ -165,7 +165,7 @@ export const role_grant_revoke_notification_spec = {
|
|
|
165
165
|
* Pass to `create_app_server`'s `event_specs` so the attack surface reflects
|
|
166
166
|
* them and DEV-mode `create_validated_broadcaster` catches payload drift.
|
|
167
167
|
*/
|
|
168
|
-
export const
|
|
168
|
+
export const role_grant_offer_notification_specs = [
|
|
169
169
|
create_action_event_spec(role_grant_offer_received_notification_spec),
|
|
170
170
|
create_action_event_spec(role_grant_offer_retracted_notification_spec),
|
|
171
171
|
create_action_event_spec(role_grant_offer_accepted_notification_spec),
|
|
@@ -113,6 +113,27 @@ export declare const query_role_grant_find_active_for_actor: (deps: QueryDeps, a
|
|
|
113
113
|
* The `IS NOT DISTINCT FROM` comparison handles the NULL case uniformly.
|
|
114
114
|
*/
|
|
115
115
|
export declare const query_role_grant_has_role: (deps: QueryDeps, actor_id: string, role: string, scope_id?: string | null) => Promise<boolean>;
|
|
116
|
+
/**
|
|
117
|
+
* Account-grain check: does any actor on `account_id` hold an active
|
|
118
|
+
* global role_grant for `role`?
|
|
119
|
+
*
|
|
120
|
+
* Symmetric with `query_role_grant_has_role` but keyed on the account
|
|
121
|
+
* instead of a single actor — for surfaces with `auth: actor: 'none'`
|
|
122
|
+
* that don't load `auth.role_grants` and can't use the in-memory
|
|
123
|
+
* `has_scoped_role` predicate. Joins `role_grant` → `actor`; matches
|
|
124
|
+
* only global role_grants (`scope_id IS NULL`) since the use case is
|
|
125
|
+
* "is the caller's account broadly admin", not scope-aware.
|
|
126
|
+
*
|
|
127
|
+
* Fast under the existing `idx_role_grant_actor` index — the inner
|
|
128
|
+
* `actor_id IN (...)` subquery is index-scan, and the outer EXISTS
|
|
129
|
+
* stops at the first match.
|
|
130
|
+
*
|
|
131
|
+
* @param deps - query dependencies
|
|
132
|
+
* @param account_id - the account to check
|
|
133
|
+
* @param role - the role to check for (e.g. `ROLE_ADMIN`)
|
|
134
|
+
* @returns `true` if any actor on the account has an active global role_grant for `role`
|
|
135
|
+
*/
|
|
136
|
+
export declare const query_account_has_global_role: (deps: QueryDeps, account_id: string, role: string) => Promise<boolean>;
|
|
116
137
|
/**
|
|
117
138
|
* List all role_grants for an actor (including revoked/expired).
|
|
118
139
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_queries.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"role_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -180,6 +180,37 @@ export const query_role_grant_has_role = async (deps, actor_id, role, scope_id)
|
|
|
180
180
|
) AS exists`, [actor_id, role, scope_id ?? null]);
|
|
181
181
|
return row?.exists ?? false;
|
|
182
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* Account-grain check: does any actor on `account_id` hold an active
|
|
185
|
+
* global role_grant for `role`?
|
|
186
|
+
*
|
|
187
|
+
* Symmetric with `query_role_grant_has_role` but keyed on the account
|
|
188
|
+
* instead of a single actor — for surfaces with `auth: actor: 'none'`
|
|
189
|
+
* that don't load `auth.role_grants` and can't use the in-memory
|
|
190
|
+
* `has_scoped_role` predicate. Joins `role_grant` → `actor`; matches
|
|
191
|
+
* only global role_grants (`scope_id IS NULL`) since the use case is
|
|
192
|
+
* "is the caller's account broadly admin", not scope-aware.
|
|
193
|
+
*
|
|
194
|
+
* Fast under the existing `idx_role_grant_actor` index — the inner
|
|
195
|
+
* `actor_id IN (...)` subquery is index-scan, and the outer EXISTS
|
|
196
|
+
* stops at the first match.
|
|
197
|
+
*
|
|
198
|
+
* @param deps - query dependencies
|
|
199
|
+
* @param account_id - the account to check
|
|
200
|
+
* @param role - the role to check for (e.g. `ROLE_ADMIN`)
|
|
201
|
+
* @returns `true` if any actor on the account has an active global role_grant for `role`
|
|
202
|
+
*/
|
|
203
|
+
export const query_account_has_global_role = async (deps, account_id, role) => {
|
|
204
|
+
const row = await deps.db.query_one(`SELECT EXISTS(
|
|
205
|
+
SELECT 1 FROM role_grant
|
|
206
|
+
WHERE actor_id IN (SELECT id FROM actor WHERE account_id = $1)
|
|
207
|
+
AND role = $2
|
|
208
|
+
AND scope_id IS NULL
|
|
209
|
+
AND revoked_at IS NULL
|
|
210
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
211
|
+
) AS exists`, [account_id, role]);
|
|
212
|
+
return row?.exists ?? false;
|
|
213
|
+
};
|
|
183
214
|
/**
|
|
184
215
|
* List all role_grants for an actor (including revoked/expired).
|
|
185
216
|
*/
|
|
@@ -63,7 +63,7 @@ export type BuiltinRole = z.infer<typeof BuiltinRole>;
|
|
|
63
63
|
* flows. Only useful for diagnostic snapshotting.
|
|
64
64
|
*
|
|
65
65
|
* Builtins (`keeper`, `admin`) ship preconfigured in
|
|
66
|
-
* `
|
|
66
|
+
* `builtin_role_specs_by_name`.
|
|
67
67
|
*/
|
|
68
68
|
export interface RoleSpec {
|
|
69
69
|
/** Unique role name. Must match `RoleName` regex; collisions with builtins throw. */
|
|
@@ -105,7 +105,7 @@ export interface RoleSpec {
|
|
|
105
105
|
* no effect on already-built role schemas (the factory copies entries
|
|
106
106
|
* into a fresh `Map`).
|
|
107
107
|
*/
|
|
108
|
-
export declare const
|
|
108
|
+
export declare const builtin_role_specs_by_name: ReadonlyMap<string, RoleSpec>;
|
|
109
109
|
/** Optional registries to validate `RoleSpec` cross-axis fields against at construction time. */
|
|
110
110
|
export interface CreateRoleSchemaOptions {
|
|
111
111
|
/** Pass `create_credential_type_schema()` to validate `RoleSpec.required_credential_types` entries. */
|
package/dist/auth/role_schema.js
CHANGED
|
@@ -44,7 +44,7 @@ export const BuiltinRole = z.enum(BUILTIN_ROLES);
|
|
|
44
44
|
* no effect on already-built role schemas (the factory copies entries
|
|
45
45
|
* into a fresh `Map`).
|
|
46
46
|
*/
|
|
47
|
-
export const
|
|
47
|
+
export const builtin_role_specs_by_name = new Map([
|
|
48
48
|
[
|
|
49
49
|
ROLE_KEEPER,
|
|
50
50
|
{
|
|
@@ -139,7 +139,7 @@ export const create_role_schema = (consumer_roles, options = {}) => {
|
|
|
139
139
|
if (!parsed.success) {
|
|
140
140
|
throw new Error(`Invalid role name "${spec.name}": ${parsed.error.issues[0].message}`);
|
|
141
141
|
}
|
|
142
|
-
if (
|
|
142
|
+
if (builtin_role_specs_by_name.has(spec.name)) {
|
|
143
143
|
throw new Error(`App role "${spec.name}" collides with builtin role`);
|
|
144
144
|
}
|
|
145
145
|
if (seen.has(spec.name)) {
|
|
@@ -150,7 +150,7 @@ export const create_role_schema = (consumer_roles, options = {}) => {
|
|
|
150
150
|
validate_registry_membership(spec.name, 'applicable_scope_kinds', spec.applicable_scope_kinds, scope_kinds_registry);
|
|
151
151
|
validate_registry_membership(spec.name, 'grant_paths', spec.grant_paths, grant_paths_registry);
|
|
152
152
|
}
|
|
153
|
-
const role_specs = new Map(
|
|
153
|
+
const role_specs = new Map(builtin_role_specs_by_name);
|
|
154
154
|
for (const spec of consumer_roles) {
|
|
155
155
|
role_specs.set(spec.name, spec);
|
|
156
156
|
}
|
|
@@ -44,7 +44,7 @@ export interface SelfServiceRoleActionsOptions {
|
|
|
44
44
|
/**
|
|
45
45
|
* Optional override allowlist of role strings eligible for
|
|
46
46
|
* self-service. When omitted, eligibility is derived from
|
|
47
|
-
* `roles.role_specs` (or `
|
|
47
|
+
* `roles.role_specs` (or `builtin_role_specs_by_name` when `roles`
|
|
48
48
|
* is also omitted) by selecting every role whose
|
|
49
49
|
* `RoleSpec.grant_paths` includes `'self_service'`. Pass an empty
|
|
50
50
|
* array to lock the surface down (every call comes back as
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { rpc_action } from '../actions/action_rpc.js';
|
|
40
40
|
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
41
|
-
import {
|
|
41
|
+
import { builtin_role_specs_by_name, list_roles_with_grant_path, } from './role_schema.js';
|
|
42
42
|
import { GRANT_PATH_SELF_SERVICE } from './grant_path_schema.js';
|
|
43
43
|
import { query_create_role_grant, query_revoke_role_grant } from './role_grant_queries.js';
|
|
44
44
|
import { is_role_grant_active } from './account_schema.js';
|
|
@@ -55,7 +55,7 @@ import { ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE, self_service_role_set_action_spec
|
|
|
55
55
|
* @throws Error at factory time if any `eligible_roles` entry is missing from `options.roles.role_specs`
|
|
56
56
|
*/
|
|
57
57
|
export const create_self_service_role_actions = (deps, options = {}) => {
|
|
58
|
-
const role_specs = options.roles?.role_specs ??
|
|
58
|
+
const role_specs = options.roles?.role_specs ?? builtin_role_specs_by_name;
|
|
59
59
|
const eligible = options.eligible_roles
|
|
60
60
|
? new Set(options.eligible_roles)
|
|
61
61
|
: new Set(list_roles_with_grant_path(role_specs, GRANT_PATH_SELF_SERVICE));
|
|
@@ -38,7 +38,7 @@ export interface SessionCookieOptions {
|
|
|
38
38
|
* - `sameSite: 'strict'` - Prevents CSRF
|
|
39
39
|
* - `httpOnly: true` - Prevents XSS access to cookie
|
|
40
40
|
*/
|
|
41
|
-
export declare const
|
|
41
|
+
export declare const session_cookie_options: SessionCookieOptions;
|
|
42
42
|
/**
|
|
43
43
|
* Configuration for a session cookie format.
|
|
44
44
|
*
|
|
@@ -31,7 +31,7 @@ const EXPIRES_AT_INTEGER_RE = /^\d+$/;
|
|
|
31
31
|
* - `sameSite: 'strict'` - Prevents CSRF
|
|
32
32
|
* - `httpOnly: true` - Prevents XSS access to cookie
|
|
33
33
|
*/
|
|
34
|
-
export const
|
|
34
|
+
export const session_cookie_options = {
|
|
35
35
|
path: '/',
|
|
36
36
|
httpOnly: true,
|
|
37
37
|
secure: true,
|
|
@@ -18,7 +18,7 @@ export declare const get_session_cookie: <T>(c: Context, options: SessionOptions
|
|
|
18
18
|
* `options.max_age` is the single source of truth for cookie lifetime: it
|
|
19
19
|
* drives both the embedded `expires_at` (via `create_session_cookie_value`)
|
|
20
20
|
* and the cookie's HTTP `Max-Age` attribute set here. Falls back to
|
|
21
|
-
* `
|
|
21
|
+
* `session_cookie_options.maxAge` (= `SESSION_AGE_MAX`) when unset.
|
|
22
22
|
* `options.cookie_options` cannot carry `maxAge` (omitted in the type) so
|
|
23
23
|
* the two values can't drift.
|
|
24
24
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
|
|
8
|
-
import {
|
|
8
|
+
import { session_cookie_options, process_session_cookie, create_session_cookie_value, } from './session_cookie.js';
|
|
9
9
|
import { generate_session_token, hash_session_token, AUTH_SESSION_LIFETIME_MS, query_create_session, query_session_enforce_limit, } from './session_queries.js';
|
|
10
10
|
/**
|
|
11
11
|
* Read the session cookie value from a request.
|
|
@@ -19,15 +19,15 @@ export const get_session_cookie = (c, options) => {
|
|
|
19
19
|
* `options.max_age` is the single source of truth for cookie lifetime: it
|
|
20
20
|
* drives both the embedded `expires_at` (via `create_session_cookie_value`)
|
|
21
21
|
* and the cookie's HTTP `Max-Age` attribute set here. Falls back to
|
|
22
|
-
* `
|
|
22
|
+
* `session_cookie_options.maxAge` (= `SESSION_AGE_MAX`) when unset.
|
|
23
23
|
* `options.cookie_options` cannot carry `maxAge` (omitted in the type) so
|
|
24
24
|
* the two values can't drift.
|
|
25
25
|
*/
|
|
26
26
|
export const set_session_cookie = (c, value, options) => {
|
|
27
27
|
const cookie_options = {
|
|
28
|
-
...
|
|
28
|
+
...session_cookie_options,
|
|
29
29
|
...options.cookie_options,
|
|
30
|
-
maxAge: options.max_age ??
|
|
30
|
+
maxAge: options.max_age ?? session_cookie_options.maxAge,
|
|
31
31
|
};
|
|
32
32
|
setCookie(c, options.cookie_name, value, cookie_options);
|
|
33
33
|
};
|
|
@@ -36,7 +36,7 @@ export const set_session_cookie = (c, value, options) => {
|
|
|
36
36
|
*/
|
|
37
37
|
export const clear_session_cookie = (c, options) => {
|
|
38
38
|
const cookie_options = {
|
|
39
|
-
...
|
|
39
|
+
...session_cookie_options,
|
|
40
40
|
...options.cookie_options,
|
|
41
41
|
};
|
|
42
42
|
deleteCookie(c, options.cookie_name, cookie_options);
|
package/dist/rate_limiter.d.ts
CHANGED
|
@@ -43,9 +43,9 @@ export interface RateLimiterOptions {
|
|
|
43
43
|
max_keys?: number | null;
|
|
44
44
|
}
|
|
45
45
|
/** Default options for per-IP login rate limiting: 5 attempts per 15 minutes. */
|
|
46
|
-
export declare const
|
|
46
|
+
export declare const default_login_ip_rate_limit: RateLimiterOptions;
|
|
47
47
|
/** Default options for per-account login rate limiting: 10 attempts per 30 minutes. */
|
|
48
|
-
export declare const
|
|
48
|
+
export declare const default_login_account_rate_limit: RateLimiterOptions;
|
|
49
49
|
/**
|
|
50
50
|
* Default options for per-IP action-dispatcher rate limiting: 600 attempts
|
|
51
51
|
* per 15 minutes. Shared by the HTTP RPC and WebSocket action dispatchers
|
|
@@ -53,7 +53,7 @@ export declare const DEFAULT_LOGIN_ACCOUNT_RATE_LIMIT: RateLimiterOptions;
|
|
|
53
53
|
* scripts and egregious oracle probes, but well above human or normal
|
|
54
54
|
* automation pace. Tighten downstream for stricter deployments.
|
|
55
55
|
*/
|
|
56
|
-
export declare const
|
|
56
|
+
export declare const default_action_ip_rate_limit: RateLimiterOptions;
|
|
57
57
|
/**
|
|
58
58
|
* Default options for per-actor action-dispatcher rate limiting: 1200
|
|
59
59
|
* attempts per 15 minutes. Shared by the HTTP RPC and WebSocket action
|
|
@@ -61,7 +61,7 @@ export declare const DEFAULT_ACTION_IP_RATE_LIMIT: RateLimiterOptions;
|
|
|
61
61
|
* admin workflow; an oracle probing 10k addresses still finishes in
|
|
62
62
|
* ~2 hours, slow enough to surface in audit. Tighten downstream.
|
|
63
63
|
*/
|
|
64
|
-
export declare const
|
|
64
|
+
export declare const default_action_account_rate_limit: RateLimiterOptions;
|
|
65
65
|
/**
|
|
66
66
|
* Result of a rate limit check or record operation.
|
|
67
67
|
*/
|
|
@@ -139,7 +139,7 @@ export declare class RateLimiter {
|
|
|
139
139
|
/**
|
|
140
140
|
* Create a `RateLimiter` with sensible defaults for per-IP login protection.
|
|
141
141
|
*
|
|
142
|
-
* @param options - override individual options; unset fields use `
|
|
142
|
+
* @param options - override individual options; unset fields use `default_login_ip_rate_limit`
|
|
143
143
|
*/
|
|
144
144
|
export declare const create_rate_limiter: (options?: Partial<RateLimiterOptions>) => RateLimiter;
|
|
145
145
|
/**
|
package/dist/rate_limiter.js
CHANGED
|
@@ -17,14 +17,14 @@ import { ERROR_RATE_LIMIT_EXCEEDED } from './http/error_schemas.js';
|
|
|
17
17
|
*/
|
|
18
18
|
export const DEFAULT_RATE_LIMITER_MAX_KEYS = 100_000;
|
|
19
19
|
/** Default options for per-IP login rate limiting: 5 attempts per 15 minutes. */
|
|
20
|
-
export const
|
|
20
|
+
export const default_login_ip_rate_limit = {
|
|
21
21
|
max_attempts: 5,
|
|
22
22
|
window_ms: 15 * 60_000,
|
|
23
23
|
cleanup_interval_ms: 5 * 60_000,
|
|
24
24
|
max_keys: DEFAULT_RATE_LIMITER_MAX_KEYS,
|
|
25
25
|
};
|
|
26
26
|
/** Default options for per-account login rate limiting: 10 attempts per 30 minutes. */
|
|
27
|
-
export const
|
|
27
|
+
export const default_login_account_rate_limit = {
|
|
28
28
|
max_attempts: 10,
|
|
29
29
|
window_ms: 30 * 60_000,
|
|
30
30
|
cleanup_interval_ms: 5 * 60_000,
|
|
@@ -37,7 +37,7 @@ export const DEFAULT_LOGIN_ACCOUNT_RATE_LIMIT = {
|
|
|
37
37
|
* scripts and egregious oracle probes, but well above human or normal
|
|
38
38
|
* automation pace. Tighten downstream for stricter deployments.
|
|
39
39
|
*/
|
|
40
|
-
export const
|
|
40
|
+
export const default_action_ip_rate_limit = {
|
|
41
41
|
max_attempts: 600,
|
|
42
42
|
window_ms: 15 * 60_000,
|
|
43
43
|
cleanup_interval_ms: 5 * 60_000,
|
|
@@ -50,7 +50,7 @@ export const DEFAULT_ACTION_IP_RATE_LIMIT = {
|
|
|
50
50
|
* admin workflow; an oracle probing 10k addresses still finishes in
|
|
51
51
|
* ~2 hours, slow enough to surface in audit. Tighten downstream.
|
|
52
52
|
*/
|
|
53
|
-
export const
|
|
53
|
+
export const default_action_account_rate_limit = {
|
|
54
54
|
max_attempts: 1200,
|
|
55
55
|
window_ms: 15 * 60_000,
|
|
56
56
|
cleanup_interval_ms: 5 * 60_000,
|
|
@@ -203,10 +203,10 @@ export class RateLimiter {
|
|
|
203
203
|
/**
|
|
204
204
|
* Create a `RateLimiter` with sensible defaults for per-IP login protection.
|
|
205
205
|
*
|
|
206
|
-
* @param options - override individual options; unset fields use `
|
|
206
|
+
* @param options - override individual options; unset fields use `default_login_ip_rate_limit`
|
|
207
207
|
*/
|
|
208
208
|
export const create_rate_limiter = (options) => {
|
|
209
|
-
return new RateLimiter({ ...
|
|
209
|
+
return new RateLimiter({ ...default_login_ip_rate_limit, ...options });
|
|
210
210
|
};
|
|
211
211
|
/**
|
|
212
212
|
* Build a 429 rate-limit-exceeded JSON response with `Retry-After` header.
|
|
@@ -27,7 +27,7 @@ export declare const AUDIT_LOG_CHANNEL = "audit_log";
|
|
|
27
27
|
* (matched by the blake3 session hash in `event.metadata.session_id`) — closing
|
|
28
28
|
* all of a user's streams for a single-session revoke would be over-aggressive.
|
|
29
29
|
*/
|
|
30
|
-
export declare const
|
|
30
|
+
export declare const disconnect_event_types: ReadonlySet<string>;
|
|
31
31
|
/**
|
|
32
32
|
* Create an audit event handler that closes SSE streams on auth changes.
|
|
33
33
|
*
|
|
@@ -69,7 +69,7 @@ export interface AuditLogSse {
|
|
|
69
69
|
* One spec per `AUDIT_EVENT_TYPES` entry, all sharing the `AuditLogEventJson` params schema.
|
|
70
70
|
* Pass to `create_app_server`'s `event_specs` for surface generation and DEV validation.
|
|
71
71
|
*/
|
|
72
|
-
export declare const
|
|
72
|
+
export declare const audit_log_event_specs: Array<EventSpec>;
|
|
73
73
|
/**
|
|
74
74
|
* Default max concurrent SSE subscribers per session scope for the audit log.
|
|
75
75
|
*
|
|
@@ -102,7 +102,7 @@ export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
|
|
|
102
102
|
* create_audit_log_route_specs({stream: audit_sse});
|
|
103
103
|
*
|
|
104
104
|
* // In create_app_server options:
|
|
105
|
-
* event_specs:
|
|
105
|
+
* event_specs: audit_log_event_specs,
|
|
106
106
|
* ```
|
|
107
107
|
*/
|
|
108
108
|
export declare const create_audit_log_sse: (options: {
|
|
@@ -25,7 +25,7 @@ export const AUDIT_LOG_CHANNEL = 'audit_log';
|
|
|
25
25
|
* (matched by the blake3 session hash in `event.metadata.session_id`) — closing
|
|
26
26
|
* all of a user's streams for a single-session revoke would be over-aggressive.
|
|
27
27
|
*/
|
|
28
|
-
export const
|
|
28
|
+
export const disconnect_event_types = new Set([
|
|
29
29
|
'role_grant_revoke', // role revoked — user lost access
|
|
30
30
|
'session_revoke', // single session revoked — close only that stream
|
|
31
31
|
'session_revoke_all', // all sessions invalidated — user should be kicked
|
|
@@ -51,7 +51,7 @@ export const DISCONNECT_EVENT_TYPES = new Set([
|
|
|
51
51
|
*/
|
|
52
52
|
export const create_sse_auth_guard = (registry, required_role, log) => {
|
|
53
53
|
return (event) => {
|
|
54
|
-
if (!
|
|
54
|
+
if (!disconnect_event_types.has(event.event_type))
|
|
55
55
|
return;
|
|
56
56
|
// Only act on successful revocations. Failed attempts carry
|
|
57
57
|
// attacker-controlled identifiers (e.g., session_revoke with outcome=failure
|
|
@@ -98,7 +98,7 @@ export const create_sse_auth_guard = (registry, required_role, log) => {
|
|
|
98
98
|
* One spec per `AUDIT_EVENT_TYPES` entry, all sharing the `AuditLogEventJson` params schema.
|
|
99
99
|
* Pass to `create_app_server`'s `event_specs` for surface generation and DEV validation.
|
|
100
100
|
*/
|
|
101
|
-
export const
|
|
101
|
+
export const audit_log_event_specs = AUDIT_EVENT_TYPES.map((event_type) => ({
|
|
102
102
|
method: event_type,
|
|
103
103
|
params: AuditLogEventJson,
|
|
104
104
|
description: `Audit log: ${event_type.replaceAll('_', ' ')}`,
|
|
@@ -136,7 +136,7 @@ export const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
|
|
|
136
136
|
* create_audit_log_route_specs({stream: audit_sse});
|
|
137
137
|
*
|
|
138
138
|
* // In create_app_server options:
|
|
139
|
-
* event_specs:
|
|
139
|
+
* event_specs: audit_log_event_specs,
|
|
140
140
|
* ```
|
|
141
141
|
*/
|
|
142
142
|
export const create_audit_log_sse = (options) => {
|
|
@@ -66,7 +66,7 @@ export interface CreateAppBackendOptions {
|
|
|
66
66
|
* Audit-log config for consumer event-type extensions. Built once at
|
|
67
67
|
* startup via `create_audit_log_config({extra_events})` and captured
|
|
68
68
|
* inside `AppDeps.audit` so consumer handlers cannot silently fall
|
|
69
|
-
* back to the builtin config. Omit to use `
|
|
69
|
+
* back to the builtin config. Omit to use `builtin_audit_log_config`
|
|
70
70
|
* (no extra events).
|
|
71
71
|
*/
|
|
72
72
|
audit_log_config?: AuditLogConfig;
|
|
@@ -76,7 +76,7 @@ export interface CreateAppBackendOptions {
|
|
|
76
76
|
* (`namespace`, `name`, `sequence`); order is append-only so forward-only
|
|
77
77
|
* guarantees hold per-namespace.
|
|
78
78
|
*
|
|
79
|
-
* Names in `
|
|
79
|
+
* Names in `reserved_migration_namespaces` (currently `['fuz_auth']`) are
|
|
80
80
|
* rejected at startup. Omit for no extra namespaces. This is the only
|
|
81
81
|
* place to splice consumer migrations — DB init belongs to the backend
|
|
82
82
|
* lifecycle, not server assembly.
|
|
@@ -92,7 +92,7 @@ export interface CreateAppBackendOptions {
|
|
|
92
92
|
*
|
|
93
93
|
* @param options - keyring, password deps, optional database URL, and optional `migration_namespaces`
|
|
94
94
|
* @returns app backend with deps, database metadata, and combined migration results
|
|
95
|
-
* @throws Error if `migration_namespaces` contains a namespace in `
|
|
95
|
+
* @throws Error if `migration_namespaces` contains a namespace in `reserved_migration_namespaces`
|
|
96
96
|
*/
|
|
97
97
|
export declare const create_app_backend: (options: CreateAppBackendOptions) => Promise<AppBackend>;
|
|
98
98
|
//# sourceMappingURL=app_backend.d.ts.map
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
14
14
|
import { create_audit_emitter } from '../auth/audit_emitter.js';
|
|
15
15
|
import { run_migrations } from '../db/migrate.js';
|
|
16
|
-
import {
|
|
16
|
+
import { auth_migration_ns, reserved_migration_namespaces } from '../auth/migrations.js';
|
|
17
17
|
import { create_db } from '../db/create_db.js';
|
|
18
18
|
/**
|
|
19
19
|
* Initialize the backend: database + auth migrations + deps.
|
|
@@ -24,7 +24,7 @@ import { create_db } from '../db/create_db.js';
|
|
|
24
24
|
*
|
|
25
25
|
* @param options - keyring, password deps, optional database URL, and optional `migration_namespaces`
|
|
26
26
|
* @returns app backend with deps, database metadata, and combined migration results
|
|
27
|
-
* @throws Error if `migration_namespaces` contains a namespace in `
|
|
27
|
+
* @throws Error if `migration_namespaces` contains a namespace in `reserved_migration_namespaces`
|
|
28
28
|
*/
|
|
29
29
|
export const create_app_backend = async (options) => {
|
|
30
30
|
const { database_url, keyring, password, stat, read_text_file, delete_file } = options;
|
|
@@ -32,13 +32,13 @@ export const create_app_backend = async (options) => {
|
|
|
32
32
|
const { db, close, db_type, db_name } = await create_db(database_url);
|
|
33
33
|
if (options.migration_namespaces?.length) {
|
|
34
34
|
for (const ns of options.migration_namespaces) {
|
|
35
|
-
if (
|
|
35
|
+
if (reserved_migration_namespaces.includes(ns.namespace)) {
|
|
36
36
|
throw new Error(`Migration namespace "${ns.namespace}" is reserved by fuz_app — choose a different namespace`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
const migration_results = await run_migrations(db, [
|
|
41
|
-
|
|
41
|
+
auth_migration_ns,
|
|
42
42
|
...(options.migration_namespaces ?? []),
|
|
43
43
|
]);
|
|
44
44
|
const audit = create_audit_emitter({
|
|
@@ -136,7 +136,7 @@ export interface AppServerOptions {
|
|
|
136
136
|
* When truthy, creates an `AuditLogSse` instance internally, appends the SSE
|
|
137
137
|
* listener to `backend.deps.audit.on_event_chain` (composing with the
|
|
138
138
|
* consumer's `on_audit_event` callback rather than rebuilding `AppDeps`), and
|
|
139
|
-
* auto-includes `
|
|
139
|
+
* auto-includes `audit_log_event_specs` in the surface. The result is exposed
|
|
140
140
|
* on `AppServerContext` (for route factories) and `AppServer` (for the caller).
|
|
141
141
|
*
|
|
142
142
|
* Pass `true` for defaults (admin role), or `{role: 'custom'}` for a custom role.
|
|
@@ -11,10 +11,10 @@ import { Hono } from 'hono';
|
|
|
11
11
|
import { logger } from 'hono/logger';
|
|
12
12
|
import { bodyLimit } from 'hono/body-limit';
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
-
import {
|
|
15
|
-
import { create_audit_log_sse,
|
|
14
|
+
import { session_cookie_options, } from '../auth/session_cookie.js';
|
|
15
|
+
import { create_audit_log_sse, audit_log_event_specs, } from '../realtime/sse_auth_guard.js';
|
|
16
16
|
import { query_app_settings_load } from '../auth/app_settings_queries.js';
|
|
17
|
-
import { create_rate_limiter,
|
|
17
|
+
import { create_rate_limiter, default_login_account_rate_limit, default_action_account_rate_limit, default_action_ip_rate_limit, } from '../rate_limiter.js';
|
|
18
18
|
// Side-effect import: augments Hono's ContextVariableMap so consumers
|
|
19
19
|
// that import app_server get type-safe c.get('auth_session_id') etc.
|
|
20
20
|
import '../hono_context.js';
|
|
@@ -53,19 +53,19 @@ export const create_app_server = async (options) => {
|
|
|
53
53
|
// Rate limiter defaults (undefined = default, null = disable)
|
|
54
54
|
const ip_rate_limiter = options.ip_rate_limiter === undefined ? create_rate_limiter() : options.ip_rate_limiter;
|
|
55
55
|
const login_account_rate_limiter = options.login_account_rate_limiter === undefined
|
|
56
|
-
? create_rate_limiter(
|
|
56
|
+
? create_rate_limiter(default_login_account_rate_limit)
|
|
57
57
|
: options.login_account_rate_limiter;
|
|
58
58
|
const signup_account_rate_limiter = options.signup_account_rate_limiter === undefined
|
|
59
|
-
? create_rate_limiter(
|
|
59
|
+
? create_rate_limiter(default_login_account_rate_limit)
|
|
60
60
|
: options.signup_account_rate_limiter;
|
|
61
61
|
const bearer_ip_rate_limiter = options.bearer_ip_rate_limiter === undefined
|
|
62
62
|
? create_rate_limiter()
|
|
63
63
|
: options.bearer_ip_rate_limiter;
|
|
64
64
|
const action_ip_rate_limiter = options.action_ip_rate_limiter === undefined
|
|
65
|
-
? create_rate_limiter(
|
|
65
|
+
? create_rate_limiter(default_action_ip_rate_limit)
|
|
66
66
|
: options.action_ip_rate_limiter;
|
|
67
67
|
const action_account_rate_limiter = options.action_account_rate_limiter === undefined
|
|
68
|
-
? create_rate_limiter(
|
|
68
|
+
? create_rate_limiter(default_action_account_rate_limit)
|
|
69
69
|
: options.action_account_rate_limiter;
|
|
70
70
|
// Factory-managed audit SSE — appends a listener to the bound emitter's
|
|
71
71
|
// chain so SSE fan-out runs alongside the consumer's `on_audit_event`
|
|
@@ -156,7 +156,7 @@ export const create_app_server = async (options) => {
|
|
|
156
156
|
: middleware_specs;
|
|
157
157
|
const all_event_specs = [
|
|
158
158
|
...(options.event_specs ?? []),
|
|
159
|
-
...(audit_sse ?
|
|
159
|
+
...(audit_sse ? audit_log_event_specs : []),
|
|
160
160
|
];
|
|
161
161
|
const surface_spec = create_app_surface_spec({
|
|
162
162
|
middleware_specs: surface_middleware,
|
|
@@ -176,11 +176,11 @@ export const create_app_server = async (options) => {
|
|
|
176
176
|
message: 'Session cookie secure=false — cookies sent over HTTP',
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
|
-
if (cookie_opts.sameSite && cookie_opts.sameSite !==
|
|
179
|
+
if (cookie_opts.sameSite && cookie_opts.sameSite !== session_cookie_options.sameSite) {
|
|
180
180
|
config_diagnostics.push({
|
|
181
181
|
level: 'warning',
|
|
182
182
|
category: 'security',
|
|
183
|
-
message: `Session cookie sameSite='${cookie_opts.sameSite}' — weakened from default '${
|
|
183
|
+
message: `Session cookie sameSite='${cookie_opts.sameSite}' — weakened from default '${session_cookie_options.sameSite}'`,
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
if (cookie_opts.httpOnly === false) {
|