@baseworks/auth 0.2.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # @baseworks/auth
2
+
3
+ Authentication utilities for the baseworks monorepo. Uses subpath imports so each environment only pulls in what it needs.
4
+
5
+ ## Subpath Imports
6
+
7
+ | Import | Runtime | Contents |
8
+ |--------|---------|----------|
9
+ | `@baseworks/auth` | Any | token helpers, verifyHs256Jwt, verifyOidcToken, pkce, cli-auth, url-helpers |
10
+ | `@baseworks/auth/jwt` | Node.js | HS256 JWT verification |
11
+ | `@baseworks/auth/hono` | Node.js + Hono | `requireAuth` middleware |
12
+ | `@baseworks/auth/oidc` | Workers / Node / Browser | OIDC token verification via JWKS |
13
+ | `@baseworks/auth/oidc-human` | Any | OIDC → DB user resolver factory |
14
+ | `@baseworks/auth/pkce` | Workers / Node / Browser | PKCE generation + OIDC auth URL builder |
15
+ | `@baseworks/auth/token` | Workers / Node / Browser | hashToken, looksLikeJwt, stripBearer |
16
+ | `@baseworks/auth/cli-auth` | Node.js CLI | Browser-based polling auth flow |
17
+ | `@baseworks/auth/session` | Next.js (Server) | OIDC cookie session, token exchange, refresh |
18
+ | `@baseworks/auth/edge` | Next.js (Edge/Server) | Pomerium JWT assertions + cookie fallback |
19
+ | `@baseworks/auth/url-helpers` | Next.js | Auth/account public URL builders |
20
+
21
+ ---
22
+
23
+ ## File Reference
24
+
25
+ ### `jwt.ts` → `@baseworks/auth/jwt`
26
+ **Runtime:** Node.js — uses `node:crypto`
27
+
28
+ HS256 JWT verification. Used by Flect services (`org-service`, `account-service`) to verify service-to-service JWTs signed with `JWT_SECRET`.
29
+
30
+ ```ts
31
+ import { verifyHs256Jwt } from '@baseworks/auth/jwt'
32
+
33
+ const claims = verifyHs256Jwt(token, process.env.JWT_SECRET)
34
+ // null → invalid signature or expired
35
+ ```
36
+
37
+ ---
38
+
39
+ ### `hono.ts` → `@baseworks/auth/hono`
40
+ **Runtime:** Node.js + Hono — peer dep: `hono`
41
+
42
+ Hono middleware. Reads `Authorization: Bearer <token>`, verifies with `jwt.ts`, sets `c.var.auth` (typed as `ServiceAuthUser`).
43
+
44
+ ```ts
45
+ import { requireAuth } from '@baseworks/auth/hono'
46
+
47
+ app.use('*', requireAuth)
48
+ app.get('/me', (c) => {
49
+ const { userId, orgId } = c.var.auth
50
+ })
51
+ ```
52
+
53
+ > `ContextVariableMap` is augmented here — do not re-declare it in services.
54
+
55
+ ---
56
+
57
+ ### `oidc.ts` → `@baseworks/auth/oidc`
58
+ **Runtime:** Workers / Node / Browser — dep: `jose`
59
+
60
+ Provider-agnostic OIDC JWT verification. Fetches the public key from the JWKS endpoint, verifies the signature, and returns a normalised `OidcIdentity`.
61
+
62
+ ```ts
63
+ import { verifyOidcToken } from '@baseworks/auth/oidc'
64
+
65
+ const identity = await verifyOidcToken(bearerToken, {
66
+ issuer: 'https://auth.example.com',
67
+ audience: 'my-app',
68
+ })
69
+ // null → verification failed
70
+ ```
71
+
72
+ JWKS endpoint: `{issuer}/oauth/v2/keys` (Zitadel-compatible).
73
+
74
+ ---
75
+
76
+ ### `oidc-human.ts` → `@baseworks/auth/oidc-human`
77
+ **Runtime:** Any — dep: `oidc.ts`, `jose`
78
+
79
+ Combines `verifyOidcToken` with a DB user upsert into a single resolver. The `findOrCreate` callback stays app-specific.
80
+
81
+ ```ts
82
+ import { createOidcHumanResolver } from '@baseworks/auth/oidc-human'
83
+
84
+ const resolve = createOidcHumanResolver({
85
+ issuer: process.env.OIDC_ISSUER,
86
+ findOrCreate: async (identity) => db.users.upsert(identity),
87
+ })
88
+
89
+ const user = await resolve(bearerToken) // null → verification failed
90
+ ```
91
+
92
+ Fast-fail: if the issuer doesn't match it returns null before hitting the JWKS endpoint.
93
+
94
+ ---
95
+
96
+ ### `pkce.ts` → `@baseworks/auth/pkce`
97
+ **Runtime:** Workers / Node / Browser — dep: Web Crypto API, `@baseworks/core`
98
+
99
+ PKCE (RFC 7636) `code_verifier` + `code_challenge` generation. OIDC authorization URL builder compatible with any provider (Zitadel, Auth0, Keycloak, etc.).
100
+
101
+ ```ts
102
+ import { generatePkce, buildOidcAuthUrl } from '@baseworks/auth/pkce'
103
+
104
+ const { verifier, challenge } = await generatePkce()
105
+ const url = buildOidcAuthUrl({ issuer, clientId, redirectUri, challenge })
106
+ ```
107
+
108
+ ---
109
+
110
+ ### `token.ts` → `@baseworks/auth/token`
111
+ **Runtime:** Workers / Node / Browser — dep: Web Crypto API
112
+
113
+ General-purpose token helpers.
114
+
115
+ ```ts
116
+ import { hashToken, looksLikeJwt, stripBearer } from '@baseworks/auth/token'
117
+
118
+ const hash = await hashToken(rawToken, 'optional-pepper') // SHA-256 hex
119
+ const raw = stripBearer(request.headers.get('Authorization')) // null if missing
120
+ const isJwt = looksLikeJwt(token) // true if 3-part dot-separated
121
+ ```
122
+
123
+ ---
124
+
125
+ ### `cli-auth.ts` → `@baseworks/auth/cli-auth`
126
+ **Runtime:** Node.js CLI — dep: `fetch`
127
+
128
+ CLI → browser auth polling flow. Used by commands like `flect login`. The user opens a URL in the browser; the CLI polls until auth completes.
129
+
130
+ ```ts
131
+ import { startCliAuth, pollCliAuth } from '@baseworks/auth/cli-auth'
132
+
133
+ const { state, url } = await startCliAuth('https://api.flect.run')
134
+ console.log(`Open: ${url}`)
135
+ const { token } = await pollCliAuth('https://api.flect.run', state)
136
+ ```
137
+
138
+ ---
139
+
140
+ ### `session.ts` → `@baseworks/auth/session`
141
+ **Runtime:** Next.js (Server Components, Route Handlers) — dep: `next`
142
+
143
+ Full OIDC PKCE session management for Next.js. Handles cookie set/clear, authorization code exchange, and token refresh. Uses `@baseworks/core` for base64url encoding and `pkce.ts` for PKCE generation.
144
+
145
+ ```ts
146
+ import {
147
+ buildAuthorizationRedirect,
148
+ handleAuthorizationCallback,
149
+ getSessionFromCookies,
150
+ getServerAccessToken,
151
+ buildLogoutResponse,
152
+ } from '@baseworks/auth/session'
153
+ ```
154
+
155
+ Reads env vars: `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` (optional).
156
+
157
+ ---
158
+
159
+ ### `edge.ts` → `@baseworks/auth/edge`
160
+ **Runtime:** Next.js (Middleware, Server Components) — dep: `next`
161
+
162
+ Reads the session from two sources in priority order: **Pomerium JWT assertion headers** (`x-pomerium-jwt-assertion`) first, then **OIDC cookie** fallback. Returns an `EdgeSession`.
163
+
164
+ ```ts
165
+ import { getEdgeSession } from '@baseworks/auth/edge'
166
+
167
+ const session = await getEdgeSession()
168
+ // session.provider: 'pomerium' | 'cookie' | 'anonymous'
169
+ ```
170
+
171
+ ---
172
+
173
+ ### `url-helpers.ts` → `@baseworks/auth/url-helpers`
174
+ **Runtime:** Next.js — dep: none
175
+
176
+ Reads Next.js env vars and exposes URL builder functions.
177
+
178
+ ```ts
179
+ import { buildAuthLoginUrl, buildAuthLogoutUrl } from '@baseworks/auth/url-helpers'
180
+
181
+ const loginUrl = buildAuthLoginUrl('/dashboard')
182
+ const logoutUrl = buildAuthLogoutUrl('/')
183
+ ```
184
+
185
+ Env vars read: `NEXT_PUBLIC_AUTH_URL`, `NEXT_PUBLIC_ACCOUNT_URL`, `APP_PUBLIC_URL`, `NEXT_PUBLIC_SITE_URL`.
186
+
187
+ ---
188
+
189
+ ### `_internal.ts` — not exported
190
+ Shared internal helpers. Not part of the public API.
191
+
192
+ - `parseJwtPayload(token)` — splits a JWT and decodes the payload using `@baseworks/core/codec`. Used by `session.ts` and `edge.ts`.
193
+
194
+ ---
195
+
196
+ ## Internal Dependencies
197
+
198
+ ```
199
+ @baseworks/core/codec
200
+ └── base64urlEncode / base64urlDecodeString
201
+ ├── pkce.ts (PKCE verifier/challenge generation)
202
+ ├── session.ts (cookie encode/decode)
203
+ └── _internal.ts (parseJwtPayload)
204
+ └── edge.ts
205
+ ```
206
+
207
+ ## When to Use What
208
+
209
+ ```
210
+ Service-to-service JWT verification (Hono/Node)
211
+ └─ @baseworks/auth/hono → requireAuth middleware
212
+
213
+ Raw HS256 JWT verify
214
+ └─ @baseworks/auth/jwt → verifyHs256Jwt
215
+
216
+ User OIDC token verification (Workers/Node)
217
+ └─ @baseworks/auth/oidc → verifyOidcToken
218
+
219
+ OIDC token + DB user sync in one step
220
+ └─ @baseworks/auth/oidc-human → createOidcHumanResolver
221
+
222
+ API key hashing / Bearer header parsing
223
+ └─ @baseworks/auth/token → hashToken / stripBearer
224
+
225
+ Next.js OIDC login / callback / logout
226
+ └─ @baseworks/auth/session → buildAuthorizationRedirect / handleAuthorizationCallback
227
+
228
+ Who is this user in Next.js Middleware?
229
+ └─ @baseworks/auth/edge → getEdgeSession
230
+
231
+ CLI browser auth
232
+ └─ @baseworks/auth/cli-auth → startCliAuth / pollCliAuth
233
+ ```
@@ -1,11 +1,9 @@
1
1
  // src/pkce.ts
