@uln/auth 0.1.0 → 0.3.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/client.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export type { TokenPayload, TokenConfig, CookieConfig } from "./types.ts";
2
- export { createToken, validateToken } from "./token.ts";
1
+ export type { TokenPayload, VerifyConfig, PublicKeyEntry, CookieConfig } from "./types.ts";
2
+ export { validateToken, importPublicKey } from "./token.ts";
3
3
  export { makeSessionCookie, makeClearCookie, extractCookie } from "./cookie.ts";
4
- export { getAuthUser, redirectToLogin, getClientIP, checkBearerToken } from "./middleware.ts";
4
+ export { getAuthUser, redirectToLogin, handleCallback, getClientIP, checkBearerToken } from "./middleware.ts";
5
+ import type { PublicKeyEntry } from "./types.ts";
6
+ export declare function fetchPublicKeys(baseUrl: string): Promise<PublicKeyEntry[]>;
package/dist/client.js CHANGED
@@ -41,21 +41,24 @@ function base64urlDecode(str) {
41
41
  }
42
42
  return bytes;
43
43
  }
44
- async function hmacSign(secret, data) {
45
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
46
- "sign"
47
- ]);
48
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
49
- return base64urlEncode(new Uint8Array(signature));
50
- }
51
- function timingSafeEqual(a, b) {
52
- if (a.length !== b.length)
53
- return false;
54
- let diff = 0;
55
- for (let i = 0;i < a.length; i++) {
56
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
57
- }
58
- return diff === 0;
44
+ var ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
45
+ var SIGN_PARAMS = { name: "ECDSA", hash: "SHA-256" };
46
+ async function generateKeyPair() {
47
+ const kid = crypto.randomUUID().slice(0, 8);
48
+ const keyPair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
49
+ if (!("privateKey" in keyPair))
50
+ throw new Error("Expected CryptoKeyPair from ECDSA generateKey");
51
+ const privateExport = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
52
+ const publicExport = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
53
+ if (privateExport instanceof ArrayBuffer || publicExport instanceof ArrayBuffer)
54
+ throw new Error("Expected JWK from exportKey");
55
+ return { kid, privateKeyJwk: privateExport, publicKeyJwk: publicExport };
56
+ }
57
+ async function importPrivateKey(jwk) {
58
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["sign"]);
59
+ }
60
+ async function importPublicKey(jwk) {
61
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
59
62
  }
60
63
  async function createToken(config, user) {
61
64
  const now = Math.floor(Date.now() / 1000);
@@ -65,34 +68,49 @@ async function createToken(config, user) {
65
68
  name: user.name,
66
69
  admin: user.isAdmin,
67
70
  scopes: user.scopes,
71
+ flags: user.flags,
72
+ kid: config.kid,
68
73
  iat: now,
69
74
  exp: now + config.maxAgeSeconds
70
75
  };
71
76
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
72
- const sig = await hmacSign(config.secret, payloadB64);
73
- return `${payloadB64}.${sig}`;
77
+ const payloadBytes = encoder.encode(payloadB64);
78
+ const signature = await crypto.subtle.sign(SIGN_PARAMS, config.privateKey, payloadBytes);
79
+ const sigB64 = base64urlEncode(new Uint8Array(signature));
80
+ return `${payloadB64}.${sigB64}`;
74
81
  }
