@draftlab/auth 0.10.4 → 0.12.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.
@@ -0,0 +1,145 @@
1
+ //#region src/toolkit/storage.d.ts
2
+ /**
3
+ * Storage adapters for PKCE state management across different runtime environments.
4
+ * Provides implementations for browser (sessionStorage/localStorage), server (cookies),
5
+ * and custom storage backends.
6
+ */
7
+ /**
8
+ * PKCE state data stored during OAuth flow.
9
+ */
10
+ interface PKCEState {
11
+ /** Random state parameter for CSRF protection */
12
+ readonly state: string;
13
+ /** PKCE code verifier for token exchange */
14
+ readonly verifier: string;
15
+ /** OAuth provider identifier */
16
+ readonly provider: string;
17
+ /** Optional nonce for additional security */
18
+ readonly nonce?: string;
19
+ }
20
+ /**
21
+ * Storage interface for persisting PKCE state during OAuth flow.
22
+ * Implement this interface to provide custom storage backends.
23
+ */
24
+ interface AuthStorage {
25
+ /**
26
+ * Store PKCE state data.
27
+ * @param state - PKCE state to persist
28
+ */
29
+ set(state: PKCEState): void | Promise<void>;
30
+ /**
31
+ * Retrieve stored PKCE state data.
32
+ * @returns Stored state or null if not found
33
+ */
34
+ get(): PKCEState | null | Promise<PKCEState | null>;
35
+ /**
36
+ * Clear stored PKCE state data.
37
+ */
38
+ clear(): void | Promise<void>;
39
+ }
40
+ /**
41
+ * Creates a browser sessionStorage adapter for PKCE state.
42
+ * Suitable for client-side SPAs where state should only persist during the browser session.
43
+ *
44
+ * @returns AuthStorage implementation using sessionStorage
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const storage = createSessionStorage()
49
+ *
50
+ * // Use in toolkit client
51
+ * const client = createOAuthClient({
52
+ * storage,
53
+ * providers: { ... }
54
+ * })
55
+ * ```
56
+ */
57
+ declare const createSessionStorage: () => AuthStorage;
58
+ /**
59
+ * Creates a browser localStorage adapter for PKCE state.
60
+ * Suitable for client-side SPAs where state should persist across browser sessions.
61
+ *
62
+ * ⚠️ Warning: localStorage persists data indefinitely. Consider using sessionStorage
63
+ * for better security, as it automatically clears on browser close.
64
+ *
65
+ * @returns AuthStorage implementation using localStorage
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const storage = createLocalStorage()
70
+ *
71
+ * // Use in toolkit client
72
+ * const client = createOAuthClient({
73
+ * storage,
74
+ * providers: { ... }
75
+ * })
76
+ * ```
77
+ */
78
+ declare const createLocalStorage: () => AuthStorage;
79
+ /**
80
+ * Creates a memory-based storage adapter for PKCE state.
81
+ * Suitable for server-side rendering or testing environments.
82
+ * State is lost when the process terminates or page reloads.
83
+ *
84
+ * @returns AuthStorage implementation using in-memory storage
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * // Server-side or testing
89
+ * const storage = createMemoryStorage()
90
+ *
91
+ * const client = createOAuthClient({
92
+ * storage,
93
+ * providers: { ... }
94
+ * })
95
+ * ```
96
+ */
97
+ declare const createMemoryStorage: () => AuthStorage;
98
+ /**
99
+ * Creates a cookie-based storage adapter for PKCE state.
100
+ * Suitable for server-side OAuth flows (Next.js, Remix, etc.) where you need
101
+ * to persist state across requests.
102
+ *
103
+ * @param options - Cookie configuration options
104
+ * @returns AuthStorage implementation using cookies
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // Next.js App Router
109
+ * import { cookies } from 'next/headers'
110
+ *
111
+ * const storage = createCookieStorage({
112
+ * getCookie: (name) => cookies().get(name)?.value ?? null,
113
+ * setCookie: (name, value, opts) => {
114
+ * cookies().set(name, value, {
115
+ * httpOnly: true,
116
+ * secure: true,
117
+ * sameSite: 'lax',
118
+ * maxAge: opts.maxAge
119
+ * })
120
+ * },
121
+ * deleteCookie: (name) => cookies().delete(name)
122
+ * })
123
+ *
124
+ * // TanStack Start
125
+ * import { getCookie, setCookie, deleteCookie } from '@tanstack/react-start/server'
126
+ *
127
+ * const storage = createCookieStorage({
128
+ * getCookie,
129
+ * setCookie: (name, value, opts) => setCookie(name, value, opts),
130
+ * deleteCookie
131
+ * })
132
+ * ```
133
+ */
134
+ declare const createCookieStorage: (options: {
135
+ getCookie: (name: string) => string | null | Promise<string | null>;
136
+ setCookie: (name: string, value: string, options: {
137
+ maxAge?: number;
138
+ httpOnly?: boolean;
139
+ secure?: boolean;
140
+ sameSite?: string;
141
+ }) => void | Promise<void>;
142
+ deleteCookie: (name: string) => void | Promise<void>;
143
+ }) => AuthStorage;
144
+ //#endregion
145
+ export { AuthStorage, PKCEState, createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage };
@@ -0,0 +1,157 @@
1
+ //#region src/toolkit/storage.ts
2
+ const STORAGE_KEY = "draftauth.pkce";
3
+ /**
4
+ * Creates a browser sessionStorage adapter for PKCE state.
5
+ * Suitable for client-side SPAs where state should only persist during the browser session.
6
+ *
7
+ * @returns AuthStorage implementation using sessionStorage
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const storage = createSessionStorage()
12
+ *
13
+ * // Use in toolkit client
14
+ * const client = createOAuthClient({
15
+ * storage,
16
+ * providers: { ... }
17
+ * })
18
+ * ```
19
+ */
20
+ const createSessionStorage = () => ({
21
+ set: (data) => {
22
+ if (typeof sessionStorage === "undefined") throw new Error("sessionStorage is not available in this environment");
23
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
24
+ },
25
+ get: () => {
26
+ if (typeof sessionStorage === "undefined") return null;
27
+ const data = sessionStorage.getItem(STORAGE_KEY);
28
+ return data ? JSON.parse(data) : null;
29
+ },
30
+ clear: () => {
31
+ if (typeof sessionStorage === "undefined") return;
32
+ sessionStorage.removeItem(STORAGE_KEY);
33
+ }
34
+ });
35
+ /**
36
+ * Creates a browser localStorage adapter for PKCE state.
37
+ * Suitable for client-side SPAs where state should persist across browser sessions.
38
+ *
39
+ * ⚠️ Warning: localStorage persists data indefinitely. Consider using sessionStorage
40
+ * for better security, as it automatically clears on browser close.
41
+ *
42
+ * @returns AuthStorage implementation using localStorage
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const storage = createLocalStorage()
47
+ *
48
+ * // Use in toolkit client
49
+ * const client = createOAuthClient({
50
+ * storage,
51
+ * providers: { ... }
52
+ * })
53
+ * ```
54
+ */
55
+ const createLocalStorage = () => ({
56
+ set: (data) => {
57
+ if (typeof localStorage === "undefined") throw new Error("localStorage is not available in this environment");
58
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
59
+ },
60
+ get: () => {
61
+ if (typeof localStorage === "undefined") return null;
62
+ const data = localStorage.getItem(STORAGE_KEY);
63
+ return data ? JSON.parse(data) : null;
64
+ },
65
+ clear: () => {
66
+ if (typeof localStorage === "undefined") return;
67
+ localStorage.removeItem(STORAGE_KEY);
68
+ }
69
+ });
70
+ /**
71
+ * Creates a memory-based storage adapter for PKCE state.
72
+ * Suitable for server-side rendering or testing environments.
73
+ * State is lost when the process terminates or page reloads.
74
+ *
75
+ * @returns AuthStorage implementation using in-memory storage
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * // Server-side or testing
80
+ * const storage = createMemoryStorage()
81
+ *
82
+ * const client = createOAuthClient({
83
+ * storage,
84
+ * providers: { ... }
85
+ * })
86
+ * ```
87
+ */
88
+ const createMemoryStorage = () => {
89
+ let state = null;
90
+ return {
91
+ set: (data) => {
92
+ state = data;
93
+ },
94
+ get: () => {
95
+ return state;
96
+ },
97
+ clear: () => {
98
+ state = null;
99
+ }
100
+ };
101
+ };
102
+ /**
103
+ * Creates a cookie-based storage adapter for PKCE state.
104
+ * Suitable for server-side OAuth flows (Next.js, Remix, etc.) where you need
105
+ * to persist state across requests.
106
+ *
107
+ * @param options - Cookie configuration options
108
+ * @returns AuthStorage implementation using cookies
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Next.js App Router
113
+ * import { cookies } from 'next/headers'
114
+ *
115
+ * const storage = createCookieStorage({
116
+ * getCookie: (name) => cookies().get(name)?.value ?? null,
117
+ * setCookie: (name, value, opts) => {
118
+ * cookies().set(name, value, {
119
+ * httpOnly: true,
120
+ * secure: true,
121
+ * sameSite: 'lax',
122
+ * maxAge: opts.maxAge
123
+ * })
124
+ * },
125
+ * deleteCookie: (name) => cookies().delete(name)
126
+ * })
127
+ *
128
+ * // TanStack Start
129
+ * import { getCookie, setCookie, deleteCookie } from '@tanstack/react-start/server'
130
+ *
131
+ * const storage = createCookieStorage({
132
+ * getCookie,
133
+ * setCookie: (name, value, opts) => setCookie(name, value, opts),
134
+ * deleteCookie
135
+ * })
136
+ * ```
137
+ */
138
+ const createCookieStorage = (options) => ({
139
+ set: async (data) => {
140
+ await options.setCookie(STORAGE_KEY, JSON.stringify(data), {
141
+ maxAge: 600,
142
+ httpOnly: true,
143
+ secure: process.env.NODE_ENV === "production",
144
+ sameSite: "lax"
145
+ });
146
+ },
147
+ get: async () => {
148
+ const value = await options.getCookie(STORAGE_KEY);
149
+ return value ? JSON.parse(value) : null;
150
+ },
151
+ clear: async () => {
152
+ await options.deleteCookie(STORAGE_KEY);
153
+ }
154
+ });
155
+
156
+ //#endregion
157
+ export { createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage };
@@ -0,0 +1,21 @@
1
+ //#region src/toolkit/utils.d.ts
2
+ /**
3
+ * Universal utilities for the OAuth toolkit.
4
+ * These functions work in both browser and Node.js environments.
5
+ */
6
+ /**
7
+ * Generates a cryptographically secure random string using Web Crypto API.
8
+ * Works in both browser and Node.js environments (Node 15+).
9
+ *
10
+ * @param length - Length of random data in bytes (default: 32 for 256-bit security)
11
+ * @returns Base64url-encoded secure random string
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const state = generateSecureRandom(16) // 128-bit random state
16
+ * const token = generateSecureRandom(32) // 256-bit random token
17
+ * ```
18
+ */
19
+ declare const generateSecureRandom: (length?: number) => string;
20
+ //#endregion
21
+ export { generateSecureRandom };
@@ -0,0 +1,30 @@
1
+ //#region src/toolkit/utils.ts
2
+ /**
3
+ * Universal utilities for the OAuth toolkit.
4
+ * These functions work in both browser and Node.js environments.
5
+ */
6
+ /**
7
+ * Generates a cryptographically secure random string using Web Crypto API.
8
+ * Works in both browser and Node.js environments (Node 15+).
9
+ *
10
+ * @param length - Length of random data in bytes (default: 32 for 256-bit security)
11
+ * @returns Base64url-encoded secure random string
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const state = generateSecureRandom(16) // 128-bit random state
16
+ * const token = generateSecureRandom(32) // 256-bit random token
17
+ * ```
18
+ */
19
+ const generateSecureRandom = (length = 32) => {
20
+ if (length <= 0 || !Number.isInteger(length)) throw new RangeError("Length must be a positive integer");
21
+ const randomBytes = new Uint8Array(length);
22
+ crypto.getRandomValues(randomBytes);
23
+ let base64 = "";
24
+ if (typeof btoa !== "undefined") base64 = btoa(String.fromCharCode(...randomBytes));
25
+ else base64 = Buffer.from(randomBytes).toString("base64");
26
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
27
+ };
28
+
29
+ //#endregion
30
+ export { generateSecureRandom };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draftlab/auth",
3
- "version": "0.10.4",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Core implementation for @draftlab/auth",
6
6
  "author": "Matheus Pergoli",
