@supertokens-plugins/rownd-nodejs 0.3.0-beta.5 → 0.3.0-beta.7
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 +58 -2
- package/dist/bulkMigrate.js +3 -2
- package/dist/generateAppConfig.js +4 -1
- package/dist/index.d.mts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +476 -34
- package/dist/index.mjs +473 -33
- package/dist/setupCoreInstance.js +17 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -667,7 +667,7 @@ var PLUGIN_SDK_VERSION = ["23.0.0", "23.0.1", ">=23.0.1"];
|
|
|
667
667
|
var HANDLE_BASE_PATH = "/plugin/rownd";
|
|
668
668
|
var PUBLIC_TENANT_ID = "public";
|
|
669
669
|
var GUEST_AUTH_METHOD_ID = "guest";
|
|
670
|
-
var
|
|
670
|
+
var INSTANT_AUTH_METHOD_ID = "instant";
|
|
671
671
|
var ROWND_JWT_CLAIMS = {
|
|
672
672
|
AppUserId: "https://auth.rownd.io/app_user_id",
|
|
673
673
|
IsVerifiedUser: "https://auth.rownd.io/is_verified_user",
|
|
@@ -701,6 +701,7 @@ var DEFAULT_ROWND_SCHEMA = {
|
|
|
701
701
|
};
|
|
702
702
|
var HUB_LOGIN_PAGE_PATH = "/account/login";
|
|
703
703
|
var HUB_VERIFY_EMAIL_PAGE_PATH = "/account/verify-email";
|
|
704
|
+
var PASSWORDLESS_BYPASS_DEVICE_CONFIRMATION_PARAM = "bypassDeviceConfirmation";
|
|
704
705
|
|
|
705
706
|
// src/logger.ts
|
|
706
707
|
var { logDebugMessage, enableDebugLogs } = buildLogger(
|
|
@@ -943,6 +944,109 @@ function getStringList(value) {
|
|
|
943
944
|
function getErrorMessage(error) {
|
|
944
945
|
return error instanceof Error ? error.message : "Unknown error";
|
|
945
946
|
}
|
|
947
|
+
function getAppInfoString(value) {
|
|
948
|
+
return value.getAsStringDangerous();
|
|
949
|
+
}
|
|
950
|
+
function getWebsiteDomain(input) {
|
|
951
|
+
return getAppInfoString(input.stConfig.appInfo.getOrigin({
|
|
952
|
+
request: input.request,
|
|
953
|
+
userContext: input.userContext ?? {}
|
|
954
|
+
}));
|
|
955
|
+
}
|
|
956
|
+
function normalizeClientDomain(value) {
|
|
957
|
+
if (value.endsWith("://")) {
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
960
|
+
const parsed = new URL(value);
|
|
961
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
962
|
+
return parsed.origin;
|
|
963
|
+
}
|
|
964
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
965
|
+
}
|
|
966
|
+
function resolveClientDomain(input) {
|
|
967
|
+
if (!input.clientDomain) {
|
|
968
|
+
return input.websiteDomain;
|
|
969
|
+
}
|
|
970
|
+
const resolved = input.pluginConfig.clientDomains?.[input.clientDomain];
|
|
971
|
+
if (!resolved) {
|
|
972
|
+
throw new Error(`Unknown clientDomain key: ${input.clientDomain}`);
|
|
973
|
+
}
|
|
974
|
+
return resolved;
|
|
975
|
+
}
|
|
976
|
+
function resolveAllowedClientDomain(input) {
|
|
977
|
+
const websiteDomain = getWebsiteDomain(input);
|
|
978
|
+
const resolvedClientDomain = resolveClientDomain({
|
|
979
|
+
clientDomain: input.clientDomain,
|
|
980
|
+
pluginConfig: input.pluginConfig,
|
|
981
|
+
websiteDomain
|
|
982
|
+
});
|
|
983
|
+
const normalizedClientDomain = normalizeClientDomain(resolvedClientDomain);
|
|
984
|
+
const allowed = [
|
|
985
|
+
websiteDomain,
|
|
986
|
+
...Object.values(input.pluginConfig.clientDomains ?? {})
|
|
987
|
+
].map(normalizeClientDomain);
|
|
988
|
+
if (!allowed.includes(normalizedClientDomain)) {
|
|
989
|
+
throw new Error(`clientDomain is not allowed: ${resolvedClientDomain}`);
|
|
990
|
+
}
|
|
991
|
+
return normalizedClientDomain;
|
|
992
|
+
}
|
|
993
|
+
function normalizeRedirectToPathForClientDomain(redirectToPath, clientDomain) {
|
|
994
|
+
if (!redirectToPath) {
|
|
995
|
+
return void 0;
|
|
996
|
+
}
|
|
997
|
+
if (redirectToPath === "NATIVE_APP") {
|
|
998
|
+
return redirectToPath;
|
|
999
|
+
}
|
|
1000
|
+
if (redirectToPath.startsWith("//")) {
|
|
1001
|
+
throw new Error("redirectToPath cannot be schemaless");
|
|
1002
|
+
}
|
|
1003
|
+
const normalizedClientDomain = normalizeClientDomain(clientDomain);
|
|
1004
|
+
const redirectUrl = new URL(
|
|
1005
|
+
redirectToPath,
|
|
1006
|
+
normalizedClientDomain.endsWith("://") ? "http://localhost" : normalizedClientDomain
|
|
1007
|
+
);
|
|
1008
|
+
const hasExplicitScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(redirectToPath);
|
|
1009
|
+
if (hasExplicitScheme) {
|
|
1010
|
+
if (redirectUrl.protocol !== "http:" && redirectUrl.protocol !== "https:") {
|
|
1011
|
+
throw new Error("redirectToPath must be http(s) or relative");
|
|
1012
|
+
}
|
|
1013
|
+
if (redirectUrl.origin !== normalizedClientDomain) {
|
|
1014
|
+
throw new Error("redirectToPath must match clientDomain");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return `${redirectUrl.pathname}${redirectUrl.search}${redirectUrl.hash}`;
|
|
1018
|
+
}
|
|
1019
|
+
function assertAllowedBypassRedirectPath(pluginConfig2, redirectToPath) {
|
|
1020
|
+
if (!redirectToPath) {
|
|
1021
|
+
throw new Error("redirectToPath is required for confirmation bypass magic links");
|
|
1022
|
+
}
|
|
1023
|
+
const allowedRedirectPaths = pluginConfig2.crossDeviceConfirmationBypass?.allowedRedirectPaths ?? [];
|
|
1024
|
+
if (allowedRedirectPaths.length === 0) {
|
|
1025
|
+
throw new Error("crossDeviceConfirmationBypass.allowedRedirectPaths must be configured");
|
|
1026
|
+
}
|
|
1027
|
+
if (!allowedRedirectPaths.includes(redirectToPath)) {
|
|
1028
|
+
throw new Error(`redirectToPath is not allowed for confirmation bypass: ${redirectToPath}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function getMagicLinkBootstrapParams(input) {
|
|
1032
|
+
return {
|
|
1033
|
+
appKey: input.appKey,
|
|
1034
|
+
apiDomain: input.apiDomain,
|
|
1035
|
+
apiBasePath: input.apiBasePath,
|
|
1036
|
+
...input.appVariantId ? { appVariantId: input.appVariantId } : {},
|
|
1037
|
+
...input.displayContext ? { displayContext: input.displayContext } : {},
|
|
1038
|
+
...input.redirectToPath ? { redirectToPath: input.redirectToPath } : {},
|
|
1039
|
+
...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function rewriteMagicLink(input) {
|
|
1043
|
+
return rewriteLinkToBaseUrl(
|
|
1044
|
+
input.magicLink,
|
|
1045
|
+
HUB_LOGIN_PAGE_PATH,
|
|
1046
|
+
input.clientDomain,
|
|
1047
|
+
input.bootstrapParams
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
946
1050
|
function getRequestedAppVariantIdFromRequest(req) {
|
|
947
1051
|
return req.getKeyValueFromQuery("app_variant_id");
|
|
948
1052
|
}
|
|
@@ -961,12 +1065,19 @@ function getRequestedRedirectToPathFromRequest(req) {
|
|
|
961
1065
|
|
|
962
1066
|
// src/config.ts
|
|
963
1067
|
var pluginConfig;
|
|
1068
|
+
var superTokensConfig;
|
|
964
1069
|
function setPluginConfig(config2) {
|
|
965
1070
|
pluginConfig = config2;
|
|
966
1071
|
}
|
|
967
1072
|
function getPluginConfig() {
|
|
968
1073
|
return pluginConfig;
|
|
969
1074
|
}
|
|
1075
|
+
function setSuperTokensConfig(config2) {
|
|
1076
|
+
superTokensConfig = config2;
|
|
1077
|
+
}
|
|
1078
|
+
function getSuperTokensConfig() {
|
|
1079
|
+
return superTokensConfig;
|
|
1080
|
+
}
|
|
970
1081
|
function assertRowndAppVariantIsConfigured(appVariantId) {
|
|
971
1082
|
if (!appVariantId) {
|
|
972
1083
|
return;
|
|
@@ -1047,7 +1158,10 @@ function buildSignInMethodsConfig(methodsArray) {
|
|
|
1047
1158
|
},
|
|
1048
1159
|
apple: {
|
|
1049
1160
|
enabled: !!appleMethod,
|
|
1050
|
-
client_id: getStringMethodProperty(appleMethod, "clientId") ?? ""
|
|
1161
|
+
client_id: getStringMethodProperty(appleMethod, "clientId") ?? "",
|
|
1162
|
+
...getStringMethodProperty(appleMethod, "webClientType") !== void 0 ? { web_client_type: getStringMethodProperty(appleMethod, "webClientType") } : {},
|
|
1163
|
+
...getStringMethodProperty(appleMethod, "iosClientType") !== void 0 ? { ios_client_type: getStringMethodProperty(appleMethod, "iosClientType") } : {},
|
|
1164
|
+
...getStringMethodProperty(appleMethod, "androidClientType") !== void 0 ? { android_client_type: getStringMethodProperty(appleMethod, "androidClientType") } : {}
|
|
1051
1165
|
},
|
|
1052
1166
|
anonymous: {
|
|
1053
1167
|
enabled: !!anonymousMethod && anonymousType !== "instant",
|
|
@@ -1321,6 +1435,7 @@ function buildAuthMobileConfig(authMobile) {
|
|
|
1321
1435
|
|
|
1322
1436
|
// src/rownd-compatibility.ts
|
|
1323
1437
|
import SuperTokens from "supertokens-node";
|
|
1438
|
+
import UserMetadata2 from "supertokens-node/recipe/usermetadata";
|
|
1324
1439
|
var IDENTITY_USER_DATA_FIELDS = /* @__PURE__ */ new Set([
|
|
1325
1440
|
"user_id",
|
|
1326
1441
|
"email",
|
|
@@ -1385,7 +1500,7 @@ function mapRowndUserToSuperTokens(rowndUser) {
|
|
|
1385
1500
|
}
|
|
1386
1501
|
let authLevel = rowndUser.auth_level;
|
|
1387
1502
|
if (loginMethods.length === 0) {
|
|
1388
|
-
const thirdPartyId = authLevel === GUEST_AUTH_METHOD_ID ? GUEST_AUTH_METHOD_ID :
|
|
1503
|
+
const thirdPartyId = authLevel === GUEST_AUTH_METHOD_ID ? GUEST_AUTH_METHOD_ID : INSTANT_AUTH_METHOD_ID;
|
|
1389
1504
|
if (!authLevel) authLevel = thirdPartyId;
|
|
1390
1505
|
loginMethods.push({
|
|
1391
1506
|
recipeId: "thirdparty",
|
|
@@ -1457,11 +1572,10 @@ function getAnonymousId(userId, user, metadata) {
|
|
|
1457
1572
|
if (typeof originalAnonymousId === "string") {
|
|
1458
1573
|
return originalAnonymousId;
|
|
1459
1574
|
}
|
|
1460
|
-
const
|
|
1461
|
-
return loginMethod.recipeId === "thirdparty" && getThirdPartyId(loginMethod) ===
|
|
1575
|
+
const guestMethod = user?.loginMethods.find((loginMethod) => {
|
|
1576
|
+
return loginMethod.recipeId === "thirdparty" && getThirdPartyId(loginMethod) === GUEST_AUTH_METHOD_ID;
|
|
1462
1577
|
});
|
|
1463
|
-
|
|
1464
|
-
return thirdPartyUserId || (hasAnonymousLoginMethod(user) ? `anon_${userId}` : void 0);
|
|
1578
|
+
return guestMethod ? `anon_${user?.id || userId}` : void 0;
|
|
1465
1579
|
}
|
|
1466
1580
|
function buildRowndSessionClaimPayload(input) {
|
|
1467
1581
|
const currentPayload = input.currentPayload ?? {};
|
|
@@ -1478,7 +1592,7 @@ function buildRowndSessionClaimPayload(input) {
|
|
|
1478
1592
|
currentPayload,
|
|
1479
1593
|
input.metadata
|
|
1480
1594
|
);
|
|
1481
|
-
const isAnonymous = currentPayload.is_anonymous === true ||
|
|
1595
|
+
const isAnonymous = currentPayload.is_anonymous === true || [GUEST_AUTH_METHOD_ID, INSTANT_AUTH_METHOD_ID].includes(authLevel);
|
|
1482
1596
|
const anonymousId = getAnonymousId(input.userId, input.user, input.metadata);
|
|
1483
1597
|
const isVerifiedUser = authLevel !== "unverified";
|
|
1484
1598
|
const audience = buildRowndAudience(currentPayload, input.appVariantId);
|
|
@@ -1492,10 +1606,144 @@ function buildRowndSessionClaimPayload(input) {
|
|
|
1492
1606
|
[ROWND_JWT_CLAIMS.AppUserId]: appUserId,
|
|
1493
1607
|
[ROWND_JWT_CLAIMS.AuthLevel]: authLevel,
|
|
1494
1608
|
[ROWND_JWT_CLAIMS.IsVerifiedUser]: isVerifiedUser,
|
|
1495
|
-
[ROWND_JWT_CLAIMS.IsAnonymous]:
|
|
1609
|
+
...isAnonymous ? { [ROWND_JWT_CLAIMS.IsAnonymous]: true } : {},
|
|
1496
1610
|
...anonymousId ? { anonymous_id: anonymousId } : {}
|
|
1497
1611
|
};
|
|
1498
1612
|
}
|
|
1613
|
+
function normalizeRowndOAuthScopes(scopes) {
|
|
1614
|
+
return [...new Set(scopes.filter((scope) => scope.length > 0))];
|
|
1615
|
+
}
|
|
1616
|
+
function getRowndOAuthAudience(input) {
|
|
1617
|
+
const requested = input.requestedResource ?? input.requestedAudience;
|
|
1618
|
+
if (requested?.startsWith("app:")) {
|
|
1619
|
+
return requested;
|
|
1620
|
+
}
|
|
1621
|
+
return void 0;
|
|
1622
|
+
}
|
|
1623
|
+
function applyRowndOAuthResourceParams(input) {
|
|
1624
|
+
const resource = firstString(input.params.resource);
|
|
1625
|
+
const audience = firstString(input.params.audience);
|
|
1626
|
+
const rowndAudience = getRowndOAuthAudience({
|
|
1627
|
+
requestedResource: resource,
|
|
1628
|
+
requestedAudience: audience
|
|
1629
|
+
});
|
|
1630
|
+
if (!rowndAudience) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
input.userContext.rowndOAuthAudience = rowndAudience;
|
|
1634
|
+
input.params.audience = audience ?? rowndAudience;
|
|
1635
|
+
delete input.params.resource;
|
|
1636
|
+
}
|
|
1637
|
+
async function buildRowndOAuthPayload(input) {
|
|
1638
|
+
const currentPayload = input.currentPayload ?? {};
|
|
1639
|
+
const standardClaims = input.user ? await buildStandardOAuthClaims(input.user, input.scopes) : {};
|
|
1640
|
+
const rowndClaims = input.user ? await buildRowndOAuthSessionClaims(input.user, currentPayload) : {};
|
|
1641
|
+
const audience = getRowndOAuthAudience({
|
|
1642
|
+
requestedAudience: typeof input.userContext?.rowndOAuthAudience === "string" ? input.userContext.rowndOAuthAudience : void 0
|
|
1643
|
+
});
|
|
1644
|
+
return {
|
|
1645
|
+
...currentPayload,
|
|
1646
|
+
...standardClaims,
|
|
1647
|
+
...rowndClaims,
|
|
1648
|
+
...audience ? { aud: audience } : {}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
async function buildRowndOAuthUserInfo(input) {
|
|
1652
|
+
const standardClaims = await buildStandardOAuthClaims(
|
|
1653
|
+
input.user,
|
|
1654
|
+
input.scopes
|
|
1655
|
+
);
|
|
1656
|
+
const rowndClaims = pickOAuthUserInfoRowndClaims(input.accessTokenPayload);
|
|
1657
|
+
return {
|
|
1658
|
+
...input.currentPayload,
|
|
1659
|
+
...standardClaims,
|
|
1660
|
+
...rowndClaims
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async function buildStandardOAuthClaims(user, scopes) {
|
|
1664
|
+
const claims = {};
|
|
1665
|
+
const metadata = await getRowndMetadata(user.id);
|
|
1666
|
+
const rowndData = isJsonRecord(metadata.original_rownd_user?.data) ? metadata.original_rownd_user.data : {};
|
|
1667
|
+
const verifiedData = isJsonRecord(metadata.original_rownd_user?.verified_data) ? metadata.original_rownd_user.verified_data : {};
|
|
1668
|
+
if (scopes.includes("email")) {
|
|
1669
|
+
const email = firstString(rowndData.email) ?? user.emails[0];
|
|
1670
|
+
if (email) {
|
|
1671
|
+
claims.email = email;
|
|
1672
|
+
claims.email_verified = isOAuthClaimVerified(
|
|
1673
|
+
verifiedData.email,
|
|
1674
|
+
email,
|
|
1675
|
+
user.loginMethods.some(
|
|
1676
|
+
(method) => method.hasSameEmailAs(email) && method.verified
|
|
1677
|
+
)
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (scopes.includes("phone")) {
|
|
1682
|
+
const phoneNumber = firstString(rowndData.phone_number) ?? user.phoneNumbers[0];
|
|
1683
|
+
if (phoneNumber) {
|
|
1684
|
+
claims.phone_number = phoneNumber;
|
|
1685
|
+
claims.phone_number_verified = isOAuthClaimVerified(
|
|
1686
|
+
verifiedData.phone_number,
|
|
1687
|
+
phoneNumber,
|
|
1688
|
+
user.loginMethods.some(
|
|
1689
|
+
(method) => method.hasSamePhoneNumberAs(phoneNumber) && method.verified
|
|
1690
|
+
)
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (scopes.includes("profile")) {
|
|
1695
|
+
const givenName = firstString(rowndData.first_name);
|
|
1696
|
+
const familyName = firstString(rowndData.last_name);
|
|
1697
|
+
const name = [givenName, familyName].filter(Boolean).join(" ");
|
|
1698
|
+
if (name) claims.name = name;
|
|
1699
|
+
if (givenName) claims.given_name = givenName;
|
|
1700
|
+
if (familyName) claims.family_name = familyName;
|
|
1701
|
+
if (typeof metadata.original_rownd_user?.data?.updated_at === "string") {
|
|
1702
|
+
claims.updated_at = metadata.original_rownd_user.data.updated_at;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return claims;
|
|
1706
|
+
}
|
|
1707
|
+
async function getRowndMetadata(userId) {
|
|
1708
|
+
const metadata = await UserMetadata2.getUserMetadata(userId);
|
|
1709
|
+
return metadata.metadata || {};
|
|
1710
|
+
}
|
|
1711
|
+
function pickOAuthUserInfoRowndClaims(payload) {
|
|
1712
|
+
const claims = {};
|
|
1713
|
+
for (const key of [
|
|
1714
|
+
"app_user_id",
|
|
1715
|
+
"auth_level",
|
|
1716
|
+
"is_verified_user",
|
|
1717
|
+
"is_anonymous",
|
|
1718
|
+
"anonymous_id",
|
|
1719
|
+
"https://auth.rownd.io/app_user_id",
|
|
1720
|
+
"https://auth.rownd.io/auth_level",
|
|
1721
|
+
"https://auth.rownd.io/is_verified_user",
|
|
1722
|
+
"https://auth.rownd.io/is_anonymous"
|
|
1723
|
+
]) {
|
|
1724
|
+
if (payload[key] !== void 0) {
|
|
1725
|
+
claims[key] = payload[key];
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return claims;
|
|
1729
|
+
}
|
|
1730
|
+
function firstString(value) {
|
|
1731
|
+
if (Array.isArray(value)) {
|
|
1732
|
+
return value.find((entry) => typeof entry === "string");
|
|
1733
|
+
}
|
|
1734
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1735
|
+
}
|
|
1736
|
+
function isOAuthClaimVerified(value, expectedValue, fallback) {
|
|
1737
|
+
return value === true || value === expectedValue || fallback;
|
|
1738
|
+
}
|
|
1739
|
+
async function buildRowndOAuthSessionClaims(user, currentPayload) {
|
|
1740
|
+
return buildRowndSessionClaimPayload({
|
|
1741
|
+
userId: user.id,
|
|
1742
|
+
user,
|
|
1743
|
+
metadata: await getRowndMetadata(user.id),
|
|
1744
|
+
currentPayload
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1499
1747
|
async function shouldLinkRowndAccounts(input) {
|
|
1500
1748
|
const [newAccountInfo, , session] = input;
|
|
1501
1749
|
if (!session) {
|
|
@@ -1538,23 +1786,26 @@ function getThirdPartyUserId(method) {
|
|
|
1538
1786
|
return method.thirdParty?.userId;
|
|
1539
1787
|
}
|
|
1540
1788
|
function getGuestAuthLevel(user) {
|
|
1541
|
-
const guestMethod = user?.loginMethods.find(
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
function hasAnonymousLoginMethod(user) {
|
|
1545
|
-
return !!user?.loginMethods.some((loginMethod) => {
|
|
1546
|
-
return loginMethod.recipeId === "thirdparty" && getThirdPartyId(loginMethod) === ANONYMOUS_AUTH_METHOD_ID;
|
|
1789
|
+
const guestMethod = user?.loginMethods.find((method) => {
|
|
1790
|
+
return method.recipeId === "thirdparty" && getThirdPartyId(method) === GUEST_AUTH_METHOD_ID;
|
|
1547
1791
|
});
|
|
1792
|
+
if (guestMethod) {
|
|
1793
|
+
return GUEST_AUTH_METHOD_ID;
|
|
1794
|
+
}
|
|
1795
|
+
const instantMethod = user?.loginMethods.find((method) => {
|
|
1796
|
+
return method.recipeId === "thirdparty" && getThirdPartyId(method) === INSTANT_AUTH_METHOD_ID;
|
|
1797
|
+
});
|
|
1798
|
+
return instantMethod ? INSTANT_AUTH_METHOD_ID : void 0;
|
|
1548
1799
|
}
|
|
1549
1800
|
function hasOnlyGuestLoginMethods(user) {
|
|
1550
1801
|
return !!user?.loginMethods.length && user.loginMethods.every(isGuestLoginMethod);
|
|
1551
1802
|
}
|
|
1552
1803
|
function isGuestLoginMethod(method) {
|
|
1553
1804
|
const thirdPartyId = getThirdPartyId(method);
|
|
1554
|
-
return method.recipeId === "thirdparty" && (thirdPartyId === GUEST_AUTH_METHOD_ID || thirdPartyId ===
|
|
1805
|
+
return method.recipeId === "thirdparty" && (thirdPartyId === GUEST_AUTH_METHOD_ID || thirdPartyId === INSTANT_AUTH_METHOD_ID);
|
|
1555
1806
|
}
|
|
1556
1807
|
function isGuestAccountInfo(input) {
|
|
1557
|
-
return input?.recipeId === "thirdparty" && (input.thirdParty?.id === GUEST_AUTH_METHOD_ID || input.thirdParty?.id ===
|
|
1808
|
+
return input?.recipeId === "thirdparty" && (input.thirdParty?.id === GUEST_AUTH_METHOD_ID || input.thirdParty?.id === INSTANT_AUTH_METHOD_ID);
|
|
1558
1809
|
}
|
|
1559
1810
|
function doesAccountInfoMatchAuthMethod(user, accountInfo) {
|
|
1560
1811
|
if (!user) {
|
|
@@ -1594,8 +1845,8 @@ function hasVerifiedRealLoginMethod(user) {
|
|
|
1594
1845
|
});
|
|
1595
1846
|
}
|
|
1596
1847
|
function getEffectiveAuthLevel(user, originalAuthLevel, verifiedData) {
|
|
1597
|
-
if (originalAuthLevel ===
|
|
1598
|
-
return
|
|
1848
|
+
if (originalAuthLevel === INSTANT_AUTH_METHOD_ID) {
|
|
1849
|
+
return INSTANT_AUTH_METHOD_ID;
|
|
1599
1850
|
}
|
|
1600
1851
|
if (hasVerifiedRealLoginMethod(user)) {
|
|
1601
1852
|
return "verified";
|
|
@@ -1640,7 +1891,7 @@ import EmailVerification from "supertokens-node/recipe/emailverification";
|
|
|
1640
1891
|
import Passwordless from "supertokens-node/recipe/passwordless";
|
|
1641
1892
|
import Session from "supertokens-node/recipe/session";
|
|
1642
1893
|
import { BooleanClaim } from "supertokens-node/recipe/session/claims";
|
|
1643
|
-
import
|
|
1894
|
+
import UserMetadata3 from "supertokens-node/recipe/usermetadata";
|
|
1644
1895
|
async function importUser(stUser, config2) {
|
|
1645
1896
|
const headers = {
|
|
1646
1897
|
"Content-Type": "application/json"
|
|
@@ -1679,7 +1930,7 @@ async function recordRowndAppVariantForUser(userId, appVariantId) {
|
|
|
1679
1930
|
if (appVariants.includes(appVariantId)) {
|
|
1680
1931
|
return;
|
|
1681
1932
|
}
|
|
1682
|
-
await
|
|
1933
|
+
await UserMetadata3.updateUserMetadata(userId, {
|
|
1683
1934
|
...metadata,
|
|
1684
1935
|
original_rownd_user: {
|
|
1685
1936
|
...originalRowndUser,
|
|
@@ -1697,7 +1948,7 @@ var RowndIsAnonymousClaim = new BooleanClaim({
|
|
|
1697
1948
|
fetchValue: async (userId) => {
|
|
1698
1949
|
const user = await SuperTokens2.getUser(userId);
|
|
1699
1950
|
const effectiveAuthLevel = getEffectiveAuthLevel(user);
|
|
1700
|
-
return effectiveAuthLevel
|
|
1951
|
+
return [GUEST_AUTH_METHOD_ID, INSTANT_AUTH_METHOD_ID].includes(effectiveAuthLevel);
|
|
1701
1952
|
}
|
|
1702
1953
|
});
|
|
1703
1954
|
async function buildRowndSessionClaims(userId, currentPayload = {}, appVariantId) {
|
|
@@ -1711,8 +1962,78 @@ async function buildRowndSessionClaims(userId, currentPayload = {}, appVariantId
|
|
|
1711
1962
|
appVariantId
|
|
1712
1963
|
});
|
|
1713
1964
|
}
|
|
1965
|
+
async function createMagicLinkWithConfirmationBypass(input) {
|
|
1966
|
+
const hasEmail = typeof input.email === "string" && input.email.length > 0;
|
|
1967
|
+
const hasPhoneNumber = typeof input.phoneNumber === "string" && input.phoneNumber.length > 0;
|
|
1968
|
+
if (hasEmail === hasPhoneNumber) {
|
|
1969
|
+
throw new Error("Exactly one of email or phoneNumber is required");
|
|
1970
|
+
}
|
|
1971
|
+
const stConfig = getSuperTokensConfig();
|
|
1972
|
+
if (!stConfig) {
|
|
1973
|
+
throw new Error("SuperTokens config is not initialized");
|
|
1974
|
+
}
|
|
1975
|
+
const pluginConfig2 = getPluginConfig();
|
|
1976
|
+
if (!pluginConfig2) {
|
|
1977
|
+
throw new Error("Rownd plugin config is not initialized");
|
|
1978
|
+
}
|
|
1979
|
+
const tenantId = input.tenantId ?? PUBLIC_TENANT_ID;
|
|
1980
|
+
const appVariantId = input.appVariantId;
|
|
1981
|
+
assertRowndAppVariantIsConfigured(appVariantId);
|
|
1982
|
+
const clientDomain = resolveAllowedClientDomain({
|
|
1983
|
+
clientDomain: input.clientDomain,
|
|
1984
|
+
pluginConfig: pluginConfig2,
|
|
1985
|
+
stConfig,
|
|
1986
|
+
request: input.request,
|
|
1987
|
+
userContext: input.userContext
|
|
1988
|
+
});
|
|
1989
|
+
const redirectToPath = normalizeRedirectToPathForClientDomain(input.redirectToPath, clientDomain);
|
|
1990
|
+
assertAllowedBypassRedirectPath(pluginConfig2, redirectToPath);
|
|
1991
|
+
const userContext = {
|
|
1992
|
+
...input.userContext ?? {},
|
|
1993
|
+
...input.displayContext ? { rowndDisplayContext: input.displayContext } : {},
|
|
1994
|
+
rowndRedirectToPath: redirectToPath,
|
|
1995
|
+
...input.clientDomain ? { rowndClientDomain: input.clientDomain } : {},
|
|
1996
|
+
...appVariantId ? { rowndAppVariantId: appVariantId } : {}
|
|
1997
|
+
};
|
|
1998
|
+
const codeInfo = hasEmail ? await Passwordless.createCode({
|
|
1999
|
+
email: input.email,
|
|
2000
|
+
tenantId,
|
|
2001
|
+
session: input.session,
|
|
2002
|
+
userContext
|
|
2003
|
+
}) : await Passwordless.createCode({
|
|
2004
|
+
phoneNumber: input.phoneNumber,
|
|
2005
|
+
tenantId,
|
|
2006
|
+
session: input.session,
|
|
2007
|
+
userContext
|
|
2008
|
+
});
|
|
2009
|
+
if (codeInfo.status !== "OK") {
|
|
2010
|
+
throw new Error("Failed to create magic link");
|
|
2011
|
+
}
|
|
2012
|
+
const magicLink = `${getWebsiteDomain({
|
|
2013
|
+
stConfig,
|
|
2014
|
+
request: input.request,
|
|
2015
|
+
userContext
|
|
2016
|
+
})}${getAppInfoString(stConfig.appInfo.websiteBasePath)}/verify?preAuthSessionId=${encodeURIComponent(
|
|
2017
|
+
codeInfo.preAuthSessionId
|
|
2018
|
+
)}&tenantId=${encodeURIComponent(tenantId)}#${encodeURIComponent(codeInfo.linkCode)}`;
|
|
2019
|
+
const rewrittenUrl = new URL(rewriteMagicLink({
|
|
2020
|
+
magicLink,
|
|
2021
|
+
clientDomain,
|
|
2022
|
+
bootstrapParams: getMagicLinkBootstrapParams({
|
|
2023
|
+
appKey: pluginConfig2.rowndAppKey,
|
|
2024
|
+
apiDomain: getAppInfoString(stConfig.appInfo.apiDomain),
|
|
2025
|
+
apiBasePath: getAppInfoString(stConfig.appInfo.apiBasePath),
|
|
2026
|
+
appVariantId,
|
|
2027
|
+
displayContext: input.displayContext,
|
|
2028
|
+
redirectToPath,
|
|
2029
|
+
clientDomainKey: input.clientDomain
|
|
2030
|
+
})
|
|
2031
|
+
}));
|
|
2032
|
+
rewrittenUrl.searchParams.set(PASSWORDLESS_BYPASS_DEVICE_CONFIRMATION_PARAM, "true");
|
|
2033
|
+
return rewrittenUrl.toString();
|
|
2034
|
+
}
|
|
1714
2035
|
async function getUserMetadata(userId) {
|
|
1715
|
-
const metadata = await
|
|
2036
|
+
const metadata = await UserMetadata3.getUserMetadata(userId);
|
|
1716
2037
|
return metadata.metadata || {};
|
|
1717
2038
|
}
|
|
1718
2039
|
function getPendingVerifications(metadata) {
|
|
@@ -1869,7 +2190,7 @@ async function updateUserData(userId, inputData) {
|
|
|
1869
2190
|
...metadata,
|
|
1870
2191
|
...inputData
|
|
1871
2192
|
};
|
|
1872
|
-
await
|
|
2193
|
+
await UserMetadata3.updateUserMetadata(userId, updatedMetadata);
|
|
1873
2194
|
return getUserById(userId);
|
|
1874
2195
|
}
|
|
1875
2196
|
async function startPendingEmailVerification(input) {
|
|
@@ -1889,7 +2210,7 @@ async function startPendingEmailVerification(input) {
|
|
|
1889
2210
|
);
|
|
1890
2211
|
}
|
|
1891
2212
|
if (pendingEmailVerifications.length > 0) {
|
|
1892
|
-
await
|
|
2213
|
+
await UserMetadata3.updateUserMetadata(input.userId, {
|
|
1893
2214
|
...metadata,
|
|
1894
2215
|
rownd_pending_verification: pendingVerifications.filter(
|
|
1895
2216
|
(pendingVerification2) => pendingVerification2.field !== "email"
|
|
@@ -1912,7 +2233,7 @@ async function startPendingEmailVerification(input) {
|
|
|
1912
2233
|
value: input.email,
|
|
1913
2234
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1914
2235
|
};
|
|
1915
|
-
await
|
|
2236
|
+
await UserMetadata3.updateUserMetadata(input.userId, {
|
|
1916
2237
|
...metadata,
|
|
1917
2238
|
rownd_pending_verification: [
|
|
1918
2239
|
...pendingVerifications.filter(
|
|
@@ -2033,7 +2354,7 @@ async function completePendingEmailVerification(input) {
|
|
|
2033
2354
|
)
|
|
2034
2355
|
)
|
|
2035
2356
|
};
|
|
2036
|
-
await
|
|
2357
|
+
await UserMetadata3.updateUserMetadata(metadataUserId, updatedMetadata);
|
|
2037
2358
|
}
|
|
2038
2359
|
function isMatchingPendingEmailVerification(verification, email) {
|
|
2039
2360
|
return verification.field === "email" && verification.value === email;
|
|
@@ -2044,7 +2365,7 @@ async function updateUserMetadata(userId, inputMeta) {
|
|
|
2044
2365
|
...metadata,
|
|
2045
2366
|
...inputMeta
|
|
2046
2367
|
};
|
|
2047
|
-
await
|
|
2368
|
+
await UserMetadata3.updateUserMetadata(userId, updatedMetadata);
|
|
2048
2369
|
return {
|
|
2049
2370
|
id: userId,
|
|
2050
2371
|
meta: Object.fromEntries(
|
|
@@ -2065,6 +2386,9 @@ import { randomUUID } from "crypto";
|
|
|
2065
2386
|
import SuperTokens3 from "supertokens-node";
|
|
2066
2387
|
import Session2 from "supertokens-node/recipe/session";
|
|
2067
2388
|
import ThirdParty from "supertokens-node/recipe/thirdparty";
|
|
2389
|
+
function isBodyString(body, key) {
|
|
2390
|
+
return isRecord(body) && typeof body[key] === "string" && body[key].length > 0;
|
|
2391
|
+
}
|
|
2068
2392
|
function handleGetAppConfig(deps) {
|
|
2069
2393
|
return async (req) => {
|
|
2070
2394
|
const appVariantId = getRequestedAppVariantIdFromRequest(req);
|
|
@@ -2085,6 +2409,43 @@ function handleGetAppConfig(deps) {
|
|
|
2085
2409
|
};
|
|
2086
2410
|
};
|
|
2087
2411
|
}
|
|
2412
|
+
function handleValidatePasswordlessConfirmationBypass(deps) {
|
|
2413
|
+
return async (req) => {
|
|
2414
|
+
try {
|
|
2415
|
+
const body = await getJsonBody(req);
|
|
2416
|
+
const clientDomain = isBodyString(body, "clientDomain") ? body.clientDomain : void 0;
|
|
2417
|
+
const redirectToPath = isBodyString(body, "redirectToPath") ? body.redirectToPath : void 0;
|
|
2418
|
+
const appVariantId = isBodyString(body, "appVariantId") ? body.appVariantId : void 0;
|
|
2419
|
+
assertRowndAppVariantIsConfigured(appVariantId);
|
|
2420
|
+
const resolvedClientDomain = resolveAllowedClientDomain({
|
|
2421
|
+
clientDomain,
|
|
2422
|
+
pluginConfig: deps.pluginConfig,
|
|
2423
|
+
stConfig: deps.stConfig,
|
|
2424
|
+
request: req
|
|
2425
|
+
});
|
|
2426
|
+
const normalizedRedirectToPath = normalizeRedirectToPathForClientDomain(
|
|
2427
|
+
redirectToPath,
|
|
2428
|
+
resolvedClientDomain
|
|
2429
|
+
);
|
|
2430
|
+
assertAllowedBypassRedirectPath(
|
|
2431
|
+
deps.pluginConfig,
|
|
2432
|
+
normalizedRedirectToPath
|
|
2433
|
+
);
|
|
2434
|
+
return {
|
|
2435
|
+
status: "OK",
|
|
2436
|
+
bypass: true
|
|
2437
|
+
};
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
logDebugMessage(
|
|
2440
|
+
`Passwordless confirmation bypass validation failed. Error: ${getErrorMessage(error)}`
|
|
2441
|
+
);
|
|
2442
|
+
return {
|
|
2443
|
+
status: "ERROR",
|
|
2444
|
+
bypass: false
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2088
2449
|
function handleGuestLogin(deps) {
|
|
2089
2450
|
return async (req, res, _session, userContext) => {
|
|
2090
2451
|
const startedAt = Date.now();
|
|
@@ -2093,9 +2454,9 @@ function handleGuestLogin(deps) {
|
|
|
2093
2454
|
const body = parseGuestBody(await getJsonBody(req));
|
|
2094
2455
|
const appVariantId = getRequestedAppVariantIdFromRequest(req);
|
|
2095
2456
|
assertRowndAppVariantIsConfigured(appVariantId);
|
|
2096
|
-
const thirdPartyId = body.authLevel ===
|
|
2097
|
-
const thirdPartyUserId = thirdPartyId ===
|
|
2098
|
-
const authLevel = thirdPartyId ===
|
|
2457
|
+
const thirdPartyId = body.authLevel === INSTANT_AUTH_METHOD_ID ? INSTANT_AUTH_METHOD_ID : GUEST_AUTH_METHOD_ID;
|
|
2458
|
+
const thirdPartyUserId = thirdPartyId === INSTANT_AUTH_METHOD_ID ? `anon_${randomUUID()}` : guestId;
|
|
2459
|
+
const authLevel = thirdPartyId === INSTANT_AUTH_METHOD_ID ? INSTANT_AUTH_METHOD_ID : GUEST_AUTH_METHOD_ID;
|
|
2099
2460
|
const response = await ThirdParty.manuallyCreateOrUpdateUser(
|
|
2100
2461
|
PUBLIC_TENANT_ID,
|
|
2101
2462
|
thirdPartyId,
|
|
@@ -2118,7 +2479,7 @@ function handleGuestLogin(deps) {
|
|
|
2118
2479
|
{
|
|
2119
2480
|
...buildRowndAudience({}, appVariantId),
|
|
2120
2481
|
auth_level: authLevel,
|
|
2121
|
-
is_anonymous: true,
|
|
2482
|
+
...[GUEST_AUTH_METHOD_ID, INSTANT_AUTH_METHOD_ID].includes(authLevel) ? { is_anonymous: true } : {},
|
|
2122
2483
|
app_user_id: response.user.id
|
|
2123
2484
|
},
|
|
2124
2485
|
{},
|
|
@@ -2482,6 +2843,7 @@ var init = createPluginInitFunction(
|
|
|
2482
2843
|
}
|
|
2483
2844
|
},
|
|
2484
2845
|
routeHandlers(stConfig) {
|
|
2846
|
+
setSuperTokensConfig(stConfig);
|
|
2485
2847
|
const apiBasePath = stConfig.appInfo.apiBasePath.getAsStringDangerous();
|
|
2486
2848
|
hubBootstrapParams = {
|
|
2487
2849
|
apiDomain: stConfig.appInfo.apiDomain.getAsStringDangerous(),
|
|
@@ -2512,6 +2874,13 @@ var init = createPluginInitFunction(
|
|
|
2512
2874
|
method: "post",
|
|
2513
2875
|
handler: withRequestHandler(handleMigrate(routeHandlerDeps))
|
|
2514
2876
|
},
|
|
2877
|
+
{
|
|
2878
|
+
path: `${apiBasePath}/plugin/passwordless-cross-device-confirmation/validate`,
|
|
2879
|
+
method: "post",
|
|
2880
|
+
handler: withRequestHandler(
|
|
2881
|
+
handleValidatePasswordlessConfirmationBypass(routeHandlerDeps)
|
|
2882
|
+
)
|
|
2883
|
+
},
|
|
2515
2884
|
{
|
|
2516
2885
|
path: `${apiBasePath}/plugin/migrate-session`,
|
|
2517
2886
|
method: "post",
|
|
@@ -2572,6 +2941,74 @@ var init = createPluginInitFunction(
|
|
|
2572
2941
|
};
|
|
2573
2942
|
},
|
|
2574
2943
|
overrideMap: {
|
|
2944
|
+
oauth2provider: {
|
|
2945
|
+
recipeInitRequired: false,
|
|
2946
|
+
functions: (originalImplementation) => ({
|
|
2947
|
+
...originalImplementation,
|
|
2948
|
+
getRequestedScopes: async (input) => {
|
|
2949
|
+
const scopes = await originalImplementation.getRequestedScopes(
|
|
2950
|
+
input
|
|
2951
|
+
);
|
|
2952
|
+
return normalizeRowndOAuthScopes(scopes);
|
|
2953
|
+
},
|
|
2954
|
+
buildAccessTokenPayload: async (input) => {
|
|
2955
|
+
const payload = await originalImplementation.buildAccessTokenPayload(
|
|
2956
|
+
input
|
|
2957
|
+
);
|
|
2958
|
+
return buildRowndOAuthPayload({
|
|
2959
|
+
user: input.user,
|
|
2960
|
+
client: input.client,
|
|
2961
|
+
scopes: input.scopes,
|
|
2962
|
+
currentPayload: payload,
|
|
2963
|
+
userContext: input.userContext
|
|
2964
|
+
});
|
|
2965
|
+
},
|
|
2966
|
+
buildIdTokenPayload: async (input) => {
|
|
2967
|
+
const payload = await originalImplementation.buildIdTokenPayload(
|
|
2968
|
+
input
|
|
2969
|
+
);
|
|
2970
|
+
return buildRowndOAuthPayload({
|
|
2971
|
+
user: input.user,
|
|
2972
|
+
client: input.client,
|
|
2973
|
+
scopes: input.scopes,
|
|
2974
|
+
currentPayload: payload,
|
|
2975
|
+
userContext: input.userContext
|
|
2976
|
+
});
|
|
2977
|
+
},
|
|
2978
|
+
buildUserInfo: async (input) => {
|
|
2979
|
+
const payload = await originalImplementation.buildUserInfo(input);
|
|
2980
|
+
return buildRowndOAuthUserInfo({
|
|
2981
|
+
user: input.user,
|
|
2982
|
+
accessTokenPayload: input.accessTokenPayload,
|
|
2983
|
+
scopes: input.scopes,
|
|
2984
|
+
currentPayload: payload
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
}),
|
|
2988
|
+
apis: (originalImplementation) => ({
|
|
2989
|
+
...originalImplementation,
|
|
2990
|
+
authGET: async (input) => {
|
|
2991
|
+
if (originalImplementation.authGET === void 0) {
|
|
2992
|
+
throw new Error("OAuth2Provider authGET is unavailable");
|
|
2993
|
+
}
|
|
2994
|
+
applyRowndOAuthResourceParams({
|
|
2995
|
+
params: input.params,
|
|
2996
|
+
userContext: input.userContext
|
|
2997
|
+
});
|
|
2998
|
+
return originalImplementation.authGET(input);
|
|
2999
|
+
},
|
|
3000
|
+
tokenPOST: async (input) => {
|
|
3001
|
+
if (originalImplementation.tokenPOST === void 0) {
|
|
3002
|
+
throw new Error("OAuth2Provider tokenPOST is unavailable");
|
|
3003
|
+
}
|
|
3004
|
+
applyRowndOAuthResourceParams({
|
|
3005
|
+
params: input.body,
|
|
3006
|
+
userContext: input.userContext
|
|
3007
|
+
});
|
|
3008
|
+
return originalImplementation.tokenPOST(input);
|
|
3009
|
+
}
|
|
3010
|
+
})
|
|
3011
|
+
},
|
|
2575
3012
|
passwordless: {
|
|
2576
3013
|
config: (config2) => {
|
|
2577
3014
|
const originalEmailDeliveryOverride = config2.emailDelivery?.override;
|
|
@@ -2834,6 +3271,7 @@ var init = createPluginInitFunction(
|
|
|
2834
3271
|
rowndAppSecret: config2.rowndAppSecret,
|
|
2835
3272
|
enableDebugLogs: config2.enableDebugLogs,
|
|
2836
3273
|
clientDomains: config2.clientDomains,
|
|
3274
|
+
crossDeviceConfirmationBypass: config2.crossDeviceConfirmationBypass,
|
|
2837
3275
|
telemetry: config2.telemetry,
|
|
2838
3276
|
schema: config2.schema,
|
|
2839
3277
|
appConfig: config2.appConfig,
|
|
@@ -2855,12 +3293,13 @@ function validateClientDomainUrl(key, value) {
|
|
|
2855
3293
|
// src/index.ts
|
|
2856
3294
|
var index_default = { init };
|
|
2857
3295
|
export {
|
|
2858
|
-
ANONYMOUS_AUTH_METHOD_ID,
|
|
2859
3296
|
DEFAULT_ROWND_SCHEMA,
|
|
2860
3297
|
GUEST_AUTH_METHOD_ID,
|
|
2861
3298
|
HANDLE_BASE_PATH,
|
|
2862
3299
|
HUB_LOGIN_PAGE_PATH,
|
|
2863
3300
|
HUB_VERIFY_EMAIL_PAGE_PATH,
|
|
3301
|
+
INSTANT_AUTH_METHOD_ID,
|
|
3302
|
+
PASSWORDLESS_BYPASS_DEVICE_CONFIRMATION_PARAM,
|
|
2864
3303
|
PLUGIN_ID,
|
|
2865
3304
|
PLUGIN_SDK_VERSION,
|
|
2866
3305
|
PLUGIN_VERSION,
|
|
@@ -2868,6 +3307,7 @@ export {
|
|
|
2868
3307
|
ROWND_JWT_CLAIMS,
|
|
2869
3308
|
ROWND_PLUGIN_ERROR_MESSAGES,
|
|
2870
3309
|
RowndPluginError,
|
|
3310
|
+
createMagicLinkWithConfirmationBypass,
|
|
2871
3311
|
index_default as default,
|
|
2872
3312
|
getRowndClient,
|
|
2873
3313
|
init,
|