@auth-gate/core 0.14.0 → 0.15.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/dist/index.cjs CHANGED
@@ -1,1342 +1,1454 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __defProps = Object.defineProperties;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
10
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
- var __spreadValues = (a, b) => {
12
- for (var prop in b || (b = {}))
13
- if (__hasOwnProp.call(b, prop))
14
- __defNormalProp(a, prop, b[prop]);
15
- if (__getOwnPropSymbols)
16
- for (var prop of __getOwnPropSymbols(b)) {
17
- if (__propIsEnum.call(b, prop))
18
- __defNormalProp(a, prop, b[prop]);
19
- }
20
- return a;
21
- };
22
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
23
- var __export = (target, all) => {
24
- for (var name in all)
25
- __defProp(target, name, { get: all[name], enumerable: true });
26
- };
27
- var __copyProps = (to, from, except, desc) => {
28
- if (from && typeof from === "object" || typeof from === "function") {
29
- for (let key of __getOwnPropNames(from))
30
- if (!__hasOwnProp.call(to, key) && key !== except)
31
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
32
- }
33
- return to;
34
- };
35
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
-
37
- // src/index.ts
38
- var index_exports = {};
39
- __export(index_exports, {
40
- AuthGateClient: () => AuthGateClient,
41
- AuthGateError: () => AuthGateError,
42
- ConfigurationError: () => ConfigurationError,
43
- DEFAULT_COOKIE_NAME: () => DEFAULT_COOKIE_NAME,
44
- InvalidStateError: () => InvalidStateError,
45
- NONCE_COOKIE_NAME: () => NONCE_COOKIE_NAME,
46
- REFRESH_TOKEN_COOKIE_NAME: () => REFRESH_TOKEN_COOKIE_NAME,
47
- SESSION_MAX_AGE: () => SESSION_MAX_AGE,
48
- STATE_TTL_MS: () => STATE_TTL_MS,
49
- SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
50
- SessionDecryptionError: () => SessionDecryptionError,
51
- TokenVerificationError: () => TokenVerificationError,
52
- checkFeature: () => checkFeature,
53
- createAuthGateClient: () => createAuthGateClient,
54
- defineConfig: () => defineConfig,
55
- hasAllPermissions: () => hasAllPermissions,
56
- hasAnyPermission: () => hasAnyPermission,
57
- hasFeature: () => hasFeature,
58
- hasMergedPermission: () => hasMergedPermission,
59
- hasPermission: () => hasPermission,
60
- hasPlan: () => hasPlan,
61
- hasRole: () => hasRole,
62
- mergePermissions: () => mergePermissions
63
- });
64
- module.exports = __toCommonJS(index_exports);
65
-
66
- // src/crypto/encoding.ts
67
- var encoder = new TextEncoder();
68
- var decoder = new TextDecoder();
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/crypto/encoding.ts
3
+ /**
4
+ * Low-level encoding utilities used internally by the crypto layer.
5
+ *
6
+ * These functions are not exported from the public API — they exist to
7
+ * avoid dependencies on Node.js `Buffer` so the package works in
8
+ * Edge Runtime, Cloudflare Workers, and browsers.
9
+ *
10
+ * @internal
11
+ * @module
12
+ */
13
+ const encoder = new TextEncoder();
14
+ const decoder = new TextDecoder();
15
+ /** Encode a `Uint8Array` to a URL-safe Base64 string (no padding). */
69
16
  function toBase64Url(bytes) {
70
- let binary = "";
71
- for (let i = 0; i < bytes.length; i++) {
72
- binary += String.fromCharCode(bytes[i]);
73
- }
74
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
17
+ let binary = "";
18
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
19
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
75
20
  }
21
+ /** Decode a URL-safe Base64 string back to a `Uint8Array`. */
76
22
  function fromBase64Url(str) {
77
- const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
78
- const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
79
- const binary = atob(padded);
80
- const bytes = new Uint8Array(binary.length);
81
- for (let i = 0; i < binary.length; i++) {
82
- bytes[i] = binary.charCodeAt(i);
83
- }
84
- return bytes;
23
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
24
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
25
+ const binary = atob(padded);
26
+ const bytes = new Uint8Array(binary.length);
27
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
28
+ return bytes;
85
29
  }
30
+ /** Encode a `Uint8Array` to a lowercase hex string. */
86
31
  function toHex(bytes) {
87
- let hex = "";
88
- for (let i = 0; i < bytes.length; i++) {
89
- hex += bytes[i].toString(16).padStart(2, "0");
90
- }
91
- return hex;
32
+ let hex = "";
33
+ for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
34
+ return hex;
92
35
  }
36
+ /** Encode a UTF-8 string to a `Uint8Array`. */
93
37
  function encodeUtf8(str) {
94
- return encoder.encode(str);
38
+ return encoder.encode(str);
95
39
  }
40
+ /** Decode a `Uint8Array` to a UTF-8 string. */
96
41
  function decodeUtf8(bytes) {
97
- return decoder.decode(bytes);
42
+ return decoder.decode(bytes);
98
43
  }
44
+ /** Concatenate multiple `Uint8Array` buffers into a single buffer. */
99
45
  function concat(...arrays) {
100
- let length = 0;
101
- for (const arr of arrays) length += arr.length;
102
- const result = new Uint8Array(length);
103
- let offset = 0;
104
- for (const arr of arrays) {
105
- result.set(arr, offset);
106
- offset += arr.length;
107
- }
108
- return result;
46
+ let length = 0;
47
+ for (const arr of arrays) length += arr.length;
48
+ const result = new Uint8Array(length);
49
+ let offset = 0;
50
+ for (const arr of arrays) {
51
+ result.set(arr, offset);
52
+ offset += arr.length;
53
+ }
54
+ return result;
109
55
  }
56
+ /**
57
+ * Constant-time comparison of two byte arrays.
58
+ *
59
+ * Prevents timing side-channel attacks by always comparing every byte
60
+ * regardless of where the first difference occurs.
61
+ */
110
62
  function constantTimeEqual(a, b) {
111
- if (a.length !== b.length) return false;
112
- let diff = 0;
113
- for (let i = 0; i < a.length; i++) {
114
- diff |= a[i] ^ b[i];
115
- }
116
- return diff === 0;
63
+ if (a.length !== b.length) return false;
64
+ let diff = 0;
65
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
66
+ return diff === 0;
117
67
  }
118
-
119
- // src/crypto/keys.ts
68
+ //#endregion
69
+ //#region src/crypto/keys.ts
70
+ /**
71
+ * Key derivation using HKDF-SHA256 via the Web Crypto API.
72
+ *
73
+ * Derives deterministic, purpose-specific keys from the user's API key.
74
+ * Each key uses a distinct `info` label so that a single secret produces
75
+ * independent keys for encryption vs. signing.
76
+ *
77
+ * @internal
78
+ * @module
79
+ */
80
+ /**
81
+ * Derive a key from a secret using HKDF-SHA256.
82
+ *
83
+ * @param secret - The input key material (the user's API key).
84
+ * @param info - A context string that differentiates derived keys.
85
+ * @param keylen - Desired key length in bytes.
86
+ * @returns A `Uint8Array` of `keylen` bytes.
87
+ */
120
88
  async function hkdfDerive(secret, info, keylen) {
121
- const ikm = encodeUtf8(secret);
122
- const baseKey = await crypto.subtle.importKey("raw", ikm, "HKDF", false, [
123
- "deriveBits"
124
- ]);
125
- const bits = await crypto.subtle.deriveBits(
126
- {
127
- name: "HKDF",
128
- hash: "SHA-256",
129
- salt: new Uint8Array(0),
130
- info: encodeUtf8(info)
131
- },
132
- baseKey,
133
- keylen * 8
134
- );
135
- return new Uint8Array(bits);
89
+ const ikm = encodeUtf8(secret);
90
+ const baseKey = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
91
+ const bits = await crypto.subtle.deriveBits({
92
+ name: "HKDF",
93
+ hash: "SHA-256",
94
+ salt: /* @__PURE__ */ new Uint8Array(0),
95
+ info: encodeUtf8(info)
96
+ }, baseKey, keylen * 8);
97
+ return new Uint8Array(bits);
136
98
  }
99
+ /**
100
+ * Derive a 256-bit AES-GCM encryption key for session cookies.
101
+ *
102
+ * Uses the info label `"authgate-session-encryption"` to ensure this key
103
+ * is distinct from the state signing key.
104
+ *
105
+ * @param secret - The API key.
106
+ * @returns A 32-byte encryption key.
107
+ */
137
108
  async function deriveEncryptionKey(secret) {
138
- return hkdfDerive(secret, "authgate-session-encryption", 32);
109
+ return hkdfDerive(secret, "authgate-session-encryption", 32);
139
110
  }
111
+ /**
112
+ * Derive a 256-bit HMAC-SHA256 signing key for OAuth state parameters.
113
+ *
114
+ * Uses the info label `"authgate-state-signing"` to ensure this key
115
+ * is distinct from the session encryption key.
116
+ *
117
+ * @param secret - The API key.
118
+ * @returns A 32-byte signing key.
119
+ */
140
120
  async function deriveStateKey(secret) {
141
- return hkdfDerive(secret, "authgate-state-signing", 32);
121
+ return hkdfDerive(secret, "authgate-state-signing", 32);
142
122
  }
