@shaferllc/keel 0.58.0 → 0.59.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/core/auth.d.ts +21 -2
- package/dist/core/auth.js +38 -3
- package/dist/core/crypto.d.ts +50 -0
- package/dist/core/crypto.js +84 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +2 -2
- package/package.json +1 -1
package/dist/core/auth.d.ts
CHANGED
|
@@ -21,7 +21,11 @@ export declare class Auth {
|
|
|
21
21
|
login(id: string | number): void;
|
|
22
22
|
/** Log the current user out. */
|
|
23
23
|
logout(): void;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The authenticated user's id, or null. A token verified by `bearerAuth()`
|
|
26
|
+
* wins; otherwise it falls back to the session (set via `login()`). Reads the
|
|
27
|
+
* request context directly so token-only APIs work without a session store.
|
|
28
|
+
*/
|
|
25
29
|
id(): string | null;
|
|
26
30
|
/** Whether a user is authenticated. */
|
|
27
31
|
check(): boolean;
|
|
@@ -34,8 +38,23 @@ export declare class Auth {
|
|
|
34
38
|
export declare function auth(): Auth;
|
|
35
39
|
/**
|
|
36
40
|
* A guard middleware: rejects unauthenticated requests with a 401, or redirects
|
|
37
|
-
* if `redirectTo` is set.
|
|
41
|
+
* if `redirectTo` is set. Honors both session logins and a `bearerAuth()` token.
|
|
38
42
|
*/
|
|
39
43
|
export declare function authGuard(options?: {
|
|
40
44
|
redirectTo?: string;
|
|
41
45
|
}): MiddlewareHandler;
|
|
46
|
+
/**
|
|
47
|
+
* Token (API) auth: read a `Bearer` JWT from the `Authorization` header, verify
|
|
48
|
+
* it, and make its `sub` the authenticated user id — so `auth().user()` resolves
|
|
49
|
+
* through your registered provider, exactly as with session auth. Issue the
|
|
50
|
+
* token in your login handler with `jwt.sign({ sub: String(user.id) }, …)`.
|
|
51
|
+
*
|
|
52
|
+
* router.get("/api/me", bearerAuth(), async () => json(await auth().user()));
|
|
53
|
+
*
|
|
54
|
+
* Rejects a missing or invalid token with a 401. Pass `{ optional: true }` to
|
|
55
|
+
* let the request through unauthenticated (e.g. content that varies by login
|
|
56
|
+
* but doesn't require it) — `auth().check()` is then false downstream.
|
|
57
|
+
*/
|
|
58
|
+
export declare function bearerAuth(options?: {
|
|
59
|
+
optional?: boolean;
|
|
60
|
+
}): MiddlewareHandler;
|
package/dist/core/auth.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* await auth().user(); // the full user (via your provider)
|
|
13
13
|
*/
|
|
14
14
|
import { session } from "./session.js";
|
|
15
|
+
import { ctx } from "./request.js";
|
|
16
|
+
import { jwt } from "./crypto.js";
|
|
15
17
|
const KEY = "auth_id";
|
|
16
18
|
let provider;
|
|
17
19
|
/** Tell Keel how to load the authenticated user from its id. */
|
|
@@ -27,9 +29,17 @@ export class Auth {
|
|
|
27
29
|
logout() {
|
|
28
30
|
session().forget(KEY);
|
|
29
31
|
}
|
|
30
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The authenticated user's id, or null. A token verified by `bearerAuth()`
|
|
34
|
+
* wins; otherwise it falls back to the session (set via `login()`). Reads the
|
|
35
|
+
* request context directly so token-only APIs work without a session store.
|
|
36
|
+
*/
|
|
31
37
|
id() {
|
|
32
|
-
|
|
38
|
+
const fromToken = ctx().get("auth_id");
|
|
39
|
+
if (fromToken != null)
|
|
40
|
+
return String(fromToken);
|
|
41
|
+
const data = ctx().get("session");
|
|
42
|
+
return data && data[KEY] != null ? String(data[KEY]) : null;
|
|
33
43
|
}
|
|
34
44
|
/** Whether a user is authenticated. */
|
|
35
45
|
check() {
|
|
@@ -56,7 +66,7 @@ export function auth() {
|
|
|
56
66
|
}
|
|
57
67
|
/**
|
|
58
68
|
* A guard middleware: rejects unauthenticated requests with a 401, or redirects
|
|
59
|
-
* if `redirectTo` is set.
|
|
69
|
+
* if `redirectTo` is set. Honors both session logins and a `bearerAuth()` token.
|
|
60
70
|
*/
|
|
61
71
|
export function authGuard(options = {}) {
|
|
62
72
|
return async (c, next) => {
|
|
@@ -68,3 +78,28 @@ export function authGuard(options = {}) {
|
|
|
68
78
|
await next();
|
|
69
79
|
};
|
|
70
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Token (API) auth: read a `Bearer` JWT from the `Authorization` header, verify
|
|
83
|
+
* it, and make its `sub` the authenticated user id — so `auth().user()` resolves
|
|
84
|
+
* through your registered provider, exactly as with session auth. Issue the
|
|
85
|
+
* token in your login handler with `jwt.sign({ sub: String(user.id) }, …)`.
|
|
86
|
+
*
|
|
87
|
+
* router.get("/api/me", bearerAuth(), async () => json(await auth().user()));
|
|
88
|
+
*
|
|
89
|
+
* Rejects a missing or invalid token with a 401. Pass `{ optional: true }` to
|
|
90
|
+
* let the request through unauthenticated (e.g. content that varies by login
|
|
91
|
+
* but doesn't require it) — `auth().check()` is then false downstream.
|
|
92
|
+
*/
|
|
93
|
+
export function bearerAuth(options = {}) {
|
|
94
|
+
return async (c, next) => {
|
|
95
|
+
const token = c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
96
|
+
const payload = token ? await jwt.verify(token) : null;
|
|
97
|
+
if (!payload || payload.sub == null) {
|
|
98
|
+
if (options.optional)
|
|
99
|
+
return next();
|
|
100
|
+
return c.json({ error: "Unauthenticated", status: 401 }, 401);
|
|
101
|
+
}
|
|
102
|
+
c.set("auth_id", String(payload.sub));
|
|
103
|
+
await next();
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/core/crypto.d.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* const token = await encryption.encrypt({ userId: 1 });
|
|
9
9
|
* await encryption.decrypt(token); // { userId: 1 } | null
|
|
10
|
+
*
|
|
11
|
+
* const token = await jwt.sign({ sub: "42" }, { expiresIn: "1h" });
|
|
12
|
+
* await jwt.verify(token); // { sub: "42", iat, exp } | null
|
|
10
13
|
*/
|
|
11
14
|
export declare const hash: {
|
|
12
15
|
/** Hash a password (PBKDF2-SHA256 with a random salt). */
|
|
@@ -22,3 +25,50 @@ export declare const encryption: {
|
|
|
22
25
|
/** Decrypt a value; returns null if the payload is tampered or invalid. */
|
|
23
26
|
decrypt<T = unknown>(payload: string): Promise<T | null>;
|
|
24
27
|
};
|
|
28
|
+
/** Standard registered claims, plus whatever custom fields you sign. */
|
|
29
|
+
export interface JwtPayload {
|
|
30
|
+
/** Subject — conventionally the user id. */
|
|
31
|
+
sub?: string;
|
|
32
|
+
/** Issued-at (seconds since the epoch); set automatically by `sign`. */
|
|
33
|
+
iat?: number;
|
|
34
|
+
/** Expiry (seconds since the epoch); set from `expiresIn`. */
|
|
35
|
+
exp?: number;
|
|
36
|
+
/** Not-before (seconds since the epoch); the token is invalid until then. */
|
|
37
|
+
nbf?: number;
|
|
38
|
+
/** Issuer. */
|
|
39
|
+
iss?: string;
|
|
40
|
+
/** Audience. */
|
|
41
|
+
aud?: string;
|
|
42
|
+
[claim: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
export interface JwtSignOptions {
|
|
45
|
+
/** Lifetime — seconds (number) or a duration string like `"30s"`, `"15m"`, `"1h"`, `"7d"`. */
|
|
46
|
+
expiresIn?: number | string;
|
|
47
|
+
/** Sets the `iss` claim. */
|
|
48
|
+
issuer?: string;
|
|
49
|
+
/** Sets the `aud` claim. */
|
|
50
|
+
audience?: string;
|
|
51
|
+
/** Sets the `sub` claim (overrides any `sub` already in the payload). */
|
|
52
|
+
subject?: string;
|
|
53
|
+
/** Signing secret; defaults to `config('app.key')`. */
|
|
54
|
+
secret?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface JwtVerifyOptions {
|
|
57
|
+
/** Require this `iss`; a token that doesn't match is rejected. */
|
|
58
|
+
issuer?: string;
|
|
59
|
+
/** Require this `aud`; a token that doesn't match is rejected. */
|
|
60
|
+
audience?: string;
|
|
61
|
+
/** Verifying secret; defaults to `config('app.key')`. */
|
|
62
|
+
secret?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare const jwt: {
|
|
65
|
+
/** Sign a payload into an HS256 JWT. Adds `iat`, and `exp` when `expiresIn` is set. */
|
|
66
|
+
sign(payload: JwtPayload, options?: JwtSignOptions): Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Verify an HS256 JWT and return its payload, or `null` if the token is
|
|
69
|
+
* malformed, tampered, expired, not-yet-valid, or fails an issuer/audience
|
|
70
|
+
* check. Only HS256 is accepted — `alg: none` and asymmetric algs are refused,
|
|
71
|
+
* closing the classic JWT algorithm-confusion hole.
|
|
72
|
+
*/
|
|
73
|
+
verify<T extends JwtPayload = JwtPayload>(token: string, options?: JwtVerifyOptions): Promise<T | null>;
|
|
74
|
+
};
|
package/dist/core/crypto.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* const token = await encryption.encrypt({ userId: 1 });
|
|
9
9
|
* await encryption.decrypt(token); // { userId: 1 } | null
|
|
10
|
+
*
|
|
11
|
+
* const token = await jwt.sign({ sub: "42" }, { expiresIn: "1h" });
|
|
12
|
+
* await jwt.verify(token); // { sub: "42", iat, exp } | null
|
|
10
13
|
*/
|
|
11
14
|
import { config } from "./helpers.js";
|
|
12
15
|
/* --------------------------- shared utilities -------------------------- */
|
|
@@ -104,3 +107,84 @@ export const encryption = {
|
|
|
104
107
|
}
|
|
105
108
|
},
|
|
106
109
|
};
|
|
110
|
+
const DURATION = /^(\d+)\s*(s|m|h|d)$/;
|
|
111
|
+
const UNIT = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
112
|
+
/** Coerce a lifetime to seconds — a bare number passes through; `"1h"` → 3600. */
|
|
113
|
+
function seconds(value) {
|
|
114
|
+
if (typeof value === "number")
|
|
115
|
+
return value;
|
|
116
|
+
const match = DURATION.exec(value.trim());
|
|
117
|
+
if (!match)
|
|
118
|
+
throw new Error(`Invalid duration "${value}" (use e.g. 30, "30s", "15m", "1h", "7d").`);
|
|
119
|
+
return Number(match[1]) * UNIT[match[2]];
|
|
120
|
+
}
|
|
121
|
+
/* base64url — JWT segments use the URL-safe alphabet with no padding. */
|
|
122
|
+
function b64url(bytes) {
|
|
123
|
+
return b64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
124
|
+
}
|
|
125
|
+
function fromB64url(str) {
|
|
126
|
+
const pad = str.length % 4 ? "=".repeat(4 - (str.length % 4)) : "";
|
|
127
|
+
return fromB64(str.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
|
128
|
+
}
|
|
129
|
+
function b64urlJson(value) {
|
|
130
|
+
return b64url(new TextEncoder().encode(JSON.stringify(value)));
|
|
131
|
+
}
|
|
132
|
+
/** HMAC-SHA256 the signing input, returned base64url — the JWT signature. */
|
|
133
|
+
async function hmacSha256(data, secret) {
|
|
134
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
135
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
|
|
136
|
+
return b64url(new Uint8Array(sig));
|
|
137
|
+
}
|
|
138
|
+
const JWT_HEADER = b64urlJson({ alg: "HS256", typ: "JWT" });
|
|
139
|
+
export const jwt = {
|
|
140
|
+
/** Sign a payload into an HS256 JWT. Adds `iat`, and `exp` when `expiresIn` is set. */
|
|
141
|
+
async sign(payload, options = {}) {
|
|
142
|
+
const now = Math.floor(Date.now() / 1000);
|
|
143
|
+
const claims = { ...payload, iat: now };
|
|
144
|
+
if (options.subject !== undefined)
|
|
145
|
+
claims.sub = options.subject;
|
|
146
|
+
if (options.issuer !== undefined)
|
|
147
|
+
claims.iss = options.issuer;
|
|
148
|
+
if (options.audience !== undefined)
|
|
149
|
+
claims.aud = options.audience;
|
|
150
|
+
if (options.expiresIn !== undefined)
|
|
151
|
+
claims.exp = now + seconds(options.expiresIn);
|
|
152
|
+
const body = `${JWT_HEADER}.${b64urlJson(claims)}`;
|
|
153
|
+
const sig = await hmacSha256(body, options.secret ?? appKey());
|
|
154
|
+
return `${body}.${sig}`;
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* Verify an HS256 JWT and return its payload, or `null` if the token is
|
|
158
|
+
* malformed, tampered, expired, not-yet-valid, or fails an issuer/audience
|
|
159
|
+
* check. Only HS256 is accepted — `alg: none` and asymmetric algs are refused,
|
|
160
|
+
* closing the classic JWT algorithm-confusion hole.
|
|
161
|
+
*/
|
|
162
|
+
async verify(token, options = {}) {
|
|
163
|
+
try {
|
|
164
|
+
const parts = token.split(".");
|
|
165
|
+
if (parts.length !== 3)
|
|
166
|
+
return null;
|
|
167
|
+
const [header, body, sig] = parts;
|
|
168
|
+
const alg = JSON.parse(new TextDecoder().decode(fromB64url(header))).alg;
|
|
169
|
+
if (alg !== "HS256")
|
|
170
|
+
return null;
|
|
171
|
+
const expected = await hmacSha256(`${header}.${body}`, options.secret ?? appKey());
|
|
172
|
+
if (!safeEqual(sig, expected))
|
|
173
|
+
return null;
|
|
174
|
+
const claims = JSON.parse(new TextDecoder().decode(fromB64url(body)));
|
|
175
|
+
const now = Math.floor(Date.now() / 1000);
|
|
176
|
+
if (typeof claims.exp === "number" && now >= claims.exp)
|
|
177
|
+
return null;
|
|
178
|
+
if (typeof claims.nbf === "number" && now < claims.nbf)
|
|
179
|
+
return null;
|
|
180
|
+
if (options.issuer !== undefined && claims.iss !== options.issuer)
|
|
181
|
+
return null;
|
|
182
|
+
if (options.audience !== undefined && claims.aud !== options.audience)
|
|
183
|
+
return null;
|
|
184
|
+
return claims;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
package/dist/core/index.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export type { StaticOptions } from "./static.js";
|
|
|
18
18
|
export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
|
|
19
19
|
export type { Disk, Contents } from "./storage.js";
|
|
20
20
|
export { dump, dd } from "./debug.js";
|
|
21
|
-
export { hash, encryption } from "./crypto.js";
|
|
21
|
+
export { hash, encryption, jwt } from "./crypto.js";
|
|
22
|
+
export type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from "./crypto.js";
|
|
22
23
|
export { rateLimiter } from "./rate-limit.js";
|
|
23
24
|
export type { RateLimiterOptions } from "./rate-limit.js";
|
|
24
25
|
export { db, setConnection, QueryBuilder } from "./database.js";
|
|
@@ -62,7 +63,7 @@ export { validate, validateRequest, validated } from "./validation.js";
|
|
|
62
63
|
export type { Schema, RequestSchemas } from "./validation.js";
|
|
63
64
|
export { Session, session, sessionMiddleware } from "./session.js";
|
|
64
65
|
export type { SessionOptions } from "./session.js";
|
|
65
|
-
export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
|
|
66
|
+
export { Auth, auth, authGuard, bearerAuth, setUserProvider } from "./auth.js";
|
|
66
67
|
export type { UserProvider } from "./auth.js";
|
|
67
68
|
export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
|
|
68
69
|
export type { GateCallback, BeforeCallback } from "./authorization.js";
|
package/dist/core/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { Cache, MemoryStore } from "./cache.js";
|
|
|
10
10
|
export { serveStatic } from "./static.js";
|
|
11
11
|
export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
|
|
12
12
|
export { dump, dd } from "./debug.js";
|
|
13
|
-
export { hash, encryption } from "./crypto.js";
|
|
13
|
+
export { hash, encryption, jwt } from "./crypto.js";
|
|
14
14
|
export { rateLimiter } from "./rate-limit.js";
|
|
15
15
|
export { db, setConnection, QueryBuilder } from "./database.js";
|
|
16
16
|
export { Model } from "./model.js";
|
|
@@ -35,7 +35,7 @@ export { TestClient, TestResponse, testClient } from "./testing.js";
|
|
|
35
35
|
export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
|
|
36
36
|
export { validate, validateRequest, validated } from "./validation.js";
|
|
37
37
|
export { Session, session, sessionMiddleware } from "./session.js";
|
|
38
|
-
export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
|
|
38
|
+
export { Auth, auth, authGuard, bearerAuth, setUserProvider } from "./auth.js";
|
|
39
39
|
export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
|
|
40
40
|
export { Transformer } from "./transformer.js";
|
|
41
41
|
export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
|
package/package.json
CHANGED