@ericminassian/auth 0.1.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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @ericminassian/auth
2
+
3
+ TypeScript SDK for [auth.ericminassian.com](https://auth.ericminassian.com) — the
4
+ OIDC provider for `*.ericminassian.com` apps. ESM-only; subpath exports keep the
5
+ browser and server surfaces separate.
6
+
7
+ ```sh
8
+ pnpm add @ericminassian/auth
9
+ ```
10
+
11
+ ## Browser (`/client`, `/react`)
12
+
13
+ Authorization code + PKCE, run entirely in the browser:
14
+
15
+ ```ts
16
+ import { createAuthClient } from "@ericminassian/auth/client";
17
+
18
+ const auth = createAuthClient({
19
+ clientId: "my-app",
20
+ redirectUri: "https://my-app.ericminassian.com/callback",
21
+ });
22
+
23
+ // Kick off login
24
+ await auth.signInWithRedirect();
25
+
26
+ // On your /callback route
27
+ const { returnTo } = await auth.handleRedirectCallback();
28
+
29
+ // Call your API
30
+ const token = await auth.getAccessToken();
31
+ ```
32
+
33
+ React bindings:
34
+
35
+ ```tsx
36
+ import { createAuthClient } from "@ericminassian/auth/client";
37
+ import { AuthProvider, useAuth, useUser } from "@ericminassian/auth/react";
38
+
39
+ const client = createAuthClient({ clientId: "my-app", redirectUri: "…/callback" });
40
+
41
+ function App() {
42
+ return (
43
+ <AuthProvider client={client}>
44
+ <Profile />
45
+ </AuthProvider>
46
+ );
47
+ }
48
+
49
+ function Profile() {
50
+ const { state, signIn, signOut } = useAuth();
51
+ const user = useUser();
52
+ if (state.status !== "authenticated") return <button onClick={() => signIn()}>Sign in</button>;
53
+ return <button onClick={() => signOut()}>Sign out {user?.email}</button>;
54
+ }
55
+ ```
56
+
57
+ ## Server (`/server`, `/server/hono`, `/server/express`)
58
+
59
+ Verify access tokens locally against the JWKS (no network call per request, edge-safe):
60
+
61
+ ```ts
62
+ import { createAuthVerifier } from "@ericminassian/auth/server";
63
+
64
+ const verifier = createAuthVerifier({ audience: "my-app" });
65
+
66
+ const result = await verifier.authenticateRequest(request);
67
+ if (result.authenticated) {
68
+ console.log(result.claims.sub, result.claims.scope);
69
+ }
70
+ ```
71
+
72
+ Framework middleware:
73
+
74
+ ```ts
75
+ import { authMiddleware } from "@ericminassian/auth/server/hono";
76
+ app.use("/api/*", authMiddleware(verifier)); // claims at c.var.auth
77
+
78
+ import { requireAuth } from "@ericminassian/auth/server/express";
79
+ app.use("/api", requireAuth(verifier)); // claims at req.auth
80
+ ```
81
+
82
+ `verifyLogoutToken` validates back-channel logout tokens at your RP's logout receiver.
83
+
84
+ ## Development
85
+
86
+ ```sh
87
+ pnpm generate # regenerate wire types from ../../openapi/openapi.json
88
+ pnpm build # tsdown → dist/
89
+ pnpm test # vitest
90
+ ```
@@ -0,0 +1,59 @@
1
+ import { i as User } from "./index-CiPxwNnj.js";
2
+
3
+ //#region src/client/storage.d.ts
4
+
5
+ /**
6
+ * Pluggable persistence for the refresh token and the in-flight authorization
7
+ * transaction (PKCE verifier + state). Access tokens are never persisted —
8
+ * they live in memory only.
9
+ */
10
+ interface TokenStorage {
11
+ get(key: string): string | null;
12
+ set(key: string, value: string): void;
13
+ remove(key: string): void;
14
+ }
15
+ //#endregion
16
+ //#region src/client/auth-client.d.ts
17
+ interface AuthClientOptions {
18
+ clientId: string;
19
+ redirectUri: string;
20
+ /** Defaults to `https://auth.ericminassian.com`. */
21
+ issuer?: string;
22
+ /** Defaults to `openid email offline_access`. */
23
+ scope?: string;
24
+ storage?: TokenStorage;
25
+ }
26
+ type AuthState = {
27
+ status: "loading";
28
+ } | {
29
+ status: "authenticated";
30
+ user: User;
31
+ } | {
32
+ status: "unauthenticated";
33
+ };
34
+ interface SignInOptions {
35
+ /** Where to return after the callback completes. Defaults to the current URL. */
36
+ returnTo?: string;
37
+ }
38
+ interface AuthClient {
39
+ /** Build a PKCE+state transaction and navigate to the authorize endpoint. */
40
+ signInWithRedirect(options?: SignInOptions): Promise<void>;
41
+ /** Complete the redirect: exchange the code for tokens. Returns the saved returnTo. */
42
+ handleRedirectCallback(url?: string): Promise<{
43
+ returnTo: string | undefined;
44
+ }>;
45
+ /** A valid access token, refreshing if necessary. Throws `login_required` if not signed in. */
46
+ getAccessToken(options?: {
47
+ forceRefresh?: boolean;
48
+ }): Promise<string>;
49
+ getUser(): User | undefined;
50
+ getState(): AuthState;
51
+ onStateChange(listener: (state: AuthState) => void): () => void;
52
+ /** Revoke the refresh token, clear local state, and navigate to end_session. */
53
+ signOut(options?: {
54
+ postLogoutRedirectUri?: string;
55
+ }): Promise<void>;
56
+ }
57
+ declare function createAuthClient(options: AuthClientOptions): AuthClient;
58
+ //#endregion
59
+ export { createAuthClient as a, SignInOptions as i, AuthClientOptions as n, TokenStorage as o, AuthState as r, AuthClient as t };
@@ -0,0 +1,3 @@
1
+ import { i as User, n as AuthErrorCode, r as DEFAULT_ISSUER, t as AuthError } from "../index-CiPxwNnj.js";
2
+ import { a as createAuthClient, i as SignInOptions, n as AuthClientOptions, o as TokenStorage, r as AuthState, t as AuthClient } from "../auth-client-DZWvgw3X.js";
3
+ export { type AuthClient, type AuthClientOptions, AuthError, type AuthErrorCode, type AuthState, DEFAULT_ISSUER, type SignInOptions, type TokenStorage, type User, createAuthClient };
@@ -0,0 +1,234 @@
1
+ import { n as DEFAULT_ISSUER, t as AuthError } from "../src-C0axRlLQ.js";
2
+
3
+ //#region src/client/pkce.ts
4
+ /** PKCE (RFC 7636) S256 helpers, built on the Web Crypto API. */
5
+ const VERIFIER_BYTES = 32;
6
+ function base64url(bytes) {
7
+ let binary = "";
8
+ for (const byte of bytes) binary += String.fromCharCode(byte);
9
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
+ }
11
+ function randomBase64url(byteLength) {
12
+ const bytes = new Uint8Array(byteLength);
13
+ crypto.getRandomValues(bytes);
14
+ return base64url(bytes);
15
+ }
16
+ function createState() {
17
+ return randomBase64url(16);
18
+ }
19
+ async function createPkcePair() {
20
+ const verifier = randomBase64url(VERIFIER_BYTES);
21
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
22
+ return {
23
+ verifier,
24
+ challenge: base64url(new Uint8Array(digest))
25
+ };
26
+ }
27
+
28
+ //#endregion
29
+ //#region src/client/storage.ts
30
+ /** Default storage: `sessionStorage` when available, else an in-memory map. */
31
+ function defaultStorage() {
32
+ if (typeof sessionStorage !== "undefined") return {
33
+ get: (key) => sessionStorage.getItem(key),
34
+ set: (key, value) => sessionStorage.setItem(key, value),
35
+ remove: (key) => sessionStorage.removeItem(key)
36
+ };
37
+ const map = /* @__PURE__ */ new Map();
38
+ return {
39
+ get: (key) => map.get(key) ?? null,
40
+ set: (key, value) => {
41
+ map.set(key, value);
42
+ },
43
+ remove: (key) => {
44
+ map.delete(key);
45
+ }
46
+ };
47
+ }
48
+
49
+ //#endregion
50
+ //#region src/client/auth-client.ts
51
+ const TX_KEY = "ema_auth_tx";
52
+ const RT_KEY = "ema_auth_rt";
53
+ const ID_KEY = "ema_auth_id";
54
+ const EXPIRY_SKEW_SECONDS = 30;
55
+ function createAuthClient(options) {
56
+ const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/$/, "");
57
+ const scope = options.scope ?? "openid email offline_access";
58
+ const storage = options.storage ?? defaultStorage();
59
+ let discovery;
60
+ let cachedToken;
61
+ let user = decodeStoredUser(storage);
62
+ let state = user ? {
63
+ status: "authenticated",
64
+ user
65
+ } : { status: "unauthenticated" };
66
+ const listeners = /* @__PURE__ */ new Set();
67
+ function setState(next) {
68
+ state = next;
69
+ user = next.status === "authenticated" ? next.user : void 0;
70
+ for (const listener of listeners) listener(state);
71
+ }
72
+ function getDiscovery() {
73
+ discovery ??= fetchJson(`${issuer}/.well-known/openid-configuration`).catch(() => {
74
+ discovery = void 0;
75
+ throw new AuthError("network_error", "failed to load OIDC discovery document");
76
+ });
77
+ return discovery;
78
+ }
79
+ async function exchange(body) {
80
+ const { token_endpoint } = await getDiscovery();
81
+ const response = await fetch(token_endpoint, {
82
+ method: "POST",
83
+ headers: { "content-type": "application/x-www-form-urlencoded" },
84
+ body: new URLSearchParams(body)
85
+ });
86
+ if (!response.ok) throw new AuthError(body.grant_type === "refresh_token" ? "token_refresh_failed" : "invalid_grant", "token endpoint rejected the request");
87
+ const tokens = await response.json();
88
+ cachedToken = {
89
+ accessToken: tokens.access_token,
90
+ expiresAt: Date.now() + (tokens.expires_in - EXPIRY_SKEW_SECONDS) * 1e3
91
+ };
92
+ if (tokens.refresh_token) storage.set(RT_KEY, tokens.refresh_token);
93
+ if (tokens.id_token) {
94
+ storage.set(ID_KEY, tokens.id_token);
95
+ const next = userFromIdToken(tokens.id_token);
96
+ if (next) setState({
97
+ status: "authenticated",
98
+ user: next
99
+ });
100
+ }
101
+ }
102
+ return {
103
+ async signInWithRedirect(signInOptions) {
104
+ const { authorization_endpoint } = await getDiscovery();
105
+ const pkce = await createPkcePair();
106
+ const tx = {
107
+ verifier: pkce.verifier,
108
+ state: createState(),
109
+ returnTo: signInOptions?.returnTo ?? currentUrl()
110
+ };
111
+ storage.set(TX_KEY, JSON.stringify(tx));
112
+ const url = new URL(authorization_endpoint);
113
+ url.search = new URLSearchParams({
114
+ response_type: "code",
115
+ client_id: options.clientId,
116
+ redirect_uri: options.redirectUri,
117
+ scope,
118
+ state: tx.state,
119
+ code_challenge: pkce.challenge,
120
+ code_challenge_method: "S256"
121
+ }).toString();
122
+ redirect(url.toString());
123
+ },
124
+ async handleRedirectCallback(url) {
125
+ const params = new URL(url ?? currentUrl()).searchParams;
126
+ const raw = storage.get(TX_KEY);
127
+ storage.remove(TX_KEY);
128
+ if (!raw) throw new AuthError("state_mismatch", "no authorization transaction in progress");
129
+ const tx = JSON.parse(raw);
130
+ if (params.get("error")) throw new AuthError("invalid_grant", params.get("error_description") ?? params.get("error") ?? "authorization failed");
131
+ if (params.get("state") !== tx.state) throw new AuthError("state_mismatch", "state parameter mismatch");
132
+ const code = params.get("code");
133
+ if (!code) throw new AuthError("invalid_grant", "missing authorization code");
134
+ await exchange({
135
+ grant_type: "authorization_code",
136
+ code,
137
+ redirect_uri: options.redirectUri,
138
+ client_id: options.clientId,
139
+ code_verifier: tx.verifier
140
+ });
141
+ return { returnTo: tx.returnTo };
142
+ },
143
+ async getAccessToken(getOptions) {
144
+ if (!getOptions?.forceRefresh && cachedToken && cachedToken.expiresAt > Date.now()) return cachedToken.accessToken;
145
+ const refreshToken = storage.get(RT_KEY);
146
+ if (!refreshToken) throw new AuthError("login_required", "no refresh token available");
147
+ try {
148
+ await exchange({
149
+ grant_type: "refresh_token",
150
+ refresh_token: refreshToken,
151
+ client_id: options.clientId
152
+ });
153
+ } catch (error) {
154
+ storage.remove(RT_KEY);
155
+ storage.remove(ID_KEY);
156
+ setState({ status: "unauthenticated" });
157
+ if (error instanceof AuthError) throw new AuthError("login_required", error.message);
158
+ throw new AuthError("login_required");
159
+ }
160
+ if (!cachedToken) throw new AuthError("login_required");
161
+ return cachedToken.accessToken;
162
+ },
163
+ getUser: () => user,
164
+ getState: () => state,
165
+ onStateChange(listener) {
166
+ listeners.add(listener);
167
+ return () => listeners.delete(listener);
168
+ },
169
+ async signOut(signOutOptions) {
170
+ const idToken = storage.get(ID_KEY);
171
+ const refreshToken = storage.get(RT_KEY);
172
+ const { end_session_endpoint, revocation_endpoint } = await getDiscovery();
173
+ if (refreshToken) await fetch(revocation_endpoint, {
174
+ method: "POST",
175
+ headers: { "content-type": "application/x-www-form-urlencoded" },
176
+ body: new URLSearchParams({ token: refreshToken })
177
+ }).catch(() => void 0);
178
+ storage.remove(RT_KEY);
179
+ storage.remove(ID_KEY);
180
+ cachedToken = void 0;
181
+ setState({ status: "unauthenticated" });
182
+ const url = new URL(end_session_endpoint);
183
+ const search = new URLSearchParams();
184
+ if (idToken) search.set("id_token_hint", idToken);
185
+ search.set("client_id", options.clientId);
186
+ const postLogout = signOutOptions?.postLogoutRedirectUri;
187
+ if (postLogout) search.set("post_logout_redirect_uri", postLogout);
188
+ url.search = search.toString();
189
+ redirect(url.toString());
190
+ }
191
+ };
192
+ }
193
+ function decodeStoredUser(storage) {
194
+ const idToken = storage.get(ID_KEY);
195
+ return idToken ? userFromIdToken(idToken) : void 0;
196
+ }
197
+ /**
198
+ * Decode (not verify) the ID token to surface profile info in the UI. The
199
+ * token arrives directly from the token endpoint over TLS; RP *backends* must
200
+ * still verify via the server entry point's JWKS check.
201
+ */
202
+ function userFromIdToken(idToken) {
203
+ const parts = idToken.split(".");
204
+ if (parts.length !== 3) return void 0;
205
+ try {
206
+ const payload = JSON.parse(base64urlDecode(parts[1] ?? ""));
207
+ if (!payload.sub) return void 0;
208
+ if (typeof payload.exp === "number" && payload.exp * 1e3 <= Date.now()) return;
209
+ const user = { sub: payload.sub };
210
+ if (payload.email !== void 0) user.email = payload.email;
211
+ if (payload.email_verified !== void 0) user.emailVerified = payload.email_verified;
212
+ return user;
213
+ } catch {
214
+ return;
215
+ }
216
+ }
217
+ function base64urlDecode(input) {
218
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
219
+ return atob(padded);
220
+ }
221
+ async function fetchJson(url) {
222
+ const response = await fetch(url);
223
+ if (!response.ok) throw new Error(`fetch ${url} failed: ${response.status}`);
224
+ return await response.json();
225
+ }
226
+ function currentUrl() {
227
+ return typeof location !== "undefined" ? location.href : "";
228
+ }
229
+ function redirect(url) {
230
+ if (typeof location !== "undefined") location.assign(url);
231
+ }
232
+
233
+ //#endregion
234
+ export { AuthError, DEFAULT_ISSUER, createAuthClient };
@@ -0,0 +1,20 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * Shared surface for `@ericminassian/auth`: error type, public user shape,
4
+ * and the default issuer. Import the client, react, or server entry points
5
+ * for actual functionality.
6
+ */
7
+ declare const DEFAULT_ISSUER = "https://auth.ericminassian.com";
8
+ type AuthErrorCode = "invalid_grant" | "login_required" | "token_refresh_failed" | "state_mismatch" | "network_error" | "invalid_token" | "configuration_error";
9
+ declare class AuthError extends Error {
10
+ readonly code: AuthErrorCode;
11
+ constructor(code: AuthErrorCode, message?: string);
12
+ }
13
+ /** The authenticated subject, derived from the ID token / userinfo. */
14
+ interface User {
15
+ sub: string;
16
+ email?: string;
17
+ emailVerified?: boolean;
18
+ }
19
+ //#endregion
20
+ export { User as i, AuthErrorCode as n, DEFAULT_ISSUER as r, AuthError as t };
@@ -0,0 +1,2 @@
1
+ import { i as User, n as AuthErrorCode, r as DEFAULT_ISSUER, t as AuthError } from "./index-CiPxwNnj.js";
2
+ export { AuthError, AuthErrorCode, DEFAULT_ISSUER, User };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { n as DEFAULT_ISSUER, t as AuthError } from "./src-C0axRlLQ.js";
2
+
3
+ export { AuthError, DEFAULT_ISSUER };
@@ -0,0 +1,23 @@
1
+ import { i as User } from "../index-CiPxwNnj.js";
2
+ import { i as SignInOptions, r as AuthState, t as AuthClient } from "../auth-client-DZWvgw3X.js";
3
+ import { ReactNode } from "react";
4
+
5
+ //#region src/react/context.d.ts
6
+ interface AuthContextValue {
7
+ state: AuthState;
8
+ signIn: (options?: SignInOptions) => Promise<void>;
9
+ signOut: (options?: {
10
+ postLogoutRedirectUri?: string;
11
+ }) => Promise<void>;
12
+ getAccessToken: (options?: {
13
+ forceRefresh?: boolean;
14
+ }) => Promise<string>;
15
+ }
16
+ declare function AuthProvider(props: {
17
+ client: AuthClient;
18
+ children: ReactNode;
19
+ }): ReactNode;
20
+ declare function useAuth(): AuthContextValue;
21
+ declare function useUser(): User | undefined;
22
+ //#endregion
23
+ export { type AuthContextValue, AuthProvider, useAuth, useUser };
@@ -0,0 +1,31 @@
1
+ import { createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
2
+
3
+ //#region src/react/context.tsx
4
+ const AuthContext = createContext(void 0);
5
+ function AuthProvider(props) {
6
+ const { client, children } = props;
7
+ const [state, setState] = useState(() => client.getState());
8
+ useEffect(() => {
9
+ setState(client.getState());
10
+ return client.onStateChange(setState);
11
+ }, [client]);
12
+ const value = useMemo(() => ({
13
+ state,
14
+ signIn: (options) => client.signInWithRedirect(options),
15
+ signOut: (options) => client.signOut(options),
16
+ getAccessToken: (options) => client.getAccessToken(options)
17
+ }), [client, state]);
18
+ return createElement(AuthContext.Provider, { value }, children);
19
+ }
20
+ function useAuth() {
21
+ const context = useContext(AuthContext);
22
+ if (!context) throw new Error("useAuth must be used within an <AuthProvider>");
23
+ return context;
24
+ }
25
+ function useUser() {
26
+ const { state } = useAuth();
27
+ return state.status === "authenticated" ? state.user : void 0;
28
+ }
29
+
30
+ //#endregion
31
+ export { AuthProvider, useAuth, useUser };
@@ -0,0 +1,18 @@
1
+ import { r as AuthVerifier, t as AccessTokenClaims } from "../verify-QJxbHc6P.js";
2
+ import { RequestHandler } from "express";
3
+
4
+ //#region src/server/express.d.ts
5
+ declare global {
6
+ namespace Express {
7
+ interface Request {
8
+ auth?: AccessTokenClaims;
9
+ }
10
+ }
11
+ }
12
+ /**
13
+ * Express middleware that verifies the Bearer access token and attaches the
14
+ * claims to `req.auth`. Responds 401 when the token is missing or invalid.
15
+ */
16
+ declare function requireAuth(verifier: AuthVerifier): RequestHandler;
17
+ //#endregion
18
+ export { requireAuth };
@@ -0,0 +1,22 @@
1
+ //#region src/server/express.ts
2
+ /**
3
+ * Express middleware that verifies the Bearer access token and attaches the
4
+ * claims to `req.auth`. Responds 401 when the token is missing or invalid.
5
+ */
6
+ function requireAuth(verifier) {
7
+ return (req, res, next) => {
8
+ const header = req.header("authorization");
9
+ const request = new Request("http://local/", { headers: header ? { authorization: header } : {} });
10
+ verifier.authenticateRequest(request).then((result) => {
11
+ if (!result.authenticated) {
12
+ res.status(401).json({ error: "unauthorized" });
13
+ return;
14
+ }
15
+ req.auth = result.claims;
16
+ next();
17
+ }).catch(next);
18
+ };
19
+ }
20
+
21
+ //#endregion
22
+ export { requireAuth };
@@ -0,0 +1,16 @@
1
+ import { r as AuthVerifier, t as AccessTokenClaims } from "../verify-QJxbHc6P.js";
2
+ import { MiddlewareHandler } from "hono";
3
+
4
+ //#region src/server/hono.d.ts
5
+ declare module "hono" {
6
+ interface ContextVariableMap {
7
+ auth: AccessTokenClaims;
8
+ }
9
+ }
10
+ /**
11
+ * Hono middleware that verifies the Bearer access token and stores the claims
12
+ * at `c.var.auth`. Responds 401 when the token is missing or invalid.
13
+ */
14
+ declare function authMiddleware(verifier: AuthVerifier): MiddlewareHandler;
15
+ //#endregion
16
+ export { authMiddleware };
@@ -0,0 +1,16 @@
1
+ //#region src/server/hono.ts
2
+ /**
3
+ * Hono middleware that verifies the Bearer access token and stores the claims
4
+ * at `c.var.auth`. Responds 401 when the token is missing or invalid.
5
+ */
6
+ function authMiddleware(verifier) {
7
+ return async (c, next) => {
8
+ const result = await verifier.authenticateRequest(c.req.raw);
9
+ if (!result.authenticated) return c.json({ error: "unauthorized" }, 401);
10
+ c.set("auth", result.claims);
11
+ await next();
12
+ };
13
+ }
14
+
15
+ //#endregion
16
+ export { authMiddleware };
@@ -0,0 +1,3 @@
1
+ import { n as AuthErrorCode, t as AuthError } from "../index-CiPxwNnj.js";
2
+ import { a as createAuthVerifier, i as VerifierOptions, n as AuthResult, r as AuthVerifier, t as AccessTokenClaims } from "../verify-QJxbHc6P.js";
3
+ export { type AccessTokenClaims, AuthError, type AuthErrorCode, type AuthResult, type AuthVerifier, type VerifierOptions, createAuthVerifier };
@@ -0,0 +1,72 @@
1
+ import { n as DEFAULT_ISSUER, t as AuthError } from "../src-C0axRlLQ.js";
2
+ import { createRemoteJWKSet, jwtVerify } from "jose";
3
+
4
+ //#region src/server/verify.ts
5
+ function createAuthVerifier(options) {
6
+ const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/$/, "");
7
+ const clockTolerance = options.clockTolerance ?? "30s";
8
+ const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
9
+ async function verifyAccessToken(token) {
10
+ try {
11
+ const { payload } = await jwtVerify(token, jwks, {
12
+ issuer,
13
+ audience: options.audience,
14
+ clockTolerance,
15
+ typ: "at+jwt",
16
+ algorithms: ["ES256"]
17
+ });
18
+ return payload;
19
+ } catch (error) {
20
+ throw new AuthError("invalid_token", describe(error));
21
+ }
22
+ }
23
+ return {
24
+ verifyAccessToken,
25
+ async authenticateRequest(request) {
26
+ const header = request.headers.get("authorization");
27
+ const token = header?.startsWith("Bearer ") ? header.slice(7) : void 0;
28
+ if (!token) return {
29
+ authenticated: false,
30
+ reason: "missing"
31
+ };
32
+ try {
33
+ return {
34
+ authenticated: true,
35
+ claims: await verifyAccessToken(token)
36
+ };
37
+ } catch {
38
+ return {
39
+ authenticated: false,
40
+ reason: "invalid"
41
+ };
42
+ }
43
+ },
44
+ async verifyLogoutToken(token) {
45
+ try {
46
+ const { payload } = await jwtVerify(token, jwks, {
47
+ issuer,
48
+ audience: options.audience,
49
+ clockTolerance,
50
+ typ: "logout+jwt",
51
+ algorithms: ["ES256"]
52
+ });
53
+ if (!payload["events"]?.["http://schemas.openid.net/event/backchannel-logout"]) throw new AuthError("invalid_token", "not a back-channel logout token");
54
+ if ("nonce" in payload) throw new AuthError("invalid_token", "logout token must not contain a nonce");
55
+ const result = {};
56
+ if (typeof payload.sub === "string") result.sub = payload.sub;
57
+ if (typeof payload["sid"] === "string") result.sid = payload["sid"];
58
+ if (result.sub === void 0 && result.sid === void 0) throw new AuthError("invalid_token", "logout token has neither sub nor sid");
59
+ return result;
60
+ } catch (error) {
61
+ if (error instanceof AuthError) throw error;
62
+ throw new AuthError("invalid_token", describe(error));
63
+ }
64
+ }
65
+ };
66
+ }
67
+ function describe(error) {
68
+ return error instanceof Error ? error.message : "token verification failed";
69
+ }
70
+
71
+ //#endregion
72
+ export { AuthError, createAuthVerifier };
@@ -0,0 +1,18 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * Shared surface for `@ericminassian/auth`: error type, public user shape,
4
+ * and the default issuer. Import the client, react, or server entry points
5
+ * for actual functionality.
6
+ */
7
+ const DEFAULT_ISSUER = "https://auth.ericminassian.com";
8
+ var AuthError = class extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message ?? code);
12
+ this.name = "AuthError";
13
+ this.code = code;
14
+ }
15
+ };
16
+
17
+ //#endregion
18
+ export { DEFAULT_ISSUER as n, AuthError as t };
@@ -0,0 +1,40 @@
1
+ //#region src/server/verify.d.ts
2
+ interface VerifierOptions {
3
+ /** Your registered `client_id` — checked against the token `aud`. */
4
+ audience: string;
5
+ /** Defaults to `https://auth.ericminassian.com`. */
6
+ issuer?: string;
7
+ /** Clock skew tolerance, e.g. `"30s"`. Defaults to `"30s"`. */
8
+ clockTolerance?: string;
9
+ }
10
+ interface AccessTokenClaims {
11
+ sub: string;
12
+ sid: string;
13
+ scope: string;
14
+ client_id: string;
15
+ iat: number;
16
+ exp: number;
17
+ jti: string;
18
+ email?: string;
19
+ }
20
+ type AuthResult = {
21
+ authenticated: true;
22
+ claims: AccessTokenClaims;
23
+ } | {
24
+ authenticated: false;
25
+ reason: "missing" | "invalid" | "expired";
26
+ };
27
+ interface AuthVerifier {
28
+ /** Verify a Bearer access token; throws `AuthError` on failure. */
29
+ verifyAccessToken(token: string): Promise<AccessTokenClaims>;
30
+ /** Inspect an HTTP `Request`'s `Authorization: Bearer` header. Never throws. */
31
+ authenticateRequest(request: Request): Promise<AuthResult>;
32
+ /** Verify a back-channel logout token (for an RP's logout receiver). */
33
+ verifyLogoutToken(token: string): Promise<{
34
+ sub?: string;
35
+ sid?: string;
36
+ }>;
37
+ }
38
+ declare function createAuthVerifier(options: VerifierOptions): AuthVerifier;
39
+ //#endregion
40
+ export { createAuthVerifier as a, VerifierOptions as i, AuthResult as n, AuthVerifier as r, AccessTokenClaims as t };
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@ericminassian/auth",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for auth.ericminassian.com — OIDC client, React bindings, and server-side JWT verification.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/eric-minassian/auth.git",
9
+ "directory": "packages/auth-sdk"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "engines": {
14
+ "node": ">=20.19"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./client": {
25
+ "types": "./dist/client/index.d.ts",
26
+ "default": "./dist/client/index.js"
27
+ },
28
+ "./react": {
29
+ "types": "./dist/react/index.d.ts",
30
+ "default": "./dist/react/index.js"
31
+ },
32
+ "./server": {
33
+ "types": "./dist/server/index.d.ts",
34
+ "default": "./dist/server/index.js"
35
+ },
36
+ "./server/hono": {
37
+ "types": "./dist/server/hono.d.ts",
38
+ "default": "./dist/server/hono.js"
39
+ },
40
+ "./server/express": {
41
+ "types": "./dist/server/express.d.ts",
42
+ "default": "./dist/server/express.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "typecheck": "tsc --noEmit",
48
+ "generate": "openapi-typescript ../../openapi/openapi.json -o src/generated/api.d.ts",
49
+ "test": "vitest run"
50
+ },
51
+ "dependencies": {
52
+ "jose": "^6"
53
+ },
54
+ "peerDependencies": {
55
+ "react": ">=18",
56
+ "hono": ">=4",
57
+ "express": ">=5"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "react": {
61
+ "optional": true
62
+ },
63
+ "hono": {
64
+ "optional": true
65
+ },
66
+ "express": {
67
+ "optional": true
68
+ }
69
+ },
70
+ "devDependencies": {
71
+ "@types/express": "^5.0.0",
72
+ "@types/react": "^19",
73
+ "express": "^5.1.0",
74
+ "hono": "^4.6.0",
75
+ "openapi-typescript": "^7.4.0",
76
+ "react": "^19.0.0",
77
+ "tsdown": "^0.15.0",
78
+ "typescript": "~5.7.0",
79
+ "vitest": "^3.0.0"
80
+ },
81
+ "publishConfig": {
82
+ "access": "public"
83
+ }
84
+ }