@spfn/auth 0.2.0-beta.71 → 0.2.0-beta.74
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/{authenticate-NhEb_j27.d.ts → authenticate-DXVtYh8X.d.ts} +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/nextjs/api.js +25 -2
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js +4 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +16 -3
- package/dist/server.js +292 -26
- package/dist/server.js.map +1 -1
- package/migrations/0005_lethal_lifeguard.sql +32 -0
- package/migrations/meta/0005_snapshot.json +1721 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +4 -4
package/dist/server.js
CHANGED
|
@@ -7604,6 +7604,10 @@ var COOKIE_NAMES = {
|
|
|
7604
7604
|
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
7605
7605
|
get OAUTH_PENDING() {
|
|
7606
7606
|
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
7607
|
+
},
|
|
7608
|
+
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
7609
|
+
get OAUTH_CSRF() {
|
|
7610
|
+
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
7607
7611
|
}
|
|
7608
7612
|
};
|
|
7609
7613
|
function parseDuration(duration) {
|
|
@@ -8317,11 +8321,14 @@ function generateNonce() {
|
|
|
8317
8321
|
crypto.getRandomValues(array);
|
|
8318
8322
|
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
8319
8323
|
}
|
|
8324
|
+
function generateOAuthNonce() {
|
|
8325
|
+
return generateNonce();
|
|
8326
|
+
}
|
|
8320
8327
|
async function createOAuthState(params) {
|
|
8321
8328
|
const key = await getStateKey();
|
|
8322
8329
|
const state = {
|
|
8323
8330
|
returnUrl: params.returnUrl,
|
|
8324
|
-
nonce: generateNonce(),
|
|
8331
|
+
nonce: params.nonce ?? generateNonce(),
|
|
8325
8332
|
provider: params.provider,
|
|
8326
8333
|
publicKey: params.publicKey,
|
|
8327
8334
|
keyId: params.keyId,
|
|
@@ -8539,7 +8546,7 @@ function tokenExpiryDate(expiresIn) {
|
|
|
8539
8546
|
return new Date(Date.now() + expiresIn * 1e3);
|
|
8540
8547
|
}
|
|
8541
8548
|
async function oauthStartService(params) {
|
|
8542
|
-
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
8549
|
+
const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata, nonce } = params;
|
|
8543
8550
|
const oauthProvider = requireEnabledProvider(provider);
|
|
8544
8551
|
const state = await createOAuthState({
|
|
8545
8552
|
provider,
|
|
@@ -8548,13 +8555,19 @@ async function oauthStartService(params) {
|
|
|
8548
8555
|
keyId,
|
|
8549
8556
|
fingerprint,
|
|
8550
8557
|
algorithm,
|
|
8551
|
-
metadata
|
|
8558
|
+
metadata,
|
|
8559
|
+
nonce
|
|
8552
8560
|
});
|
|
8553
8561
|
return { authUrl: oauthProvider.getAuthUrl(state) };
|
|
8554
8562
|
}
|
|
8555
8563
|
async function oauthCallbackService(params) {
|
|
8556
|
-
const { provider, code, state } = params;
|
|
8564
|
+
const { provider, code, state, expectedNonce } = params;
|
|
8557
8565
|
const stateData = await verifyOAuthState(state);
|
|
8566
|
+
if (!expectedNonce || stateData.nonce !== expectedNonce) {
|
|
8567
|
+
throw new ValidationError5({
|
|
8568
|
+
message: "OAuth state validation failed"
|
|
8569
|
+
});
|
|
8570
|
+
}
|
|
8558
8571
|
if (stateData.provider !== provider) {
|
|
8559
8572
|
throw new ValidationError5({
|
|
8560
8573
|
message: "OAuth state provider mismatch"
|
|
@@ -8764,7 +8777,56 @@ async function persistNativeLogin(identity, params) {
|
|
|
8764
8777
|
// src/server/routes/auth/index.ts
|
|
8765
8778
|
init_esm();
|
|
8766
8779
|
import { Transactional } from "@spfn/core/db";
|
|
8767
|
-
import {
|
|
8780
|
+
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
8781
|
+
|
|
8782
|
+
// src/server/lib/rate-limit-keys.ts
|
|
8783
|
+
import { getClientIp } from "@spfn/core/middleware";
|
|
8784
|
+
async function readJsonBody(c) {
|
|
8785
|
+
try {
|
|
8786
|
+
return await c.req.json();
|
|
8787
|
+
} catch {
|
|
8788
|
+
return {};
|
|
8789
|
+
}
|
|
8790
|
+
}
|
|
8791
|
+
function accountKey(body) {
|
|
8792
|
+
if (typeof body.email === "string" && body.email.trim()) {
|
|
8793
|
+
return `email:${body.email.trim().toLowerCase()}`;
|
|
8794
|
+
}
|
|
8795
|
+
if (typeof body.phone === "string" && body.phone.trim()) {
|
|
8796
|
+
return `phone:${body.phone.trim()}`;
|
|
8797
|
+
}
|
|
8798
|
+
return void 0;
|
|
8799
|
+
}
|
|
8800
|
+
function targetKey(body) {
|
|
8801
|
+
if (typeof body.target !== "string" || !body.target.trim()) {
|
|
8802
|
+
return void 0;
|
|
8803
|
+
}
|
|
8804
|
+
const type = typeof body.targetType === "string" ? body.targetType : "target";
|
|
8805
|
+
const value = type === "email" ? body.target.trim().toLowerCase() : body.target.trim();
|
|
8806
|
+
return `${type}:${value}`;
|
|
8807
|
+
}
|
|
8808
|
+
function byIpAndAccount(options = {}) {
|
|
8809
|
+
return async (c) => {
|
|
8810
|
+
const body = await readJsonBody(c);
|
|
8811
|
+
const account = accountKey(body);
|
|
8812
|
+
return [
|
|
8813
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
8814
|
+
account ? `acct:${account}` : void 0
|
|
8815
|
+
];
|
|
8816
|
+
};
|
|
8817
|
+
}
|
|
8818
|
+
function byIpAndTarget(options = {}) {
|
|
8819
|
+
return async (c) => {
|
|
8820
|
+
const body = await readJsonBody(c);
|
|
8821
|
+
const target = targetKey(body);
|
|
8822
|
+
return [
|
|
8823
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
8824
|
+
target ? `tgt:${target}` : void 0
|
|
8825
|
+
];
|
|
8826
|
+
};
|
|
8827
|
+
}
|
|
8828
|
+
|
|
8829
|
+
// src/server/routes/auth/index.ts
|
|
8768
8830
|
import { defineRouter, route } from "@spfn/core/route";
|
|
8769
8831
|
var sendVerificationCode = route.post("/_auth/codes").input({
|
|
8770
8832
|
body: Type.Object({
|
|
@@ -8774,7 +8836,7 @@ var sendVerificationCode = route.post("/_auth/codes").input({
|
|
|
8774
8836
|
targetType: TargetTypeSchema,
|
|
8775
8837
|
purpose: VerificationPurposeSchema
|
|
8776
8838
|
})
|
|
8777
|
-
}).use([
|
|
8839
|
+
}).use([rateLimitPolicy("auth-code-send", { limit: 5, windowMs: 6e4, by: byIpAndTarget({ ipLimit: 20 }) })]).skip(["auth"]).handler(async (c) => {
|
|
8778
8840
|
const { body } = await c.data();
|
|
8779
8841
|
return await sendVerificationCodeService(body);
|
|
8780
8842
|
});
|
|
@@ -8792,7 +8854,7 @@ var verifyCode = route.post("/_auth/codes/verify").input({
|
|
|
8792
8854
|
}),
|
|
8793
8855
|
purpose: VerificationPurposeSchema
|
|
8794
8856
|
})
|
|
8795
|
-
}).use([
|
|
8857
|
+
}).use([rateLimitPolicy("auth-code-verify", { limit: 10, windowMs: 6e4, by: byIpAndTarget({ ipLimit: 50 }) })]).skip(["auth"]).handler(async (c) => {
|
|
8796
8858
|
const { body } = await c.data();
|
|
8797
8859
|
return await verifyCodeService(body);
|
|
8798
8860
|
});
|
|
@@ -8819,7 +8881,7 @@ var register = route.post("/_auth/register").input({
|
|
|
8819
8881
|
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
8820
8882
|
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
|
|
8821
8883
|
})
|
|
8822
|
-
}).use([Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
8884
|
+
}).use([rateLimitPolicy("auth-register", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
8823
8885
|
const { body } = await c.data();
|
|
8824
8886
|
return await registerService(body);
|
|
8825
8887
|
});
|
|
@@ -8844,7 +8906,7 @@ var login = route.post("/_auth/login").input({
|
|
|
8844
8906
|
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
8845
8907
|
oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" }))
|
|
8846
8908
|
})
|
|
8847
|
-
}).use([
|
|
8909
|
+
}).use([rateLimitPolicy("auth-login", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
8848
8910
|
const { body } = await c.data();
|
|
8849
8911
|
return await loginService(body);
|
|
8850
8912
|
});
|
|
@@ -8884,7 +8946,7 @@ var changePassword = route.put("/_auth/password").input({
|
|
|
8884
8946
|
})),
|
|
8885
8947
|
newPassword: PasswordSchema
|
|
8886
8948
|
})
|
|
8887
|
-
}).handler(async (c) => {
|
|
8949
|
+
}).use([rateLimitPolicy("auth-password-change", { limit: 10, windowMs: 6e4 })]).handler(async (c) => {
|
|
8888
8950
|
const { body } = await c.data();
|
|
8889
8951
|
const user = getUser(c);
|
|
8890
8952
|
await changePasswordService({
|
|
@@ -9244,6 +9306,7 @@ function extractOTTHeader(header) {
|
|
|
9244
9306
|
init_types();
|
|
9245
9307
|
init_esm();
|
|
9246
9308
|
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
9309
|
+
import { rateLimitPolicy as rateLimitPolicy2 } from "@spfn/core/middleware";
|
|
9247
9310
|
import { defineRouter as defineRouter2, route as route2 } from "@spfn/core/route";
|
|
9248
9311
|
var INVITATION_STATUSES2 = ["pending", "accepted", "expired", "cancelled"];
|
|
9249
9312
|
var getInvitation = route2.get("/_auth/invitations/:token").input({
|
|
@@ -9253,7 +9316,7 @@ var getInvitation = route2.get("/_auth/invitations/:token").input({
|
|
|
9253
9316
|
description: "Invitation token (UUID v4)"
|
|
9254
9317
|
})
|
|
9255
9318
|
})
|
|
9256
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9319
|
+
}).use([rateLimitPolicy2("auth-invitation-lookup", { limit: 30, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9257
9320
|
const { params } = await c.data();
|
|
9258
9321
|
const token = params.token;
|
|
9259
9322
|
const validation = await validateInvitation(token);
|
|
@@ -9292,7 +9355,7 @@ var acceptInvitation2 = route2.post("/_auth/invitations/accept").input({
|
|
|
9292
9355
|
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
9293
9356
|
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
|
|
9294
9357
|
})
|
|
9295
|
-
}).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
9358
|
+
}).use([rateLimitPolicy2("auth-invitation-accept", { limit: 10, windowMs: 6e4 }), Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
9296
9359
|
const { body } = await c.data();
|
|
9297
9360
|
return await acceptInvitation({
|
|
9298
9361
|
token: body.token,
|
|
@@ -9442,6 +9505,7 @@ var invitationRouter = defineRouter2({
|
|
|
9442
9505
|
|
|
9443
9506
|
// src/server/routes/users/index.ts
|
|
9444
9507
|
init_esm();
|
|
9508
|
+
import { rateLimitPolicy as rateLimitPolicy3 } from "@spfn/core/middleware";
|
|
9445
9509
|
import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
|
|
9446
9510
|
var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
|
|
9447
9511
|
const { userId } = getAuth(c);
|
|
@@ -9473,7 +9537,7 @@ var checkUsername = route3.get("/_auth/users/username/check").input({
|
|
|
9473
9537
|
query: Type.Object({
|
|
9474
9538
|
username: Type.String({ minLength: 1 })
|
|
9475
9539
|
})
|
|
9476
|
-
}).handler(async (c) => {
|
|
9540
|
+
}).use([rateLimitPolicy3("auth-username-check", { limit: 30, windowMs: 6e4 })]).handler(async (c) => {
|
|
9477
9541
|
const { query } = await c.data();
|
|
9478
9542
|
return { available: await checkUsernameAvailableService(query.username) };
|
|
9479
9543
|
});
|
|
@@ -9508,10 +9572,197 @@ var userRouter = defineRouter3({
|
|
|
9508
9572
|
|
|
9509
9573
|
// src/server/routes/oauth/index.ts
|
|
9510
9574
|
init_esm();
|
|
9575
|
+
|
|
9576
|
+
// ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/url.js
|
|
9577
|
+
var tryDecode = (str, decoder) => {
|
|
9578
|
+
try {
|
|
9579
|
+
return decoder(str);
|
|
9580
|
+
} catch {
|
|
9581
|
+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
|
|
9582
|
+
try {
|
|
9583
|
+
return decoder(match);
|
|
9584
|
+
} catch {
|
|
9585
|
+
return match;
|
|
9586
|
+
}
|
|
9587
|
+
});
|
|
9588
|
+
}
|
|
9589
|
+
};
|
|
9590
|
+
var decodeURIComponent_ = decodeURIComponent;
|
|
9591
|
+
|
|
9592
|
+
// ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/cookie.js
|
|
9593
|
+
var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
|
|
9594
|
+
var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
|
|
9595
|
+
var trimCookieWhitespace = (value) => {
|
|
9596
|
+
let start = 0;
|
|
9597
|
+
let end = value.length;
|
|
9598
|
+
while (start < end) {
|
|
9599
|
+
const charCode = value.charCodeAt(start);
|
|
9600
|
+
if (charCode !== 32 && charCode !== 9) {
|
|
9601
|
+
break;
|
|
9602
|
+
}
|
|
9603
|
+
start++;
|
|
9604
|
+
}
|
|
9605
|
+
while (end > start) {
|
|
9606
|
+
const charCode = value.charCodeAt(end - 1);
|
|
9607
|
+
if (charCode !== 32 && charCode !== 9) {
|
|
9608
|
+
break;
|
|
9609
|
+
}
|
|
9610
|
+
end--;
|
|
9611
|
+
}
|
|
9612
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
9613
|
+
};
|
|
9614
|
+
var parse = (cookie, name) => {
|
|
9615
|
+
if (name && cookie.indexOf(name) === -1) {
|
|
9616
|
+
return {};
|
|
9617
|
+
}
|
|
9618
|
+
const pairs = cookie.split(";");
|
|
9619
|
+
const parsedCookie = /* @__PURE__ */ Object.create(null);
|
|
9620
|
+
for (const pairStr of pairs) {
|
|
9621
|
+
const valueStartPos = pairStr.indexOf("=");
|
|
9622
|
+
if (valueStartPos === -1) {
|
|
9623
|
+
continue;
|
|
9624
|
+
}
|
|
9625
|
+
const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
|
|
9626
|
+
if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) {
|
|
9627
|
+
continue;
|
|
9628
|
+
}
|
|
9629
|
+
let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
|
|
9630
|
+
if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
|
|
9631
|
+
cookieValue = cookieValue.slice(1, -1);
|
|
9632
|
+
}
|
|
9633
|
+
if (validCookieValueRegEx.test(cookieValue)) {
|
|
9634
|
+
parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
|
|
9635
|
+
if (name) {
|
|
9636
|
+
break;
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
9640
|
+
return parsedCookie;
|
|
9641
|
+
};
|
|
9642
|
+
var _serialize = (name, value, opt = {}) => {
|
|
9643
|
+
if (!validCookieNameRegEx.test(name)) {
|
|
9644
|
+
throw new Error("Invalid cookie name");
|
|
9645
|
+
}
|
|
9646
|
+
let cookie = `${name}=${value}`;
|
|
9647
|
+
if (name.startsWith("__Secure-") && !opt.secure) {
|
|
9648
|
+
throw new Error("__Secure- Cookie must have Secure attributes");
|
|
9649
|
+
}
|
|
9650
|
+
if (name.startsWith("__Host-")) {
|
|
9651
|
+
if (!opt.secure) {
|
|
9652
|
+
throw new Error("__Host- Cookie must have Secure attributes");
|
|
9653
|
+
}
|
|
9654
|
+
if (opt.path !== "/") {
|
|
9655
|
+
throw new Error('__Host- Cookie must have Path attributes with "/"');
|
|
9656
|
+
}
|
|
9657
|
+
if (opt.domain) {
|
|
9658
|
+
throw new Error("__Host- Cookie must not have Domain attributes");
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
for (const key of ["domain", "path", "sameSite", "priority"]) {
|
|
9662
|
+
if (opt[key] && /[;\r\n]/.test(opt[key])) {
|
|
9663
|
+
throw new Error(`${key} must not contain ";", "\\r", or "\\n"`);
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
|
|
9667
|
+
if (opt.maxAge > 3456e4) {
|
|
9668
|
+
throw new Error(
|
|
9669
|
+
"Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."
|
|
9670
|
+
);
|
|
9671
|
+
}
|
|
9672
|
+
cookie += `; Max-Age=${opt.maxAge | 0}`;
|
|
9673
|
+
}
|
|
9674
|
+
if (opt.domain && opt.prefix !== "host") {
|
|
9675
|
+
cookie += `; Domain=${opt.domain}`;
|
|
9676
|
+
}
|
|
9677
|
+
if (opt.path) {
|
|
9678
|
+
cookie += `; Path=${opt.path}`;
|
|
9679
|
+
}
|
|
9680
|
+
if (opt.expires) {
|
|
9681
|
+
if (opt.expires.getTime() - Date.now() > 3456e7) {
|
|
9682
|
+
throw new Error(
|
|
9683
|
+
"Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."
|
|
9684
|
+
);
|
|
9685
|
+
}
|
|
9686
|
+
cookie += `; Expires=${opt.expires.toUTCString()}`;
|
|
9687
|
+
}
|
|
9688
|
+
if (opt.httpOnly) {
|
|
9689
|
+
cookie += "; HttpOnly";
|
|
9690
|
+
}
|
|
9691
|
+
if (opt.secure) {
|
|
9692
|
+
cookie += "; Secure";
|
|
9693
|
+
}
|
|
9694
|
+
if (opt.sameSite) {
|
|
9695
|
+
cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
|
|
9696
|
+
}
|
|
9697
|
+
if (opt.priority) {
|
|
9698
|
+
cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`;
|
|
9699
|
+
}
|
|
9700
|
+
if (opt.partitioned) {
|
|
9701
|
+
if (!opt.secure) {
|
|
9702
|
+
throw new Error("Partitioned Cookie must have Secure attributes");
|
|
9703
|
+
}
|
|
9704
|
+
cookie += "; Partitioned";
|
|
9705
|
+
}
|
|
9706
|
+
return cookie;
|
|
9707
|
+
};
|
|
9708
|
+
var serialize = (name, value, opt) => {
|
|
9709
|
+
value = encodeURIComponent(value);
|
|
9710
|
+
return _serialize(name, value, opt);
|
|
9711
|
+
};
|
|
9712
|
+
|
|
9713
|
+
// ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/helper/cookie/index.js
|
|
9714
|
+
var getCookie = (c, key, prefix) => {
|
|
9715
|
+
const cookie = c.req.raw.headers.get("Cookie");
|
|
9716
|
+
if (typeof key === "string") {
|
|
9717
|
+
if (!cookie) {
|
|
9718
|
+
return void 0;
|
|
9719
|
+
}
|
|
9720
|
+
let finalKey = key;
|
|
9721
|
+
if (prefix === "secure") {
|
|
9722
|
+
finalKey = "__Secure-" + key;
|
|
9723
|
+
} else if (prefix === "host") {
|
|
9724
|
+
finalKey = "__Host-" + key;
|
|
9725
|
+
}
|
|
9726
|
+
const obj2 = parse(cookie, finalKey);
|
|
9727
|
+
return obj2[finalKey];
|
|
9728
|
+
}
|
|
9729
|
+
if (!cookie) {
|
|
9730
|
+
return {};
|
|
9731
|
+
}
|
|
9732
|
+
const obj = parse(cookie);
|
|
9733
|
+
return obj;
|
|
9734
|
+
};
|
|
9735
|
+
var generateCookie = (name, value, opt) => {
|
|
9736
|
+
let cookie;
|
|
9737
|
+
if (opt?.prefix === "secure") {
|
|
9738
|
+
cookie = serialize("__Secure-" + name, value, { path: "/", ...opt, secure: true });
|
|
9739
|
+
} else if (opt?.prefix === "host") {
|
|
9740
|
+
cookie = serialize("__Host-" + name, value, {
|
|
9741
|
+
...opt,
|
|
9742
|
+
path: "/",
|
|
9743
|
+
secure: true,
|
|
9744
|
+
domain: void 0
|
|
9745
|
+
});
|
|
9746
|
+
} else {
|
|
9747
|
+
cookie = serialize(name, value, { path: "/", ...opt });
|
|
9748
|
+
}
|
|
9749
|
+
return cookie;
|
|
9750
|
+
};
|
|
9751
|
+
var setCookie = (c, name, value, opt) => {
|
|
9752
|
+
const cookie = generateCookie(name, value, opt);
|
|
9753
|
+
c.header("Set-Cookie", cookie, { append: true });
|
|
9754
|
+
};
|
|
9755
|
+
var deleteCookie = (c, name, opt) => {
|
|
9756
|
+
const deletedCookie = getCookie(c, name, opt?.prefix);
|
|
9757
|
+
setCookie(c, name, "", { ...opt, maxAge: 0 });
|
|
9758
|
+
return deletedCookie;
|
|
9759
|
+
};
|
|
9760
|
+
|
|
9761
|
+
// src/server/routes/oauth/index.ts
|
|
9511
9762
|
init_types();
|
|
9512
9763
|
import { Transactional as Transactional3 } from "@spfn/core/db";
|
|
9513
9764
|
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
9514
|
-
import {
|
|
9765
|
+
import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
|
|
9515
9766
|
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
9516
9767
|
var providerParams = Type.Object({
|
|
9517
9768
|
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
@@ -9524,7 +9775,7 @@ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
|
9524
9775
|
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
9525
9776
|
})
|
|
9526
9777
|
})
|
|
9527
|
-
}).use([
|
|
9778
|
+
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9528
9779
|
const { query } = await c.data();
|
|
9529
9780
|
if (!isGoogleOAuthEnabled()) {
|
|
9530
9781
|
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
@@ -9547,7 +9798,7 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
|
9547
9798
|
description: "Error description from Google"
|
|
9548
9799
|
}))
|
|
9549
9800
|
})
|
|
9550
|
-
}).use([Transactional3()]).skip(["auth"]).handler(async (c) => {
|
|
9801
|
+
}).use([rateLimitPolicy4("oauth-callback", { limit: 30, windowMs: 6e4 }), Transactional3()]).skip(["auth"]).handler(async (c) => {
|
|
9551
9802
|
const { query } = await c.data();
|
|
9552
9803
|
if (query.error) {
|
|
9553
9804
|
const errorMessage = query.error_description || query.error;
|
|
@@ -9556,11 +9807,14 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
|
|
|
9556
9807
|
if (!query.code || !query.state) {
|
|
9557
9808
|
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9558
9809
|
}
|
|
9810
|
+
const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
|
|
9811
|
+
deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
|
|
9559
9812
|
try {
|
|
9560
9813
|
const result = await oauthCallbackService({
|
|
9561
9814
|
provider: "google",
|
|
9562
9815
|
code: query.code,
|
|
9563
|
-
state: query.state
|
|
9816
|
+
state: query.state,
|
|
9817
|
+
expectedNonce
|
|
9564
9818
|
});
|
|
9565
9819
|
return c.redirect(result.redirectUrl);
|
|
9566
9820
|
} catch (err) {
|
|
@@ -9592,9 +9846,17 @@ var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
|
9592
9846
|
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
9593
9847
|
}))
|
|
9594
9848
|
})
|
|
9595
|
-
}).use([
|
|
9849
|
+
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9596
9850
|
const { body } = await c.data();
|
|
9597
|
-
const
|
|
9851
|
+
const nonce = generateOAuthNonce();
|
|
9852
|
+
setCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, nonce, {
|
|
9853
|
+
httpOnly: true,
|
|
9854
|
+
secure: process.env.NODE_ENV === "production",
|
|
9855
|
+
sameSite: "Lax",
|
|
9856
|
+
maxAge: 600,
|
|
9857
|
+
path: "/"
|
|
9858
|
+
});
|
|
9859
|
+
const result = await oauthStartService({ ...body, nonce });
|
|
9598
9860
|
return result;
|
|
9599
9861
|
});
|
|
9600
9862
|
var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
@@ -9611,7 +9873,7 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
|
9611
9873
|
description: "Encrypted OAuth state (injected by interceptor)"
|
|
9612
9874
|
}))
|
|
9613
9875
|
})
|
|
9614
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9876
|
+
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9615
9877
|
const { body } = await c.data();
|
|
9616
9878
|
if (!isGoogleOAuthEnabled()) {
|
|
9617
9879
|
throw new ValidationError7({ message: "Google OAuth is not configured" });
|
|
@@ -9629,7 +9891,7 @@ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
|
|
|
9629
9891
|
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
9630
9892
|
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
9631
9893
|
})
|
|
9632
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9894
|
+
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9633
9895
|
const { body } = await c.data();
|
|
9634
9896
|
return {
|
|
9635
9897
|
success: true,
|
|
@@ -9645,7 +9907,7 @@ var oauthProviderStart = route4.get("/_auth/oauth/:provider").input({
|
|
|
9645
9907
|
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
9646
9908
|
})
|
|
9647
9909
|
})
|
|
9648
|
-
}).use([
|
|
9910
|
+
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9649
9911
|
const { params, query } = await c.data();
|
|
9650
9912
|
const provider = getOAuthProvider(params.provider);
|
|
9651
9913
|
if (!provider?.isEnabled()) {
|
|
@@ -9669,7 +9931,7 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
|
|
|
9669
9931
|
description: "Error description from provider"
|
|
9670
9932
|
}))
|
|
9671
9933
|
})
|
|
9672
|
-
}).use([Transactional3()]).skip(["auth"]).handler(async (c) => {
|
|
9934
|
+
}).use([rateLimitPolicy4("oauth-callback", { limit: 30, windowMs: 6e4 }), Transactional3()]).skip(["auth"]).handler(async (c) => {
|
|
9673
9935
|
const { params, query } = await c.data();
|
|
9674
9936
|
if (query.error) {
|
|
9675
9937
|
const errorMessage = query.error_description || query.error;
|
|
@@ -9678,11 +9940,14 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
|
|
|
9678
9940
|
if (!query.code || !query.state) {
|
|
9679
9941
|
return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
|
|
9680
9942
|
}
|
|
9943
|
+
const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
|
|
9944
|
+
deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
|
|
9681
9945
|
try {
|
|
9682
9946
|
const result = await oauthCallbackService({
|
|
9683
9947
|
provider: params.provider,
|
|
9684
9948
|
code: query.code,
|
|
9685
|
-
state: query.state
|
|
9949
|
+
state: query.state,
|
|
9950
|
+
expectedNonce
|
|
9686
9951
|
});
|
|
9687
9952
|
return c.redirect(result.redirectUrl);
|
|
9688
9953
|
} catch (err) {
|
|
@@ -9700,7 +9965,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
|
|
|
9700
9965
|
description: "Encrypted OAuth state (injected by interceptor)"
|
|
9701
9966
|
}))
|
|
9702
9967
|
})
|
|
9703
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9968
|
+
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9704
9969
|
const { params, body } = await c.data();
|
|
9705
9970
|
const provider = requireEnabledProvider(params.provider);
|
|
9706
9971
|
if (!body.state) {
|
|
@@ -9730,7 +9995,7 @@ var oauthNative = route4.post("/_auth/oauth/:provider/native").input({
|
|
|
9730
9995
|
description: "Custom metadata passed to auth events (e.g. referral code, UTM params)"
|
|
9731
9996
|
}))
|
|
9732
9997
|
})
|
|
9733
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9998
|
+
}).use([rateLimitPolicy4("oauth-native", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9734
9999
|
const { params, body } = await c.data();
|
|
9735
10000
|
return await oauthNativeService({ provider: params.provider, ...body });
|
|
9736
10001
|
});
|
|
@@ -10264,6 +10529,7 @@ export {
|
|
|
10264
10529
|
generateKeyPair,
|
|
10265
10530
|
generateKeyPairES256,
|
|
10266
10531
|
generateKeyPairRS256,
|
|
10532
|
+
generateOAuthNonce,
|
|
10267
10533
|
generateToken,
|
|
10268
10534
|
getAllRoles,
|
|
10269
10535
|
getAuth,
|