@fuzdev/fuz_app 0.7.1 → 0.9.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/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +1 -6
- package/dist/actions/action_rpc.d.ts +3 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +22 -10
- package/dist/auth/bearer_auth.d.ts +10 -0
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +29 -7
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +5 -1
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +7 -10
- package/dist/testing/app_server.d.ts +2 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +17 -0
- package/dist/testing/data_exposure.js +1 -1
- package/dist/testing/integration.d.ts +1 -1
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +11 -17
- package/dist/testing/round_trip.js +1 -1
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +25 -5
- package/dist/testing/rpc_round_trip.js +1 -1
- package/dist/ui/AdminOverview.svelte +33 -33
- package/dist/ui/BootstrapForm.svelte +13 -1
- package/dist/ui/BootstrapForm.svelte.d.ts +4 -1
- package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -1
- package/dist/ui/ColumnLayout.svelte +6 -6
- package/dist/ui/Datatable.svelte +14 -14
- package/dist/ui/LoginForm.svelte +1 -1
- package/dist/ui/OpenSignupToggle.svelte +1 -1
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/SurfaceExplorer.svelte +1 -5
- package/dist/ui/SurfaceExplorer.svelte.d.ts +3 -3
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.d.ts +9 -9
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +5 -5
- package/dist/ui/sidebar_state.svelte.d.ts +5 -5
- package/dist/ui/sidebar_state.svelte.d.ts.map +1 -1
- package/dist/ui/sidebar_state.svelte.js +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAKxE;;GAEG;AACH,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAgCtD;;;OAGG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAqDb;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,aAAa,MAAM,KACjB,MAkBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,KACpB,
|
|
1
|
+
{"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAKxE;;GAEG;AACH,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAgCtD;;;OAGG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAqDb;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,aAAa,MAAM,KACjB,MAkBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,KACpB,MAyBF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,MACU,CAAC"}
|
|
@@ -257,18 +257,13 @@ export const generate_phase_handlers = (spec, executor, imports) => {
|
|
|
257
257
|
// Backend types file is in server/ subdirectory, so needs different relative paths
|
|
258
258
|
const path_prefix = executor === 'frontend' ? './' : '../';
|
|
259
259
|
imports.add_type(`${path_prefix}action_event.js`, 'ActionEvent');
|
|
260
|
-
// Add environment type import
|
|
261
|
-
const environment_type = executor === 'frontend' ? 'Frontend' : 'Backend';
|
|
262
|
-
const environment_module = executor === 'frontend' ? './frontend.svelte.js' : './backend.js';
|
|
263
|
-
imports.add_type(environment_module, environment_type);
|
|
264
260
|
// Generate handler definitions for each phase
|
|
265
261
|
const phase_handlers = phases
|
|
266
262
|
.map((phase) => {
|
|
267
263
|
// Pass imports to get_handler_return_type so it can add necessary imports
|
|
268
264
|
const return_type = get_handler_return_type(spec, phase, imports, path_prefix);
|
|
269
|
-
// Use the new type parameter approach
|
|
270
265
|
return `${phase}?: (
|
|
271
|
-
action_event: ActionEvent<'${method}',
|
|
266
|
+
action_event: ActionEvent<'${method}', '${phase}', 'handling'>
|
|
272
267
|
) => ${return_type}`;
|
|
273
268
|
})
|
|
274
269
|
.join(';\n\t\t');
|
|
@@ -16,6 +16,7 @@ import type { RequestResponseActionSpec } from './action_spec.js';
|
|
|
16
16
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
17
17
|
import { type RequestContext } from '../auth/request_context.js';
|
|
18
18
|
import type { Db } from '../db/db.js';
|
|
19
|
+
import { type JsonrpcRequestId } from '../http/jsonrpc.js';
|
|
19
20
|
/**
|
|
20
21
|
* Per-request context provided to RPC action handlers.
|
|
21
22
|
*
|
|
@@ -26,6 +27,8 @@ import type { Db } from '../db/db.js';
|
|
|
26
27
|
export interface ActionContext {
|
|
27
28
|
/** The authenticated identity, or `null` for public routes. */
|
|
28
29
|
auth: RequestContext | null;
|
|
30
|
+
/** The JSON-RPC request ID from the envelope. */
|
|
31
|
+
request_id: JsonrpcRequestId;
|
|
29
32
|
/** Transaction-scoped for mutations, pool-level for reads. */
|
|
30
33
|
db: Db;
|
|
31
34
|
/** Always pool-level — for fire-and-forget effects that outlive the transaction. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAE9F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAkC,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAS1F;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,+DAA+D;IAC/D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,CACxD,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACZ;AAkDD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,wBAAwB,KAAG,KAAK,CAAC,SAAS,CAoOtF,CAAC"}
|
|
@@ -15,9 +15,10 @@ import { z } from 'zod';
|
|
|
15
15
|
import { DEV } from 'esm-env';
|
|
16
16
|
import {} from '../http/route_spec.js';
|
|
17
17
|
import { get_request_context, has_role } from '../auth/request_context.js';
|
|
18
|
+
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
18
19
|
import { is_null_schema } from '../http/schema_helpers.js';
|
|
19
20
|
import { JSONRPC_VERSION, JsonrpcRequest } from '../http/jsonrpc.js';
|
|
20
|
-
import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES,
|
|
21
|
+
import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
|
|
21
22
|
/**
|
|
22
23
|
* Format a JSON-RPC error response.
|
|
23
24
|
*
|
|
@@ -35,9 +36,10 @@ const jsonrpc_error_response = (id, error) => ({
|
|
|
35
36
|
*
|
|
36
37
|
* @param auth - the action's auth requirement
|
|
37
38
|
* @param request_context - the resolved identity (null if unauthenticated)
|
|
39
|
+
* @param credential_type - how the request was authenticated (session, api_token, daemon_token)
|
|
38
40
|
* @returns an error json if auth fails, or null if authorized
|
|
39
41
|
*/
|
|
40
|
-
const check_action_auth = (auth, request_context) => {
|
|
42
|
+
const check_action_auth = (auth, request_context, credential_type) => {
|
|
41
43
|
if (auth === 'public')
|
|
42
44
|
return null;
|
|
43
45
|
if (!request_context)
|
|
@@ -45,9 +47,12 @@ const check_action_auth = (auth, request_context) => {
|
|
|
45
47
|
if (auth === 'authenticated')
|
|
46
48
|
return null;
|
|
47
49
|
if (auth === 'keeper') {
|
|
48
|
-
// keeper requires the keeper role
|
|
49
|
-
|
|
50
|
+
// keeper requires daemon_token credential type AND the keeper role.
|
|
51
|
+
// API tokens and session cookies cannot access keeper actions even
|
|
52
|
+
// if the account has the keeper permit.
|
|
53
|
+
if (credential_type !== 'daemon_token' || !has_role(request_context, 'keeper')) {
|
|
50
54
|
return jsonrpc_error_messages.forbidden();
|
|
55
|
+
}
|
|
51
56
|
return null;
|
|
52
57
|
}
|
|
53
58
|
// role check
|
|
@@ -117,7 +122,8 @@ export const create_rpc_endpoint = (options) => {
|
|
|
117
122
|
}
|
|
118
123
|
// step 3: auth check
|
|
119
124
|
const request_context = get_request_context(c);
|
|
120
|
-
const
|
|
125
|
+
const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
|
|
126
|
+
const auth_error = check_action_auth(action.spec.auth, request_context, credential_type);
|
|
121
127
|
if (auth_error) {
|
|
122
128
|
const error = jsonrpc_error_response(id, auth_error);
|
|
123
129
|
return c.json(error, jsonrpc_error_code_to_http_status(auth_error.code));
|
|
@@ -136,6 +142,7 @@ export const create_rpc_endpoint = (options) => {
|
|
|
136
142
|
const execute = async (db) => {
|
|
137
143
|
const action_context = {
|
|
138
144
|
auth: request_context,
|
|
145
|
+
request_id: id,
|
|
139
146
|
db,
|
|
140
147
|
background_db: route.background_db,
|
|
141
148
|
pending_effects: route.pending_effects,
|
|
@@ -160,11 +167,16 @@ export const create_rpc_endpoint = (options) => {
|
|
|
160
167
|
return await execute(route.db);
|
|
161
168
|
}
|
|
162
169
|
catch (err) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
// Duck-type check: Error with numeric `code` signals a JSON-RPC error.
|
|
171
|
+
// Avoids instanceof which fails when consumers throw their own ThrownJsonrpcError
|
|
172
|
+
// (structurally identical but different class identity, e.g. zzz's copy).
|
|
173
|
+
if (err instanceof Error && typeof err.code === 'number') {
|
|
174
|
+
const code = err.code;
|
|
175
|
+
const data = err.data;
|
|
176
|
+
const status = jsonrpc_error_code_to_http_status(code);
|
|
177
|
+
const error_json = { code, message: err.message };
|
|
178
|
+
if (data !== undefined)
|
|
179
|
+
error_json.data = data;
|
|
168
180
|
return c.json(jsonrpc_error_response(id, error_json), status);
|
|
169
181
|
}
|
|
170
182
|
// generic error
|
|
@@ -17,6 +17,13 @@ import { type RateLimiter } from '../rate_limiter.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* Create middleware that authenticates via bearer token.
|
|
19
19
|
*
|
|
20
|
+
* Soft-fails for invalid, expired, or empty tokens — calls `next()` without
|
|
21
|
+
* setting a request context, letting downstream auth enforcement (per-action
|
|
22
|
+
* `check_action_auth` or `require_auth`) return a consistent JSON-RPC or
|
|
23
|
+
* route-level error. This avoids leaking token-specific diagnostics
|
|
24
|
+
* (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
|
|
25
|
+
* and ensures public actions are not blocked by bad credentials.
|
|
26
|
+
*
|
|
20
27
|
* Rejects bearer tokens when an `Origin` or `Referer` header is present —
|
|
21
28
|
* browsers must use cookie auth to reduce attack surface.
|
|
22
29
|
* Auth scheme matching is case-insensitive per RFC 7235.
|
|
@@ -24,6 +31,9 @@ import { type RateLimiter } from '../rate_limiter.js';
|
|
|
24
31
|
* and sets it on the Hono context. Skips if a request context is already set
|
|
25
32
|
* (e.g. by session middleware).
|
|
26
33
|
*
|
|
34
|
+
* Rate limiting (429) is the only hard-fail — it's a throttling concern
|
|
35
|
+
* independent of auth identity.
|
|
36
|
+
*
|
|
27
37
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
28
38
|
* @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
|
|
29
39
|
* @param log - the logger instance
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,iBAAiB,WAAW,GAAG,IAAI,EACnC,KAAK,MAAM,KACT,iBAqFF,CAAC"}
|
package/dist/auth/bearer_auth.js
CHANGED
|
@@ -15,10 +15,16 @@ import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
|
15
15
|
import { query_validate_api_token } from './api_token_queries.js';
|
|
16
16
|
import { get_client_ip } from '../http/proxy.js';
|
|
17
17
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
18
|
-
import { ERROR_BEARER_REJECTED_BROWSER, ERROR_INVALID_TOKEN, ERROR_ACCOUNT_NOT_FOUND, } from '../http/error_schemas.js';
|
|
19
18
|
/**
|
|
20
19
|
* Create middleware that authenticates via bearer token.
|
|
21
20
|
*
|
|
21
|
+
* Soft-fails for invalid, expired, or empty tokens — calls `next()` without
|
|
22
|
+
* setting a request context, letting downstream auth enforcement (per-action
|
|
23
|
+
* `check_action_auth` or `require_auth`) return a consistent JSON-RPC or
|
|
24
|
+
* route-level error. This avoids leaking token-specific diagnostics
|
|
25
|
+
* (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
|
|
26
|
+
* and ensures public actions are not blocked by bad credentials.
|
|
27
|
+
*
|
|
22
28
|
* Rejects bearer tokens when an `Origin` or `Referer` header is present —
|
|
23
29
|
* browsers must use cookie auth to reduce attack surface.
|
|
24
30
|
* Auth scheme matching is case-insensitive per RFC 7235.
|
|
@@ -26,6 +32,9 @@ import { ERROR_BEARER_REJECTED_BROWSER, ERROR_INVALID_TOKEN, ERROR_ACCOUNT_NOT_F
|
|
|
26
32
|
* and sets it on the Hono context. Skips if a request context is already set
|
|
27
33
|
* (e.g. by session middleware).
|
|
28
34
|
*
|
|
35
|
+
* Rate limiting (429) is the only hard-fail — it's a throttling concern
|
|
36
|
+
* independent of auth identity.
|
|
37
|
+
*
|
|
29
38
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
30
39
|
* @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
|
|
31
40
|
* @param log - the logger instance
|
|
@@ -46,19 +55,24 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
46
55
|
await next();
|
|
47
56
|
return;
|
|
48
57
|
}
|
|
49
|
-
//
|
|
58
|
+
// Silently discard bearer tokens in browser context — defense-in-depth:
|
|
50
59
|
// checks both Origin and Referer (not just Origin) because some browser
|
|
51
60
|
// requests send only Referer. Uses `!== undefined` so that empty-string
|
|
52
61
|
// headers (e.g. `Origin: ''`) are still treated as browser context.
|
|
62
|
+
// Discards rather than returning 403 so that the RPC dispatcher can still
|
|
63
|
+
// handle public actions or fall through to cookie auth.
|
|
53
64
|
if (c.req.header('Origin') !== undefined || c.req.header('Referer') !== undefined) {
|
|
54
|
-
|
|
65
|
+
log.debug('bearer auth rejected: browser context (Origin/Referer present)');
|
|
66
|
+
await next();
|
|
67
|
+
return;
|
|
55
68
|
}
|
|
56
69
|
const raw_token = auth_header.slice(7);
|
|
57
|
-
//
|
|
70
|
+
// Empty token body — soft-fail (treat as "no credential").
|
|
58
71
|
// (The Fetch API trims 'Bearer ' to 'Bearer' which skips this middleware entirely,
|
|
59
72
|
// but raw HTTP clients may send 'Bearer ' with an empty token.)
|
|
60
73
|
if (!raw_token) {
|
|
61
|
-
|
|
74
|
+
await next();
|
|
75
|
+
return;
|
|
62
76
|
}
|
|
63
77
|
const ip = get_client_ip(c);
|
|
64
78
|
// Per-IP rate limit: record before async DB work to close the TOCTOU
|
|
@@ -73,7 +87,11 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
73
87
|
}
|
|
74
88
|
const api_token = await query_validate_api_token({ ...deps, log }, raw_token, ip, c.var.pending_effects);
|
|
75
89
|
if (!api_token) {
|
|
76
|
-
|
|
90
|
+
// Invalid or expired token — soft-fail. Rate limit counter stays
|
|
91
|
+
// incremented (recorded above), correctly penalizing bad attempts.
|
|
92
|
+
log.debug('bearer auth soft-fail: token not found or expired');
|
|
93
|
+
await next();
|
|
94
|
+
return;
|
|
77
95
|
}
|
|
78
96
|
// Valid token — reset rate limit counter
|
|
79
97
|
if (ip_rate_limiter)
|
|
@@ -81,7 +99,11 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
81
99
|
// Build request context from the token's account
|
|
82
100
|
const ctx = await build_request_context(deps, api_token.account_id);
|
|
83
101
|
if (!ctx) {
|
|
84
|
-
|
|
102
|
+
// Token exists but account/actor missing — soft-fail to avoid
|
|
103
|
+
// leaking account lifecycle information.
|
|
104
|
+
log.debug('bearer auth soft-fail: account or actor not found for token');
|
|
105
|
+
await next();
|
|
106
|
+
return;
|
|
85
107
|
}
|
|
86
108
|
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
87
109
|
c.set(CREDENTIAL_TYPE_KEY, 'api_token');
|
|
@@ -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,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAG/D;;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;IACtC,oFAAoF;IACpF,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAG/D;;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;IACtC,oFAAoF;IACpF,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAmE/B,CAAC"}
|
package/dist/auth/middleware.js
CHANGED
|
@@ -47,7 +47,11 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
47
47
|
name: 'bearer_auth',
|
|
48
48
|
path,
|
|
49
49
|
handler: bearer_auth_middleware,
|
|
50
|
-
|
|
50
|
+
// Bearer middleware soft-fails for invalid/expired tokens (calls next()
|
|
51
|
+
// without setting context). Only 429 is a hard-fail from this layer.
|
|
52
|
+
// Auth enforcement (401/403) happens downstream via check_action_auth
|
|
53
|
+
// or require_auth, producing consistent JSON-RPC or route-level errors.
|
|
54
|
+
errors: { 429: RateLimitError },
|
|
51
55
|
},
|
|
52
56
|
];
|
|
53
57
|
if (daemon_token_state) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAIzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,GAC7C,gBAAgB,MAAM,KACpB,KAAK,CAAC,qBAAqB,CA+D7B,CAAC;AAIF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qCAAqC,GACjD,YAAY,MAAM,EAClB,SAAS,0BAA0B,EACnC,gBAAgB,MAAM,EACtB,cAAc,KAAK,CAAC,qBAAqB,CAAC,KACxC,IAkCF,CAAC"}
|
|
@@ -8,7 +8,7 @@ import './assert_dev_env.js';
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import { test, assert, describe } from 'vitest';
|
|
11
|
-
import { ApiError, ERROR_FORBIDDEN_ORIGIN, ERROR_FORBIDDEN_REFERER
|
|
11
|
+
import { ApiError, ERROR_FORBIDDEN_ORIGIN, ERROR_FORBIDDEN_REFERER } from '../http/error_schemas.js';
|
|
12
12
|
import { create_test_middleware_stack_app, TEST_MIDDLEWARE_PATH, } from './middleware.js';
|
|
13
13
|
// --- Standard adversarial header cases ---
|
|
14
14
|
/**
|
|
@@ -29,13 +29,12 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
|
|
|
29
29
|
validate_expectation: 'not_called',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
name: 'bearer token with allowed Origin
|
|
32
|
+
name: 'bearer token with allowed Origin — bearer silently discarded (browser context)',
|
|
33
33
|
headers: {
|
|
34
34
|
Authorization: 'Bearer secret_fuz_token_test',
|
|
35
35
|
Origin: allowed_origin,
|
|
36
36
|
},
|
|
37
|
-
expected_status:
|
|
38
|
-
expected_error: ERROR_BEARER_REJECTED_BROWSER,
|
|
37
|
+
expected_status: 200,
|
|
39
38
|
validate_expectation: 'not_called',
|
|
40
39
|
},
|
|
41
40
|
{
|
|
@@ -55,12 +54,11 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
|
|
|
55
54
|
validate_expectation: 'not_called',
|
|
56
55
|
},
|
|
57
56
|
{
|
|
58
|
-
name: 'lowercase bearer scheme is recognized (case-insensitive per RFC 7235)',
|
|
57
|
+
name: 'lowercase bearer scheme is recognized (case-insensitive per RFC 7235), soft-fails',
|
|
59
58
|
headers: {
|
|
60
59
|
Authorization: 'bearer secret_fuz_token_test',
|
|
61
60
|
},
|
|
62
|
-
expected_status:
|
|
63
|
-
expected_error: ERROR_INVALID_TOKEN,
|
|
61
|
+
expected_status: 200,
|
|
64
62
|
validate_expectation: 'called',
|
|
65
63
|
},
|
|
66
64
|
{
|
|
@@ -74,13 +72,12 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
|
|
|
74
72
|
validate_expectation: 'not_called',
|
|
75
73
|
},
|
|
76
74
|
{
|
|
77
|
-
name: 'bearer token with Referer from allowed origin
|
|
75
|
+
name: 'bearer token with Referer from allowed origin — bearer silently discarded (browser context)',
|
|
78
76
|
headers: {
|
|
79
77
|
Authorization: 'Bearer secret_fuz_token_test',
|
|
80
78
|
Referer: `${allowed_origin}/page`,
|
|
81
79
|
},
|
|
82
|
-
expected_status:
|
|
83
|
-
expected_error: ERROR_BEARER_REJECTED_BROWSER,
|
|
80
|
+
expected_status: 200,
|
|
84
81
|
validate_expectation: 'not_called',
|
|
85
82
|
},
|
|
86
83
|
];
|
|
@@ -144,6 +144,8 @@ export interface TestApp {
|
|
|
144
144
|
create_session_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
145
145
|
/** Build request headers with the bootstrapped Bearer token. */
|
|
146
146
|
create_bearer_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
147
|
+
/** Build request headers with the daemon token (keeper auth). */
|
|
148
|
+
create_daemon_token_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
147
149
|
/** Create an additional account with credentials. */
|
|
148
150
|
create_account: (options?: {
|
|
149
151
|
username?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAK/B,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAU1D,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG3F,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAK/B,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAU1D,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG3F,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAUrD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,gBAIhC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,QAAiB,CAAC;AASjD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,EAAE,EAAE,EAAE,CAAC;IACP,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,2BAA2B,KAClC,OAAO,CAAC;IACV,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACvB,CAyCA,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,UAAU;IAChD,gCAAgC;IAChC,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,uCAAuC;IACvC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,mDAAmD;IACnD,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kGAAkG;IAClG,EAAE,CAAC,EAAE,EAAE,CAAC;IACR,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yHAAyH;IACzH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAqBD,eAAO,MAAM,sBAAsB,GAClC,SAAS,oBAAoB,KAC3B,OAAO,CAAC,aAAa,CAsFvB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IACjE,yEAAyE;IACzE,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IACpE,gHAAgH;IAChH,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnF,8DAA8D;IAC9D,qBAAqB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,GAAG,EAAE,IAAI,CAAC;IACV,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,EAAE,cAAc,CAAC;IAC7B,OAAO,EAAE,UAAU,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9B,kEAAkE;IAClE,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnF,gEAAgE;IAChE,qBAAqB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClF,iEAAiE;IACjE,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxF,qDAAqD;IACrD,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KACtB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAU,SAAS,oBAAoB,KAAG,OAAO,CAAC,OAAO,CAkGpF,CAAC"}
|
|
@@ -12,6 +12,7 @@ import { create_session_cookie_value } from '../auth/session_cookie.js';
|
|
|
12
12
|
import { run_migrations } from '../db/migrate.js';
|
|
13
13
|
import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
14
14
|
import { create_app_server, } from '../server/app_server.js';
|
|
15
|
+
import { generate_daemon_token, DAEMON_TOKEN_HEADER, } from '../auth/daemon_token.js';
|
|
15
16
|
import { create_pglite_factory } from './db.js';
|
|
16
17
|
/* eslint-disable @typescript-eslint/require-await */
|
|
17
18
|
/**
|
|
@@ -172,6 +173,15 @@ export const create_test_app_server = async (options) => {
|
|
|
172
173
|
*/
|
|
173
174
|
export const create_test_app = async (options) => {
|
|
174
175
|
const test_server = await create_test_app_server(options);
|
|
176
|
+
// Daemon token state for keeper auth in tests.
|
|
177
|
+
// Uses a static token (no rotation) — sufficient for request-level testing.
|
|
178
|
+
const test_daemon_token = generate_daemon_token();
|
|
179
|
+
const daemon_token_state = {
|
|
180
|
+
current_token: test_daemon_token,
|
|
181
|
+
previous_token: null,
|
|
182
|
+
rotated_at: new Date(),
|
|
183
|
+
keeper_account_id: test_server.account.id,
|
|
184
|
+
};
|
|
175
185
|
const result = await create_app_server({
|
|
176
186
|
backend: test_server,
|
|
177
187
|
session_options: options.session_options,
|
|
@@ -183,6 +193,7 @@ export const create_test_app = async (options) => {
|
|
|
183
193
|
signup_account_rate_limiter: null,
|
|
184
194
|
bearer_ip_rate_limiter: null,
|
|
185
195
|
await_pending_effects: true,
|
|
196
|
+
daemon_token_state,
|
|
186
197
|
...options.app_options,
|
|
187
198
|
create_route_specs: options.create_route_specs,
|
|
188
199
|
});
|
|
@@ -200,6 +211,11 @@ export const create_test_app = async (options) => {
|
|
|
200
211
|
authorization: `Bearer ${test_server.api_token}`,
|
|
201
212
|
...extra,
|
|
202
213
|
});
|
|
214
|
+
const create_daemon_token_headers = (extra) => ({
|
|
215
|
+
host: 'localhost',
|
|
216
|
+
[DAEMON_TOKEN_HEADER]: test_daemon_token,
|
|
217
|
+
...extra,
|
|
218
|
+
});
|
|
203
219
|
let account_counter = 0;
|
|
204
220
|
const create_account = async (account_options) => {
|
|
205
221
|
account_counter++;
|
|
@@ -235,6 +251,7 @@ export const create_test_app = async (options) => {
|
|
|
235
251
|
route_specs: surface_spec.route_specs,
|
|
236
252
|
create_session_headers,
|
|
237
253
|
create_bearer_headers,
|
|
254
|
+
create_daemon_token_headers,
|
|
238
255
|
create_account,
|
|
239
256
|
cleanup: () => test_server.cleanup(),
|
|
240
257
|
};
|
|
@@ -291,6 +291,6 @@ const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
|
|
|
291
291
|
// keeper role uses the bootstrapped account
|
|
292
292
|
return test_app.create_session_headers();
|
|
293
293
|
case 'keeper':
|
|
294
|
-
return test_app.
|
|
294
|
+
return test_app.create_daemon_token_headers();
|
|
295
295
|
}
|
|
296
296
|
};
|
|
@@ -24,7 +24,7 @@ export interface StandardIntegrationTestOptions {
|
|
|
24
24
|
*
|
|
25
25
|
* Exercises login/logout, cookie attributes, session security, session
|
|
26
26
|
* revocation, password change (incl. API token revocation), origin
|
|
27
|
-
* verification, bearer auth (incl. browser context
|
|
27
|
+
* verification, bearer auth (incl. browser context discard on mutations),
|
|
28
28
|
* token revocation, cross-account isolation, expired credential rejection,
|
|
29
29
|
* signup invite edge cases, and response body validation.
|
|
30
30
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAGrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAgBjB;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAeD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAGrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAgBjB;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAeD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAo+CF,CAAC"}
|
|
@@ -38,7 +38,7 @@ const build_test_app_options = (options, db) => ({
|
|
|
38
38
|
*
|
|
39
39
|
* Exercises login/logout, cookie attributes, session security, session
|
|
40
40
|
* revocation, password change (incl. API token revocation), origin
|
|
41
|
-
* verification, bearer auth (incl. browser context
|
|
41
|
+
* verification, bearer auth (incl. browser context discard on mutations),
|
|
42
42
|
* token revocation, cross-account isolation, expired credential rejection,
|
|
43
43
|
* signup invite edge cases, and response body validation.
|
|
44
44
|
*
|
|
@@ -525,17 +525,15 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
525
525
|
headers: bearer_headers,
|
|
526
526
|
});
|
|
527
527
|
assert.strictEqual(ok_res.status, 200);
|
|
528
|
-
// With Origin —
|
|
528
|
+
// With Origin — bearer silently discarded (browser context), falls through to no auth
|
|
529
529
|
const res = await test_app.app.request(verify_route.path, {
|
|
530
530
|
headers: {
|
|
531
531
|
...bearer_headers,
|
|
532
532
|
origin: 'http://localhost:5173',
|
|
533
533
|
},
|
|
534
534
|
});
|
|
535
|
-
assert.strictEqual(res.status,
|
|
536
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path,
|
|
537
|
-
const body = await res.json();
|
|
538
|
-
assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
|
|
535
|
+
assert.strictEqual(res.status, 401);
|
|
536
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
539
537
|
});
|
|
540
538
|
});
|
|
541
539
|
// --- 7. Token revocation ---
|
|
@@ -912,8 +910,8 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
912
910
|
});
|
|
913
911
|
});
|
|
914
912
|
// --- 12. Bearer token browser context on mutation routes ---
|
|
915
|
-
describe('bearer token browser context
|
|
916
|
-
test('bearer token with Origin header
|
|
913
|
+
describe('bearer token browser context silently discarded on mutations', () => {
|
|
914
|
+
test('bearer token with Origin header discarded on POST logout', async () => {
|
|
917
915
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
918
916
|
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
919
917
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
@@ -924,12 +922,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
924
922
|
method: 'POST',
|
|
925
923
|
headers: { ...bearer_headers, origin: 'http://localhost:5173' },
|
|
926
924
|
});
|
|
927
|
-
assert.strictEqual(res.status,
|
|
928
|
-
|
|
929
|
-
assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
|
|
930
|
-
error_collector.record(test_app.route_specs, 'POST', logout_route.path, 403);
|
|
925
|
+
assert.strictEqual(res.status, 401, 'Bearer with Origin should be discarded → unauthenticated');
|
|
926
|
+
error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
|
|
931
927
|
});
|
|
932
|
-
test('bearer token with Referer header
|
|
928
|
+
test('bearer token with Referer header discarded on POST password', async () => {
|
|
933
929
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
934
930
|
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
935
931
|
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
@@ -940,10 +936,8 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
940
936
|
method: 'POST',
|
|
941
937
|
headers: { ...bearer_headers, referer: 'http://localhost:5173/admin' },
|
|
942
938
|
});
|
|
943
|
-
assert.strictEqual(res.status,
|
|
944
|
-
|
|
945
|
-
assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
|
|
946
|
-
error_collector.record(test_app.route_specs, 'POST', password_route.path, 403);
|
|
939
|
+
assert.strictEqual(res.status, 401, 'Bearer with Referer should be discarded → unauthenticated');
|
|
940
|
+
error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
|
|
947
941
|
});
|
|
948
942
|
});
|
|
949
943
|
// --- 13. Password change revokes API tokens ---
|
|
@@ -122,6 +122,6 @@ const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
|
|
|
122
122
|
// Keeper role uses the bootstrapped account (which has keeper role)
|
|
123
123
|
return test_app.create_session_headers();
|
|
124
124
|
case 'keeper':
|
|
125
|
-
return test_app.
|
|
125
|
+
return test_app.create_daemon_token_headers();
|
|
126
126
|
}
|
|
127
127
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAA6C,cAAc,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAA6C,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAoBnG,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACvC,+FAA+F;IAC/F,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AAodD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,uBAAuB,KAAG,IAOpF,CAAC"}
|
|
@@ -14,7 +14,7 @@ import './assert_dev_env.js';
|
|
|
14
14
|
*/
|
|
15
15
|
import { test, assert, describe } from 'vitest';
|
|
16
16
|
import { JSONRPC_ERROR_CODES } from '../http/jsonrpc_errors.js';
|
|
17
|
-
import { create_auth_test_apps, select_auth_app } from './auth_apps.js';
|
|
17
|
+
import { create_auth_test_apps, create_test_app_from_specs, create_test_request_context, select_auth_app, } from './auth_apps.js';
|
|
18
18
|
import { generate_input_test_cases } from './adversarial_input.js';
|
|
19
19
|
import { ERROR_INVALID_JSON_BODY } from '../http/error_schemas.js';
|
|
20
20
|
import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, } from './rpc_helpers.js';
|
|
@@ -23,6 +23,8 @@ import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response
|
|
|
23
23
|
const filter_protected_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type !== 'none');
|
|
24
24
|
/** Filter RPC methods that require a specific role. */
|
|
25
25
|
const filter_role_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type === 'role');
|
|
26
|
+
/** Filter RPC methods that require keeper auth (daemon_token + keeper role). */
|
|
27
|
+
const filter_keeper_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type === 'keeper');
|
|
26
28
|
/** Find the `RpcAction` source spec for a surface method. */
|
|
27
29
|
const find_rpc_action = (rpc_endpoint_specs, endpoint_path, method_name) => {
|
|
28
30
|
const ep = rpc_endpoint_specs.find((e) => e.path === endpoint_path);
|
|
@@ -40,7 +42,7 @@ const find_rpc_action = (rpc_endpoint_specs, endpoint_path, method_name) => {
|
|
|
40
42
|
* - unauthenticated → error code -32001 — every protected method
|
|
41
43
|
* - wrong role → error code -32002 — every role method with non-matching roles
|
|
42
44
|
* - authenticated without role → -32002 — every role method, no-role context
|
|
43
|
-
* - keeper
|
|
45
|
+
* - keeper rejects non-daemon credentials → -32002 — session and api_token rejected
|
|
44
46
|
* - correct auth passes — every protected method, assert not 401/403
|
|
45
47
|
*/
|
|
46
48
|
const describe_rpc_auth = (options) => {
|
|
@@ -94,9 +96,27 @@ const describe_rpc_auth = (options) => {
|
|
|
94
96
|
}
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const keeper_methods = filter_keeper_rpc_methods(endpoint);
|
|
100
|
+
if (keeper_methods.length > 0) {
|
|
101
|
+
describe('keeper rejects non-daemon credentials', () => {
|
|
102
|
+
const session_app = create_test_app_from_specs(route_specs, create_test_request_context('keeper'), 'session');
|
|
103
|
+
const api_token_app = create_test_app_from_specs(route_specs, create_test_request_context('keeper'), 'api_token');
|
|
104
|
+
for (const method of keeper_methods) {
|
|
105
|
+
test(`${method.name} rejects session credential`, async () => {
|
|
106
|
+
const res = await session_app.request(endpoint.path, create_rpc_post_init(method.name));
|
|
107
|
+
assert.strictEqual(res.status, 403, `${method.name} should reject session credential`);
|
|
108
|
+
const body = await res.json();
|
|
109
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.forbidden);
|
|
110
|
+
});
|
|
111
|
+
test(`${method.name} rejects api_token credential`, async () => {
|
|
112
|
+
const res = await api_token_app.request(endpoint.path, create_rpc_post_init(method.name));
|
|
113
|
+
assert.strictEqual(res.status, 403, `${method.name} should reject api_token credential`);
|
|
114
|
+
const body = await res.json();
|
|
115
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.forbidden);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
100
120
|
describe('correct auth passes', () => {
|
|
101
121
|
for (const method of protected_methods) {
|
|
102
122
|
test(method.name, async () => {
|
|
@@ -33,7 +33,7 @@ const pick_rpc_auth_headers = (method, test_app, authed_account, admin_account)
|
|
|
33
33
|
// keeper role uses the bootstrapped account
|
|
34
34
|
return test_app.create_session_headers();
|
|
35
35
|
case 'keeper':
|
|
36
|
-
return test_app.
|
|
36
|
+
return test_app.create_daemon_token_headers();
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
@@ -67,20 +67,20 @@
|
|
|
67
67
|
<!-- TODO: panels will be user-draggable/rearrangeable, hardcoded order for now -->
|
|
68
68
|
<div class="overview">
|
|
69
69
|
<section>
|
|
70
|
-
<div class="
|
|
70
|
+
<div class="panel-header">
|
|
71
71
|
<h3>accounts</h3>
|
|
72
72
|
<a href={resolve('/admin/accounts' as any)} class="text_50 font_size_sm">view all →</a>
|
|
73
73
|
</div>
|
|
74
74
|
{#if accounts.loading}
|
|
75
75
|
<p class="text_50">loading...</p>
|
|
76
76
|
{:else if accounts.error}
|
|
77
|
-
<p class="
|
|
77
|
+
<p class="color_c_50">{accounts.error}</p>
|
|
78
78
|
{:else}
|
|
79
|
-
<div class="
|
|
79
|
+
<div class="baseline-row gap_xs">
|
|
80
80
|
<strong class="font_size_lg">{accounts.account_count}</strong>
|
|
81
81
|
<span class="text_50">accounts</span>
|
|
82
82
|
</div>
|
|
83
|
-
<div class="
|
|
83
|
+
<div class="baseline-row gap_xs flex-wrap:wrap font_size_sm mt_xs">
|
|
84
84
|
{#each role_counts as [role, count] (role)}
|
|
85
85
|
<span>{count} {role}</span>
|
|
86
86
|
<span class="text_50">·</span>
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
<span>{unroled_count} unroled</span>
|
|
89
89
|
</div>
|
|
90
90
|
{#if accounts.accounts.length > 0}
|
|
91
|
-
<ul class="
|
|
91
|
+
<ul class="compact-list">
|
|
92
92
|
{#each accounts.accounts.slice(0, 6) as entry (entry)}
|
|
93
93
|
<li>
|
|
94
94
|
<strong>{entry.account.username}</strong>
|
|
@@ -109,25 +109,25 @@
|
|
|
109
109
|
</section>
|
|
110
110
|
|
|
111
111
|
<section>
|
|
112
|
-
<div class="
|
|
112
|
+
<div class="panel-header">
|
|
113
113
|
<h3>sessions</h3>
|
|
114
114
|
<a href={resolve('/admin/sessions' as any)} class="text_50 font_size_sm">view all →</a>
|
|
115
115
|
</div>
|
|
116
116
|
{#if sessions.loading}
|
|
117
117
|
<p class="text_50">loading...</p>
|
|
118
118
|
{:else if sessions.error}
|
|
119
|
-
<p class="
|
|
119
|
+
<p class="color_c_50">{sessions.error}</p>
|
|
120
120
|
{:else}
|
|
121
|
-
<div class="
|
|
121
|
+
<div class="baseline-row gap_xs">
|
|
122
122
|
<strong class="font_size_lg">{sessions.active_count}</strong>
|
|
123
123
|
<span class="text_50">active</span>
|
|
124
124
|
</div>
|
|
125
|
-
<div class="
|
|
125
|
+
<div class="baseline-row gap_xs">
|
|
126
126
|
<strong class="font_size_lg">{unique_users}</strong>
|
|
127
127
|
<span class="text_50">unique users</span>
|
|
128
128
|
</div>
|
|
129
129
|
{#if most_recent}
|
|
130
|
-
<div class="
|
|
130
|
+
<div class="baseline-row gap_xs font_size_sm mt_sm">
|
|
131
131
|
<span class="text_50">last active:</span>
|
|
132
132
|
<strong>{most_recent.username}</strong>
|
|
133
133
|
<span class="text_50" title={format_datetime_local(most_recent.last_seen_at)}
|
|
@@ -139,16 +139,16 @@
|
|
|
139
139
|
</section>
|
|
140
140
|
|
|
141
141
|
<section>
|
|
142
|
-
<div class="
|
|
142
|
+
<div class="panel-header">
|
|
143
143
|
<h3>invites</h3>
|
|
144
144
|
<a href={resolve('/admin/invites' as any)} class="text_50 font_size_sm">view all →</a>
|
|
145
145
|
</div>
|
|
146
146
|
{#if invites.loading}
|
|
147
147
|
<p class="text_50">loading...</p>
|
|
148
148
|
{:else if invites.error}
|
|
149
|
-
<p class="
|
|
149
|
+
<p class="color_c_50">{invites.error}</p>
|
|
150
150
|
{:else}
|
|
151
|
-
<div class="
|
|
151
|
+
<div class="baseline-row gap_sm">
|
|
152
152
|
<span class="text_50">public signup</span>
|
|
153
153
|
{#if app_settings.settings?.open_signup}
|
|
154
154
|
<span class="chip color_b">open</span>
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
<span class="chip">closed</span>
|
|
157
157
|
{/if}
|
|
158
158
|
</div>
|
|
159
|
-
<div class="
|
|
159
|
+
<div class="baseline-row gap_xs">
|
|
160
160
|
<strong class="font_size_lg">{invites.unclaimed_count}</strong>
|
|
161
161
|
<span class="text_50">unclaimed</span>
|
|
162
162
|
<span class="text_50">/</span>
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
<span class="text_50">total</span>
|
|
165
165
|
</div>
|
|
166
166
|
{#if invites.invites.length > 0}
|
|
167
|
-
<ul class="
|
|
167
|
+
<ul class="compact-list">
|
|
168
168
|
{#each invites.invites.slice(0, 4) as invite (invite.id)}
|
|
169
169
|
<li>
|
|
170
170
|
<span>{invite.email || invite.username || '—'}</span>
|
|
@@ -184,18 +184,18 @@
|
|
|
184
184
|
</section>
|
|
185
185
|
|
|
186
186
|
<section>
|
|
187
|
-
<div class="
|
|
187
|
+
<div class="panel-header">
|
|
188
188
|
<h3>recent activity</h3>
|
|
189
189
|
<a href={resolve('/admin/audit-log' as any)} class="text_50 font_size_sm">view all →</a>
|
|
190
190
|
</div>
|
|
191
191
|
{#if audit_log.loading}
|
|
192
192
|
<p class="text_50">loading...</p>
|
|
193
193
|
{:else if audit_log.error}
|
|
194
|
-
<p class="
|
|
194
|
+
<p class="color_c_50">{audit_log.error}</p>
|
|
195
195
|
{:else if recent_events.length === 0}
|
|
196
196
|
<p class="text_50">no events</p>
|
|
197
197
|
{:else}
|
|
198
|
-
<ul class="
|
|
198
|
+
<ul class="compact-list">
|
|
199
199
|
{#each recent_events as event (event.id)}
|
|
200
200
|
<li>
|
|
201
201
|
<span class="text_50 font_size_sm" title={format_datetime_local(event.created_at)}
|
|
@@ -212,27 +212,27 @@
|
|
|
212
212
|
</section>
|
|
213
213
|
|
|
214
214
|
<section>
|
|
215
|
-
<div class="
|
|
215
|
+
<div class="panel-header">
|
|
216
216
|
<h3>security</h3>
|
|
217
217
|
<a href={resolve('/admin/audit-log' as any)} class="text_50 font_size_sm">audit log →</a>
|
|
218
218
|
</div>
|
|
219
219
|
{#if audit_log.loading}
|
|
220
220
|
<p class="text_50">loading...</p>
|
|
221
221
|
{:else if audit_log.error}
|
|
222
|
-
<p class="
|
|
222
|
+
<p class="color_c_50">{audit_log.error}</p>
|
|
223
223
|
{:else}
|
|
224
|
-
<div class="
|
|
225
|
-
<strong class="font_size_lg" class:
|
|
224
|
+
<div class="baseline-row gap_xs">
|
|
225
|
+
<strong class="font_size_lg" class:color_c_50={failed_logins.length > 0}>
|
|
226
226
|
{failed_logins.length}
|
|
227
227
|
</strong>
|
|
228
228
|
<span class="text_50">failed logins</span>
|
|
229
229
|
</div>
|
|
230
|
-
<div class="
|
|
230
|
+
<div class="baseline-row gap_xs">
|
|
231
231
|
<strong class="font_size_lg">{permit_changes.length}</strong>
|
|
232
232
|
<span class="text_50">permit changes</span>
|
|
233
233
|
</div>
|
|
234
234
|
{#if permit_changes.length > 0}
|
|
235
|
-
<ul class="
|
|
235
|
+
<ul class="compact-list">
|
|
236
236
|
{#each permit_changes.slice(0, 4) as event (event.id)}
|
|
237
237
|
<li class="font_size_sm">
|
|
238
238
|
<span class="text_50" title={format_datetime_local(event.created_at)}
|
|
@@ -250,15 +250,15 @@
|
|
|
250
250
|
</section>
|
|
251
251
|
|
|
252
252
|
<section>
|
|
253
|
-
<div class="
|
|
253
|
+
<div class="panel-header">
|
|
254
254
|
<h3>system</h3>
|
|
255
255
|
</div>
|
|
256
256
|
{#if app_settings.loading}
|
|
257
257
|
<p class="text_50">loading...</p>
|
|
258
258
|
{:else if app_settings.error}
|
|
259
|
-
<p class="
|
|
259
|
+
<p class="color_c_50">{app_settings.error}</p>
|
|
260
260
|
{:else}
|
|
261
|
-
<div class="
|
|
261
|
+
<div class="baseline-row gap_sm">
|
|
262
262
|
<span class="text_50">public signup</span>
|
|
263
263
|
{#if app_settings.settings?.open_signup}
|
|
264
264
|
<span class="chip color_b">open</span>
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
{/if}
|
|
268
268
|
</div>
|
|
269
269
|
{#if app_settings.settings?.updated_at}
|
|
270
|
-
<div class="
|
|
270
|
+
<div class="baseline-row gap_xs font_size_sm mt_xs">
|
|
271
271
|
<span class="text_50">last changed:</span>
|
|
272
272
|
<span title={format_datetime_local(app_settings.settings.updated_at)}>
|
|
273
273
|
{format_relative_time(app_settings.settings.updated_at)}
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
{/if}
|
|
281
281
|
{/if}
|
|
282
282
|
{#if auth_state.account}
|
|
283
|
-
<div class="
|
|
283
|
+
<div class="baseline-row gap_sm">
|
|
284
284
|
<span class="text_50">logged in as</span>
|
|
285
285
|
<strong>{auth_state.account.username}</strong>
|
|
286
286
|
</div>
|
|
@@ -310,25 +310,25 @@
|
|
|
310
310
|
gap: var(--space_lg);
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
.
|
|
313
|
+
.panel-header {
|
|
314
314
|
display: flex;
|
|
315
315
|
justify-content: space-between;
|
|
316
316
|
align-items: baseline;
|
|
317
317
|
margin-bottom: var(--space_md);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
.
|
|
320
|
+
.baseline-row {
|
|
321
321
|
display: flex;
|
|
322
322
|
align-items: baseline;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
.
|
|
325
|
+
.compact-list {
|
|
326
326
|
list-style: none;
|
|
327
327
|
padding: 0;
|
|
328
328
|
margin: var(--space_sm) 0 0;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
.
|
|
331
|
+
.compact-list li {
|
|
332
332
|
display: flex;
|
|
333
333
|
align-items: baseline;
|
|
334
334
|
gap: var(--space_xs);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {goto} from '$app/navigation';
|
|
3
|
+
import {resolve} from '$app/paths';
|
|
2
4
|
import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
|
|
3
5
|
import {autofocus} from '@fuzdev/fuz_ui/autofocus.svelte.js';
|
|
4
6
|
|
|
@@ -7,6 +9,12 @@
|
|
|
7
9
|
import {auth_state_context} from './auth_state.svelte.js';
|
|
8
10
|
import {FormState} from './form_state.svelte.js';
|
|
9
11
|
|
|
12
|
+
const {
|
|
13
|
+
redirect_on_bootstrap = resolve('/'),
|
|
14
|
+
}: {
|
|
15
|
+
redirect_on_bootstrap?: string;
|
|
16
|
+
} = $props();
|
|
17
|
+
|
|
10
18
|
const auth_state = auth_state_context.get();
|
|
11
19
|
const form_state = new FormState();
|
|
12
20
|
|
|
@@ -35,7 +43,11 @@
|
|
|
35
43
|
else if (!passwords_match) form_state.focus('password_confirm');
|
|
36
44
|
return;
|
|
37
45
|
}
|
|
38
|
-
await auth_state.bootstrap(token.trim(), username.trim(), password);
|
|
46
|
+
const success = await auth_state.bootstrap(token.trim(), username.trim(), password);
|
|
47
|
+
if (success) {
|
|
48
|
+
form_state.reset();
|
|
49
|
+
await goto(redirect_on_bootstrap);
|
|
50
|
+
}
|
|
39
51
|
};
|
|
40
52
|
</script>
|
|
41
53
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
redirect_on_bootstrap?: string;
|
|
3
|
+
};
|
|
4
|
+
declare const BootstrapForm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
5
|
type BootstrapForm = ReturnType<typeof BootstrapForm>;
|
|
3
6
|
export default BootstrapForm;
|
|
4
7
|
//# sourceMappingURL=BootstrapForm.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAaC,KAAK,gBAAgB,GAAI;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAqGH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -15,29 +15,29 @@
|
|
|
15
15
|
} = $props();
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
|
-
<div class="
|
|
19
|
-
<aside class="
|
|
18
|
+
<div class="column-layout {class_name}" style:--column_width={column_width} {...rest}>
|
|
19
|
+
<aside class="column-fixed unstyled">
|
|
20
20
|
{@render aside()}
|
|
21
21
|
</aside>
|
|
22
|
-
<div class="
|
|
22
|
+
<div class="column-fluid">
|
|
23
23
|
{@render children()}
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<style>
|
|
28
|
-
.
|
|
28
|
+
.column-layout {
|
|
29
29
|
display: flex;
|
|
30
30
|
height: 100%;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
.
|
|
33
|
+
.column-fixed {
|
|
34
34
|
width: var(--column_width, 280px);
|
|
35
35
|
min-width: var(--column_width, 280px);
|
|
36
36
|
height: 100%;
|
|
37
37
|
overflow: auto;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
.
|
|
40
|
+
.column-fluid {
|
|
41
41
|
flex: 1;
|
|
42
42
|
height: 100%;
|
|
43
43
|
min-width: 0;
|
package/dist/ui/Datatable.svelte
CHANGED
|
@@ -78,16 +78,16 @@
|
|
|
78
78
|
aria-rowcount={rows.length + 1}
|
|
79
79
|
>
|
|
80
80
|
<!-- sticky header -->
|
|
81
|
-
<div class="
|
|
81
|
+
<div class="datatable-header" role="row" aria-rowindex={1}>
|
|
82
82
|
{#each columns as column, i (column.key)}
|
|
83
|
-
<div class="
|
|
83
|
+
<div class="datatable-header-cell" role="columnheader">
|
|
84
84
|
{#if header}
|
|
85
85
|
{@render header(column)}
|
|
86
86
|
{:else}
|
|
87
87
|
{column.label}
|
|
88
88
|
{/if}
|
|
89
89
|
<div
|
|
90
|
-
class="
|
|
90
|
+
class="datatable-resize-handle"
|
|
91
91
|
role="separator"
|
|
92
92
|
onpointerdown={(e) => handle_resize_start(e, i)}
|
|
93
93
|
onpointermove={handle_resize_move}
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
{#if rows.length === 0}
|
|
101
|
-
<div class="
|
|
101
|
+
<div class="datatable-empty">
|
|
102
102
|
{#if empty}
|
|
103
103
|
{@render empty()}
|
|
104
104
|
{:else}
|
|
@@ -107,9 +107,9 @@
|
|
|
107
107
|
</div>
|
|
108
108
|
{:else}
|
|
109
109
|
{#each rows as row, i (row[row_key] ?? i)}
|
|
110
|
-
<div class="
|
|
110
|
+
<div class="datatable-row" role="row" aria-rowindex={i + 2}>
|
|
111
111
|
{#each columns as column (column.key)}
|
|
112
|
-
<div class="
|
|
112
|
+
<div class="datatable-cell" role="gridcell">
|
|
113
113
|
{#if cell}
|
|
114
114
|
{@render cell(column, row, row[column.key])}
|
|
115
115
|
{:else if column.format}
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
overflow: auto;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
.
|
|
134
|
+
.datatable-header {
|
|
135
135
|
display: grid;
|
|
136
136
|
grid-column: 1 / -1;
|
|
137
137
|
grid-template-columns: subgrid;
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
border-bottom: var(--border_width, 1px) solid var(--border_color);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
.
|
|
145
|
+
.datatable-header-cell {
|
|
146
146
|
position: relative;
|
|
147
147
|
display: flex;
|
|
148
148
|
align-items: center;
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
font-weight: 600;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
.
|
|
153
|
+
.datatable-resize-handle {
|
|
154
154
|
position: absolute;
|
|
155
155
|
top: 0;
|
|
156
156
|
right: 0;
|
|
@@ -159,11 +159,11 @@
|
|
|
159
159
|
cursor: col-resize;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
.
|
|
163
|
-
background: var(--
|
|
162
|
+
.datatable-resize-handle:hover {
|
|
163
|
+
background: var(--color_a_10);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
.
|
|
166
|
+
.datatable-row {
|
|
167
167
|
display: grid;
|
|
168
168
|
grid-column: 1 / -1;
|
|
169
169
|
grid-template-columns: subgrid;
|
|
@@ -172,13 +172,13 @@
|
|
|
172
172
|
border-bottom: var(--border_width, 1px) solid var(--border_color);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
.
|
|
175
|
+
.datatable-cell {
|
|
176
176
|
padding: var(--space_xs);
|
|
177
177
|
min-width: 0; /* override grid auto minimum to respect column widths */
|
|
178
178
|
overflow-wrap: break-word;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
.
|
|
181
|
+
.datatable-empty {
|
|
182
182
|
grid-column: 1 / -1;
|
|
183
183
|
padding: var(--space_lg);
|
|
184
184
|
}
|
package/dist/ui/LoginForm.svelte
CHANGED
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
import type {AppSurface, AppSurfaceRoute, AppSurfaceDiagnostic} from '../http/surface.js';
|
|
5
5
|
import {surface_auth_summary, format_route_key} from '../http/surface_query.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
surface: AppSurface;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const {surface}: Props = $props();
|
|
7
|
+
const {surface}: {surface: AppSurface} = $props();
|
|
12
8
|
|
|
13
9
|
const auth_types = ['all', 'none', 'authenticated', 'role', 'keeper'] as const;
|
|
14
10
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AppSurface } from '../http/surface.js';
|
|
2
|
-
|
|
2
|
+
type $$ComponentProps = {
|
|
3
3
|
surface: AppSurface;
|
|
4
|
-
}
|
|
5
|
-
declare const SurfaceExplorer: import("svelte").Component
|
|
4
|
+
};
|
|
5
|
+
declare const SurfaceExplorer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
6
|
type SurfaceExplorer = ReturnType<typeof SurfaceExplorer>;
|
|
7
7
|
export default SurfaceExplorer;
|
|
8
8
|
//# sourceMappingURL=SurfaceExplorer.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SurfaceExplorer.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SurfaceExplorer.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,UAAU,EAAwC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"SurfaceExplorer.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SurfaceExplorer.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,UAAU,EAAwC,MAAM,oBAAoB,CAAC;AAGzF,KAAK,gBAAgB,GAAI;IAAC,OAAO,EAAE,UAAU,CAAA;CAAC,CAAC;AAsRhD,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
* @module
|
|
33
33
|
*/
|
|
34
34
|
import { type Permit, type SessionAccount } from '../auth/account_schema.js';
|
|
35
|
+
/**
|
|
36
|
+
* Svelte context for `AuthState`.
|
|
37
|
+
* Use `auth_state_context.set(state)` in the provider and `auth_state_context.get()` to access.
|
|
38
|
+
*/
|
|
39
|
+
export declare const auth_state_context: {
|
|
40
|
+
get: (error_message?: string) => AuthState;
|
|
41
|
+
get_maybe: () => AuthState | undefined;
|
|
42
|
+
set: (value: AuthState) => AuthState;
|
|
43
|
+
};
|
|
35
44
|
export declare class AuthState {
|
|
36
45
|
verifying: boolean;
|
|
37
46
|
verified: boolean;
|
|
@@ -73,13 +82,4 @@ export declare class AuthState {
|
|
|
73
82
|
*/
|
|
74
83
|
logout(): Promise<void>;
|
|
75
84
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Svelte context for `AuthState`.
|
|
78
|
-
* Use `auth_state_context.set(state)` in the provider and `auth_state_context.get()` to access.
|
|
79
|
-
*/
|
|
80
|
-
export declare const auth_state_context: {
|
|
81
|
-
get: (error_message?: string) => AuthState;
|
|
82
|
-
get_maybe: () => AuthState | undefined;
|
|
83
|
-
set: (value: AuthState) => AuthState;
|
|
84
|
-
};
|
|
85
85
|
//# sourceMappingURL=auth_state.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/auth_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;
|
|
1
|
+
{"version":3,"file":"auth_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/auth_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,OAAO,EAAC,KAAK,MAAM,EAAoB,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE7F;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAA8B,CAAC;AAE9D,qBAAa,SAAS;IACrB,SAAS,UAAqB;IAC9B,QAAQ,UAAqB;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC/C,OAAO,EAAE,cAAc,GAAG,IAAI,CAAoB;IAClD,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAkB;IACxC,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAC,MAAM,CAAC,CAEpC;IACF,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAoD;IAEjF,gEAAgE;IAChE,eAAe,UAAqB;IAEpC;;;;;;OAMG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BpC;;;;OAIG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwCjE;;;;OAIG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiCpF;;;;OAIG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4ClF;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAU7B"}
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
import { create_context } from '@fuzdev/fuz_ui/context_helpers.js';
|
|
35
35
|
import { ui_fetch } from './ui_fetch.js';
|
|
36
36
|
import { is_permit_active } from '../auth/account_schema.js';
|
|
37
|
+
/**
|
|
38
|
+
* Svelte context for `AuthState`.
|
|
39
|
+
* Use `auth_state_context.set(state)` in the provider and `auth_state_context.get()` to access.
|
|
40
|
+
*/
|
|
41
|
+
export const auth_state_context = create_context();
|
|
37
42
|
export class AuthState {
|
|
38
43
|
verifying = $state.raw(false);
|
|
39
44
|
verified = $state.raw(false);
|
|
@@ -231,8 +236,3 @@ export class AuthState {
|
|
|
231
236
|
this.permits = [];
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Svelte context for `AuthState`.
|
|
236
|
-
* Use `auth_state_context.set(state)` in the provider and `auth_state_context.get()` to access.
|
|
237
|
-
*/
|
|
238
|
-
export const auth_state_context = create_context();
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare const sidebar_state_context: {
|
|
2
|
+
get: (error_message?: string) => () => SidebarState;
|
|
3
|
+
get_maybe: () => (() => SidebarState) | undefined;
|
|
4
|
+
set: (value: () => SidebarState) => () => SidebarState;
|
|
5
|
+
};
|
|
1
6
|
/**
|
|
2
7
|
* Options for configuring a `SidebarState`.
|
|
3
8
|
*
|
|
@@ -22,9 +27,4 @@ export declare class SidebarState {
|
|
|
22
27
|
*/
|
|
23
28
|
activate(): () => void;
|
|
24
29
|
}
|
|
25
|
-
export declare const sidebar_state_context: {
|
|
26
|
-
get: (error_message?: string) => () => SidebarState;
|
|
27
|
-
get_maybe: () => (() => SidebarState) | undefined;
|
|
28
|
-
set: (value: () => SidebarState) => () => SidebarState;
|
|
29
|
-
};
|
|
30
30
|
//# sourceMappingURL=sidebar_state.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/sidebar_state.svelte.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACxB;AAED,qBAAa,YAAY;;IAKxB,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAEzB;IAED,IAAI,YAAY,IAAI,OAAO,CAG1B;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,OAAO,EAE9B;gBAEW,OAAO,CAAC,EAAE,mBAAmB;IAIzC,cAAc,CAAC,KAAK,GAAE,OAA4B,GAAG,IAAI;IAIzD;;;OAGG;IACH,QAAQ,IAAI,MAAM,IAAI;CAQtB
|
|
1
|
+
{"version":3,"file":"sidebar_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/sidebar_state.svelte.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,qBAAqB;2CAAwB,YAAY;4BAAZ,YAAY;uBAAZ,YAAY,WAAZ,YAAY;CAAG,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACxB;AAED,qBAAa,YAAY;;IAKxB,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAEzB;IAED,IAAI,YAAY,IAAI,OAAO,CAG1B;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,OAAO,EAE9B;gBAEW,OAAO,CAAC,EAAE,mBAAmB;IAIzC,cAAc,CAAC,KAAK,GAAE,OAA4B,GAAG,IAAI;IAIzD;;;OAGG;IACH,QAAQ,IAAI,MAAM,IAAI;CAQtB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create_context } from '@fuzdev/fuz_ui/context_helpers.js';
|
|
2
|
+
export const sidebar_state_context = create_context();
|
|
2
3
|
export class SidebarState {
|
|
3
4
|
#get_enabled;
|
|
4
5
|
#enabled = $state.raw(true);
|
|
@@ -36,4 +37,3 @@ export class SidebarState {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
export const sidebar_state_context = create_context();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "fullstack app library",
|
|
5
5
|
"glyph": "🗝",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@fuzdev/fuz_code": "^0.45.1",
|
|
47
47
|
"@fuzdev/fuz_css": "^0.58.0",
|
|
48
48
|
"@fuzdev/fuz_ui": "^0.191.2",
|
|
49
|
-
"@fuzdev/fuz_util": "^0.
|
|
49
|
+
"@fuzdev/fuz_util": "^0.56.0",
|
|
50
50
|
"@fuzdev/gro": "^0.197.3",
|
|
51
51
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
52
52
|
"@node-rs/argon2": "^2.0.2",
|