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