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