75
82
  async function validateToken(config, token) {
76
83
  const dotIndex = token.indexOf(".");
77
84
  if (dotIndex === -1)
78
85
  return null;
79
86
  const payloadB64 = token.substring(0, dotIndex);
80
- const sig = token.substring(dotIndex + 1);
81
- const expectedSig = await hmacSign(config.secret, payloadB64);
82
- if (!timingSafeEqual(sig, expectedSig))
83
- return null;
87
+ const sigB64 = token.substring(dotIndex + 1);
88
+ let payload;
84
89
  try {
85
90
  const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
86
- const payload = JSON.parse(decoded);
87
- if (typeof payload.sub !== "string" || typeof payload.exp !== "number")
88
- return null;
89
- const now = Math.floor(Date.now() / 1000);
90
- if (now > payload.exp)
91
+ payload = JSON.parse(decoded);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
96
+ return null;
97
+ }
98
+ const now = Math.floor(Date.now() / 1000);
99
+ if (now > payload.exp)
100
+ return null;
101
+ const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
+ if (!entry)
103
+ return null;
104
+ try {
105
+ const signatureBytes = base64urlDecode(sigB64);
106
+ const payloadBytes = encoder.encode(payloadB64);
107
+ const valid = await crypto.subtle.verify(SIGN_PARAMS, entry.key, signatureBytes, payloadBytes);
108
+ if (!valid)
91
109
  return null;
92
- return payload;
93
110
  } catch {
94
111
  return null;
95
112
  }
113
+ return payload;
96
114
  }
97
115
 
98
116
  // src/middleware.ts
@@ -103,27 +121,58 @@ function checkBearerToken(request, expectedToken) {
103
121
  const header = request.headers.get("Authorization");
104
122
  return header === `Bearer ${expectedToken}`;
105
123
  }