@@ -39,7 +39,7 @@
39
39
  "devDependencies": {
40
40
  "@types/node": "^25.0.3",
41
41
  "@types/qrcode": "^1.5.6",
42
- "tsdown": "^0.18.2",
42
+ "tsdown": "^0.18.3",
43
43
  "typescript": "^5.9.3",
44
44
  "@draftlab/tsconfig": "0.1.0"
45
45
  },
@@ -63,7 +63,7 @@
63
63
  "preact": "^10.28.1",
64
64
  "preact-render-to-string": "^6.6.4",
65
65
  "qrcode": "^1.5.4",
66
- "@draftlab/auth-router": "0.4.1"
66
+ "@draftlab/auth-router": "0.5.0"
67
67
  },
68
68
  "engines": {
69
69
  "node": ">=18"
@@ -1,27 +0,0 @@
1
- import { PluginBuilder } from "./plugin.mjs";
2
-
3
- //#region src/plugin/builder.d.ts
4
-
5
- /**
6
- * Create a new plugin builder.
7
- * Plugins are built using a fluent API that supports routes and lifecycle hooks.
8
- *
9
- * @param id - Unique identifier for the plugin
10
- * @returns Plugin builder with chainable methods
11
- *
12
- * @example
13
- * ```ts
14
- * const analytics = plugin("analytics")
15
- * .onSuccess(async (ctx) => {
16
- * await ctx.storage.set(`success:${ctx.clientID}`, ctx.subject)
17
- * })
18
- * .post("/stats", async (ctx) => {
19
- * const stats = await ctx.pluginStorage.get("stats")
20
- * return ctx.json(stats)
21
- * })
22
- * .build()
23
- * ```
24
- */
25
- declare const plugin: (id: string) => PluginBuilder;
26
- //#endregion
27
- export { plugin };
@@ -1,93 +0,0 @@
1
- //#region src/plugin/builder.ts
2
- /**
3
- * Create a new plugin builder.
4
- * Plugins are built using a fluent API that supports routes and lifecycle hooks.
5
- *
6
- * @param id - Unique identifier for the plugin
7
- * @returns Plugin builder with chainable methods
8
- *
9
- * @example
10
- * ```ts
11
- * const analytics = plugin("analytics")
12
- * .onSuccess(async (ctx) => {
13
- * await ctx.storage.set(`success:${ctx.clientID}`, ctx.subject)
14
- * })
15
- * .post("/stats", async (ctx) => {
16
- * const stats = await ctx.pluginStorage.get("stats")
17
- * return ctx.json(stats)
18
- * })
19
- * .build()
20
- * ```
21
- */
22
- const plugin = (id) => {
23
- if (!id || typeof id !== "string") throw new Error("Plugin id must be a non-empty string");
24
- const routes = [];
25
- const registeredPaths = /* @__PURE__ */ new Set();
26
- let initHook;
27
- let authorizeHook;
28
- let successHook;
29
- let errorHook;
30
- const validatePath = (path) => {
31
- if (!path || typeof path !== "string") throw new Error("Route path must be a non-empty string");
32
- if (!path.startsWith("/")) throw new Error("Route path must start with '/'");
33
- };
34
- return {
35
- get(path, handler) {
36
- validatePath(path);
37
- const routeKey = `GET ${path}`;
38
- if (registeredPaths.has(routeKey)) throw new Error(`Route ${routeKey} already registered in plugin '${id}'`);
39
- registeredPaths.add(routeKey);
40
- routes.push({
41
- method: "GET",
42
- path,
43
- handler
44
- });
45
- return this;
46
- },
47
- post(path, handler) {
48
- validatePath(path);
49
- const routeKey = `POST ${path}`;
50
- if (registeredPaths.has(routeKey)) throw new Error(`Route ${routeKey} already registered in plugin '${id}'`);
51
- registeredPaths.add(routeKey);
52
- routes.push({
53
- method: "POST",
54
- path,
55
- handler
56
- });
57
- return this;
58
- },
59
- onInit(handler) {
60
- if (initHook) throw new Error(`onInit hook already defined for plugin '${id}'`);
61
- initHook = handler;
62
- return this;
63
- },
64
- onAuthorize(handler) {
65
- if (authorizeHook) throw new Error(`onAuthorize hook already defined for plugin '${id}'`);
66
- authorizeHook = handler;
67
- return this;
68
- },
69
- onSuccess(handler) {
70
- if (successHook) throw new Error(`onSuccess hook already defined for plugin '${id}'`);
71
- successHook = handler;
72
- return this;
73
- },
74
- onError(handler) {
75
- if (errorHook) throw new Error(`onError hook already defined for plugin '${id}'`);
76
- errorHook = handler;
77
- return this;
78
- },
79
- build() {
80
- return {
81
- id,
82
- routes: routes.length > 0 ? routes : void 0,
83
- onInit: initHook,
84
- onAuthorize: authorizeHook,
85
- onSuccess: successHook,
86
- onError: errorHook
87
- };
88
- }
89
- };
90
- };
91
-
92
- //#endregion
93
- export { plugin };
@@ -1,62 +0,0 @@
1
- import { StorageAdapter } from "../storage/storage.mjs";
2
- import { Plugin } from "./types.mjs";
3
- import { Router } from "@draftlab/auth-router";
4
-
5
- //#region src/plugin/manager.d.ts
6
-
7
- declare class PluginManager {
8
- private readonly plugins;
9
- private readonly storage;
10
- constructor(storage: StorageAdapter);
11
- /**
12
- * Register a plugin
13
- */
14
- register(plugin: Plugin): void;
15
- /**
16
- * Register multiple plugins at once
17
- */
18
- registerAll(plugins: Plugin[]): void;
19
- /**
20
- * Get all registered plugins
21
- */
22
- getAll(): Plugin[];
23
- /**
24
- * Get plugin by id
25
- */
26
- get(id: string): Plugin | undefined;
27
- /**
28
- * Initialize all plugins.
29
- * Called once during issuer setup.
30
- * Plugins can set up initial state or validate configuration.
31
- *
32
- * @throws PluginError if any plugin initialization fails
33
- */
34
- initialize(): Promise<void>;
35
- /**
36
- * Execute authorize hooks for all plugins.
37
- * Called before processing an authorization request.
38
- * Can validate, rate limit, or enhance the request.
39
- */
40
- executeAuthorizeHooks(clientID: string, provider?: string, scopes?: string[]): Promise<void>;
41
- /**
42
- * Execute success hooks for all plugins.
43
- * Called after successful authentication.
44
- * Runs in parallel for better performance.
45
- * Plugins cannot modify the response.
46
- */
47
- executeSuccessHooks(clientID: string, provider: string | undefined, subject: {
48
- type: string;
49
- properties: Record<string, unknown>;
50
- }): Promise<void>;
51
- /**
52
- * Execute error hooks for all plugins.
53
- * Called when an authentication error occurs.
54
- */
55
- executeErrorHooks(error: Error, clientID?: string, provider?: string): Promise<void>;
56
- /**
57
- * Setup plugin routes on a router
58
- */
59
- setupRoutes(router: Router): void;
60
- }
61
- //#endregion
62
- export { PluginManager };