@auth-gate/core 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1387 -1275
- package/dist/index.d.cts +662 -632
- package/dist/index.d.ts +662 -632
- package/dist/index.mjs +1364 -1255
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,1322 +1,1431 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
37
|
+
return encoder.encode(str);
|
|
50
38
|
}
|
|
39
|
+
/** Decode a `Uint8Array` to a UTF-8 string. */
|
|
51
40
|
function decodeUtf8(bytes) {
|
|
52
|
-
|
|
41
|
+
return decoder.decode(bytes);
|
|
53
42
|
}
|
|
43
|
+
/** Concatenate multiple `Uint8Array` buffers into a single buffer. */
|
|
54
44
|
function concat(...arrays) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
return hkdfDerive(secret, "authgate-state-signing", 32);
|
|
97
121
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
377
|
+
constructor(message) {
|
|
378
|
+
super(message);
|
|
379
|
+
this.name = "ConfigurationError";
|
|
380
|
+
}
|
|
270
381
|
};
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1245
|
+
return new AuthGateClient(config);
|
|
1217
1246
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1303
|
+
return config;
|
|
1222
1304
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
-
|
|
1296
|
-
return matchesPermission(merged, permission);
|
|
1428
|
+
return matchesPermission(mergePermissions(projectRole, orgMembership), permission);
|
|
1297
1429
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
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 };
|