@fuzdev/fuz_app 0.107.0 → 0.108.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/auth/account_route_schema.d.ts +10 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +10 -1
- package/dist/auth/account_routes.d.ts +18 -4
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +32 -29
- package/dist/auth/api_token_queries.d.ts +4 -0
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +5 -1
- package/dist/auth/bearer_auth.d.ts +9 -6
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +12 -21
- package/dist/auth/bootstrap_routes.d.ts +12 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -9
- package/dist/auth/middleware.d.ts +0 -3
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +11 -10
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +24 -0
- package/dist/auth/session_queries.d.ts +13 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +14 -3
- package/dist/auth/signup_routes.d.ts +10 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -11
- package/dist/http/error_schemas.d.ts +1 -1
- package/dist/http/error_schemas.js +1 -1
- package/dist/rate_limiter.d.ts +36 -3
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +36 -3
- package/dist/server/app_server.d.ts +27 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +30 -20
- package/dist/server/app_server_context.d.ts +10 -2
- package/dist/server/app_server_context.d.ts.map +1 -1
- package/dist/testing/CLAUDE.md +21 -8
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +6 -4
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +9 -8
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +59 -0
- package/dist/testing/cross_backend/session_cap.d.ts +19 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/session_cap.js +93 -0
- package/dist/testing/middleware.d.ts +2 -5
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +5 -5
- package/dist/testing/rate_limiting.d.ts +10 -16
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +11 -95
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +3 -1
- package/dist/testing/testing_rate_limiter.d.ts +7 -1
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -1
- package/dist/testing/testing_rate_limiter.js +7 -1
- package/package.json +1 -1
|
@@ -79,7 +79,16 @@ export declare const PasswordChangeOutput: z.ZodObject<{
|
|
|
79
79
|
tokens_revoked: z.ZodNumber;
|
|
80
80
|
}, z.core.$strict>;
|
|
81
81
|
export type PasswordChangeOutput = z.infer<typeof PasswordChangeOutput>;
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Default maximum sessions per account.
|
|
84
|
+
*
|
|
85
|
+
* The Rust twin pins the same 5 as a `const` (`fuz_auth`'s
|
|
86
|
+
* `DEFAULT_MAX_SESSIONS`) rather than a route-state field, because its
|
|
87
|
+
* `AccountRouteState` has no `Default` and a new field would break every
|
|
88
|
+
* exhaustive literal in every consumer workspace. So this stays the
|
|
89
|
+
* configurable side of the pair (`null` disables), and the two ship the same
|
|
90
|
+
* number — retuning means editing both.
|
|
91
|
+
*/
|
|
83
92
|
export declare const DEFAULT_MAX_SESSIONS = 5;
|
|
84
93
|
/** Default maximum API tokens per account. */
|
|
85
94
|
export declare const DEFAULT_MAX_TOKENS = 10;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE
|
|
1
|
+
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAoD1B,CAAC"}
|
|
@@ -56,7 +56,16 @@ export const PasswordChangeOutput = z.strictObject({
|
|
|
56
56
|
sessions_revoked: z.number(),
|
|
57
57
|
tokens_revoked: z.number()
|
|
58
58
|
});
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Default maximum sessions per account.
|
|
61
|
+
*
|
|
62
|
+
* The Rust twin pins the same 5 as a `const` (`fuz_auth`'s
|
|
63
|
+
* `DEFAULT_MAX_SESSIONS`) rather than a route-state field, because its
|
|
64
|
+
* `AccountRouteState` has no `Default` and a new field would break every
|
|
65
|
+
* exhaustive literal in every consumer workspace. So this stays the
|
|
66
|
+
* configurable side of the pair (`null` disables), and the two ship the same
|
|
67
|
+
* number — retuning means editing both.
|
|
68
|
+
*/
|
|
60
69
|
export const DEFAULT_MAX_SESSIONS = 5;
|
|
61
70
|
/** Default maximum API tokens per account. */
|
|
62
71
|
export const DEFAULT_MAX_TOKENS = 10;
|
|
@@ -69,20 +69,34 @@ export declare const DEFAULT_LOGIN_FAIL_FLOOR_MS = 250;
|
|
|
69
69
|
*/
|
|
70
70
|
export declare const DEFAULT_LOGIN_FAIL_JITTER_MS = 25;
|
|
71
71
|
/**
|
|
72
|
-
* Shared options for route factories that create sessions
|
|
72
|
+
* Shared options for route factories that create sessions.
|
|
73
73
|
*
|
|
74
74
|
* Extended by `AccountRouteOptions` and `SignupRouteOptions`.
|
|
75
75
|
* Consumers can destructure these from `AppServerContext` once and spread into multiple factories.
|
|
76
|
+
*
|
|
77
|
+
* The per-IP limiter is deliberately *not* here. Each auth surface names its
|
|
78
|
+
* own (`login_ip_rate_limiter`, `signup_ip_rate_limiter`,
|
|
79
|
+
* `bootstrap_ip_rate_limiter`) because the buckets are monotone within their
|
|
80
|
+
* window — see `RateLimiter.reset`. A shared base field made one instance the
|
|
81
|
+
* path of least resistance, which let a failure on any surface spend the
|
|
82
|
+
* budget that bounds guessing on every other one.
|
|
76
83
|
*/
|
|
77
84
|
export interface AuthSessionRouteOptions {
|
|
78
85
|
session_options: SessionOptions<string>;
|
|
79
|
-
/** Rate limiter for auth attempts, keyed by client IP. Pass `null` to disable. */
|
|
80
|
-
ip_rate_limiter: RateLimiter | null;
|
|
81
86
|
}
|
|
82
87
|
/**
|
|
83
88
|
* Per-factory configuration for account route specs.
|
|
84
89
|
*/
|
|
85
90
|
export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
91
|
+
/**
|
|
92
|
+
* Rate limiter for login + password-change attempts, keyed by client IP.
|
|
93
|
+
* Pass `null` to disable. The distributed-spray backstop: login is the one
|
|
94
|
+
* surface that guesses *other* accounts' credentials, so this bucket is
|
|
95
|
+
* never refunded on success (see `RateLimiter.reset`). Password change
|
|
96
|
+
* shares it — it is password-bearing on the same account grain, and the
|
|
97
|
+
* Rust spine shares the same instance across both.
|
|
98
|
+
*/
|
|
99
|
+
login_ip_rate_limiter: RateLimiter | null;
|
|
86
100
|
/** Rate limiter for login attempts, keyed by submitted username. Pass `null` to disable. */
|
|
87
101
|
login_account_rate_limiter: RateLimiter | null;
|
|
88
102
|
/** Max active sessions per account. Evicts oldest on login. Default 5, `null` disables. */
|
|
@@ -130,7 +144,7 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
|
130
144
|
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
131
145
|
*
|
|
132
146
|
* @param deps - stateless capabilities (keyring, password, log)
|
|
133
|
-
* @param options - per-factory configuration (session_options,
|
|
147
|
+
* @param options - per-factory configuration (session_options, login_ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
134
148
|
* @returns route specs (not yet applied to Hono)
|
|
135
149
|
*/
|
|
136
150
|
export declare const create_account_route_specs: (deps: RouteFactoryDeps, options: AccountRouteOptions) => Array<RouteSpec>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA4B1D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGxE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C
|
|
1
|
+
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA4B1D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGxE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE;;;;;;;OAOG;IACH,qBAAqB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1C,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CA6RjB,CAAC"}
|
|
@@ -155,12 +155,12 @@ const login_fail_delay = (floor_ms, jitter_ms) => {
|
|
|
155
155
|
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
156
156
|
*
|
|
157
157
|
* @param deps - stateless capabilities (keyring, password, log)
|
|
158
|
-
* @param options - per-factory configuration (session_options,
|
|
158
|
+
* @param options - per-factory configuration (session_options, login_ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
159
159
|
* @returns route specs (not yet applied to Hono)
|
|
160
160
|
*/
|
|
161
161
|
export const create_account_route_specs = (deps, options) => {
|
|
162
162
|
const { keyring, password } = deps;
|
|
163
|
-
const { session_options,
|
|
163
|
+
const { session_options, login_ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, connection_closer = null, bootstrap_status } = options;
|
|
164
164
|
const [verify_shape, login_shape, logout_shape, password_shape] = create_account_route_shapes({
|
|
165
165
|
login_account_rate_limited: login_account_rate_limiter !== null
|
|
166
166
|
});
|
|
@@ -181,9 +181,9 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
181
181
|
...login_shape,
|
|
182
182
|
handler: async (c, route) => {
|
|
183
183
|
// Per-IP rate limit check (before any DB/password work)
|
|
184
|
-
const ip =
|
|
185
|
-
if (
|
|
186
|
-
const check =
|
|
184
|
+
const ip = login_ip_rate_limiter ? get_client_ip(c) : null;
|
|
185
|
+
if (login_ip_rate_limiter && ip) {
|
|
186
|
+
const check = login_ip_rate_limiter.check(ip);
|
|
187
187
|
if (!check.allowed) {
|
|
188
188
|
return rate_limit_exceeded_response(c, check.retry_after);
|
|
189
189
|
}
|
|
@@ -214,8 +214,8 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
214
214
|
// enumeration prevention: verify_dummy equalizes Argon2id timing;
|
|
215
215
|
// login_fail_delay equalizes every other path difference.
|
|
216
216
|
await password.verify_dummy(pw);
|
|
217
|
-
if (
|
|
218
|
-
|
|
217
|
+
if (login_ip_rate_limiter && ip)
|
|
218
|
+
login_ip_rate_limiter.record(ip);
|
|
219
219
|
if (login_account_rate_limiter)
|
|
220
220
|
login_account_rate_limiter.record(account_rate_key);
|
|
221
221
|
deps.audit.emit(route, {
|
|
@@ -229,8 +229,8 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
229
229
|
}
|
|
230
230
|
const valid = await password.verify_password(pw, account.password_hash);
|
|
231
231
|
if (!valid) {
|
|
232
|
-
if (
|
|
233
|
-
|
|
232
|
+
if (login_ip_rate_limiter && ip)
|
|
233
|
+
login_ip_rate_limiter.record(ip);
|
|
234
234
|
if (login_account_rate_limiter)
|
|
235
235
|
login_account_rate_limiter.record(account_rate_key);
|
|
236
236
|
deps.audit.emit(route, {
|
|
@@ -243,9 +243,11 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
243
243
|
await delay;
|
|
244
244
|
return c.json({ error: ERROR_INVALID_CREDENTIALS }, 401);
|
|
245
245
|
}
|
|
246
|
-
// Successful login —
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
// Successful login — clear the account-grain failure history only.
|
|
247
|
+
// The IP aggregate is deliberately left standing: it is the
|
|
248
|
+
// distributed-spray backstop, so a success on one account must not
|
|
249
|
+
// refund the budget shared by every other account reachable from
|
|
250
|
+
// that address. See `RateLimiter.reset`.
|
|
249
251
|
if (login_account_rate_limiter)
|
|
250
252
|
login_account_rate_limiter.reset(account_rate_key);
|
|
251
253
|
await create_session_and_set_cookie({
|
|
@@ -305,9 +307,9 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
305
307
|
...password_shape,
|
|
306
308
|
handler: async (c, route) => {
|
|
307
309
|
// per-IP rate limit check (before argon2 work)
|
|
308
|
-
const ip =
|
|
309
|
-
if (
|
|
310
|
-
const check =
|
|
310
|
+
const ip = login_ip_rate_limiter ? get_client_ip(c) : null;
|
|
311
|
+
if (login_ip_rate_limiter && ip) {
|
|
312
|
+
const check = login_ip_rate_limiter.check(ip);
|
|
311
313
|
if (!check.allowed) {
|
|
312
314
|
return rate_limit_exceeded_response(c, check.retry_after);
|
|
313
315
|
}
|
|
@@ -325,8 +327,8 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
325
327
|
}
|
|
326
328
|
const valid = await password.verify_password(current_password, ctx.account.password_hash);
|
|
327
329
|
if (!valid) {
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
+
if (login_ip_rate_limiter && ip)
|
|
331
|
+
login_ip_rate_limiter.record(ip);
|
|
330
332
|
if (login_account_rate_limiter)
|
|
331
333
|
login_account_rate_limiter.record(ctx.account.id);
|
|
332
334
|
deps.audit.emit(route, {
|
|
@@ -350,16 +352,17 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
350
352
|
// incidental; password is account-level state).
|
|
351
353
|
const updated = await query_update_account_password(route, ctx.account.id, new_hash, null, ctx.account.password_hash);
|
|
352
354
|
// Verify-success contract — the caller proved knowledge, so wipe
|
|
353
|
-
// their failure history. The race-loser branch below
|
|
354
|
-
// one on top of the wiped slate so net cost stays 1
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
355
|
+
// their account-grain failure history. The race-loser branch below
|
|
356
|
+
// re-records one on top of the wiped slate so net cost stays 1 on
|
|
357
|
+
// that bucket. Deferring from "after verify" to "after UPDATE
|
|
358
|
+
// settled" is what closes the S1 bypass — a throw between reset
|
|
359
|
+
// and the UPDATE could have wiped an attacker's budget.
|
|
360
|
+
//
|
|
361
|
+
// The IP aggregate is deliberately not cleared: it is the same
|
|
362
|
+
// instance login uses, so refunding it here reopens the spray path
|
|
363
|
+
// through a route that only needs the caller's *own* password. Its
|
|
364
|
+
// record on the race-loser branch below therefore accumulates
|
|
365
|
+
// rather than netting out, which is the intent.
|
|
363
366
|
if (login_account_rate_limiter)
|
|
364
367
|
login_account_rate_limiter.reset(ctx.account.id);
|
|
365
368
|
if (!updated) {
|
|
@@ -370,8 +373,8 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
370
373
|
// audit log can distinguish "user typoed" from "two clients
|
|
371
374
|
// raced." Sessions/tokens were already revoked by the winner;
|
|
372
375
|
// no cookie clear here either.
|
|
373
|
-
if (
|
|
374
|
-
|
|
376
|
+
if (login_ip_rate_limiter && ip)
|
|
377
|
+
login_ip_rate_limiter.record(ip);
|
|
375
378
|
if (login_account_rate_limiter)
|
|
376
379
|
login_account_rate_limiter.record(ctx.account.id);
|
|
377
380
|
deps.audit.emit(route, {
|
|
@@ -82,6 +82,10 @@ export declare const query_api_token_list_for_account: (deps: QueryDeps, account
|
|
|
82
82
|
* `(account_id, credential_kind)` before count/evict/insert — see the matching
|
|
83
83
|
* note on `query_session_enforce_limit`.
|
|
84
84
|
*
|
|
85
|
+
* Ordering carries the same `id DESC` stability tie-breaker, with the same
|
|
86
|
+
* caveat, as `query_session_enforce_limit` — deterministic survivors under a
|
|
87
|
+
* `created_at` tie, not a guarantee that the row just inserted survives.
|
|
88
|
+
*
|
|
85
89
|
* @param deps - query dependencies (must be transaction-scoped)
|
|
86
90
|
* @param account_id - the account to enforce the limit for
|
|
87
91
|
* @param max_tokens - maximum number of tokens to keep
|
|
@@ -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;AAErD,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;;;;;;;;;;;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;;;;;GAKG;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
|
|
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;AAErD,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;;;;;;;;;;;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;;;;;GAKG;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"}
|
|
@@ -125,6 +125,10 @@ export const query_api_token_list_for_account = async (deps, account_id) => {
|
|
|
125
125
|
* `(account_id, credential_kind)` before count/evict/insert — see the matching
|
|
126
126
|
* note on `query_session_enforce_limit`.
|
|
127
127
|
*
|
|
128
|
+
* Ordering carries the same `id DESC` stability tie-breaker, with the same
|
|
129
|
+
* caveat, as `query_session_enforce_limit` — deterministic survivors under a
|
|
130
|
+
* `created_at` tie, not a guarantee that the row just inserted survives.
|
|
131
|
+
*
|
|
128
132
|
* @param deps - query dependencies (must be transaction-scoped)
|
|
129
133
|
* @param account_id - the account to enforce the limit for
|
|
130
134
|
* @param max_tokens - maximum number of tokens to keep
|
|
@@ -136,7 +140,7 @@ export const query_api_token_enforce_limit = async (deps, account_id, max_tokens
|
|
|
136
140
|
WHERE id IN (
|
|
137
141
|
SELECT id FROM api_token
|
|
138
142
|
WHERE account_id = $1
|
|
139
|
-
ORDER BY created_at DESC
|
|
143
|
+
ORDER BY created_at DESC, id DESC
|
|
140
144
|
OFFSET $2
|
|
141
145
|
) RETURNING id`, [account_id, max_tokens]);
|
|
142
146
|
return rows.length;
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
import type { MiddlewareHandler } from 'hono';
|
|
18
18
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
19
19
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
20
|
-
import { type RateLimiter } from '../rate_limiter.js';
|
|
21
20
|
/**
|
|
22
21
|
* Create middleware that authenticates via bearer token.
|
|
23
22
|
*
|
|
@@ -37,14 +36,18 @@ import { type RateLimiter } from '../rate_limiter.js';
|
|
|
37
36
|
* (e.g. by session middleware). Acting-actor resolution + `RequestContext`
|
|
38
37
|
* construction are deferred to the dispatcher's authorization phase.
|
|
39
38
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* There is deliberately **no rate limit on this path**, and no 429 — every
|
|
40
|
+
* failure soft-fails to "no credential". An API token is 32 bytes of CSPRNG
|
|
41
|
+
* output resolved by a blake3 hash lookup, so guessing is bounded by entropy,
|
|
42
|
+
* not by throttling; a limiter here would buy nothing measurable while costing
|
|
43
|
+
* availability (the check/record has to precede the async lookup to close its
|
|
44
|
+
* own TOCTOU window, so concurrent requests bearing a *valid* token race each
|
|
45
|
+
* other into a 429). The Rust spine never had one here; this is the converged
|
|
46
|
+
* shape. See `docs/security.md` §Why bearer auth is not rate limited.
|
|
42
47
|
*
|
|
43
48
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
44
|
-
* @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
|
|
45
49
|
* @param log - the logger instance
|
|
46
50
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and `AUTH_API_TOKEN_ID_KEY` on success
|
|
47
|
-
* @mutates `ip_rate_limiter` - records on attempt; resets on a valid token
|
|
48
51
|
*/
|
|
49
|
-
export declare const create_bearer_auth_middleware: (deps: QueryDeps,
|
|
52
|
+
export declare const create_bearer_auth_middleware: (deps: QueryDeps, log: Logger) => MiddlewareHandler;
|
|
50
53
|
//# sourceMappingURL=bearer_auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,iBAuE5E,CAAC"}
|
package/dist/auth/bearer_auth.js
CHANGED
|
@@ -20,7 +20,6 @@ import { parse_stored_token_scope, token_scope_full } from './token_scope.js';
|
|
|
20
20
|
import { query_validate_api_token } from './api_token_queries.js';
|
|
21
21
|
import { get_client_ip } from '../http/client_ip.js';
|
|
22
22
|
import { is_browser_context } from '../http/origin.js';
|
|
23
|
-
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
24
23
|
/**
|
|
25
24
|
* Create middleware that authenticates via bearer token.
|
|
26
25
|
*
|
|
@@ -40,16 +39,20 @@ import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
|
40
39
|
* (e.g. by session middleware). Acting-actor resolution + `RequestContext`
|
|
41
40
|
* construction are deferred to the dispatcher's authorization phase.
|
|
42
41
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* There is deliberately **no rate limit on this path**, and no 429 — every
|
|
43
|
+
* failure soft-fails to "no credential". An API token is 32 bytes of CSPRNG
|
|
44
|
+
* output resolved by a blake3 hash lookup, so guessing is bounded by entropy,
|
|
45
|
+
* not by throttling; a limiter here would buy nothing measurable while costing
|
|
46
|
+
* availability (the check/record has to precede the async lookup to close its
|
|
47
|
+
* own TOCTOU window, so concurrent requests bearing a *valid* token race each
|
|
48
|
+
* other into a 429). The Rust spine never had one here; this is the converged
|
|
49
|
+
* shape. See `docs/security.md` §Why bearer auth is not rate limited.
|
|
45
50
|
*
|
|
46
51
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
47
|
-
* @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
|
|
48
52
|
* @param log - the logger instance
|
|
49
53
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and `AUTH_API_TOKEN_ID_KEY` on success
|
|
50
|
-
* @mutates `ip_rate_limiter` - records on attempt; resets on a valid token
|
|
51
54
|
*/
|
|
52
|
-
export const create_bearer_auth_middleware = (deps,
|
|
55
|
+
export const create_bearer_auth_middleware = (deps, log) => {
|
|
53
56
|
return async (c, next) => {
|
|
54
57
|
// Skip if an account is already authenticated (e.g. by session middleware)
|
|
55
58
|
if (c.get(ACCOUNT_ID_KEY) != null) {
|
|
@@ -88,28 +91,16 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
88
91
|
await next();
|
|
89
92
|
return;
|
|
90
93
|
}
|
|
94
|
+
// `ip` feeds the token's `last_used_ip` bookkeeping, not a limiter.
|
|
91
95
|
const ip = get_client_ip(c);
|
|
92
|
-
// Per-IP rate limit: record before async DB work to close the TOCTOU
|
|
93
|
-
// window where concurrent requests could all pass check() before any
|
|
94
|
-
// reaches record(). On valid token, reset the counter below.
|
|
95
|
-
if (ip_rate_limiter) {
|
|
96
|
-
const check = ip_rate_limiter.check(ip);
|
|
97
|
-
if (!check.allowed) {
|
|
98
|
-
return rate_limit_exceeded_response(c, check.retry_after);
|
|
99
|
-
}
|
|
100
|
-
ip_rate_limiter.record(ip);
|
|
101
|
-
}
|
|
102
96
|
const api_token = await query_validate_api_token({ ...deps, log }, raw_token, ip, c.var.pending_effects);
|
|
103
97
|
if (!api_token) {
|
|
104
|
-
// Invalid or expired token — soft-fail
|
|
105
|
-
//
|
|
98
|
+
// Invalid or expired token — soft-fail, indistinguishable from
|
|
99
|
+
// sending no credential at all.
|
|
106
100
|
log.debug('bearer auth soft-fail: token not found or expired');
|
|
107
101
|
await next();
|
|
108
102
|
return;
|
|
109
103
|
}
|
|
110
|
-
// Valid token — reset rate limit counter
|
|
111
|
-
if (ip_rate_limiter)
|
|
112
|
-
ip_rate_limiter.reset(ip);
|
|
113
104
|
c.set(ACCOUNT_ID_KEY, api_token.account_id);
|
|
114
105
|
c.set(CREDENTIAL_TYPE_KEY, 'api_token');
|
|
115
106
|
c.set(AUTH_API_TOKEN_ID_KEY, api_token.id);
|
|
@@ -37,8 +37,18 @@ export interface BootstrapRouteOptions {
|
|
|
37
37
|
* Use for app-specific post-bootstrap work like generating API tokens.
|
|
38
38
|
*/
|
|
39
39
|
on_bootstrap?: (result: BootstrapAccountSuccess, c: Context) => Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Rate limiter for bootstrap attempts, keyed by client IP. Pass `null` to
|
|
42
|
+
* disable. Its own instance, not login's: bootstrap is one-shot and its
|
|
43
|
+
* bucket is never refunded on success (see `RateLimiter.reset`), so a
|
|
44
|
+
* fumbled token would otherwise leave the operator's *login* budget nearly
|
|
45
|
+
* spent on a deployment where their new account is the only one that
|
|
46
|
+
* exists. The Rust spine rate-limits bootstrap not at all (the token is
|
|
47
|
+
* 32 bytes of CSPRNG compared in constant time); this is the tighter side
|
|
48
|
+
* of that divergence, kept because the token also sits in a file whose
|
|
49
|
+
* read path an operator can misconfigure.
|
|
50
|
+
*/
|
|
51
|
+
bootstrap_ip_rate_limiter: RateLimiter | null;
|
|
42
52
|
}
|
|
43
53
|
/**
|
|
44
54
|
* Dependencies for checking bootstrap status at startup.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E
|
|
1
|
+
{"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,EAAE,EAAE,EAAE,CAAC;IACP,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,wBAAwB,EAC9B,SAAS;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KACpC,OAAO,CAAC,eAAe,CAwBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA0GjB,CAAC"}
|
|
@@ -54,7 +54,7 @@ export const check_bootstrap_status = async (deps, options) => {
|
|
|
54
54
|
*/
|
|
55
55
|
export const create_bootstrap_route_specs = (deps, options) => {
|
|
56
56
|
const { keyring } = deps;
|
|
57
|
-
const { session_options, bootstrap_status, on_bootstrap,
|
|
57
|
+
const { session_options, bootstrap_status, on_bootstrap, bootstrap_ip_rate_limiter } = options;
|
|
58
58
|
const { token_path } = bootstrap_status;
|
|
59
59
|
return [
|
|
60
60
|
{
|
|
@@ -68,9 +68,9 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
68
68
|
return c.json({ error: ERROR_ALREADY_BOOTSTRAPPED }, 403);
|
|
69
69
|
}
|
|
70
70
|
// Per-IP rate limit check (before any token/DB work)
|
|
71
|
-
const ip =
|
|
72
|
-
if (
|
|
73
|
-
const check =
|
|
71
|
+
const ip = bootstrap_ip_rate_limiter ? get_client_ip(c) : null;
|
|
72
|
+
if (bootstrap_ip_rate_limiter && ip) {
|
|
73
|
+
const check = bootstrap_ip_rate_limiter.check(ip);
|
|
74
74
|
if (!check.allowed) {
|
|
75
75
|
return rate_limit_exceeded_response(c, check.retry_after);
|
|
76
76
|
}
|
|
@@ -87,8 +87,8 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
87
87
|
log: deps.log
|
|
88
88
|
}, input.token, input);
|
|
89
89
|
if (!result.ok) {
|
|
90
|
-
if (
|
|
91
|
-
|
|
90
|
+
if (bootstrap_ip_rate_limiter && ip)
|
|
91
|
+
bootstrap_ip_rate_limiter.record(ip);
|
|
92
92
|
deps.audit.emit(route, {
|
|
93
93
|
event_type: 'bootstrap',
|
|
94
94
|
outcome: 'failure',
|
|
@@ -97,9 +97,15 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
97
97
|
});
|
|
98
98
|
return c.json({ error: result.error }, result.status);
|
|
99
99
|
}
|
|
100
|
-
// Successful bootstrap — update state immediately
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// Successful bootstrap — update state immediately. Nothing is
|
|
101
|
+
// reset: bootstrap has no account-grain bucket to forgive (it
|
|
102
|
+
// predates any account), and the IP bucket is never refunded on
|
|
103
|
+
// success (see `RateLimiter.reset`). So this is the one auth
|
|
104
|
+
// surface where a success clears nothing at all. Bootstrap is
|
|
105
|
+
// one-shot, so at most `max_attempts - 1` residual entries
|
|
106
|
+
// survive — and they sit on this route's own limiter, not
|
|
107
|
+
// login's, so a fumbled token can't spend the operator's login
|
|
108
|
+
// budget on a deployment where their account is the only one.
|
|
103
109
|
bootstrap_status.available = false;
|
|
104
110
|
await create_session_and_set_cookie({
|
|
105
111
|
keyring,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import type { SessionOptions } from './session_cookie.js';
|
|
10
10
|
import type { AppDeps } from './deps.js';
|
|
11
11
|
import type { DaemonTokenState } from './daemon_token.js';
|
|
12
|
-
import type { RateLimiter } from '../rate_limiter.js';
|
|
13
12
|
import type { MiddlewareSpec } from '../http/middleware_spec.js';
|
|
14
13
|
/**
|
|
15
14
|
* Per-factory configuration for the standard auth middleware stack.
|
|
@@ -21,8 +20,6 @@ export interface AuthMiddlewareOptions {
|
|
|
21
20
|
path?: string;
|
|
22
21
|
/** Daemon token state for keeper auth. Omit to disable daemon token middleware. */
|
|
23
22
|
daemon_token_state?: DaemonTokenState;
|
|
24
|
-
/** Rate limiter for bearer token auth attempts (per-IP). Pass `null` to disable. */
|
|
25
|
-
bearer_ip_rate_limiter: RateLimiter | null;
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
25
|
* Create the auth middleware stack.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAuE/B,CAAC"}
|
package/dist/auth/middleware.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { ApiError
|
|
9
|
+
import { ApiError } from '../http/error_schemas.js';
|
|
10
10
|
/**
|
|
11
11
|
* Create the auth middleware stack.
|
|
12
12
|
*
|
|
@@ -21,7 +21,7 @@ import { ApiError, RateLimitError } from '../http/error_schemas.js';
|
|
|
21
21
|
*/
|
|
22
22
|
export const create_auth_middleware_specs = async (deps, options) => {
|
|
23
23
|
const { keyring, db } = deps;
|
|
24
|
-
const { allowed_origins, session_options, path = '/api/*', daemon_token_state
|
|
24
|
+
const { allowed_origins, session_options, path = '/api/*', daemon_token_state } = options;
|
|
25
25
|
const query_deps = { db };
|
|
26
26
|
// Dynamic imports preserve the bundle-split for type-only consumers of
|
|
27
27
|
// this module (`MiddlewareSpec`, `AuthMiddlewareOptions`, etc.). The
|
|
@@ -36,7 +36,7 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
36
36
|
]);
|
|
37
37
|
const session_middleware = create_session_middleware(keyring, session_options);
|
|
38
38
|
const request_context_middleware = create_request_context_middleware(query_deps, deps.log);
|
|
39
|
-
const bearer_auth_middleware = create_bearer_auth_middleware(query_deps,
|
|
39
|
+
const bearer_auth_middleware = create_bearer_auth_middleware(query_deps, deps.log);
|
|
40
40
|
const specs = [
|
|
41
41
|
{
|
|
42
42
|
name: 'origin',
|
|
@@ -50,13 +50,14 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
50
50
|
name: 'bearer_auth',
|
|
51
51
|
path,
|
|
52
52
|
handler: bearer_auth_middleware,
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
53
|
+
// Soft-fails on every non-success path — browser context, malformed,
|
|
54
|
+
// invalid, and expired tokens all `next()` through without setting
|
|
55
|
+
// context. The layer returns no error response of its own (it carries
|
|
56
|
+
// no rate limiter, so not even a 429). Auth enforcement (401/403)
|
|
57
|
+
// happens downstream — the RPC dispatcher's pre-authorization /
|
|
58
|
+
// post-authorization auth gates, or `require_auth` / `require_role` on
|
|
59
|
+
// REST — producing consistent JSON-RPC or route-level errors.
|
|
60
|
+
errors: {}
|
|
60
61
|
}
|
|
61
62
|
];
|
|
62
63
|
if (daemon_token_state) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuJ5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/auth/migrations.js
CHANGED
|
@@ -186,6 +186,30 @@ export const auth_migrations = [
|
|
|
186
186
|
WHERE scope IS NULL`);
|
|
187
187
|
await db.query('ALTER TABLE api_token ALTER COLUMN scope SET NOT NULL');
|
|
188
188
|
}
|
|
189
|
+
},
|
|
190
|
+
// Index the column every bearer request looks up.
|
|
191
|
+
//
|
|
192
|
+
// `full_auth_schema` indexes `api_token(account_id)` and nothing else, so
|
|
193
|
+
// `query_validate_api_token`'s `WHERE token_hash = $1` — the lookup on the
|
|
194
|
+
// hot path of *every* bearer-authenticated request, valid or not — has been
|
|
195
|
+
// a sequential scan since the table shipped. The asymmetry is accidental:
|
|
196
|
+
// `auth_session` gets the same access pattern for free because its session
|
|
197
|
+
// hash *is* the primary key.
|
|
198
|
+
//
|
|
199
|
+
// UNIQUE rather than a plain index, because both spines resolve a token with
|
|
200
|
+
// an at-most-one read (`query_one` / `query_opt`). Two rows sharing a hash
|
|
201
|
+
// would mean the two impls could silently disagree about which credential
|
|
202
|
+
// answered; the constraint makes that unrepresentable instead of merely
|
|
203
|
+
// improbable.
|
|
204
|
+
//
|
|
205
|
+
// The name must match the Rust twin's `api_token_hash_unique_index` byte for
|
|
206
|
+
// byte or the `_testing_migration_tracker` parity gate fails and the
|
|
207
|
+
// swap-freely invariant breaks.
|
|
208
|
+
{
|
|
209
|
+
name: 'api_token_hash_unique_index',
|
|
210
|
+
up: async (db) => {
|
|
211
|
+
await db.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_api_token_hash ON api_token(token_hash)');
|
|
212
|
+
}
|
|
189
213
|
}
|
|
190
214
|
];
|
|
191
215
|
/** Pre-composed migration namespace for auth tables. */
|
|
@@ -113,8 +113,19 @@ export declare const query_session_list_for_account: (deps: QueryDeps, account_i
|
|
|
113
113
|
* both commit above `max_sessions`. A transaction is necessary here but not
|
|
114
114
|
* sufficient. Closing this needs one serialization point per
|
|
115
115
|
* `(account_id, credential_kind)` — a locked parent row or a transaction-scoped
|
|
116
|
-
* advisory lock — taken before count/evict/insert
|
|
117
|
-
*
|
|
116
|
+
* advisory lock — taken before count/evict/insert.
|
|
117
|
+
*
|
|
118
|
+
* Ordering is `created_at DESC, id DESC`. The `id` leg is a **stability**
|
|
119
|
+
* tie-breaker, not a recency one: `created_at` defaults to `NOW()`, the
|
|
120
|
+
* *transaction* timestamp, so two sessions born in one transaction — or in two
|
|
121
|
+
* transactions that started in the same microsecond — tie, and an untied
|
|
122
|
+
* `OFFSET` may keep a different set on two evaluations of the same rows. `id` is
|
|
123
|
+
* the blake3 token hash, so the tie-break is arbitrary but deterministic; it
|
|
124
|
+
* makes the survivors reproducible, it does not make the row just inserted a
|
|
125
|
+
* guaranteed survivor. Only the serialization point above does that.
|
|
126
|
+
*
|
|
127
|
+
* Expired-but-unreaped rows count toward the cap (the predicate is `account_id`
|
|
128
|
+
* alone). Matches the Rust twin `query_session_enforce_limit`.
|
|
118
129
|
*
|
|
119
130
|
* @param deps - query dependencies (must be transaction-scoped)
|
|
120
131
|
* @param account_id - the account to enforce the limit for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;;;;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;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,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
|
|
1
|
+
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;;;;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;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,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;;;;;;GAMG;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;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EACjD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAMd,CAAC"}
|