@chidchanun/bcp 0.2.16 → 0.2.18
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/README.md +150 -408
- package/docs/README.md +36 -38
- package/docs/api-manifest.json +27 -19
- package/docs/api-reference.md +257 -305
- package/docs/deployment-platform-v2.md +449 -0
- package/docs/docs-web-manifest.json +7 -3
- package/docs/observability-v3.md +402 -0
- package/docs/platform-manifest.json +30 -4
- package/docs/releases/0.2.17.md +166 -0
- package/docs/releases/0.2.18.md +136 -0
- package/package.json +11 -6
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/auth.mjs +1391 -0
- package/packages/client/src/config.mjs +1132 -0
- package/packages/client/src/deployment.mjs +609 -0
- package/packages/client/src/deployment.ts +20 -0
- package/packages/client/src/observability.mjs +1251 -0
- package/packages/client/src/observability.ts +37 -0
- package/packages/client/src/server.mjs +5615 -0
- package/packages/server/src/deployment.ts +936 -0
- package/packages/server/src/middleware.mjs +631 -0
- package/packages/server/src/observability-v3.ts +878 -0
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
// packages/server/src/auth.ts
|
|
2
|
+
import {
|
|
3
|
+
randomUUID
|
|
4
|
+
} from "node:crypto";
|
|
5
|
+
|
|
6
|
+
// packages/server/src/session.ts
|
|
7
|
+
import {
|
|
8
|
+
createHmac,
|
|
9
|
+
timingSafeEqual
|
|
10
|
+
} from "node:crypto";
|
|
11
|
+
|
|
12
|
+
// packages/server/src/request-context.ts
|
|
13
|
+
import {
|
|
14
|
+
AsyncLocalStorage
|
|
15
|
+
} from "node:async_hooks";
|
|
16
|
+
var requestStorage = new AsyncLocalStorage();
|
|
17
|
+
async function requestUrl() {
|
|
18
|
+
const context = getRequestContext();
|
|
19
|
+
return new URL(
|
|
20
|
+
context.url.href
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
async function cookies() {
|
|
24
|
+
const context = getRequestContext();
|
|
25
|
+
return context.cookies;
|
|
26
|
+
}
|
|
27
|
+
function getRequestContext() {
|
|
28
|
+
const context = requestStorage.getStore();
|
|
29
|
+
if (!context) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"BCP Framework: server request APIs can only be used while handling a request."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// packages/server/src/session.ts
|
|
38
|
+
var DEFAULT_COOKIE_NAME = "bcp_session";
|
|
39
|
+
var DEFAULT_EXPIRES_IN = 60 * 60 * 12;
|
|
40
|
+
var MAX_TOKEN_LENGTH = 16 * 1024;
|
|
41
|
+
var MINIMUM_SECRET_BYTES = 32;
|
|
42
|
+
async function createSessionToken(payload, options = {}) {
|
|
43
|
+
assertPayload(
|
|
44
|
+
payload
|
|
45
|
+
);
|
|
46
|
+
const secret = resolveSessionSecret(
|
|
47
|
+
options.secret
|
|
48
|
+
);
|
|
49
|
+
const expiresIn = resolveExpiresIn(
|
|
50
|
+
options.expiresIn
|
|
51
|
+
);
|
|
52
|
+
const issuer = resolveOptionalIssuer(
|
|
53
|
+
options.issuer
|
|
54
|
+
);
|
|
55
|
+
const audience = options.audience === void 0 ? void 0 : normalizeAudience(
|
|
56
|
+
options.audience
|
|
57
|
+
);
|
|
58
|
+
const now = Math.floor(
|
|
59
|
+
Date.now() / 1e3
|
|
60
|
+
);
|
|
61
|
+
const header = {
|
|
62
|
+
alg: "HS256",
|
|
63
|
+
typ: "JWT"
|
|
64
|
+
};
|
|
65
|
+
const claims = {
|
|
66
|
+
...payload,
|
|
67
|
+
iat: now,
|
|
68
|
+
exp: now + expiresIn
|
|
69
|
+
};
|
|
70
|
+
if (issuer !== void 0) {
|
|
71
|
+
claims.iss = issuer;
|
|
72
|
+
}
|
|
73
|
+
if (audience !== void 0) {
|
|
74
|
+
claims.aud = audience;
|
|
75
|
+
}
|
|
76
|
+
const encodedHeader = encodeJson(
|
|
77
|
+
header
|
|
78
|
+
);
|
|
79
|
+
const encodedPayload = encodeJson(
|
|
80
|
+
claims
|
|
81
|
+
);
|
|
82
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
83
|
+
const signature = signHs256(
|
|
84
|
+
signingInput,
|
|
85
|
+
secret
|
|
86
|
+
);
|
|
87
|
+
return `${signingInput}.${signature}`;
|
|
88
|
+
}
|
|
89
|
+
async function verifySessionToken(token, options = {}) {
|
|
90
|
+
const secret = resolveSessionSecret(
|
|
91
|
+
options.secret
|
|
92
|
+
);
|
|
93
|
+
const issuer = resolveOptionalIssuer(
|
|
94
|
+
options.issuer
|
|
95
|
+
);
|
|
96
|
+
const audience = options.audience === void 0 ? void 0 : normalizeAudience(
|
|
97
|
+
options.audience
|
|
98
|
+
);
|
|
99
|
+
try {
|
|
100
|
+
if (typeof token !== "string" || token.length === 0 || token.length > MAX_TOKEN_LENGTH) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const parts = token.split(".");
|
|
104
|
+
if (parts.length !== 3) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const [
|
|
108
|
+
encodedHeader,
|
|
109
|
+
encodedPayload,
|
|
110
|
+
signature
|
|
111
|
+
] = parts;
|
|
112
|
+
const header = decodeJson(
|
|
113
|
+
encodedHeader
|
|
114
|
+
);
|
|
115
|
+
if (header.alg !== "HS256" || header.typ !== void 0 && header.typ !== "JWT") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
119
|
+
const expectedSignature = signHs256(
|
|
120
|
+
signingInput,
|
|
121
|
+
secret
|
|
122
|
+
);
|
|
123
|
+
if (!safeEqual(
|
|
124
|
+
signature,
|
|
125
|
+
expectedSignature
|
|
126
|
+
)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const claims = decodeJson(
|
|
130
|
+
encodedPayload
|
|
131
|
+
);
|
|
132
|
+
const now = Math.floor(
|
|
133
|
+
Date.now() / 1e3
|
|
134
|
+
);
|
|
135
|
+
if (typeof claims.iat !== "number" || !Number.isFinite(
|
|
136
|
+
claims.iat
|
|
137
|
+
) || typeof claims.exp !== "number" || !Number.isFinite(
|
|
138
|
+
claims.exp
|
|
139
|
+
) || claims.exp <= now) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (claims.nbf !== void 0) {
|
|
143
|
+
if (typeof claims.nbf !== "number" || !Number.isFinite(
|
|
144
|
+
claims.nbf
|
|
145
|
+
) || claims.nbf > now) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (issuer !== void 0 && claims.iss !== issuer) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (audience !== void 0 && !audienceMatches(
|
|
153
|
+
claims.aud,
|
|
154
|
+
audience
|
|
155
|
+
)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return claims;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function createSession(payload, options = {}) {
|
|
164
|
+
const expiresIn = resolveExpiresIn(
|
|
165
|
+
options.expiresIn
|
|
166
|
+
);
|
|
167
|
+
const token = await createSessionToken(
|
|
168
|
+
payload,
|
|
169
|
+
{
|
|
170
|
+
secret: options.secret,
|
|
171
|
+
expiresIn,
|
|
172
|
+
issuer: options.issuer,
|
|
173
|
+
audience: options.audience
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
const cookieStore = await cookies();
|
|
177
|
+
cookieStore.set(
|
|
178
|
+
resolveCookieName(
|
|
179
|
+
options.cookieName
|
|
180
|
+
),
|
|
181
|
+
token,
|
|
182
|
+
{
|
|
183
|
+
httpOnly: options.httpOnly ?? true,
|
|
184
|
+
secure: options.secure ?? process.env.NODE_ENV === "production",
|
|
185
|
+
sameSite: options.sameSite ?? "lax",
|
|
186
|
+
path: options.path ?? "/",
|
|
187
|
+
domain: options.domain,
|
|
188
|
+
maxAge: expiresIn
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
return token;
|
|
192
|
+
}
|
|
193
|
+
async function getSession(options = {}) {
|
|
194
|
+
const cookieStore = await cookies();
|
|
195
|
+
const token = cookieStore.get(
|
|
196
|
+
resolveCookieName(
|
|
197
|
+
options.cookieName
|
|
198
|
+
)
|
|
199
|
+
)?.value;
|
|
200
|
+
if (!token) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return verifySessionToken(
|
|
204
|
+
token,
|
|
205
|
+
{
|
|
206
|
+
secret: options.secret,
|
|
207
|
+
issuer: options.issuer,
|
|
208
|
+
audience: options.audience
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
async function destroySession(options = {}) {
|
|
213
|
+
const cookieStore = await cookies();
|
|
214
|
+
cookieStore.delete(
|
|
215
|
+
resolveCookieName(
|
|
216
|
+
options.cookieName
|
|
217
|
+
),
|
|
218
|
+
{
|
|
219
|
+
path: options.path ?? "/",
|
|
220
|
+
domain: options.domain
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
function resolveSessionSecret(explicitSecret) {
|
|
225
|
+
const secret = explicitSecret ?? process.env.BCP_SESSION_SECRET;
|
|
226
|
+
if (!secret) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"BCP Framework: BCP_SESSION_SECRET is required for session tokens."
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (Buffer.byteLength(
|
|
232
|
+
secret,
|
|
233
|
+
"utf8"
|
|
234
|
+
) < MINIMUM_SECRET_BYTES) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`BCP Framework: session secret must be at least ${MINIMUM_SECRET_BYTES} bytes.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return secret;
|
|
240
|
+
}
|
|
241
|
+
function resolveExpiresIn(value) {
|
|
242
|
+
const expiresIn = value ?? DEFAULT_EXPIRES_IN;
|
|
243
|
+
if (!Number.isFinite(
|
|
244
|
+
expiresIn
|
|
245
|
+
) || expiresIn <= 0) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"BCP Framework: session expiresIn must be a positive finite number of seconds."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return Math.floor(
|
|
251
|
+
expiresIn
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
function resolveCookieName(value) {
|
|
255
|
+
const cookieName = value ?? DEFAULT_COOKIE_NAME;
|
|
256
|
+
assertNonEmptyString(
|
|
257
|
+
cookieName,
|
|
258
|
+
"cookieName"
|
|
259
|
+
);
|
|
260
|
+
return cookieName;
|
|
261
|
+
}
|
|
262
|
+
function resolveOptionalIssuer(value) {
|
|
263
|
+
if (value === void 0) {
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
assertNonEmptyString(
|
|
267
|
+
value,
|
|
268
|
+
"issuer"
|
|
269
|
+
);
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
function assertPayload(payload) {
|
|
273
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(
|
|
274
|
+
payload
|
|
275
|
+
)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
"BCP Framework: session payload must be a plain object."
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function assertNonEmptyString(value, label) {
|
|
282
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`BCP Framework: session ${label} must be a non-empty string.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function normalizeAudience(value) {
|
|
289
|
+
if (typeof value === "string") {
|
|
290
|
+
assertNonEmptyString(
|
|
291
|
+
value,
|
|
292
|
+
"audience"
|
|
293
|
+
);
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
if (!Array.isArray(
|
|
297
|
+
value
|
|
298
|
+
) || value.length === 0) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"BCP Framework: session audience must contain at least one value."
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
for (const audience of value) {
|
|
304
|
+
assertNonEmptyString(
|
|
305
|
+
audience,
|
|
306
|
+
"audience"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return [
|
|
310
|
+
...value
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
function audienceMatches(claim, expected) {
|
|
314
|
+
const expectedValues = typeof expected === "string" ? [
|
|
315
|
+
expected
|
|
316
|
+
] : expected;
|
|
317
|
+
const claimValues = typeof claim === "string" ? [
|
|
318
|
+
claim
|
|
319
|
+
] : Array.isArray(
|
|
320
|
+
claim
|
|
321
|
+
) ? claim.filter(
|
|
322
|
+
(value) => typeof value === "string"
|
|
323
|
+
) : [];
|
|
324
|
+
return expectedValues.some(
|
|
325
|
+
(value) => claimValues.includes(
|
|
326
|
+
value
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
function encodeJson(value) {
|
|
331
|
+
return Buffer.from(
|
|
332
|
+
JSON.stringify(
|
|
333
|
+
value
|
|
334
|
+
),
|
|
335
|
+
"utf8"
|
|
336
|
+
).toString(
|
|
337
|
+
"base64url"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function decodeJson(value) {
|
|
341
|
+
const decoded = Buffer.from(
|
|
342
|
+
value,
|
|
343
|
+
"base64url"
|
|
344
|
+
).toString(
|
|
345
|
+
"utf8"
|
|
346
|
+
);
|
|
347
|
+
return JSON.parse(
|
|
348
|
+
decoded
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
function signHs256(signingInput, secret) {
|
|
352
|
+
return createHmac(
|
|
353
|
+
"sha256",
|
|
354
|
+
secret
|
|
355
|
+
).update(
|
|
356
|
+
signingInput,
|
|
357
|
+
"utf8"
|
|
358
|
+
).digest(
|
|
359
|
+
"base64url"
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
function safeEqual(actual, expected) {
|
|
363
|
+
const actualBuffer = Buffer.from(
|
|
364
|
+
actual,
|
|
365
|
+
"base64url"
|
|
366
|
+
);
|
|
367
|
+
const expectedBuffer = Buffer.from(
|
|
368
|
+
expected,
|
|
369
|
+
"base64url"
|
|
370
|
+
);
|
|
371
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(
|
|
372
|
+
actualBuffer,
|
|
373
|
+
expectedBuffer
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// packages/server/src/auth.ts
|
|
378
|
+
async function auth(options = {}) {
|
|
379
|
+
const session = await readSignedAuthSession(
|
|
380
|
+
toSessionCookieOptions(
|
|
381
|
+
options
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
if (!session) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const idleTimeout = resolveIdleTimeout(
|
|
388
|
+
options.idleTimeout
|
|
389
|
+
);
|
|
390
|
+
const store = options.store;
|
|
391
|
+
if (idleTimeout !== void 0 && !store) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
"BCP Auth: idleTimeout requires a server-side auth session store."
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (!store) {
|
|
397
|
+
return session;
|
|
398
|
+
}
|
|
399
|
+
const record = await store.get(
|
|
400
|
+
session.sid
|
|
401
|
+
);
|
|
402
|
+
if (!record) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const now = currentUnixTime();
|
|
406
|
+
const userId = serializeAuthUserId(
|
|
407
|
+
session.user.id
|
|
408
|
+
);
|
|
409
|
+
if (record.userId !== userId || record.revokedAt !== void 0 && record.revokedAt !== null || record.expiresAt <= now) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
if (idleTimeout !== void 0 && record.lastSeenAt + idleTimeout <= now) {
|
|
413
|
+
await store.revoke(
|
|
414
|
+
session.sid,
|
|
415
|
+
now
|
|
416
|
+
);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
await store.touch(
|
|
420
|
+
session.sid,
|
|
421
|
+
now
|
|
422
|
+
);
|
|
423
|
+
return session;
|
|
424
|
+
}
|
|
425
|
+
async function getSession2(options = {}) {
|
|
426
|
+
return auth(
|
|
427
|
+
options
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
async function login(user, options = {}) {
|
|
431
|
+
assertAuthUser(
|
|
432
|
+
user
|
|
433
|
+
);
|
|
434
|
+
if (options.data !== void 0 && !isPlainObject(
|
|
435
|
+
options.data
|
|
436
|
+
)) {
|
|
437
|
+
throw new TypeError(
|
|
438
|
+
"BCP Auth: session data must be a plain object."
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const idleTimeout = resolveIdleTimeout(
|
|
442
|
+
options.idleTimeout
|
|
443
|
+
);
|
|
444
|
+
if (idleTimeout !== void 0 && !options.store) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
"BCP Auth: idleTimeout requires a server-side auth session store."
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const {
|
|
450
|
+
data
|
|
451
|
+
} = options;
|
|
452
|
+
const sessionOptions = toSessionCookieOptions(
|
|
453
|
+
options
|
|
454
|
+
);
|
|
455
|
+
const payload = {
|
|
456
|
+
sid: randomUUID(),
|
|
457
|
+
user,
|
|
458
|
+
...data === void 0 ? {} : {
|
|
459
|
+
data
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
await createSession(
|
|
463
|
+
payload,
|
|
464
|
+
sessionOptions
|
|
465
|
+
);
|
|
466
|
+
const session = await readSignedAuthSession(
|
|
467
|
+
sessionOptions
|
|
468
|
+
);
|
|
469
|
+
if (!session) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
"BCP Auth: login created a session but it could not be read back."
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (options.store) {
|
|
475
|
+
await options.store.set(
|
|
476
|
+
createStoreRecord(
|
|
477
|
+
session
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return session;
|
|
482
|
+
}
|
|
483
|
+
async function logout(options = {}) {
|
|
484
|
+
if (options.store) {
|
|
485
|
+
const current = await auth(
|
|
486
|
+
options
|
|
487
|
+
);
|
|
488
|
+
if (current) {
|
|
489
|
+
await options.store.revoke(
|
|
490
|
+
current.sid,
|
|
491
|
+
currentUnixTime()
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
await destroySession(
|
|
496
|
+
toSessionCookieOptions(
|
|
497
|
+
options
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
async function logoutAll(options = {}) {
|
|
502
|
+
const store = requireAuthSessionStore(
|
|
503
|
+
options.store,
|
|
504
|
+
"logoutAll"
|
|
505
|
+
);
|
|
506
|
+
const current = await auth(
|
|
507
|
+
options
|
|
508
|
+
);
|
|
509
|
+
let revoked = 0;
|
|
510
|
+
if (current) {
|
|
511
|
+
revoked = await store.revokeUser(
|
|
512
|
+
serializeAuthUserId(
|
|
513
|
+
current.user.id
|
|
514
|
+
),
|
|
515
|
+
currentUnixTime()
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
await destroySession(
|
|
519
|
+
toSessionCookieOptions(
|
|
520
|
+
options
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
return revoked;
|
|
524
|
+
}
|
|
525
|
+
async function rotateSession(options = {}) {
|
|
526
|
+
const current = await auth(
|
|
527
|
+
options
|
|
528
|
+
);
|
|
529
|
+
if (!current) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
const rotated = await login(
|
|
533
|
+
current.user,
|
|
534
|
+
{
|
|
535
|
+
...options,
|
|
536
|
+
issuer: options.issuer ?? current.iss,
|
|
537
|
+
audience: options.audience ?? current.aud,
|
|
538
|
+
...current.data === void 0 ? {} : {
|
|
539
|
+
data: current.data
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
if (options.store) {
|
|
544
|
+
await options.store.revoke(
|
|
545
|
+
current.sid,
|
|
546
|
+
currentUnixTime()
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return rotated;
|
|
550
|
+
}
|
|
551
|
+
async function revokeSession(sid, store) {
|
|
552
|
+
assertSessionId(
|
|
553
|
+
sid
|
|
554
|
+
);
|
|
555
|
+
assertAuthSessionStore(
|
|
556
|
+
store
|
|
557
|
+
);
|
|
558
|
+
return Boolean(
|
|
559
|
+
await store.revoke(
|
|
560
|
+
sid,
|
|
561
|
+
currentUnixTime()
|
|
562
|
+
)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
async function revokeUserSessions(userId, store) {
|
|
566
|
+
assertAuthSessionStore(
|
|
567
|
+
store
|
|
568
|
+
);
|
|
569
|
+
return store.revokeUser(
|
|
570
|
+
serializeAuthUserId(
|
|
571
|
+
userId
|
|
572
|
+
),
|
|
573
|
+
currentUnixTime()
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
function createAuth(defaults = {}) {
|
|
577
|
+
return {
|
|
578
|
+
auth: (options = {}) => auth({
|
|
579
|
+
...defaults,
|
|
580
|
+
...options
|
|
581
|
+
}),
|
|
582
|
+
getSession: (options = {}) => auth({
|
|
583
|
+
...defaults,
|
|
584
|
+
...options
|
|
585
|
+
}),
|
|
586
|
+
login: (user, options = {}) => {
|
|
587
|
+
const {
|
|
588
|
+
data,
|
|
589
|
+
...sessionOptions
|
|
590
|
+
} = options;
|
|
591
|
+
return login(
|
|
592
|
+
user,
|
|
593
|
+
{
|
|
594
|
+
...defaults,
|
|
595
|
+
...sessionOptions,
|
|
596
|
+
...data === void 0 ? {} : {
|
|
597
|
+
data
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
},
|
|
602
|
+
logout: (options = {}) => logout({
|
|
603
|
+
...defaults,
|
|
604
|
+
...options
|
|
605
|
+
}),
|
|
606
|
+
logoutAll: (options = {}) => logoutAll({
|
|
607
|
+
...defaults,
|
|
608
|
+
...options
|
|
609
|
+
}),
|
|
610
|
+
rotateSession: (options = {}) => rotateSession({
|
|
611
|
+
...defaults,
|
|
612
|
+
...options
|
|
613
|
+
}),
|
|
614
|
+
revokeSession: (sid) => revokeSession(
|
|
615
|
+
sid,
|
|
616
|
+
requireAuthSessionStore(
|
|
617
|
+
defaults.store,
|
|
618
|
+
"revokeSession"
|
|
619
|
+
)
|
|
620
|
+
),
|
|
621
|
+
revokeUserSessions: (userId) => revokeUserSessions(
|
|
622
|
+
userId,
|
|
623
|
+
requireAuthSessionStore(
|
|
624
|
+
defaults.store,
|
|
625
|
+
"revokeUserSessions"
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
async function readSignedAuthSession(options) {
|
|
631
|
+
const session = await getSession(
|
|
632
|
+
options
|
|
633
|
+
);
|
|
634
|
+
if (!session) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
if (typeof session.sid !== "string" || session.sid.trim().length === 0 || !isAuthUser(
|
|
638
|
+
session.user
|
|
639
|
+
)) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
if (session.data !== void 0 && !isPlainObject(
|
|
643
|
+
session.data
|
|
644
|
+
)) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return session;
|
|
648
|
+
}
|
|
649
|
+
function createStoreRecord(session) {
|
|
650
|
+
return {
|
|
651
|
+
sid: session.sid,
|
|
652
|
+
userId: serializeAuthUserId(
|
|
653
|
+
session.user.id
|
|
654
|
+
),
|
|
655
|
+
createdAt: session.iat,
|
|
656
|
+
expiresAt: session.exp,
|
|
657
|
+
lastSeenAt: session.iat,
|
|
658
|
+
revokedAt: null
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function toSessionCookieOptions(options) {
|
|
662
|
+
const {
|
|
663
|
+
store: _store,
|
|
664
|
+
idleTimeout: _idleTimeout,
|
|
665
|
+
...sessionOptions
|
|
666
|
+
} = options;
|
|
667
|
+
return sessionOptions;
|
|
668
|
+
}
|
|
669
|
+
function serializeAuthUserId(value) {
|
|
670
|
+
if (typeof value === "string") {
|
|
671
|
+
const normalized = value.trim();
|
|
672
|
+
if (!normalized) {
|
|
673
|
+
throw new TypeError(
|
|
674
|
+
"BCP Auth: user id must not be empty."
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return `string:${normalized}`;
|
|
678
|
+
}
|
|
679
|
+
if (!Number.isFinite(
|
|
680
|
+
value
|
|
681
|
+
)) {
|
|
682
|
+
throw new TypeError(
|
|
683
|
+
"BCP Auth: numeric user id must be finite."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return `number:${value}`;
|
|
687
|
+
}
|
|
688
|
+
function resolveIdleTimeout(value) {
|
|
689
|
+
if (value === void 0) {
|
|
690
|
+
return void 0;
|
|
691
|
+
}
|
|
692
|
+
if (!Number.isFinite(
|
|
693
|
+
value
|
|
694
|
+
) || value <= 0) {
|
|
695
|
+
throw new TypeError(
|
|
696
|
+
"BCP Auth: idleTimeout must be a positive finite number of seconds."
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return Math.floor(
|
|
700
|
+
value
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
function requireAuthSessionStore(store, operation) {
|
|
704
|
+
if (!store) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`BCP Auth: ${operation} requires a server-side auth session store.`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
assertAuthSessionStore(
|
|
710
|
+
store
|
|
711
|
+
);
|
|
712
|
+
return store;
|
|
713
|
+
}
|
|
714
|
+
function assertAuthSessionStore(store) {
|
|
715
|
+
if (!store || typeof store !== "object" || typeof store.set !== "function" || typeof store.get !== "function" || typeof store.touch !== "function" || typeof store.revoke !== "function" || typeof store.revokeUser !== "function") {
|
|
716
|
+
throw new TypeError(
|
|
717
|
+
"BCP Auth: session store must implement set, get, touch, revoke and revokeUser."
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function assertSessionId(sid) {
|
|
722
|
+
if (typeof sid !== "string" || sid.trim() === "") {
|
|
723
|
+
throw new TypeError(
|
|
724
|
+
"BCP Auth: session id must be a non-empty string."
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function assertAuthUser(value) {
|
|
729
|
+
if (!isAuthUser(
|
|
730
|
+
value
|
|
731
|
+
)) {
|
|
732
|
+
throw new TypeError(
|
|
733
|
+
"BCP Auth: user must be a plain object with a non-empty string or finite numeric id."
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function isAuthUser(value) {
|
|
738
|
+
if (!isPlainObject(
|
|
739
|
+
value
|
|
740
|
+
)) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
const id = value.id;
|
|
744
|
+
return typeof id === "string" && id.trim().length > 0 || typeof id === "number" && Number.isFinite(
|
|
745
|
+
id
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
function isPlainObject(value) {
|
|
749
|
+
if (value === null || typeof value !== "object" || Array.isArray(
|
|
750
|
+
value
|
|
751
|
+
)) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
const prototype = Object.getPrototypeOf(
|
|
755
|
+
value
|
|
756
|
+
);
|
|
757
|
+
return prototype === Object.prototype || prototype === null;
|
|
758
|
+
}
|
|
759
|
+
function currentUnixTime() {
|
|
760
|
+
return Math.floor(
|
|
761
|
+
Date.now() / 1e3
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// packages/server/src/auth-session-store.ts
|
|
766
|
+
function createMemoryAuthSessionStore() {
|
|
767
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
768
|
+
const clearExpired = (now = currentUnixTime2()) => {
|
|
769
|
+
let removed = 0;
|
|
770
|
+
for (const [
|
|
771
|
+
sid,
|
|
772
|
+
record
|
|
773
|
+
] of sessions) {
|
|
774
|
+
if (record.expiresAt <= now) {
|
|
775
|
+
sessions.delete(
|
|
776
|
+
sid
|
|
777
|
+
);
|
|
778
|
+
removed += 1;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return removed;
|
|
782
|
+
};
|
|
783
|
+
return {
|
|
784
|
+
set(record) {
|
|
785
|
+
assertRecord(
|
|
786
|
+
record
|
|
787
|
+
);
|
|
788
|
+
clearExpired();
|
|
789
|
+
sessions.set(
|
|
790
|
+
record.sid,
|
|
791
|
+
cloneRecord(
|
|
792
|
+
record
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
},
|
|
796
|
+
get(sid) {
|
|
797
|
+
assertSid(
|
|
798
|
+
sid
|
|
799
|
+
);
|
|
800
|
+
clearExpired();
|
|
801
|
+
const record = sessions.get(
|
|
802
|
+
sid
|
|
803
|
+
);
|
|
804
|
+
return record ? cloneRecord(
|
|
805
|
+
record
|
|
806
|
+
) : null;
|
|
807
|
+
},
|
|
808
|
+
touch(sid, lastSeenAt) {
|
|
809
|
+
assertSid(
|
|
810
|
+
sid
|
|
811
|
+
);
|
|
812
|
+
assertTimestamp(
|
|
813
|
+
lastSeenAt,
|
|
814
|
+
"lastSeenAt"
|
|
815
|
+
);
|
|
816
|
+
clearExpired(
|
|
817
|
+
lastSeenAt
|
|
818
|
+
);
|
|
819
|
+
const record = sessions.get(
|
|
820
|
+
sid
|
|
821
|
+
);
|
|
822
|
+
if (!record || record.revokedAt !== void 0 && record.revokedAt !== null) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
record.lastSeenAt = Math.max(
|
|
826
|
+
record.lastSeenAt,
|
|
827
|
+
lastSeenAt
|
|
828
|
+
);
|
|
829
|
+
},
|
|
830
|
+
revoke(sid, revokedAt = currentUnixTime2()) {
|
|
831
|
+
assertSid(
|
|
832
|
+
sid
|
|
833
|
+
);
|
|
834
|
+
assertTimestamp(
|
|
835
|
+
revokedAt,
|
|
836
|
+
"revokedAt"
|
|
837
|
+
);
|
|
838
|
+
const record = sessions.get(
|
|
839
|
+
sid
|
|
840
|
+
);
|
|
841
|
+
if (!record) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
record.revokedAt = revokedAt;
|
|
845
|
+
return true;
|
|
846
|
+
},
|
|
847
|
+
revokeUser(userId, revokedAt = currentUnixTime2()) {
|
|
848
|
+
assertUserId(
|
|
849
|
+
userId
|
|
850
|
+
);
|
|
851
|
+
assertTimestamp(
|
|
852
|
+
revokedAt,
|
|
853
|
+
"revokedAt"
|
|
854
|
+
);
|
|
855
|
+
clearExpired(
|
|
856
|
+
revokedAt
|
|
857
|
+
);
|
|
858
|
+
let revoked = 0;
|
|
859
|
+
for (const record of sessions.values()) {
|
|
860
|
+
if (record.userId !== userId || record.revokedAt !== void 0 && record.revokedAt !== null) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
record.revokedAt = revokedAt;
|
|
864
|
+
revoked += 1;
|
|
865
|
+
}
|
|
866
|
+
return revoked;
|
|
867
|
+
},
|
|
868
|
+
clearExpired,
|
|
869
|
+
size() {
|
|
870
|
+
clearExpired();
|
|
871
|
+
return sessions.size;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function cloneRecord(record) {
|
|
876
|
+
return {
|
|
877
|
+
...record
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function assertRecord(record) {
|
|
881
|
+
if (!record || typeof record !== "object") {
|
|
882
|
+
throw new TypeError(
|
|
883
|
+
"BCP Auth Store: session record must be an object."
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
assertSid(
|
|
887
|
+
record.sid
|
|
888
|
+
);
|
|
889
|
+
assertUserId(
|
|
890
|
+
record.userId
|
|
891
|
+
);
|
|
892
|
+
assertTimestamp(
|
|
893
|
+
record.createdAt,
|
|
894
|
+
"createdAt"
|
|
895
|
+
);
|
|
896
|
+
assertTimestamp(
|
|
897
|
+
record.expiresAt,
|
|
898
|
+
"expiresAt"
|
|
899
|
+
);
|
|
900
|
+
assertTimestamp(
|
|
901
|
+
record.lastSeenAt,
|
|
902
|
+
"lastSeenAt"
|
|
903
|
+
);
|
|
904
|
+
if (record.expiresAt <= record.createdAt) {
|
|
905
|
+
throw new TypeError(
|
|
906
|
+
"BCP Auth Store: expiresAt must be later than createdAt."
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (record.revokedAt !== void 0 && record.revokedAt !== null) {
|
|
910
|
+
assertTimestamp(
|
|
911
|
+
record.revokedAt,
|
|
912
|
+
"revokedAt"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function assertSid(value) {
|
|
917
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
918
|
+
throw new TypeError(
|
|
919
|
+
"BCP Auth Store: sid must be a non-empty string."
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function assertUserId(value) {
|
|
924
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
925
|
+
throw new TypeError(
|
|
926
|
+
"BCP Auth Store: userId must be a non-empty string."
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function assertTimestamp(value, label) {
|
|
931
|
+
if (!Number.isFinite(
|
|
932
|
+
value
|
|
933
|
+
) || value < 0) {
|
|
934
|
+
throw new TypeError(
|
|
935
|
+
`BCP Auth Store: ${label} must be a non-negative finite Unix timestamp.`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function currentUnixTime2() {
|
|
940
|
+
return Math.floor(
|
|
941
|
+
Date.now() / 1e3
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// packages/server/src/authorization.ts
|
|
946
|
+
var AuthorizationError = class extends Error {
|
|
947
|
+
status = 403;
|
|
948
|
+
code = "FORBIDDEN";
|
|
949
|
+
constructor(message = "Forbidden") {
|
|
950
|
+
super(message);
|
|
951
|
+
this.name = "AuthorizationError";
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
function getUserPermissions(user, field = "permissions") {
|
|
955
|
+
const normalizedField = normalizeField(
|
|
956
|
+
field
|
|
957
|
+
);
|
|
958
|
+
const value = user[normalizedField];
|
|
959
|
+
if (typeof value === "string") {
|
|
960
|
+
const permission = value.trim();
|
|
961
|
+
return permission ? [permission] : [];
|
|
962
|
+
}
|
|
963
|
+
if (!Array.isArray(
|
|
964
|
+
value
|
|
965
|
+
)) {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
return Array.from(
|
|
969
|
+
new Set(
|
|
970
|
+
value.filter(
|
|
971
|
+
(permission) => typeof permission === "string"
|
|
972
|
+
).map(
|
|
973
|
+
(permission) => permission.trim()
|
|
974
|
+
).filter(Boolean)
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
function hasPermission(user, required, options = {}) {
|
|
979
|
+
const requiredPermissions = normalizePermissions(
|
|
980
|
+
required
|
|
981
|
+
);
|
|
982
|
+
const assignedPermissions = getUserPermissions(
|
|
983
|
+
user,
|
|
984
|
+
options.field
|
|
985
|
+
);
|
|
986
|
+
const match = options.match ?? "any";
|
|
987
|
+
assertAuthorizationMatch(
|
|
988
|
+
match
|
|
989
|
+
);
|
|
990
|
+
return match === "all" ? requiredPermissions.every(
|
|
991
|
+
(permission) => assignedPermissions.includes(
|
|
992
|
+
permission
|
|
993
|
+
)
|
|
994
|
+
) : requiredPermissions.some(
|
|
995
|
+
(permission) => assignedPermissions.includes(
|
|
996
|
+
permission
|
|
997
|
+
)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
function assertPermission(user, required, options = {}) {
|
|
1001
|
+
if (!hasPermission(
|
|
1002
|
+
user,
|
|
1003
|
+
required,
|
|
1004
|
+
options
|
|
1005
|
+
)) {
|
|
1006
|
+
throw new AuthorizationError();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function defineAuthorizationPolicy(policy) {
|
|
1010
|
+
if (typeof policy !== "function") {
|
|
1011
|
+
throw new TypeError(
|
|
1012
|
+
"BCP Authorization: policy must be a function."
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
return policy;
|
|
1016
|
+
}
|
|
1017
|
+
async function can(policy, context) {
|
|
1018
|
+
assertAuthorizationContext(
|
|
1019
|
+
context
|
|
1020
|
+
);
|
|
1021
|
+
return Boolean(
|
|
1022
|
+
await policy(
|
|
1023
|
+
context
|
|
1024
|
+
)
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
async function cannot(policy, context) {
|
|
1028
|
+
return !await can(
|
|
1029
|
+
policy,
|
|
1030
|
+
context
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
async function authorize(policy, context, message = "Forbidden") {
|
|
1034
|
+
if (!await can(
|
|
1035
|
+
policy,
|
|
1036
|
+
context
|
|
1037
|
+
)) {
|
|
1038
|
+
throw new AuthorizationError(
|
|
1039
|
+
message
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function normalizePermissions(value) {
|
|
1044
|
+
const permissions = typeof value === "string" ? [value] : [
|
|
1045
|
+
...value
|
|
1046
|
+
];
|
|
1047
|
+
const normalized = permissions.map(
|
|
1048
|
+
(permission) => permission.trim()
|
|
1049
|
+
);
|
|
1050
|
+
if (normalized.length === 0 || normalized.some(
|
|
1051
|
+
(permission) => permission.length === 0
|
|
1052
|
+
)) {
|
|
1053
|
+
throw new TypeError(
|
|
1054
|
+
"BCP Authorization: required permissions must contain non-empty strings."
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
return Array.from(
|
|
1058
|
+
new Set(
|
|
1059
|
+
normalized
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
function normalizeField(value) {
|
|
1064
|
+
const field = value.trim();
|
|
1065
|
+
if (!field) {
|
|
1066
|
+
throw new TypeError(
|
|
1067
|
+
"BCP Authorization: permission field must be a non-empty string."
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return field;
|
|
1071
|
+
}
|
|
1072
|
+
function assertAuthorizationMatch(value) {
|
|
1073
|
+
if (value !== "any" && value !== "all") {
|
|
1074
|
+
throw new TypeError(
|
|
1075
|
+
'BCP Authorization: match must be either "any" or "all".'
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function assertAuthorizationContext(value) {
|
|
1080
|
+
if (!value || typeof value !== "object" || Array.isArray(
|
|
1081
|
+
value
|
|
1082
|
+
) || !("user" in value) || !value.user || typeof value.user !== "object" || Array.isArray(
|
|
1083
|
+
value.user
|
|
1084
|
+
)) {
|
|
1085
|
+
throw new TypeError(
|
|
1086
|
+
"BCP Authorization: context must contain a user object."
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// packages/server/src/server-response.ts
|
|
1092
|
+
async function redirect(destination, status = 307) {
|
|
1093
|
+
assertRedirectStatus(
|
|
1094
|
+
status
|
|
1095
|
+
);
|
|
1096
|
+
const base = await requestUrl();
|
|
1097
|
+
const target = destination instanceof URL ? new URL(
|
|
1098
|
+
destination.href
|
|
1099
|
+
) : new URL(
|
|
1100
|
+
destination,
|
|
1101
|
+
base
|
|
1102
|
+
);
|
|
1103
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`BCP Framework: redirect only supports http and https URLs, received "${target.protocol}".`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
return new Response(
|
|
1109
|
+
null,
|
|
1110
|
+
{
|
|
1111
|
+
status,
|
|
1112
|
+
headers: {
|
|
1113
|
+
Location: target.href
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
function assertRedirectStatus(status) {
|
|
1119
|
+
if (status !== 301 && status !== 302 && status !== 303 && status !== 307 && status !== 308) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`BCP Framework: invalid redirect status ${status}. Use 301, 302, 303, 307 or 308.`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// packages/server/src/auth-guard.ts
|
|
1127
|
+
async function requireAuth(options = {}) {
|
|
1128
|
+
const {
|
|
1129
|
+
redirectTo = "/login",
|
|
1130
|
+
redirectStatus = 303,
|
|
1131
|
+
...authOptions
|
|
1132
|
+
} = options;
|
|
1133
|
+
const session = await auth(
|
|
1134
|
+
authOptions
|
|
1135
|
+
);
|
|
1136
|
+
if (!session) {
|
|
1137
|
+
if (redirectTo === null) {
|
|
1138
|
+
return new Response(
|
|
1139
|
+
"Unauthorized",
|
|
1140
|
+
{
|
|
1141
|
+
status: 401
|
|
1142
|
+
}
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return redirect(
|
|
1146
|
+
redirectTo,
|
|
1147
|
+
redirectStatus
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
auth: session
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
async function requireGuest(options = {}) {
|
|
1155
|
+
const {
|
|
1156
|
+
redirectTo = "/",
|
|
1157
|
+
redirectStatus = 303,
|
|
1158
|
+
...authOptions
|
|
1159
|
+
} = options;
|
|
1160
|
+
const session = await auth(
|
|
1161
|
+
authOptions
|
|
1162
|
+
);
|
|
1163
|
+
if (!session) {
|
|
1164
|
+
return {};
|
|
1165
|
+
}
|
|
1166
|
+
if (redirectTo === null) {
|
|
1167
|
+
return new Response(
|
|
1168
|
+
"Already authenticated",
|
|
1169
|
+
{
|
|
1170
|
+
status: 409
|
|
1171
|
+
}
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
return redirect(
|
|
1175
|
+
redirectTo,
|
|
1176
|
+
redirectStatus
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
async function requireRole(requiredRole, options = {}) {
|
|
1180
|
+
const roles = normalizeRequiredRoles(
|
|
1181
|
+
requiredRole
|
|
1182
|
+
);
|
|
1183
|
+
const {
|
|
1184
|
+
roleField = "role",
|
|
1185
|
+
match = "any",
|
|
1186
|
+
forbiddenRedirectTo = null,
|
|
1187
|
+
forbiddenRedirectStatus = 303,
|
|
1188
|
+
...authOptions
|
|
1189
|
+
} = options;
|
|
1190
|
+
const authenticated = await requireAuth(
|
|
1191
|
+
authOptions
|
|
1192
|
+
);
|
|
1193
|
+
if (authenticated instanceof Response) {
|
|
1194
|
+
return authenticated;
|
|
1195
|
+
}
|
|
1196
|
+
const assignedRoles = readAssignedRoles(
|
|
1197
|
+
authenticated.auth.user,
|
|
1198
|
+
roleField
|
|
1199
|
+
);
|
|
1200
|
+
const allowed = match === "all" ? roles.every(
|
|
1201
|
+
(role) => assignedRoles.includes(
|
|
1202
|
+
role
|
|
1203
|
+
)
|
|
1204
|
+
) : roles.some(
|
|
1205
|
+
(role) => assignedRoles.includes(
|
|
1206
|
+
role
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
return finalizeAuthorization(
|
|
1210
|
+
allowed,
|
|
1211
|
+
authenticated,
|
|
1212
|
+
forbiddenRedirectTo,
|
|
1213
|
+
forbiddenRedirectStatus
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
async function requirePermission(requiredPermission, options = {}) {
|
|
1217
|
+
const {
|
|
1218
|
+
permissionField = "permissions",
|
|
1219
|
+
match = "any",
|
|
1220
|
+
forbiddenRedirectTo = null,
|
|
1221
|
+
forbiddenRedirectStatus = 303,
|
|
1222
|
+
...authOptions
|
|
1223
|
+
} = options;
|
|
1224
|
+
const authenticated = await requireAuth(
|
|
1225
|
+
authOptions
|
|
1226
|
+
);
|
|
1227
|
+
if (authenticated instanceof Response) {
|
|
1228
|
+
return authenticated;
|
|
1229
|
+
}
|
|
1230
|
+
const allowed = hasPermission(
|
|
1231
|
+
authenticated.auth.user,
|
|
1232
|
+
requiredPermission,
|
|
1233
|
+
{
|
|
1234
|
+
field: permissionField,
|
|
1235
|
+
match
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
return finalizeAuthorization(
|
|
1239
|
+
allowed,
|
|
1240
|
+
authenticated,
|
|
1241
|
+
forbiddenRedirectTo,
|
|
1242
|
+
forbiddenRedirectStatus
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
function createAuthGuard(options = {}) {
|
|
1246
|
+
return () => requireAuth(
|
|
1247
|
+
options
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
function createGuestGuard(options = {}) {
|
|
1251
|
+
return () => requireGuest(
|
|
1252
|
+
options
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
function createRoleGuard(requiredRole, options = {}) {
|
|
1256
|
+
return () => requireRole(
|
|
1257
|
+
requiredRole,
|
|
1258
|
+
options
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
function createPermissionGuard(requiredPermission, options = {}) {
|
|
1262
|
+
return () => requirePermission(
|
|
1263
|
+
requiredPermission,
|
|
1264
|
+
options
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
function getGuardAuth(guardData) {
|
|
1268
|
+
const value = guardData.auth;
|
|
1269
|
+
if (!value || typeof value !== "object" || Array.isArray(
|
|
1270
|
+
value
|
|
1271
|
+
)) {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
const session = value;
|
|
1275
|
+
if (typeof session.sid !== "string" || session.sid.trim().length === 0 || typeof session.iat !== "number" || !Number.isFinite(
|
|
1276
|
+
session.iat
|
|
1277
|
+
) || typeof session.exp !== "number" || !Number.isFinite(
|
|
1278
|
+
session.exp
|
|
1279
|
+
) || !isGuardAuthUser(
|
|
1280
|
+
session.user
|
|
1281
|
+
)) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
return value;
|
|
1285
|
+
}
|
|
1286
|
+
function finalizeAuthorization(allowed, authenticated, forbiddenRedirectTo, forbiddenRedirectStatus) {
|
|
1287
|
+
if (allowed) {
|
|
1288
|
+
return authenticated;
|
|
1289
|
+
}
|
|
1290
|
+
if (forbiddenRedirectTo !== null) {
|
|
1291
|
+
return redirect(
|
|
1292
|
+
forbiddenRedirectTo,
|
|
1293
|
+
forbiddenRedirectStatus
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
return new Response(
|
|
1297
|
+
"Forbidden",
|
|
1298
|
+
{
|
|
1299
|
+
status: 403
|
|
1300
|
+
}
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
function normalizeRequiredRoles(value) {
|
|
1304
|
+
const roles = typeof value === "string" ? [
|
|
1305
|
+
value
|
|
1306
|
+
] : [
|
|
1307
|
+
...value
|
|
1308
|
+
];
|
|
1309
|
+
const normalized = roles.map(
|
|
1310
|
+
(role) => role.trim()
|
|
1311
|
+
);
|
|
1312
|
+
if (normalized.length === 0 || normalized.some(
|
|
1313
|
+
(role) => role.length === 0
|
|
1314
|
+
)) {
|
|
1315
|
+
throw new TypeError(
|
|
1316
|
+
"BCP Auth Guard: required roles must contain non-empty strings."
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
return Array.from(
|
|
1320
|
+
new Set(
|
|
1321
|
+
normalized
|
|
1322
|
+
)
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
function readAssignedRoles(user, roleField) {
|
|
1326
|
+
const field = roleField.trim();
|
|
1327
|
+
if (!field) {
|
|
1328
|
+
throw new TypeError(
|
|
1329
|
+
"BCP Auth Guard: roleField must be a non-empty string."
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const value = user[field];
|
|
1333
|
+
if (typeof value === "string") {
|
|
1334
|
+
const role = value.trim();
|
|
1335
|
+
return role ? [
|
|
1336
|
+
role
|
|
1337
|
+
] : [];
|
|
1338
|
+
}
|
|
1339
|
+
if (Array.isArray(
|
|
1340
|
+
value
|
|
1341
|
+
)) {
|
|
1342
|
+
return value.filter(
|
|
1343
|
+
(role) => typeof role === "string"
|
|
1344
|
+
).map(
|
|
1345
|
+
(role) => role.trim()
|
|
1346
|
+
).filter(
|
|
1347
|
+
Boolean
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
return [];
|
|
1351
|
+
}
|
|
1352
|
+
function isGuardAuthUser(value) {
|
|
1353
|
+
if (!value || typeof value !== "object" || Array.isArray(
|
|
1354
|
+
value
|
|
1355
|
+
)) {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
const id = value.id;
|
|
1359
|
+
return typeof id === "string" && id.trim().length > 0 || typeof id === "number" && Number.isFinite(
|
|
1360
|
+
id
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
export {
|
|
1364
|
+
AuthorizationError,
|
|
1365
|
+
assertPermission,
|
|
1366
|
+
auth,
|
|
1367
|
+
authorize,
|
|
1368
|
+
can,
|
|
1369
|
+
cannot,
|
|
1370
|
+
createAuth,
|
|
1371
|
+
createAuthGuard,
|
|
1372
|
+
createGuestGuard,
|
|
1373
|
+
createMemoryAuthSessionStore,
|
|
1374
|
+
createPermissionGuard,
|
|
1375
|
+
createRoleGuard,
|
|
1376
|
+
defineAuthorizationPolicy,
|
|
1377
|
+
getGuardAuth,
|
|
1378
|
+
getSession2 as getSession,
|
|
1379
|
+
getUserPermissions,
|
|
1380
|
+
hasPermission,
|
|
1381
|
+
login,
|
|
1382
|
+
logout,
|
|
1383
|
+
logoutAll,
|
|
1384
|
+
requireAuth,
|
|
1385
|
+
requireGuest,
|
|
1386
|
+
requirePermission,
|
|
1387
|
+
requireRole,
|
|
1388
|
+
revokeSession,
|
|
1389
|
+
revokeUserSessions,
|
|
1390
|
+
rotateSession
|
|
1391
|
+
};
|