@usebetterdev/console 0.3.0-beta.2
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/drizzle.cjs +220 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +343 -0
- package/dist/drizzle.d.ts +343 -0
- package/dist/drizzle.js +189 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/hono.cjs +101 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +43 -0
- package/dist/hono.d.ts +43 -0
- package/dist/hono.js +74 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +1155 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +162 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +1106 -0
- package/dist/index.js.map +1 -0
- package/dist/types-Bm35VBpM.d.cts +169 -0
- package/dist/types-Bm35VBpM.d.ts +169 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ConsoleError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ConsoleError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var ConsoleSessionExpiredError = class extends ConsoleError {
|
|
11
|
+
constructor(message = "Session has expired") {
|
|
12
|
+
super(message, "SESSION_EXPIRED");
|
|
13
|
+
this.name = "ConsoleSessionExpiredError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var ConsoleEmailNotAllowedError = class extends ConsoleError {
|
|
17
|
+
constructor(email) {
|
|
18
|
+
super(
|
|
19
|
+
`Email "${email}" is not in the allowed list. Update BETTER_CONSOLE_ALLOWED_EMAILS to grant access.`,
|
|
20
|
+
"EMAIL_NOT_ALLOWED"
|
|
21
|
+
);
|
|
22
|
+
this.name = "ConsoleEmailNotAllowedError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var ConsoleMagicLinkExpiredError = class extends ConsoleError {
|
|
26
|
+
constructor(message = "Magic link has expired or was already used") {
|
|
27
|
+
super(message, "MAGIC_LINK_EXPIRED");
|
|
28
|
+
this.name = "ConsoleMagicLinkExpiredError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ConsoleInvalidTokenError = class extends ConsoleError {
|
|
32
|
+
constructor(message = "Invalid or expired session token") {
|
|
33
|
+
super(message, "INVALID_TOKEN");
|
|
34
|
+
this.name = "ConsoleInvalidTokenError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ConsoleAdapterRequiredError = class extends ConsoleError {
|
|
38
|
+
constructor(message = "Magic link sessions require a database adapter. Either provide an adapter or use autoApprove for development.") {
|
|
39
|
+
super(message, "ADAPTER_REQUIRED");
|
|
40
|
+
this.name = "ConsoleAdapterRequiredError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var ConsoleWeakSecretError = class extends ConsoleError {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(
|
|
46
|
+
"connectionTokenHash secret is too short (minimum 32 characters in production). Generate a strong secret with: npx better-console init",
|
|
47
|
+
"WEAK_SECRET"
|
|
48
|
+
);
|
|
49
|
+
this.name = "ConsoleWeakSecretError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ConsoleEmailRelayError = class extends ConsoleError {
|
|
53
|
+
constructor(message = "Failed to deliver verification code via Console email relay") {
|
|
54
|
+
super(message, "EMAIL_RELAY_FAILED");
|
|
55
|
+
this.name = "ConsoleEmailRelayError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ConsoleAutoApproveInProductionError = class extends ConsoleError {
|
|
59
|
+
constructor() {
|
|
60
|
+
super(
|
|
61
|
+
'autoApprove is enabled outside of development. This is a security risk. Set NODE_ENV="development" or disable autoApprove.',
|
|
62
|
+
"AUTO_APPROVE_IN_PRODUCTION"
|
|
63
|
+
);
|
|
64
|
+
this.name = "ConsoleAutoApproveInProductionError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/token.ts
|
|
69
|
+
var ALPHANUMERIC = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
70
|
+
var ALPHABET_LIMIT = 240;
|
|
71
|
+
function bytesToHex(bytes) {
|
|
72
|
+
let hex = "";
|
|
73
|
+
for (const b of bytes) {
|
|
74
|
+
hex += b.toString(16).padStart(2, "0");
|
|
75
|
+
}
|
|
76
|
+
return hex;
|
|
77
|
+
}
|
|
78
|
+
function bytesToBase64url(bytes) {
|
|
79
|
+
let binary = "";
|
|
80
|
+
for (const b of bytes) {
|
|
81
|
+
binary += String.fromCharCode(b);
|
|
82
|
+
}
|
|
83
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
84
|
+
}
|
|
85
|
+
function generateSessionToken() {
|
|
86
|
+
const bytes = new Uint8Array(32);
|
|
87
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
88
|
+
return bytesToBase64url(bytes);
|
|
89
|
+
}
|
|
90
|
+
async function hashToken(token) {
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const data = encoder.encode(token);
|
|
93
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
94
|
+
return bytesToHex(new Uint8Array(hashBuffer));
|
|
95
|
+
}
|
|
96
|
+
async function verifyTokenHash(token, hash) {
|
|
97
|
+
const computed = await hashToken(token);
|
|
98
|
+
if (computed.length !== hash.length) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let result = 0;
|
|
102
|
+
for (let i = 0; i < computed.length; i++) {
|
|
103
|
+
result |= computed.charCodeAt(i) ^ hash.charCodeAt(i);
|
|
104
|
+
}
|
|
105
|
+
return result === 0;
|
|
106
|
+
}
|
|
107
|
+
function generateMagicLinkCode() {
|
|
108
|
+
const codeLength = 6;
|
|
109
|
+
let code = "";
|
|
110
|
+
const buffer = new Uint8Array(12);
|
|
111
|
+
globalThis.crypto.getRandomValues(buffer);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (code.length < codeLength) {
|
|
114
|
+
if (offset >= buffer.length) {
|
|
115
|
+
globalThis.crypto.getRandomValues(buffer);
|
|
116
|
+
offset = 0;
|
|
117
|
+
}
|
|
118
|
+
const b = buffer[offset];
|
|
119
|
+
offset++;
|
|
120
|
+
if (b < ALPHABET_LIMIT) {
|
|
121
|
+
code += ALPHANUMERIC[b % ALPHANUMERIC.length];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return code;
|
|
125
|
+
}
|
|
126
|
+
async function hashMagicLinkCode(code) {
|
|
127
|
+
return hashToken(code);
|
|
128
|
+
}
|
|
129
|
+
function parseConnectionTokenHash(envValue) {
|
|
130
|
+
if (envValue.startsWith("sha256:")) {
|
|
131
|
+
return envValue.slice(7);
|
|
132
|
+
}
|
|
133
|
+
return envValue;
|
|
134
|
+
}
|
|
135
|
+
function parseDuration(duration) {
|
|
136
|
+
const match = /^(\d+)(ms|s|m|h|d)$/.exec(duration.trim());
|
|
137
|
+
if (!match) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const value = Number(match[1]);
|
|
141
|
+
const unit = match[2];
|
|
142
|
+
switch (unit) {
|
|
143
|
+
case "ms":
|
|
144
|
+
return value;
|
|
145
|
+
case "s":
|
|
146
|
+
return value * 1e3;
|
|
147
|
+
case "m":
|
|
148
|
+
return value * 60 * 1e3;
|
|
149
|
+
case "h":
|
|
150
|
+
return value * 60 * 60 * 1e3;
|
|
151
|
+
case "d":
|
|
152
|
+
return value * 24 * 60 * 60 * 1e3;
|
|
153
|
+
default:
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/config-validation.ts
|
|
159
|
+
var MIN_TOKEN_LIFETIME_MS = 60 * 60 * 1e3;
|
|
160
|
+
var MAX_TOKEN_LIFETIME_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
161
|
+
var DEFAULT_TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1e3;
|
|
162
|
+
function parseAllowedEmails(input) {
|
|
163
|
+
if (!input) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const raw = typeof input === "string" ? input.split(",") : input;
|
|
167
|
+
return raw.map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0);
|
|
168
|
+
}
|
|
169
|
+
function validateConfig(config) {
|
|
170
|
+
if (!config.connectionTokenHash || config.connectionTokenHash.trim().length === 0) {
|
|
171
|
+
throw new ConsoleError(
|
|
172
|
+
"connectionTokenHash is required. Run: npx better-console init",
|
|
173
|
+
"MISSING_CONNECTION_TOKEN"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const connectionTokenSecret = parseConnectionTokenHash(
|
|
177
|
+
config.connectionTokenHash
|
|
178
|
+
);
|
|
179
|
+
if (connectionTokenSecret.length === 0) {
|
|
180
|
+
throw new ConsoleError(
|
|
181
|
+
"connectionTokenHash is empty after parsing. Check your BETTER_CONSOLE_TOKEN_HASH env var.",
|
|
182
|
+
"EMPTY_CONNECTION_TOKEN"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const nodeEnv = (typeof process !== "undefined" ? process.env?.NODE_ENV : void 0) ?? "";
|
|
186
|
+
if (nodeEnv !== "development" && nodeEnv !== "test" && connectionTokenSecret.length < 32) {
|
|
187
|
+
throw new ConsoleWeakSecretError();
|
|
188
|
+
}
|
|
189
|
+
if (config.sessions.autoApprove) {
|
|
190
|
+
if (nodeEnv !== "development" && nodeEnv !== "test") {
|
|
191
|
+
throw new ConsoleAutoApproveInProductionError();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (config.sessions.magicLink && !config.adapter) {
|
|
195
|
+
throw new ConsoleAdapterRequiredError();
|
|
196
|
+
}
|
|
197
|
+
if (!config.sessions.autoApprove && !config.sessions.magicLink && !config.sessions.authenticate) {
|
|
198
|
+
throw new ConsoleError(
|
|
199
|
+
"No session method configured. Enable autoApprove, magicLink, or authenticate.",
|
|
200
|
+
"NO_SESSION_METHOD"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
let tokenLifetimeMs = DEFAULT_TOKEN_LIFETIME_MS;
|
|
204
|
+
if (config.sessions.tokenLifetime) {
|
|
205
|
+
const parsed = parseDuration(config.sessions.tokenLifetime);
|
|
206
|
+
if (parsed === null) {
|
|
207
|
+
throw new ConsoleError(
|
|
208
|
+
`Invalid tokenLifetime "${config.sessions.tokenLifetime}". Use format: "24h", "8h", "1h", "30m", "7d".`,
|
|
209
|
+
"INVALID_TOKEN_LIFETIME"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (parsed < MIN_TOKEN_LIFETIME_MS || parsed > MAX_TOKEN_LIFETIME_MS) {
|
|
213
|
+
throw new ConsoleError(
|
|
214
|
+
`tokenLifetime must be between 1h and 7d. Got: "${config.sessions.tokenLifetime}".`,
|
|
215
|
+
"TOKEN_LIFETIME_OUT_OF_RANGE"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
tokenLifetimeMs = parsed;
|
|
219
|
+
}
|
|
220
|
+
const allowedOrigins = config.allowedOrigins && config.allowedOrigins.length > 0 ? config.allowedOrigins : ["https://console.usebetter.dev"];
|
|
221
|
+
const allowedActions = config.allowedActions && config.allowedActions.length > 0 ? config.allowedActions : ["read", "write", "admin"];
|
|
222
|
+
const allowedEmails = parseAllowedEmails(
|
|
223
|
+
config.sessions.magicLink?.allowedEmails
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
connectionTokenSecret,
|
|
227
|
+
tokenLifetimeMs,
|
|
228
|
+
allowedOrigins,
|
|
229
|
+
allowedActions,
|
|
230
|
+
allowedEmails
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/jwt.ts
|
|
235
|
+
function base64urlEncode(data) {
|
|
236
|
+
return btoa(data).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
237
|
+
}
|
|
238
|
+
function base64urlDecode(encoded) {
|
|
239
|
+
const padded = encoded + "=".repeat((4 - encoded.length % 4) % 4);
|
|
240
|
+
return atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
241
|
+
}
|
|
242
|
+
async function hmacSign(data, secret) {
|
|
243
|
+
const encoder = new TextEncoder();
|
|
244
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
245
|
+
"raw",
|
|
246
|
+
encoder.encode(secret),
|
|
247
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
248
|
+
false,
|
|
249
|
+
["sign"]
|
|
250
|
+
);
|
|
251
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
252
|
+
"HMAC",
|
|
253
|
+
key,
|
|
254
|
+
encoder.encode(data)
|
|
255
|
+
);
|
|
256
|
+
const bytes = new Uint8Array(signature);
|
|
257
|
+
let binary = "";
|
|
258
|
+
for (const b of bytes) {
|
|
259
|
+
binary += String.fromCharCode(b);
|
|
260
|
+
}
|
|
261
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
262
|
+
}
|
|
263
|
+
async function hmacVerify(data, signature, secret) {
|
|
264
|
+
const encoder = new TextEncoder();
|
|
265
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
266
|
+
"raw",
|
|
267
|
+
encoder.encode(secret),
|
|
268
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
269
|
+
false,
|
|
270
|
+
["verify"]
|
|
271
|
+
);
|
|
272
|
+
const sigPadded = signature + "=".repeat((4 - signature.length % 4) % 4);
|
|
273
|
+
const sigBytes = Uint8Array.from(
|
|
274
|
+
atob(sigPadded.replace(/-/g, "+").replace(/_/g, "/")),
|
|
275
|
+
(c) => c.charCodeAt(0)
|
|
276
|
+
);
|
|
277
|
+
return globalThis.crypto.subtle.verify(
|
|
278
|
+
"HMAC",
|
|
279
|
+
key,
|
|
280
|
+
sigBytes,
|
|
281
|
+
encoder.encode(data)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
async function signSessionJwt(payload, secret) {
|
|
285
|
+
const header = base64urlEncode(
|
|
286
|
+
JSON.stringify({ alg: "HS256", typ: "JWT" })
|
|
287
|
+
);
|
|
288
|
+
const body = base64urlEncode(JSON.stringify(payload));
|
|
289
|
+
const signingInput = `${header}.${body}`;
|
|
290
|
+
const signature = await hmacSign(signingInput, secret);
|
|
291
|
+
return `${signingInput}.${signature}`;
|
|
292
|
+
}
|
|
293
|
+
async function verifySessionJwt(token, secret) {
|
|
294
|
+
const parts = token.split(".");
|
|
295
|
+
if (parts.length !== 3) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const [header, body, signature] = parts;
|
|
299
|
+
const signingInput = `${header}.${body}`;
|
|
300
|
+
let valid;
|
|
301
|
+
try {
|
|
302
|
+
valid = await hmacVerify(signingInput, signature, secret);
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (!valid) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
let payload;
|
|
310
|
+
try {
|
|
311
|
+
payload = JSON.parse(base64urlDecode(body));
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (typeof payload !== "object" || payload === null || !("sessionId" in payload) || !("email" in payload) || !("permissions" in payload) || !("expiresAt" in payload)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const sessionToken = payload;
|
|
319
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
320
|
+
if (sessionToken.expiresAt <= nowSeconds) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return sessionToken;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/permissions.ts
|
|
327
|
+
function resolveHighestPermission(allowedActions) {
|
|
328
|
+
if (allowedActions.includes("admin")) {
|
|
329
|
+
return "admin";
|
|
330
|
+
}
|
|
331
|
+
if (allowedActions.includes("write")) {
|
|
332
|
+
return "write";
|
|
333
|
+
}
|
|
334
|
+
return "read";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/sessions/auto-approve.ts
|
|
338
|
+
async function createAutoApproveSession(email, validated) {
|
|
339
|
+
const expiresInSeconds = Math.floor(validated.tokenLifetimeMs / 1e3);
|
|
340
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + expiresInSeconds;
|
|
341
|
+
const highestPermission = resolveHighestPermission(validated.allowedActions);
|
|
342
|
+
const payload = {
|
|
343
|
+
sessionId: globalThis.crypto.randomUUID(),
|
|
344
|
+
email,
|
|
345
|
+
permissions: highestPermission,
|
|
346
|
+
expiresAt
|
|
347
|
+
};
|
|
348
|
+
const sessionToken = await signSessionJwt(
|
|
349
|
+
payload,
|
|
350
|
+
validated.connectionTokenSecret
|
|
351
|
+
);
|
|
352
|
+
return { sessionToken, expiresIn: expiresInSeconds };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/sessions/email-relay.ts
|
|
356
|
+
var RELAY_URL = "https://api.usebetter.dev/api/email/send-magic-link";
|
|
357
|
+
async function sendMagicLinkViaRelay(data) {
|
|
358
|
+
const normalizedBase = data.baseUrl.replace(/\/+$/, "");
|
|
359
|
+
const magicLinkUrl = `${normalizedBase}/.well-known/better/console/session/verify?code=${encodeURIComponent(data.code)}&session_id=${encodeURIComponent(data.sessionId)}`;
|
|
360
|
+
const envName = process.env.NODE_ENV ?? "production";
|
|
361
|
+
const response = await fetch(RELAY_URL, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: { "Content-Type": "application/json" },
|
|
364
|
+
body: JSON.stringify({
|
|
365
|
+
to: data.email,
|
|
366
|
+
magicLinkUrl,
|
|
367
|
+
appName: data.appName,
|
|
368
|
+
envName
|
|
369
|
+
})
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
const body = await response.text().catch(() => "");
|
|
373
|
+
throw new ConsoleEmailRelayError(
|
|
374
|
+
`Console email relay returned ${String(response.status)}: ${body}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/sessions/magic-link.ts
|
|
380
|
+
var MAGIC_LINK_EXPIRY_MS = 10 * 60 * 1e3;
|
|
381
|
+
async function initMagicLinkSession(email, validated, adapter, magicLinkConfig, relayContext) {
|
|
382
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
383
|
+
if (!validated.allowedEmails.includes(normalizedEmail)) {
|
|
384
|
+
throw new ConsoleEmailNotAllowedError(normalizedEmail);
|
|
385
|
+
}
|
|
386
|
+
const code = generateMagicLinkCode();
|
|
387
|
+
const codeHash = await hashMagicLinkCode(code);
|
|
388
|
+
const sessionId = globalThis.crypto.randomUUID();
|
|
389
|
+
const expiresAt = new Date(Date.now() + MAGIC_LINK_EXPIRY_MS);
|
|
390
|
+
await adapter.magicLinks.createMagicLink({
|
|
391
|
+
email: normalizedEmail,
|
|
392
|
+
codeHash,
|
|
393
|
+
sessionId,
|
|
394
|
+
expiresAt
|
|
395
|
+
});
|
|
396
|
+
if (magicLinkConfig.sendMagicLinkEmail) {
|
|
397
|
+
await magicLinkConfig.sendMagicLinkEmail({
|
|
398
|
+
email: normalizedEmail,
|
|
399
|
+
sessionId,
|
|
400
|
+
code
|
|
401
|
+
});
|
|
402
|
+
} else if (relayContext) {
|
|
403
|
+
await sendMagicLinkViaRelay({
|
|
404
|
+
email: normalizedEmail,
|
|
405
|
+
code,
|
|
406
|
+
sessionId,
|
|
407
|
+
appName: relayContext.appName,
|
|
408
|
+
baseUrl: relayContext.baseUrl
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return { sessionId };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/sessions/verify.ts
|
|
415
|
+
async function verifySession(token, validated, adapter) {
|
|
416
|
+
const jwtPayload = await verifySessionJwt(
|
|
417
|
+
token,
|
|
418
|
+
validated.connectionTokenSecret
|
|
419
|
+
);
|
|
420
|
+
if (jwtPayload) {
|
|
421
|
+
return {
|
|
422
|
+
id: jwtPayload.sessionId,
|
|
423
|
+
email: jwtPayload.email,
|
|
424
|
+
permissions: jwtPayload.permissions,
|
|
425
|
+
expiresAt: new Date(jwtPayload.expiresAt * 1e3),
|
|
426
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (adapter) {
|
|
430
|
+
const tokenHash = await hashToken(token);
|
|
431
|
+
const session = await adapter.sessions.getSessionByTokenHash(tokenHash);
|
|
432
|
+
if (!session) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
if (session.expiresAt.getTime() <= Date.now()) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return session;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/router.ts
|
|
444
|
+
function matchPath(pattern, actual) {
|
|
445
|
+
const patternParts = pattern.split("/").filter((p) => p.length > 0);
|
|
446
|
+
const actualParts = actual.split("/").filter((p) => p.length > 0);
|
|
447
|
+
if (patternParts.length !== actualParts.length) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const params = {};
|
|
451
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
452
|
+
const pp = patternParts[i];
|
|
453
|
+
const ap = actualParts[i];
|
|
454
|
+
if (pp.startsWith(":")) {
|
|
455
|
+
params[pp.slice(1)] = ap;
|
|
456
|
+
} else if (pp !== ap) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return params;
|
|
461
|
+
}
|
|
462
|
+
var ConsoleRouter = class {
|
|
463
|
+
routes = [];
|
|
464
|
+
/**
|
|
465
|
+
* Register a built-in console route (e.g. session/init, health).
|
|
466
|
+
*/
|
|
467
|
+
addConsoleRoute(method, path, handler, options = {}) {
|
|
468
|
+
this.routes.push({
|
|
469
|
+
method: method.toUpperCase(),
|
|
470
|
+
pattern: `/console${path}`,
|
|
471
|
+
route: {
|
|
472
|
+
handler,
|
|
473
|
+
requiresAuth: options.requiresAuth ?? false,
|
|
474
|
+
requiredPermission: options.requiredPermission ?? "read"
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Register all endpoints from a product.
|
|
480
|
+
*/
|
|
481
|
+
registerProduct(product) {
|
|
482
|
+
for (const endpoint of product.endpoints) {
|
|
483
|
+
const handler = endpoint.handler;
|
|
484
|
+
this.routes.push({
|
|
485
|
+
method: endpoint.method.toUpperCase(),
|
|
486
|
+
pattern: `/${product.id}${endpoint.path}`,
|
|
487
|
+
route: {
|
|
488
|
+
handler,
|
|
489
|
+
requiresAuth: true,
|
|
490
|
+
requiredPermission: endpoint.requiredPermission ?? "read"
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Match an incoming request method + path against registered routes.
|
|
497
|
+
* For OPTIONS, matches any registered path regardless of method.
|
|
498
|
+
*/
|
|
499
|
+
match(method, path) {
|
|
500
|
+
const upperMethod = method.toUpperCase();
|
|
501
|
+
if (upperMethod === "OPTIONS") {
|
|
502
|
+
for (const entry of this.routes) {
|
|
503
|
+
const params = matchPath(entry.pattern, path);
|
|
504
|
+
if (params) {
|
|
505
|
+
return {
|
|
506
|
+
handler: entry.route.handler,
|
|
507
|
+
params,
|
|
508
|
+
requiresAuth: false,
|
|
509
|
+
requiredPermission: "read"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
for (const entry of this.routes) {
|
|
516
|
+
if (entry.method !== upperMethod) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const params = matchPath(entry.pattern, path);
|
|
520
|
+
if (params) {
|
|
521
|
+
return {
|
|
522
|
+
handler: entry.route.handler,
|
|
523
|
+
params,
|
|
524
|
+
requiresAuth: entry.route.requiresAuth,
|
|
525
|
+
requiredPermission: entry.route.requiredPermission
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function hasPermission(sessionPermission, required) {
|
|
533
|
+
const levels = {
|
|
534
|
+
read: 1,
|
|
535
|
+
write: 2,
|
|
536
|
+
admin: 3
|
|
537
|
+
};
|
|
538
|
+
return levels[sessionPermission] >= levels[required];
|
|
539
|
+
}
|
|
540
|
+
function extractBearerToken(headers) {
|
|
541
|
+
const auth = headers.get("authorization");
|
|
542
|
+
if (!auth) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const parts = auth.split(" ");
|
|
546
|
+
if (parts.length !== 2 || parts[0]?.toLowerCase() !== "bearer") {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
return parts[1] ?? null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/cors.ts
|
|
553
|
+
var ALLOWED_HEADERS = "Authorization, Content-Type";
|
|
554
|
+
var ALLOWED_METHODS = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
|
|
555
|
+
var MAX_AGE = "86400";
|
|
556
|
+
function isLocalhost(origin) {
|
|
557
|
+
try {
|
|
558
|
+
const url = new URL(origin);
|
|
559
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
560
|
+
} catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function isDevEnvironment() {
|
|
565
|
+
const nodeEnv = (typeof process !== "undefined" ? process.env?.NODE_ENV : void 0) ?? "";
|
|
566
|
+
return nodeEnv === "development" || nodeEnv === "test";
|
|
567
|
+
}
|
|
568
|
+
function isOriginAllowed(origin, config) {
|
|
569
|
+
if (config.allowedOrigins.includes(origin)) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
if (isDevEnvironment() && isLocalhost(origin)) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
function getCorsHeaders(origin, config) {
|
|
578
|
+
if (!origin) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
if (!isOriginAllowed(origin, config)) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
"Access-Control-Allow-Origin": origin,
|
|
586
|
+
"Access-Control-Allow-Headers": ALLOWED_HEADERS,
|
|
587
|
+
"Access-Control-Allow-Methods": ALLOWED_METHODS,
|
|
588
|
+
"Vary": "Origin"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function getPreflightCorsHeaders(origin, config) {
|
|
592
|
+
const headers = getCorsHeaders(origin, config);
|
|
593
|
+
if (!headers) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
...headers,
|
|
598
|
+
"Access-Control-Max-Age": MAX_AGE
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/handlers/health.ts
|
|
603
|
+
async function handleHealth(_request) {
|
|
604
|
+
return {
|
|
605
|
+
status: 200,
|
|
606
|
+
body: { status: "ok" }
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/handlers/capabilities.ts
|
|
611
|
+
function createCapabilitiesHandler(instance) {
|
|
612
|
+
return async (_request) => {
|
|
613
|
+
return {
|
|
614
|
+
status: 200,
|
|
615
|
+
body: instance.getCapabilities()
|
|
616
|
+
};
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/handlers/session-init.ts
|
|
621
|
+
function createSessionInitHandler(instance) {
|
|
622
|
+
return async (request) => {
|
|
623
|
+
try {
|
|
624
|
+
const result = await instance.initSession(request);
|
|
625
|
+
return { status: 200, body: result };
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error instanceof ConsoleError) {
|
|
628
|
+
return {
|
|
629
|
+
status: 400,
|
|
630
|
+
body: { error: error.message, code: error.code }
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/input-validation.ts
|
|
639
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
640
|
+
var MAGIC_CODE_RE = /^[A-HJ-NP-Z2-9]{6}$/;
|
|
641
|
+
function isValidUuid(value) {
|
|
642
|
+
return UUID_RE.test(value);
|
|
643
|
+
}
|
|
644
|
+
function isValidMagicLinkCode(value) {
|
|
645
|
+
return MAGIC_CODE_RE.test(value);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/handlers/session-verify.ts
|
|
649
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
650
|
+
async function verifyMagicLink(code, sessionId, adapter, maxAttempts) {
|
|
651
|
+
if (!code || !sessionId) {
|
|
652
|
+
return { ok: false, status: 400, error: "Missing code or session_id" };
|
|
653
|
+
}
|
|
654
|
+
if (typeof code !== "string" || !isValidMagicLinkCode(code)) {
|
|
655
|
+
return { ok: false, status: 400, error: "Invalid code format" };
|
|
656
|
+
}
|
|
657
|
+
if (typeof sessionId !== "string" || !isValidUuid(sessionId)) {
|
|
658
|
+
return { ok: false, status: 400, error: "Invalid session_id format" };
|
|
659
|
+
}
|
|
660
|
+
const magicLink = await adapter.magicLinks.getMagicLinkBySessionId(sessionId);
|
|
661
|
+
if (!magicLink) {
|
|
662
|
+
return { ok: false, status: 404, error: "Magic link not found" };
|
|
663
|
+
}
|
|
664
|
+
if (magicLink.usedAt !== null) {
|
|
665
|
+
return { ok: false, status: 410, error: "Magic link already used" };
|
|
666
|
+
}
|
|
667
|
+
if (magicLink.expiresAt.getTime() <= Date.now()) {
|
|
668
|
+
return { ok: false, status: 410, error: "Magic link expired" };
|
|
669
|
+
}
|
|
670
|
+
if (magicLink.failedAttempts >= maxAttempts) {
|
|
671
|
+
return { ok: false, status: 429, error: "Too many failed attempts" };
|
|
672
|
+
}
|
|
673
|
+
const providedCodeHash = await hashMagicLinkCode(code);
|
|
674
|
+
if (providedCodeHash !== magicLink.codeHash) {
|
|
675
|
+
await adapter.magicLinks.incrementFailedAttempts(magicLink.id);
|
|
676
|
+
return { ok: false, status: 401, error: "Invalid code" };
|
|
677
|
+
}
|
|
678
|
+
await adapter.magicLinks.markMagicLinkUsed(magicLink.id);
|
|
679
|
+
return { ok: true };
|
|
680
|
+
}
|
|
681
|
+
function createSessionVerifyHandler(adapter, options = {}) {
|
|
682
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
683
|
+
return async (request) => {
|
|
684
|
+
const body = request.body;
|
|
685
|
+
const result = await verifyMagicLink(
|
|
686
|
+
body?.code,
|
|
687
|
+
body?.session_id,
|
|
688
|
+
adapter,
|
|
689
|
+
maxAttempts
|
|
690
|
+
);
|
|
691
|
+
if (!result.ok) {
|
|
692
|
+
return {
|
|
693
|
+
status: result.status,
|
|
694
|
+
body: { error: result.error }
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
status: 200,
|
|
699
|
+
body: { status: "verified" }
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function htmlPage(title, message) {
|
|
704
|
+
return `<!DOCTYPE html>
|
|
705
|
+
<html lang="en">
|
|
706
|
+
<head>
|
|
707
|
+
<meta charset="utf-8">
|
|
708
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
709
|
+
<title>${title}</title>
|
|
710
|
+
<style>
|
|
711
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #fafafa; }
|
|
712
|
+
.card { text-align: center; padding: 2rem; max-width: 400px; }
|
|
713
|
+
h1 { font-size: 1.25rem; margin-bottom: 0.5rem; }
|
|
714
|
+
p { color: #555; }
|
|
715
|
+
</style>
|
|
716
|
+
</head>
|
|
717
|
+
<body>
|
|
718
|
+
<div class="card">
|
|
719
|
+
<h1>${title}</h1>
|
|
720
|
+
<p>${message}</p>
|
|
721
|
+
</div>
|
|
722
|
+
</body>
|
|
723
|
+
</html>`;
|
|
724
|
+
}
|
|
725
|
+
function createSessionVerifyGetHandler(adapter, options = {}) {
|
|
726
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
727
|
+
return async (request) => {
|
|
728
|
+
const code = request.query.code;
|
|
729
|
+
const sessionId = request.query.session_id;
|
|
730
|
+
const result = await verifyMagicLink(
|
|
731
|
+
code,
|
|
732
|
+
sessionId,
|
|
733
|
+
adapter,
|
|
734
|
+
maxAttempts
|
|
735
|
+
);
|
|
736
|
+
if (!result.ok) {
|
|
737
|
+
return {
|
|
738
|
+
status: result.status,
|
|
739
|
+
body: htmlPage("Verification failed", result.error),
|
|
740
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
status: 200,
|
|
745
|
+
body: htmlPage(
|
|
746
|
+
"Verification successful",
|
|
747
|
+
"You can close this tab and return to the Console."
|
|
748
|
+
),
|
|
749
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/handlers/session-poll.ts
|
|
755
|
+
function createSessionPollHandler(adapter) {
|
|
756
|
+
return async (request) => {
|
|
757
|
+
const sessionId = request.query.session_id;
|
|
758
|
+
if (!sessionId) {
|
|
759
|
+
return {
|
|
760
|
+
status: 400,
|
|
761
|
+
body: { error: "Missing session_id" }
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (!isValidUuid(sessionId)) {
|
|
765
|
+
return {
|
|
766
|
+
status: 400,
|
|
767
|
+
body: { error: "Invalid session_id format" }
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const magicLink = await adapter.magicLinks.getMagicLinkBySessionId(sessionId);
|
|
771
|
+
if (!magicLink) {
|
|
772
|
+
return {
|
|
773
|
+
status: 404,
|
|
774
|
+
body: { error: "Magic link not found" }
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (magicLink.usedAt === null && magicLink.expiresAt.getTime() <= Date.now()) {
|
|
778
|
+
return {
|
|
779
|
+
status: 200,
|
|
780
|
+
body: { status: "expired" }
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (magicLink.usedAt === null) {
|
|
784
|
+
return {
|
|
785
|
+
status: 200,
|
|
786
|
+
body: { status: "pending" }
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
status: 200,
|
|
791
|
+
body: { status: "verified" }
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/handlers/session-claim.ts
|
|
797
|
+
var CLAIM_GRACE_MS = 60 * 1e3;
|
|
798
|
+
function createSessionClaimHandler(validated, adapter) {
|
|
799
|
+
return async (request) => {
|
|
800
|
+
const body = request.body;
|
|
801
|
+
const sessionId = body?.session_id;
|
|
802
|
+
if (!sessionId) {
|
|
803
|
+
return {
|
|
804
|
+
status: 400,
|
|
805
|
+
body: { error: "Missing session_id" }
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
if (typeof sessionId !== "string" || !isValidUuid(sessionId)) {
|
|
809
|
+
return {
|
|
810
|
+
status: 400,
|
|
811
|
+
body: { error: "Invalid session_id format" }
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
const magicLink = await adapter.magicLinks.getMagicLinkBySessionId(sessionId);
|
|
815
|
+
if (!magicLink) {
|
|
816
|
+
return {
|
|
817
|
+
status: 404,
|
|
818
|
+
body: { error: "Magic link not found" }
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (magicLink.usedAt === null) {
|
|
822
|
+
return {
|
|
823
|
+
status: 409,
|
|
824
|
+
body: { error: "Magic link not yet verified" }
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
if (magicLink.expiresAt.getTime() + CLAIM_GRACE_MS <= Date.now()) {
|
|
828
|
+
return {
|
|
829
|
+
status: 410,
|
|
830
|
+
body: { error: "Magic link expired" }
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
if (magicLink.tokenHash !== null) {
|
|
834
|
+
return {
|
|
835
|
+
status: 200,
|
|
836
|
+
body: { status: "already_claimed" }
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const sessionToken = generateSessionToken();
|
|
840
|
+
const tokenHash = await hashToken(sessionToken);
|
|
841
|
+
const claimed = await adapter.magicLinks.setMagicLinkTokenHash(
|
|
842
|
+
magicLink.id,
|
|
843
|
+
tokenHash
|
|
844
|
+
);
|
|
845
|
+
if (!claimed) {
|
|
846
|
+
return {
|
|
847
|
+
status: 200,
|
|
848
|
+
body: { status: "already_claimed" }
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const expiresInSeconds = Math.floor(validated.tokenLifetimeMs / 1e3);
|
|
852
|
+
const expiresAt = new Date(Date.now() + validated.tokenLifetimeMs);
|
|
853
|
+
const highestPermission = resolveHighestPermission(validated.allowedActions);
|
|
854
|
+
try {
|
|
855
|
+
await adapter.sessions.createSession({
|
|
856
|
+
email: magicLink.email,
|
|
857
|
+
tokenHash,
|
|
858
|
+
permissions: highestPermission,
|
|
859
|
+
expiresAt
|
|
860
|
+
});
|
|
861
|
+
} catch (error) {
|
|
862
|
+
await adapter.magicLinks.clearMagicLinkTokenHash(magicLink.id);
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
status: 200,
|
|
867
|
+
body: {
|
|
868
|
+
status: "claimed",
|
|
869
|
+
sessionToken,
|
|
870
|
+
expiresIn: expiresInSeconds
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/better-console.ts
|
|
877
|
+
function addCorsHeaders(response, origin, allowedOrigins) {
|
|
878
|
+
const corsHeaders = getCorsHeaders(origin, { allowedOrigins });
|
|
879
|
+
if (!corsHeaders) {
|
|
880
|
+
return response;
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
...response,
|
|
884
|
+
headers: { ...response.headers, ...corsHeaders }
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function betterConsole(config) {
|
|
888
|
+
const validated = validateConfig(config);
|
|
889
|
+
const products = [];
|
|
890
|
+
const router = new ConsoleRouter();
|
|
891
|
+
const instance = {
|
|
892
|
+
async initSession(request) {
|
|
893
|
+
if (config.sessions.autoApprove) {
|
|
894
|
+
const body = request.body;
|
|
895
|
+
const email = body?.email ?? "dev@localhost";
|
|
896
|
+
const result = await createAutoApproveSession(email, validated);
|
|
897
|
+
return { method: "auto_approve", ...result };
|
|
898
|
+
}
|
|
899
|
+
if (config.sessions.magicLink) {
|
|
900
|
+
if (!config.adapter) {
|
|
901
|
+
throw new ConsoleAdapterRequiredError();
|
|
902
|
+
}
|
|
903
|
+
const body = request.body;
|
|
904
|
+
const email = body?.email;
|
|
905
|
+
if (!email || typeof email !== "string") {
|
|
906
|
+
throw new ConsoleError(
|
|
907
|
+
"Email is required for magic link sessions",
|
|
908
|
+
"EMAIL_REQUIRED"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
const relayContext = body?.appName && body?.baseUrl ? { appName: body.appName, baseUrl: body.baseUrl } : void 0;
|
|
912
|
+
const result = await initMagicLinkSession(
|
|
913
|
+
email,
|
|
914
|
+
validated,
|
|
915
|
+
config.adapter,
|
|
916
|
+
config.sessions.magicLink,
|
|
917
|
+
relayContext
|
|
918
|
+
);
|
|
919
|
+
return { method: "magic_link", sessionId: result.sessionId };
|
|
920
|
+
}
|
|
921
|
+
throw new ConsoleError(
|
|
922
|
+
"No session method configured",
|
|
923
|
+
"NO_SESSION_METHOD"
|
|
924
|
+
);
|
|
925
|
+
},
|
|
926
|
+
async verifySession(token) {
|
|
927
|
+
return verifySession(token, validated, config.adapter);
|
|
928
|
+
},
|
|
929
|
+
async refreshSession(_token) {
|
|
930
|
+
return null;
|
|
931
|
+
},
|
|
932
|
+
async revokeSession(sessionId) {
|
|
933
|
+
if (config.adapter) {
|
|
934
|
+
await config.adapter.sessions.deleteSession(sessionId);
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
async revokeAllSessions() {
|
|
938
|
+
if (config.adapter) {
|
|
939
|
+
await config.adapter.sessions.deleteAllSessions();
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
registerProduct(product) {
|
|
943
|
+
const existing = products.find((p) => p.id === product.id);
|
|
944
|
+
if (existing) {
|
|
945
|
+
throw new ConsoleError(
|
|
946
|
+
`Product "${product.id}" is already registered`,
|
|
947
|
+
"DUPLICATE_PRODUCT"
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
products.push(product);
|
|
951
|
+
router.registerProduct(product);
|
|
952
|
+
},
|
|
953
|
+
getRegisteredProducts() {
|
|
954
|
+
return [...products];
|
|
955
|
+
},
|
|
956
|
+
async handleConsoleRequest(request) {
|
|
957
|
+
const origin = request.headers.get("origin");
|
|
958
|
+
try {
|
|
959
|
+
const match = router.match(request.method, request.path);
|
|
960
|
+
if (!match) {
|
|
961
|
+
return addCorsHeaders(
|
|
962
|
+
{ status: 404, body: { error: "Not found" } },
|
|
963
|
+
origin,
|
|
964
|
+
validated.allowedOrigins
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (request.method.toUpperCase() === "OPTIONS") {
|
|
968
|
+
const preflightHeaders = getPreflightCorsHeaders(origin, {
|
|
969
|
+
allowedOrigins: validated.allowedOrigins
|
|
970
|
+
}) ?? {};
|
|
971
|
+
return { status: 204, body: null, headers: preflightHeaders };
|
|
972
|
+
}
|
|
973
|
+
if (match.requiresAuth) {
|
|
974
|
+
const token = extractBearerToken(request.headers);
|
|
975
|
+
if (!token) {
|
|
976
|
+
return addCorsHeaders(
|
|
977
|
+
{ status: 401, body: { error: "Missing session token" } },
|
|
978
|
+
origin,
|
|
979
|
+
validated.allowedOrigins
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
const session = await instance.verifySession(token);
|
|
983
|
+
if (!session) {
|
|
984
|
+
return addCorsHeaders(
|
|
985
|
+
{ status: 401, body: { error: "Invalid or expired session" } },
|
|
986
|
+
origin,
|
|
987
|
+
validated.allowedOrigins
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
if (!hasPermission(session.permissions, match.requiredPermission)) {
|
|
991
|
+
return addCorsHeaders(
|
|
992
|
+
{ status: 403, body: { error: "Insufficient permissions" } },
|
|
993
|
+
origin,
|
|
994
|
+
validated.allowedOrigins
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const enrichedRequest2 = {
|
|
998
|
+
...request,
|
|
999
|
+
params: match.params,
|
|
1000
|
+
session
|
|
1001
|
+
};
|
|
1002
|
+
const response2 = await match.handler(enrichedRequest2);
|
|
1003
|
+
return addCorsHeaders(response2, origin, validated.allowedOrigins);
|
|
1004
|
+
}
|
|
1005
|
+
const enrichedRequest = { ...request, params: match.params };
|
|
1006
|
+
const response = await match.handler(enrichedRequest);
|
|
1007
|
+
return addCorsHeaders(response, origin, validated.allowedOrigins);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
if (error instanceof ConsoleError) {
|
|
1010
|
+
return addCorsHeaders(
|
|
1011
|
+
{ status: 400, body: { error: error.message, code: error.code } },
|
|
1012
|
+
origin,
|
|
1013
|
+
validated.allowedOrigins
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (config.onError) {
|
|
1017
|
+
config.onError(error);
|
|
1018
|
+
}
|
|
1019
|
+
return addCorsHeaders(
|
|
1020
|
+
{ status: 500, body: { error: "Internal server error" } },
|
|
1021
|
+
origin,
|
|
1022
|
+
validated.allowedOrigins
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
getCapabilities() {
|
|
1027
|
+
const authMethods = [];
|
|
1028
|
+
if (config.sessions.autoApprove) {
|
|
1029
|
+
authMethods.push("auto_approve");
|
|
1030
|
+
}
|
|
1031
|
+
if (config.sessions.magicLink) {
|
|
1032
|
+
authMethods.push("magic_link");
|
|
1033
|
+
}
|
|
1034
|
+
if (config.sessions.authenticate) {
|
|
1035
|
+
authMethods.push("authenticate");
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
products: products.map((p) => p.id),
|
|
1039
|
+
authMethods,
|
|
1040
|
+
permissions: validated.allowedActions
|
|
1041
|
+
};
|
|
1042
|
+
},
|
|
1043
|
+
config
|
|
1044
|
+
};
|
|
1045
|
+
router.addConsoleRoute("GET", "/health", handleHealth);
|
|
1046
|
+
router.addConsoleRoute(
|
|
1047
|
+
"GET",
|
|
1048
|
+
"/capabilities",
|
|
1049
|
+
createCapabilitiesHandler(instance)
|
|
1050
|
+
);
|
|
1051
|
+
router.addConsoleRoute(
|
|
1052
|
+
"POST",
|
|
1053
|
+
"/session/init",
|
|
1054
|
+
createSessionInitHandler(instance)
|
|
1055
|
+
);
|
|
1056
|
+
if (config.adapter) {
|
|
1057
|
+
const verifyOptions = config.sessions.magicLink?.maxAttempts !== void 0 ? { maxAttempts: config.sessions.magicLink.maxAttempts } : {};
|
|
1058
|
+
router.addConsoleRoute(
|
|
1059
|
+
"POST",
|
|
1060
|
+
"/session/verify",
|
|
1061
|
+
createSessionVerifyHandler(config.adapter, verifyOptions)
|
|
1062
|
+
);
|
|
1063
|
+
router.addConsoleRoute(
|
|
1064
|
+
"GET",
|
|
1065
|
+
"/session/verify",
|
|
1066
|
+
createSessionVerifyGetHandler(config.adapter, verifyOptions)
|
|
1067
|
+
);
|
|
1068
|
+
router.addConsoleRoute(
|
|
1069
|
+
"GET",
|
|
1070
|
+
"/session/poll",
|
|
1071
|
+
createSessionPollHandler(config.adapter)
|
|
1072
|
+
);
|
|
1073
|
+
router.addConsoleRoute(
|
|
1074
|
+
"POST",
|
|
1075
|
+
"/session/claim",
|
|
1076
|
+
createSessionClaimHandler(validated, config.adapter)
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
return instance;
|
|
1080
|
+
}
|
|
1081
|
+
export {
|
|
1082
|
+
ConsoleAdapterRequiredError,
|
|
1083
|
+
ConsoleAutoApproveInProductionError,
|
|
1084
|
+
ConsoleEmailNotAllowedError,
|
|
1085
|
+
ConsoleError,
|
|
1086
|
+
ConsoleInvalidTokenError,
|
|
1087
|
+
ConsoleMagicLinkExpiredError,
|
|
1088
|
+
ConsoleRouter,
|
|
1089
|
+
ConsoleSessionExpiredError,
|
|
1090
|
+
betterConsole,
|
|
1091
|
+
extractBearerToken,
|
|
1092
|
+
generateMagicLinkCode,
|
|
1093
|
+
generateSessionToken,
|
|
1094
|
+
getCorsHeaders,
|
|
1095
|
+
getPreflightCorsHeaders,
|
|
1096
|
+
hasPermission,
|
|
1097
|
+
hashMagicLinkCode,
|
|
1098
|
+
hashToken,
|
|
1099
|
+
isOriginAllowed,
|
|
1100
|
+
parseConnectionTokenHash,
|
|
1101
|
+
parseDuration,
|
|
1102
|
+
signSessionJwt,
|
|
1103
|
+
verifySessionJwt,
|
|
1104
|
+
verifyTokenHash
|
|
1105
|
+
};
|
|
1106
|
+
//# sourceMappingURL=index.js.map
|