@fuzdev/fuz_app 0.7.1 → 0.8.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_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/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/LoginForm.svelte +1 -1
- package/dist/ui/SignupForm.svelte +1 -1
- package/package.json +1 -1
|
@@ -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":"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"}
|
|
@@ -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
|
/**
|
|
@@ -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"}
|
package/dist/ui/LoginForm.svelte
CHANGED