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