143
-
144
- // src/constants.ts
145
- var DEFAULT_COOKIE_NAME = "__authgate";
146
- var NONCE_COOKIE_NAME = "__authgate_nonce";
147
- var REFRESH_TOKEN_COOKIE_NAME = "__authgate_refresh";
148
- var SESSION_MAX_AGE = 604800;
149
- var STATE_TTL_MS = 6e5;
150
- var SUPPORTED_PROVIDERS = [
151
- "google",
152
- "github",
153
- "discord",
154
- "azure",
155
- "apple"
123
+ //#endregion
124
+ //#region src/constants.ts
125
+ /** Default name for the encrypted session cookie. */
126
+ const DEFAULT_COOKIE_NAME = "__authgate";
127
+ /** Cookie name used to store the OAuth CSRF nonce during the authorization flow. */
128
+ const NONCE_COOKIE_NAME = "__authgate_nonce";
129
+ /** Cookie name used to store the opaque refresh token. */
130
+ const REFRESH_TOKEN_COOKIE_NAME = "__authgate_refresh";
131
+ /**
132
+ * Default session lifetime in seconds (7 days).
133
+ *
134
+ * Can be overridden per-client via {@link AuthGateConfig.sessionMaxAge}
135
+ * or per-project in the AuthGate dashboard under Settings > Session Duration.
136
+ */
137
+ const SESSION_MAX_AGE = 604800;
138
+ /** Maximum age of an OAuth state parameter before it is considered expired (10 minutes). */
139
+ const STATE_TTL_MS = 6e5;
140
+ /**
141
+ * List of OAuth providers supported by AuthGate.
142
+ *
143
+ * Configure provider credentials in the AuthGate dashboard under Providers.
144
+ */
145
+ const SUPPORTED_PROVIDERS = [
146
+ "google",
147
+ "github",
148
+ "discord",
149
+ "azure",
150
+ "apple"
156
151
  ];
157
-
158
- // src/crypto/session.ts
159
- var VERSION = 1;
152
+ //#endregion
153
+ //#region src/crypto/session.ts
154
+ /** Current wire format version. Increment when the binary layout changes. */
155
+ const VERSION = 1;
156
+ /**
157
+ * Encrypt a user object into a session cookie value.
158
+ *
159
+ * Creates a {@link SessionPayload} with `iat`/`exp` timestamps, encrypts it
160
+ * with AES-256-GCM, and returns a Base64url-encoded string suitable for
161
+ * storing in an HTTP cookie.
162
+ *
163
+ * @param key - 32-byte AES-GCM key from {@link deriveEncryptionKey}.
164
+ * @param user - The authenticated user to store in the session.
165
+ * @param maxAge - Session lifetime in seconds. Defaults to {@link SESSION_MAX_AGE}.
166
+ * @returns Base64url-encoded encrypted session string.
167
+ */
160
168
  async function encryptSession(key, user, maxAge = SESSION_MAX_AGE) {
161
- const now = Math.floor(Date.now() / 1e3);
162
- const payload = {
163
- user,
164
- iat: now,
165
- exp: now + maxAge
166
- };
167
- const iv = crypto.getRandomValues(new Uint8Array(12));
168
- const cryptoKey = await crypto.subtle.importKey(
169
- "raw",
170
- key,
171
- "AES-GCM",
172
- false,
173
- ["encrypt"]
174
- );
175
- const plaintext = encodeUtf8(JSON.stringify(payload));
176
- const ciphertextWithTag = new Uint8Array(
177
- await crypto.subtle.encrypt(
178
- { name: "AES-GCM", iv, tagLength: 128 },
179
- cryptoKey,
180
- plaintext
181
- )
182
- );
183
- const ciphertext = ciphertextWithTag.subarray(
184
- 0,
185
- ciphertextWithTag.length - 16
186
- );
187
- const authTag = ciphertextWithTag.subarray(ciphertextWithTag.length - 16);
188
- const result = concat(new Uint8Array([VERSION]), iv, authTag, ciphertext);
189
- return toBase64Url(result);
169
+ const now = Math.floor(Date.now() / 1e3);
170
+ const payload = {
171
+ user,
172
+ iat: now,
173
+ exp: now + maxAge
174
+ };
175
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
176
+ const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
177
+ const plaintext = encodeUtf8(JSON.stringify(payload));
178
+ const ciphertextWithTag = new Uint8Array(await crypto.subtle.encrypt({
179
+ name: "AES-GCM",
180
+ iv,
181
+ tagLength: 128
182
+ }, cryptoKey, plaintext));
183
+ const ciphertext = ciphertextWithTag.subarray(0, ciphertextWithTag.length - 16);
184
+ const authTag = ciphertextWithTag.subarray(ciphertextWithTag.length - 16);
185
+ return toBase64Url(concat(new Uint8Array([VERSION]), iv, authTag, ciphertext));
190
186
  }
187
+ /**
188
+ * Decrypt a session cookie and return the full payload if valid.
189
+ *
190
+ * Returns `null` for any failure (expired, tampered, wrong key, malformed)
191
+ * to prevent oracle attacks — all failures are observationally identical.
192
+ *
193
+ * @param key - 32-byte AES-GCM key from {@link deriveEncryptionKey}.
194
+ * @param cookieValue - The Base64url-encoded encrypted session string.
195
+ * @returns The full session payload, or `null` if the session is invalid/expired.
196
+ * @internal
197
+ */
191
198
  async function decryptSessionPayload(key, cookieValue) {
192
- try {
193
- const data = fromBase64Url(cookieValue);
194
- if (data.length < 30) return null;
195
- const version = data[0];
196
- if (version !== VERSION) return null;
197
- const iv = data.subarray(1, 13);
198
- const authTag = data.subarray(13, 29);
199
- const ciphertext = data.subarray(29);
200
- const ciphertextWithTag = concat(ciphertext, authTag);
201
- const cryptoKey = await crypto.subtle.importKey(
202
- "raw",
203
- key,
204
- "AES-GCM",
205
- false,
206
- ["decrypt"]
207
- );
208
- const decrypted = new Uint8Array(
209
- await crypto.subtle.decrypt(
210
- { name: "AES-GCM", iv, tagLength: 128 },
211
- cryptoKey,
212
- ciphertextWithTag
213
- )
214
- );
215
- const payload = JSON.parse(decodeUtf8(decrypted));
216
- const now = Math.floor(Date.now() / 1e3);
217
- if (payload.exp <= now) return null;
218
- return payload;
219
- } catch (e) {
220
- return null;
221
- }
199
+ try {
200
+ const data = fromBase64Url(cookieValue);
201
+ if (data.length < 30) return null;
202
+ if (data[0] !== VERSION) return null;
203
+ const iv = data.subarray(1, 13);
204
+ const authTag = data.subarray(13, 29);
205
+ const ciphertextWithTag = concat(data.subarray(29), authTag);
206
+ const cryptoKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
207
+ const decrypted = new Uint8Array(await crypto.subtle.decrypt({
208
+ name: "AES-GCM",
209
+ iv,
210
+ tagLength: 128
211
+ }, cryptoKey, ciphertextWithTag));
212
+ const payload = JSON.parse(decodeUtf8(decrypted));
213
+ const now = Math.floor(Date.now() / 1e3);
214
+ if (payload.exp <= now) return null;
215
+ return payload;
216
+ } catch {
217
+ return null;
218
+ }
222
219
  }
220
+ /**
221
+ * Decrypt a session cookie value and return the user if the session is still valid.
222
+ *
223
+ * Returns `null` for any failure (expired, tampered, wrong key, malformed)
224
+ * to prevent oracle attacks — all failures are observationally identical.
225
+ *
226
+ * @param key - 32-byte AES-GCM key from {@link deriveEncryptionKey}.
227
+ * @param cookieValue - The Base64url-encoded encrypted session string.
228
+ * @returns The authenticated user, or `null` if the session is invalid/expired.
229
+ */
223
230
  async function decryptSession(key, cookieValue) {
224
- var _a;
225
- const payload = await decryptSessionPayload(key, cookieValue);
226
- return (_a = payload == null ? void 0 : payload.user) != null ? _a : null;
231
+ return (await decryptSessionPayload(key, cookieValue))?.user ?? null;
227
232
  }
228
-
229
- // src/crypto/state.ts
233
+ //#endregion
234
+ //#region src/crypto/state.ts
235
+ /**
236
+ * OAuth state parameter creation and verification.
237
+ *
238
+ * The state parameter prevents CSRF attacks during the OAuth authorization
239
+ * flow. It combines a random nonce with a timestamp and HMAC-SHA256
240
+ * signature:
241
+ *
242
+ * ```
243
+ * state = "{nonce}.{timestamp}.{hmac_hex}"
244
+ * ```
245
+ *
246
+ * The nonce is stored in a short-lived httpOnly cookie and compared against
247
+ * the state returned by the OAuth provider's callback.
248
+ *
249
+ * @internal
250
+ * @module
251
+ */
252
+ /**
253
+ * Create a signed OAuth state parameter and its corresponding nonce.
254
+ *
255
+ * The nonce should be stored in a short-lived cookie (`__authgate_nonce`)
256
+ * and the state should be passed as the `state` query parameter in the
257
+ * OAuth authorization URL.
258
+ *
259
+ * @param signingKey - 32-byte HMAC key from {@link deriveStateKey}.
260
+ * @returns An object with `state` (the full signed string) and `nonce` (hex-encoded random bytes).
261
+ */
230
262
  async function createOAuthState(signingKey) {
231
- const nonceBytes = crypto.getRandomValues(new Uint8Array(16));
232
- const nonce = toHex(nonceBytes);
233
- const timestamp = Date.now().toString();
234
- const payload = `${nonce}.${timestamp}`;
235
- const key = await crypto.subtle.importKey(
236
- "raw",
237
- signingKey,
238
- { name: "HMAC", hash: "SHA-256" },
239
- false,
240
- ["sign"]
241
- );
242
- const sigBytes = new Uint8Array(
243
- await crypto.subtle.sign("HMAC", key, encodeUtf8(payload))
244
- );
245
- const signature = toHex(sigBytes);
246
- return {
247
- state: `${payload}.${signature}`,
248
- nonce
249
- };
263
+ const nonce = toHex(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)));
264
+ const payload = `${nonce}.${Date.now().toString()}`;
265
+ const key = await crypto.subtle.importKey("raw", signingKey, {
266
+ name: "HMAC",
267
+ hash: "SHA-256"
268
+ }, false, ["sign"]);
269
+ return {
270
+ state: `${payload}.${toHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, encodeUtf8(payload))))}`,
271
+ nonce
272
+ };
250
273
  }
274
+ /**
275
+ * Verify an OAuth state parameter against the stored nonce.
276
+ *
277
+ * Performs three checks:
278
+ * 1. **Nonce match** — constant-time comparison against the cookie nonce.
279
+ * 2. **TTL check** — the state must be younger than {@link STATE_TTL_MS} (10 min).
280
+ * 3. **HMAC verification** — via `crypto.subtle.verify` (timing-safe).
281
+ *
282
+ * @param state - The `state` query parameter from the OAuth callback.
283
+ * @param nonce - The nonce value from the `__authgate_nonce` cookie.
284
+ * @param signingKey - 32-byte HMAC key from {@link deriveStateKey}.
285
+ * @returns `true` if all checks pass, `false` otherwise.
286
+ */
251
287
  async function verifyOAuthState(state, nonce, signingKey) {
252
- try {
253
- const parts = state.split(".");
254
- if (parts.length !== 3) return false;
255
- const [stateNonce, timestamp, signature] = parts;
256
- const nonceBuf = encodeUtf8(stateNonce);
257
- const expectedNonceBuf = encodeUtf8(nonce);
258
- if (nonceBuf.length !== expectedNonceBuf.length) return false;
259
- if (!constantTimeEqual(nonceBuf, expectedNonceBuf)) return false;
260
- const stateTime = parseInt(timestamp, 10);
261
- if (isNaN(stateTime) || Date.now() - stateTime > STATE_TTL_MS) return false;
262
- const payload = `${stateNonce}.${timestamp}`;
263
- const key = await crypto.subtle.importKey(
264
- "raw",
265
- signingKey,
266
- { name: "HMAC", hash: "SHA-256" },
267
- false,
268
- ["verify"]
269
- );
270
- const sigBytes = new Uint8Array(signature.length / 2);
271
- for (let i = 0; i < signature.length; i += 2) {
272
- sigBytes[i / 2] = parseInt(signature.substring(i, i + 2), 16);
273
- }
274
- return crypto.subtle.verify(
275
- "HMAC",
276
- key,
277
- sigBytes,
278
- encodeUtf8(payload)
279
- );
280
- } catch (e) {
281
- return false;
282
- }
288
+ try {
289
+ const parts = state.split(".");
290
+ if (parts.length !== 3) return false;
291
+ const [stateNonce, timestamp, signature] = parts;
292
+ const nonceBuf = encodeUtf8(stateNonce);
293
+ const expectedNonceBuf = encodeUtf8(nonce);
294
+ if (nonceBuf.length !== expectedNonceBuf.length) return false;
295
+ if (!constantTimeEqual(nonceBuf, expectedNonceBuf)) return false;
296
+ const stateTime = parseInt(timestamp, 10);
297
+ if (isNaN(stateTime) || Date.now() - stateTime > 6e5) return false;
298
+ const payload = `${stateNonce}.${timestamp}`;
299
+ const key = await crypto.subtle.importKey("raw", signingKey, {
300
+ name: "HMAC",
301
+ hash: "SHA-256"
302
+ }, false, ["verify"]);
303
+ const sigBytes = new Uint8Array(signature.length / 2);
304
+ for (let i = 0; i < signature.length; i += 2) sigBytes[i / 2] = parseInt(signature.substring(i, i + 2), 16);
305
+ return crypto.subtle.verify("HMAC", key, sigBytes, encodeUtf8(payload));
306
+ } catch {
307
+ return false;
308
+ }
283
309
  }
284
-
285
- // src/errors.ts
310
+ //#endregion
311
+ //#region src/errors.ts
312
+ /**
313
+ * Base error class for all AuthGate errors.
314
+ *
315
+ * All errors thrown by the `@auth-gate/*` packages extend this class,
316
+ * so you can catch `AuthGateError` to handle any AuthGate-specific failure.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * try {
321
+ * await client.verifyToken(token);
322
+ * } catch (err) {
323
+ * if (err instanceof AuthGateError) {
324
+ * console.error("AuthGate error:", err.message);
325
+ * }
326
+ * }
327
+ * ```
328
+ */
286
329
  var AuthGateError = class extends Error {
287
- constructor(message) {
288
- super(message);
289
- this.name = "AuthGateError";
290
- }
330
+ constructor(message) {
331
+ super(message);
332
+ this.name = "AuthGateError";
333
+ }
291
334
  };
335
+ /**
336
+ * Thrown when JWT token verification fails or times out.
337
+ *
338
+ * This can occur when the token is malformed, expired, or the
339
+ * AuthGate API is unreachable within the 5-second timeout.
340
+ */
292
341
  var TokenVerificationError = class extends AuthGateError {
293
- constructor(message = "Token verification failed") {
294
- super(message);
295
- this.name = "TokenVerificationError";
296
- }
342
+ constructor(message = "Token verification failed") {
343
+ super(message);
344
+ this.name = "TokenVerificationError";
345
+ }
297
346
  };
347
+ /**
348
+ * Thrown when an encrypted session cookie cannot be decrypted.
349
+ *
350
+ * Common causes: the session secret was rotated, the cookie was
351
+ * tampered with, or the cookie format version is unsupported.
352
+ */
298
353
  var SessionDecryptionError = class extends AuthGateError {
299
- constructor(message = "Session decryption failed") {
300
- super(message);
301
- this.name = "SessionDecryptionError";
302
- }
354
+ constructor(message = "Session decryption failed") {
355
+ super(message);
356
+ this.name = "SessionDecryptionError";
357
+ }
303
358
  };
359
+ /**
360
+ * Thrown when OAuth state verification fails.
361
+ *
362
+ * This indicates a potential CSRF attack, an expired state parameter
363
+ * (older than 10 minutes), or a nonce mismatch.
364
+ */
304
365
  var InvalidStateError = class extends AuthGateError {
305
- constructor(message = "Invalid OAuth state") {
306
- super(message);
307
- this.name = "InvalidStateError";
308
- }
366
+ constructor(message = "Invalid OAuth state") {
367
+ super(message);
368
+ this.name = "InvalidStateError";
369
+ }
309
370
  };
371
+ /**
372
+ * Thrown when the AuthGate SDK is initialized with invalid configuration.
373
+ *
374
+ * Check the error message for details on which config field is invalid
375
+ * and how to fix it.
376
+ */
310
377
  var ConfigurationError = class extends AuthGateError {
311
- constructor(message) {
312
- super(message);
313
- this.name = "ConfigurationError";
314
- }
378
+ constructor(message) {
379
+ super(message);
380
+ this.name = "ConfigurationError";
381
+ }
315
382
  };
