@spfn/auth 0.3.0-beta.23 → 0.3.0-beta.24
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 +165 -2
- package/dist/client-proof.js +13 -5
- package/dist/client-proof.js.map +1 -1
- package/dist/client.d.ts +52 -1
- package/dist/client.js +40 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +82 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/errors.d.ts +113 -3
- package/dist/errors.js +68 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +38 -5
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-CaEFq61K.d.ts → machine-principals-ZJd9anVT.d.ts} +1667 -900
- package/dist/nextjs/api.js +156 -6
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.d.ts +42 -24
- package/dist/nextjs/server.js +72 -5
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +448 -525
- package/dist/server.js +1389 -653
- package/dist/server.js.map +1 -1
- package/dist/{session-Dfwu5g2W.d.ts → session-BbhAGZtA.d.ts} +57 -1
- package/dist/{types-DYyhze28.d.ts → types-CTdoTOxM.d.ts} +24 -1
- package/migrations/20260918184037_happy_mordo/migration.sql +4 -0
- package/migrations/20260918184037_happy_mordo/snapshot.json +6000 -0
- package/migrations/20260918184152_dear_rictor/migration.sql +3 -0
- package/migrations/20260918184152_dear_rictor/snapshot.json +6039 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -4455,13 +4455,14 @@ var init_esm = __esm({
|
|
|
4455
4455
|
});
|
|
4456
4456
|
|
|
4457
4457
|
// src/server/types.ts
|
|
4458
|
-
var KEY_ALGORITHM, KEY_PLATFORM, KEY_DEVICE_NAME_MAX_LENGTH, INVITATION_STATUSES, USER_STATUSES, SOCIAL_PROVIDERS, ACCOUNT_DELETION_REQUEST_STATUSES, ACCOUNT_DELETION_REQUESTED_BY, PURGE_STRATEGIES;
|
|
4458
|
+
var KEY_ALGORITHM, KEY_PLATFORM, KEY_DEVICE_NAME_MAX_LENGTH, SESSION_BINDINGS, INVITATION_STATUSES, USER_STATUSES, SOCIAL_PROVIDERS, ACCOUNT_DELETION_REQUEST_STATUSES, ACCOUNT_DELETION_REQUESTED_BY, PURGE_STRATEGIES;
|
|
4459
4459
|
var init_types = __esm({
|
|
4460
4460
|
"src/server/types.ts"() {
|
|
4461
4461
|
"use strict";
|
|
4462
4462
|
KEY_ALGORITHM = ["ES256", "RS256"];
|
|
4463
4463
|
KEY_PLATFORM = ["ios", "android", "web", "desktop"];
|
|
4464
4464
|
KEY_DEVICE_NAME_MAX_LENGTH = 64;
|
|
4465
|
+
SESSION_BINDINGS = ["none", "passkey"];
|
|
4465
4466
|
INVITATION_STATUSES = ["pending", "accepted", "expired", "cancelled"];
|
|
4466
4467
|
USER_STATUSES = ["active", "inactive", "suspended", "pending_deletion", "deleted"];
|
|
4467
4468
|
SOCIAL_PROVIDERS = ["google", "apple", "github", "kakao", "naver", "superself"];
|
|
@@ -4523,7 +4524,15 @@ var init_schema3 = __esm({
|
|
|
4523
4524
|
publicId: Type.String(),
|
|
4524
4525
|
email: Type.Optional(Type.String()),
|
|
4525
4526
|
phone: Type.Optional(Type.String()),
|
|
4526
|
-
passwordChangeRequired: Type.Boolean()
|
|
4527
|
+
passwordChangeRequired: Type.Boolean(),
|
|
4528
|
+
// The approved branch is a `LoginResult` spread whole, so it carries the
|
|
4529
|
+
// binding fields a sign-in can carry (#97). In practice it never does: a
|
|
4530
|
+
// waiting device polls the backend directly, and a key is bound only on a
|
|
4531
|
+
// request that reached it through the trusted Next.js proxy. Declared
|
|
4532
|
+
// anyway, because the branch is whatever the login answered and the two
|
|
4533
|
+
// must not be able to drift apart.
|
|
4534
|
+
sessionBinding: Type.Optional(Type.Union(SESSION_BINDINGS.map((mode) => Type.Literal(mode)))),
|
|
4535
|
+
keyExpiresAtMillis: Type.Optional(Type.Integer())
|
|
4527
4536
|
})
|
|
4528
4537
|
], {
|
|
4529
4538
|
description: "Pending, or the login the approval produced"
|
|
@@ -4685,6 +4694,20 @@ var init_users = __esm({
|
|
|
4685
4694
|
// it was issued against — a link is dead once any other path has already
|
|
4686
4695
|
// signed every device out
|
|
4687
4696
|
keyEpoch: integer2("key_epoch").notNull().default(0),
|
|
4697
|
+
// Whether this account's web sessions run on a key bound to a passkey
|
|
4698
|
+
// 'none' (default): the key lives 90 days and a copy of the session
|
|
4699
|
+
// cookie signs with it for as long as it lasts — the behaviour every
|
|
4700
|
+
// account had before this column existed
|
|
4701
|
+
// 'passkey': a web session started through the trusted Next.js proxy gets
|
|
4702
|
+
// a key that expires in hours, and only a fresh WebAuthn assertion can
|
|
4703
|
+
// put a new one in the cookie
|
|
4704
|
+
// Opt-in, and only while the account has a live platform passkey to renew
|
|
4705
|
+
// with. Leaving 'passkey' needs a fresh credential, never key age alone
|
|
4706
|
+
sessionBinding: enumText("session_binding", SESSION_BINDINGS).notNull().default("none"),
|
|
4707
|
+
// When the setting above last changed, so an account timeline can say
|
|
4708
|
+
// when the protection was turned on or off
|
|
4709
|
+
// null: never changed — the account has been on the default since it existed
|
|
4710
|
+
sessionBindingChangedAt: utcTimestamp("session_binding_changed_at"),
|
|
4688
4711
|
// Metadata
|
|
4689
4712
|
// Last successful login timestamp
|
|
4690
4713
|
// Used for: security auditing, dormant account detection
|
|
@@ -4849,6 +4872,56 @@ var init_user_public_keys = __esm({
|
|
|
4849
4872
|
// Written once at registration, for the same reason and with the same
|
|
4850
4873
|
// standing as registeredIp above
|
|
4851
4874
|
registeredUserAgent: text4("registered_user_agent"),
|
|
4875
|
+
// Browser family the registering request's user-agent named — one of the
|
|
4876
|
+
// five badges uaFamily() answers with, never the raw string
|
|
4877
|
+
// null: the request sent no user-agent, or the key predates this column
|
|
4878
|
+
// Written once at registration, like the two columns above. The proxy is
|
|
4879
|
+
// what compares a family against a request (the browser's user-agent does
|
|
4880
|
+
// not survive a server-component hop), so this is the displayable record
|
|
4881
|
+
// of where the key came from rather than the value any check reads
|
|
4882
|
+
registeredUaFamily: text4("registered_ua_family"),
|
|
4883
|
+
// Client address this key was last seen signing from, as getClientIp
|
|
4884
|
+
// resolved it on that request
|
|
4885
|
+
// null: that request resolved no address, or the key predates this column
|
|
4886
|
+
//
|
|
4887
|
+
// Unlike registeredIp above, this MOVES: it is overwritten by the same
|
|
4888
|
+
// throttled UPDATE that stamps lastUsedAt, so it is a new class of stored
|
|
4889
|
+
// PII in this table — an address that follows the device around rather
|
|
4890
|
+
// than one captured once. It exists for one purpose: to notice that one
|
|
4891
|
+
// key was used from two addresses inside a short window, which is what
|
|
4892
|
+
// concurrentUseAt records. Nothing else reads it and nothing is refused
|
|
4893
|
+
// by it, because addresses change legitimately all the time.
|
|
4894
|
+
//
|
|
4895
|
+
// It is NOT exposed. `listKeys` returns concurrentUseAt and never this,
|
|
4896
|
+
// so the account surface can say "this device was in two places at once"
|
|
4897
|
+
// without publishing a trail of where. Retention is the row's: it holds
|
|
4898
|
+
// only the most recent observation, is overwritten on the next one, and
|
|
4899
|
+
// goes with the key when the key is deleted.
|
|
4900
|
+
//
|
|
4901
|
+
// The literal string 'unknown' is never stored — getClientIp's fallback
|
|
4902
|
+
// becomes NULL, which reads as "no observation" everywhere it is compared.
|
|
4903
|
+
lastSeenIp: text4("last_seen_ip"),
|
|
4904
|
+
// When lastSeenIp was written, which is the same moment lastUsedAt was
|
|
4905
|
+
// The pair is what makes "within the window" answerable in one statement
|
|
4906
|
+
lastSeenAt: utcTimestamp2("last_seen_at"),
|
|
4907
|
+
// The last time this key was seen from an address different from the one
|
|
4908
|
+
// recorded, within SPFN_AUTH_CONCURRENT_USE_WINDOW_MS of the previous
|
|
4909
|
+
// sighting
|
|
4910
|
+
// null: never observed, which is the ordinary state for every key
|
|
4911
|
+
// Surfaced on listKeys as concurrentUseAtMillis for the owner to act on.
|
|
4912
|
+
// A signal, never a refusal — a phone moving between wifi and cellular
|
|
4913
|
+
// does this several times an hour, and so does a laptop behind a pool of
|
|
4914
|
+
// egress addresses
|
|
4915
|
+
concurrentUseAt: utcTimestamp2("concurrent_use_at"),
|
|
4916
|
+
// Whether this key is bound to a passkey — decided by the server when the
|
|
4917
|
+
// key was registered, from the owner's session_binding setting and
|
|
4918
|
+
// whether the request came through the trusted Next.js proxy
|
|
4919
|
+
// 'none' (default): a 90-day key, the behaviour that predates #97
|
|
4920
|
+
// 'passkey': a short-lived key; only a fresh WebAuthn assertion renews it,
|
|
4921
|
+
// and a login that re-registers it does not extend its expiry
|
|
4922
|
+
// Never read off a request body, and `platform` is not consulted: that
|
|
4923
|
+
// field is display-only and a native client may declare any value it likes
|
|
4924
|
+
binding: enumText2("binding", SESSION_BINDINGS).notNull().default("none"),
|
|
4852
4925
|
// What the client said about itself on the last request signed by this key.
|
|
4853
4926
|
//
|
|
4854
4927
|
// The three come from x-spfn-client-kind, x-spfn-client-version and
|
|
@@ -5274,7 +5347,7 @@ var init_webauthn_challenges = __esm({
|
|
|
5274
5347
|
"use strict";
|
|
5275
5348
|
init_users();
|
|
5276
5349
|
init_schema4();
|
|
5277
|
-
WEBAUTHN_CHALLENGE_KINDS = ["registration", "authentication", "mfa"];
|
|
5350
|
+
WEBAUTHN_CHALLENGE_KINDS = ["registration", "authentication", "mfa", "renewal"];
|
|
5278
5351
|
webauthnChallenges = authSchema.table(
|
|
5279
5352
|
"webauthn_challenges",
|
|
5280
5353
|
{
|
|
@@ -6523,6 +6596,333 @@ var init_users_repository = __esm({
|
|
|
6523
6596
|
}
|
|
6524
6597
|
});
|
|
6525
6598
|
|
|
6599
|
+
// src/server/lib/key-policy.ts
|
|
6600
|
+
function decideKeyBinding(accountBinding, webProxy) {
|
|
6601
|
+
return accountBinding === "passkey" && webProxy === true ? "passkey" : "none";
|
|
6602
|
+
}
|
|
6603
|
+
var KEY_TTL_DAYS, BOUND_KEY_TTL_HOURS, BOUND_KEY_RENEW_GRACE_HOURS, CONCURRENT_USE_WINDOW_MS;
|
|
6604
|
+
var init_key_policy = __esm({
|
|
6605
|
+
"src/server/lib/key-policy.ts"() {
|
|
6606
|
+
"use strict";
|
|
6607
|
+
KEY_TTL_DAYS = 90;
|
|
6608
|
+
BOUND_KEY_TTL_HOURS = 24;
|
|
6609
|
+
BOUND_KEY_RENEW_GRACE_HOURS = 168;
|
|
6610
|
+
CONCURRENT_USE_WINDOW_MS = 3e5;
|
|
6611
|
+
}
|
|
6612
|
+
});
|
|
6613
|
+
|
|
6614
|
+
// src/server/logger.ts
|
|
6615
|
+
import { logger as rootLogger } from "@spfn/core/logger";
|
|
6616
|
+
var authLogger;
|
|
6617
|
+
var init_logger = __esm({
|
|
6618
|
+
"src/server/logger.ts"() {
|
|
6619
|
+
"use strict";
|
|
6620
|
+
authLogger = {
|
|
6621
|
+
plugin: rootLogger.child("@spfn/auth:plugin"),
|
|
6622
|
+
middleware: rootLogger.child("@spfn/auth:middleware"),
|
|
6623
|
+
interceptor: {
|
|
6624
|
+
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
6625
|
+
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
6626
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
6627
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth"),
|
|
6628
|
+
csrf: rootLogger.child("@spfn/auth:interceptor:csrf")
|
|
6629
|
+
},
|
|
6630
|
+
session: rootLogger.child("@spfn/auth:session"),
|
|
6631
|
+
service: rootLogger.child("@spfn/auth:service"),
|
|
6632
|
+
setup: rootLogger.child("@spfn/auth:setup"),
|
|
6633
|
+
email: rootLogger.child("@spfn/auth:email"),
|
|
6634
|
+
sms: rootLogger.child("@spfn/auth:sms")
|
|
6635
|
+
};
|
|
6636
|
+
}
|
|
6637
|
+
});
|
|
6638
|
+
|
|
6639
|
+
// src/server/lib/config.ts
|
|
6640
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6641
|
+
import { PasskeyConfigError } from "@spfn/auth/errors";
|
|
6642
|
+
function getCookieSuffix() {
|
|
6643
|
+
const port = process.env.SPFN_PORT;
|
|
6644
|
+
return port ? `_${port}` : "";
|
|
6645
|
+
}
|
|
6646
|
+
function matchOAuthCsrfCookies(cookies) {
|
|
6647
|
+
return Object.entries(cookies).filter(([name]) => /^spfn_oauth_csrf(_\d+)?$/.test(name)).map(([name, value]) => ({ name, value }));
|
|
6648
|
+
}
|
|
6649
|
+
function parseDuration(duration) {
|
|
6650
|
+
if (typeof duration === "number") {
|
|
6651
|
+
return duration;
|
|
6652
|
+
}
|
|
6653
|
+
const match = duration.match(/^(\d+)([dhms]?)$/);
|
|
6654
|
+
if (!match) {
|
|
6655
|
+
throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);
|
|
6656
|
+
}
|
|
6657
|
+
const value = parseInt(match[1], 10);
|
|
6658
|
+
const unit = match[2] || "s";
|
|
6659
|
+
switch (unit) {
|
|
6660
|
+
case "d":
|
|
6661
|
+
return value * 24 * 60 * 60;
|
|
6662
|
+
case "h":
|
|
6663
|
+
return value * 60 * 60;
|
|
6664
|
+
case "m":
|
|
6665
|
+
return value * 60;
|
|
6666
|
+
case "s":
|
|
6667
|
+
return value;
|
|
6668
|
+
default:
|
|
6669
|
+
throw new Error(`Unknown duration unit: ${unit}`);
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
function configureAuth(config4) {
|
|
6673
|
+
globalConfig = {
|
|
6674
|
+
...globalConfig,
|
|
6675
|
+
...config4
|
|
6676
|
+
};
|
|
6677
|
+
}
|
|
6678
|
+
function getAuthConfig() {
|
|
6679
|
+
return { ...globalConfig };
|
|
6680
|
+
}
|
|
6681
|
+
async function runBeforeRegister(context) {
|
|
6682
|
+
const { beforeRegister } = globalConfig;
|
|
6683
|
+
if (beforeRegister) {
|
|
6684
|
+
await beforeRegister({ ...context, email: normalizeOptionalEmail(context.email) });
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
function getSessionTtl(override) {
|
|
6688
|
+
if (override !== void 0) {
|
|
6689
|
+
return parseDuration(override);
|
|
6690
|
+
}
|
|
6691
|
+
if (globalConfig.sessionTtl !== void 0) {
|
|
6692
|
+
return parseDuration(globalConfig.sessionTtl);
|
|
6693
|
+
}
|
|
6694
|
+
const envTtl = env3.SPFN_AUTH_SESSION_TTL;
|
|
6695
|
+
if (envTtl) {
|
|
6696
|
+
return parseDuration(envTtl);
|
|
6697
|
+
}
|
|
6698
|
+
return 7 * 24 * 60 * 60;
|
|
6699
|
+
}
|
|
6700
|
+
function getCsrfMode() {
|
|
6701
|
+
const configured2 = globalConfig.csrf?.mode ?? env3.SPFN_AUTH_CSRF;
|
|
6702
|
+
if (!configured2) {
|
|
6703
|
+
return "warn";
|
|
6704
|
+
}
|
|
6705
|
+
const normalized = String(configured2).trim().toLowerCase();
|
|
6706
|
+
if (!CSRF_MODES.includes(normalized)) {
|
|
6707
|
+
if (!unrecognizedCsrfModeReported) {
|
|
6708
|
+
unrecognizedCsrfModeReported = true;
|
|
6709
|
+
authLogger.interceptor.csrf.error(
|
|
6710
|
+
`Unrecognized CSRF mode "${configured2}" \u2014 expected off | warn | enforce. Enforcing.`
|
|
6711
|
+
);
|
|
6712
|
+
}
|
|
6713
|
+
return "enforce";
|
|
6714
|
+
}
|
|
6715
|
+
return normalized;
|
|
6716
|
+
}
|
|
6717
|
+
function getCsrfExemptPaths() {
|
|
6718
|
+
return [...PACKAGE_CSRF_EXEMPT_PATHS, ...globalConfig.csrf?.exemptPaths ?? []];
|
|
6719
|
+
}
|
|
6720
|
+
function getBoundKeyTtlMs() {
|
|
6721
|
+
return positiveOr(env3.SPFN_AUTH_BOUND_KEY_TTL_HOURS, BOUND_KEY_TTL_HOURS) * 60 * 60 * 1e3;
|
|
6722
|
+
}
|
|
6723
|
+
function getBoundKeyRenewGraceMs() {
|
|
6724
|
+
return positiveOr(env3.SPFN_AUTH_BOUND_KEY_RENEW_GRACE_HOURS, BOUND_KEY_RENEW_GRACE_HOURS) * 60 * 60 * 1e3;
|
|
6725
|
+
}
|
|
6726
|
+
function getConcurrentUseWindowMs() {
|
|
6727
|
+
return positiveOr(env3.SPFN_AUTH_CONCURRENT_USE_WINDOW_MS, CONCURRENT_USE_WINDOW_MS);
|
|
6728
|
+
}
|
|
6729
|
+
function getSessionRenewPath() {
|
|
6730
|
+
return env3.SPFN_AUTH_SESSION_RENEW_PATH?.trim() || DEFAULT_SESSION_RENEW_PATH;
|
|
6731
|
+
}
|
|
6732
|
+
function positiveOr(configured2, fallback) {
|
|
6733
|
+
return Number.isFinite(configured2) && configured2 > 0 ? configured2 : fallback;
|
|
6734
|
+
}
|
|
6735
|
+
function passkeyEnvSource() {
|
|
6736
|
+
return { ...process.env, SPFN_APP_URL: process.env.SPFN_APP_URL || env3.SPFN_APP_URL };
|
|
6737
|
+
}
|
|
6738
|
+
function passkeyAppUrl(env21) {
|
|
6739
|
+
const configured2 = env21.NEXT_PUBLIC_SPFN_APP_URL || env21.SPFN_APP_URL;
|
|
6740
|
+
if (!configured2) {
|
|
6741
|
+
throw new PasskeyConfigError({
|
|
6742
|
+
message: "Passkeys need a relying party ID. Set SPFN_AUTH_PASSKEY_RP_ID, or set NEXT_PUBLIC_SPFN_APP_URL / SPFN_APP_URL to the app origin it should be derived from."
|
|
6743
|
+
});
|
|
6744
|
+
}
|
|
6745
|
+
try {
|
|
6746
|
+
return new URL(configured2);
|
|
6747
|
+
} catch {
|
|
6748
|
+
throw new PasskeyConfigError({
|
|
6749
|
+
message: `Passkeys cannot derive a relying party ID: "${configured2}" is not a URL. Fix NEXT_PUBLIC_SPFN_APP_URL / SPFN_APP_URL, or set SPFN_AUTH_PASSKEY_RP_ID explicitly.`
|
|
6750
|
+
});
|
|
6751
|
+
}
|
|
6752
|
+
}
|
|
6753
|
+
function isLocalhost(hostname) {
|
|
6754
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
6755
|
+
}
|
|
6756
|
+
function isUnderRpId(hostname, rpId) {
|
|
6757
|
+
return hostname === rpId || hostname.endsWith(`.${rpId}`);
|
|
6758
|
+
}
|
|
6759
|
+
function assertOriginServesRpId(origin, rpId) {
|
|
6760
|
+
let url;
|
|
6761
|
+
try {
|
|
6762
|
+
url = new URL(origin);
|
|
6763
|
+
} catch {
|
|
6764
|
+
throw new PasskeyConfigError({
|
|
6765
|
+
message: `SPFN_AUTH_PASSKEY_ORIGINS contains "${origin}", which is not a URL. List full origins, e.g. https://app.example.com.`
|
|
6766
|
+
});
|
|
6767
|
+
}
|
|
6768
|
+
if (url.protocol !== "https:" && !isLocalhost(url.hostname)) {
|
|
6769
|
+
throw new PasskeyConfigError({
|
|
6770
|
+
message: `Passkey origin "${origin}" is not https. WebAuthn runs only in a secure context, and localhost is the only host a browser treats as one over plain http.`
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6773
|
+
if (!isUnderRpId(url.hostname, rpId)) {
|
|
6774
|
+
throw new PasskeyConfigError({
|
|
6775
|
+
message: `Passkey origin "${origin}" is not on relying party ID "${rpId}". Each origin must be that host or a subdomain of it, or the browser refuses the ceremony.`
|
|
6776
|
+
});
|
|
6777
|
+
}
|
|
6778
|
+
}
|
|
6779
|
+
function resolveUserVerification(env21) {
|
|
6780
|
+
const configured2 = env21.SPFN_AUTH_PASSKEY_USER_VERIFICATION;
|
|
6781
|
+
if (!configured2) {
|
|
6782
|
+
return "preferred";
|
|
6783
|
+
}
|
|
6784
|
+
const normalized = configured2.trim().toLowerCase();
|
|
6785
|
+
if (!PASSKEY_USER_VERIFICATIONS.includes(normalized)) {
|
|
6786
|
+
throw new PasskeyConfigError({
|
|
6787
|
+
message: `SPFN_AUTH_PASSKEY_USER_VERIFICATION is "${configured2}" \u2014 expected preferred or required. A passkey is the whole credential here, so an assertion that skipped user verification would sign someone in on an unlocked device alone.`
|
|
6788
|
+
});
|
|
6789
|
+
}
|
|
6790
|
+
return normalized;
|
|
6791
|
+
}
|
|
6792
|
+
function resolvePositiveNumber(env21, variable, fallback) {
|
|
6793
|
+
const configured2 = env21[variable];
|
|
6794
|
+
if (!configured2) {
|
|
6795
|
+
return fallback;
|
|
6796
|
+
}
|
|
6797
|
+
const parsed = Number(configured2);
|
|
6798
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
6799
|
+
throw new PasskeyConfigError({
|
|
6800
|
+
message: `${variable} is "${configured2}" \u2014 expected a positive number.`
|
|
6801
|
+
});
|
|
6802
|
+
}
|
|
6803
|
+
return parsed;
|
|
6804
|
+
}
|
|
6805
|
+
function getPasskeyConfig(env21 = passkeyEnvSource()) {
|
|
6806
|
+
const rpId = env21.SPFN_AUTH_PASSKEY_RP_ID?.trim() || passkeyAppUrl(env21).hostname;
|
|
6807
|
+
const configuredOrigins = env21.SPFN_AUTH_PASSKEY_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
6808
|
+
const origins = configuredOrigins?.length ? configuredOrigins : [passkeyAppUrl(env21).origin];
|
|
6809
|
+
for (const origin of origins) {
|
|
6810
|
+
assertOriginServesRpId(origin, rpId);
|
|
6811
|
+
}
|
|
6812
|
+
return {
|
|
6813
|
+
rpId,
|
|
6814
|
+
rpName: env21.SPFN_AUTH_PASSKEY_RP_NAME?.trim() || rpId,
|
|
6815
|
+
origins,
|
|
6816
|
+
userVerification: resolveUserVerification(env21),
|
|
6817
|
+
challengeTtlMs: resolvePositiveNumber(
|
|
6818
|
+
env21,
|
|
6819
|
+
"SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS",
|
|
6820
|
+
DEFAULT_CHALLENGE_TTL_SECONDS
|
|
6821
|
+
) * 1e3,
|
|
6822
|
+
recentAuthMs: resolvePositiveNumber(
|
|
6823
|
+
env21,
|
|
6824
|
+
"SPFN_AUTH_PASSKEY_RECENT_AUTH_MINUTES",
|
|
6825
|
+
DEFAULT_RECENT_AUTH_MINUTES
|
|
6826
|
+
) * 6e4
|
|
6827
|
+
};
|
|
6828
|
+
}
|
|
6829
|
+
function assertPasskeyConfig(env21 = passkeyEnvSource()) {
|
|
6830
|
+
if (PASSKEY_VARS.some((variable) => env21[variable])) {
|
|
6831
|
+
getPasskeyConfig(env21);
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
try {
|
|
6835
|
+
getPasskeyConfig(env21);
|
|
6836
|
+
} catch (error) {
|
|
6837
|
+
authLogger.service.info(
|
|
6838
|
+
`Passkeys cannot be served with the configuration derived from the app URL, and no SPFN_AUTH_PASSKEY_* variable is set, so boot continues. ${error.message}`
|
|
6839
|
+
);
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
function getMfaConfig() {
|
|
6843
|
+
const configuredMinutes = Number(process.env.SPFN_AUTH_MFA_STEP_UP_MINUTES);
|
|
6844
|
+
const minutes = Number.isFinite(configuredMinutes) && configuredMinutes > 0 ? configuredMinutes : DEFAULT_STEP_UP_MINUTES;
|
|
6845
|
+
return {
|
|
6846
|
+
issuer: process.env.SPFN_AUTH_MFA_ISSUER?.trim() || process.env.SPFN_AUTH_PASSKEY_RP_NAME?.trim() || mfaIssuerFromAppUrl() || DEFAULT_MFA_ISSUER,
|
|
6847
|
+
stepUpWindowMs: minutes * 6e4
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
function mfaIssuerFromAppUrl() {
|
|
6851
|
+
const configured2 = process.env.NEXT_PUBLIC_SPFN_APP_URL || process.env.SPFN_APP_URL;
|
|
6852
|
+
if (!configured2) {
|
|
6853
|
+
return null;
|
|
6854
|
+
}
|
|
6855
|
+
try {
|
|
6856
|
+
return new URL(configured2).hostname;
|
|
6857
|
+
} catch {
|
|
6858
|
+
return null;
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
var COOKIE_NAMES, globalConfig, CSRF_MODES, unrecognizedCsrfModeReported, PACKAGE_CSRF_EXEMPT_PATHS, DEFAULT_SESSION_RENEW_PATH, PASSKEY_USER_VERIFICATIONS, DEFAULT_CHALLENGE_TTL_SECONDS, DEFAULT_RECENT_AUTH_MINUTES, PASSKEY_VARS, DEFAULT_MFA_ISSUER, DEFAULT_STEP_UP_MINUTES;
|
|
6862
|
+
var init_config = __esm({
|
|
6863
|
+
"src/server/lib/config.ts"() {
|
|
6864
|
+
"use strict";
|
|
6865
|
+
init_key_policy();
|
|
6866
|
+
init_email();
|
|
6867
|
+
init_logger();
|
|
6868
|
+
COOKIE_NAMES = {
|
|
6869
|
+
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
6870
|
+
get SESSION() {
|
|
6871
|
+
return `spfn_session${getCookieSuffix()}`;
|
|
6872
|
+
},
|
|
6873
|
+
/** Current key ID (for key rotation) */
|
|
6874
|
+
get SESSION_KEY_ID() {
|
|
6875
|
+
return `spfn_session_key_id${getCookieSuffix()}`;
|
|
6876
|
+
},
|
|
6877
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
6878
|
+
get OAUTH_PENDING() {
|
|
6879
|
+
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
6880
|
+
},
|
|
6881
|
+
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
6882
|
+
get OAUTH_CSRF() {
|
|
6883
|
+
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
6884
|
+
},
|
|
6885
|
+
/** Password-setup session for verified-email signup — temporary, single-purpose */
|
|
6886
|
+
get SIGNUP_SETUP() {
|
|
6887
|
+
return `spfn_signup_setup${getCookieSuffix()}`;
|
|
6888
|
+
},
|
|
6889
|
+
/** Password-setup session for a password reset — temporary, single-purpose */
|
|
6890
|
+
get PASSWORD_RESET_SETUP() {
|
|
6891
|
+
return `spfn_password_reset_setup${getCookieSuffix()}`;
|
|
6892
|
+
},
|
|
6893
|
+
/** CSRF token — the only cookie here the browser can read */
|
|
6894
|
+
get CSRF() {
|
|
6895
|
+
return `spfn_csrf${getCookieSuffix()}`;
|
|
6896
|
+
}
|
|
6897
|
+
};
|
|
6898
|
+
globalConfig = {
|
|
6899
|
+
sessionTtl: "7d"
|
|
6900
|
+
// Default: 7 days
|
|
6901
|
+
};
|
|
6902
|
+
CSRF_MODES = ["off", "warn", "enforce"];
|
|
6903
|
+
unrecognizedCsrfModeReported = false;
|
|
6904
|
+
PACKAGE_CSRF_EXEMPT_PATHS = [
|
|
6905
|
+
"/_auth/oauth2/register",
|
|
6906
|
+
"/_auth/oauth2/token",
|
|
6907
|
+
"/_auth/oauth2/revoke"
|
|
6908
|
+
];
|
|
6909
|
+
DEFAULT_SESSION_RENEW_PATH = "/auth/renew";
|
|
6910
|
+
PASSKEY_USER_VERIFICATIONS = ["preferred", "required"];
|
|
6911
|
+
DEFAULT_CHALLENGE_TTL_SECONDS = 300;
|
|
6912
|
+
DEFAULT_RECENT_AUTH_MINUTES = 10;
|
|
6913
|
+
PASSKEY_VARS = [
|
|
6914
|
+
"SPFN_AUTH_PASSKEY_RP_ID",
|
|
6915
|
+
"SPFN_AUTH_PASSKEY_RP_NAME",
|
|
6916
|
+
"SPFN_AUTH_PASSKEY_ORIGINS",
|
|
6917
|
+
"SPFN_AUTH_PASSKEY_USER_VERIFICATION",
|
|
6918
|
+
"SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS",
|
|
6919
|
+
"SPFN_AUTH_PASSKEY_RECENT_AUTH_MINUTES"
|
|
6920
|
+
];
|
|
6921
|
+
DEFAULT_MFA_ISSUER = "SPFN";
|
|
6922
|
+
DEFAULT_STEP_UP_MINUTES = 10;
|
|
6923
|
+
}
|
|
6924
|
+
});
|
|
6925
|
+
|
|
6526
6926
|
// src/server/repositories/keys.repository.ts
|
|
6527
6927
|
import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
|
|
6528
6928
|
import { eq as eq2, and as and2, or, isNull, lt, desc, sql as sql5 } from "drizzle-orm";
|
|
@@ -6532,6 +6932,7 @@ var init_keys_repository = __esm({
|
|
|
6532
6932
|
"use strict";
|
|
6533
6933
|
init_user_public_keys();
|
|
6534
6934
|
init_users();
|
|
6935
|
+
init_config();
|
|
6535
6936
|
LAST_USED_THROTTLE_MS = 6e4;
|
|
6536
6937
|
KeysRepository = class extends BaseRepository2 {
|
|
6537
6938
|
/**
|
|
@@ -6574,6 +6975,10 @@ var init_keys_repository = __esm({
|
|
|
6574
6975
|
*
|
|
6575
6976
|
* `includeRevoked`는 이미 끊은 기기까지 보여준다 — "내가 언제 무엇을 끊었나"를
|
|
6576
6977
|
* 확인하는 용도라, 폐기 시각과 사유를 함께 고른다.
|
|
6978
|
+
*
|
|
6979
|
+
* `lastSeenIp` is not selected, deliberately. The concurrent-use moment is
|
|
6980
|
+
* what the owner acts on; the trail of addresses behind it is stored PII the
|
|
6981
|
+
* account surface has no use for.
|
|
6577
6982
|
* Read replica 사용
|
|
6578
6983
|
*/
|
|
6579
6984
|
async listForUser(userId, includeRevoked = false) {
|
|
@@ -6589,7 +6994,9 @@ var init_keys_repository = __esm({
|
|
|
6589
6994
|
expiresAt: userPublicKeys.expiresAt,
|
|
6590
6995
|
revokedAt: userPublicKeys.revokedAt,
|
|
6591
6996
|
registeredIp: userPublicKeys.registeredIp,
|
|
6592
|
-
registeredUserAgent: userPublicKeys.registeredUserAgent
|
|
6997
|
+
registeredUserAgent: userPublicKeys.registeredUserAgent,
|
|
6998
|
+
binding: userPublicKeys.binding,
|
|
6999
|
+
concurrentUseAt: userPublicKeys.concurrentUseAt
|
|
6593
7000
|
}).from(userPublicKeys).where(
|
|
6594
7001
|
includeRevoked ? eq2(userPublicKeys.userId, userId) : and2(
|
|
6595
7002
|
eq2(userPublicKeys.userId, userId),
|
|
@@ -6737,19 +7144,55 @@ var init_keys_repository = __esm({
|
|
|
6737
7144
|
return result[0] ?? null;
|
|
6738
7145
|
}
|
|
6739
7146
|
/**
|
|
6740
|
-
*
|
|
7147
|
+
* Bind one live key to the account's passkey, and give it the short life
|
|
7148
|
+
* that goes with it.
|
|
6741
7149
|
*
|
|
6742
|
-
*
|
|
6743
|
-
*
|
|
6744
|
-
*
|
|
7150
|
+
* Scoped by user and by `isActive`, like every other targeted update here, so
|
|
7151
|
+
* the answer is "this call bound something" rather than "a row exists".
|
|
7152
|
+
* Write primary 사용
|
|
6745
7153
|
*/
|
|
6746
|
-
async
|
|
6747
|
-
const result = await this.
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
7154
|
+
async bindByKeyIdAndUserId(keyId, userId, expiresAt) {
|
|
7155
|
+
const result = await this.db.update(userPublicKeys).set({ binding: "passkey", expiresAt }).where(
|
|
7156
|
+
and2(
|
|
7157
|
+
eq2(userPublicKeys.keyId, keyId),
|
|
7158
|
+
eq2(userPublicKeys.userId, userId),
|
|
7159
|
+
eq2(userPublicKeys.isActive, true)
|
|
7160
|
+
)
|
|
7161
|
+
).returning();
|
|
7162
|
+
return result[0] ?? null;
|
|
7163
|
+
}
|
|
7164
|
+
/**
|
|
7165
|
+
* Return every one of a user's bound keys to an ordinary long-lived key.
|
|
7166
|
+
*
|
|
7167
|
+
* One statement, because turning the setting off has to leave no key behind:
|
|
7168
|
+
* a row still marked `'passkey'` would keep expiring in hours with nothing
|
|
7169
|
+
* left to renew it, and its owner has just said they do not want that.
|
|
7170
|
+
* Write primary 사용
|
|
7171
|
+
*/
|
|
7172
|
+
async unbindActiveByUserId(userId, expiresAt) {
|
|
7173
|
+
const result = await this.db.update(userPublicKeys).set({ binding: "none", expiresAt }).where(
|
|
7174
|
+
and2(
|
|
7175
|
+
eq2(userPublicKeys.userId, userId),
|
|
7176
|
+
eq2(userPublicKeys.isActive, true),
|
|
7177
|
+
eq2(userPublicKeys.binding, "passkey")
|
|
7178
|
+
)
|
|
7179
|
+
).returning();
|
|
7180
|
+
return result.length;
|
|
7181
|
+
}
|
|
7182
|
+
/**
|
|
7183
|
+
* Key ID로 공개키 조회 — 활성 여부 무관 (clientProofV1 admission의 revocation 판정용)
|
|
7184
|
+
*
|
|
7185
|
+
* 폐기(isActive=false)·만료(expiresAt 경과)를 SESSION_REVOKED로, 미등록을
|
|
7186
|
+
* PROOF_INVALID로 구분해야 하므로 활성 필터 없이 조회한다. keyId는 UNIQUE.
|
|
7187
|
+
* Read replica 사용
|
|
7188
|
+
*/
|
|
7189
|
+
async findByKeyId(keyId) {
|
|
7190
|
+
const result = await this.readDb.select().from(userPublicKeys).where(eq2(userPublicKeys.keyId, keyId)).limit(1);
|
|
7191
|
+
return result[0] ?? null;
|
|
7192
|
+
}
|
|
7193
|
+
/**
|
|
7194
|
+
* Key ID로 활성 공개키 조회 (authenticate용)
|
|
7195
|
+
* Read replica 사용
|
|
6753
7196
|
*/
|
|
6754
7197
|
async findActiveByKeyId(keyId) {
|
|
6755
7198
|
const result = await this.readDb.select().from(userPublicKeys).where(
|
|
@@ -6779,35 +7222,75 @@ var init_keys_repository = __esm({
|
|
|
6779
7222
|
* `clientSeenAt` moves only when one of the three values differs from what is
|
|
6780
7223
|
* stored, so it answers "since when has this device been on this release"
|
|
6781
7224
|
* rather than "when was it last seen", which lastUsedAt already answers.
|
|
7225
|
+
*
|
|
7226
|
+
* `ip` is the client address this request resolved to, or null when none did.
|
|
7227
|
+
* It joins this statement rather than getting one of its own: the rule the
|
|
7228
|
+
* concurrent-use signal needs — write the address, and move
|
|
7229
|
+
* `concurrentUseAt` when it differs from the stored one inside the window —
|
|
7230
|
+
* is the rule already implemented here, and a second fire-and-forget write
|
|
7231
|
+
* beside it would double the per-request write on the authenticated path. It
|
|
7232
|
+
* is also why the comparison is a `CASE` over the stored value rather than a
|
|
7233
|
+
* read followed by a write: the row comes off the read replica, and a
|
|
7234
|
+
* replica-lagged address compared in application code both misses real
|
|
7235
|
+
* switches and invents ones that did not happen.
|
|
6782
7236
|
*/
|
|
6783
|
-
async updateLastUsedById(id25, identity) {
|
|
7237
|
+
async updateLastUsedById(id25, identity, ip) {
|
|
7238
|
+
const now = /* @__PURE__ */ new Date();
|
|
7239
|
+
const nowParam = sql5`${now.toISOString()}::timestamptz`;
|
|
6784
7240
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
6785
7241
|
const lastUsedIsStale = or(
|
|
6786
7242
|
isNull(userPublicKeys.lastUsedAt),
|
|
6787
7243
|
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
6788
7244
|
);
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
}
|
|
6793
|
-
const identityChanged = sql5`(
|
|
6794
|
-
${userPublicKeys.clientKind} IS DISTINCT FROM ${identity.kind}
|
|
6795
|
-
OR ${userPublicKeys.clientVersion} IS DISTINCT FROM ${identity.version}
|
|
6796
|
-
OR ${userPublicKeys.clientContractVersion} IS DISTINCT FROM ${identity.contractVersion}
|
|
7245
|
+
const ipChanged = sql5`(
|
|
7246
|
+
${ip ?? null}::text IS NOT NULL
|
|
7247
|
+
AND ${userPublicKeys.lastSeenIp} IS DISTINCT FROM ${ip ?? null}::text
|
|
6797
7248
|
)`;
|
|
6798
|
-
const
|
|
6799
|
-
|
|
7249
|
+
const notStampedThisWindow = sql5`(
|
|
7250
|
+
${userPublicKeys.concurrentUseAt} IS NULL
|
|
7251
|
+
OR ${userPublicKeys.concurrentUseAt} < ${this.concurrentUseSince(now)}
|
|
7252
|
+
)`;
|
|
7253
|
+
const identityChanged = identity ? sql5`(
|
|
7254
|
+
${userPublicKeys.clientKind} IS DISTINCT FROM ${identity.kind}
|
|
7255
|
+
OR ${userPublicKeys.clientVersion} IS DISTINCT FROM ${identity.version}
|
|
7256
|
+
OR ${userPublicKeys.clientContractVersion} IS DISTINCT FROM ${identity.contractVersion}
|
|
7257
|
+
)` : sql5`false`;
|
|
6800
7258
|
await this.db.update(userPublicKeys).set({
|
|
6801
7259
|
lastUsedAt: now,
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
7260
|
+
lastSeenIp: ip ?? null,
|
|
7261
|
+
lastSeenAt: now,
|
|
7262
|
+
// `last_seen_ip IS NOT NULL` is the second observation this needs:
|
|
7263
|
+
// a request whose address did not resolve stores NULL and stamps
|
|
7264
|
+
// `last_seen_at`, so without it the next ordinary request would
|
|
7265
|
+
// read as an address change and raise the signal from one device
|
|
7266
|
+
// that never moved.
|
|
7267
|
+
concurrentUseAt: sql5`CASE WHEN ${ipChanged}
|
|
7268
|
+
AND ${userPublicKeys.lastSeenIp} IS NOT NULL
|
|
7269
|
+
AND ${userPublicKeys.lastSeenAt} > ${this.concurrentUseSince(now)}
|
|
7270
|
+
THEN ${nowParam} ELSE ${userPublicKeys.concurrentUseAt} END`,
|
|
7271
|
+
...identity ? {
|
|
7272
|
+
clientKind: identity.kind,
|
|
7273
|
+
clientVersion: identity.version,
|
|
7274
|
+
clientContractVersion: identity.contractVersion,
|
|
7275
|
+
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
7276
|
+
} : {}
|
|
6806
7277
|
}).where(and2(
|
|
6807
7278
|
eq2(userPublicKeys.id, id25),
|
|
6808
|
-
or(lastUsedIsStale, identityChanged)
|
|
7279
|
+
or(lastUsedIsStale, identityChanged, sql5`(${ipChanged} AND ${notStampedThisWindow})`)
|
|
6809
7280
|
));
|
|
6810
7281
|
}
|
|
7282
|
+
/**
|
|
7283
|
+
* The boundary a previous sighting has to be newer than to count as concurrent.
|
|
7284
|
+
*
|
|
7285
|
+
* Computed here rather than written as `now() - interval` so that the moment
|
|
7286
|
+
* the row is stamped with and the moment the window is measured from are the
|
|
7287
|
+
* same one — a statement that used the database clock for the boundary and
|
|
7288
|
+
* ours for the value would disagree with itself by the round trip.
|
|
7289
|
+
*/
|
|
7290
|
+
concurrentUseSince(now) {
|
|
7291
|
+
const since = new Date(now.getTime() - getConcurrentUseWindowMs());
|
|
7292
|
+
return sql5`${since.toISOString()}::timestamptz`;
|
|
7293
|
+
}
|
|
6811
7294
|
};
|
|
6812
7295
|
keysRepository = new KeysRepository();
|
|
6813
7296
|
}
|
|
@@ -8884,6 +9367,43 @@ var init_schema5 = __esm({
|
|
|
8884
9367
|
})
|
|
8885
9368
|
},
|
|
8886
9369
|
// ============================================================================
|
|
9370
|
+
// Session binding (#97)
|
|
9371
|
+
// ============================================================================
|
|
9372
|
+
SPFN_AUTH_BOUND_KEY_TTL_HOURS: {
|
|
9373
|
+
...envNumber({
|
|
9374
|
+
description: "How long a session key bound to a passkey lives. This is the window in which a copied session cookie is still indistinguishable from the original, so it is hours rather than days; past it the browser runs one WebAuthn ceremony and gets a new key. Only applies to accounts that turned session binding on.",
|
|
9375
|
+
default: 24,
|
|
9376
|
+
required: false,
|
|
9377
|
+
examples: [8, 24, 72]
|
|
9378
|
+
})
|
|
9379
|
+
},
|
|
9380
|
+
SPFN_AUTH_BOUND_KEY_RENEW_GRACE_HOURS: {
|
|
9381
|
+
...envNumber({
|
|
9382
|
+
description: "How long after a bound key expires a passkey renewal is still offered. Past it the account signs in again. Open-ended grace would make an expired key a long-lived key with extra steps.",
|
|
9383
|
+
default: 168,
|
|
9384
|
+
required: false,
|
|
9385
|
+
examples: [24, 168, 720]
|
|
9386
|
+
})
|
|
9387
|
+
},
|
|
9388
|
+
SPFN_AUTH_CONCURRENT_USE_WINDOW_MS: {
|
|
9389
|
+
...envNumber({
|
|
9390
|
+
description: "How far apart two sightings of one device key from two client addresses still count as concurrent use, surfaced as `concurrentUseAtMillis` on the key list. A signal for the owner to read, never a refusal \u2014 addresses change legitimately. Meaningful only where proxy-guard is configured, since without it every web request carries the Next.js server's address.",
|
|
9391
|
+
default: 3e5,
|
|
9392
|
+
required: false,
|
|
9393
|
+
examples: [6e4, 3e5, 9e5]
|
|
9394
|
+
})
|
|
9395
|
+
},
|
|
9396
|
+
SPFN_AUTH_SESSION_RENEW_PATH: {
|
|
9397
|
+
...envString({
|
|
9398
|
+
description: "Page in your app that runs the renewal ceremony. `RequireAuth` redirects a bound session whose key expired here instead of to the sign-in page; the page calls `renewSession(api)` and returns the user to where they were. Override per guard with the `renewalPath` prop.",
|
|
9399
|
+
default: "/auth/renew",
|
|
9400
|
+
required: false,
|
|
9401
|
+
nextjs: true,
|
|
9402
|
+
// Read by RequireAuth, which renders in the Next.js runtime
|
|
9403
|
+
examples: ["/auth/renew", "/session/renew"]
|
|
9404
|
+
})
|
|
9405
|
+
},
|
|
9406
|
+
// ============================================================================
|
|
8887
9407
|
// API Configuration
|
|
8888
9408
|
// ============================================================================
|
|
8889
9409
|
SPFN_API_URL: {
|
|
@@ -9142,21 +9662,21 @@ var init_schema5 = __esm({
|
|
|
9142
9662
|
|
|
9143
9663
|
// src/config/index.ts
|
|
9144
9664
|
import { createEnvRegistry } from "@spfn/core/env";
|
|
9145
|
-
var registry,
|
|
9146
|
-
var
|
|
9665
|
+
var registry, env4;
|
|
9666
|
+
var init_config2 = __esm({
|
|
9147
9667
|
"src/config/index.ts"() {
|
|
9148
9668
|
"use strict";
|
|
9149
9669
|
init_schema5();
|
|
9150
9670
|
init_schema5();
|
|
9151
9671
|
registry = createEnvRegistry(authEnvSchema);
|
|
9152
|
-
|
|
9672
|
+
env4 = registry.validate();
|
|
9153
9673
|
}
|
|
9154
9674
|
});
|
|
9155
9675
|
|
|
9156
9676
|
// src/server/lib/oauth/token-cipher.ts
|
|
9157
9677
|
import crypto3 from "crypto";
|
|
9158
9678
|
function getEncryptionKeyring() {
|
|
9159
|
-
const raw =
|
|
9679
|
+
const raw = env4.SPFN_AUTH_TOKEN_ENCRYPTION_KEYS;
|
|
9160
9680
|
if (!raw) {
|
|
9161
9681
|
return [];
|
|
9162
9682
|
}
|
|
@@ -9226,7 +9746,7 @@ function decryptAesGcm(payload, encoding, key, aad) {
|
|
|
9226
9746
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
9227
9747
|
}
|
|
9228
9748
|
function getLegacyV1Key() {
|
|
9229
|
-
return crypto3.createHash("sha256").update(`social-token:${
|
|
9749
|
+
return crypto3.createHash("sha256").update(`social-token:${env4.SPFN_AUTH_SESSION_SECRET}`).digest();
|
|
9230
9750
|
}
|
|
9231
9751
|
function configureOAuthTokenCipher(cipher) {
|
|
9232
9752
|
configuredCipher = cipher;
|
|
@@ -9244,7 +9764,7 @@ var V1_PREFIX, V2_PREFIX, ENCRYPTED_PREFIX, IV_BYTES, TAG_BYTES, KEY_BYTES, KEY_
|
|
|
9244
9764
|
var init_token_cipher = __esm({
|
|
9245
9765
|
"src/server/lib/oauth/token-cipher.ts"() {
|
|
9246
9766
|
"use strict";
|
|
9247
|
-
|
|
9767
|
+
init_config2();
|
|
9248
9768
|
V1_PREFIX = "enc:v1:";
|
|
9249
9769
|
V2_PREFIX = "enc:v2:";
|
|
9250
9770
|
ENCRYPTED_PREFIX = "enc:";
|
|
@@ -10344,7 +10864,7 @@ function getKeyId(c) {
|
|
|
10344
10864
|
// src/server/routes/auth/index.ts
|
|
10345
10865
|
init_types();
|
|
10346
10866
|
import { KeyNotFoundError } from "@spfn/auth/errors";
|
|
10347
|
-
import { ValidationError as
|
|
10867
|
+
import { ValidationError as ValidationError15 } from "@spfn/core/errors";
|
|
10348
10868
|
|
|
10349
10869
|
// src/server/services/auth.service.ts
|
|
10350
10870
|
init_repositories();
|
|
@@ -10361,28 +10881,8 @@ import {
|
|
|
10361
10881
|
|
|
10362
10882
|
// src/server/services/oauth2-grant.service.ts
|
|
10363
10883
|
init_oauth2_grants_repository();
|
|
10884
|
+
init_logger();
|
|
10364
10885
|
import { OAuth2GrantNotFoundError } from "@spfn/auth/errors";
|
|
10365
|
-
|
|
10366
|
-
// src/server/logger.ts
|
|
10367
|
-
import { logger as rootLogger } from "@spfn/core/logger";
|
|
10368
|
-
var authLogger = {
|
|
10369
|
-
plugin: rootLogger.child("@spfn/auth:plugin"),
|
|
10370
|
-
middleware: rootLogger.child("@spfn/auth:middleware"),
|
|
10371
|
-
interceptor: {
|
|
10372
|
-
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
10373
|
-
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
10374
|
-
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
10375
|
-
oauth: rootLogger.child("@spfn/auth:interceptor:oauth"),
|
|
10376
|
-
csrf: rootLogger.child("@spfn/auth:interceptor:csrf")
|
|
10377
|
-
},
|
|
10378
|
-
session: rootLogger.child("@spfn/auth:session"),
|
|
10379
|
-
service: rootLogger.child("@spfn/auth:service"),
|
|
10380
|
-
setup: rootLogger.child("@spfn/auth:setup"),
|
|
10381
|
-
email: rootLogger.child("@spfn/auth:email"),
|
|
10382
|
-
sms: rootLogger.child("@spfn/auth:sms")
|
|
10383
|
-
};
|
|
10384
|
-
|
|
10385
|
-
// src/server/services/oauth2-grant.service.ts
|
|
10386
10886
|
async function listOAuth2GrantsService(userId) {
|
|
10387
10887
|
const rows = await oauth2GrantsRepository.listActiveByUserId(userId);
|
|
10388
10888
|
return rows.map(({ grant, client }) => ({
|
|
@@ -10395,299 +10895,41 @@ async function listOAuth2GrantsService(userId) {
|
|
|
10395
10895
|
lastUsedAtMillis: client.lastUsedAt?.getTime()
|
|
10396
10896
|
}));
|
|
10397
10897
|
}
|
|
10398
|
-
async function revokeOAuth2GrantService(id25, userId) {
|
|
10399
|
-
const revoked = await oauth2GrantsRepository.revokeByIdForUser(id25, userId);
|
|
10400
|
-
if (!revoked) {
|
|
10401
|
-
throw new OAuth2GrantNotFoundError();
|
|
10402
|
-
}
|
|
10403
|
-
await oauth2GrantsRepository.revokeTokensOfGrants([revoked.id]);
|
|
10404
|
-
}
|
|
10405
|
-
async function revokeAllOAuth2GrantsForUser(userId) {
|
|
10406
|
-
const grantIds = await oauth2GrantsRepository.revokeAllActiveByUserId(userId);
|
|
10407
|
-
if (grantIds.length === 0) {
|
|
10408
|
-
return;
|
|
10409
|
-
}
|
|
10410
|
-
const tokens = await oauth2GrantsRepository.revokeTokensOfGrants(grantIds);
|
|
10411
|
-
authLogger.service.info("OAuth2 grants revoked for a user", {
|
|
10412
|
-
userId,
|
|
10413
|
-
grants: grantIds.length,
|
|
10414
|
-
tokens
|
|
10415
|
-
});
|
|
10416
|
-
}
|
|
10417
|
-
|
|
10418
|
-
// src/server/lib/config.ts
|
|
10419
|
-
init_email();
|
|
10420
|
-
import { env as env4 } from "@spfn/auth/config";
|
|
10421
|
-
import { PasskeyConfigError } from "@spfn/auth/errors";
|
|
10422
|
-
function getCookieSuffix() {
|
|
10423
|
-
const port = process.env.SPFN_PORT;
|
|
10424
|
-
return port ? `_${port}` : "";
|
|
10425
|
-
}
|
|
10426
|
-
var COOKIE_NAMES = {
|
|
10427
|
-
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
10428
|
-
get SESSION() {
|
|
10429
|
-
return `spfn_session${getCookieSuffix()}`;
|
|
10430
|
-
},
|
|
10431
|
-
/** Current key ID (for key rotation) */
|
|
10432
|
-
get SESSION_KEY_ID() {
|
|
10433
|
-
return `spfn_session_key_id${getCookieSuffix()}`;
|
|
10434
|
-
},
|
|
10435
|
-
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
10436
|
-
get OAUTH_PENDING() {
|
|
10437
|
-
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
10438
|
-
},
|
|
10439
|
-
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
10440
|
-
get OAUTH_CSRF() {
|
|
10441
|
-
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
10442
|
-
},
|
|
10443
|
-
/** Password-setup session for verified-email signup — temporary, single-purpose */
|
|
10444
|
-
get SIGNUP_SETUP() {
|
|
10445
|
-
return `spfn_signup_setup${getCookieSuffix()}`;
|
|
10446
|
-
},
|
|
10447
|
-
/** Password-setup session for a password reset — temporary, single-purpose */
|
|
10448
|
-
get PASSWORD_RESET_SETUP() {
|
|
10449
|
-
return `spfn_password_reset_setup${getCookieSuffix()}`;
|
|
10450
|
-
},
|
|
10451
|
-
/** CSRF token — the only cookie here the browser can read */
|
|
10452
|
-
get CSRF() {
|
|
10453
|
-
return `spfn_csrf${getCookieSuffix()}`;
|
|
10454
|
-
}
|
|
10455
|
-
};
|
|
10456
|
-
function matchOAuthCsrfCookies(cookies) {
|
|
10457
|
-
return Object.entries(cookies).filter(([name]) => /^spfn_oauth_csrf(_\d+)?$/.test(name)).map(([name, value]) => ({ name, value }));
|
|
10458
|
-
}
|
|
10459
|
-
function parseDuration(duration) {
|
|
10460
|
-
if (typeof duration === "number") {
|
|
10461
|
-
return duration;
|
|
10462
|
-
}
|
|
10463
|
-
const match = duration.match(/^(\d+)([dhms]?)$/);
|
|
10464
|
-
if (!match) {
|
|
10465
|
-
throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);
|
|
10466
|
-
}
|
|
10467
|
-
const value = parseInt(match[1], 10);
|
|
10468
|
-
const unit = match[2] || "s";
|
|
10469
|
-
switch (unit) {
|
|
10470
|
-
case "d":
|
|
10471
|
-
return value * 24 * 60 * 60;
|
|
10472
|
-
case "h":
|
|
10473
|
-
return value * 60 * 60;
|
|
10474
|
-
case "m":
|
|
10475
|
-
return value * 60;
|
|
10476
|
-
case "s":
|
|
10477
|
-
return value;
|
|
10478
|
-
default:
|
|
10479
|
-
throw new Error(`Unknown duration unit: ${unit}`);
|
|
10480
|
-
}
|
|
10481
|
-
}
|
|
10482
|
-
var globalConfig = {
|
|
10483
|
-
sessionTtl: "7d"
|
|
10484
|
-
// Default: 7 days
|
|
10485
|
-
};
|
|
10486
|
-
function configureAuth(config4) {
|
|
10487
|
-
globalConfig = {
|
|
10488
|
-
...globalConfig,
|
|
10489
|
-
...config4
|
|
10490
|
-
};
|
|
10491
|
-
}
|
|
10492
|
-
function getAuthConfig() {
|
|
10493
|
-
return { ...globalConfig };
|
|
10494
|
-
}
|
|
10495
|
-
async function runBeforeRegister(context) {
|
|
10496
|
-
const { beforeRegister } = globalConfig;
|
|
10497
|
-
if (beforeRegister) {
|
|
10498
|
-
await beforeRegister({ ...context, email: normalizeOptionalEmail(context.email) });
|
|
10499
|
-
}
|
|
10500
|
-
}
|
|
10501
|
-
function getSessionTtl(override) {
|
|
10502
|
-
if (override !== void 0) {
|
|
10503
|
-
return parseDuration(override);
|
|
10504
|
-
}
|
|
10505
|
-
if (globalConfig.sessionTtl !== void 0) {
|
|
10506
|
-
return parseDuration(globalConfig.sessionTtl);
|
|
10507
|
-
}
|
|
10508
|
-
const envTtl = env4.SPFN_AUTH_SESSION_TTL;
|
|
10509
|
-
if (envTtl) {
|
|
10510
|
-
return parseDuration(envTtl);
|
|
10511
|
-
}
|
|
10512
|
-
return 7 * 24 * 60 * 60;
|
|
10513
|
-
}
|
|
10514
|
-
var CSRF_MODES = ["off", "warn", "enforce"];
|
|
10515
|
-
var unrecognizedCsrfModeReported = false;
|
|
10516
|
-
function getCsrfMode() {
|
|
10517
|
-
const configured2 = globalConfig.csrf?.mode ?? env4.SPFN_AUTH_CSRF;
|
|
10518
|
-
if (!configured2) {
|
|
10519
|
-
return "warn";
|
|
10520
|
-
}
|
|
10521
|
-
const normalized = String(configured2).trim().toLowerCase();
|
|
10522
|
-
if (!CSRF_MODES.includes(normalized)) {
|
|
10523
|
-
if (!unrecognizedCsrfModeReported) {
|
|
10524
|
-
unrecognizedCsrfModeReported = true;
|
|
10525
|
-
authLogger.interceptor.csrf.error(
|
|
10526
|
-
`Unrecognized CSRF mode "${configured2}" \u2014 expected off | warn | enforce. Enforcing.`
|
|
10527
|
-
);
|
|
10528
|
-
}
|
|
10529
|
-
return "enforce";
|
|
10530
|
-
}
|
|
10531
|
-
return normalized;
|
|
10532
|
-
}
|
|
10533
|
-
var PACKAGE_CSRF_EXEMPT_PATHS = [
|
|
10534
|
-
"/_auth/oauth2/register",
|
|
10535
|
-
"/_auth/oauth2/token",
|
|
10536
|
-
"/_auth/oauth2/revoke"
|
|
10537
|
-
];
|
|
10538
|
-
function getCsrfExemptPaths() {
|
|
10539
|
-
return [...PACKAGE_CSRF_EXEMPT_PATHS, ...globalConfig.csrf?.exemptPaths ?? []];
|
|
10540
|
-
}
|
|
10541
|
-
var PASSKEY_USER_VERIFICATIONS = ["preferred", "required"];
|
|
10542
|
-
var DEFAULT_CHALLENGE_TTL_SECONDS = 300;
|
|
10543
|
-
var DEFAULT_RECENT_AUTH_MINUTES = 10;
|
|
10544
|
-
function passkeyEnvSource() {
|
|
10545
|
-
return { ...process.env, SPFN_APP_URL: process.env.SPFN_APP_URL || env4.SPFN_APP_URL };
|
|
10546
|
-
}
|
|
10547
|
-
function passkeyAppUrl(env21) {
|
|
10548
|
-
const configured2 = env21.NEXT_PUBLIC_SPFN_APP_URL || env21.SPFN_APP_URL;
|
|
10549
|
-
if (!configured2) {
|
|
10550
|
-
throw new PasskeyConfigError({
|
|
10551
|
-
message: "Passkeys need a relying party ID. Set SPFN_AUTH_PASSKEY_RP_ID, or set NEXT_PUBLIC_SPFN_APP_URL / SPFN_APP_URL to the app origin it should be derived from."
|
|
10552
|
-
});
|
|
10553
|
-
}
|
|
10554
|
-
try {
|
|
10555
|
-
return new URL(configured2);
|
|
10556
|
-
} catch {
|
|
10557
|
-
throw new PasskeyConfigError({
|
|
10558
|
-
message: `Passkeys cannot derive a relying party ID: "${configured2}" is not a URL. Fix NEXT_PUBLIC_SPFN_APP_URL / SPFN_APP_URL, or set SPFN_AUTH_PASSKEY_RP_ID explicitly.`
|
|
10559
|
-
});
|
|
10560
|
-
}
|
|
10561
|
-
}
|
|
10562
|
-
function isLocalhost(hostname) {
|
|
10563
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
10564
|
-
}
|
|
10565
|
-
function isUnderRpId(hostname, rpId) {
|
|
10566
|
-
return hostname === rpId || hostname.endsWith(`.${rpId}`);
|
|
10567
|
-
}
|
|
10568
|
-
function assertOriginServesRpId(origin, rpId) {
|
|
10569
|
-
let url;
|
|
10570
|
-
try {
|
|
10571
|
-
url = new URL(origin);
|
|
10572
|
-
} catch {
|
|
10573
|
-
throw new PasskeyConfigError({
|
|
10574
|
-
message: `SPFN_AUTH_PASSKEY_ORIGINS contains "${origin}", which is not a URL. List full origins, e.g. https://app.example.com.`
|
|
10575
|
-
});
|
|
10576
|
-
}
|
|
10577
|
-
if (url.protocol !== "https:" && !isLocalhost(url.hostname)) {
|
|
10578
|
-
throw new PasskeyConfigError({
|
|
10579
|
-
message: `Passkey origin "${origin}" is not https. WebAuthn runs only in a secure context, and localhost is the only host a browser treats as one over plain http.`
|
|
10580
|
-
});
|
|
10581
|
-
}
|
|
10582
|
-
if (!isUnderRpId(url.hostname, rpId)) {
|
|
10583
|
-
throw new PasskeyConfigError({
|
|
10584
|
-
message: `Passkey origin "${origin}" is not on relying party ID "${rpId}". Each origin must be that host or a subdomain of it, or the browser refuses the ceremony.`
|
|
10585
|
-
});
|
|
10586
|
-
}
|
|
10587
|
-
}
|
|
10588
|
-
function resolveUserVerification(env21) {
|
|
10589
|
-
const configured2 = env21.SPFN_AUTH_PASSKEY_USER_VERIFICATION;
|
|
10590
|
-
if (!configured2) {
|
|
10591
|
-
return "preferred";
|
|
10592
|
-
}
|
|
10593
|
-
const normalized = configured2.trim().toLowerCase();
|
|
10594
|
-
if (!PASSKEY_USER_VERIFICATIONS.includes(normalized)) {
|
|
10595
|
-
throw new PasskeyConfigError({
|
|
10596
|
-
message: `SPFN_AUTH_PASSKEY_USER_VERIFICATION is "${configured2}" \u2014 expected preferred or required. A passkey is the whole credential here, so an assertion that skipped user verification would sign someone in on an unlocked device alone.`
|
|
10597
|
-
});
|
|
10598
|
-
}
|
|
10599
|
-
return normalized;
|
|
10600
|
-
}
|
|
10601
|
-
function resolvePositiveNumber(env21, variable, fallback) {
|
|
10602
|
-
const configured2 = env21[variable];
|
|
10603
|
-
if (!configured2) {
|
|
10604
|
-
return fallback;
|
|
10605
|
-
}
|
|
10606
|
-
const parsed = Number(configured2);
|
|
10607
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
10608
|
-
throw new PasskeyConfigError({
|
|
10609
|
-
message: `${variable} is "${configured2}" \u2014 expected a positive number.`
|
|
10610
|
-
});
|
|
10611
|
-
}
|
|
10612
|
-
return parsed;
|
|
10613
|
-
}
|
|
10614
|
-
function getPasskeyConfig(env21 = passkeyEnvSource()) {
|
|
10615
|
-
const rpId = env21.SPFN_AUTH_PASSKEY_RP_ID?.trim() || passkeyAppUrl(env21).hostname;
|
|
10616
|
-
const configuredOrigins = env21.SPFN_AUTH_PASSKEY_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
10617
|
-
const origins = configuredOrigins?.length ? configuredOrigins : [passkeyAppUrl(env21).origin];
|
|
10618
|
-
for (const origin of origins) {
|
|
10619
|
-
assertOriginServesRpId(origin, rpId);
|
|
10620
|
-
}
|
|
10621
|
-
return {
|
|
10622
|
-
rpId,
|
|
10623
|
-
rpName: env21.SPFN_AUTH_PASSKEY_RP_NAME?.trim() || rpId,
|
|
10624
|
-
origins,
|
|
10625
|
-
userVerification: resolveUserVerification(env21),
|
|
10626
|
-
challengeTtlMs: resolvePositiveNumber(
|
|
10627
|
-
env21,
|
|
10628
|
-
"SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS",
|
|
10629
|
-
DEFAULT_CHALLENGE_TTL_SECONDS
|
|
10630
|
-
) * 1e3,
|
|
10631
|
-
recentAuthMs: resolvePositiveNumber(
|
|
10632
|
-
env21,
|
|
10633
|
-
"SPFN_AUTH_PASSKEY_RECENT_AUTH_MINUTES",
|
|
10634
|
-
DEFAULT_RECENT_AUTH_MINUTES
|
|
10635
|
-
) * 6e4
|
|
10636
|
-
};
|
|
10637
|
-
}
|
|
10638
|
-
var PASSKEY_VARS = [
|
|
10639
|
-
"SPFN_AUTH_PASSKEY_RP_ID",
|
|
10640
|
-
"SPFN_AUTH_PASSKEY_RP_NAME",
|
|
10641
|
-
"SPFN_AUTH_PASSKEY_ORIGINS",
|
|
10642
|
-
"SPFN_AUTH_PASSKEY_USER_VERIFICATION",
|
|
10643
|
-
"SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS",
|
|
10644
|
-
"SPFN_AUTH_PASSKEY_RECENT_AUTH_MINUTES"
|
|
10645
|
-
];
|
|
10646
|
-
function assertPasskeyConfig(env21 = passkeyEnvSource()) {
|
|
10647
|
-
if (PASSKEY_VARS.some((variable) => env21[variable])) {
|
|
10648
|
-
getPasskeyConfig(env21);
|
|
10649
|
-
return;
|
|
10650
|
-
}
|
|
10651
|
-
try {
|
|
10652
|
-
getPasskeyConfig(env21);
|
|
10653
|
-
} catch (error) {
|
|
10654
|
-
authLogger.service.info(
|
|
10655
|
-
`Passkeys cannot be served with the configuration derived from the app URL, and no SPFN_AUTH_PASSKEY_* variable is set, so boot continues. ${error.message}`
|
|
10656
|
-
);
|
|
10657
|
-
}
|
|
10658
|
-
}
|
|
10659
|
-
var DEFAULT_MFA_ISSUER = "SPFN";
|
|
10660
|
-
var DEFAULT_STEP_UP_MINUTES = 10;
|
|
10661
|
-
function getMfaConfig() {
|
|
10662
|
-
const configuredMinutes = Number(process.env.SPFN_AUTH_MFA_STEP_UP_MINUTES);
|
|
10663
|
-
const minutes = Number.isFinite(configuredMinutes) && configuredMinutes > 0 ? configuredMinutes : DEFAULT_STEP_UP_MINUTES;
|
|
10664
|
-
return {
|
|
10665
|
-
issuer: process.env.SPFN_AUTH_MFA_ISSUER?.trim() || process.env.SPFN_AUTH_PASSKEY_RP_NAME?.trim() || mfaIssuerFromAppUrl() || DEFAULT_MFA_ISSUER,
|
|
10666
|
-
stepUpWindowMs: minutes * 6e4
|
|
10667
|
-
};
|
|
10668
|
-
}
|
|
10669
|
-
function mfaIssuerFromAppUrl() {
|
|
10670
|
-
const configured2 = process.env.NEXT_PUBLIC_SPFN_APP_URL || process.env.SPFN_APP_URL;
|
|
10671
|
-
if (!configured2) {
|
|
10672
|
-
return null;
|
|
10898
|
+
async function revokeOAuth2GrantService(id25, userId) {
|
|
10899
|
+
const revoked = await oauth2GrantsRepository.revokeByIdForUser(id25, userId);
|
|
10900
|
+
if (!revoked) {
|
|
10901
|
+
throw new OAuth2GrantNotFoundError();
|
|
10673
10902
|
}
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10903
|
+
await oauth2GrantsRepository.revokeTokensOfGrants([revoked.id]);
|
|
10904
|
+
}
|
|
10905
|
+
async function revokeAllOAuth2GrantsForUser(userId) {
|
|
10906
|
+
const grantIds = await oauth2GrantsRepository.revokeAllActiveByUserId(userId);
|
|
10907
|
+
if (grantIds.length === 0) {
|
|
10908
|
+
return;
|
|
10678
10909
|
}
|
|
10910
|
+
const tokens = await oauth2GrantsRepository.revokeTokensOfGrants(grantIds);
|
|
10911
|
+
authLogger.service.info("OAuth2 grants revoked for a user", {
|
|
10912
|
+
userId,
|
|
10913
|
+
grants: grantIds.length,
|
|
10914
|
+
tokens
|
|
10915
|
+
});
|
|
10679
10916
|
}
|
|
10680
10917
|
|
|
10918
|
+
// src/server/services/auth.service.ts
|
|
10919
|
+
init_config();
|
|
10920
|
+
|
|
10681
10921
|
// src/server/services/verification.service.ts
|
|
10922
|
+
init_logger();
|
|
10923
|
+
init_email();
|
|
10924
|
+
init_repositories();
|
|
10682
10925
|
import crypto5 from "crypto";
|
|
10683
10926
|
import { env as env8 } from "@spfn/auth/config";
|
|
10684
10927
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
10685
10928
|
import jwt2 from "jsonwebtoken";
|
|
10686
10929
|
import { sendEmail as sendEmail2, sendSMS as sendSMS2 } from "@spfn/notification/server";
|
|
10687
|
-
init_email();
|
|
10688
|
-
init_repositories();
|
|
10689
10930
|
|
|
10690
10931
|
// src/server/lib/link-mail-delivery.ts
|
|
10932
|
+
init_logger();
|
|
10691
10933
|
import { env as env7 } from "@spfn/auth/config";
|
|
10692
10934
|
import { getBoss } from "@spfn/core/job";
|
|
10693
10935
|
|
|
@@ -10697,6 +10939,7 @@ init_schema3();
|
|
|
10697
10939
|
import { job } from "@spfn/core/job";
|
|
10698
10940
|
|
|
10699
10941
|
// src/server/services/link-mail.service.ts
|
|
10942
|
+
init_logger();
|
|
10700
10943
|
import { env as env6 } from "@spfn/auth/config";
|
|
10701
10944
|
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
10702
10945
|
|
|
@@ -11019,8 +11262,23 @@ async function verifyCodeService(params) {
|
|
|
11019
11262
|
};
|
|
11020
11263
|
}
|
|
11021
11264
|
|
|
11022
|
-
// src/server/
|
|
11023
|
-
|
|
11265
|
+
// src/server/services/key.service.ts
|
|
11266
|
+
init_key_policy();
|
|
11267
|
+
init_config();
|
|
11268
|
+
|
|
11269
|
+
// src/server/lib/ua-family.ts
|
|
11270
|
+
var FAMILY_MARKERS = [
|
|
11271
|
+
{ family: "edge", marker: /\bEdg(?:A|iOS)?\// },
|
|
11272
|
+
{ family: "chrome", marker: /\b(?:Chrome|CriOS)\// },
|
|
11273
|
+
{ family: "firefox", marker: /\b(?:Firefox|FxiOS)\// },
|
|
11274
|
+
{ family: "safari", marker: /\bSafari\// }
|
|
11275
|
+
];
|
|
11276
|
+
function uaFamily(userAgent) {
|
|
11277
|
+
if (!userAgent) {
|
|
11278
|
+
return "other";
|
|
11279
|
+
}
|
|
11280
|
+
return FAMILY_MARKERS.find((entry) => entry.marker.test(userAgent))?.family ?? "other";
|
|
11281
|
+
}
|
|
11024
11282
|
|
|
11025
11283
|
// src/server/services/key.service.ts
|
|
11026
11284
|
init_repositories();
|
|
@@ -11065,7 +11323,8 @@ var DeviceRegistrationChannelSchema = Type.Union([
|
|
|
11065
11323
|
Type.Literal("oauth-native"),
|
|
11066
11324
|
Type.Literal("device-code"),
|
|
11067
11325
|
Type.Literal("password-reset"),
|
|
11068
|
-
Type.Literal("passkey")
|
|
11326
|
+
Type.Literal("passkey"),
|
|
11327
|
+
Type.Literal("renewal")
|
|
11069
11328
|
]);
|
|
11070
11329
|
var authDeviceRegisteredEvent = defineEvent(
|
|
11071
11330
|
"auth.device.registered",
|
|
@@ -11175,6 +11434,8 @@ var authPasswordResetEvent = defineEvent(
|
|
|
11175
11434
|
);
|
|
11176
11435
|
|
|
11177
11436
|
// src/server/services/mfa.service.ts
|
|
11437
|
+
init_logger();
|
|
11438
|
+
init_config();
|
|
11178
11439
|
import { onAfterCommit, runInTransaction as runInTransaction2 } from "@spfn/core/db";
|
|
11179
11440
|
import { ValidationError } from "@spfn/core/errors";
|
|
11180
11441
|
import {
|
|
@@ -11466,8 +11727,9 @@ init_repositories();
|
|
|
11466
11727
|
init_mfa_totp();
|
|
11467
11728
|
|
|
11468
11729
|
// src/server/services/webauthn-challenge.service.ts
|
|
11469
|
-
|
|
11730
|
+
init_config();
|
|
11470
11731
|
init_repositories();
|
|
11732
|
+
import crypto9 from "crypto";
|
|
11471
11733
|
var CHALLENGE_BYTES = 32;
|
|
11472
11734
|
async function mintChallenge(kind, userId) {
|
|
11473
11735
|
const challenge = crypto9.randomBytes(CHALLENGE_BYTES).toString("base64url");
|
|
@@ -11715,7 +11977,10 @@ async function emitDeviceRegistered(row, channel) {
|
|
|
11715
11977
|
// src/server/services/key.service.ts
|
|
11716
11978
|
var KEY_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
11717
11979
|
var DEFAULT_KEY_ALGORITHM = "ES256";
|
|
11718
|
-
function getKeyExpiryDate() {
|
|
11980
|
+
function getKeyExpiryDate(binding) {
|
|
11981
|
+
if (binding === "passkey") {
|
|
11982
|
+
return new Date(Date.now() + getBoundKeyTtlMs());
|
|
11983
|
+
}
|
|
11719
11984
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
11720
11985
|
expiresAt.setDate(expiresAt.getDate() + KEY_TTL_DAYS);
|
|
11721
11986
|
return expiresAt;
|
|
@@ -11725,13 +11990,11 @@ function isExpired(expiresAt) {
|
|
|
11725
11990
|
}
|
|
11726
11991
|
async function registerPublicKeyService(params) {
|
|
11727
11992
|
const { userId, keyId, publicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM, deviceName, platform } = params;
|
|
11993
|
+
const binding = params.binding ?? "none";
|
|
11728
11994
|
const existing = await keysRepository.findByKeyId(keyId);
|
|
11729
11995
|
if (existing) {
|
|
11730
11996
|
if (existing.userId === userId && existing.isActive) {
|
|
11731
|
-
|
|
11732
|
-
await keysRepository.extendExpiry(keyId, userId, getKeyExpiryDate());
|
|
11733
|
-
}
|
|
11734
|
-
return;
|
|
11997
|
+
return await reRegisterOwnActiveKey(existing, userId);
|
|
11735
11998
|
}
|
|
11736
11999
|
throw new KeyIdAlreadyRegisteredError();
|
|
11737
12000
|
}
|
|
@@ -11748,16 +12011,27 @@ async function registerPublicKeyService(params) {
|
|
|
11748
12011
|
fingerprint,
|
|
11749
12012
|
deviceName,
|
|
11750
12013
|
platform,
|
|
12014
|
+
binding,
|
|
11751
12015
|
registeredIp: params.ip ?? null,
|
|
11752
12016
|
registeredUserAgent: params.userAgent ?? null,
|
|
12017
|
+
registeredUaFamily: params.userAgent ? uaFamily(params.userAgent) : null,
|
|
11753
12018
|
isActive: true,
|
|
11754
|
-
expiresAt: getKeyExpiryDate()
|
|
12019
|
+
expiresAt: getKeyExpiryDate(binding)
|
|
11755
12020
|
});
|
|
11756
|
-
if (
|
|
12021
|
+
if (params.replacesKeyId) {
|
|
12022
|
+
await carryStepUpVerification(userId, params.replacesKeyId, keyId);
|
|
12023
|
+
} else {
|
|
11757
12024
|
await emitDeviceRegistered(row, params.channel);
|
|
11758
|
-
return;
|
|
11759
12025
|
}
|
|
11760
|
-
|
|
12026
|
+
return { binding, expiresAt: row.expiresAt };
|
|
12027
|
+
}
|
|
12028
|
+
async function reRegisterOwnActiveKey(existing, userId) {
|
|
12029
|
+
if (existing.binding !== "passkey" && isExpired(existing.expiresAt)) {
|
|
12030
|
+
const extended = getKeyExpiryDate("none");
|
|
12031
|
+
await keysRepository.extendExpiry(existing.keyId, userId, extended);
|
|
12032
|
+
return { binding: "none", expiresAt: extended };
|
|
12033
|
+
}
|
|
12034
|
+
return { binding: existing.binding, expiresAt: existing.expiresAt };
|
|
11761
12035
|
}
|
|
11762
12036
|
async function rotateKeyService(params) {
|
|
11763
12037
|
const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM } = params;
|
|
@@ -11780,8 +12054,12 @@ async function rotateKeyService(params) {
|
|
|
11780
12054
|
fingerprint,
|
|
11781
12055
|
deviceName: params.deviceName ?? replaced?.deviceName ?? void 0,
|
|
11782
12056
|
platform: params.platform ?? replaced?.platform ?? void 0,
|
|
12057
|
+
binding: replaced?.binding ?? "none",
|
|
12058
|
+
registeredIp: replaced?.registeredIp ?? null,
|
|
12059
|
+
registeredUserAgent: replaced?.registeredUserAgent ?? null,
|
|
12060
|
+
registeredUaFamily: replaced?.registeredUaFamily ?? null,
|
|
11783
12061
|
isActive: true,
|
|
11784
|
-
expiresAt: getKeyExpiryDate()
|
|
12062
|
+
expiresAt: replaced?.binding === "passkey" ? replaced.expiresAt : getKeyExpiryDate("none")
|
|
11785
12063
|
});
|
|
11786
12064
|
await carryStepUpVerification(userId, oldKeyId, newKeyId);
|
|
11787
12065
|
return {
|
|
@@ -11809,7 +12087,9 @@ async function listKeysService(params) {
|
|
|
11809
12087
|
isActive: row.isActive,
|
|
11810
12088
|
revokedAtMillis: row.revokedAt?.getTime(),
|
|
11811
12089
|
registeredIp: row.registeredIp ?? void 0,
|
|
11812
|
-
registeredUserAgent: row.registeredUserAgent ?? void 0
|
|
12090
|
+
registeredUserAgent: row.registeredUserAgent ?? void 0,
|
|
12091
|
+
binding: row.binding === "passkey" ? row.binding : void 0,
|
|
12092
|
+
concurrentUseAtMillis: row.concurrentUseAt?.getTime()
|
|
11813
12093
|
}));
|
|
11814
12094
|
}
|
|
11815
12095
|
async function revokeAllKeysService(params) {
|
|
@@ -11826,6 +12106,9 @@ async function revokeAllKeysService(params) {
|
|
|
11826
12106
|
return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
|
|
11827
12107
|
}
|
|
11828
12108
|
|
|
12109
|
+
// src/server/services/auth.service.ts
|
|
12110
|
+
init_key_policy();
|
|
12111
|
+
|
|
11829
12112
|
// src/server/services/user.service.ts
|
|
11830
12113
|
init_repositories();
|
|
11831
12114
|
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
@@ -11940,6 +12223,7 @@ function getDeletionConfig() {
|
|
|
11940
12223
|
}
|
|
11941
12224
|
|
|
11942
12225
|
// src/server/services/account-deletion.service.ts
|
|
12226
|
+
init_logger();
|
|
11943
12227
|
var POSTGRES_UNIQUE_VIOLATION = "23505";
|
|
11944
12228
|
function isUniqueViolation(error) {
|
|
11945
12229
|
return typeof error === "object" && error !== null && error.code === POSTGRES_UNIQUE_VIOLATION;
|
|
@@ -12224,6 +12508,12 @@ async function sweepDuePurges(now = /* @__PURE__ */ new Date()) {
|
|
|
12224
12508
|
}
|
|
12225
12509
|
|
|
12226
12510
|
// src/server/services/auth.service.ts
|
|
12511
|
+
function loginBindingFields(registered) {
|
|
12512
|
+
if (registered.binding !== "passkey" || !registered.expiresAt) {
|
|
12513
|
+
return {};
|
|
12514
|
+
}
|
|
12515
|
+
return { sessionBinding: "passkey", keyExpiresAtMillis: registered.expiresAt.getTime() };
|
|
12516
|
+
}
|
|
12227
12517
|
async function registerService(params) {
|
|
12228
12518
|
const { email, verificationToken } = params;
|
|
12229
12519
|
const phone = params.phone?.trim();
|
|
@@ -12327,7 +12617,7 @@ async function loginService(params) {
|
|
|
12327
12617
|
});
|
|
12328
12618
|
replacesKeyId = revoked ? oldKeyId : void 0;
|
|
12329
12619
|
}
|
|
12330
|
-
await registerPublicKeyService({
|
|
12620
|
+
const registered = await registerPublicKeyService({
|
|
12331
12621
|
userId: user.id,
|
|
12332
12622
|
keyId,
|
|
12333
12623
|
publicKey,
|
|
@@ -12338,6 +12628,7 @@ async function loginService(params) {
|
|
|
12338
12628
|
channel: "password",
|
|
12339
12629
|
ip: params.ip,
|
|
12340
12630
|
userAgent: params.userAgent,
|
|
12631
|
+
binding: decideKeyBinding(user.sessionBinding, params.webProxy),
|
|
12341
12632
|
replacesKeyId
|
|
12342
12633
|
});
|
|
12343
12634
|
await updateLastLoginService(user.id);
|
|
@@ -12346,7 +12637,8 @@ async function loginService(params) {
|
|
|
12346
12637
|
publicId: user.publicId,
|
|
12347
12638
|
email: user.email || void 0,
|
|
12348
12639
|
phone: user.phone || void 0,
|
|
12349
|
-
passwordChangeRequired: user.passwordChangeRequired
|
|
12640
|
+
passwordChangeRequired: user.passwordChangeRequired,
|
|
12641
|
+
...loginBindingFields(registered)
|
|
12350
12642
|
};
|
|
12351
12643
|
await authLoginEvent.emit({
|
|
12352
12644
|
userId: result.userId,
|
|
@@ -12395,9 +12687,10 @@ async function changePasswordService(params) {
|
|
|
12395
12687
|
}
|
|
12396
12688
|
|
|
12397
12689
|
// src/server/services/signup-link.service.ts
|
|
12690
|
+
init_logger();
|
|
12691
|
+
init_repositories();
|
|
12398
12692
|
import { env as env10 } from "@spfn/auth/config";
|
|
12399
12693
|
import { InvalidSignupLinkError, InvalidSignupSetupSessionError } from "@spfn/auth/errors";
|
|
12400
|
-
init_repositories();
|
|
12401
12694
|
|
|
12402
12695
|
// src/lib/return-path.ts
|
|
12403
12696
|
var URL_STRIPPED_CHARACTER = /[\t\n\r]/;
|
|
@@ -12524,11 +12817,13 @@ async function completeSignupService(params) {
|
|
|
12524
12817
|
}
|
|
12525
12818
|
|
|
12526
12819
|
// src/server/services/password-reset.service.ts
|
|
12820
|
+
init_logger();
|
|
12821
|
+
init_repositories();
|
|
12527
12822
|
import { env as env11 } from "@spfn/auth/config";
|
|
12528
12823
|
import { PasswordResetLinkError, PasswordResetSessionError } from "@spfn/auth/errors";
|
|
12529
12824
|
import { ValidationError as ValidationError6 } from "@spfn/core/errors";
|
|
12530
12825
|
import { onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
|
|
12531
|
-
|
|
12826
|
+
init_key_policy();
|
|
12532
12827
|
async function activeUserOf(userId) {
|
|
12533
12828
|
const user = await usersRepository.findByIdOnPrimary(userId);
|
|
12534
12829
|
return user?.status === "active" ? user : null;
|
|
@@ -12591,7 +12886,7 @@ async function replaceCredentials(row, user, params) {
|
|
|
12591
12886
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
12592
12887
|
await revokeAllOAuth2GrantsForUser(user.id);
|
|
12593
12888
|
await keysRepository.revokeAllActiveByUserId(user.id, "Revoked by password reset");
|
|
12594
|
-
await registerPublicKeyService({
|
|
12889
|
+
const registered = await registerPublicKeyService({
|
|
12595
12890
|
userId: user.id,
|
|
12596
12891
|
keyId: params.keyId,
|
|
12597
12892
|
publicKey: params.publicKey,
|
|
@@ -12601,13 +12896,15 @@ async function replaceCredentials(row, user, params) {
|
|
|
12601
12896
|
platform: params.platform,
|
|
12602
12897
|
channel: "password-reset",
|
|
12603
12898
|
ip: params.ip,
|
|
12604
|
-
userAgent: params.userAgent
|
|
12899
|
+
userAgent: params.userAgent,
|
|
12900
|
+
binding: decideKeyBinding(user.sessionBinding, params.webProxy)
|
|
12605
12901
|
});
|
|
12606
12902
|
await updateLastLoginService(user.id);
|
|
12607
12903
|
if (!await passwordResetTokensRepository.complete(row.id)) {
|
|
12608
12904
|
authLogger.service.warn("Password reset session refused", { reason: "lost the claim race" });
|
|
12609
12905
|
throw new PasswordResetSessionError();
|
|
12610
12906
|
}
|
|
12907
|
+
return loginBindingFields(registered);
|
|
12611
12908
|
}
|
|
12612
12909
|
async function completePasswordResetService(params) {
|
|
12613
12910
|
if (!params.publicKey || !params.keyId || !params.fingerprint) {
|
|
@@ -12626,7 +12923,7 @@ async function completePasswordResetService(params) {
|
|
|
12626
12923
|
authLogger.service.warn("Password reset session refused", { reason: "account is no longer active" });
|
|
12627
12924
|
throw new PasswordResetSessionError();
|
|
12628
12925
|
}
|
|
12629
|
-
await replaceCredentials(row, user, params);
|
|
12926
|
+
const binding = await replaceCredentials(row, user, params);
|
|
12630
12927
|
onAfterCommit4(() => authPasswordResetEvent.emit({
|
|
12631
12928
|
userId: String(user.id),
|
|
12632
12929
|
email: row.email
|
|
@@ -12635,11 +12932,13 @@ async function completePasswordResetService(params) {
|
|
|
12635
12932
|
userId: String(user.id),
|
|
12636
12933
|
publicId: user.publicId,
|
|
12637
12934
|
email: user.email || void 0,
|
|
12638
|
-
phone: user.phone || void 0
|
|
12935
|
+
phone: user.phone || void 0,
|
|
12936
|
+
...binding
|
|
12639
12937
|
};
|
|
12640
12938
|
}
|
|
12641
12939
|
|
|
12642
12940
|
// src/server/services/revoke-all-link.service.ts
|
|
12941
|
+
init_logger();
|
|
12643
12942
|
import { env as env12 } from "@spfn/auth/config";
|
|
12644
12943
|
import { NotFoundError as NotFoundError3, ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
12645
12944
|
import { RevokeAllLinkError } from "@spfn/auth/errors";
|
|
@@ -12766,6 +13065,7 @@ function hashDeviceCode(deviceCode) {
|
|
|
12766
13065
|
}
|
|
12767
13066
|
|
|
12768
13067
|
// src/server/services/device-auth.service.ts
|
|
13068
|
+
init_key_policy();
|
|
12769
13069
|
var USER_CODE_ATTEMPTS = 3;
|
|
12770
13070
|
function assertActionable(record) {
|
|
12771
13071
|
if (!record || record.status === "consumed") {
|
|
@@ -12904,7 +13204,7 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
12904
13204
|
}
|
|
12905
13205
|
throw new AccountDisabledError2({ status: user.status });
|
|
12906
13206
|
}
|
|
12907
|
-
await registerPublicKeyService({
|
|
13207
|
+
const registered = await registerPublicKeyService({
|
|
12908
13208
|
userId: user.id,
|
|
12909
13209
|
keyId: record.keyId,
|
|
12910
13210
|
publicKey: record.publicKey,
|
|
@@ -12914,7 +13214,8 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
12914
13214
|
platform: record.platform ?? void 0,
|
|
12915
13215
|
channel: "device-code",
|
|
12916
13216
|
ip: provenance.ip,
|
|
12917
|
-
userAgent: provenance.userAgent
|
|
13217
|
+
userAgent: provenance.userAgent,
|
|
13218
|
+
binding: decideKeyBinding(user.sessionBinding, provenance.webProxy)
|
|
12918
13219
|
});
|
|
12919
13220
|
await updateLastLoginService(user.id);
|
|
12920
13221
|
const result = {
|
|
@@ -12922,7 +13223,8 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
12922
13223
|
publicId: user.publicId,
|
|
12923
13224
|
email: user.email || void 0,
|
|
12924
13225
|
phone: user.phone || void 0,
|
|
12925
|
-
passwordChangeRequired: user.passwordChangeRequired
|
|
13226
|
+
passwordChangeRequired: user.passwordChangeRequired,
|
|
13227
|
+
...loginBindingFields(registered)
|
|
12926
13228
|
};
|
|
12927
13229
|
const mfaEnrolled = await mfaEnrolledForUser(Number(result.userId));
|
|
12928
13230
|
onAfterCommit5(() => authLoginEvent.emit({
|
|
@@ -12936,6 +13238,8 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
12936
13238
|
}
|
|
12937
13239
|
|
|
12938
13240
|
// src/server/services/passkey.service.ts
|
|
13241
|
+
init_logger();
|
|
13242
|
+
init_config();
|
|
12939
13243
|
import { onAfterCommit as onAfterCommit6, runInTransaction as runInTransaction4 } from "@spfn/core/db";
|
|
12940
13244
|
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
12941
13245
|
import {
|
|
@@ -12949,6 +13253,7 @@ import {
|
|
|
12949
13253
|
RecentAuthenticationRequiredError
|
|
12950
13254
|
} from "@spfn/auth/errors";
|
|
12951
13255
|
init_repositories();
|
|
13256
|
+
init_key_policy();
|
|
12952
13257
|
var NO_PASSWORD_PRESENTED = "spfn-passkey-no-password-presented";
|
|
12953
13258
|
function toSummary(row) {
|
|
12954
13259
|
return {
|
|
@@ -13066,6 +13371,42 @@ async function startPasskeyLoginService() {
|
|
|
13066
13371
|
challenge: await mintChallenge("authentication", null)
|
|
13067
13372
|
});
|
|
13068
13373
|
}
|
|
13374
|
+
async function startRenewalCeremonyService(userId) {
|
|
13375
|
+
return await buildAuthenticationOptions({
|
|
13376
|
+
config: getPasskeyConfig(),
|
|
13377
|
+
challenge: await mintChallenge("renewal", userId)
|
|
13378
|
+
});
|
|
13379
|
+
}
|
|
13380
|
+
async function verifyRenewalAssertionService(userId, response) {
|
|
13381
|
+
const challenge = response?.response?.clientDataJSON ? presentedChallenge(response.response.clientDataJSON) : "";
|
|
13382
|
+
if (!await spendChallenge(challenge, "renewal", userId)) {
|
|
13383
|
+
authLogger.service.warn("Renewal challenge refused");
|
|
13384
|
+
return false;
|
|
13385
|
+
}
|
|
13386
|
+
return await assertionMatchesLivePasskey(userId, response, challenge);
|
|
13387
|
+
}
|
|
13388
|
+
async function assertionMatchesLivePasskey(userId, response, challenge) {
|
|
13389
|
+
const passkey = await passkeysRepository.findLiveByCredentialId(response.id);
|
|
13390
|
+
if (!passkey || passkey.userId !== userId) {
|
|
13391
|
+
return false;
|
|
13392
|
+
}
|
|
13393
|
+
const outcome = await verifyAuthentication({
|
|
13394
|
+
config: getPasskeyConfig(),
|
|
13395
|
+
response,
|
|
13396
|
+
expectedChallenge: challenge,
|
|
13397
|
+
credential: {
|
|
13398
|
+
credentialId: passkey.credentialId,
|
|
13399
|
+
publicKey: passkey.publicKey,
|
|
13400
|
+
counter: passkey.counter,
|
|
13401
|
+
transports: passkey.transports
|
|
13402
|
+
}
|
|
13403
|
+
});
|
|
13404
|
+
if (!outcome.verified) {
|
|
13405
|
+
return false;
|
|
13406
|
+
}
|
|
13407
|
+
await passkeysRepository.recordUse(passkey.id, outcome.newCounter);
|
|
13408
|
+
return true;
|
|
13409
|
+
}
|
|
13069
13410
|
async function finishPasskeyLoginService(params) {
|
|
13070
13411
|
assertDeviceKeyPresent(params);
|
|
13071
13412
|
return runInTransaction4(async () => {
|
|
@@ -13090,7 +13431,7 @@ async function startSession(user, params) {
|
|
|
13090
13431
|
});
|
|
13091
13432
|
replacesKeyId = revoked ? params.oldKeyId : void 0;
|
|
13092
13433
|
}
|
|
13093
|
-
await registerPublicKeyService({
|
|
13434
|
+
const registered = await registerPublicKeyService({
|
|
13094
13435
|
userId: user.id,
|
|
13095
13436
|
keyId: params.keyId,
|
|
13096
13437
|
publicKey: params.publicKey,
|
|
@@ -13101,6 +13442,7 @@ async function startSession(user, params) {
|
|
|
13101
13442
|
channel: "passkey",
|
|
13102
13443
|
ip: params.ip,
|
|
13103
13444
|
userAgent: params.userAgent,
|
|
13445
|
+
binding: decideKeyBinding(user.sessionBinding, params.webProxy),
|
|
13104
13446
|
replacesKeyId
|
|
13105
13447
|
});
|
|
13106
13448
|
await updateLastLoginService(user.id);
|
|
@@ -13109,7 +13451,8 @@ async function startSession(user, params) {
|
|
|
13109
13451
|
publicId: user.publicId,
|
|
13110
13452
|
email: user.email || void 0,
|
|
13111
13453
|
phone: user.phone || void 0,
|
|
13112
|
-
passwordChangeRequired: user.passwordChangeRequired
|
|
13454
|
+
passwordChangeRequired: user.passwordChangeRequired,
|
|
13455
|
+
...loginBindingFields(registered)
|
|
13113
13456
|
};
|
|
13114
13457
|
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13115
13458
|
onAfterCommit6(() => authLoginEvent.emit({
|
|
@@ -13206,6 +13549,8 @@ async function revokePasskeyService(params) {
|
|
|
13206
13549
|
// src/server/services/rbac.service.ts
|
|
13207
13550
|
init_repositories();
|
|
13208
13551
|
init_rbac();
|
|
13552
|
+
init_config();
|
|
13553
|
+
init_logger();
|
|
13209
13554
|
import { createHash as createHash2 } from "crypto";
|
|
13210
13555
|
var RBAC_HASH_KEY = "rbac_config_hash";
|
|
13211
13556
|
function computeConfigHash(allRoles, allPermissions, allMappings) {
|
|
@@ -13345,6 +13690,7 @@ async function syncMappings(allMappings, rolesByName, permsByName) {
|
|
|
13345
13690
|
}
|
|
13346
13691
|
|
|
13347
13692
|
// src/server/services/email-normalization.service.ts
|
|
13693
|
+
init_logger();
|
|
13348
13694
|
init_repositories();
|
|
13349
13695
|
var BACKFILL_KEY = "auth:email_normalization";
|
|
13350
13696
|
async function normalizeStoredEmails() {
|
|
@@ -13456,6 +13802,7 @@ init_role_service();
|
|
|
13456
13802
|
|
|
13457
13803
|
// src/server/services/invitation.service.ts
|
|
13458
13804
|
init_repositories();
|
|
13805
|
+
init_config();
|
|
13459
13806
|
import crypto10 from "crypto";
|
|
13460
13807
|
import { BadRequestError, NotFoundError as NotFoundError4, ConflictError } from "@spfn/core/errors";
|
|
13461
13808
|
function generateInvitationToken() {
|
|
@@ -13783,6 +14130,7 @@ async function updateUserProfileService(userId, params) {
|
|
|
13783
14130
|
|
|
13784
14131
|
// src/server/services/oauth.service.ts
|
|
13785
14132
|
init_repositories();
|
|
14133
|
+
init_logger();
|
|
13786
14134
|
import { env as env17 } from "@spfn/auth/config";
|
|
13787
14135
|
import { ValidationError as ValidationError13 } from "@spfn/core/errors";
|
|
13788
14136
|
import {
|
|
@@ -13790,6 +14138,7 @@ import {
|
|
|
13790
14138
|
AccountPendingDeletionError as AccountPendingDeletionError4,
|
|
13791
14139
|
UnverifiedEmailLinkError
|
|
13792
14140
|
} from "@spfn/auth/errors";
|
|
14141
|
+
init_config();
|
|
13793
14142
|
|
|
13794
14143
|
// src/server/lib/oauth/google.ts
|
|
13795
14144
|
import { env as env13 } from "@spfn/auth/config";
|
|
@@ -13951,6 +14300,7 @@ function getRegisteredProviders() {
|
|
|
13951
14300
|
}
|
|
13952
14301
|
|
|
13953
14302
|
// src/server/lib/oauth/jwks-verify.ts
|
|
14303
|
+
init_logger();
|
|
13954
14304
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
13955
14305
|
import { InvalidSocialTokenError } from "@spfn/auth/errors";
|
|
13956
14306
|
var CLOCK_TOLERANCE_SECONDS = 30;
|
|
@@ -14121,7 +14471,7 @@ var appleProvider = {
|
|
|
14121
14471
|
registerOAuthProvider(appleProvider);
|
|
14122
14472
|
|
|
14123
14473
|
// src/server/lib/oauth/github-provider.ts
|
|
14124
|
-
|
|
14474
|
+
init_config2();
|
|
14125
14475
|
import { ValidationError as ValidationError10 } from "@spfn/core/errors";
|
|
14126
14476
|
var GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize";
|
|
14127
14477
|
var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
@@ -14130,22 +14480,22 @@ var GITHUB_EMAILS_URL = "https://api.github.com/user/emails";
|
|
|
14130
14480
|
var GITHUB_USER_AGENT = "spfn-auth";
|
|
14131
14481
|
var GITHUB_DEFAULT_EXPIRES_IN = 60 * 60 * 24 * 365;
|
|
14132
14482
|
function getGithubConfig() {
|
|
14133
|
-
const clientId =
|
|
14134
|
-
const clientSecret =
|
|
14483
|
+
const clientId = env4.SPFN_AUTH_GITHUB_CLIENT_ID;
|
|
14484
|
+
const clientSecret = env4.SPFN_AUTH_GITHUB_CLIENT_SECRET;
|
|
14135
14485
|
if (!clientId || !clientSecret) {
|
|
14136
14486
|
throw new ValidationError10({
|
|
14137
14487
|
message: "GitHub OAuth is not configured. Set SPFN_AUTH_GITHUB_CLIENT_ID and SPFN_AUTH_GITHUB_CLIENT_SECRET."
|
|
14138
14488
|
});
|
|
14139
14489
|
}
|
|
14140
|
-
const baseUrl =
|
|
14490
|
+
const baseUrl = env4.NEXT_PUBLIC_SPFN_APP_URL || env4.SPFN_APP_URL;
|
|
14141
14491
|
return {
|
|
14142
14492
|
clientId,
|
|
14143
14493
|
clientSecret,
|
|
14144
|
-
redirectUri:
|
|
14494
|
+
redirectUri: env4.SPFN_AUTH_GITHUB_REDIRECT_URI || `${baseUrl}/_auth/oauth/github/callback`
|
|
14145
14495
|
};
|
|
14146
14496
|
}
|
|
14147
14497
|
function getGithubScopes() {
|
|
14148
|
-
const configured2 =
|
|
14498
|
+
const configured2 = env4.SPFN_AUTH_GITHUB_SCOPES;
|
|
14149
14499
|
return configured2 ? configured2.split(",").map((scope) => scope.trim()).filter(Boolean) : ["read:user", "user:email"];
|
|
14150
14500
|
}
|
|
14151
14501
|
async function requestGithubTokens(params) {
|
|
@@ -14195,7 +14545,7 @@ async function fetchPrimaryEmail(accessToken) {
|
|
|
14195
14545
|
var githubProvider = {
|
|
14196
14546
|
id: "github",
|
|
14197
14547
|
isEnabled() {
|
|
14198
|
-
return !!(
|
|
14548
|
+
return !!(env4.SPFN_AUTH_GITHUB_CLIENT_ID && env4.SPFN_AUTH_GITHUB_CLIENT_SECRET);
|
|
14199
14549
|
},
|
|
14200
14550
|
getAuthUrl(state, scopes) {
|
|
14201
14551
|
const config4 = getGithubConfig();
|
|
@@ -14254,7 +14604,8 @@ var githubProvider = {
|
|
|
14254
14604
|
registerOAuthProvider(githubProvider);
|
|
14255
14605
|
|
|
14256
14606
|
// src/server/lib/oauth/kakao-provider.ts
|
|
14257
|
-
|
|
14607
|
+
init_config2();
|
|
14608
|
+
init_logger();
|
|
14258
14609
|
import { ValidationError as ValidationError11 } from "@spfn/core/errors";
|
|
14259
14610
|
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError3 } from "@spfn/auth/errors";
|
|
14260
14611
|
import { timingSafeEqual } from "crypto";
|
|
@@ -14265,28 +14616,28 @@ var KAKAO_ISSUER = "https://kauth.kakao.com";
|
|
|
14265
14616
|
var KAKAO_JWKS_URI = "https://kauth.kakao.com/.well-known/jwks.json";
|
|
14266
14617
|
var USERINFO_TIMEOUT_MS = 5e3;
|
|
14267
14618
|
function getKakaoConfig() {
|
|
14268
|
-
const clientId =
|
|
14269
|
-
const clientSecret =
|
|
14619
|
+
const clientId = env4.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
14620
|
+
const clientSecret = env4.SPFN_AUTH_KAKAO_CLIENT_SECRET;
|
|
14270
14621
|
if (!clientId) {
|
|
14271
14622
|
throw new ValidationError11({
|
|
14272
14623
|
message: "Kakao OAuth is not configured. Set SPFN_AUTH_KAKAO_CLIENT_ID."
|
|
14273
14624
|
});
|
|
14274
14625
|
}
|
|
14275
|
-
const baseUrl =
|
|
14626
|
+
const baseUrl = env4.NEXT_PUBLIC_SPFN_APP_URL || env4.SPFN_APP_URL;
|
|
14276
14627
|
return {
|
|
14277
14628
|
clientId,
|
|
14278
14629
|
clientSecret,
|
|
14279
|
-
redirectUri:
|
|
14630
|
+
redirectUri: env4.SPFN_AUTH_KAKAO_REDIRECT_URI || `${baseUrl}/_auth/oauth/kakao/callback`
|
|
14280
14631
|
};
|
|
14281
14632
|
}
|
|
14282
14633
|
function getKakaoScopes() {
|
|
14283
|
-
const configured2 =
|
|
14634
|
+
const configured2 = env4.SPFN_AUTH_KAKAO_SCOPES;
|
|
14284
14635
|
return configured2 ? configured2.split(",").map((scope) => scope.trim()).filter(Boolean) : ["account_email"];
|
|
14285
14636
|
}
|
|
14286
14637
|
function getKakaoNativeAudiences() {
|
|
14287
|
-
const ids = (
|
|
14288
|
-
if (
|
|
14289
|
-
ids.push(
|
|
14638
|
+
const ids = (env4.SPFN_AUTH_KAKAO_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
14639
|
+
if (env4.SPFN_AUTH_KAKAO_CLIENT_ID) {
|
|
14640
|
+
ids.push(env4.SPFN_AUTH_KAKAO_CLIENT_ID);
|
|
14290
14641
|
}
|
|
14291
14642
|
return ids;
|
|
14292
14643
|
}
|
|
@@ -14366,7 +14717,7 @@ var kakaoProvider = {
|
|
|
14366
14717
|
* 실패하므로, 판정을 웹 키에 그대로 둔다.
|
|
14367
14718
|
*/
|
|
14368
14719
|
isEnabled() {
|
|
14369
|
-
return !!
|
|
14720
|
+
return !!env4.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
14370
14721
|
},
|
|
14371
14722
|
getAuthUrl(state, scopes) {
|
|
14372
14723
|
const config4 = getKakaoConfig();
|
|
@@ -14449,7 +14800,7 @@ var kakaoProvider = {
|
|
|
14449
14800
|
* 본문 필드는 app_id · user_id · referrer_type.
|
|
14450
14801
|
*/
|
|
14451
14802
|
async verifyUnlinkNotification(request) {
|
|
14452
|
-
const adminKey =
|
|
14803
|
+
const adminKey = env4.SPFN_AUTH_KAKAO_ADMIN_KEY;
|
|
14453
14804
|
if (!adminKey) {
|
|
14454
14805
|
throw new UnlinkNotifyRejection(401, "SPFN_AUTH_KAKAO_ADMIN_KEY is not configured");
|
|
14455
14806
|
}
|
|
@@ -14471,7 +14822,8 @@ var kakaoProvider = {
|
|
|
14471
14822
|
registerOAuthProvider(kakaoProvider);
|
|
14472
14823
|
|
|
14473
14824
|
// src/server/lib/oauth/naver-provider.ts
|
|
14474
|
-
|
|
14825
|
+
init_config2();
|
|
14826
|
+
init_logger();
|
|
14475
14827
|
import { ValidationError as ValidationError12 } from "@spfn/core/errors";
|
|
14476
14828
|
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError4 } from "@spfn/auth/errors";
|
|
14477
14829
|
import { createDecipheriv, createHash as createHash4, createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
@@ -14482,24 +14834,24 @@ var NAVER_ISSUER = "https://nid.naver.com";
|
|
|
14482
14834
|
var NAVER_JWKS_URI = "https://nid.naver.com/oauth2/jwks";
|
|
14483
14835
|
var USERINFO_TIMEOUT_MS2 = 5e3;
|
|
14484
14836
|
function getNaverConfig() {
|
|
14485
|
-
const clientId =
|
|
14486
|
-
const clientSecret =
|
|
14837
|
+
const clientId = env4.SPFN_AUTH_NAVER_CLIENT_ID;
|
|
14838
|
+
const clientSecret = env4.SPFN_AUTH_NAVER_CLIENT_SECRET;
|
|
14487
14839
|
if (!clientId || !clientSecret) {
|
|
14488
14840
|
throw new ValidationError12({
|
|
14489
14841
|
message: "Naver OAuth is not configured. Set SPFN_AUTH_NAVER_CLIENT_ID and SPFN_AUTH_NAVER_CLIENT_SECRET."
|
|
14490
14842
|
});
|
|
14491
14843
|
}
|
|
14492
|
-
const baseUrl =
|
|
14844
|
+
const baseUrl = env4.NEXT_PUBLIC_SPFN_APP_URL || env4.SPFN_APP_URL;
|
|
14493
14845
|
return {
|
|
14494
14846
|
clientId,
|
|
14495
14847
|
clientSecret,
|
|
14496
|
-
redirectUri:
|
|
14848
|
+
redirectUri: env4.SPFN_AUTH_NAVER_REDIRECT_URI || `${baseUrl}/_auth/oauth/naver/callback`
|
|
14497
14849
|
};
|
|
14498
14850
|
}
|
|
14499
14851
|
function getNaverNativeAudiences() {
|
|
14500
|
-
const ids = (
|
|
14501
|
-
if (
|
|
14502
|
-
ids.push(
|
|
14852
|
+
const ids = (env4.SPFN_AUTH_NAVER_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
14853
|
+
if (env4.SPFN_AUTH_NAVER_CLIENT_ID) {
|
|
14854
|
+
ids.push(env4.SPFN_AUTH_NAVER_CLIENT_ID);
|
|
14503
14855
|
}
|
|
14504
14856
|
return ids;
|
|
14505
14857
|
}
|
|
@@ -14592,7 +14944,7 @@ async function withNaverProfile(identity, accessToken) {
|
|
|
14592
14944
|
var naverProvider = {
|
|
14593
14945
|
id: "naver",
|
|
14594
14946
|
isEnabled() {
|
|
14595
|
-
return !!(
|
|
14947
|
+
return !!(env4.SPFN_AUTH_NAVER_CLIENT_ID && env4.SPFN_AUTH_NAVER_CLIENT_SECRET);
|
|
14596
14948
|
},
|
|
14597
14949
|
getAuthUrl(state) {
|
|
14598
14950
|
const config4 = getNaverConfig();
|
|
@@ -14674,8 +15026,8 @@ var naverProvider = {
|
|
|
14674
15026
|
* 삭제가 멱등이라는 점에 의존한다.
|
|
14675
15027
|
*/
|
|
14676
15028
|
async verifyUnlinkNotification(request) {
|
|
14677
|
-
const clientId =
|
|
14678
|
-
const clientSecret =
|
|
15029
|
+
const clientId = env4.SPFN_AUTH_NAVER_CLIENT_ID;
|
|
15030
|
+
const clientSecret = env4.SPFN_AUTH_NAVER_CLIENT_SECRET;
|
|
14679
15031
|
if (!clientId || !clientSecret) {
|
|
14680
15032
|
throw new UnlinkNotifyRejection(403, "Naver OAuth is not configured");
|
|
14681
15033
|
}
|
|
@@ -14703,6 +15055,7 @@ var naverProvider = {
|
|
|
14703
15055
|
registerOAuthProvider(naverProvider);
|
|
14704
15056
|
|
|
14705
15057
|
// src/server/services/oauth.service.ts
|
|
15058
|
+
init_key_policy();
|
|
14706
15059
|
function requireEnabledProvider(provider) {
|
|
14707
15060
|
const oauthProvider = getOAuthProvider(provider);
|
|
14708
15061
|
if (!oauthProvider) {
|
|
@@ -14777,7 +15130,8 @@ async function oauthCallbackService(params) {
|
|
|
14777
15130
|
isNewUser = result.isNewUser;
|
|
14778
15131
|
}
|
|
14779
15132
|
await assertActiveForOAuthSession(userId);
|
|
14780
|
-
await
|
|
15133
|
+
const user = await usersRepository.findById(userId);
|
|
15134
|
+
const registered = await registerPublicKeyService({
|
|
14781
15135
|
userId,
|
|
14782
15136
|
keyId: stateData.keyId,
|
|
14783
15137
|
publicKey: stateData.publicKey,
|
|
@@ -14785,7 +15139,8 @@ async function oauthCallbackService(params) {
|
|
|
14785
15139
|
algorithm: stateData.algorithm,
|
|
14786
15140
|
channel: "oauth",
|
|
14787
15141
|
ip: params.ip,
|
|
14788
|
-
userAgent: params.userAgent
|
|
15142
|
+
userAgent: params.userAgent,
|
|
15143
|
+
binding: decideKeyBinding(user?.sessionBinding ?? "none", params.webProxy)
|
|
14789
15144
|
});
|
|
14790
15145
|
await updateLastLoginService(userId);
|
|
14791
15146
|
const appUrl = env17.NEXT_PUBLIC_SPFN_APP_URL || env17.SPFN_APP_URL;
|
|
@@ -14799,9 +15154,15 @@ async function oauthCallbackService(params) {
|
|
|
14799
15154
|
// destination that leaves the app out of the callback URL no matter which
|
|
14800
15155
|
// seam sealed it.
|
|
14801
15156
|
returnUrl: isSafeReturnPath(stateData.returnUrl) ? stateData.returnUrl : "/",
|
|
14802
|
-
isNewUser: String(isNewUser)
|
|
15157
|
+
isNewUser: String(isNewUser),
|
|
15158
|
+
// The callback page and `createOAuthCallbackHandler` are the two seams
|
|
15159
|
+
// that seal a session out of this redirect, and neither calls a route
|
|
15160
|
+
// that could tell them the key is bound. The two values ride the query
|
|
15161
|
+
// for the same reason `userId` and `keyId` do — and, like those, nothing
|
|
15162
|
+
// is authorized by them: the key row already expires when it says it
|
|
15163
|
+
// does, whatever a tampered query claims the cookie should believe.
|
|
15164
|
+
...bindingRedirectParams(registered)
|
|
14803
15165
|
});
|
|
14804
|
-
const user = await usersRepository.findById(userId);
|
|
14805
15166
|
const eventPayload = {
|
|
14806
15167
|
userId: String(userId),
|
|
14807
15168
|
provider,
|
|
@@ -14910,6 +15271,15 @@ async function createOrLinkUser(provider, identity, tokens, metadata) {
|
|
|
14910
15271
|
});
|
|
14911
15272
|
return { userId, isNewUser };
|
|
14912
15273
|
}
|
|
15274
|
+
function bindingRedirectParams(registered) {
|
|
15275
|
+
if (registered.binding !== "passkey" || !registered.expiresAt) {
|
|
15276
|
+
return {};
|
|
15277
|
+
}
|
|
15278
|
+
return {
|
|
15279
|
+
sessionBinding: registered.binding,
|
|
15280
|
+
keyExpiresAtMillis: String(registered.expiresAt.getTime())
|
|
15281
|
+
};
|
|
15282
|
+
}
|
|
14913
15283
|
function buildRedirectUrl(baseUrl, params) {
|
|
14914
15284
|
const url = new URL(baseUrl, "http://placeholder");
|
|
14915
15285
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -15052,6 +15422,7 @@ async function persistNativeLogin(identity, params) {
|
|
|
15052
15422
|
|
|
15053
15423
|
// src/server/services/ops-token.service.ts
|
|
15054
15424
|
init_ops_tokens_repository();
|
|
15425
|
+
init_logger();
|
|
15055
15426
|
import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
|
|
15056
15427
|
var OPS_TOKEN_PREFIX = "spfn_ops_";
|
|
15057
15428
|
function isOpsToken(bearer) {
|
|
@@ -15106,6 +15477,7 @@ init_oauth2_clients_repository();
|
|
|
15106
15477
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
15107
15478
|
|
|
15108
15479
|
// src/server/lib/oauth2/config.ts
|
|
15480
|
+
init_logger();
|
|
15109
15481
|
var DEFAULT_ACCESS_TOKEN_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
15110
15482
|
var DEFAULT_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
15111
15483
|
var DEFAULT_CODE_TTL_MS = 60 * 1e3;
|
|
@@ -15367,6 +15739,7 @@ function secondsUntil(at, from = /* @__PURE__ */ new Date()) {
|
|
|
15367
15739
|
}
|
|
15368
15740
|
|
|
15369
15741
|
// src/server/services/oauth2-client.service.ts
|
|
15742
|
+
init_logger();
|
|
15370
15743
|
var MAX_UNGRANTED_CLIENTS_PER_IP = 20;
|
|
15371
15744
|
var UNGRANTED_CLIENT_WINDOW_MS = 60 * 60 * 1e3;
|
|
15372
15745
|
var STALE_CLIENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -15615,6 +15988,7 @@ init_oauth2_grants_repository();
|
|
|
15615
15988
|
init_oauth2_authorization_codes_repository();
|
|
15616
15989
|
init_oauth2_tokens_repository();
|
|
15617
15990
|
import { runInTransaction as runInTransaction6 } from "@spfn/core/db";
|
|
15991
|
+
init_logger();
|
|
15618
15992
|
function invalidGrant() {
|
|
15619
15993
|
return {
|
|
15620
15994
|
ok: false,
|
|
@@ -15805,6 +16179,7 @@ async function issuedToClient(grantId, clientId) {
|
|
|
15805
16179
|
// src/server/services/oauth2-access-token.service.ts
|
|
15806
16180
|
init_oauth2_grants_repository();
|
|
15807
16181
|
init_oauth2_tokens_repository();
|
|
16182
|
+
init_logger();
|
|
15808
16183
|
async function verifyAccessToken(token, resource) {
|
|
15809
16184
|
if (!isAccessTokenShaped(token)) {
|
|
15810
16185
|
return null;
|
|
@@ -15830,6 +16205,149 @@ function isUsable(record) {
|
|
|
15830
16205
|
return record.kind === "access" && record.revokedAt === null && record.expiresAt.getTime() > Date.now();
|
|
15831
16206
|
}
|
|
15832
16207
|
|
|
16208
|
+
// src/server/services/session-binding.service.ts
|
|
16209
|
+
init_config();
|
|
16210
|
+
init_repositories();
|
|
16211
|
+
init_key_policy();
|
|
16212
|
+
import { RecentAuthenticationRequiredError as RecentAuthenticationRequiredError2, SessionBindingUnavailableError } from "@spfn/auth/errors";
|
|
16213
|
+
import { ValidationError as ValidationError14 } from "@spfn/core/errors";
|
|
16214
|
+
async function getSessionBindingService(params) {
|
|
16215
|
+
const user = await usersRepository.findById(params.userId);
|
|
16216
|
+
const key = await keysRepository.findByKeyIdAndUserId(params.keyId, params.userId);
|
|
16217
|
+
return {
|
|
16218
|
+
mode: user?.sessionBinding ?? "none",
|
|
16219
|
+
keyExpiresAtMillis: key?.binding === "passkey" ? key.expiresAt?.getTime() : void 0
|
|
16220
|
+
};
|
|
16221
|
+
}
|
|
16222
|
+
async function keySessionBindingService(keyId) {
|
|
16223
|
+
const key = await keysRepository.findByKeyId(keyId);
|
|
16224
|
+
if (key?.binding !== "passkey" || !key.expiresAt) {
|
|
16225
|
+
return {};
|
|
16226
|
+
}
|
|
16227
|
+
return { sessionBinding: "passkey", keyExpiresAtMillis: key.expiresAt.getTime() };
|
|
16228
|
+
}
|
|
16229
|
+
async function enableSessionBindingService(params) {
|
|
16230
|
+
if (!params.webProxy) {
|
|
16231
|
+
throw new SessionBindingUnavailableError();
|
|
16232
|
+
}
|
|
16233
|
+
const live = await passkeysRepository.listLiveByUserId(params.userId);
|
|
16234
|
+
if (live.length === 0) {
|
|
16235
|
+
throw new ValidationError14({
|
|
16236
|
+
message: "Session binding needs a passkey to renew with. Enroll one first."
|
|
16237
|
+
});
|
|
16238
|
+
}
|
|
16239
|
+
await assertRecentAuthentication({ userId: params.userId, keyId: params.keyId });
|
|
16240
|
+
const user = await usersRepository.findById(params.userId);
|
|
16241
|
+
if (user?.sessionBinding === "passkey") {
|
|
16242
|
+
return await getSessionBindingService(params);
|
|
16243
|
+
}
|
|
16244
|
+
await usersRepository.updateById(params.userId, {
|
|
16245
|
+
sessionBinding: "passkey",
|
|
16246
|
+
sessionBindingChangedAt: /* @__PURE__ */ new Date()
|
|
16247
|
+
});
|
|
16248
|
+
const bound = await keysRepository.bindByKeyIdAndUserId(
|
|
16249
|
+
params.keyId,
|
|
16250
|
+
params.userId,
|
|
16251
|
+
new Date(Date.now() + getBoundKeyTtlMs())
|
|
16252
|
+
);
|
|
16253
|
+
return { mode: "passkey", keyExpiresAtMillis: bound?.expiresAt?.getTime() };
|
|
16254
|
+
}
|
|
16255
|
+
async function disableSessionBindingService(params) {
|
|
16256
|
+
if (!await provedOwnership(params)) {
|
|
16257
|
+
throw new RecentAuthenticationRequiredError2({
|
|
16258
|
+
message: "Turning session binding off needs a passkey or your current password."
|
|
16259
|
+
});
|
|
16260
|
+
}
|
|
16261
|
+
await usersRepository.updateById(params.userId, {
|
|
16262
|
+
sessionBinding: "none",
|
|
16263
|
+
sessionBindingChangedAt: /* @__PURE__ */ new Date()
|
|
16264
|
+
});
|
|
16265
|
+
const unbound = /* @__PURE__ */ new Date();
|
|
16266
|
+
unbound.setDate(unbound.getDate() + KEY_TTL_DAYS);
|
|
16267
|
+
await keysRepository.unbindActiveByUserId(params.userId, unbound);
|
|
16268
|
+
return { mode: "none" };
|
|
16269
|
+
}
|
|
16270
|
+
async function startSessionBindingDisableService(userId) {
|
|
16271
|
+
return await startRenewalCeremonyService(userId);
|
|
16272
|
+
}
|
|
16273
|
+
async function provedOwnership(params) {
|
|
16274
|
+
if (params.response) {
|
|
16275
|
+
return await verifyRenewalAssertionService(params.userId, params.response);
|
|
16276
|
+
}
|
|
16277
|
+
const user = await usersRepository.findById(params.userId);
|
|
16278
|
+
const storedHash = user?.passwordHash;
|
|
16279
|
+
const matched = await verifyPassword(
|
|
16280
|
+
params.currentPassword || NO_PASSWORD_PRESENTED2,
|
|
16281
|
+
storedHash ?? await getDummyPasswordHash()
|
|
16282
|
+
);
|
|
16283
|
+
return Boolean(storedHash) && matched;
|
|
16284
|
+
}
|
|
16285
|
+
var NO_PASSWORD_PRESENTED2 = "spfn-session-binding-no-password-presented";
|
|
16286
|
+
|
|
16287
|
+
// src/server/services/session-renew.service.ts
|
|
16288
|
+
init_config();
|
|
16289
|
+
init_repositories();
|
|
16290
|
+
import { SessionRenewalRefusedError } from "@spfn/auth/errors";
|
|
16291
|
+
var RENEWAL_REVOCATION_REASON = "Replaced by bound-session renewal";
|
|
16292
|
+
async function startSessionRenewService(params) {
|
|
16293
|
+
const { key } = await admitForRenewal(params.expiredKeyId);
|
|
16294
|
+
return await startRenewalCeremonyService(key.userId);
|
|
16295
|
+
}
|
|
16296
|
+
async function finishSessionRenewService(params) {
|
|
16297
|
+
const { key, user } = await admitForRenewal(params.expiredKeyId);
|
|
16298
|
+
if (!await verifyRenewalAssertionService(key.userId, params.response)) {
|
|
16299
|
+
throw new SessionRenewalRefusedError();
|
|
16300
|
+
}
|
|
16301
|
+
const revoked = await revokeKeyService({
|
|
16302
|
+
userId: key.userId,
|
|
16303
|
+
keyId: key.keyId,
|
|
16304
|
+
reason: RENEWAL_REVOCATION_REASON
|
|
16305
|
+
});
|
|
16306
|
+
if (!revoked) {
|
|
16307
|
+
throw new SessionRenewalRefusedError();
|
|
16308
|
+
}
|
|
16309
|
+
const registered = await registerPublicKeyService({
|
|
16310
|
+
userId: key.userId,
|
|
16311
|
+
keyId: params.keyId,
|
|
16312
|
+
publicKey: params.publicKey,
|
|
16313
|
+
fingerprint: params.fingerprint,
|
|
16314
|
+
algorithm: params.algorithm,
|
|
16315
|
+
deviceName: key.deviceName ?? void 0,
|
|
16316
|
+
platform: key.platform ?? void 0,
|
|
16317
|
+
channel: "renewal",
|
|
16318
|
+
// The old row's provenance, not this request's: renewing is not appearing
|
|
16319
|
+
// for the first time. Passing the recorded `user-agent` also re-derives
|
|
16320
|
+
// the same family, so `registered_ua_family` carries over unchanged.
|
|
16321
|
+
ip: key.registeredIp ?? void 0,
|
|
16322
|
+
userAgent: key.registeredUserAgent ?? void 0,
|
|
16323
|
+
binding: "passkey",
|
|
16324
|
+
replacesKeyId: key.keyId
|
|
16325
|
+
});
|
|
16326
|
+
return {
|
|
16327
|
+
keyId: params.keyId,
|
|
16328
|
+
userId: String(user.id),
|
|
16329
|
+
publicId: user.publicId,
|
|
16330
|
+
email: user.email || void 0,
|
|
16331
|
+
phone: user.phone || void 0,
|
|
16332
|
+
passwordChangeRequired: user.passwordChangeRequired,
|
|
16333
|
+
...loginBindingFields(registered)
|
|
16334
|
+
};
|
|
16335
|
+
}
|
|
16336
|
+
async function admitForRenewal(expiredKeyId) {
|
|
16337
|
+
const key = expiredKeyId ? await keysRepository.findByKeyId(expiredKeyId) : null;
|
|
16338
|
+
if (!key || !key.isActive || key.binding !== "passkey" || !key.expiresAt) {
|
|
16339
|
+
throw new SessionRenewalRefusedError();
|
|
16340
|
+
}
|
|
16341
|
+
if (Date.now() > key.expiresAt.getTime() + getBoundKeyRenewGraceMs()) {
|
|
16342
|
+
throw new SessionRenewalRefusedError();
|
|
16343
|
+
}
|
|
16344
|
+
const user = await usersRepository.findById(key.userId);
|
|
16345
|
+
if (!user || user.status !== "active") {
|
|
16346
|
+
throw new SessionRenewalRefusedError();
|
|
16347
|
+
}
|
|
16348
|
+
return { key, user };
|
|
16349
|
+
}
|
|
16350
|
+
|
|
15833
16351
|
// src/server/routes/auth/index.ts
|
|
15834
16352
|
init_esm();
|
|
15835
16353
|
import { Transactional } from "@spfn/core/db";
|
|
@@ -15919,9 +16437,14 @@ function deviceProvenance(c) {
|
|
|
15919
16437
|
const userAgent = c.req.header("user-agent");
|
|
15920
16438
|
return {
|
|
15921
16439
|
ip: ip === "unknown" ? void 0 : ip,
|
|
15922
|
-
userAgent: userAgent?.slice(0, REGISTERED_USER_AGENT_MAX_LENGTH)
|
|
16440
|
+
userAgent: userAgent?.slice(0, REGISTERED_USER_AGENT_MAX_LENGTH),
|
|
16441
|
+
webProxy: c.get("clientType") === "web"
|
|
15923
16442
|
};
|
|
15924
16443
|
}
|
|
16444
|
+
function attestedClientIp(c) {
|
|
16445
|
+
const provenance = deviceProvenance(c);
|
|
16446
|
+
return provenance.webProxy ? provenance.ip ?? null : null;
|
|
16447
|
+
}
|
|
15925
16448
|
|
|
15926
16449
|
// src/server/routes/auth/index.ts
|
|
15927
16450
|
var sendVerificationCode = route.post("/_auth/codes").input({
|
|
@@ -15994,7 +16517,7 @@ var requestSignupLink = route.post("/_auth/signup/email").input({
|
|
|
15994
16517
|
}).use([rateLimitPolicy("auth-signup-link", { limit: 5, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 20 }) })]).skip(["auth"]).handler(async (c) => {
|
|
15995
16518
|
const { body } = await c.data();
|
|
15996
16519
|
if (body.returnPath !== void 0 && !isSafeReturnPath(body.returnPath)) {
|
|
15997
|
-
throw new
|
|
16520
|
+
throw new ValidationError15({ message: "returnPath must be a relative path within the app" });
|
|
15998
16521
|
}
|
|
15999
16522
|
return await requestSignupLinkService(body);
|
|
16000
16523
|
});
|
|
@@ -16251,7 +16774,7 @@ init_esm();
|
|
|
16251
16774
|
init_schema3();
|
|
16252
16775
|
init_types();
|
|
16253
16776
|
import { Transactional as Transactional2 } from "@spfn/core/db";
|
|
16254
|
-
import { ValidationError as
|
|
16777
|
+
import { ValidationError as ValidationError16 } from "@spfn/core/errors";
|
|
16255
16778
|
import { rateLimitPolicy as rateLimitPolicy2 } from "@spfn/core/middleware";
|
|
16256
16779
|
import { route as route2 } from "@spfn/core/route";
|
|
16257
16780
|
var requestPasswordReset = route2.post("/_auth/password/reset").input({
|
|
@@ -16265,7 +16788,7 @@ var requestPasswordReset = route2.post("/_auth/password/reset").input({
|
|
|
16265
16788
|
}).use([rateLimitPolicy2("auth-password-reset", { limit: 5, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 20 }) })]).skip(["auth"]).handler(async (c) => {
|
|
16266
16789
|
const { body } = await c.data();
|
|
16267
16790
|
if (body.returnPath !== void 0 && !isSafeReturnPath(body.returnPath)) {
|
|
16268
|
-
throw new
|
|
16791
|
+
throw new ValidationError16({ message: "returnPath must be a relative path within the app" });
|
|
16269
16792
|
}
|
|
16270
16793
|
return await requestPasswordResetService(body);
|
|
16271
16794
|
});
|
|
@@ -16418,130 +16941,75 @@ var mfaStepUpOptions = route4.post("/_auth/mfa/step-up/options").input({
|
|
|
16418
16941
|
return await startStepUpService(Number(userId));
|
|
16419
16942
|
});
|
|
16420
16943
|
|
|
16421
|
-
// src/server/routes/auth/
|
|
16944
|
+
// src/server/routes/auth/session-binding.ts
|
|
16422
16945
|
init_esm();
|
|
16423
16946
|
import { Transactional as Transactional4 } from "@spfn/core/db";
|
|
16424
16947
|
import { rateLimitPolicy as rateLimitPolicy5 } from "@spfn/core/middleware";
|
|
16425
16948
|
import { route as route5 } from "@spfn/core/route";
|
|
16426
|
-
init_types();
|
|
16427
|
-
init_passkeys();
|
|
16428
|
-
init_schema3();
|
|
16429
16949
|
var CredentialResponseSchema = Type.Unknown({
|
|
16430
16950
|
description: "The credential from @simplewebauthn/browser, passed through unchanged"
|
|
16431
16951
|
});
|
|
16432
|
-
var PasskeyIdSchema2 = Type.String({
|
|
16433
|
-
pattern: "^[0-9]{1,19}$",
|
|
16434
|
-
description: "Passkey identifier, as returned by list"
|
|
16435
|
-
});
|
|
16436
|
-
var PasskeyLabelSchema = Type.String({
|
|
16437
|
-
minLength: 1,
|
|
16438
|
-
maxLength: PASSKEY_LABEL_MAX_LENGTH,
|
|
16439
|
-
description: `Owner-facing name in the passkey list (1-${PASSKEY_LABEL_MAX_LENGTH} chars)`
|
|
16440
|
-
});
|
|
16441
16952
|
var CurrentPasswordSchema = Type.String({
|
|
16442
16953
|
minLength: 1,
|
|
16443
|
-
description: "Account password
|
|
16444
|
-
});
|
|
16445
|
-
var passkeyRegisterOptions = route5.post("/_auth/passkeys/register/options").input({
|
|
16446
|
-
body: Type.Object({
|
|
16447
|
-
currentPassword: Type.Optional(CurrentPasswordSchema)
|
|
16448
|
-
})
|
|
16449
|
-
}).use([rateLimitPolicy5("auth-passkey-register-options", {
|
|
16450
|
-
limit: 10,
|
|
16451
|
-
windowMs: 6e4,
|
|
16452
|
-
by: byIpAndCaller({ ipLimit: 100 })
|
|
16453
|
-
})]).handler(async (c) => {
|
|
16454
|
-
const { body } = await c.data();
|
|
16455
|
-
const { userId, keyId } = getAuth(c);
|
|
16456
|
-
return await startPasskeyEnrollmentService({
|
|
16457
|
-
userId: Number(userId),
|
|
16458
|
-
keyId,
|
|
16459
|
-
currentPassword: body.currentPassword
|
|
16460
|
-
});
|
|
16461
|
-
});
|
|
16462
|
-
var passkeyRegisterVerify = route5.post("/_auth/passkeys/register/verify").input({
|
|
16463
|
-
body: Type.Object({
|
|
16464
|
-
response: CredentialResponseSchema,
|
|
16465
|
-
label: Type.Optional(PasskeyLabelSchema)
|
|
16466
|
-
})
|
|
16467
|
-
}).use([
|
|
16468
|
-
rateLimitPolicy5("auth-passkey-register-verify", {
|
|
16469
|
-
limit: 10,
|
|
16470
|
-
windowMs: 6e4,
|
|
16471
|
-
by: byIpAndCaller({ ipLimit: 100 })
|
|
16472
|
-
}),
|
|
16473
|
-
Transactional4()
|
|
16474
|
-
]).handler(async (c) => {
|
|
16475
|
-
const { body } = await c.data();
|
|
16476
|
-
const { userId } = getAuth(c);
|
|
16477
|
-
return await finishPasskeyEnrollmentService({
|
|
16478
|
-
userId: Number(userId),
|
|
16479
|
-
response: body.response,
|
|
16480
|
-
label: body.label
|
|
16481
|
-
});
|
|
16482
|
-
});
|
|
16483
|
-
var passkeyLoginOptions = route5.post("/_auth/passkeys/login/options").input({
|
|
16484
|
-
body: Type.Object({}, {
|
|
16485
|
-
additionalProperties: false,
|
|
16486
|
-
description: "No identifier is accepted \u2014 sign-in is discoverable"
|
|
16487
|
-
})
|
|
16488
|
-
}).use([rateLimitPolicy5("auth-passkey-login-options", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async () => {
|
|
16489
|
-
return await startPasskeyLoginService();
|
|
16490
|
-
});
|
|
16491
|
-
var passkeyLoginVerify = route5.post("/_auth/passkeys/login/verify").input({
|
|
16492
|
-
body: Type.Object({
|
|
16493
|
-
response: CredentialResponseSchema
|
|
16494
|
-
})
|
|
16495
|
-
}).interceptor({
|
|
16496
|
-
body: Type.Object({
|
|
16497
|
-
publicKey: Type.String({ description: "Client public key" }),
|
|
16498
|
-
keyId: Type.String({ description: "Key identifier" }),
|
|
16499
|
-
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
16500
|
-
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
16501
|
-
oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" })),
|
|
16502
|
-
deviceName: Type.Optional(DeviceNameSchema),
|
|
16503
|
-
platform: Type.Optional(PlatformSchema)
|
|
16504
|
-
})
|
|
16505
|
-
}).use([rateLimitPolicy5("auth-passkey-login-verify", { limit: 10, windowMs: 6e4 }), Transactional4()]).skip(["auth"]).handler(async (c) => {
|
|
16506
|
-
const { body } = await c.data();
|
|
16507
|
-
return await finishPasskeyLoginService({
|
|
16508
|
-
...body,
|
|
16509
|
-
...deviceProvenance(c.raw),
|
|
16510
|
-
response: body.response
|
|
16511
|
-
});
|
|
16512
|
-
});
|
|
16513
|
-
var listPasskeys = route5.post("/_auth/passkeys/list").input({
|
|
16514
|
-
body: Type.Object({})
|
|
16515
|
-
}).handler(async (c) => {
|
|
16516
|
-
const { userId } = getAuth(c);
|
|
16517
|
-
return { passkeys: await listPasskeysService(Number(userId)) };
|
|
16518
|
-
});
|
|
16519
|
-
var renamePasskey = route5.post("/_auth/passkeys/rename").input({
|
|
16520
|
-
body: Type.Object({
|
|
16521
|
-
passkeyId: PasskeyIdSchema2,
|
|
16522
|
-
label: PasskeyLabelSchema
|
|
16523
|
-
})
|
|
16524
|
-
}).handler(async (c) => {
|
|
16525
|
-
const { body } = await c.data();
|
|
16526
|
-
const { userId } = getAuth(c);
|
|
16527
|
-
return await renamePasskeyService({ userId: Number(userId), ...body });
|
|
16954
|
+
description: "Account password \u2014 one of the two ways to prove ownership when turning binding off"
|
|
16528
16955
|
});
|
|
16529
|
-
var
|
|
16956
|
+
var setSessionBinding = route5.post("/_auth/session/binding").input({
|
|
16530
16957
|
body: Type.Object({
|
|
16531
|
-
|
|
16958
|
+
mode: Type.Union([Type.Literal("passkey"), Type.Literal("none")], {
|
|
16959
|
+
description: "passkey binds this account's web sessions; none returns them to ordinary long-lived keys"
|
|
16960
|
+
}),
|
|
16961
|
+
response: Type.Optional(CredentialResponseSchema),
|
|
16532
16962
|
currentPassword: Type.Optional(CurrentPasswordSchema)
|
|
16533
16963
|
})
|
|
16534
16964
|
}).use([
|
|
16535
|
-
rateLimitPolicy5("auth-
|
|
16965
|
+
rateLimitPolicy5("auth-session-binding", {
|
|
16966
|
+
limit: 10,
|
|
16967
|
+
windowMs: 6e4,
|
|
16968
|
+
by: byIpAndCaller({ ipLimit: 100 })
|
|
16969
|
+
}),
|
|
16536
16970
|
Transactional4()
|
|
16537
16971
|
]).handler(async (c) => {
|
|
16538
16972
|
const { body } = await c.data();
|
|
16539
16973
|
const { userId, keyId } = getAuth(c);
|
|
16540
|
-
|
|
16974
|
+
const params = { userId: Number(userId), keyId, webProxy: deviceProvenance(c.raw).webProxy };
|
|
16975
|
+
if (body.mode === "none") {
|
|
16976
|
+
return await disableSessionBindingService({
|
|
16977
|
+
...params,
|
|
16978
|
+
response: body.response,
|
|
16979
|
+
currentPassword: body.currentPassword
|
|
16980
|
+
});
|
|
16981
|
+
}
|
|
16982
|
+
return await enableSessionBindingService(params);
|
|
16983
|
+
});
|
|
16984
|
+
var getSessionBinding = route5.get("/_auth/session/binding").handler(async (c) => {
|
|
16985
|
+
const { userId, keyId } = getAuth(c);
|
|
16986
|
+
return await getSessionBindingService({ userId: Number(userId), keyId });
|
|
16987
|
+
});
|
|
16988
|
+
var sessionBindingDisableOptions = route5.post("/_auth/session/binding/disable/options").input({
|
|
16989
|
+
body: Type.Object({}, {
|
|
16990
|
+
additionalProperties: false,
|
|
16991
|
+
description: "No input \u2014 the session names the account"
|
|
16992
|
+
})
|
|
16993
|
+
}).use([rateLimitPolicy5("auth-session-binding-disable-options", {
|
|
16994
|
+
limit: 10,
|
|
16995
|
+
windowMs: 6e4,
|
|
16996
|
+
by: byIpAndCaller({ ipLimit: 100 })
|
|
16997
|
+
})]).handler(async (c) => {
|
|
16998
|
+
const { userId } = getAuth(c);
|
|
16999
|
+
return await startSessionBindingDisableService(Number(userId));
|
|
16541
17000
|
});
|
|
16542
17001
|
|
|
16543
|
-
// src/server/routes/
|
|
16544
|
-
|
|
17002
|
+
// src/server/routes/auth/session-renew.ts
|
|
17003
|
+
init_esm();
|
|
17004
|
+
import { Transactional as Transactional5 } from "@spfn/core/db";
|
|
17005
|
+
import { rateLimitPolicy as rateLimitPolicy6 } from "@spfn/core/middleware";
|
|
17006
|
+
import { route as route6 } from "@spfn/core/route";
|
|
17007
|
+
|
|
17008
|
+
// src/server/middleware/authenticate-for-renewal.ts
|
|
17009
|
+
init_config();
|
|
17010
|
+
init_repositories();
|
|
17011
|
+
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
17012
|
+
import { SessionRenewalRefusedError as SessionRenewalRefusedError2 } from "@spfn/auth/errors";
|
|
16545
17013
|
|
|
16546
17014
|
// src/server/middleware/authenticate.ts
|
|
16547
17015
|
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
@@ -17014,12 +17482,13 @@ function contractViolation(message) {
|
|
|
17014
17482
|
}
|
|
17015
17483
|
|
|
17016
17484
|
// src/server/client-proof/contract-bundle.ts
|
|
17485
|
+
init_key_policy();
|
|
17486
|
+
init_types();
|
|
17017
17487
|
import { createHash as createHash9 } from "crypto";
|
|
17018
17488
|
import {
|
|
17019
17489
|
CORE_TIME_OPERATION_ID as CORE_TIME_OPERATION_ID2,
|
|
17020
17490
|
ServerTimeResponseSchema
|
|
17021
17491
|
} from "@spfn/core/server";
|
|
17022
|
-
init_types();
|
|
17023
17492
|
|
|
17024
17493
|
// src/server/client-proof/proof.ts
|
|
17025
17494
|
import { createHash as createHash8, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
|
|
@@ -17184,8 +17653,8 @@ var CORE_PREREQUISITE_OPERATIONS = [
|
|
|
17184
17653
|
|
|
17185
17654
|
// src/server/client-proof/contract-bundle.ts
|
|
17186
17655
|
init_wire_headers();
|
|
17187
|
-
var CONTRACT_VERSION = "0.
|
|
17188
|
-
var CONTRACT_SUPPORTED_RANGE = ">=0.
|
|
17656
|
+
var CONTRACT_VERSION = "0.12.0";
|
|
17657
|
+
var CONTRACT_SUPPORTED_RANGE = ">=0.12.0 <0.13.0";
|
|
17189
17658
|
function required(name, type) {
|
|
17190
17659
|
return { name, type, optional: false };
|
|
17191
17660
|
}
|
|
@@ -17306,7 +17775,9 @@ var CONTRACT_TYPES = [
|
|
|
17306
17775
|
required("publicId", "string"),
|
|
17307
17776
|
optional("email", "string"),
|
|
17308
17777
|
optional("phone", "string"),
|
|
17309
|
-
required("passwordChangeRequired", "boolean")
|
|
17778
|
+
required("passwordChangeRequired", "boolean"),
|
|
17779
|
+
optional("sessionBinding", "KeyBinding"),
|
|
17780
|
+
optional("keyExpiresAtMillis", "integer")
|
|
17310
17781
|
]
|
|
17311
17782
|
},
|
|
17312
17783
|
{
|
|
@@ -17366,7 +17837,9 @@ var CONTRACT_TYPES = [
|
|
|
17366
17837
|
required("isActive", "boolean"),
|
|
17367
17838
|
optional("revokedAtMillis", "integer"),
|
|
17368
17839
|
optional("registeredIp", "string"),
|
|
17369
|
-
optional("registeredUserAgent", "string")
|
|
17840
|
+
optional("registeredUserAgent", "string"),
|
|
17841
|
+
optional("binding", "KeyBinding"),
|
|
17842
|
+
optional("concurrentUseAtMillis", "integer")
|
|
17370
17843
|
]
|
|
17371
17844
|
},
|
|
17372
17845
|
{
|
|
@@ -17446,7 +17919,9 @@ var CONTRACT_TYPES = [
|
|
|
17446
17919
|
optional("publicId", "string"),
|
|
17447
17920
|
optional("email", "string"),
|
|
17448
17921
|
optional("phone", "string"),
|
|
17449
|
-
optional("passwordChangeRequired", "boolean")
|
|
17922
|
+
optional("passwordChangeRequired", "boolean"),
|
|
17923
|
+
optional("sessionBinding", "KeyBinding"),
|
|
17924
|
+
optional("keyExpiresAtMillis", "integer")
|
|
17450
17925
|
]
|
|
17451
17926
|
},
|
|
17452
17927
|
/**
|
|
@@ -17487,6 +17962,7 @@ var CONTRACT_TYPES = [
|
|
|
17487
17962
|
var CONTRACT_ENUMS = [
|
|
17488
17963
|
{ name: "KeyAlgorithm", values: [...KEY_ALGORITHM] },
|
|
17489
17964
|
{ name: "KeyPlatform", values: [...KEY_PLATFORM] },
|
|
17965
|
+
{ name: "KeyBinding", values: [...SESSION_BINDINGS] },
|
|
17490
17966
|
{ name: "DeviceAuthPollStatus", values: ["pending", "approved"] }
|
|
17491
17967
|
];
|
|
17492
17968
|
var BUNDLE_FILENAME = "spfn-mobile-contract.json";
|
|
@@ -17716,7 +18192,7 @@ async function verifyClientProofProfile(c) {
|
|
|
17716
18192
|
throw refusalError(ClientProofRefusal.proofReplayed());
|
|
17717
18193
|
}
|
|
17718
18194
|
const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
|
|
17719
|
-
keysRepository2.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
18195
|
+
keysRepository2.updateLastUsedById(keyRecord.id, readContextClientIdentity(c), attestedClientIp(c)).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
17720
18196
|
authLogger2.middleware.info("API access", {
|
|
17721
18197
|
userId: user.id,
|
|
17722
18198
|
email: user.email,
|
|
@@ -17789,6 +18265,7 @@ function registerAuthProfile(profileId, verifier) {
|
|
|
17789
18265
|
import { decodeProtectedHeader } from "jose";
|
|
17790
18266
|
import { defineMiddleware } from "@spfn/core/route";
|
|
17791
18267
|
import { ForbiddenError as ForbiddenError2, UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
|
|
18268
|
+
init_logger();
|
|
17792
18269
|
function getMachinePrincipal(c) {
|
|
17793
18270
|
return c.get("machinePrincipal") ?? null;
|
|
17794
18271
|
}
|
|
@@ -17950,69 +18427,100 @@ function extractBearer(header) {
|
|
|
17950
18427
|
|
|
17951
18428
|
// src/server/middleware/authenticate.ts
|
|
17952
18429
|
var INVALID_TOKEN_MESSAGE = "Invalid token: missing keyId";
|
|
17953
|
-
|
|
17954
|
-
const profile = await runAuthProfile(c);
|
|
17955
|
-
if (profile.kind === "refused") {
|
|
17956
|
-
return profile.response;
|
|
17957
|
-
}
|
|
17958
|
-
if (profile.kind === "authenticated") {
|
|
17959
|
-
c.set("auth", profile.auth);
|
|
17960
|
-
await next();
|
|
17961
|
-
return void 0;
|
|
17962
|
-
}
|
|
18430
|
+
async function admitBearerKey(c, admitsExpired) {
|
|
17963
18431
|
const authHeader = c.req.header("Authorization");
|
|
17964
18432
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
17965
|
-
|
|
17966
|
-
headers: c.req.header(),
|
|
17967
|
-
path: c.req.path
|
|
17968
|
-
});
|
|
17969
|
-
throw new UnauthorizedError3({ message: "Authentication header missing or invalid: Bearer {token}" });
|
|
18433
|
+
return { refused: "absent" };
|
|
17970
18434
|
}
|
|
17971
18435
|
const token = authHeader.substring(7);
|
|
17972
18436
|
if (matchesMachineDiscriminator(token)) {
|
|
17973
18437
|
authLogger3.middleware.warn("Machine credential presented to the user path \u2014 refused", { path: c.req.path });
|
|
17974
|
-
|
|
18438
|
+
return { refused: "machine" };
|
|
17975
18439
|
}
|
|
17976
18440
|
const decoded = decodeToken2(token);
|
|
17977
18441
|
if (!decoded || !decoded.keyId) {
|
|
17978
|
-
|
|
18442
|
+
return { refused: "undecodable" };
|
|
17979
18443
|
}
|
|
17980
|
-
const
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
throw new UnauthorizedError3({ message: "Invalid or revoked key" });
|
|
18444
|
+
const key = await keysRepository3.findActiveByKeyId(decoded.keyId);
|
|
18445
|
+
if (!key) {
|
|
18446
|
+
return { refused: "unknown" };
|
|
17984
18447
|
}
|
|
17985
|
-
if (
|
|
17986
|
-
|
|
18448
|
+
if (key.expiresAt && /* @__PURE__ */ new Date() > key.expiresAt && !admitsExpired(key)) {
|
|
18449
|
+
return { refused: "expired" };
|
|
17987
18450
|
}
|
|
18451
|
+
return signatureOutcome(token, key);
|
|
18452
|
+
}
|
|
18453
|
+
function signatureOutcome(token, key) {
|
|
17988
18454
|
try {
|
|
17989
18455
|
verifyClientToken2(
|
|
17990
18456
|
token,
|
|
17991
|
-
|
|
17992
|
-
|
|
18457
|
+
key.publicKey,
|
|
18458
|
+
key.algorithm
|
|
17993
18459
|
// entity.algorithm is always defined
|
|
17994
18460
|
);
|
|
17995
18461
|
} catch (err) {
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
}
|
|
18000
|
-
if (err.name === "JsonWebTokenError") {
|
|
18001
|
-
throw new InvalidTokenError({ message: "Invalid token signature" });
|
|
18002
|
-
}
|
|
18462
|
+
const name = err instanceof Error ? err.name : "";
|
|
18463
|
+
if (name === "TokenExpiredError") {
|
|
18464
|
+
return { refused: "token-expired" };
|
|
18003
18465
|
}
|
|
18004
|
-
|
|
18466
|
+
return { refused: name === "JsonWebTokenError" ? "bad-signature" : "unverifiable" };
|
|
18005
18467
|
}
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18468
|
+
return { key };
|
|
18469
|
+
}
|
|
18470
|
+
function bearerRefusal(c, refused) {
|
|
18471
|
+
if (refused === "absent") {
|
|
18472
|
+
authLogger3.middleware.error("Missing or invalid authorization header. If using Next.js API routes, ensure you have imported '@spfn/auth/nextjs/api' in your API route handler (e.g., src/app/api/actions/[[...path]]/route.ts) to enable automatic authentication header forwarding from client to backend.", {
|
|
18473
|
+
headers: c.req.header(),
|
|
18474
|
+
path: c.req.path
|
|
18475
|
+
});
|
|
18476
|
+
return new UnauthorizedError3({ message: "Authentication header missing or invalid: Bearer {token}" });
|
|
18477
|
+
}
|
|
18478
|
+
switch (refused) {
|
|
18479
|
+
case "machine":
|
|
18480
|
+
case "undecodable":
|
|
18481
|
+
return new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
|
|
18482
|
+
case "unknown":
|
|
18483
|
+
return new UnauthorizedError3({ message: "Invalid or revoked key" });
|
|
18484
|
+
case "expired":
|
|
18485
|
+
return new KeyExpiredError();
|
|
18486
|
+
case "token-expired":
|
|
18487
|
+
return new TokenExpiredError();
|
|
18488
|
+
case "bad-signature":
|
|
18489
|
+
return new InvalidTokenError({ message: "Invalid token signature" });
|
|
18490
|
+
default:
|
|
18491
|
+
return new UnauthorizedError3({ message: "Authentication failed" });
|
|
18492
|
+
}
|
|
18493
|
+
}
|
|
18494
|
+
function bearerAuthContext(keyId, resolved) {
|
|
18495
|
+
return {
|
|
18496
|
+
user: resolved.user,
|
|
18497
|
+
userId: String(resolved.user.id),
|
|
18011
18498
|
keyId,
|
|
18012
|
-
role,
|
|
18013
|
-
locale,
|
|
18499
|
+
role: resolved.role,
|
|
18500
|
+
locale: resolved.locale,
|
|
18014
18501
|
scheme: "bearer"
|
|
18015
|
-
}
|
|
18502
|
+
};
|
|
18503
|
+
}
|
|
18504
|
+
var authenticate = defineMiddleware2("auth", async (c, next) => {
|
|
18505
|
+
const profile = await runAuthProfile(c);
|
|
18506
|
+
if (profile.kind === "refused") {
|
|
18507
|
+
return profile.response;
|
|
18508
|
+
}
|
|
18509
|
+
if (profile.kind === "authenticated") {
|
|
18510
|
+
c.set("auth", profile.auth);
|
|
18511
|
+
await next();
|
|
18512
|
+
return void 0;
|
|
18513
|
+
}
|
|
18514
|
+
const outcome = await admitBearerKey(c, () => false);
|
|
18515
|
+
if ("refused" in outcome) {
|
|
18516
|
+
throw bearerRefusal(c, outcome.refused);
|
|
18517
|
+
}
|
|
18518
|
+
const keyRecord = outcome.key;
|
|
18519
|
+
const keyId = keyRecord.keyId;
|
|
18520
|
+
const resolved = await resolveAuthenticatedUser(keyRecord.userId);
|
|
18521
|
+
const { user } = resolved;
|
|
18522
|
+
keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c), attestedClientIp(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
|
|
18523
|
+
c.set("auth", bearerAuthContext(keyId, resolved));
|
|
18016
18524
|
const method = c.req.method;
|
|
18017
18525
|
const path = c.req.path;
|
|
18018
18526
|
authLogger3.middleware.info("API access", {
|
|
@@ -18077,7 +18585,7 @@ var optionalAuth = defineMiddleware2("optionalAuth", async (c, next) => {
|
|
|
18077
18585
|
return void 0;
|
|
18078
18586
|
}
|
|
18079
18587
|
const { user, role } = result;
|
|
18080
|
-
keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
|
|
18588
|
+
keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c), attestedClientIp(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
|
|
18081
18589
|
c.set("auth", {
|
|
18082
18590
|
user,
|
|
18083
18591
|
userId: String(user.id),
|
|
@@ -18092,12 +18600,199 @@ var optionalAuth = defineMiddleware2("optionalAuth", async (c, next) => {
|
|
|
18092
18600
|
return void 0;
|
|
18093
18601
|
}, { skips: ["auth"] });
|
|
18094
18602
|
|
|
18603
|
+
// src/server/middleware/authenticate-for-renewal.ts
|
|
18604
|
+
function admitsForRenewal(key) {
|
|
18605
|
+
return key.binding === "passkey" && key.expiresAt !== null && Date.now() < key.expiresAt.getTime() + getBoundKeyRenewGraceMs();
|
|
18606
|
+
}
|
|
18607
|
+
var authenticateForRenewal = defineMiddleware3("authForRenewal", async (c, next) => {
|
|
18608
|
+
const outcome = await admitBearerKey(c, admitsForRenewal);
|
|
18609
|
+
if ("refused" in outcome || !admitsForRenewal(outcome.key)) {
|
|
18610
|
+
throw new SessionRenewalRefusedError2();
|
|
18611
|
+
}
|
|
18612
|
+
const resolved = await activeAccount(outcome.key.userId);
|
|
18613
|
+
if (!resolved) {
|
|
18614
|
+
throw new SessionRenewalRefusedError2();
|
|
18615
|
+
}
|
|
18616
|
+
c.set("auth", bearerAuthContext(outcome.key.keyId, resolved));
|
|
18617
|
+
await next();
|
|
18618
|
+
return void 0;
|
|
18619
|
+
}, { skips: ["auth"] });
|
|
18620
|
+
async function activeAccount(userId) {
|
|
18621
|
+
const [result, locale] = await Promise.all([
|
|
18622
|
+
usersRepository.findByIdWithRole(userId),
|
|
18623
|
+
userProfilesRepository.findLocaleByUserId(userId)
|
|
18624
|
+
]);
|
|
18625
|
+
if (!result || result.user.status !== "active") {
|
|
18626
|
+
return null;
|
|
18627
|
+
}
|
|
18628
|
+
return { user: result.user, role: result.role?.name ?? null, locale };
|
|
18629
|
+
}
|
|
18630
|
+
|
|
18631
|
+
// src/server/routes/auth/session-renew.ts
|
|
18632
|
+
init_types();
|
|
18633
|
+
var CredentialResponseSchema2 = Type.Unknown({
|
|
18634
|
+
description: "The credential from @simplewebauthn/browser, passed through unchanged"
|
|
18635
|
+
});
|
|
18636
|
+
var RENEW_RATE_LIMIT = { limit: 10, windowMs: 6e4 };
|
|
18637
|
+
var sessionRenewOptions = route6.post("/_auth/session/renew/options").input({
|
|
18638
|
+
body: Type.Object({})
|
|
18639
|
+
}).use([rateLimitPolicy6("auth-session-renew", RENEW_RATE_LIMIT), authenticateForRenewal]).handler(async (c) => {
|
|
18640
|
+
return await startSessionRenewService({ expiredKeyId: getAuth(c).keyId });
|
|
18641
|
+
});
|
|
18642
|
+
var sessionRenewVerify = route6.post("/_auth/session/renew/verify").input({
|
|
18643
|
+
body: Type.Object({
|
|
18644
|
+
response: CredentialResponseSchema2
|
|
18645
|
+
})
|
|
18646
|
+
}).interceptor({
|
|
18647
|
+
body: Type.Object({
|
|
18648
|
+
publicKey: Type.String({ description: "Client public key" }),
|
|
18649
|
+
keyId: Type.String({ description: "Key identifier" }),
|
|
18650
|
+
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
18651
|
+
algorithm: Type.Optional(Type.Union(
|
|
18652
|
+
KEY_ALGORITHM.map((algo) => Type.Literal(algo)),
|
|
18653
|
+
{ description: "Signature algorithm \u2014 the service default when absent" }
|
|
18654
|
+
))
|
|
18655
|
+
})
|
|
18656
|
+
}).use([rateLimitPolicy6("auth-session-renew", RENEW_RATE_LIMIT), authenticateForRenewal, Transactional5()]).handler(async (c) => {
|
|
18657
|
+
const { body } = await c.data();
|
|
18658
|
+
return await finishSessionRenewService({
|
|
18659
|
+
...body,
|
|
18660
|
+
expiredKeyId: getAuth(c).keyId,
|
|
18661
|
+
response: body.response
|
|
18662
|
+
});
|
|
18663
|
+
});
|
|
18664
|
+
|
|
18665
|
+
// src/server/routes/auth/passkeys.ts
|
|
18666
|
+
init_esm();
|
|
18667
|
+
import { Transactional as Transactional6 } from "@spfn/core/db";
|
|
18668
|
+
import { rateLimitPolicy as rateLimitPolicy7 } from "@spfn/core/middleware";
|
|
18669
|
+
import { route as route7 } from "@spfn/core/route";
|
|
18670
|
+
init_types();
|
|
18671
|
+
init_passkeys();
|
|
18672
|
+
init_schema3();
|
|
18673
|
+
var CredentialResponseSchema3 = Type.Unknown({
|
|
18674
|
+
description: "The credential from @simplewebauthn/browser, passed through unchanged"
|
|
18675
|
+
});
|
|
18676
|
+
var PasskeyIdSchema2 = Type.String({
|
|
18677
|
+
pattern: "^[0-9]{1,19}$",
|
|
18678
|
+
description: "Passkey identifier, as returned by list"
|
|
18679
|
+
});
|
|
18680
|
+
var PasskeyLabelSchema = Type.String({
|
|
18681
|
+
minLength: 1,
|
|
18682
|
+
maxLength: PASSKEY_LABEL_MAX_LENGTH,
|
|
18683
|
+
description: `Owner-facing name in the passkey list (1-${PASSKEY_LABEL_MAX_LENGTH} chars)`
|
|
18684
|
+
});
|
|
18685
|
+
var CurrentPasswordSchema2 = Type.String({
|
|
18686
|
+
minLength: 1,
|
|
18687
|
+
description: "Account password, needed when the session proved itself longer ago than the recent-authentication window"
|
|
18688
|
+
});
|
|
18689
|
+
var passkeyRegisterOptions = route7.post("/_auth/passkeys/register/options").input({
|
|
18690
|
+
body: Type.Object({
|
|
18691
|
+
currentPassword: Type.Optional(CurrentPasswordSchema2)
|
|
18692
|
+
})
|
|
18693
|
+
}).use([rateLimitPolicy7("auth-passkey-register-options", {
|
|
18694
|
+
limit: 10,
|
|
18695
|
+
windowMs: 6e4,
|
|
18696
|
+
by: byIpAndCaller({ ipLimit: 100 })
|
|
18697
|
+
})]).handler(async (c) => {
|
|
18698
|
+
const { body } = await c.data();
|
|
18699
|
+
const { userId, keyId } = getAuth(c);
|
|
18700
|
+
return await startPasskeyEnrollmentService({
|
|
18701
|
+
userId: Number(userId),
|
|
18702
|
+
keyId,
|
|
18703
|
+
currentPassword: body.currentPassword
|
|
18704
|
+
});
|
|
18705
|
+
});
|
|
18706
|
+
var passkeyRegisterVerify = route7.post("/_auth/passkeys/register/verify").input({
|
|
18707
|
+
body: Type.Object({
|
|
18708
|
+
response: CredentialResponseSchema3,
|
|
18709
|
+
label: Type.Optional(PasskeyLabelSchema)
|
|
18710
|
+
})
|
|
18711
|
+
}).use([
|
|
18712
|
+
rateLimitPolicy7("auth-passkey-register-verify", {
|
|
18713
|
+
limit: 10,
|
|
18714
|
+
windowMs: 6e4,
|
|
18715
|
+
by: byIpAndCaller({ ipLimit: 100 })
|
|
18716
|
+
}),
|
|
18717
|
+
Transactional6()
|
|
18718
|
+
]).handler(async (c) => {
|
|
18719
|
+
const { body } = await c.data();
|
|
18720
|
+
const { userId } = getAuth(c);
|
|
18721
|
+
return await finishPasskeyEnrollmentService({
|
|
18722
|
+
userId: Number(userId),
|
|
18723
|
+
response: body.response,
|
|
18724
|
+
label: body.label
|
|
18725
|
+
});
|
|
18726
|
+
});
|
|
18727
|
+
var passkeyLoginOptions = route7.post("/_auth/passkeys/login/options").input({
|
|
18728
|
+
body: Type.Object({}, {
|
|
18729
|
+
additionalProperties: false,
|
|
18730
|
+
description: "No identifier is accepted \u2014 sign-in is discoverable"
|
|
18731
|
+
})
|
|
18732
|
+
}).use([rateLimitPolicy7("auth-passkey-login-options", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async () => {
|
|
18733
|
+
return await startPasskeyLoginService();
|
|
18734
|
+
});
|
|
18735
|
+
var passkeyLoginVerify = route7.post("/_auth/passkeys/login/verify").input({
|
|
18736
|
+
body: Type.Object({
|
|
18737
|
+
response: CredentialResponseSchema3
|
|
18738
|
+
})
|
|
18739
|
+
}).interceptor({
|
|
18740
|
+
body: Type.Object({
|
|
18741
|
+
publicKey: Type.String({ description: "Client public key" }),
|
|
18742
|
+
keyId: Type.String({ description: "Key identifier" }),
|
|
18743
|
+
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
18744
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
18745
|
+
oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" })),
|
|
18746
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
18747
|
+
platform: Type.Optional(PlatformSchema)
|
|
18748
|
+
})
|
|
18749
|
+
}).use([rateLimitPolicy7("auth-passkey-login-verify", { limit: 10, windowMs: 6e4 }), Transactional6()]).skip(["auth"]).handler(async (c) => {
|
|
18750
|
+
const { body } = await c.data();
|
|
18751
|
+
return await finishPasskeyLoginService({
|
|
18752
|
+
...body,
|
|
18753
|
+
...deviceProvenance(c.raw),
|
|
18754
|
+
response: body.response
|
|
18755
|
+
});
|
|
18756
|
+
});
|
|
18757
|
+
var listPasskeys = route7.post("/_auth/passkeys/list").input({
|
|
18758
|
+
body: Type.Object({})
|
|
18759
|
+
}).handler(async (c) => {
|
|
18760
|
+
const { userId } = getAuth(c);
|
|
18761
|
+
return { passkeys: await listPasskeysService(Number(userId)) };
|
|
18762
|
+
});
|
|
18763
|
+
var renamePasskey = route7.post("/_auth/passkeys/rename").input({
|
|
18764
|
+
body: Type.Object({
|
|
18765
|
+
passkeyId: PasskeyIdSchema2,
|
|
18766
|
+
label: PasskeyLabelSchema
|
|
18767
|
+
})
|
|
18768
|
+
}).handler(async (c) => {
|
|
18769
|
+
const { body } = await c.data();
|
|
18770
|
+
const { userId } = getAuth(c);
|
|
18771
|
+
return await renamePasskeyService({ userId: Number(userId), ...body });
|
|
18772
|
+
});
|
|
18773
|
+
var revokePasskey = route7.post("/_auth/passkeys/revoke").input({
|
|
18774
|
+
body: Type.Object({
|
|
18775
|
+
passkeyId: PasskeyIdSchema2,
|
|
18776
|
+
currentPassword: Type.Optional(CurrentPasswordSchema2)
|
|
18777
|
+
})
|
|
18778
|
+
}).use([
|
|
18779
|
+
rateLimitPolicy7("auth-passkey-revoke", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 100 }) }),
|
|
18780
|
+
Transactional6()
|
|
18781
|
+
]).handler(async (c) => {
|
|
18782
|
+
const { body } = await c.data();
|
|
18783
|
+
const { userId, keyId } = getAuth(c);
|
|
18784
|
+
return await revokePasskeyService({ userId: Number(userId), keyId, ...body });
|
|
18785
|
+
});
|
|
18786
|
+
|
|
18787
|
+
// src/server/routes/invitations/index.ts
|
|
18788
|
+
import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
|
|
18789
|
+
|
|
18095
18790
|
// src/server/middleware/require-permission.ts
|
|
18096
|
-
import { defineMiddleware as
|
|
18791
|
+
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
18097
18792
|
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
18098
18793
|
import { InsufficientPermissionsError } from "@spfn/auth/errors";
|
|
18099
18794
|
import { getAuth as getAuth2, hasAllPermissions as hasAllPermissions2, hasAnyPermission as hasAnyPermission2, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
18100
|
-
var requirePermissions =
|
|
18795
|
+
var requirePermissions = defineMiddleware4(
|
|
18101
18796
|
"permission",
|
|
18102
18797
|
(...permissionNames) => async (c, next) => {
|
|
18103
18798
|
const auth = getAuth2(c);
|
|
@@ -18125,7 +18820,7 @@ var requirePermissions = defineMiddleware3(
|
|
|
18125
18820
|
await next();
|
|
18126
18821
|
}
|
|
18127
18822
|
);
|
|
18128
|
-
var requireAnyPermission =
|
|
18823
|
+
var requireAnyPermission = defineMiddleware4(
|
|
18129
18824
|
"anyPermission",
|
|
18130
18825
|
(...permissionNames) => async (c, next) => {
|
|
18131
18826
|
const auth = getAuth2(c);
|
|
@@ -18155,11 +18850,11 @@ var requireAnyPermission = defineMiddleware3(
|
|
|
18155
18850
|
);
|
|
18156
18851
|
|
|
18157
18852
|
// src/server/middleware/require-role.ts
|
|
18158
|
-
import { defineMiddleware as
|
|
18853
|
+
import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
|
|
18159
18854
|
import { getAuth as getAuth3, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
18160
18855
|
import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
|
|
18161
18856
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
18162
|
-
var requireRole =
|
|
18857
|
+
var requireRole = defineMiddleware5(
|
|
18163
18858
|
"role",
|
|
18164
18859
|
(...roleNames) => async (c, next) => {
|
|
18165
18860
|
const auth = getAuth3(c);
|
|
@@ -18190,11 +18885,11 @@ var requireRole = defineMiddleware4(
|
|
|
18190
18885
|
);
|
|
18191
18886
|
|
|
18192
18887
|
// src/server/middleware/role-guard.ts
|
|
18193
|
-
import { defineMiddleware as
|
|
18888
|
+
import { defineMiddleware as defineMiddleware6 } from "@spfn/core/route";
|
|
18194
18889
|
import { getAuth as getAuth4, authLogger as authLogger6 } from "@spfn/auth/server";
|
|
18195
18890
|
import { ForbiddenError as ForbiddenError5 } from "@spfn/core/errors";
|
|
18196
18891
|
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
18197
|
-
var roleGuard =
|
|
18892
|
+
var roleGuard = defineMiddleware6(
|
|
18198
18893
|
"roleGuard",
|
|
18199
18894
|
(options) => async (c, next) => {
|
|
18200
18895
|
const { allow, deny } = options;
|
|
@@ -18242,10 +18937,10 @@ var roleGuard = defineMiddleware5(
|
|
|
18242
18937
|
);
|
|
18243
18938
|
|
|
18244
18939
|
// src/server/middleware/one-time-token-auth.ts
|
|
18245
|
-
import { defineMiddleware as
|
|
18940
|
+
import { defineMiddleware as defineMiddleware7 } from "@spfn/core/route";
|
|
18246
18941
|
import { UnauthorizedError as UnauthorizedError4 } from "@spfn/core/errors";
|
|
18247
18942
|
import { usersRepository as usersRepository4, userProfilesRepository as userProfilesRepository4 } from "@spfn/auth/server";
|
|
18248
|
-
var oneTimeTokenAuth =
|
|
18943
|
+
var oneTimeTokenAuth = defineMiddleware7("oneTimeTokenAuth", async (c, next) => {
|
|
18249
18944
|
const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
|
|
18250
18945
|
if (!token) {
|
|
18251
18946
|
throw new UnauthorizedError4({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
|
|
@@ -18284,12 +18979,12 @@ function extractOTTHeader(header) {
|
|
|
18284
18979
|
}
|
|
18285
18980
|
|
|
18286
18981
|
// src/server/middleware/ops-token-auth.ts
|
|
18287
|
-
import { defineMiddleware as
|
|
18982
|
+
import { defineMiddleware as defineMiddleware8 } from "@spfn/core/route";
|
|
18288
18983
|
import { ForbiddenError as ForbiddenError6, UnauthorizedError as UnauthorizedError5 } from "@spfn/core/errors";
|
|
18289
18984
|
function getOpsToken(c) {
|
|
18290
18985
|
return c.get("opsToken") ?? null;
|
|
18291
18986
|
}
|
|
18292
|
-
var opsTokenAuth =
|
|
18987
|
+
var opsTokenAuth = defineMiddleware8("opsTokenAuth", async (c, next) => {
|
|
18293
18988
|
const token = extractBearer2(c.req.header("Authorization"));
|
|
18294
18989
|
if (!token) {
|
|
18295
18990
|
throw new UnauthorizedError5({ message: "Ops token required: Authorization: Bearer <token>" });
|
|
@@ -18301,7 +18996,7 @@ var opsTokenAuth = defineMiddleware7("opsTokenAuth", async (c, next) => {
|
|
|
18301
18996
|
c.set("opsToken", verified);
|
|
18302
18997
|
await next();
|
|
18303
18998
|
}, { skips: ["auth"] });
|
|
18304
|
-
var requireOpsScope =
|
|
18999
|
+
var requireOpsScope = defineMiddleware8(
|
|
18305
19000
|
"opsScope",
|
|
18306
19001
|
(...scopes) => async (c, next) => {
|
|
18307
19002
|
const token = getOpsToken(c);
|
|
@@ -18362,18 +19057,18 @@ function chain(handlers) {
|
|
|
18362
19057
|
// src/server/routes/invitations/index.ts
|
|
18363
19058
|
init_types();
|
|
18364
19059
|
init_esm();
|
|
18365
|
-
import { Transactional as
|
|
18366
|
-
import { rateLimitPolicy as
|
|
18367
|
-
import { defineRouter as defineRouter2, route as
|
|
19060
|
+
import { Transactional as Transactional7 } from "@spfn/core/db";
|
|
19061
|
+
import { rateLimitPolicy as rateLimitPolicy8 } from "@spfn/core/middleware";
|
|
19062
|
+
import { defineRouter as defineRouter2, route as route8 } from "@spfn/core/route";
|
|
18368
19063
|
var INVITATION_STATUSES2 = ["pending", "accepted", "expired", "cancelled"];
|
|
18369
|
-
var getInvitation =
|
|
19064
|
+
var getInvitation = route8.get("/_auth/invitations/:token").input({
|
|
18370
19065
|
params: Type.Object({
|
|
18371
19066
|
token: Type.String({
|
|
18372
19067
|
pattern: UUID_PATTERN,
|
|
18373
19068
|
description: "Invitation token (UUID v4)"
|
|
18374
19069
|
})
|
|
18375
19070
|
})
|
|
18376
|
-
}).use([
|
|
19071
|
+
}).use([rateLimitPolicy8("auth-invitation-lookup", { limit: 30, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18377
19072
|
const { params } = await c.data();
|
|
18378
19073
|
const token = params.token;
|
|
18379
19074
|
const validation = await validateInvitation(token);
|
|
@@ -18393,7 +19088,7 @@ var getInvitation = route6.get("/_auth/invitations/:token").input({
|
|
|
18393
19088
|
metadata: invitation.metadata || void 0
|
|
18394
19089
|
};
|
|
18395
19090
|
});
|
|
18396
|
-
var acceptInvitation2 =
|
|
19091
|
+
var acceptInvitation2 = route8.post("/_auth/invitations/accept").input({
|
|
18397
19092
|
body: Type.Object({
|
|
18398
19093
|
token: Type.String({
|
|
18399
19094
|
pattern: UUID_PATTERN,
|
|
@@ -18412,7 +19107,7 @@ var acceptInvitation2 = route6.post("/_auth/invitations/accept").input({
|
|
|
18412
19107
|
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
18413
19108
|
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
|
|
18414
19109
|
})
|
|
18415
|
-
}).use([
|
|
19110
|
+
}).use([rateLimitPolicy8("auth-invitation-accept", { limit: 10, windowMs: 6e4 }), Transactional7()]).skip(["auth"]).handler(async (c) => {
|
|
18416
19111
|
const { body } = await c.data();
|
|
18417
19112
|
return await acceptInvitation({
|
|
18418
19113
|
token: body.token,
|
|
@@ -18424,7 +19119,7 @@ var acceptInvitation2 = route6.post("/_auth/invitations/accept").input({
|
|
|
18424
19119
|
...deviceProvenance(c.raw)
|
|
18425
19120
|
});
|
|
18426
19121
|
});
|
|
18427
|
-
var createInvitation2 =
|
|
19122
|
+
var createInvitation2 = route8.post("/_auth/invitations").input({
|
|
18428
19123
|
body: Type.Object({
|
|
18429
19124
|
email: Type.String({
|
|
18430
19125
|
pattern: EMAIL_PATTERN2,
|
|
@@ -18469,7 +19164,7 @@ var createInvitation2 = route6.post("/_auth/invitations").input({
|
|
|
18469
19164
|
invitationUrl
|
|
18470
19165
|
};
|
|
18471
19166
|
});
|
|
18472
|
-
var listInvitations2 =
|
|
19167
|
+
var listInvitations2 = route8.get("/_auth/invitations").input({
|
|
18473
19168
|
query: Type.Object({
|
|
18474
19169
|
status: Type.Optional(Type.Union(
|
|
18475
19170
|
INVITATION_STATUSES2.map((s) => Type.Literal(s)),
|
|
@@ -18498,7 +19193,7 @@ var listInvitations2 = route6.get("/_auth/invitations").input({
|
|
|
18498
19193
|
invitations: formattedInvitations
|
|
18499
19194
|
};
|
|
18500
19195
|
});
|
|
18501
|
-
var cancelInvitation2 =
|
|
19196
|
+
var cancelInvitation2 = route8.post("/_auth/invitations/cancel").input({
|
|
18502
19197
|
body: Type.Object({
|
|
18503
19198
|
id: Type.Number({
|
|
18504
19199
|
description: "Invitation ID"
|
|
@@ -18519,7 +19214,7 @@ var cancelInvitation2 = route6.post("/_auth/invitations/cancel").input({
|
|
|
18519
19214
|
cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18520
19215
|
};
|
|
18521
19216
|
});
|
|
18522
|
-
var resendInvitation2 =
|
|
19217
|
+
var resendInvitation2 = route8.post("/_auth/invitations/resend").input({
|
|
18523
19218
|
body: Type.Object({
|
|
18524
19219
|
id: Type.Number({
|
|
18525
19220
|
description: "Invitation ID"
|
|
@@ -18540,7 +19235,7 @@ var resendInvitation2 = route6.post("/_auth/invitations/resend").input({
|
|
|
18540
19235
|
expiresAt: updated.expiresAt.toISOString()
|
|
18541
19236
|
};
|
|
18542
19237
|
});
|
|
18543
|
-
var deleteInvitation2 =
|
|
19238
|
+
var deleteInvitation2 = route8.post("/_auth/invitations/delete").input({
|
|
18544
19239
|
body: Type.Object({
|
|
18545
19240
|
id: Type.Number({
|
|
18546
19241
|
description: "Invitation ID"
|
|
@@ -18563,13 +19258,13 @@ var invitationRouter = defineRouter2({
|
|
|
18563
19258
|
|
|
18564
19259
|
// src/server/routes/users/index.ts
|
|
18565
19260
|
init_esm();
|
|
18566
|
-
import { rateLimitPolicy as
|
|
18567
|
-
import { defineRouter as defineRouter3, route as
|
|
18568
|
-
var getUserProfile =
|
|
19261
|
+
import { rateLimitPolicy as rateLimitPolicy9 } from "@spfn/core/middleware";
|
|
19262
|
+
import { defineRouter as defineRouter3, route as route9 } from "@spfn/core/route";
|
|
19263
|
+
var getUserProfile = route9.get("/_auth/users/profile").handler(async (c) => {
|
|
18569
19264
|
const { userId } = getAuth(c);
|
|
18570
19265
|
return await getUserProfileService(userId);
|
|
18571
19266
|
});
|
|
18572
|
-
var updateUserProfile =
|
|
19267
|
+
var updateUserProfile = route9.patch("/_auth/users/profile").input({
|
|
18573
19268
|
body: Type.Object({
|
|
18574
19269
|
displayName: Type.Optional(Type.String({ description: "Display name shown in UI" })),
|
|
18575
19270
|
firstName: Type.Optional(Type.String({ description: "First name" })),
|
|
@@ -18591,15 +19286,15 @@ var updateUserProfile = route7.patch("/_auth/users/profile").input({
|
|
|
18591
19286
|
const { body } = await c.data();
|
|
18592
19287
|
return await updateUserProfileService(userId, body);
|
|
18593
19288
|
});
|
|
18594
|
-
var checkUsername =
|
|
19289
|
+
var checkUsername = route9.get("/_auth/users/username/check").input({
|
|
18595
19290
|
query: Type.Object({
|
|
18596
19291
|
username: Type.String({ minLength: 1 })
|
|
18597
19292
|
})
|
|
18598
|
-
}).use([
|
|
19293
|
+
}).use([rateLimitPolicy9("auth-username-check", { limit: 30, windowMs: 6e4 })]).handler(async (c) => {
|
|
18599
19294
|
const { query } = await c.data();
|
|
18600
19295
|
return { available: await checkUsernameAvailableService(query.username) };
|
|
18601
19296
|
});
|
|
18602
|
-
var updateUsername =
|
|
19297
|
+
var updateUsername = route9.patch("/_auth/users/username").input({
|
|
18603
19298
|
body: Type.Object({
|
|
18604
19299
|
username: Type.Union([
|
|
18605
19300
|
Type.String({ minLength: 1 }),
|
|
@@ -18611,7 +19306,7 @@ var updateUsername = route7.patch("/_auth/users/username").input({
|
|
|
18611
19306
|
const { body } = await c.data();
|
|
18612
19307
|
return await updateUsernameService(userId, body.username);
|
|
18613
19308
|
});
|
|
18614
|
-
var updateLocale =
|
|
19309
|
+
var updateLocale = route9.patch("/_auth/users/locale").input({
|
|
18615
19310
|
body: Type.Object({
|
|
18616
19311
|
locale: Type.String({ minLength: 1, description: "Locale code (e.g., en, ko, ja)" })
|
|
18617
19312
|
})
|
|
@@ -18821,22 +19516,23 @@ var deleteCookie = (c, name, opt) => {
|
|
|
18821
19516
|
// src/server/routes/oauth/index.ts
|
|
18822
19517
|
init_types();
|
|
18823
19518
|
init_schema3();
|
|
18824
|
-
|
|
18825
|
-
import {
|
|
18826
|
-
import {
|
|
18827
|
-
import {
|
|
19519
|
+
init_config();
|
|
19520
|
+
import { Transactional as Transactional8 } from "@spfn/core/db";
|
|
19521
|
+
import { ValidationError as ValidationError17 } from "@spfn/core/errors";
|
|
19522
|
+
import { rateLimitPolicy as rateLimitPolicy10 } from "@spfn/core/middleware";
|
|
19523
|
+
import { defineRouter as defineRouter4, route as route10 } from "@spfn/core/route";
|
|
18828
19524
|
var providerParams = Type.Object({
|
|
18829
19525
|
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
18830
19526
|
description: "OAuth provider id (google, github, kakao, naver, superself)"
|
|
18831
19527
|
})
|
|
18832
19528
|
});
|
|
18833
|
-
var oauthGoogleStart =
|
|
19529
|
+
var oauthGoogleStart = route10.get("/_auth/oauth/google").input({
|
|
18834
19530
|
query: Type.Object({
|
|
18835
19531
|
state: Type.String({
|
|
18836
19532
|
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
18837
19533
|
})
|
|
18838
19534
|
})
|
|
18839
|
-
}).use([
|
|
19535
|
+
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18840
19536
|
const { query } = await c.data();
|
|
18841
19537
|
if (!isGoogleOAuthEnabled()) {
|
|
18842
19538
|
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
@@ -18844,7 +19540,7 @@ var oauthGoogleStart = route8.get("/_auth/oauth/google").input({
|
|
|
18844
19540
|
const authUrl = getGoogleAuthUrl(query.state);
|
|
18845
19541
|
return c.redirect(authUrl);
|
|
18846
19542
|
});
|
|
18847
|
-
var oauthGoogleCallback =
|
|
19543
|
+
var oauthGoogleCallback = route10.get("/_auth/oauth/google/callback").input({
|
|
18848
19544
|
query: Type.Object({
|
|
18849
19545
|
code: Type.Optional(Type.String({
|
|
18850
19546
|
description: "Authorization code from Google"
|
|
@@ -18859,7 +19555,7 @@ var oauthGoogleCallback = route8.get("/_auth/oauth/google/callback").input({
|
|
|
18859
19555
|
description: "Error description from Google"
|
|
18860
19556
|
}))
|
|
18861
19557
|
})
|
|
18862
|
-
}).use([
|
|
19558
|
+
}).use([rateLimitPolicy10("oauth-callback", { limit: 30, windowMs: 6e4 }), Transactional8()]).skip(["auth"]).handler(async (c) => {
|
|
18863
19559
|
const { query } = await c.data();
|
|
18864
19560
|
if (query.error) {
|
|
18865
19561
|
const errorMessage = query.error_description || query.error;
|
|
@@ -18886,7 +19582,7 @@ var oauthGoogleCallback = route8.get("/_auth/oauth/google/callback").input({
|
|
|
18886
19582
|
return c.redirect(buildOAuthErrorUrl(message));
|
|
18887
19583
|
}
|
|
18888
19584
|
});
|
|
18889
|
-
var oauthStart =
|
|
19585
|
+
var oauthStart = route10.post("/_auth/oauth/start").input({
|
|
18890
19586
|
body: Type.Object({
|
|
18891
19587
|
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
18892
19588
|
description: "OAuth provider (google, github, kakao, naver)"
|
|
@@ -18910,10 +19606,10 @@ var oauthStart = route8.post("/_auth/oauth/start").input({
|
|
|
18910
19606
|
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
18911
19607
|
}))
|
|
18912
19608
|
})
|
|
18913
|
-
}).use([
|
|
19609
|
+
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18914
19610
|
const { body } = await c.data();
|
|
18915
19611
|
if (!isSafeReturnPath(body.returnUrl)) {
|
|
18916
|
-
throw new
|
|
19612
|
+
throw new ValidationError17({ message: "returnUrl must be a relative path within the app" });
|
|
18917
19613
|
}
|
|
18918
19614
|
const nonce = generateOAuthNonce();
|
|
18919
19615
|
setCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, nonce, {
|
|
@@ -18926,12 +19622,12 @@ var oauthStart = route8.post("/_auth/oauth/start").input({
|
|
|
18926
19622
|
const result = await oauthStartService({ ...body, nonce });
|
|
18927
19623
|
return result;
|
|
18928
19624
|
});
|
|
18929
|
-
var oauthProviders =
|
|
19625
|
+
var oauthProviders = route10.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
|
|
18930
19626
|
return {
|
|
18931
19627
|
providers: getEnabledOAuthProviders()
|
|
18932
19628
|
};
|
|
18933
19629
|
});
|
|
18934
|
-
var getGoogleOAuthUrl =
|
|
19630
|
+
var getGoogleOAuthUrl = route10.post("/_auth/oauth/google/url").input({
|
|
18935
19631
|
body: Type.Object({
|
|
18936
19632
|
returnUrl: Type.Optional(Type.String({
|
|
18937
19633
|
description: "URL to redirect after OAuth success"
|
|
@@ -18943,44 +19639,45 @@ var getGoogleOAuthUrl = route8.post("/_auth/oauth/google/url").input({
|
|
|
18943
19639
|
description: "Encrypted OAuth state (injected by interceptor)"
|
|
18944
19640
|
}))
|
|
18945
19641
|
})
|
|
18946
|
-
}).use([
|
|
19642
|
+
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18947
19643
|
const { body } = await c.data();
|
|
18948
19644
|
if (!isGoogleOAuthEnabled()) {
|
|
18949
|
-
throw new
|
|
19645
|
+
throw new ValidationError17({ message: "Google OAuth is not configured" });
|
|
18950
19646
|
}
|
|
18951
19647
|
if (!body.state) {
|
|
18952
|
-
throw new
|
|
19648
|
+
throw new ValidationError17({
|
|
18953
19649
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
18954
19650
|
});
|
|
18955
19651
|
}
|
|
18956
19652
|
return { authUrl: getGoogleAuthUrl(body.state) };
|
|
18957
19653
|
});
|
|
18958
|
-
var oauthFinalize =
|
|
19654
|
+
var oauthFinalize = route10.post("/_auth/oauth/finalize").input({
|
|
18959
19655
|
body: Type.Object({
|
|
18960
19656
|
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
18961
19657
|
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
18962
19658
|
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
18963
19659
|
})
|
|
18964
|
-
}).use([
|
|
19660
|
+
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18965
19661
|
const { body } = await c.data();
|
|
18966
19662
|
if (body.returnUrl && !isSafeReturnPath(body.returnUrl)) {
|
|
18967
|
-
throw new
|
|
19663
|
+
throw new ValidationError17({ message: "returnUrl must be a relative path within the app" });
|
|
18968
19664
|
}
|
|
18969
19665
|
return {
|
|
18970
19666
|
success: true,
|
|
18971
19667
|
userId: body.userId,
|
|
18972
19668
|
keyId: body.keyId,
|
|
18973
|
-
returnUrl: body.returnUrl || "/"
|
|
19669
|
+
returnUrl: body.returnUrl || "/",
|
|
19670
|
+
...await keySessionBindingService(body.keyId)
|
|
18974
19671
|
};
|
|
18975
19672
|
});
|
|
18976
|
-
var oauthProviderStart =
|
|
19673
|
+
var oauthProviderStart = route10.get("/_auth/oauth/:provider").input({
|
|
18977
19674
|
params: providerParams,
|
|
18978
19675
|
query: Type.Object({
|
|
18979
19676
|
state: Type.String({
|
|
18980
19677
|
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
18981
19678
|
})
|
|
18982
19679
|
})
|
|
18983
|
-
}).use([
|
|
19680
|
+
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18984
19681
|
const { params, query } = await c.data();
|
|
18985
19682
|
const provider = getOAuthProvider(params.provider);
|
|
18986
19683
|
if (!provider?.isEnabled()) {
|
|
@@ -18988,7 +19685,7 @@ var oauthProviderStart = route8.get("/_auth/oauth/:provider").input({
|
|
|
18988
19685
|
}
|
|
18989
19686
|
return c.redirect(provider.getAuthUrl(query.state));
|
|
18990
19687
|
});
|
|
18991
|
-
var oauthProviderCallback =
|
|
19688
|
+
var oauthProviderCallback = route10.get("/_auth/oauth/:provider/callback").input({
|
|
18992
19689
|
params: providerParams,
|
|
18993
19690
|
query: Type.Object({
|
|
18994
19691
|
code: Type.Optional(Type.String({
|
|
@@ -19004,7 +19701,7 @@ var oauthProviderCallback = route8.get("/_auth/oauth/:provider/callback").input(
|
|
|
19004
19701
|
description: "Error description from provider"
|
|
19005
19702
|
}))
|
|
19006
19703
|
})
|
|
19007
|
-
}).use([
|
|
19704
|
+
}).use([rateLimitPolicy10("oauth-callback", { limit: 30, windowMs: 6e4 }), Transactional8()]).skip(["auth"]).handler(async (c) => {
|
|
19008
19705
|
const { params, query } = await c.data();
|
|
19009
19706
|
if (query.error) {
|
|
19010
19707
|
const errorMessage = query.error_description || query.error;
|
|
@@ -19031,7 +19728,7 @@ var oauthProviderCallback = route8.get("/_auth/oauth/:provider/callback").input(
|
|
|
19031
19728
|
return c.redirect(buildOAuthErrorUrl(message));
|
|
19032
19729
|
}
|
|
19033
19730
|
});
|
|
19034
|
-
var getProviderOAuthUrl =
|
|
19731
|
+
var getProviderOAuthUrl = route10.post("/_auth/oauth/:provider/url").input({
|
|
19035
19732
|
params: providerParams,
|
|
19036
19733
|
body: Type.Object({
|
|
19037
19734
|
returnUrl: Type.Optional(Type.String({
|
|
@@ -19044,17 +19741,17 @@ var getProviderOAuthUrl = route8.post("/_auth/oauth/:provider/url").input({
|
|
|
19044
19741
|
description: "Encrypted OAuth state (injected by interceptor)"
|
|
19045
19742
|
}))
|
|
19046
19743
|
})
|
|
19047
|
-
}).use([
|
|
19744
|
+
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
19048
19745
|
const { params, body } = await c.data();
|
|
19049
19746
|
const provider = requireEnabledProvider(params.provider);
|
|
19050
19747
|
if (!body.state) {
|
|
19051
|
-
throw new
|
|
19748
|
+
throw new ValidationError17({
|
|
19052
19749
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
19053
19750
|
});
|
|
19054
19751
|
}
|
|
19055
19752
|
return { authUrl: provider.getAuthUrl(body.state) };
|
|
19056
19753
|
});
|
|
19057
|
-
var oauthNative =
|
|
19754
|
+
var oauthNative = route10.post("/_auth/oauth/:provider/native").input({
|
|
19058
19755
|
params: providerParams,
|
|
19059
19756
|
body: Type.Object({
|
|
19060
19757
|
idToken: Type.String({ description: "id_token from native/web social SDK" }),
|
|
@@ -19081,7 +19778,7 @@ var oauthNative = route8.post("/_auth/oauth/:provider/native").input({
|
|
|
19081
19778
|
description: "Custom metadata passed to auth events (e.g. referral code, UTM params)"
|
|
19082
19779
|
}))
|
|
19083
19780
|
})
|
|
19084
|
-
}).use([
|
|
19781
|
+
}).use([rateLimitPolicy10("oauth-native", {
|
|
19085
19782
|
limit: 5,
|
|
19086
19783
|
windowMs: 6e4,
|
|
19087
19784
|
by: byIpAndIdToken({ ipLimit: 20 })
|
|
@@ -19129,12 +19826,12 @@ async function processUnlinkNotify(provider, raw) {
|
|
|
19129
19826
|
return error instanceof UnlinkNotifyRejection ? error.status : 400;
|
|
19130
19827
|
}
|
|
19131
19828
|
}
|
|
19132
|
-
var oauthUnlinkNotify =
|
|
19829
|
+
var oauthUnlinkNotify = route10.post("/_auth/oauth/:provider/unlink-notify").input({ params: providerParams }).use([rateLimitPolicy10("oauth-unlink-notify", { limit: 300, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
19133
19830
|
const { params } = await c.data();
|
|
19134
19831
|
const status = await processUnlinkNotify(params.provider, c.raw);
|
|
19135
19832
|
return status === 204 ? c.noContent() : c.json({}, status);
|
|
19136
19833
|
});
|
|
19137
|
-
var oauthUnlinkNotifyGet =
|
|
19834
|
+
var oauthUnlinkNotifyGet = route10.get("/_auth/oauth/:provider/unlink-notify").input({ params: providerParams }).use([rateLimitPolicy10("oauth-unlink-notify", { limit: 300, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
19138
19835
|
const { params } = await c.data();
|
|
19139
19836
|
const status = await processUnlinkNotify(params.provider, c.raw);
|
|
19140
19837
|
return status === 204 ? c.noContent() : c.json({}, status);
|
|
@@ -19157,8 +19854,8 @@ var oauthRouter = defineRouter4({
|
|
|
19157
19854
|
// src/server/routes/admin/index.ts
|
|
19158
19855
|
init_esm();
|
|
19159
19856
|
import { ForbiddenError as ForbiddenError7 } from "@spfn/core/errors";
|
|
19160
|
-
import { route as
|
|
19161
|
-
var listRoles =
|
|
19857
|
+
import { route as route11 } from "@spfn/core/route";
|
|
19858
|
+
var listRoles = route11.get("/_auth/admin/roles").input({
|
|
19162
19859
|
query: Type.Object({
|
|
19163
19860
|
includeInactive: Type.Optional(Type.Boolean({
|
|
19164
19861
|
description: "Include inactive roles (default: false)"
|
|
@@ -19169,7 +19866,7 @@ var listRoles = route9.get("/_auth/admin/roles").input({
|
|
|
19169
19866
|
const roles2 = await getAllRoles(query.includeInactive ?? false);
|
|
19170
19867
|
return { roles: roles2 };
|
|
19171
19868
|
});
|
|
19172
|
-
var createAdminRole =
|
|
19869
|
+
var createAdminRole = route11.post("/_auth/admin/roles").input({
|
|
19173
19870
|
body: Type.Object({
|
|
19174
19871
|
name: Type.String({ description: "Unique role name (slug)" }),
|
|
19175
19872
|
displayName: Type.String({ description: "Human-readable role name" }),
|
|
@@ -19191,7 +19888,7 @@ var createAdminRole = route9.post("/_auth/admin/roles").input({
|
|
|
19191
19888
|
});
|
|
19192
19889
|
return { role };
|
|
19193
19890
|
});
|
|
19194
|
-
var updateAdminRole =
|
|
19891
|
+
var updateAdminRole = route11.patch("/_auth/admin/roles/:id").input({
|
|
19195
19892
|
params: Type.Object({
|
|
19196
19893
|
id: Type.Number({ description: "Role ID" })
|
|
19197
19894
|
}),
|
|
@@ -19206,7 +19903,7 @@ var updateAdminRole = route9.patch("/_auth/admin/roles/:id").input({
|
|
|
19206
19903
|
const role = await updateRole(params.id, body);
|
|
19207
19904
|
return { role };
|
|
19208
19905
|
});
|
|
19209
|
-
var deleteAdminRole =
|
|
19906
|
+
var deleteAdminRole = route11.delete("/_auth/admin/roles/:id").input({
|
|
19210
19907
|
params: Type.Object({
|
|
19211
19908
|
id: Type.Number({ description: "Role ID" })
|
|
19212
19909
|
})
|
|
@@ -19215,7 +19912,7 @@ var deleteAdminRole = route9.delete("/_auth/admin/roles/:id").input({
|
|
|
19215
19912
|
await deleteRole(params.id);
|
|
19216
19913
|
return c.noContent();
|
|
19217
19914
|
});
|
|
19218
|
-
var updateUserRole =
|
|
19915
|
+
var updateUserRole = route11.patch("/_auth/admin/users/:userId/role").input({
|
|
19219
19916
|
params: Type.Object({
|
|
19220
19917
|
userId: Type.Number({ description: "User ID" })
|
|
19221
19918
|
}),
|
|
@@ -19240,17 +19937,17 @@ var updateUserRole = route9.patch("/_auth/admin/users/:userId/role").input({
|
|
|
19240
19937
|
// src/server/routes/deletion/index.ts
|
|
19241
19938
|
init_esm();
|
|
19242
19939
|
init_schema3();
|
|
19243
|
-
import { Transactional as
|
|
19244
|
-
import { rateLimitPolicy as
|
|
19245
|
-
import { defineRouter as defineRouter5, route as
|
|
19246
|
-
var requestAccountDeletion =
|
|
19940
|
+
import { Transactional as Transactional9 } from "@spfn/core/db";
|
|
19941
|
+
import { rateLimitPolicy as rateLimitPolicy11 } from "@spfn/core/middleware";
|
|
19942
|
+
import { defineRouter as defineRouter5, route as route12 } from "@spfn/core/route";
|
|
19943
|
+
var requestAccountDeletion = route12.post("/_auth/deletion/request").input({
|
|
19247
19944
|
body: Type.Object({
|
|
19248
19945
|
password: Type.Optional(Type.String({ minLength: 1, description: "Current password, if the account has one" })),
|
|
19249
19946
|
verificationToken: Type.Optional(Type.String({ description: "Verification token (purpose: account_deletion), for passwordless/OAuth-only accounts" })),
|
|
19250
19947
|
reason: Type.Optional(Type.String({ maxLength: 500, description: "Optional free-text reason" })),
|
|
19251
19948
|
immediate: Type.Optional(Type.Boolean({ description: "Skip the grace period \u2014 requires deletion.allowSelfImmediate on the server" }))
|
|
19252
19949
|
})
|
|
19253
|
-
}).use([
|
|
19950
|
+
}).use([rateLimitPolicy11("auth-deletion-request", { limit: 5, windowMs: 6e4 }), Transactional9()]).handler(async (c) => {
|
|
19254
19951
|
const { body } = await c.data();
|
|
19255
19952
|
const { userId } = getAuth(c);
|
|
19256
19953
|
const result = await requestAccountDeletionService(Number(userId), {
|
|
@@ -19264,7 +19961,7 @@ var requestAccountDeletion = route10.post("/_auth/deletion/request").input({
|
|
|
19264
19961
|
purgeScheduledAt: result.purgeScheduledAt.toISOString()
|
|
19265
19962
|
};
|
|
19266
19963
|
});
|
|
19267
|
-
var cancelAccountDeletion =
|
|
19964
|
+
var cancelAccountDeletion = route12.post("/_auth/deletion/cancel").input({
|
|
19268
19965
|
body: Type.Object({
|
|
19269
19966
|
email: Type.Optional(EmailSchema),
|
|
19270
19967
|
phone: Type.Optional(PhoneSchema),
|
|
@@ -19275,7 +19972,7 @@ var cancelAccountDeletion = route10.post("/_auth/deletion/cancel").input({
|
|
|
19275
19972
|
// email/phone + password|verificationToken
|
|
19276
19973
|
description: "Email or phone must be provided with password or verificationToken"
|
|
19277
19974
|
})
|
|
19278
|
-
}).use([
|
|
19975
|
+
}).use([rateLimitPolicy11("auth-deletion-cancel", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 50 }) }), Transactional9()]).skip(["auth"]).handler(async (c) => {
|
|
19279
19976
|
const { body } = await c.data();
|
|
19280
19977
|
await cancelAccountDeletionService(body);
|
|
19281
19978
|
return c.noContent();
|
|
@@ -19287,7 +19984,7 @@ var deletionRouter = defineRouter5({
|
|
|
19287
19984
|
|
|
19288
19985
|
// src/server/routes/ops-tokens/index.ts
|
|
19289
19986
|
init_esm();
|
|
19290
|
-
import { route as
|
|
19987
|
+
import { route as route13 } from "@spfn/core/route";
|
|
19291
19988
|
import { BadRequestError as BadRequestError2, NotFoundError as NotFoundError5 } from "@spfn/core/errors";
|
|
19292
19989
|
var MAX_EXPIRY_DAYS = 36500;
|
|
19293
19990
|
function toSummary2(record) {
|
|
@@ -19301,7 +19998,7 @@ function toSummary2(record) {
|
|
|
19301
19998
|
createdAt: record.createdAt ? record.createdAt.toISOString() : null
|
|
19302
19999
|
};
|
|
19303
20000
|
}
|
|
19304
|
-
var issueOpsToken =
|
|
20001
|
+
var issueOpsToken = route13.post("/_auth/ops-tokens").input({
|
|
19305
20002
|
body: Type.Object({
|
|
19306
20003
|
name: Type.String({ minLength: 1, description: "Operator-facing label" }),
|
|
19307
20004
|
scopes: Type.Array(Type.String({ minLength: 1 }), {
|
|
@@ -19327,10 +20024,10 @@ var issueOpsToken = route11.post("/_auth/ops-tokens").input({
|
|
|
19327
20024
|
const { token, record } = await issueOpsTokenService(body.name, body.scopes, expiresAt);
|
|
19328
20025
|
return { token, opsToken: toSummary2(record) };
|
|
19329
20026
|
});
|
|
19330
|
-
var listOpsTokens =
|
|
20027
|
+
var listOpsTokens = route13.get("/_auth/ops-tokens").use([authenticate, requireRole("admin", "superadmin")]).handler(async () => {
|
|
19331
20028
|
return { opsTokens: (await listOpsTokensService()).map(toSummary2) };
|
|
19332
20029
|
});
|
|
19333
|
-
var revokeOpsToken =
|
|
20030
|
+
var revokeOpsToken = route13.delete("/_auth/ops-tokens/:id").input({
|
|
19334
20031
|
params: Type.Object({
|
|
19335
20032
|
id: Type.Number({ description: "Ops token id" })
|
|
19336
20033
|
})
|
|
@@ -19345,8 +20042,8 @@ var revokeOpsToken = route11.delete("/_auth/ops-tokens/:id").input({
|
|
|
19345
20042
|
|
|
19346
20043
|
// src/server/routes/oauth2/index.ts
|
|
19347
20044
|
init_esm();
|
|
19348
|
-
import { route as
|
|
19349
|
-
import { getClientIp as getClientIp3, rateLimitPolicy as
|
|
20045
|
+
import { route as route14 } from "@spfn/core/route";
|
|
20046
|
+
import { getClientIp as getClientIp3, rateLimitPolicy as rateLimitPolicy12 } from "@spfn/core/middleware";
|
|
19350
20047
|
|
|
19351
20048
|
// src/server/routes/oauth2/http.ts
|
|
19352
20049
|
import { NotFoundError as NotFoundError6 } from "@spfn/core/errors";
|
|
@@ -19405,7 +20102,7 @@ function flatten(parsed) {
|
|
|
19405
20102
|
}
|
|
19406
20103
|
|
|
19407
20104
|
// src/server/routes/oauth2/index.ts
|
|
19408
|
-
var registerOAuth2Client =
|
|
20105
|
+
var registerOAuth2Client = route14.post("/_auth/oauth2/register").use([rateLimitPolicy12("auth-oauth2-register", { limit: 10, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
19409
20106
|
requireAuthorizationServer();
|
|
19410
20107
|
const body = await readRegistrationBody(c.raw);
|
|
19411
20108
|
const result = await registerOAuth2ClientService(body, getClientIp3(c.raw) || null);
|
|
@@ -19422,17 +20119,17 @@ async function readRegistrationBody(c) {
|
|
|
19422
20119
|
return {};
|
|
19423
20120
|
}
|
|
19424
20121
|
}
|
|
19425
|
-
var listOAuth2Grants =
|
|
20122
|
+
var listOAuth2Grants = route14.get("/_auth/oauth2/grants").use([authenticate]).handler(async (c) => {
|
|
19426
20123
|
requireAuthorizationServer();
|
|
19427
20124
|
return { grants: await listOAuth2GrantsService(Number(getAuth(c).userId)) };
|
|
19428
20125
|
});
|
|
19429
|
-
var revokeOAuth2Grant =
|
|
20126
|
+
var revokeOAuth2Grant = route14.delete("/_auth/oauth2/grants/:id").input({ params: Type.Object({ id: Type.Number({ description: "Grant id from the list" }) }) }).use([authenticate]).handler(async (c) => {
|
|
19430
20127
|
requireAuthorizationServer();
|
|
19431
20128
|
const { params } = await c.data();
|
|
19432
20129
|
await revokeOAuth2GrantService(params.id, Number(getAuth(c).userId));
|
|
19433
20130
|
return { revoked: true };
|
|
19434
20131
|
});
|
|
19435
|
-
var oauth2AuthorizationServerMetadata =
|
|
20132
|
+
var oauth2AuthorizationServerMetadata = route14.get("/.well-known/oauth-authorization-server").skip(["auth"]).handler(async (c) => {
|
|
19436
20133
|
const config4 = requireAuthorizationServer();
|
|
19437
20134
|
return c.json({
|
|
19438
20135
|
issuer: config4.issuer,
|
|
@@ -19450,8 +20147,8 @@ var oauth2AuthorizationServerMetadata = route12.get("/.well-known/oauth-authoriz
|
|
|
19450
20147
|
|
|
19451
20148
|
// src/server/routes/oauth2/authorize.ts
|
|
19452
20149
|
init_esm();
|
|
19453
|
-
import { route as
|
|
19454
|
-
import { rateLimitPolicy as
|
|
20150
|
+
import { route as route15 } from "@spfn/core/route";
|
|
20151
|
+
import { rateLimitPolicy as rateLimitPolicy13 } from "@spfn/core/middleware";
|
|
19455
20152
|
var AUTHORIZE_FIELDS = {
|
|
19456
20153
|
client_id: Type.String({ minLength: 1, description: "client_id from dynamic registration" }),
|
|
19457
20154
|
redirect_uri: Type.String({ minLength: 1, description: "Where the code is sent; must be registered" }),
|
|
@@ -19472,17 +20169,17 @@ function toParams(input) {
|
|
|
19472
20169
|
state: input.state
|
|
19473
20170
|
};
|
|
19474
20171
|
}
|
|
19475
|
-
var authorizeRateLimit =
|
|
20172
|
+
var authorizeRateLimit = rateLimitPolicy13("auth-oauth2-authorize", {
|
|
19476
20173
|
limit: 30,
|
|
19477
20174
|
windowMs: 6e4,
|
|
19478
20175
|
by: byIpAndCaller({ ipLimit: 120 })
|
|
19479
20176
|
});
|
|
19480
|
-
var getOAuth2Authorize =
|
|
20177
|
+
var getOAuth2Authorize = route15.get("/_auth/oauth2/authorize").input({ query: Type.Object(AUTHORIZE_FIELDS) }).use([authenticate, authorizeRateLimit]).handler(async (c) => {
|
|
19481
20178
|
requireAuthorizationServer();
|
|
19482
20179
|
const { query } = await c.data();
|
|
19483
20180
|
return await describeOAuth2AuthorizeRequestService(toParams(query));
|
|
19484
20181
|
});
|
|
19485
|
-
var createOAuth2AuthorizationCode =
|
|
20182
|
+
var createOAuth2AuthorizationCode = route15.post("/_auth/oauth2/authorize").input({
|
|
19486
20183
|
body: Type.Object({
|
|
19487
20184
|
...AUTHORIZE_FIELDS,
|
|
19488
20185
|
approve: Type.Boolean({ description: "What the account owner decided" })
|
|
@@ -19498,9 +20195,9 @@ var createOAuth2AuthorizationCode = route13.post("/_auth/oauth2/authorize").inpu
|
|
|
19498
20195
|
});
|
|
19499
20196
|
|
|
19500
20197
|
// src/server/routes/oauth2/token.ts
|
|
19501
|
-
import { route as
|
|
19502
|
-
import { rateLimitPolicy as
|
|
19503
|
-
var oauth2Token =
|
|
20198
|
+
import { route as route16 } from "@spfn/core/route";
|
|
20199
|
+
import { rateLimitPolicy as rateLimitPolicy14 } from "@spfn/core/middleware";
|
|
20200
|
+
var oauth2Token = route16.post("/_auth/oauth2/token").use([rateLimitPolicy14("auth-oauth2-token", { limit: 60, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
19504
20201
|
requireAuthorizationServer();
|
|
19505
20202
|
const result = await oauth2TokenService(await readOAuth2Body(c.raw));
|
|
19506
20203
|
if (!result.ok) {
|
|
@@ -19508,7 +20205,7 @@ var oauth2Token = route14.post("/_auth/oauth2/token").use([rateLimitPolicy12("au
|
|
|
19508
20205
|
}
|
|
19509
20206
|
return oauth2JsonResponse(c.raw, 200, result.tokens);
|
|
19510
20207
|
});
|
|
19511
|
-
var oauth2Revoke =
|
|
20208
|
+
var oauth2Revoke = route16.post("/_auth/oauth2/revoke").use([rateLimitPolicy14("auth-oauth2-revoke", { limit: 60, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
19512
20209
|
requireAuthorizationServer();
|
|
19513
20210
|
const body = await readOAuth2Body(c.raw);
|
|
19514
20211
|
if (!body.client_id) {
|
|
@@ -19571,6 +20268,13 @@ var mainAuthRouter = defineRouter6({
|
|
|
19571
20268
|
consumeRevokeAllLink: consumeRevokeAllLink2,
|
|
19572
20269
|
changePassword,
|
|
19573
20270
|
getAuthSession,
|
|
20271
|
+
// Session binding routes (#97)
|
|
20272
|
+
setSessionBinding,
|
|
20273
|
+
getSessionBinding,
|
|
20274
|
+
sessionBindingDisableOptions,
|
|
20275
|
+
// Session renewal routes (#97) — public, like the sign-in paths
|
|
20276
|
+
sessionRenewOptions,
|
|
20277
|
+
sessionRenewVerify,
|
|
19574
20278
|
// One-Time Token routes
|
|
19575
20279
|
issueOneTimeToken,
|
|
19576
20280
|
// Account deletion routes
|
|
@@ -19732,6 +20436,7 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
19732
20436
|
}
|
|
19733
20437
|
|
|
19734
20438
|
// src/server/lib/session.ts
|
|
20439
|
+
init_logger();
|
|
19735
20440
|
import * as jose2 from "jose";
|
|
19736
20441
|
import { env as env19 } from "@spfn/auth/config";
|
|
19737
20442
|
import { env as coreEnv } from "@spfn/core/config";
|
|
@@ -19817,9 +20522,16 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
19817
20522
|
return hoursRemaining < thresholdHours;
|
|
19818
20523
|
}
|
|
19819
20524
|
|
|
20525
|
+
// src/server/lib/index.ts
|
|
20526
|
+
init_config();
|
|
20527
|
+
|
|
20528
|
+
// src/server.ts
|
|
20529
|
+
init_logger();
|
|
20530
|
+
|
|
19820
20531
|
// src/server/setup.ts
|
|
19821
20532
|
import { env as env20 } from "@spfn/auth/config";
|
|
19822
20533
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
20534
|
+
init_logger();
|
|
19823
20535
|
init_repositories();
|
|
19824
20536
|
function parseAdminAccounts() {
|
|
19825
20537
|
const accounts = [];
|
|
@@ -19940,7 +20652,11 @@ async function ensureAdminExists() {
|
|
|
19940
20652
|
}
|
|
19941
20653
|
}
|
|
19942
20654
|
|
|
20655
|
+
// src/server/lifecycle.ts
|
|
20656
|
+
init_logger();
|
|
20657
|
+
|
|
19943
20658
|
// src/server/lib/oauth/redirect-uri-check.ts
|
|
20659
|
+
init_logger();
|
|
19944
20660
|
var PROVIDERS = ["google", "kakao", "naver", "github"];
|
|
19945
20661
|
var OPT_OUT_VAR = "SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK";
|
|
19946
20662
|
function redirectUriVar(provider) {
|
|
@@ -20045,6 +20761,7 @@ import { defineJobRouter } from "@spfn/core/job";
|
|
|
20045
20761
|
|
|
20046
20762
|
// src/server/jobs/deletion-purge.ts
|
|
20047
20763
|
import { job as job2 } from "@spfn/core/job";
|
|
20764
|
+
init_logger();
|
|
20048
20765
|
function createAuthDeletionPurgeJob(cronExpression = DEFAULT_DELETION_PURGE_CRON) {
|
|
20049
20766
|
return job2("auth.deletion.purge").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
20050
20767
|
const result = await sweepDuePurges();
|
|
@@ -20056,6 +20773,7 @@ function createAuthDeletionPurgeJob(cronExpression = DEFAULT_DELETION_PURGE_CRON
|
|
|
20056
20773
|
|
|
20057
20774
|
// src/server/jobs/oauth2-client-purge.ts
|
|
20058
20775
|
import { job as job3 } from "@spfn/core/job";
|
|
20776
|
+
init_logger();
|
|
20059
20777
|
var DEFAULT_OAUTH2_CLIENT_PURGE_CRON = "0 5 * * *";
|
|
20060
20778
|
function createOAuth2ClientPurgeJob(cronExpression = DEFAULT_OAUTH2_CLIENT_PURGE_CRON) {
|
|
20061
20779
|
return job3("auth.oauth2.client-purge").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
@@ -20068,6 +20786,7 @@ function createOAuth2ClientPurgeJob(cronExpression = DEFAULT_OAUTH2_CLIENT_PURGE
|
|
|
20068
20786
|
|
|
20069
20787
|
// src/server/jobs/revoke-all-token-purge.ts
|
|
20070
20788
|
import { job as job4 } from "@spfn/core/job";
|
|
20789
|
+
init_logger();
|
|
20071
20790
|
var DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON = "0 6 * * *";
|
|
20072
20791
|
function createRevokeAllTokenPurgeJob(cronExpression = DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON) {
|
|
20073
20792
|
return job4("auth.revoke-all-token-purge").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
@@ -20080,6 +20799,7 @@ function createRevokeAllTokenPurgeJob(cronExpression = DEFAULT_REVOKE_ALL_TOKEN_
|
|
|
20080
20799
|
|
|
20081
20800
|
// src/server/jobs/mfa-sweep.ts
|
|
20082
20801
|
import { job as job5 } from "@spfn/core/job";
|
|
20802
|
+
init_logger();
|
|
20083
20803
|
var DEFAULT_MFA_SWEEP_CRON = "0 7 * * *";
|
|
20084
20804
|
function createMfaSweepJob(cronExpression = DEFAULT_MFA_SWEEP_CRON) {
|
|
20085
20805
|
return job5("auth.mfa.sweep").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
@@ -20166,6 +20886,7 @@ export {
|
|
|
20166
20886
|
PublicKeySchema,
|
|
20167
20887
|
RolePermissionsRepository,
|
|
20168
20888
|
RolesRepository,
|
|
20889
|
+
SESSION_BINDINGS,
|
|
20169
20890
|
SOCIAL_PROVIDERS,
|
|
20170
20891
|
STALE_CLIENT_MAX_AGE_MS,
|
|
20171
20892
|
SUPPORTED_GRANT_TYPES,
|
|
@@ -20192,6 +20913,8 @@ export {
|
|
|
20192
20913
|
accountDeletionRequests,
|
|
20193
20914
|
accountDeletionRequestsRepository,
|
|
20194
20915
|
addPermissionToRole,
|
|
20916
|
+
admitBearerKey,
|
|
20917
|
+
admitsForRenewal,
|
|
20195
20918
|
appleProvider,
|
|
20196
20919
|
approveDeviceAuthService,
|
|
20197
20920
|
approveOAuth2AuthorizeService,
|
|
@@ -20216,6 +20939,8 @@ export {
|
|
|
20216
20939
|
mainAuthRouter as authRouter,
|
|
20217
20940
|
authSchema,
|
|
20218
20941
|
authenticate,
|
|
20942
|
+
authenticateForRenewal,
|
|
20943
|
+
bearerAuthContext,
|
|
20219
20944
|
buildOAuthErrorUrl,
|
|
20220
20945
|
cancelAccountDeletionService,
|
|
20221
20946
|
cancelInvitation,
|
|
@@ -20254,11 +20979,14 @@ export {
|
|
|
20254
20979
|
deviceAuthorizations,
|
|
20255
20980
|
deviceAuthorizationsRepository,
|
|
20256
20981
|
disableMfaService,
|
|
20982
|
+
disableSessionBindingService,
|
|
20983
|
+
enableSessionBindingService,
|
|
20257
20984
|
encryptToken,
|
|
20258
20985
|
exchangeCodeForTokens,
|
|
20259
20986
|
expireOldInvitations,
|
|
20260
20987
|
finishPasskeyEnrollmentService,
|
|
20261
20988
|
finishPasskeyLoginService,
|
|
20989
|
+
finishSessionRenewService,
|
|
20262
20990
|
formatUserCode,
|
|
20263
20991
|
generateAccessToken,
|
|
20264
20992
|
generateAuthorizationCode,
|
|
@@ -20276,6 +21004,9 @@ export {
|
|
|
20276
21004
|
getAuthConfig,
|
|
20277
21005
|
getAuthSessionService,
|
|
20278
21006
|
getAuthorizationServerConfig,
|
|
21007
|
+
getBoundKeyRenewGraceMs,
|
|
21008
|
+
getBoundKeyTtlMs,
|
|
21009
|
+
getConcurrentUseWindowMs,
|
|
20279
21010
|
getCsrfExemptPaths,
|
|
20280
21011
|
getCsrfMode,
|
|
20281
21012
|
getDeletionConfig,
|
|
@@ -20305,7 +21036,9 @@ export {
|
|
|
20305
21036
|
getRole,
|
|
20306
21037
|
getRoleByName,
|
|
20307
21038
|
getRolePermissions,
|
|
21039
|
+
getSessionBindingService,
|
|
20308
21040
|
getSessionInfo,
|
|
21041
|
+
getSessionRenewPath,
|
|
20309
21042
|
getSessionTtl,
|
|
20310
21043
|
getUser,
|
|
20311
21044
|
getUserByEmailService,
|
|
@@ -20344,6 +21077,7 @@ export {
|
|
|
20344
21077
|
kakaoProvider,
|
|
20345
21078
|
keyRevokeAllTokens,
|
|
20346
21079
|
keyRevokeAllTokensRepository,
|
|
21080
|
+
keySessionBindingService,
|
|
20347
21081
|
keysRepository,
|
|
20348
21082
|
linkMailJob,
|
|
20349
21083
|
listInvitations,
|
|
@@ -20459,6 +21193,8 @@ export {
|
|
|
20459
21193
|
startDeviceAuthService,
|
|
20460
21194
|
startPasskeyEnrollmentService,
|
|
20461
21195
|
startPasskeyLoginService,
|
|
21196
|
+
startSessionBindingDisableService,
|
|
21197
|
+
startSessionRenewService,
|
|
20462
21198
|
startStepUpService,
|
|
20463
21199
|
startTotpEnrolmentService,
|
|
20464
21200
|
stepUpService,
|