106
- async function getAuthUser(request, tokenConfig, cookieConfig) {
124
+ async function getAuthUser(request, verifyConfig, cookieConfig) {
107
125
  const cookieHeader = request.headers.get("Cookie");
108
126
  if (!cookieHeader)
109
127
  return null;
110
128
  const token = extractCookie(cookieHeader, cookieConfig.name);
111
129
  if (!token)
112
130
  return null;
113
- return validateToken(tokenConfig, token);
131
+ return validateToken(verifyConfig, token);
114
132
  }
115
133
  function redirectToLogin(currentUrl) {
116
134
  const loginUrl = `https://auth.uln.industries/login?redirect=${encodeURIComponent(currentUrl)}`;
117
135
  return Response.redirect(loginUrl, 302);
118
136
  }
137
+ async function handleCallback(request, verifyConfig, cookieConfig) {
138
+ const url = new URL(request.url);
139
+ const token = url.searchParams.get("token");
140
+ if (!token)
141
+ return null;
142
+ const payload = await validateToken(verifyConfig, token);
143
+ if (!payload) {
144
+ return redirectToLogin(url.origin);
145
+ }
146
+ url.searchParams.delete("token");
147
+ return new Response(null, {
148
+ status: 302,
149
+ headers: {
150
+ Location: url.toString(),
151
+ "Set-Cookie": makeSessionCookie(cookieConfig, token)
152
+ }
153
+ });
154
+ }
155
+ // src/client.ts
156
+ async function fetchPublicKeys(baseUrl) {
157
+ const res = await fetch(`${baseUrl}/.well-known/jwk.json`);
158
+ if (!res.ok)
159
+ throw new Error(`Failed to fetch JWKS from ${baseUrl}: ${res.status}`);
160
+ const jwks = await res.json();
161
+ return Promise.all(jwks.keys.map(async (jwk) => ({
162
+ kid: jwk.kid,
163
+ key: await importPublicKey(jwk)
164
+ })));
165
+ }
119
166
  export {
120
167
  validateToken,
121
168
  redirectToLogin,
122
169
  makeSessionCookie,
123
170
  makeClearCookie,
171
+ importPublicKey,
172
+ handleCallback,
124
173
  getClientIP,
125
174
  getAuthUser,
175
+ fetchPublicKeys,
126
176
  extractCookie,
127
- createToken,
128
177
  checkBearerToken
129
178
  };
package/dist/hono.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { Context, MiddlewareHandler } from "hono";
2
+ import type { TokenPayload } from "./types.ts";
3
+ interface AuthOptions {
4
+ /**
5
+ * Service binding name (e.g. "AUTH"). If provided, validates tokens
6
+ * via the auth worker service binding instead of fetching public keys.
7
+ */
8
+ binding?: string;
9
+ /**
10
+ * Paths that don't require authentication. Supports exact matches
11
+ * and trailing wildcard (e.g. "/api/public/*").
12
+ */
13
+ public?: string[];
14
+ /**
15
+ * Auth service base URL. Defaults to "https://auth.uln.industries".
16
+ */
17
+ authUrl?: string;
18
+ /** Cookie name. Defaults to "uln_auth". */
19
+ cookie?: string;
20
+ /** Cookie/token max age in seconds. Defaults to 86400 (24h). */
21
+ maxAge?: number;
22
+ }
23
+ /**
24
+ * Hono middleware that handles the full auth flow:
25
+ *
26
+ * 1. If `?token=` in URL → validate, set cookie, redirect to clean URL
27
+ * 2. If session cookie present → validate, set `c.get("user")`
28
+ * 3. If unauthenticated → redirect to auth.uln.industries/login
29
+ *
30
+ * Usage:
31
+ * ```ts
32
+ * import { auth } from "@uln/auth/hono";
33
+ *
34
+ * // Public key validation (default — no config needed):
35
+ * app.use("/*", auth());
36
+ *
37
+ * // Service binding (if on same CF account):
38
+ * app.use("/*", auth({ binding: "AUTH" }));
39
+ *
40
+ * // With public paths:
41
+ * app.use("/*", auth({ public: ["/health", "/api/public/*"] }));
42
+ *
43
+ * // Access user in handlers:
44
+ * app.get("/me", (c) => c.json(c.get("user")));
45
+ * ```
46
+ */
47
+ export declare function auth(options?: AuthOptions): MiddlewareHandler;
48
+ /** Type-safe helper to get the authenticated user from context. */
49
+ export declare function getUser(c: Context): TokenPayload;
50
+ export type { TokenPayload } from "./types.ts";
package/dist/hono.js ADDED
@@ -0,0 +1,242 @@
1
+ // @bun
2
+ // src/cookie.ts
3
+ function makeSessionCookie(config, token) {
4
+ const parts = [`${config.name}=${token}`, `Max-Age=${config.maxAgeSeconds}`, "Path=/", "HttpOnly", "SameSite=Lax"];
5
+ if (config.secure)
6
+ parts.push("Secure");
7
+ return parts.join("; ");
8
+ }
9
+ function makeClearCookie(config) {
10
+ const parts = [`${config.name}=`, "Max-Age=0", "Path=/", "HttpOnly", "SameSite=Lax"];
11
+ if (config.secure)
12
+ parts.push("Secure");
13
+ return parts.join("; ");
14
+ }
15
+ function extractCookie(cookieHeader, name) {
16
+ const prefix = `${name}=`;
17
+ for (const cookie of cookieHeader.split(";")) {
18
+ const trimmed = cookie.trim();
19
+ if (trimmed.startsWith(prefix)) {
20
+ return trimmed.substring(prefix.length);
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
26
+ // src/token.ts
27
+ var encoder = new TextEncoder;
28
+ function base64urlEncode(data) {
29
+ let binary = "";
30
+ for (const byte of data) {
31
+ binary += String.fromCharCode(byte);
32
+ }
33
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ }
35
+ function base64urlDecode(str) {
36
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
37
+ const binary = atob(padded);
38
+ const bytes = new Uint8Array(binary.length);
39
+ for (let i = 0;i < binary.length; i++) {
40
+ bytes[i] = binary.charCodeAt(i);
41
+ }
42
+ return bytes;
43
+ }
44
+ var ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
45
+ var SIGN_PARAMS = { name: "ECDSA", hash: "SHA-256" };
46
+ async function generateKeyPair() {
47
+ const kid = crypto.randomUUID().slice(0, 8);
48
+ const keyPair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
49
+ if (!("privateKey" in keyPair))
50
+ throw new Error("Expected CryptoKeyPair from ECDSA generateKey");
51
+ const privateExport = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
52
+ const publicExport = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
53
+ if (privateExport instanceof ArrayBuffer || publicExport instanceof ArrayBuffer)
54
+ throw new Error("Expected JWK from exportKey");
55
+ return { kid, privateKeyJwk: privateExport, publicKeyJwk: publicExport };
56
+ }
57
+ async function importPrivateKey(jwk) {
58
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["sign"]);
59
+ }
60
+ async function importPublicKey(jwk) {
61
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
62
+ }
63
+ async function createToken(config, user) {
64
+ const now = Math.floor(Date.now() / 1000);
65
+ const payload = {
66
+ sub: user.id,
67
+ email: user.email,
68
+ name: user.name,
69
+ admin: user.isAdmin,
70
+ scopes: user.scopes,
71
+ flags: user.flags,
72
+ kid: config.kid,
73
+ iat: now,
74
+ exp: now + config.maxAgeSeconds
75
+ };
76
+ const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
77
+ const payloadBytes = encoder.encode(payloadB64);
78
+ const signature = await crypto.subtle.sign(SIGN_PARAMS, config.privateKey, payloadBytes);
79
+ const sigB64 = base64urlEncode(new Uint8Array(signature));
80
+ return `${payloadB64}.${sigB64}`;
81
+ }
82
+ async function validateToken(config, token) {
83
+ const dotIndex = token.indexOf(".");
84
+ if (dotIndex === -1)
85
+ return null;
86
+ const payloadB64 = token.substring(0, dotIndex);
87
+ const sigB64 = token.substring(dotIndex + 1);
88
+ let payload;
89
+ try {
90
+ const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
91
+ payload = JSON.parse(decoded);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
96
+ return null;
97
+ }
98
+ const now = Math.floor(Date.now() / 1000);
99
+ if (now > payload.exp)
100
+ return null;
101
+ const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
+ if (!entry)
103
+ return null;
104
+ try {
105
+ const signatureBytes = base64urlDecode(sigB64);
106
+ const payloadBytes = encoder.encode(payloadB64);
107
+ const valid = await crypto.subtle.verify(SIGN_PARAMS, entry.key, signatureBytes, payloadBytes);
108
+ if (!valid)
109
+ return null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ return payload;
114
+ }
115
+
116
+ // src/hono.ts
117
+ var DEFAULT_AUTH_URL = "https://auth.uln.industries";
118
+ var DEFAULT_COOKIE = "uln_auth";
119
+ var DEFAULT_MAX_AGE = 86400;
120
+ function isPublicPath(pathname, patterns) {
121
+ for (const pattern of patterns) {
122
+ if (pattern.endsWith("/*")) {
123
+ const prefix = pattern.slice(0, -1);
124
+ if (pathname === prefix.slice(0, -1) || pathname.startsWith(prefix))
125
+ return true;
126
+ } else if (pathname === pattern) {
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+ var cachedKeys = null;
133
+ var cachedAt = 0;
134
+ var CACHE_TTL = 3600000;
135
+ async function getPublicKeys(authUrl) {
136
+ if (cachedKeys && Date.now() - cachedAt < CACHE_TTL) {
137
+ return cachedKeys;
138
+ }
139
+ const res = await fetch(`${authUrl}/.well-known/jwk.json`);
140
+ if (!res.ok)
141
+ throw new Error(`Failed to fetch JWKS from ${authUrl}: ${res.status}`);
142
+ const jwks = await res.json();
143
+ cachedKeys = await Promise.all(jwks.keys.map(async (jwk) => ({
144
+ kid: jwk.kid,
145
+ key: await importPublicKey(jwk)
146
+ })));
147
+ cachedAt = Date.now();
148
+ return cachedKeys;
149
+ }
150
+ async function validateWithPublicKeys(token, authUrl, maxAge) {
151
+ const publicKeys = await getPublicKeys(authUrl);
152
+ return validateToken({ publicKeys, maxAgeSeconds: maxAge }, token);
153
+ }
154
+ async function validateWithBinding(token, binding) {
155
+ const res = await binding.fetch("https://auth/api/verify-token", {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({ token })
159
+ });
160
+ if (!res.ok)
161
+ return null;
162
+ const data = await res.json();
163
+ return {
164
+ sub: data.user.id,
165
+ email: data.user.email,
166
+ name: data.user.name,
167
+ admin: data.user.admin,
168
+ scopes: data.user.scopes,
169
+ flags: data.user.flags,
170
+ kid: data.user.kid,
171
+ iat: 0,
172
+ exp: 0
173
+ };
174
+ }
175
+ function auth(options) {
176
+ const authUrl = options?.authUrl ?? DEFAULT_AUTH_URL;
177
+ const cookieName = options?.cookie ?? DEFAULT_COOKIE;
178
+ const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE;
179
+ const publicPaths = options?.public ?? [];
180
+ const bindingName = options?.binding;
181
+ const cookieConf = { name: cookieName, maxAgeSeconds: maxAge, secure: true };
182
+ async function validate(token, env) {
183
+ if (bindingName) {
184
+ const binding = env[bindingName];
185
+ if (!binding)
186
+ throw new Error(`Service binding "${bindingName}" not found in env`);
187
+ return validateWithBinding(token, binding);
188
+ }
189
+ return validateWithPublicKeys(token, authUrl, maxAge);
190
+ }
191
+ return async (c, next) => {
192
+ const url = new URL(c.req.url);
193
+ if (isPublicPath(url.pathname, publicPaths)) {
194
+ return next();
195
+ }
196
+ const incomingToken = url.searchParams.get("token");
197
+ if (incomingToken) {
198
+ const payload = await validate(incomingToken, c.env);
199
+ if (!payload) {
200
+ return c.text("Invalid token", 401);
201
+ }
202
+ url.searchParams.delete("token");
203
+ return new Response(null, {
204
+ status: 302,
205
+ headers: {
206
+ Location: url.toString(),
207
+ "Set-Cookie": makeSessionCookie(cookieConf, incomingToken)
208
+ }
209
+ });
210
+ }
211
+ const cookieHeader = c.req.header("Cookie");
212
+ if (cookieHeader) {
213
+ const token = extractCookie(cookieHeader, cookieName);
214
+ if (token) {
215
+ const payload = await validate(token, c.env);
216
+ if (payload) {
217
+ c.set("user", payload);
218
+ return next();
219
+ }
220
+ }
221
+ }
222
+ const authHeader = c.req.header("Authorization");
223
+ if (authHeader?.startsWith("Bearer ")) {
224
+ const token = authHeader.substring(7);
225
+ const payload = await validate(token, c.env);
226
+ if (payload) {
227
+ c.set("user", payload);
228
+ return next();
229
+ }
230
+ return c.json({ error: "Invalid token" }, 401);
231
+ }
232
+ const loginUrl = `${authUrl}/login?redirect=${encodeURIComponent(c.req.url)}`;
233
+ return c.redirect(loginUrl, 302);
234
+ };
235
+ }
236
+ function getUser(c) {
237
+ return c.get("user");
238
+ }
239
+ export {
240
+ getUser,
241
+ auth
242
+ };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export type { User, UserStore, RateLimitEntry, RateLimitStore, RateLimitConfig, TokenPayload, TokenConfig, CookieConfig, } from "./types.ts";
1
+ export type { User, UserStore, RateLimitEntry, RateLimitStore, RateLimitConfig, TokenPayload, SigningConfig, VerifyConfig, PublicKeyEntry, CookieConfig, } from "./types.ts";
2
2
  export { hashPassword, verifyPassword } from "./password.ts";
3
- export { createToken, validateToken } from "./token.ts";
3
+ export { createToken, validateToken, generateKeyPair, importPrivateKey, importPublicKey } from "./token.ts";
4
4
  export type { CreateTokenInput } from "./token.ts";
5
5
  export { makeSessionCookie, makeClearCookie, extractCookie } from "./cookie.ts";
6
6
  export { checkRateLimit, recordFailedAttempt, clearRateLimit } from "./rate-limit.ts";
package/dist/index.js CHANGED
@@ -41,21 +41,24 @@ function base64urlDecode(str) {
41
41
  }
42
42
  return bytes;
43
43
  }
44
- async function hmacSign(secret, data) {
45
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
46
- "sign"
47
- ]);
48
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
49
- return base64urlEncode(new Uint8Array(signature));
50
- }
51
- function timingSafeEqual(a, b) {
52
- if (a.length !== b.length)
53
- return false;
54
- let diff = 0;
55
- for (let i = 0;i < a.length; i++) {
56
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
57
- }
58
- return diff === 0;
44
+ var ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
45
+ var SIGN_PARAMS = { name: "ECDSA", hash: "SHA-256" };
46
+ async function generateKeyPair() {
47
+ const kid = crypto.randomUUID().slice(0, 8);
48
+ const keyPair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
49
+ if (!("privateKey" in keyPair))
50
+ throw new Error("Expected CryptoKeyPair from ECDSA generateKey");
51
+ const privateExport = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
52
+ const publicExport = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
53
+ if (privateExport instanceof ArrayBuffer || publicExport instanceof ArrayBuffer)
54
+ throw new Error("Expected JWK from exportKey");
55
+ return { kid, privateKeyJwk: privateExport, publicKeyJwk: publicExport };
56
+ }
57
+ async function importPrivateKey(jwk) {
58
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["sign"]);
59
+ }
60
+ async function importPublicKey(jwk) {
61
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
59
62
  }
60
63
  async function createToken(config, user) {
61
64
  const now = Math.floor(Date.now() / 1000);
@@ -65,34 +68,49 @@ async function createToken(config, user) {
65
68
  name: user.name,
66
69
  admin: user.isAdmin,
67
70
  scopes: user.scopes,
71
+ flags: user.flags,
72
+ kid: config.kid,
68
73
  iat: now,
69
74
  exp: now + config.maxAgeSeconds
70
75
  };
71
76
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
72
- const sig = await hmacSign(config.secret, payloadB64);
73
- return `${payloadB64}.${sig}`;
77
+ const payloadBytes = encoder.encode(payloadB64);
78
+ const signature = await crypto.subtle.sign(SIGN_PARAMS, config.privateKey, payloadBytes);
79
+ const sigB64 = base64urlEncode(new Uint8Array(signature));
80
+ return `${payloadB64}.${sigB64}`;
74
81
  }
75
82
  async function validateToken(config, token) {
76
83
  const dotIndex = token.indexOf(".");
77
84
  if (dotIndex === -1)
78
85
  return null;
79
86
  const payloadB64 = token.substring(0, dotIndex);
80
- const sig = token.substring(dotIndex + 1);
81
- const expectedSig = await hmacSign(config.secret, payloadB64);
82
- if (!timingSafeEqual(sig, expectedSig))
83
- return null;
87
+ const sigB64 = token.substring(dotIndex + 1);
88
+ let payload;
84
89
  try {
85
90
  const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
86
- const payload = JSON.parse(decoded);
87
- if (typeof payload.sub !== "string" || typeof payload.exp !== "number")
88
- return null;
89
- const now = Math.floor(Date.now() / 1000);
90
- if (now > payload.exp)
91
+ payload = JSON.parse(decoded);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
96
+ return null;
97
+ }
98
+ const now = Math.floor(Date.now() / 1000);
99
+ if (now > payload.exp)
100
+ return null;
101
+ const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
+ if (!entry)
103
+ return null;
104
+ try {
105
+ const signatureBytes = base64urlDecode(sigB64);
106
+ const payloadBytes = encoder.encode(payloadB64);
107
+ const valid = await crypto.subtle.verify(SIGN_PARAMS, entry.key, signatureBytes, payloadBytes);
108
+ if (!valid)
91
109
  return null;
92
- return payload;
93
110
  } catch {
94
111
  return null;
95
112
  }
113
+ return payload;
96
114
  }
97
115
 
98
116
  // src/middleware.ts
@@ -103,19 +121,37 @@ function checkBearerToken(request, expectedToken) {
103
121
  const header = request.headers.get("Authorization");
104
122
  return header === `Bearer ${expectedToken}`;
105
123
  }
106
- async function getAuthUser(request, tokenConfig, cookieConfig) {
124
+ async function getAuthUser(request, verifyConfig, cookieConfig) {
107
125
  const cookieHeader = request.headers.get("Cookie");
108
126
  if (!cookieHeader)
109
127
  return null;
110
128
  const token = extractCookie(cookieHeader, cookieConfig.name);
111
129
  if (!token)
112
130
  return null;
113
- return validateToken(tokenConfig, token);
131
+ return validateToken(verifyConfig, token);
114
132
  }
115
133
  function redirectToLogin(currentUrl) {
116
134
  const loginUrl = `https://auth.uln.industries/login?redirect=${encodeURIComponent(currentUrl)}`;
117
135
  return Response.redirect(loginUrl, 302);
118
136
  }
137
+ async function handleCallback(request, verifyConfig, cookieConfig) {
138
+ const url = new URL(request.url);
139
+ const token = url.searchParams.get("token");
140
+ if (!token)
141
+ return null;
142
+ const payload = await validateToken(verifyConfig, token);
143
+ if (!payload) {
144
+ return redirectToLogin(url.origin);
145
+ }
146
+ url.searchParams.delete("token");
147
+ return new Response(null, {
148
+ status: 302,
149
+ headers: {
150
+ Location: url.toString(),
151
+ "Set-Cookie": makeSessionCookie(cookieConfig, token)
152
+ }
153
+ });
154
+ }
119
155
 
120
156
  // src/password.ts
121
157
  var SALT_LENGTH = 32;
@@ -237,9 +273,12 @@ export {
237
273
  recordFailedAttempt,
238
274
  makeSessionCookie,
239
275
  makeClearCookie,
276
+ importPublicKey,
277
+ importPrivateKey,
240
278
  hashPassword,
241
279
  getClientIP,
242
280
  getAuthUser,
281
+ generateKeyPair,
243
282
  extractCookie,
244
283
  createUser,
245
284
  createToken,
@@ -1,5 +1,12 @@
1
- import type { CookieConfig, TokenConfig, TokenPayload } from "./types.ts";
1
+ import type { CookieConfig, TokenPayload, VerifyConfig } from "./types.ts";
2
2
  export declare function getClientIP(request: Request): string;
3
3
  export declare function checkBearerToken(request: Request, expectedToken: string): boolean;
4
- export declare function getAuthUser(request: Request, tokenConfig: TokenConfig, cookieConfig: CookieConfig): Promise<TokenPayload | null>;
4
+ export declare function getAuthUser(request: Request, verifyConfig: VerifyConfig, cookieConfig: CookieConfig): Promise<TokenPayload | null>;
5
5
  export declare function redirectToLogin(currentUrl: string): Response;
6
+ /**
7
+ * Handle the auth callback on consuming sites.
8
+ * If the request URL has a `?token=` param, validates it, sets a session cookie,
9
+ * and redirects to the same URL without the token param.
10
+ * Returns null if there's no token param (not a callback request).
11
+ */
12
+ export declare function handleCallback(request: Request, verifyConfig: VerifyConfig, cookieConfig: CookieConfig): Promise<Response | null>;