2
+ import { base64urlEncode } from "@baseworks/core";
2
3
  async function generatePkce() {
3
- const array = new Uint8Array(32);
4
- crypto.getRandomValues(array);
5
- const verifier = base64url(array);
6
- const encoded = new TextEncoder().encode(verifier);
7
- const digest = await crypto.subtle.digest("SHA-256", encoded);
8
- const challenge = base64url(new Uint8Array(digest));
4
+ const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
5
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
6
+ const challenge = base64urlEncode(new Uint8Array(digest));
9
7
  return { verifier, challenge, method: "S256" };
10
8
  }
11
9
  function buildOidcAuthUrl(config) {
@@ -23,9 +21,6 @@ function buildOidcAuthUrl(config) {
23
21
  if (config.prompt) params.set("prompt", config.prompt);
24
22
  return `${issuer}/oauth/v2/authorize?${params.toString()}`;
25
23
  }
26
- function base64url(buf) {
27
- return btoa(Array.from(buf, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
28
- }
29
24
 
30
25
  export {
31
26
  generatePkce,
@@ -1,3 +1,6 @@
1
+ import {
2
+ generatePkce
3
+ } from "./chunk-34E5PYNA.js";
1
4
  import {
2
5
  getAuthPublicUrl,
3
6
  normalizeUrlLike
@@ -6,6 +9,7 @@ import {
6
9
  // src/session.ts
7
10
  import { cookies } from "next/headers";
8
11
  import { NextResponse } from "next/server";
12
+ import { base64urlEncode, base64urlDecodeString } from "@baseworks/core";
9
13
  var oidcCookies = {
10
14
  codeVerifier: "oidc_code_verifier",
11
15
  state: "oidc_state",
@@ -56,26 +60,8 @@ function resolvePublicRedirect(target, request) {
56
60
  if (publicSiteUrl) return new URL(target, publicSiteUrl);
57
61
  return new URL(target, request.url);
58
62
  }
59
- function base64UrlEncode(input) {
60
- const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input instanceof Uint8Array ? input : new Uint8Array(input));
61
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
62
- }
63
- function base64UrlDecode(input) {
64
- const padded = input.replace(/-/g, "+").replace(/_/g, "/");
65
- const remainder = padded.length % 4;
66
- const normalized = remainder === 0 ? padded : `${padded}${"=".repeat(4 - remainder)}`;
67
- return Buffer.from(normalized, "base64").toString("utf8");
68
- }
69
- function sha256(input) {
70
- return crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
71
- }
72
63
  function randomString() {
73
- return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
74
- }
75
- async function createPkcePair() {
76
- const verifier = randomString();
77
- const challenge = base64UrlEncode(await sha256(verifier));
78
- return { challenge, verifier };
64
+ return base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
79
65
  }
80
66
  async function discoverOidc() {
81
67
  const issuer = getIssuer();
@@ -84,26 +70,26 @@ async function discoverOidc() {
84
70
  return await response.json();
85
71
  }
86
72
  function parseJwtPayload(token) {
87
- const [, payload = ""] = token.split(".");
88
- return JSON.parse(base64UrlDecode(payload));
73
+ const part = token.split(".")[1] ?? "";
74
+ return JSON.parse(base64urlDecodeString(part));
89
75
  }
90
76
  function encodeSessionCookie(session) {
91
- return base64UrlEncode(JSON.stringify(session));
77
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(session)));
92
78
  }
93
79
  function parseSessionCookie(value) {
94
80
  try {
95
- const session = JSON.parse(base64UrlDecode(value));
81
+ const session = JSON.parse(base64urlDecodeString(value));
96
82
  return session.isAuthenticated ? session : null;
97
83
  } catch {
98
84
  return null;
99
85
  }
100
86
  }
101
87
  function encodeTransactionCookie(transaction) {
102
- return base64UrlEncode(JSON.stringify(transaction));
88
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(transaction)));
103
89
  }
104
90
  function parseTransactionCookie(value) {
105
91
  try {
106
- const transaction = JSON.parse(base64UrlDecode(value));
92
+ const transaction = JSON.parse(base64urlDecodeString(value));
107
93
  if (!transaction.codeVerifier || !transaction.returnTo) return null;
108
94
  return transaction;
109
95
  } catch {
@@ -142,7 +128,7 @@ function getRedirectUri(request) {
142
128
  }
143
129
  function appendClientAuthentication(headers, params, clientId, clientSecret) {
144
130
  if (clientSecret) {
145
- const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
131
+ const basic = btoa(`${clientId}:${clientSecret}`);
146
132
  headers.set("authorization", `Basic ${basic}`);
147
133
  return;
148
134
  }
@@ -235,7 +221,7 @@ function clearTransientCookies(response, request) {
235
221
  async function buildAuthorizationRedirect(request) {
236
222
  const discovery = await discoverOidc();
237
223
  const clientId = getClientId();
238
- const { challenge, verifier } = await createPkcePair();
224
+ const { challenge, verifier } = await generatePkce();
239
225
  const nonce = randomString();
240
226
  const state = randomString();
241
227
  const redirectUri = getRedirectUri(request);
@@ -0,0 +1,23 @@
1
+ // src/jwt.ts
2
+ import { createHmac, timingSafeEqual } from "crypto";
3
+ function verifyHs256Jwt(token, secret) {
4
+ const parts = token.split(".");
5
+ if (parts.length !== 3) return null;
6
+ const [header, payload, sig] = parts;
7
+ const expected = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
8
+ const sigBuf = Buffer.from(sig, "base64url");
9
+ const expBuf = Buffer.from(expected, "base64url");
10
+ if (sigBuf.length !== expBuf.length) return null;
11
+ if (!timingSafeEqual(sigBuf, expBuf)) return null;
12
+ try {
13
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString());
14
+ if (claims.exp && claims.exp < Math.floor(Date.now() / 1e3)) return null;
15
+ return claims;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export {
22
+ verifyHs256Jwt
23
+ };
package/dist/edge.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getSessionFromCookies
3
- } from "./chunk-VUB4GTMI.js";
3
+ } from "./chunk-NBKMWUCN.js";
4
+ import "./chunk-34E5PYNA.js";
4
5
  import {
5
6
  buildPublicUrl,
6
7
  normalizeUrlLike
@@ -9,6 +10,20 @@ import {
9
10
  // src/edge.ts
10
11
  import { headers } from "next/headers";
11
12
  import { redirect } from "next/navigation";
13
+
14
+ // src/_internal.ts
15
+ import { base64urlDecodeString } from "@baseworks/core";
16
+ function parseJwtPayload(token) {
17
+ try {
18
+ const part = token.split(".")[1];
19
+ if (!part) return null;
20
+ return JSON.parse(base64urlDecodeString(part));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ // src/edge.ts
12
27
  function firstHeader(headerStore, names) {
13
28
  for (const name of names) {
14
29
  const value = headerStore.get(name);
@@ -16,20 +31,6 @@ function firstHeader(headerStore, names) {
16
31
  }
17
32
  return void 0;
18
33
  }
19
- function decodeBase64Url(value) {
20
- const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
21
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
22
- return Buffer.from(padded, "base64").toString("utf8");
23
- }
24
- function parseJwtPayload(token) {
25
- try {
26
- const [, payload] = token.split(".");
27
- if (!payload) return null;
28
- return JSON.parse(decodeBase64Url(payload));
29
- } catch {
30
- return null;
31
- }
32
- }
33
34
  function stringClaim(claims, keys) {
34
35
  for (const key of keys) {
35
36
  const value = claims[key];
package/dist/hono.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import * as hono from 'hono';
2
+ import { Context, Next } from 'hono';
3
+
4
+ interface ServiceAuthUser {
5
+ userId: string;
6
+ orgId?: string;
7
+ role?: string;
8
+ tokenType: 'user' | 'service';
9
+ }
10
+ declare module 'hono' {
11
+ interface ContextVariableMap {
12
+ auth: ServiceAuthUser;
13
+ }
14
+ }
15
+ /**
16
+ * Hono middleware — verifies HS256 JWT from Authorization: Bearer header.
17
+ * Reads JWT_SECRET from process.env.
18
+ * Sets c.var.auth on success.
19
+ */
20
+ declare function requireAuth(c: Context, next: Next): Promise<void | (Response & hono.TypedResponse<{
21
+ error: string;
22
+ }, 401, "json">) | (Response & hono.TypedResponse<{
23
+ error: string;
24
+ }, 500, "json">)>;
25
+
26
+ export { type ServiceAuthUser, requireAuth };
package/dist/hono.js ADDED
@@ -0,0 +1,24 @@
1
+ import {
2
+ verifyHs256Jwt
3
+ } from "./chunk-TXDBQMTY.js";
4
+
5
+ // src/hono.ts
6
+ async function requireAuth(c, next) {
7
+ const header = c.req.header("Authorization");
8
+ if (!header?.startsWith("Bearer ")) return c.json({ error: "Unauthorized" }, 401);
9
+ const token = header.slice(7);
10
+ const secret = process.env["JWT_SECRET"];
11
+ if (!secret) return c.json({ error: "JWT_SECRET not configured" }, 500);
12
+ const claims = verifyHs256Jwt(token, secret);
13
+ if (!claims) return c.json({ error: "Unauthorized" }, 401);
14
+ c.set("auth", {
15
+ userId: String(claims["sub"] ?? ""),
16
+ orgId: claims["org_id"],
17
+ role: claims["role"],
18
+ tokenType: claims["type"] === "service" ? "service" : "user"
19
+ });
20
+ return next();
21
+ }
22
+ export {
23
+ requireAuth
24
+ };
package/dist/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export { OidcHumanResolverConfig, createOidcHumanResolver } from './oidc-human.j
3
3
  export { OidcAuthUrlConfig, PkceChallenge, buildOidcAuthUrl, generatePkce } from './pkce.js';
4
4
  export { CliAuthResult, CliAuthStart, PollOptions, pollCliAuth, startCliAuth } from './cli-auth.js';
5
5
  export { hashToken, looksLikeJwt, stripBearer } from './token.js';
6
+ export { JwtClaims, verifyHs256Jwt } from './jwt.js';
6
7
  export { buildAccountProfileUrl, buildAuthLoginUrl, buildAuthLogoutUrl, buildPublicUrl, getAccountPublicUrl, getAppBasePath, getAuthPublicUrl, getPublicSiteUrl, normalizeUrlLike } from './url-helpers.js';
7
- export { ZitadelClaims, ZitadelConfig, ZitadelIdentity, verifyZitadelToken } from './zitadel.js';
package/dist/index.js CHANGED
@@ -1,16 +1,12 @@
1
1
  import {
2
- verifyZitadelToken
3
- } from "./chunk-BMPRMOI7.js";
2
+ verifyHs256Jwt
3
+ } from "./chunk-TXDBQMTY.js";
4
4
  import {
5
5
  createOidcHumanResolver
6
6
  } from "./chunk-6TUNNS2B.js";
7
7
  import {
8
8
  verifyOidcToken
9
9
  } from "./chunk-BL74TFCV.js";
10
- import {
11
- buildOidcAuthUrl,
12
- generatePkce
13
- } from "./chunk-M7EACPIB.js";
14
10
  import {
15
11
  pollCliAuth,
16
12
  startCliAuth
@@ -20,6 +16,10 @@ import {
20
16
  looksLikeJwt,
21
17
  stripBearer
22
18
  } from "./chunk-VBIQJKUU.js";
19
+ import {
20
+ buildOidcAuthUrl,
21
+ generatePkce
22
+ } from "./chunk-34E5PYNA.js";
23
23
  import {
24
24
  buildAccountProfileUrl,
25
25
  buildAuthLoginUrl,
@@ -49,6 +49,6 @@ export {
49
49
  pollCliAuth,
50
50
  startCliAuth,
51
51
  stripBearer,
52
- verifyOidcToken,
53
- verifyZitadelToken
52
+ verifyHs256Jwt,
53
+ verifyOidcToken
54
54
  };
package/dist/jwt.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ interface JwtClaims {
2
+ sub?: string;
3
+ exp?: number;
4
+ iat?: number;
5
+ org_id?: string;
6
+ role?: string;
7
+ type?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * Verify an HS256 JWT using Node.js crypto.
12
+ * Returns decoded claims on success, null on any failure.
13
+ * Runtime: Node.js only (uses `crypto` module).
14
+ */
15
+ declare function verifyHs256Jwt(token: string, secret: string): JwtClaims | null;
16
+
17
+ export { type JwtClaims, verifyHs256Jwt };
package/dist/jwt.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ verifyHs256Jwt
3
+ } from "./chunk-TXDBQMTY.js";
4
+ export {
5
+ verifyHs256Jwt
6
+ };
package/dist/pkce.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * PKCE (Proof Key for Code Exchange) utilities — RFC 7636.
3
- * Runtime-agnostic: Workers, Node 20+, browser (Web Crypto API).
4
- */
5
1
  interface PkceChallenge {
6
2
  verifier: string;
7
3
  challenge: string;
package/dist/pkce.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildOidcAuthUrl,
3
3
  generatePkce
4
- } from "./chunk-M7EACPIB.js";
4
+ } from "./chunk-34E5PYNA.js";
5
5
  export {
6
6
  buildOidcAuthUrl,
7
7
  generatePkce
package/dist/session.js CHANGED
@@ -9,7 +9,8 @@ import {
9
9
  handleAuthorizationCallback,
10
10
  hasServerOidcSession,
11
11
  oidcCookies
12
- } from "./chunk-VUB4GTMI.js";
12
+ } from "./chunk-NBKMWUCN.js";
13
+ import "./chunk-34E5PYNA.js";
13
14
  import "./chunk-3NJASEF4.js";
14
15
  export {
15
16
  buildAuthorizationRedirect,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baseworks/auth",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -12,7 +12,8 @@
12
12
  "./session": "./dist/session.js",
13
13
  "./url-helpers": "./dist/url-helpers.js",
14
14
  "./edge": "./dist/edge.js",
15
- "./zitadel": "./dist/zitadel.js"
15
+ "./jwt": "./dist/jwt.js",
16
+ "./hono": "./dist/hono.js"
16
17
  },
17
18
  "files": [
18
19
  "dist",
@@ -23,20 +24,25 @@
23
24
  "typecheck": "tsc --noEmit"
24
25
  },
25
26
  "dependencies": {
26
- "@baseworks/core": "^0.2.0",
27
+ "@baseworks/core": "workspace:*",
27
28
  "jose": "^6.2.3"
28
29
  },
29
30
  "peerDependencies": {
31
+ "hono": ">=4.0.0",
30
32
  "next": ">=15.0.0"
31
33
  },
32
34
  "peerDependenciesMeta": {
33
35
  "next": {
34
36
  "optional": true
37
+ },
38
+ "hono": {
39
+ "optional": true
35
40
  }
36
41
  },
37
42
  "devDependencies": {
38
43
  "@baseworks/tsconfig": "workspace:*",
39
44
  "@types/node": "^26.0.1",
45
+ "hono": "^4.12.27",
40
46
  "tsup": "^8.5.1",
41
47
  "typescript": "^5.6.0"
42
48
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared internal utilities — not exported from package public API.
3
+ */
4
+ import { base64urlDecodeString } from '@baseworks/core'
5
+
6
+ export function parseJwtPayload(token: string): Record<string, unknown> | null {
7
+ try {
8
+ const part = token.split('.')[1]
9
+ if (!part) return null
10
+ return JSON.parse(base64urlDecodeString(part)) as Record<string, unknown>
11
+ } catch {
12
+ return null
13
+ }
14
+ }
package/src/edge.ts CHANGED
@@ -5,6 +5,7 @@ import { headers } from "next/headers";
5
5
  import { redirect } from "next/navigation";
6
6
  import { getSessionFromCookies, type OidcSession } from "./session";
7
7
  import { buildPublicUrl, normalizeUrlLike } from "./url-helpers";
8
+ import { parseJwtPayload } from "./_internal.js";
8
9
 
9
10
  export type EdgeAuthProvider = "anonymous" | "cookie" | "pomerium";
10
11
 
@@ -24,22 +25,6 @@ function firstHeader(headerStore: Headers, names: string[]) {
24
25
  return undefined;
25
26
  }
26
27
 
27
- function decodeBase64Url(value: string) {
28
- const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
29
- const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
30
- return Buffer.from(padded, "base64").toString("utf8");
31
- }
32
-
33
- function parseJwtPayload(token: string): Record<string, unknown> | null {
34
- try {
35
- const [, payload] = token.split(".");
36
- if (!payload) return null;
37
- return JSON.parse(decodeBase64Url(payload)) as Record<string, unknown>;
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
28
  function stringClaim(claims: Record<string, unknown>, keys: string[]) {
44
29
  for (const key of keys) {
45
30
  const value = claims[key];
package/src/hono.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { Context, Next } from 'hono'
2
+ import { verifyHs256Jwt } from './jwt.js'
3
+
4
+ export interface ServiceAuthUser {
5
+ userId: string
6
+ orgId?: string
7
+ role?: string
8
+ tokenType: 'user' | 'service'
9
+ }
10
+
11
+ declare module 'hono' {
12
+ interface ContextVariableMap {
13
+ auth: ServiceAuthUser
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Hono middleware — verifies HS256 JWT from Authorization: Bearer header.
19
+ * Reads JWT_SECRET from process.env.
20
+ * Sets c.var.auth on success.
21
+ */
22
+ export async function requireAuth(c: Context, next: Next) {
23
+ const header = c.req.header('Authorization')
24
+ if (!header?.startsWith('Bearer ')) return c.json({ error: 'Unauthorized' }, 401)
25
+
26
+ const token = header.slice(7)
27
+ const secret = process.env['JWT_SECRET']
28
+ if (!secret) return c.json({ error: 'JWT_SECRET not configured' }, 500)
29
+
30
+ const claims = verifyHs256Jwt(token, secret)
31
+ if (!claims) return c.json({ error: 'Unauthorized' }, 401)
32
+
33
+ c.set('auth', {
34
+ userId: String(claims['sub'] ?? ''),
35
+ orgId: claims['org_id'] as string | undefined,
36
+ role: claims['role'] as string | undefined,
37
+ tokenType: claims['type'] === 'service' ? 'service' : 'user',
38
+ })
39
+ return next()
40
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,10 @@ export type { CliAuthStart, CliAuthResult, PollOptions } from './cli-auth';
17
17
  // Token utilities
18
18
  export { hashToken, looksLikeJwt, stripBearer } from './token';
19
19
 
20
+ // HS256 JWT verification (Node.js)
21
+ export { verifyHs256Jwt } from './jwt';
22
+ export type { JwtClaims } from './jwt';
23
+
20
24
  // URL helpers (auth/account public URLs, login/logout URL builders)
21
25
  export {
22
26
  normalizeUrlLike,
@@ -34,6 +38,3 @@ export {
34
38
  // Use subpath imports: @baseworks/auth/session, @baseworks/auth/edge
35
39
  // This keeps the main index Worker-safe (no next/server dependency).
36
40
 
37
- // Legacy re-export — kept for backward compatibility, prefer verifyOidcToken
38
- export { verifyZitadelToken } from './zitadel';
39
- export type { ZitadelConfig, ZitadelIdentity, ZitadelClaims } from './zitadel';
package/src/jwt.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto'
2
+
3
+ export interface JwtClaims {
4
+ sub?: string
5
+ exp?: number
6
+ iat?: number
7
+ org_id?: string
8
+ role?: string
9
+ type?: string
10
+ [key: string]: unknown
11
+ }
12
+
13
+ /**
14
+ * Verify an HS256 JWT using Node.js crypto.
15
+ * Returns decoded claims on success, null on any failure.
16
+ * Runtime: Node.js only (uses `crypto` module).
17
+ */
18
+ export function verifyHs256Jwt(token: string, secret: string): JwtClaims | null {
19
+ const parts = token.split('.')
20
+ if (parts.length !== 3) return null
21
+
22
+ const [header, payload, sig] = parts as [string, string, string]
23
+ const expected = createHmac('sha256', secret)
24
+ .update(`${header}.${payload}`)
25
+ .digest('base64url')
26
+
27
+ const sigBuf = Buffer.from(sig, 'base64url')
28
+ const expBuf = Buffer.from(expected, 'base64url')
29
+ if (sigBuf.length !== expBuf.length) return null
30
+ if (!timingSafeEqual(sigBuf, expBuf)) return null
31
+
32
+ try {
33
+ const claims = JSON.parse(Buffer.from(payload, 'base64url').toString()) as JwtClaims
34
+ if (claims.exp && claims.exp < Math.floor(Date.now() / 1000)) return null
35
+ return claims
36
+ } catch {
37
+ return null
38
+ }
39
+ }
package/src/pkce.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * PKCE (Proof Key for Code Exchange) utilities — RFC 7636.
3
3
  * Runtime-agnostic: Workers, Node 20+, browser (Web Crypto API).
4
4
  */
5
+ import { base64urlEncode } from '@baseworks/core'
5
6
 
6
7
  export interface PkceChallenge {
7
8
  verifier: string;
@@ -29,14 +30,9 @@ export interface OidcAuthUrlConfig {
29
30
 
30
31
  /** Generate a PKCE code_verifier + S256 code_challenge pair. */
31
32
  export async function generatePkce(): Promise<PkceChallenge> {
32
- const array = new Uint8Array(32);
33
- crypto.getRandomValues(array);
34
- const verifier = base64url(array);
35
-
36
- const encoded = new TextEncoder().encode(verifier);
37
- const digest = await crypto.subtle.digest('SHA-256', encoded);
38
- const challenge = base64url(new Uint8Array(digest));
39
-
33
+ const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
34
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
35
+ const challenge = base64urlEncode(new Uint8Array(digest));
40
36
  return { verifier, challenge, method: 'S256' };
41
37
  }
42
38
 
@@ -49,12 +45,12 @@ export function buildOidcAuthUrl(config: OidcAuthUrlConfig): string {
49
45
  const scopes = (config.scopes ?? ['openid', 'email', 'profile']).join(' ');
50
46
 
51
47
  const params = new URLSearchParams({
52
- client_id: config.clientId,
53
- redirect_uri: config.redirectUri,
54
- response_type: 'code',
55
- scope: scopes,
56
- code_challenge: config.challenge,
57
- code_challenge_method: 'S256',
48
+ client_id: config.clientId,
49
+ redirect_uri: config.redirectUri,
50
+ response_type: 'code',
51
+ scope: scopes,
52
+ code_challenge: config.challenge,
53
+ code_challenge_method: 'S256',
58
54
  });
59
55
 
60
56
  if (config.state) params.set('state', config.state);
@@ -62,12 +58,3 @@ export function buildOidcAuthUrl(config: OidcAuthUrlConfig): string {
62
58
 
63
59
  return `${issuer}/oauth/v2/authorize?${params.toString()}`;
64
60
  }
65
-
66
- // ─── helpers ────────────────────────────────────────────────────────────────
67
-
68
- function base64url(buf: Uint8Array): string {
69
- return btoa(Array.from(buf, (b) => String.fromCharCode(b)).join(''))
70
- .replace(/\+/g, '-')
71
- .replace(/\//g, '_')
72
- .replace(/=/g, '');
73
- }
package/src/session.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  import { cookies } from "next/headers";
5
5
  import type { NextRequest } from "next/server";
6
6
  import { NextResponse } from "next/server";
7
+ import { base64urlEncode, base64urlDecodeString } from "@baseworks/core";
7
8
  import { getAuthPublicUrl, normalizeUrlLike } from "./url-helpers";
9
+ import { generatePkce } from "./pkce.js";
8
10
 
9
11
  type DiscoveryDocument = {
10
12
  authorization_endpoint: string;
@@ -117,33 +119,8 @@ function resolvePublicRedirect(target: string, request: NextRequest) {
117
119
  return new URL(target, request.url);
118
120
  }
119
121
 
120
- function base64UrlEncode(input: ArrayBuffer | Uint8Array | string) {
121
- const buffer =
122
- typeof input === "string"
123
- ? Buffer.from(input, "utf8")
124
- : Buffer.from(input instanceof Uint8Array ? input : new Uint8Array(input));
125
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
126
- }
127
-
128
- function base64UrlDecode(input: string) {
129
- const padded = input.replace(/-/g, "+").replace(/_/g, "/");
130
- const remainder = padded.length % 4;
131
- const normalized = remainder === 0 ? padded : `${padded}${"=".repeat(4 - remainder)}`;
132
- return Buffer.from(normalized, "base64").toString("utf8");
133
- }
134
-
135
- function sha256(input: string) {
136
- return crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
137
- }
138
-
139
122
  function randomString() {
140
- return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
141
- }
142
-
143
- async function createPkcePair() {
144
- const verifier = randomString();
145
- const challenge = base64UrlEncode(await sha256(verifier));
146
- return { challenge, verifier };
123
+ return base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
147
124
  }
148
125
 
149
126
  async function discoverOidc() {
@@ -154,17 +131,17 @@ async function discoverOidc() {
154
131
  }
155
132
 
156
133
  function parseJwtPayload(token: string): JwtPayload {
157
- const [, payload = ""] = token.split(".");
158
- return JSON.parse(base64UrlDecode(payload)) as JwtPayload;
134
+ const part = token.split(".")[1] ?? "";
135
+ return JSON.parse(base64urlDecodeString(part)) as JwtPayload;
159
136
  }
160
137
 
161
138
  function encodeSessionCookie(session: OidcSession) {
162
- return base64UrlEncode(JSON.stringify(session));
139
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(session)));
163
140
  }
164
141
 
165
142
  function parseSessionCookie(value: string): OidcSession | null {
166
143
  try {
167
- const session = JSON.parse(base64UrlDecode(value)) as OidcSession;
144
+ const session = JSON.parse(base64urlDecodeString(value)) as OidcSession;
168
145
  return session.isAuthenticated ? session : null;
169
146
  } catch {
170
147
  return null;
@@ -172,12 +149,12 @@ function parseSessionCookie(value: string): OidcSession | null {
172
149
  }
173
150
 
174
151
  function encodeTransactionCookie(transaction: OidcTransaction) {
175
- return base64UrlEncode(JSON.stringify(transaction));
152
+ return base64urlEncode(new TextEncoder().encode(JSON.stringify(transaction)));
176
153
  }
177
154
 
178
155
  function parseTransactionCookie(value: string): OidcTransaction | null {
179
156
  try {
180
- const transaction = JSON.parse(base64UrlDecode(value)) as OidcTransaction;
157
+ const transaction = JSON.parse(base64urlDecodeString(value)) as OidcTransaction;
181
158
  if (!transaction.codeVerifier || !transaction.returnTo) return null;
182
159
  return transaction;
183
160
  } catch {
@@ -232,7 +209,7 @@ function appendClientAuthentication(
232
209
  clientSecret?: string,
233
210
  ) {
234
211
  if (clientSecret) {
235
- const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
212
+ const basic = btoa(`${clientId}:${clientSecret}`);
236
213
  headers.set("authorization", `Basic ${basic}`);
237
214
  return;
238
215
  }
@@ -347,7 +324,7 @@ function clearTransientCookies(response: NextResponse, request: NextRequest) {
347
324
  export async function buildAuthorizationRedirect(request: NextRequest) {
348
325
  const discovery = await discoverOidc();
349
326
  const clientId = getClientId();
350
- const { challenge, verifier } = await createPkcePair();
327
+ const { challenge, verifier } = await generatePkce();
351
328
  const nonce = randomString();
352
329
  const state = randomString();
353
330
  const redirectUri = getRedirectUri(request);
@@ -1,23 +0,0 @@
1
- // src/zitadel.ts
2
- import { createRemoteJWKSet, jwtVerify } from "jose";
3
- function withoutTrailingSlash(v) {
4
- return v.replace(/\/+$/, "");
5
- }
6
- async function verifyZitadelToken(token, config) {
7
- const issuer = withoutTrailingSlash(config.issuer);
8
- const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/v2/keys`));
9
- const opts = config.audience ? { issuer, audience: config.audience } : { issuer };
10
- const result = await jwtVerify(token, jwks, opts).catch(() => null);
11
- if (!result) return null;
12
- const payload = result.payload;
13
- const subject = String(payload.sub ?? "");
14
- if (!subject) return null;
15
- const email = String(payload.email ?? payload.preferred_username ?? `${subject}@zitadel.local`);
16
- const name = String(payload.name ?? payload.preferred_username ?? email);
17
- const picture = payload.picture ? String(payload.picture) : void 0;
18
- return { subject, issuer, email, name, picture };
19
- }
20
-
21
- export {
22
- verifyZitadelToken
23
- };
package/dist/zitadel.d.ts DELETED
@@ -1,28 +0,0 @@
1
- interface ZitadelConfig {
2
- issuer: string;
3
- audience?: string;
4
- }
5
- interface ZitadelClaims {
6
- sub: string;
7
- email?: string;
8
- preferred_username?: string;
9
- name?: string;
10
- picture?: string;
11
- exp?: number;
12
- iss?: string;
13
- aud?: string | string[];
14
- }
15
- interface ZitadelIdentity {
16
- subject: string;
17
- issuer: string;
18
- email: string;
19
- name: string;
20
- picture?: string;
21
- }
22
- /**
23
- * Verify a Zitadel JWT and return the identity.
24
- * Returns null on any verification failure — never throws to the caller.
25
- */
26
- declare function verifyZitadelToken(token: string, config: ZitadelConfig): Promise<ZitadelIdentity | null>;
27
-
28
- export { type ZitadelClaims, type ZitadelConfig, type ZitadelIdentity, verifyZitadelToken };
package/dist/zitadel.js DELETED
@@ -1,6 +0,0 @@
1
- import {
2
- verifyZitadelToken
3
- } from "./chunk-BMPRMOI7.js";
4
- export {
5
- verifyZitadelToken
6
- };
package/src/zitadel.ts DELETED
@@ -1,56 +0,0 @@
1
- import { createRemoteJWKSet, jwtVerify } from 'jose';
2
-
3
- export interface ZitadelConfig {
4
- issuer: string;
5
- audience?: string;
6
- }
7
-
8
- export interface ZitadelClaims {
9
- sub: string;
10
- email?: string;
11
- preferred_username?: string;
12
- name?: string;
13
- picture?: string;
14
- exp?: number;
15
- iss?: string;
16
- aud?: string | string[];
17
- }
18
-
19
- // Matches OidcIdentity in @baseworks/account — intentionally compatible.
20
- export interface ZitadelIdentity {
21
- subject: string;
22
- issuer: string; // normalised, no trailing slash
23
- email: string;
24
- name: string;
25
- picture?: string;
26
- }
27
-
28
- function withoutTrailingSlash(v: string): string {
29
- return v.replace(/\/+$/, '');
30
- }
31
-
32
- /**
33
- * Verify a Zitadel JWT and return the identity.
34
- * Returns null on any verification failure — never throws to the caller.
35
- */
36
- export async function verifyZitadelToken(
37
- token: string,
38
- config: ZitadelConfig,
39
- ): Promise<ZitadelIdentity | null> {
40
- const issuer = withoutTrailingSlash(config.issuer);
41
- const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/v2/keys`));
42
- const opts = config.audience ? { issuer, audience: config.audience } : { issuer };
43
-
44
- const result = await jwtVerify(token, jwks, opts).catch(() => null);
45
- if (!result) return null;
46
-
47
- const payload = result.payload as ZitadelClaims;
48
- const subject = String(payload.sub ?? '');
49
- if (!subject) return null;
50
-
51
- const email = String(payload.email ?? payload.preferred_username ?? `${subject}@zitadel.local`);
52
- const name = String(payload.name ?? payload.preferred_username ?? email);
53
- const picture = payload.picture ? String(payload.picture) : undefined;
54
-
55
- return { subject, issuer, email, name, picture };
56
- }