316
-
317
- // src/client.ts
383
+ //#endregion
384
+ //#region src/client.ts
385
+ /**
386
+ * Core client for interacting with the AuthGate authentication proxy.
387
+ *
388
+ * Handles token verification, session cookie encryption/decryption,
389
+ * OAuth state management, and all proxy API calls (email, SMS, MFA).
390
+ *
391
+ * Use {@link createAuthGateClient} for a convenient factory function,
392
+ * or instantiate directly with `new AuthGateClient(config)`.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * const client = new AuthGateClient({
397
+ * apiKey: process.env.AUTHGATE_API_KEY!,
398
+ * baseUrl: "https://www.authgate.dev",
399
+ * });
400
+ * ```
401
+ */
318
402
  var AuthGateClient = class {
319
- /**
320
- * @param config - Client configuration. See {@link AuthGateConfig}.
321
- */
322
- constructor(config) {
323
- this.encryptionKey = null;
324
- this.stateKey = null;
325
- var _a, _b, _c;
326
- if (!config.apiKey || typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
327
- throw new ConfigurationError(
328
- "[AuthGate] Missing or empty `apiKey`. Get your API key from the AuthGate dashboard \u2192 Project \u2192 API Key."
329
- );
330
- }
331
- if (!config.baseUrl || typeof config.baseUrl !== "string" || config.baseUrl.trim() === "") {
332
- throw new ConfigurationError(
333
- '[AuthGate] Missing or empty `baseUrl`. Set this to your AuthGate service URL (e.g. "https://www.authgate.dev").'
334
- );
335
- }
336
- try {
337
- new URL(config.baseUrl);
338
- } catch (e) {
339
- throw new ConfigurationError(
340
- `[AuthGate] Invalid \`baseUrl\`: "${config.baseUrl}". Must be a valid URL (e.g. "https://www.authgate.dev").`
341
- );
342
- }
343
- if (config.callbackPath !== void 0 && !config.callbackPath.startsWith("/")) {
344
- throw new ConfigurationError(
345
- `[AuthGate] Invalid \`callbackPath\`: "${config.callbackPath}". Must start with "/" (e.g. "/api/auth/callback").`
346
- );
347
- }
348
- this.config = __spreadProps(__spreadValues({}, config), {
349
- cookieName: (_a = config.cookieName) != null ? _a : DEFAULT_COOKIE_NAME,
350
- sessionMaxAge: (_b = config.sessionMaxAge) != null ? _b : SESSION_MAX_AGE,
351
- callbackPath: (_c = config.callbackPath) != null ? _c : "/api/auth/callback"
352
- });
353
- this.keysReady = this.deriveKeys();
354
- }
355
- async deriveKeys() {
356
- const [encKey, stKey] = await Promise.all([
357
- deriveEncryptionKey(this.config.apiKey),
358
- deriveStateKey(this.config.apiKey)
359
- ]);
360
- this.encryptionKey = encKey;
361
- this.stateKey = stKey;
362
- }
363
- async getEncryptionKey() {
364
- await this.keysReady;
365
- return this.encryptionKey;
366
- }
367
- async getStateKey() {
368
- await this.keysReady;
369
- return this.stateKey;
370
- }
371
- // ---------------------------------------------------------------------------
372
- // Token verification
373
- // ---------------------------------------------------------------------------
374
- /**
375
- * Verify a JWT token against the AuthGate API.
376
- *
377
- * Makes a server-side `POST /api/v1/token/verify` call using your API key.
378
- * Times out after 5 seconds.
379
- *
380
- * @param token - The JWT token string from the OAuth callback or email/SMS flow.
381
- * @returns A {@link TokenVerifyResult} with the user data if valid.
382
- * @throws {@link TokenVerificationError} if the request times out.
383
- */
384
- async verifyToken(token) {
385
- const controller = new AbortController();
386
- const timeout = setTimeout(() => controller.abort(), 5e3);
387
- try {
388
- const response = await fetch(
389
- `${this.config.baseUrl}/api/v1/token/verify`,
390
- {
391
- method: "POST",
392
- headers: {
393
- Authorization: `Bearer ${this.config.apiKey}`,
394
- "Content-Type": "application/json"
395
- },
396
- body: JSON.stringify({ token }),
397
- signal: controller.signal
398
- }
399
- );
400
- if (!response.ok) {
401
- return { valid: false, error: `HTTP ${response.status}` };
402
- }
403
- return response.json();
404
- } catch (err) {
405
- if (err instanceof Error && err.name === "AbortError") {
406
- throw new TokenVerificationError("Token verification timed out");
407
- }
408
- return { valid: false, error: "Network error" };
409
- } finally {
410
- clearTimeout(timeout);
411
- }
412
- }
413
- // ---------------------------------------------------------------------------
414
- // Session encryption
415
- // ---------------------------------------------------------------------------
416
- /**
417
- * Encrypt user data into a session cookie value.
418
- *
419
- * Uses AES-256-GCM with a key derived from your session secret.
420
- * The returned string is safe to store in an httpOnly cookie.
421
- *
422
- * @param user - The authenticated user to store.
423
- * @returns Base64url-encoded encrypted session string.
424
- */
425
- async encryptSession(user) {
426
- const key = await this.getEncryptionKey();
427
- return await encryptSession(key, user, this.config.sessionMaxAge);
428
- }
429
- /**
430
- * Decrypt a session cookie and return the user if the session is valid.
431
- *
432
- * Returns `null` if the cookie is expired, tampered with, or encrypted
433
- * with a different secret. All failure modes return `null` to prevent
434
- * oracle attacks.
435
- *
436
- * @param cookieValue - The encrypted cookie string.
437
- * @returns The user, or `null` if the session is invalid.
438
- */
439
- async decryptSession(cookieValue) {
440
- const key = await this.getEncryptionKey();
441
- return await decryptSession(key, cookieValue);
442
- }
443
- /**
444
- * Decrypt a session cookie and return the full payload (user + timestamps).
445
- *
446
- * Used internally by middleware to check `iat` for session revalidation.
447
- *
448
- * @param cookieValue - The encrypted cookie string.
449
- * @returns The full session payload, or `null` if invalid.
450
- * @internal
451
- */
452
- async decryptSessionPayload(cookieValue) {
453
- const key = await this.getEncryptionKey();
454
- return await decryptSessionPayload(key, cookieValue);
455
- }
456
- // ---------------------------------------------------------------------------
457
- // OAuth state
458
- // ---------------------------------------------------------------------------
459
- /**
460
- * Create a signed OAuth state parameter and nonce for CSRF protection.
461
- *
462
- * Store the `nonce` in a short-lived httpOnly cookie and pass `state`
463
- * as a query parameter in the OAuth authorization URL.
464
- *
465
- * @returns `{ state, nonce }` — the state string and its corresponding nonce.
466
- */
467
- async createState() {
468
- const key = await this.getStateKey();
469
- return await createOAuthState(key);
470
- }
471
- /**
472
- * Verify an OAuth state parameter returned in the callback.
473
- *
474
- * @param state - The `state` query parameter from the callback.
475
- * @param nonce - The nonce from the httpOnly cookie.
476
- * @returns `true` if the state is valid and not expired.
477
- */
478
- async verifyState(state, nonce) {
479
- const key = await this.getStateKey();
480
- return await verifyOAuthState(state, nonce, key);
481
- }
482
- // ---------------------------------------------------------------------------
483
- // URL builders
484
- // ---------------------------------------------------------------------------
485
- /**
486
- * Build the OAuth authorization URL for a given provider.
487
- *
488
- * @param provider - The OAuth provider (e.g. `"google"`, `"github"`).
489
- * @param callbackUrl - Your app's callback URL where the user is redirected after auth.
490
- * @param options - Optional parameters.
491
- * @param options.linkTo - An existing user ID to link this OAuth account to.
492
- * @returns The full authorization URL to redirect the user to.
493
- * @throws {@link AuthGateError} if the provider is not in {@link SUPPORTED_PROVIDERS}.
494
- */
495
- async getOAuthUrl(provider, callbackUrl, options) {
496
- if (!SUPPORTED_PROVIDERS.includes(provider)) {
497
- throw new AuthGateError(`Unsupported provider: ${provider}`);
498
- }
499
- const response = await fetch(
500
- `${this.config.baseUrl}/api/proxy/${provider}`,
501
- {
502
- method: "POST",
503
- headers: {
504
- Authorization: `Bearer ${this.config.apiKey}`,
505
- "Content-Type": "application/json"
506
- },
507
- body: JSON.stringify(__spreadValues({
508
- callback_url: callbackUrl,
509
- state: options == null ? void 0 : options.state,
510
- link_to: options == null ? void 0 : options.linkTo
511
- }, (options == null ? void 0 : options.locale) && { locale: options.locale }))
512
- }
513
- );
514
- if (!response.ok) {
515
- const error = await response.json().catch(() => ({}));
516
- throw new AuthGateError(
517
- error.error || `OAuth initiation failed: ${response.status}`
518
- );
519
- }
520
- const data = await response.json();
521
- return data.redirect_url;
522
- }
523
- // ---------------------------------------------------------------------------
524
- // Email auth
525
- // ---------------------------------------------------------------------------
526
- /**
527
- * Register a new user with email and password.
528
- *
529
- * @param data - Signup data including email, password, optional name, and callback URL.
530
- * @returns The raw `Response` from the AuthGate proxy.
531
- */
532
- async emailSignup(data) {
533
- return fetch(`${this.config.baseUrl}/api/proxy/email/signup`, {
534
- method: "POST",
535
- headers: {
536
- Authorization: `Bearer ${this.config.apiKey}`,
537
- "Content-Type": "application/json"
538
- },
539
- body: JSON.stringify(__spreadValues({
540
- email: data.email,
541
- password: data.password,
542
- name: data.name,
543
- callback_url: data.callbackUrl
544
- }, data.locale && { locale: data.locale }))
545
- });
546
- }
547
- /**
548
- * Sign in an existing user with email and password.
549
- *
550
- * The response may include `mfa_required: true` if the user has MFA enabled,
551
- * in which case you should redirect to an MFA verification page.
552
- *
553
- * @param data - Sign-in data including email, password, and callback URL.
554
- * @returns The raw `Response` from the AuthGate proxy.
555
- */
556
- async emailSignin(data) {
557
- return fetch(`${this.config.baseUrl}/api/proxy/email/signin`, {
558
- method: "POST",
559
- headers: {
560
- Authorization: `Bearer ${this.config.apiKey}`,
561
- "Content-Type": "application/json"
562
- },
563
- body: JSON.stringify(__spreadValues({
564
- email: data.email,
565
- password: data.password,
566
- callback_url: data.callbackUrl
567
- }, data.locale && { locale: data.locale }))
568
- });
569
- }
570
- /**
571
- * Send a password reset email to the user.
572
- *
573
- * @param data - The user's email and the URL to redirect to after clicking the reset link.
574
- * @returns The raw `Response` from the AuthGate proxy.
575
- */
576
- async emailForgotPassword(data) {
577
- return fetch(`${this.config.baseUrl}/api/proxy/email/reset-password`, {
578
- method: "POST",
579
- headers: {
580
- Authorization: `Bearer ${this.config.apiKey}`,
581
- "Content-Type": "application/json"
582
- },
583
- body: JSON.stringify(__spreadValues({
584
- email: data.email,
585
- callback_url: data.callbackUrl
586
- }, data.locale && { locale: data.locale }))
587
- });
588
- }
589
- /**
590
- * Confirm a password reset with the token from the reset email.
591
- *
592
- * @param data - The reset token and the new password.
593
- * @returns The raw `Response` from the AuthGate proxy.
594
- */
595
- async emailResetPassword(data) {
596
- return fetch(
597
- `${this.config.baseUrl}/api/proxy/email/reset-password/confirm`,
598
- {
599
- method: "POST",
600
- headers: {
601
- Authorization: `Bearer ${this.config.apiKey}`,
602
- "Content-Type": "application/json"
603
- },
604
- body: JSON.stringify(__spreadValues({
605
- token: data.token,
606
- password: data.password
607
- }, data.locale && { locale: data.locale }))
608
- }
609
- );
610
- }
611
- // ---------------------------------------------------------------------------
612
- // Magic link auth
613
- // ---------------------------------------------------------------------------
614
- /**
615
- * Send a magic link email for passwordless authentication.
616
- *
617
- * @param data - The user's email and callback URL.
618
- * @returns The raw `Response` from the AuthGate proxy.
619
- */
620
- async magicLinkSend(data) {
621
- return fetch(`${this.config.baseUrl}/api/proxy/email/magic-link/send`, {
622
- method: "POST",
623
- headers: {
624
- Authorization: `Bearer ${this.config.apiKey}`,
625
- "Content-Type": "application/json"
626
- },
627
- body: JSON.stringify(__spreadValues({
628
- email: data.email,
629
- callback_url: data.callbackUrl
630
- }, data.locale && { locale: data.locale }))
631
- });
632
- }
633
- // ---------------------------------------------------------------------------
634
- // Email verification
635
- // ---------------------------------------------------------------------------
636
- /**
637
- * Verify an email address using a one-time code.
638
- *
639
- * @param data - The user's email and the verification code.
640
- * @returns The raw `Response` from the AuthGate proxy.
641
- */
642
- async emailVerifyCode(data) {
643
- return fetch(`${this.config.baseUrl}/api/proxy/email/verify`, {
644
- method: "POST",
645
- headers: {
646
- Authorization: `Bearer ${this.config.apiKey}`,
647
- "Content-Type": "application/json"
648
- },
649
- body: JSON.stringify(__spreadValues({
650
- email: data.email,
651
- code: data.code
652
- }, data.locale && { locale: data.locale }))
653
- });
654
- }
655
- // ---------------------------------------------------------------------------
656
- // SMS auth
657
- // ---------------------------------------------------------------------------
658
- /**
659
- * Send an SMS verification code to the user's phone number.
660
- *
661
- * @param data - The phone number (E.164 format) and callback URL.
662
- * @returns The raw `Response` from the AuthGate proxy.
663
- */
664
- async smsSendCode(data) {
665
- return fetch(`${this.config.baseUrl}/api/proxy/sms/send-code`, {
666
- method: "POST",
667
- headers: {
668
- Authorization: `Bearer ${this.config.apiKey}`,
669
- "Content-Type": "application/json"
670
- },
671
- body: JSON.stringify({
672
- phone: data.phone,
673
- callback_url: data.callbackUrl
674
- })
675
- });
676
- }
677
- /**
678
- * Verify an SMS code and authenticate the user.
679
- *
680
- * @param data - The phone number, verification code, and callback URL.
681
- * @returns The raw `Response` from the AuthGate proxy.
682
- */
683
- async smsVerifyCode(data) {
684
- return fetch(`${this.config.baseUrl}/api/proxy/sms/verify-code`, {
685
- method: "POST",
686
- headers: {
687
- Authorization: `Bearer ${this.config.apiKey}`,
688
- "Content-Type": "application/json"
689
- },
690
- body: JSON.stringify({
691
- phone: data.phone,
692
- code: data.code,
693
- callback_url: data.callbackUrl
694
- })
695
- });
696
- }
697
- // ---------------------------------------------------------------------------
698
- // Session management
699
- // ---------------------------------------------------------------------------
700
- /**
701
- * Exchange a refresh token for a new JWT.
702
- *
703
- * Refresh tokens are single-use — each call returns a new JWT and
704
- * invalidates the previous refresh token (rotation).
705
- *
706
- * @param refreshToken - The opaque refresh token.
707
- * @returns A new JWT and its `expires_in` (seconds), or `null` if the refresh token is invalid.
708
- */
709
- async refreshToken(refreshToken) {
710
- try {
711
- const response = await fetch(
712
- `${this.config.baseUrl}/api/proxy/token/refresh`,
713
- {
714
- method: "POST",
715
- headers: {
716
- Authorization: `Bearer ${this.config.apiKey}`,
717
- "Content-Type": "application/json"
718
- },
719
- body: JSON.stringify({
720
- refresh_token: refreshToken
721
- })
722
- }
723
- );
724
- if (!response.ok) return null;
725
- return response.json();
726
- } catch (e) {
727
- return null;
728
- }
729
- }
730
- /**
731
- * Revoke a session by its refresh token.
732
- *
733
- * After revocation, the refresh token can no longer be used to obtain
734
- * new JWTs. The user will need to re-authenticate.
735
- *
736
- * @param refreshToken - The opaque refresh token to revoke.
737
- * @returns `true` if the session was revoked, `false` on failure.
738
- */
739
- async revokeSession(refreshToken) {
740
- try {
741
- const response = await fetch(
742
- `${this.config.baseUrl}/api/proxy/sessions/revoke`,
743
- {
744
- method: "POST",
745
- headers: {
746
- Authorization: `Bearer ${this.config.apiKey}`,
747
- "Content-Type": "application/json"
748
- },
749
- body: JSON.stringify({
750
- refresh_token: refreshToken
751
- })
752
- }
753
- );
754
- return response.ok;
755
- } catch (e) {
756
- return false;
757
- }
758
- }
759
- // ---------------------------------------------------------------------------
760
- // MFA
761
- // ---------------------------------------------------------------------------
762
- /**
763
- * Verify an MFA code to complete authentication.
764
- *
765
- * Called after a sign-in response includes `mfa_required: true`.
766
- *
767
- * @param data - The MFA challenge token, verification code, and method.
768
- * @returns The raw `Response` — includes a JWT on success.
769
- */
770
- async mfaVerify(data) {
771
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/verify`, {
772
- method: "POST",
773
- headers: {
774
- Authorization: `Bearer ${this.config.apiKey}`,
775
- "Content-Type": "application/json"
776
- },
777
- body: JSON.stringify(data)
778
- });
779
- }
780
- /**
781
- * Begin TOTP (authenticator app) setup for the authenticated user.
782
- *
783
- * Requires a valid JWT in the `Authorization: Bearer` header.
784
- *
785
- * @param token - The user's current JWT.
786
- * @returns The raw `Response` with TOTP secret, QR code, and URI.
787
- */
788
- async mfaTotpSetup(token) {
789
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp/setup`, {
790
- method: "POST",
791
- headers: {
792
- Authorization: `Bearer ${token}`,
793
- "Content-Type": "application/json"
794
- },
795
- body: "{}"
796
- });
797
- }
798
- /**
799
- * Confirm TOTP setup by verifying a code from the authenticator app.
800
- *
801
- * @param token - The user's current JWT.
802
- * @param code - The 6-digit TOTP code from the authenticator app.
803
- * @returns The raw `Response` — confirms enrollment on success.
804
- */
805
- async mfaTotpVerifySetup(token, code) {
806
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp/verify-setup`, {
807
- method: "POST",
808
- headers: {
809
- Authorization: `Bearer ${token}`,
810
- "Content-Type": "application/json"
811
- },
812
- body: JSON.stringify({ code })
813
- });
814
- }
815
- /**
816
- * Disable TOTP for the authenticated user.
817
- *
818
- * @param token - The user's current JWT.
819
- * @param code - A valid TOTP code to confirm the action.
820
- * @returns The raw `Response`.
821
- */
822
- async mfaTotpDisable(token, code) {
823
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp`, {
824
- method: "DELETE",
825
- headers: {
826
- Authorization: `Bearer ${token}`,
827
- "Content-Type": "application/json"
828
- },
829
- body: JSON.stringify({ code })
830
- });
831
- }
832
- /**
833
- * Enable SMS-based MFA for the authenticated user.
834
- *
835
- * Uses the phone number already associated with the user's account.
836
- *
837
- * @param token - The user's current JWT.
838
- * @returns The raw `Response`.
839
- */
840
- async mfaSmsEnable(token) {
841
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms/enable`, {
842
- method: "POST",
843
- headers: {
844
- Authorization: `Bearer ${token}`,
845
- "Content-Type": "application/json"
846
- },
847
- body: "{}"
848
- });
849
- }
850
- /**
851
- * Disable SMS-based MFA for the authenticated user.
852
- *
853
- * @param token - The user's current JWT.
854
- * @returns The raw `Response`.
855
- */
856
- async mfaSmsDisable(token) {
857
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms`, {
858
- method: "DELETE",
859
- headers: {
860
- Authorization: `Bearer ${token}`,
861
- "Content-Type": "application/json"
862
- },
863
- body: "{}"
864
- });
865
- }
866
- /**
867
- * Generate a new set of single-use MFA backup codes.
868
- *
869
- * Generating new codes invalidates all previously issued backup codes.
870
- *
871
- * @param token - The user's current JWT.
872
- * @returns The raw `Response` with an array of backup codes.
873
- */
874
- async mfaBackupCodesGenerate(token) {
875
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/backup-codes/generate`, {
876
- method: "POST",
877
- headers: {
878
- Authorization: `Bearer ${token}`,
879
- "Content-Type": "application/json"
880
- },
881
- body: "{}"
882
- });
883
- }
884
- /**
885
- * Get the current MFA enrollment status for the authenticated user.
886
- *
887
- * @param token - The user's current JWT.
888
- * @returns The raw `Response` with {@link MfaStatus} data.
889
- */
890
- async mfaStatus(token) {
891
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/status`, {
892
- method: "GET",
893
- headers: { Authorization: `Bearer ${token}` }
894
- });
895
- }
896
- /**
897
- * Send an SMS code for MFA verification during the challenge flow.
898
- *
899
- * @param challenge - The MFA challenge token from the sign-in response.
900
- * @returns The raw `Response`.
901
- */
902
- async mfaSmsSendCode(challenge) {
903
- return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms/send-code`, {
904
- method: "POST",
905
- headers: {
906
- Authorization: `Bearer ${this.config.apiKey}`,
907
- "Content-Type": "application/json"
908
- },
909
- body: JSON.stringify({ challenge })
910
- });
911
- }
912
- // ---------------------------------------------------------------------------
913
- // Organizations
914
- // ---------------------------------------------------------------------------
915
- /**
916
- * Create a new organization.
917
- *
918
- * @param data - Organization creation data including name, slug, optional image URL, and metadata.
919
- * @returns The raw `Response` from the AuthGate proxy.
920
- */
921
- async createOrg(data) {
922
- return fetch(`${this.config.baseUrl}/api/proxy/orgs`, {
923
- method: "POST",
924
- headers: {
925
- Authorization: `Bearer ${this.config.apiKey}`,
926
- "Content-Type": "application/json"
927
- },
928
- body: JSON.stringify({
929
- name: data.name,
930
- slug: data.slug,
931
- image_url: data.imageUrl,
932
- metadata: data.metadata
933
- })
934
- });
935
- }
936
- /**
937
- * List all organizations accessible to the current project.
938
- *
939
- * @returns The raw `Response` from the AuthGate proxy.
940
- */
941
- async listOrgs() {
942
- return fetch(`${this.config.baseUrl}/api/proxy/orgs`, {
943
- method: "GET",
944
- headers: {
945
- Authorization: `Bearer ${this.config.apiKey}`
946
- }
947
- });
948
- }
949
- /**
950
- * Get a single organization by ID or slug.
951
- *
952
- * @param orgIdOrSlug - The organization ID or slug.
953
- * @returns The raw `Response` from the AuthGate proxy.
954
- */
955
- async getOrg(orgIdOrSlug) {
956
- return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}`, {
957
- method: "GET",
958
- headers: {
959
- Authorization: `Bearer ${this.config.apiKey}`
960
- }
961
- });
962
- }
963
- /**
964
- * Update an organization's details.
965
- *
966
- * @param orgIdOrSlug - The organization ID or slug.
967
- * @param data - Fields to update. All fields are optional.
968
- * @returns The raw `Response` from the AuthGate proxy.
969
- */
970
- async updateOrg(orgIdOrSlug, data) {
971
- return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}`, {
972
- method: "PATCH",
973
- headers: {
974
- Authorization: `Bearer ${this.config.apiKey}`,
975
- "Content-Type": "application/json"
976
- },
977
- body: JSON.stringify({
978
- name: data.name,
979
- slug: data.slug,
980
- image_url: data.imageUrl,
981
- max_members: data.maxMembers,
982
- metadata: data.metadata
983
- })
984
- });
985
- }
986
- /**
987
- * Delete an organization.
988
- *
989
- * @param orgIdOrSlug - The organization ID or slug.
990
- * @returns The raw `Response` from the AuthGate proxy.
991
- */
992
- async deleteOrg(orgIdOrSlug) {
993
- return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}`, {
994
- method: "DELETE",
995
- headers: {
996
- Authorization: `Bearer ${this.config.apiKey}`
997
- }
998
- });
999
- }
1000
- /**
1001
- * List all members of an organization.
1002
- *
1003
- * @param orgIdOrSlug - The organization ID or slug.
1004
- * @returns The raw `Response` from the AuthGate proxy.
1005
- */
1006
- async listOrgMembers(orgIdOrSlug) {
1007
- return fetch(
1008
- `${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/members`,
1009
- {
1010
- method: "GET",
1011
- headers: {
1012
- Authorization: `Bearer ${this.config.apiKey}`
1013
- }
1014
- }
1015
- );
1016
- }
1017
- /**
1018
- * Update a member's role within an organization.
1019
- *
1020
- * @param orgIdOrSlug - The organization ID or slug.
1021
- * @param memberId - The member's user ID.
1022
- * @param data - The new role to assign.
1023
- * @returns The raw `Response` from the AuthGate proxy.
1024
- */
1025
- async updateOrgMember(orgIdOrSlug, memberId, data) {
1026
- return fetch(
1027
- `${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/members/${memberId}`,
1028
- {
1029
- method: "PATCH",
1030
- headers: {
1031
- Authorization: `Bearer ${this.config.apiKey}`,
1032
- "Content-Type": "application/json"
1033
- },
1034
- body: JSON.stringify({
1035
- role_key: data.roleKey
1036
- })
1037
- }
1038
- );
1039
- }
1040
- /**
1041
- * Remove a member from an organization.
1042
- *
1043
- * @param orgIdOrSlug - The organization ID or slug.
1044
- * @param memberId - The member's user ID.
1045
- * @returns The raw `Response` from the AuthGate proxy.
1046
- */
1047
- async removeOrgMember(orgIdOrSlug, memberId) {
1048
- return fetch(
1049
- `${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/members/${memberId}`,
1050
- {
1051
- method: "DELETE",
1052
- headers: {
1053
- Authorization: `Bearer ${this.config.apiKey}`
1054
- }
1055
- }
1056
- );
1057
- }
1058
- /**
1059
- * Invite a user to an organization by email.
1060
- *
1061
- * @param orgIdOrSlug - The organization ID or slug.
1062
- * @param data - The invitee's email and optional role.
1063
- * @returns The raw `Response` from the AuthGate proxy.
1064
- */
1065
- async inviteToOrg(orgIdOrSlug, data) {
1066
- return fetch(
1067
- `${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/invitations`,
1068
- {
1069
- method: "POST",
1070
- headers: {
1071
- Authorization: `Bearer ${this.config.apiKey}`,
1072
- "Content-Type": "application/json"
1073
- },
1074
- body: JSON.stringify({
1075
- email: data.email,
1076
- role_key: data.roleKey
1077
- })
1078
- }
1079
- );
1080
- }
1081
- /**
1082
- * List all pending invitations for an organization.
1083
- *
1084
- * @param orgIdOrSlug - The organization ID or slug.
1085
- * @returns The raw `Response` from the AuthGate proxy.
1086
- */
1087
- async listOrgInvitations(orgIdOrSlug) {
1088
- return fetch(
1089
- `${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/invitations`,
1090
- {
1091
- method: "GET",
1092
- headers: {
1093
- Authorization: `Bearer ${this.config.apiKey}`
1094
- }
1095
- }
1096
- );
1097
- }
1098
- /**
1099
- * Revoke a pending organization invitation.
1100
- *
1101
- * @param orgIdOrSlug - The organization ID or slug.
1102
- * @param invitationId - The invitation ID to revoke.
1103
- * @returns The raw `Response` from the AuthGate proxy.
1104
- */
1105
- async revokeOrgInvitation(orgIdOrSlug, invitationId) {
1106
- return fetch(
1107
- `${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/invitations/${invitationId}`,
1108
- {
1109
- method: "DELETE",
1110
- headers: {
1111
- Authorization: `Bearer ${this.config.apiKey}`
1112
- }
1113
- }
1114
- );
1115
- }
1116
- /**
1117
- * Accept an organization invitation using a token.
1118
- *
1119
- * @param data - The invitation token received via email.
1120
- * @returns The raw `Response` from the AuthGate proxy.
1121
- */
1122
- async acceptOrgInvitation(data) {
1123
- return fetch(`${this.config.baseUrl}/api/proxy/orgs/invitations/accept`, {
1124
- method: "POST",
1125
- headers: {
1126
- Authorization: `Bearer ${this.config.apiKey}`,
1127
- "Content-Type": "application/json"
1128
- },
1129
- body: JSON.stringify({
1130
- token: data.token
1131
- })
1132
- });
1133
- }
1134
- /**
1135
- * Leave an organization as the current user.
1136
- *
1137
- * @param orgIdOrSlug - The organization ID or slug.
1138
- * @returns The raw `Response` from the AuthGate proxy.
1139
- */
1140
- async leaveOrg(orgIdOrSlug) {
1141
- return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/leave`, {
1142
- method: "POST",
1143
- headers: {
1144
- Authorization: `Bearer ${this.config.apiKey}`,
1145
- "Content-Type": "application/json"
1146
- },
1147
- body: "{}"
1148
- });
1149
- }
1150
- // ---------------------------------------------------------------------------
1151
- // Cookie config accessors
1152
- // ---------------------------------------------------------------------------
1153
- /** The name of the session cookie (default: `"__authgate"`). */
1154
- get cookieName() {
1155
- return this.config.cookieName;
1156
- }
1157
- /** The name of the OAuth nonce cookie (`"__authgate_nonce"`). */
1158
- get nonceCookieName() {
1159
- return NONCE_COOKIE_NAME;
1160
- }
1161
- /** The name of the refresh token cookie (`"__authgate_refresh"`). */
1162
- get refreshTokenCookieName() {
1163
- return REFRESH_TOKEN_COOKIE_NAME;
1164
- }
1165
- /** The callback path where OAuth redirects are handled (default: `"/api/auth/callback"`). */
1166
- get callbackPath() {
1167
- return this.config.callbackPath;
1168
- }
1169
- /** The AuthGate service base URL. */
1170
- get baseUrl() {
1171
- return this.config.baseUrl;
1172
- }
1173
- /** Session lifetime in seconds. */
1174
- get sessionMaxAge() {
1175
- return this.config.sessionMaxAge;
1176
- }
1177
- /**
1178
- * Get cookie options for the session cookie.
1179
- *
1180
- * Returns `secure: true` in production, `httpOnly: true` always.
1181
- */
1182
- getSessionCookieOptions() {
1183
- return {
1184
- httpOnly: true,
1185
- secure: process.env.NODE_ENV === "production",
1186
- sameSite: "lax",
1187
- maxAge: this.config.sessionMaxAge,
1188
- path: "/"
1189
- };
1190
- }
1191
- /**
1192
- * Get cookie options for the OAuth nonce cookie.
1193
- *
1194
- * Short-lived (10 minutes) to match the state TTL.
1195
- */
1196
- getNonceCookieOptions() {
1197
- return {
1198
- httpOnly: true,
1199
- secure: process.env.NODE_ENV === "production",
1200
- sameSite: "lax",
1201
- maxAge: 600,
1202
- // 10 minutes
1203
- path: "/"
1204
- };
1205
- }
1206
- // ---------------------------------------------------------------------------
1207
- // Config validation (async, called by framework factories)
1208
- // ---------------------------------------------------------------------------
1209
- /**
1210
- * Validate the SDK configuration against the AuthGate server.
1211
- *
1212
- * Checks that the API key is valid and (optionally) that a callback URL
1213
- * is registered for the project. This is called automatically by framework
1214
- * factories (`createAuthGate`, `createAuthRoutes`) and logs errors to the
1215
- * console — it does NOT throw.
1216
- *
1217
- * @param callbackUrl - The full callback URL to check registration for.
1218
- * @internal
1219
- */
1220
- async validateConfig(callbackUrl) {
1221
- try {
1222
- const response = await fetch(
1223
- `${this.config.baseUrl}/api/v1/config/validate`,
1224
- {
1225
- method: "POST",
1226
- headers: {
1227
- Authorization: `Bearer ${this.config.apiKey}`,
1228
- "Content-Type": "application/json"
1229
- },
1230
- body: JSON.stringify({
1231
- callback_url: callbackUrl
1232
- })
1233
- }
1234
- );
1235
- if (response.status === 401) {
1236
- console.error(
1237
- "\n[AuthGate] \u2717 Invalid API key.\n The API key you provided is not recognized or has been revoked.\n \u2192 Go to your AuthGate dashboard \u2192 Project \u2192 API Key to get a valid key.\n"
1238
- );
1239
- return;
1240
- }
1241
- if (!response.ok) {
1242
- console.warn(
1243
- "[AuthGate] Could not validate config (server returned " + response.status + "). Auth flows may fail."
1244
- );
1245
- return;
1246
- }
1247
- const data = await response.json();
1248
- if (callbackUrl && data.callback_url_registered === false) {
1249
- console.error(
1250
- "\n[AuthGate] \u2717 Callback URL not registered: " + callbackUrl + "\n This URL must be added to your project's allowed callback URLs.\n \u2192 Go to your AuthGate dashboard \u2192 Project \u2192 Callback URLs \u2192 Add URL.\n"
1251
- );
1252
- }
1253
- } catch (e) {
1254
- console.warn(
1255
- "[AuthGate] Could not reach AuthGate server at " + this.config.baseUrl + " to validate config. Auth flows may fail if the URL is wrong."
1256
- );
1257
- }
1258
- }
403
+ /**
404
+ * @param config - Client configuration. See {@link AuthGateConfig}.
405
+ */
406
+ constructor(config) {
407
+ this.encryptionKey = null;
408
+ this.stateKey = null;
409
+ if (!config.apiKey || typeof config.apiKey !== "string" || config.apiKey.trim() === "") throw new ConfigurationError("[AuthGate] Missing or empty `apiKey`. Get your API key from the AuthGate dashboard → Project → API Key.");
410
+ if (!config.baseUrl || typeof config.baseUrl !== "string" || config.baseUrl.trim() === "") throw new ConfigurationError("[AuthGate] Missing or empty `baseUrl`. Set this to your AuthGate service URL (e.g. \"https://www.authgate.dev\").");
411
+ try {
412
+ new URL(config.baseUrl);
413
+ } catch {
414
+ throw new ConfigurationError(`[AuthGate] Invalid \`baseUrl\`: "${config.baseUrl}". Must be a valid URL (e.g. "https://www.authgate.dev").`);
415
+ }
416
+ if (config.callbackPath !== void 0 && !config.callbackPath.startsWith("/")) throw new ConfigurationError(`[AuthGate] Invalid \`callbackPath\`: "${config.callbackPath}". Must start with "/" (e.g. "/api/auth/callback").`);
417
+ this.config = {
418
+ ...config,
419
+ cookieName: config.cookieName ?? "__authgate",
420
+ sessionMaxAge: config.sessionMaxAge ?? 604800,
421
+ callbackPath: config.callbackPath ?? "/api/auth/callback"
422
+ };
423
+ this.keysReady = this.deriveKeys();
424
+ }
425
+ async deriveKeys() {
426
+ const [encKey, stKey] = await Promise.all([deriveEncryptionKey(this.config.apiKey), deriveStateKey(this.config.apiKey)]);
427
+ this.encryptionKey = encKey;
428
+ this.stateKey = stKey;
429
+ }
430
+ async getEncryptionKey() {
431
+ await this.keysReady;
432
+ return this.encryptionKey;
433
+ }
434
+ async getStateKey() {
435
+ await this.keysReady;
436
+ return this.stateKey;
437
+ }
438
+ /**
439
+ * Verify a JWT token against the AuthGate API.
440
+ *
441
+ * Makes a server-side `POST /api/v1/token/verify` call using your API key.
442
+ * Times out after 5 seconds.
443
+ *
444
+ * @param token - The JWT token string from the OAuth callback or email/SMS flow.
445
+ * @returns A {@link TokenVerifyResult} with the user data if valid.
446
+ * @throws {@link TokenVerificationError} if the request times out.
447
+ */
448
+ async verifyToken(token) {
449
+ const controller = new AbortController();
450
+ const timeout = setTimeout(() => controller.abort(), 5e3);
451
+ try {
452
+ const response = await fetch(`${this.config.baseUrl}/api/v1/token/verify`, {
453
+ method: "POST",
454
+ headers: {
455
+ Authorization: `Bearer ${this.config.apiKey}`,
456
+ "Content-Type": "application/json"
457
+ },
458
+ body: JSON.stringify({ token }),
459
+ signal: controller.signal
460
+ });
461
+ if (!response.ok) return {
462
+ valid: false,
463
+ error: `HTTP ${response.status}`
464
+ };
465
+ return response.json();
466
+ } catch (err) {
467
+ if (err instanceof Error && err.name === "AbortError") throw new TokenVerificationError("Token verification timed out");
468
+ return {
469
+ valid: false,
470
+ error: "Network error"
471
+ };
472
+ } finally {
473
+ clearTimeout(timeout);
474
+ }
475
+ }
476
+ /**
477
+ * Encrypt user data into a session cookie value.
478
+ *
479
+ * Uses AES-256-GCM with a key derived from your session secret.
480
+ * The returned string is safe to store in an httpOnly cookie.
481
+ *
482
+ * @param user - The authenticated user to store.
483
+ * @returns Base64url-encoded encrypted session string.
484
+ */
485
+ async encryptSession(user) {
486
+ return await encryptSession(await this.getEncryptionKey(), user, this.config.sessionMaxAge);
487
+ }
488
+ /**
489
+ * Decrypt a session cookie and return the user if the session is valid.
490
+ *
491
+ * Returns `null` if the cookie is expired, tampered with, or encrypted
492
+ * with a different secret. All failure modes return `null` to prevent
493
+ * oracle attacks.
494
+ *
495
+ * @param cookieValue - The encrypted cookie string.
496
+ * @returns The user, or `null` if the session is invalid.
497
+ */
498
+ async decryptSession(cookieValue) {
499
+ return await decryptSession(await this.getEncryptionKey(), cookieValue);
500
+ }
501
+ /**
502
+ * Decrypt a session cookie and return the full payload (user + timestamps).
503
+ *
504
+ * Used internally by middleware to check `iat` for session revalidation.
505
+ *
506
+ * @param cookieValue - The encrypted cookie string.
507
+ * @returns The full session payload, or `null` if invalid.
508
+ * @internal
509
+ */
510
+ async decryptSessionPayload(cookieValue) {
511
+ return await decryptSessionPayload(await this.getEncryptionKey(), cookieValue);
512
+ }
513
+ /**
514
+ * Create a signed OAuth state parameter and nonce for CSRF protection.
515
+ *
516
+ * Store the `nonce` in a short-lived httpOnly cookie and pass `state`
517
+ * as a query parameter in the OAuth authorization URL.
518
+ *
519
+ * @returns `{ state, nonce }` — the state string and its corresponding nonce.
520
+ */
521
+ async createState() {
522
+ return await createOAuthState(await this.getStateKey());
523
+ }
524
+ /**
525
+ * Verify an OAuth state parameter returned in the callback.
526
+ *
527
+ * @param state - The `state` query parameter from the callback.
528
+ * @param nonce - The nonce from the httpOnly cookie.
529
+ * @returns `true` if the state is valid and not expired.
530
+ */
531
+ async verifyState(state, nonce) {
532
+ return await verifyOAuthState(state, nonce, await this.getStateKey());
533
+ }
534
+ /**
535
+ * Build the OAuth authorization URL for a given provider.
536
+ *
537
+ * @param provider - The OAuth provider (e.g. `"google"`, `"github"`).
538
+ * @param callbackUrl - Your app's callback URL where the user is redirected after auth.
539
+ * @param options - Optional parameters.
540
+ * @param options.linkTo - An existing user ID to link this OAuth account to.
541
+ * @returns The full authorization URL to redirect the user to.
542
+ * @throws {@link AuthGateError} if the provider is not in {@link SUPPORTED_PROVIDERS}.
543
+ */
544
+ async getOAuthUrl(provider, callbackUrl, options) {
545
+ if (!SUPPORTED_PROVIDERS.includes(provider)) throw new AuthGateError(`Unsupported provider: ${provider}`);
546
+ const response = await fetch(`${this.config.baseUrl}/api/proxy/${provider}`, {
547
+ method: "POST",
548
+ headers: {
549
+ Authorization: `Bearer ${this.config.apiKey}`,
550
+ "Content-Type": "application/json"
551
+ },
552
+ body: JSON.stringify({
553
+ callback_url: callbackUrl,
554
+ state: options?.state,
555
+ link_to: options?.linkTo,
556
+ ...options?.locale && { locale: options.locale }
557
+ })
558
+ });
559
+ if (!response.ok) throw new AuthGateError((await response.json().catch(() => ({}))).error || `OAuth initiation failed: ${response.status}`);
560
+ return (await response.json()).redirect_url;
561
+ }
562
+ /**
563
+ * Register a new user with email and password.
564
+ *
565
+ * @param data - Signup data including email, password, optional name, and callback URL.
566
+ * @returns The raw `Response` from the AuthGate proxy.
567
+ */
568
+ async emailSignup(data) {
569
+ return fetch(`${this.config.baseUrl}/api/proxy/email/signup`, {
570
+ method: "POST",
571
+ headers: {
572
+ Authorization: `Bearer ${this.config.apiKey}`,
573
+ "Content-Type": "application/json"
574
+ },
575
+ body: JSON.stringify({
576
+ email: data.email,
577
+ password: data.password,
578
+ name: data.name,
579
+ callback_url: data.callbackUrl,
580
+ ...data.locale && { locale: data.locale }
581
+ })
582
+ });
583
+ }
584
+ /**
585
+ * Sign in an existing user with email and password.
586
+ *
587
+ * The response may include `mfa_required: true` if the user has MFA enabled,
588
+ * in which case you should redirect to an MFA verification page.
589
+ *
590
+ * @param data - Sign-in data including email, password, and callback URL.
591
+ * @returns The raw `Response` from the AuthGate proxy.
592
+ */
593
+ async emailSignin(data) {
594
+ return fetch(`${this.config.baseUrl}/api/proxy/email/signin`, {
595
+ method: "POST",
596
+ headers: {
597
+ Authorization: `Bearer ${this.config.apiKey}`,
598
+ "Content-Type": "application/json"
599
+ },
600
+ body: JSON.stringify({
601
+ email: data.email,
602
+ password: data.password,
603
+ callback_url: data.callbackUrl,
604
+ ...data.locale && { locale: data.locale }
605
+ })
606
+ });
607
+ }
608
+ /**
609
+ * Send a password reset email to the user.
610
+ *
611
+ * @param data - The user's email and the URL to redirect to after clicking the reset link.
612
+ * @returns The raw `Response` from the AuthGate proxy.
613
+ */
614
+ async emailForgotPassword(data) {
615
+ return fetch(`${this.config.baseUrl}/api/proxy/email/reset-password`, {
616
+ method: "POST",
617
+ headers: {
618
+ Authorization: `Bearer ${this.config.apiKey}`,
619
+ "Content-Type": "application/json"
620
+ },
621
+ body: JSON.stringify({
622
+ email: data.email,
623
+ callback_url: data.callbackUrl,
624
+ ...data.locale && { locale: data.locale }
625
+ })
626
+ });
627
+ }
628
+ /**
629
+ * Confirm a password reset with the token from the reset email.
630
+ *
631
+ * @param data - The reset token and the new password.
632
+ * @returns The raw `Response` from the AuthGate proxy.
633
+ */
634
+ async emailResetPassword(data) {
635
+ return fetch(`${this.config.baseUrl}/api/proxy/email/reset-password/confirm`, {
636
+ method: "POST",
637
+ headers: {
638
+ Authorization: `Bearer ${this.config.apiKey}`,
639
+ "Content-Type": "application/json"
640
+ },
641
+ body: JSON.stringify({
642
+ token: data.token,
643
+ password: data.password,
644
+ ...data.locale && { locale: data.locale }
645
+ })
646
+ });
647
+ }
648
+ /**
649
+ * Send a magic link email for passwordless authentication.
650
+ *
651
+ * @param data - The user's email and callback URL.
652
+ * @returns The raw `Response` from the AuthGate proxy.
653
+ */
654
+ async magicLinkSend(data) {
655
+ return fetch(`${this.config.baseUrl}/api/proxy/email/magic-link/send`, {
656
+ method: "POST",
657
+ headers: {
658
+ Authorization: `Bearer ${this.config.apiKey}`,
659
+ "Content-Type": "application/json"
660
+ },
661
+ body: JSON.stringify({
662
+ email: data.email,
663
+ callback_url: data.callbackUrl,
664
+ ...data.locale && { locale: data.locale }
665
+ })
666
+ });
667
+ }
668
+ /**
669
+ * Verify an email address using a one-time code.
670
+ *
671
+ * @param data - The user's email and the verification code.
672
+ * @returns The raw `Response` from the AuthGate proxy.
673
+ */
674
+ async emailVerifyCode(data) {
675
+ return fetch(`${this.config.baseUrl}/api/proxy/email/verify`, {
676
+ method: "POST",
677
+ headers: {
678
+ Authorization: `Bearer ${this.config.apiKey}`,
679
+ "Content-Type": "application/json"
680
+ },
681
+ body: JSON.stringify({
682
+ email: data.email,
683
+ code: data.code,
684
+ ...data.locale && { locale: data.locale }
685
+ })
686
+ });
687
+ }
688
+ /**
689
+ * Send an SMS verification code to the user's phone number.
690
+ *
691
+ * @param data - The phone number (E.164 format) and callback URL.
692
+ * @returns The raw `Response` from the AuthGate proxy.
693
+ */
694
+ async smsSendCode(data) {
695
+ return fetch(`${this.config.baseUrl}/api/proxy/sms/send-code`, {
696
+ method: "POST",
697
+ headers: {
698
+ Authorization: `Bearer ${this.config.apiKey}`,
699
+ "Content-Type": "application/json"
700
+ },
701
+ body: JSON.stringify({
702
+ phone: data.phone,
703
+ callback_url: data.callbackUrl
704
+ })
705
+ });
706
+ }
707
+ /**
708
+ * Verify an SMS code and authenticate the user.
709
+ *
710
+ * @param data - The phone number, verification code, and callback URL.
711
+ * @returns The raw `Response` from the AuthGate proxy.
712
+ */
713
+ async smsVerifyCode(data) {
714
+ return fetch(`${this.config.baseUrl}/api/proxy/sms/verify-code`, {
715
+ method: "POST",
716
+ headers: {
717
+ Authorization: `Bearer ${this.config.apiKey}`,
718
+ "Content-Type": "application/json"
719
+ },
720
+ body: JSON.stringify({
721
+ phone: data.phone,
722
+ code: data.code,
723
+ callback_url: data.callbackUrl
724
+ })
725
+ });
726
+ }
727
+ /**
728
+ * Exchange a refresh token for a new JWT.
729
+ *
730
+ * Refresh tokens are single-use: each call invalidates the token it was
731
+ * given, so callers must store the `refresh_token` it returns or their next
732
+ * refresh fails.
733
+ *
734
+ * @param refreshToken - The opaque refresh token.
735
+ * @returns A new JWT, its `expires_in` (seconds) and the rotated
736
+ * `refresh_token`, or `null` if the refresh token is invalid.
737
+ */
738
+ async refreshToken(refreshToken) {
739
+ try {
740
+ const response = await fetch(`${this.config.baseUrl}/api/proxy/token/refresh`, {
741
+ method: "POST",
742
+ headers: {
743
+ Authorization: `Bearer ${this.config.apiKey}`,
744
+ "Content-Type": "application/json"
745
+ },
746
+ body: JSON.stringify({ refresh_token: refreshToken })
747
+ });
748
+ if (!response.ok) return null;
749
+ return response.json();
750
+ } catch {
751
+ return null;
752
+ }
753
+ }
754
+ /**
755
+ * Exchange a single-use authorization code (delivered on the callback URL)
756
+ * for the JWT + refresh token. Codes are one-shot and expire after ~60s.
757
+ *
758
+ * @param code - The `code` query parameter from the callback redirect.
759
+ * @returns The tokens, or `null` if the code is invalid/expired/used.
760
+ */
761
+ async exchangeCode(code) {
762
+ try {
763
+ const response = await fetch(`${this.config.baseUrl}/api/proxy/token/exchange`, {
764
+ method: "POST",
765
+ headers: {
766
+ Authorization: `Bearer ${this.config.apiKey}`,
767
+ "Content-Type": "application/json"
768
+ },
769
+ body: JSON.stringify({ code })
770
+ });
771
+ if (!response.ok) return null;
772
+ return response.json();
773
+ } catch {
774
+ return null;
775
+ }
776
+ }
777
+ /**
778
+ * Revoke a session by its refresh token.
779
+ *
780
+ * After revocation, the refresh token can no longer be used to obtain
781
+ * new JWTs. The user will need to re-authenticate.
782
+ *
783
+ * @param refreshToken - The opaque refresh token to revoke.
784
+ * @returns `true` if the session was revoked, `false` on failure.
785
+ */
786
+ async revokeSession(refreshToken) {
787
+ try {
788
+ return (await fetch(`${this.config.baseUrl}/api/proxy/sessions/revoke`, {
789
+ method: "POST",
790
+ headers: {
791
+ Authorization: `Bearer ${this.config.apiKey}`,
792
+ "Content-Type": "application/json"
793
+ },
794
+ body: JSON.stringify({ refresh_token: refreshToken })
795
+ })).ok;
796
+ } catch {
797
+ return false;
798
+ }
799
+ }
800
+ /**
801
+ * Verify an MFA code to complete authentication.
802
+ *
803
+ * Called after a sign-in response includes `mfa_required: true`.
804
+ *
805
+ * @param data - The MFA challenge token, verification code, and method.
806
+ * @returns The raw `Response` — includes a JWT on success.
807
+ */
808
+ async mfaVerify(data) {
809
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/verify`, {
810
+ method: "POST",
811
+ headers: {
812
+ Authorization: `Bearer ${this.config.apiKey}`,
813
+ "Content-Type": "application/json"
814
+ },
815
+ body: JSON.stringify(data)
816
+ });
817
+ }
818
+ /**
819
+ * Begin TOTP (authenticator app) setup for the authenticated user.
820
+ *
821
+ * Requires a valid JWT in the `Authorization: Bearer` header.
822
+ *
823
+ * @param token - The user's current JWT.
824
+ * @returns The raw `Response` with TOTP secret, QR code, and URI.
825
+ */
826
+ async mfaTotpSetup(token) {
827
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp/setup`, {
828
+ method: "POST",
829
+ headers: {
830
+ Authorization: `Bearer ${token}`,
831
+ "Content-Type": "application/json"
832
+ },
833
+ body: "{}"
834
+ });
835
+ }
836
+ /**
837
+ * Confirm TOTP setup by verifying a code from the authenticator app.
838
+ *
839
+ * @param token - The user's current JWT.
840
+ * @param code - The 6-digit TOTP code from the authenticator app.
841
+ * @returns The raw `Response` — confirms enrollment on success.
842
+ */
843
+ async mfaTotpVerifySetup(token, code) {
844
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp/verify-setup`, {
845
+ method: "POST",
846
+ headers: {
847
+ Authorization: `Bearer ${token}`,
848
+ "Content-Type": "application/json"
849
+ },
850
+ body: JSON.stringify({ code })
851
+ });
852
+ }
853
+ /**
854
+ * Disable TOTP for the authenticated user.
855
+ *
856
+ * @param token - The user's current JWT.
857
+ * @param code - A valid TOTP code to confirm the action.
858
+ * @returns The raw `Response`.
859
+ */
860
+ async mfaTotpDisable(token, code) {
861
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp`, {
862
+ method: "DELETE",
863
+ headers: {
864
+ Authorization: `Bearer ${token}`,
865
+ "Content-Type": "application/json"
866
+ },
867
+ body: JSON.stringify({ code })
868
+ });
869
+ }
870
+ /**
871
+ * Enable SMS-based MFA for the authenticated user.
872
+ *
873
+ * Uses the phone number already associated with the user's account.
874
+ *
875
+ * @param token - The user's current JWT.
876
+ * @returns The raw `Response`.
877
+ */
878
+ async mfaSmsEnable(token) {
879
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms/enable`, {
880
+ method: "POST",
881
+ headers: {
882
+ Authorization: `Bearer ${token}`,
883
+ "Content-Type": "application/json"
884
+ },
885
+ body: "{}"
886
+ });
887
+ }
888
+ /**
889
+ * Disable SMS-based MFA for the authenticated user.
890
+ *
891
+ * @param token - The user's current JWT.
892
+ * @returns The raw `Response`.
893
+ */
894
+ async mfaSmsDisable(token) {
895
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms`, {
896
+ method: "DELETE",
897
+ headers: {
898
+ Authorization: `Bearer ${token}`,
899
+ "Content-Type": "application/json"
900
+ },
901
+ body: "{}"
902
+ });
903
+ }
904
+ /**
905
+ * Generate a new set of single-use MFA backup codes.
906
+ *
907
+ * Generating new codes invalidates all previously issued backup codes.
908
+ *
909
+ * @param token - The user's current JWT.
910
+ * @returns The raw `Response` with an array of backup codes.
911
+ */
912
+ async mfaBackupCodesGenerate(token) {
913
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/backup-codes/generate`, {
914
+ method: "POST",
915
+ headers: {
916
+ Authorization: `Bearer ${token}`,
917
+ "Content-Type": "application/json"
918
+ },
919
+ body: "{}"
920
+ });
921
+ }
922
+ /**
923
+ * Get the current MFA enrollment status for the authenticated user.
924
+ *
925
+ * @param token - The user's current JWT.
926
+ * @returns The raw `Response` with {@link MfaStatus} data.
927
+ */
928
+ async mfaStatus(token) {
929
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/status`, {
930
+ method: "GET",
931
+ headers: { Authorization: `Bearer ${token}` }
932
+ });
933
+ }
934
+ /**
935
+ * Send an SMS code for MFA verification during the challenge flow.
936
+ *
937
+ * @param challenge - The MFA challenge token from the sign-in response.
938
+ * @returns The raw `Response`.
939
+ */
940
+ async mfaSmsSendCode(challenge) {
941
+ return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms/send-code`, {
942
+ method: "POST",
943
+ headers: {
944
+ Authorization: `Bearer ${this.config.apiKey}`,
945
+ "Content-Type": "application/json"
946
+ },
947
+ body: JSON.stringify({ challenge })
948
+ });
949
+ }
950
+ /**
951
+ * Create a new organization.
952
+ *
953
+ * @param data - Organization creation data including name, slug, optional image URL, and metadata.
954
+ * @returns The raw `Response` from the AuthGate proxy.
955
+ */
956
+ async createOrg(data) {
957
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs`, {
958
+ method: "POST",
959
+ headers: {
960
+ Authorization: `Bearer ${this.config.apiKey}`,
961
+ "Content-Type": "application/json"
962
+ },
963
+ body: JSON.stringify({
964
+ name: data.name,
965
+ slug: data.slug,
966
+ image_url: data.imageUrl,
967
+ metadata: data.metadata
968
+ })
969
+ });
970
+ }
971
+ /**
972
+ * List all organizations accessible to the current project.
973
+ *
974
+ * @returns The raw `Response` from the AuthGate proxy.
975
+ */
976
+ async listOrgs() {
977
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs`, {
978
+ method: "GET",
979
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
980
+ });
981
+ }
982
+ /**
983
+ * Get a single organization by ID or slug.
984
+ *
985
+ * @param orgIdOrSlug - The organization ID or slug.
986
+ * @returns The raw `Response` from the AuthGate proxy.
987
+ */
988
+ async getOrg(orgIdOrSlug) {
989
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}`, {
990
+ method: "GET",
991
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
992
+ });
993
+ }
994
+ /**
995
+ * Update an organization's details.
996
+ *
997
+ * @param orgIdOrSlug - The organization ID or slug.
998
+ * @param data - Fields to update. All fields are optional.
999
+ * @returns The raw `Response` from the AuthGate proxy.
1000
+ */
1001
+ async updateOrg(orgIdOrSlug, data) {
1002
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}`, {
1003
+ method: "PATCH",
1004
+ headers: {
1005
+ Authorization: `Bearer ${this.config.apiKey}`,
1006
+ "Content-Type": "application/json"
1007
+ },
1008
+ body: JSON.stringify({
1009
+ name: data.name,
1010
+ slug: data.slug,
1011
+ image_url: data.imageUrl,
1012
+ max_members: data.maxMembers,
1013
+ metadata: data.metadata
1014
+ })
1015
+ });
1016
+ }
1017
+ /**
1018
+ * Delete an organization.
1019
+ *
1020
+ * @param orgIdOrSlug - The organization ID or slug.
1021
+ * @returns The raw `Response` from the AuthGate proxy.
1022
+ */
1023
+ async deleteOrg(orgIdOrSlug) {
1024
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}`, {
1025
+ method: "DELETE",
1026
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1027
+ });
1028
+ }
1029
+ /**
1030
+ * List all members of an organization.
1031
+ *
1032
+ * @param orgIdOrSlug - The organization ID or slug.
1033
+ * @returns The raw `Response` from the AuthGate proxy.
1034
+ */
1035
+ async listOrgMembers(orgIdOrSlug) {
1036
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/members`, {
1037
+ method: "GET",
1038
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1039
+ });
1040
+ }
1041
+ /**
1042
+ * Update a member's role within an organization.
1043
+ *
1044
+ * @param orgIdOrSlug - The organization ID or slug.
1045
+ * @param memberId - The member's user ID.
1046
+ * @param data - The new role to assign.
1047
+ * @returns The raw `Response` from the AuthGate proxy.
1048
+ */
1049
+ async updateOrgMember(orgIdOrSlug, memberId, data) {
1050
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/members/${memberId}`, {
1051
+ method: "PATCH",
1052
+ headers: {
1053
+ Authorization: `Bearer ${this.config.apiKey}`,
1054
+ "Content-Type": "application/json"
1055
+ },
1056
+ body: JSON.stringify({ role_key: data.roleKey })
1057
+ });
1058
+ }
1059
+ /**
1060
+ * Remove a member from an organization.
1061
+ *
1062
+ * @param orgIdOrSlug - The organization ID or slug.
1063
+ * @param memberId - The member's user ID.
1064
+ * @returns The raw `Response` from the AuthGate proxy.
1065
+ */
1066
+ async removeOrgMember(orgIdOrSlug, memberId) {
1067
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/members/${memberId}`, {
1068
+ method: "DELETE",
1069
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1070
+ });
1071
+ }
1072
+ /**
1073
+ * Invite a user to an organization by email.
1074
+ *
1075
+ * @param orgIdOrSlug - The organization ID or slug.
1076
+ * @param data - The invitee's email and optional role.
1077
+ * @returns The raw `Response` from the AuthGate proxy.
1078
+ */
1079
+ async inviteToOrg(orgIdOrSlug, data) {
1080
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/invitations`, {
1081
+ method: "POST",
1082
+ headers: {
1083
+ Authorization: `Bearer ${this.config.apiKey}`,
1084
+ "Content-Type": "application/json"
1085
+ },
1086
+ body: JSON.stringify({
1087
+ email: data.email,
1088
+ role_key: data.roleKey
1089
+ })
1090
+ });
1091
+ }
1092
+ /**
1093
+ * List all pending invitations for an organization.
1094
+ *
1095
+ * @param orgIdOrSlug - The organization ID or slug.
1096
+ * @returns The raw `Response` from the AuthGate proxy.
1097
+ */
1098
+ async listOrgInvitations(orgIdOrSlug) {
1099
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/invitations`, {
1100
+ method: "GET",
1101
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1102
+ });
1103
+ }
1104
+ /**
1105
+ * Revoke a pending organization invitation.
1106
+ *
1107
+ * @param orgIdOrSlug - The organization ID or slug.
1108
+ * @param invitationId - The invitation ID to revoke.
1109
+ * @returns The raw `Response` from the AuthGate proxy.
1110
+ */
1111
+ async revokeOrgInvitation(orgIdOrSlug, invitationId) {
1112
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/invitations/${invitationId}`, {
1113
+ method: "DELETE",
1114
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
1115
+ });
1116
+ }
1117
+ /**
1118
+ * Accept an organization invitation using a token.
1119
+ *
1120
+ * @param data - The invitation token received via email.
1121
+ * @returns The raw `Response` from the AuthGate proxy.
1122
+ */
1123
+ async acceptOrgInvitation(data) {
1124
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/invitations/accept`, {
1125
+ method: "POST",
1126
+ headers: {
1127
+ Authorization: `Bearer ${this.config.apiKey}`,
1128
+ "Content-Type": "application/json"
1129
+ },
1130
+ body: JSON.stringify({ token: data.token })
1131
+ });
1132
+ }
1133
+ /**
1134
+ * Leave an organization as the current user.
1135
+ *
1136
+ * @param orgIdOrSlug - The organization ID or slug.
1137
+ * @returns The raw `Response` from the AuthGate proxy.
1138
+ */
1139
+ async leaveOrg(orgIdOrSlug) {
1140
+ return fetch(`${this.config.baseUrl}/api/proxy/orgs/${orgIdOrSlug}/leave`, {
1141
+ method: "POST",
1142
+ headers: {
1143
+ Authorization: `Bearer ${this.config.apiKey}`,
1144
+ "Content-Type": "application/json"
1145
+ },
1146
+ body: "{}"
1147
+ });
1148
+ }
1149
+ /** The name of the session cookie (default: `"__authgate"`). */
1150
+ get cookieName() {
1151
+ return this.config.cookieName;
1152
+ }
1153
+ /** The name of the OAuth nonce cookie (`"__authgate_nonce"`). */
1154
+ get nonceCookieName() {
1155
+ return NONCE_COOKIE_NAME;
1156
+ }
1157
+ /** The name of the refresh token cookie (`"__authgate_refresh"`). */
1158
+ get refreshTokenCookieName() {
1159
+ return REFRESH_TOKEN_COOKIE_NAME;
1160
+ }
1161
+ /** The callback path where OAuth redirects are handled (default: `"/api/auth/callback"`). */
1162
+ get callbackPath() {
1163
+ return this.config.callbackPath;
1164
+ }
1165
+ /** The AuthGate service base URL. */
1166
+ get baseUrl() {
1167
+ return this.config.baseUrl;
1168
+ }
1169
+ /** Session lifetime in seconds. */
1170
+ get sessionMaxAge() {
1171
+ return this.config.sessionMaxAge;
1172
+ }
1173
+ /**
1174
+ * Get cookie options for the session cookie.
1175
+ *
1176
+ * Returns `secure: true` in production, `httpOnly: true` always.
1177
+ */
1178
+ getSessionCookieOptions() {
1179
+ return {
1180
+ httpOnly: true,
1181
+ secure: process.env.NODE_ENV === "production",
1182
+ sameSite: "lax",
1183
+ maxAge: this.config.sessionMaxAge,
1184
+ path: "/"
1185
+ };
1186
+ }
1187
+ /**
1188
+ * Get cookie options for the OAuth nonce cookie.
1189
+ *
1190
+ * Short-lived (10 minutes) to match the state TTL.
1191
+ */
1192
+ getNonceCookieOptions() {
1193
+ return {
1194
+ httpOnly: true,
1195
+ secure: process.env.NODE_ENV === "production",
1196
+ sameSite: "lax",
1197
+ maxAge: 600,
1198
+ path: "/"
1199
+ };
1200
+ }
1201
+ /**
1202
+ * Validate the SDK configuration against the AuthGate server.
1203
+ *
1204
+ * Checks that the API key is valid and (optionally) that a callback URL
1205
+ * is registered for the project. This is called automatically by framework
1206
+ * factories (`createAuthGate`, `createAuthRoutes`) and logs errors to the
1207
+ * console — it does NOT throw.
1208
+ *
1209
+ * @param callbackUrl - The full callback URL to check registration for.
1210
+ * @internal
1211
+ */
1212
+ async validateConfig(callbackUrl) {
1213
+ try {
1214
+ const response = await fetch(`${this.config.baseUrl}/api/v1/config/validate`, {
1215
+ method: "POST",
1216
+ headers: {
1217
+ Authorization: `Bearer ${this.config.apiKey}`,
1218
+ "Content-Type": "application/json"
1219
+ },
1220
+ body: JSON.stringify({ callback_url: callbackUrl })
1221
+ });
1222
+ if (response.status === 401) {
1223
+ console.error("\n[AuthGate] ✗ Invalid API key.\n The API key you provided is not recognized or has been revoked.\n → Go to your AuthGate dashboard → Project → API Key to get a valid key.\n");
1224
+ return;
1225
+ }
1226
+ if (!response.ok) {
1227
+ console.warn("[AuthGate] Could not validate config (server returned " + response.status + "). Auth flows may fail.");
1228
+ return;
1229
+ }
1230
+ const data = await response.json();
1231
+ if (callbackUrl && data.callback_url_registered === false) console.error("\n[AuthGate] ✗ Callback URL not registered: " + callbackUrl + "\n This URL must be added to your project's allowed callback URLs.\n → Go to your AuthGate dashboard → Project → Callback URLs → Add URL.\n");
1232
+ } catch {
1233
+ console.warn("[AuthGate] Could not reach AuthGate server at " + this.config.baseUrl + " to validate config. Auth flows may fail if the URL is wrong.");
1234
+ }
1235
+ }
1259
1236
  };
1237
+ /**
1238
+ * Create an {@link AuthGateClient} instance.
1239
+ *
1240
+ * Convenience factory — equivalent to `new AuthGateClient(config)`.
1241
+ *
1242
+ * @param config - Client configuration. See {@link AuthGateConfig}.
1243
+ * @returns A configured {@link AuthGateClient}.
1244
+ */
1260
1245
  function createAuthGateClient(config) {
1261
- return new AuthGateClient(config);
1246
+ return new AuthGateClient(config);
1262
1247
  }
1263
-
1264
- // src/project-config.ts
1248
+ //#endregion
1249
+ //#region src/project-config.ts
1250
+ /**
1251
+ * Define your project-level AuthGate configuration.
1252
+ *
1253
+ * Create an `authgate.config.ts` file in your project root and export
1254
+ * the result of this function as the default export. This gives you
1255
+ * type safety and IDE autocomplete for the config.
1256
+ *
1257
+ * The CLI auto-loads `.env` and `.env.local` before importing the
1258
+ * config, so `process.env` values are available.
1259
+ *
1260
+ * @param config - The project configuration object.
1261
+ * @returns The same config object (identity function for type inference).
1262
+ *
1263
+ * @example Minimal setup (env vars provide connection details)
1264
+ * ```ts
1265
+ * // authgate.config.ts
1266
+ * import { defineConfig } from "@auth-gate/core";
1267
+ *
1268
+ * export default defineConfig({
1269
+ * connection: {
1270
+ * baseUrl: process.env.AUTHGATE_URL,
1271
+ * apiKey: process.env.AUTHGATE_API_KEY,
1272
+ * },
1273
+ * });
1274
+ * ```
1275
+ *
1276
+ * @example Monorepo with custom config paths
1277
+ * ```ts
1278
+ * // authgate.config.ts
1279
+ * import { defineConfig } from "@auth-gate/core";
1280
+ *
1281
+ * export default defineConfig({
1282
+ * connection: {
1283
+ * baseUrl: process.env.AUTHGATE_URL,
1284
+ * apiKey: process.env.AUTHGATE_API_KEY,
1285
+ * },
1286
+ * rbacConfig: "./packages/api/authgate.rbac.ts",
1287
+ * billingConfig: "./packages/api/authgate.billing.ts",
1288
+ * });
1289
+ * ```
1290
+ *
1291
+ * @example Then sync with the CLI
1292
+ * ```sh
1293
+ * # Scaffold the config file
1294
+ * npx @auth-gate/rbac init --config
1295
+ *
1296
+ * # Sync RBAC roles & permissions
1297
+ * npx @auth-gate/rbac sync --apply
1298
+ *
1299
+ * # Sync billing plans & features
1300
+ * npx @auth-gate/billing sync --apply
1301
+ * ```
1302
+ */
1265
1303
  function defineConfig(config) {
1266
- return config;
1304
+ return config;
1267
1305
  }
1268
-
1269
- // src/billing.ts
1306
+ //#endregion
1307
+ //#region src/billing.ts
1308
+ /**
1309
+ * Check whether the user's entitlements include a specific plan.
1310
+ *
1311
+ * @param entitlements - The entitlement object from the billing API.
1312
+ * @param plan - The plan name to check (case-insensitive).
1313
+ * @returns `true` if the user is on the given plan.
1314
+ */
1270
1315
  function hasPlan(entitlements, plan) {
1271
- if (!(entitlements == null ? void 0 : entitlements.plan)) return false;
1272
- return entitlements.plan.toLowerCase() === plan.toLowerCase();
1316
+ if (!entitlements?.plan) return false;
1317
+ return entitlements.plan.toLowerCase() === plan.toLowerCase();
1273
1318
  }
1319
+ /**
1320
+ * Check whether the user's entitlements include a specific feature.
1321
+ *
1322
+ * @param entitlements - The entitlement object from the billing API.
1323
+ * @param feature - The feature name to check (case-insensitive).
1324
+ * @returns `true` if the user has the given feature.
1325
+ */
1274
1326
  function hasFeature(entitlements, feature) {
1275
- if (!(entitlements == null ? void 0 : entitlements.features)) return false;
1276
- const lower = feature.toLowerCase();
1277
- return entitlements.features.some((f) => f.toLowerCase() === lower);
1327
+ if (!entitlements?.features) return false;
1328
+ const lower = feature.toLowerCase();
1329
+ return entitlements.features.some((f) => f.toLowerCase() === lower);
1278
1330
  }
1331
+ /**
1332
+ * Check a specific feature entitlement, returning typed detail with metered usage info.
1333
+ *
1334
+ * @param entitlements - The extended entitlement object from the billing API.
1335
+ * @param feature - The feature name to check (case-insensitive).
1336
+ * @returns The typed entitlement detail, or a denied boolean detail if not found.
1337
+ */
1279
1338
  function checkFeature(entitlements, feature) {
1280
- if (!(entitlements == null ? void 0 : entitlements.featureDetails)) {
1281
- const allowed = hasFeature(entitlements, feature);
1282
- return { type: "boolean", feature, allowed };
1283
- }
1284
- const lower = feature.toLowerCase();
1285
- const key = Object.keys(entitlements.featureDetails).find(
1286
- (k) => k.toLowerCase() === lower
1287
- );
1288
- if (key) return entitlements.featureDetails[key];
1289
- return { type: "boolean", feature, allowed: false };
1339
+ if (!entitlements?.featureDetails) return {
1340
+ type: "boolean",
1341
+ feature,
1342
+ allowed: hasFeature(entitlements, feature)
1343
+ };
1344
+ const lower = feature.toLowerCase();
1345
+ const key = Object.keys(entitlements.featureDetails).find((k) => k.toLowerCase() === lower);
1346
+ if (key) return entitlements.featureDetails[key];
1347
+ return {
1348
+ type: "boolean",
1349
+ feature,
1350
+ allowed: false
1351
+ };
1290
1352
  }
1291
-
1292
- // src/organizations.ts
1353
+ //#endregion
1354
+ //#region src/organizations.ts
1355
+ /**
1356
+ * Check whether a membership includes a specific permission.
1357
+ *
1358
+ * Supports wildcard matching:
1359
+ * - `"*"` matches all permissions.
1360
+ * - `"resource:*"` matches any permission starting with `"resource:"`.
1361
+ *
1362
+ * @param membership - The user's org membership (or null).
1363
+ * @param permission - The permission string to check, e.g. `"documents:write"`.
1364
+ * @returns `true` if the membership grants the permission.
1365
+ */
1293
1366
  function hasPermission(membership, permission) {
1294
- var _a;
1295
- if (!((_a = membership == null ? void 0 : membership.role) == null ? void 0 : _a.permissions)) return false;
1296
- return matchesPermission(membership.role.permissions, permission);
1367
+ if (!membership?.role?.permissions) return false;
1368
+ return matchesPermission(membership.role.permissions, permission);
1297
1369
  }
1370
+ /**
1371
+ * Check whether a membership has a specific role key.
1372
+ *
1373
+ * @param membership - The user's org membership (or null).
1374
+ * @param roleKey - The role key to check, e.g. `"admin"`.
1375
+ * @returns `true` if the membership's role key matches.
1376
+ */
1298
1377
  function hasRole(membership, roleKey) {
1299
- var _a;
1300
- if (!((_a = membership == null ? void 0 : membership.role) == null ? void 0 : _a.key)) return false;
1301
- return membership.role.key === roleKey;
1378
+ if (!membership?.role?.key) return false;
1379
+ return membership.role.key === roleKey;
1302
1380
  }
1381
+ /**
1382
+ * Check whether a membership has at least one of the given permissions.
1383
+ *
1384
+ * @param membership - The user's org membership (or null).
1385
+ * @param permissions - Array of permission strings.
1386
+ * @returns `true` if at least one permission is granted.
1387
+ */
1303
1388
  function hasAnyPermission(membership, permissions) {
1304
- var _a;
1305
- if (!((_a = membership == null ? void 0 : membership.role) == null ? void 0 : _a.permissions)) return false;
1306
- return permissions.some(
1307
- (p) => matchesPermission(membership.role.permissions, p)
1308
- );
1389
+ if (!membership?.role?.permissions) return false;
1390
+ return permissions.some((p) => matchesPermission(membership.role.permissions, p));
1309
1391
  }
1392
+ /**
1393
+ * Check whether a membership has all of the given permissions.
1394
+ *
1395
+ * @param membership - The user's org membership (or null).
1396
+ * @param permissions - Array of permission strings.
1397
+ * @returns `true` if every permission is granted.
1398
+ */
1310
1399
  function hasAllPermissions(membership, permissions) {
1311
- var _a;
1312
- if (!((_a = membership == null ? void 0 : membership.role) == null ? void 0 : _a.permissions)) return false;
1313
- return permissions.every(
1314
- (p) => matchesPermission(membership.role.permissions, p)
1315
- );
1400
+ if (!membership?.role?.permissions) return false;
1401
+ return permissions.every((p) => matchesPermission(membership.role.permissions, p));
1316
1402
  }
1317
1403
  function matchesPermission(granted, required) {
1318
- for (const perm of granted) {
1319
- if (perm === "*") return true;
1320
- if (perm === required) return true;
1321
- if (perm.endsWith(":*")) {
1322
- const prefix = perm.slice(0, -1);
1323
- if (required.startsWith(prefix)) return true;
1324
- }
1325
- }
1326
- return false;
1404
+ for (const perm of granted) {
1405
+ if (perm === "*") return true;
1406
+ if (perm === required) return true;
1407
+ if (perm.endsWith(":*")) {
1408
+ const prefix = perm.slice(0, -1);
1409
+ if (required.startsWith(prefix)) return true;
1410
+ }
1411
+ }
1412
+ return false;
1327
1413
  }
1414
+ /**
1415
+ * Merge permissions from a project-level role and an org-level role (union).
1416
+ * Either may be null/undefined.
1417
+ */
1328
1418
  function mergePermissions(projectRole, orgMembership) {
1329
- var _a;
1330
- const set = /* @__PURE__ */ new Set();
1331
- if (projectRole == null ? void 0 : projectRole.permissions) {
1332
- for (const p of projectRole.permissions) set.add(p);
1333
- }
1334
- if ((_a = orgMembership == null ? void 0 : orgMembership.role) == null ? void 0 : _a.permissions) {
1335
- for (const p of orgMembership.role.permissions) set.add(p);
1336
- }
1337
- return [...set];
1419
+ const set = /* @__PURE__ */ new Set();
1420
+ if (projectRole?.permissions) for (const p of projectRole.permissions) set.add(p);
1421
+ if (orgMembership?.role?.permissions) for (const p of orgMembership.role.permissions) set.add(p);
1422
+ return [...set];
1338
1423
  }
1424
+ /**
1425
+ * Check a permission against a merged permission set (project + org).
1426
+ * Supports wildcard matching.
1427
+ */
1339
1428
  function hasMergedPermission(projectRole, orgMembership, permission) {
1340
- const merged = mergePermissions(projectRole, orgMembership);
1341
- return matchesPermission(merged, permission);
1429
+ return matchesPermission(mergePermissions(projectRole, orgMembership), permission);
1342
1430
  }
1431
+ //#endregion
1432
+ exports.AuthGateClient = AuthGateClient;
1433
+ exports.AuthGateError = AuthGateError;
1434
+ exports.ConfigurationError = ConfigurationError;
1435
+ exports.DEFAULT_COOKIE_NAME = DEFAULT_COOKIE_NAME;
1436
+ exports.InvalidStateError = InvalidStateError;
1437
+ exports.NONCE_COOKIE_NAME = NONCE_COOKIE_NAME;
1438
+ exports.REFRESH_TOKEN_COOKIE_NAME = REFRESH_TOKEN_COOKIE_NAME;
1439
+ exports.SESSION_MAX_AGE = SESSION_MAX_AGE;
1440
+ exports.STATE_TTL_MS = STATE_TTL_MS;
1441
+ exports.SUPPORTED_PROVIDERS = SUPPORTED_PROVIDERS;
1442
+ exports.SessionDecryptionError = SessionDecryptionError;
1443
+ exports.TokenVerificationError = TokenVerificationError;
1444
+ exports.checkFeature = checkFeature;
1445
+ exports.createAuthGateClient = createAuthGateClient;
1446
+ exports.defineConfig = defineConfig;
1447
+ exports.hasAllPermissions = hasAllPermissions;
1448
+ exports.hasAnyPermission = hasAnyPermission;
1449
+ exports.hasFeature = hasFeature;
1450
+ exports.hasMergedPermission = hasMergedPermission;
1451
+ exports.hasPermission = hasPermission;
1452
+ exports.hasPlan = hasPlan;
1453
+ exports.hasRole = hasRole;
1454
+ exports.mergePermissions = mergePermissions;