@objectstack/plugin-auth 6.2.0 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +21 -5
- package/dist/index.d.ts +21 -5
- package/dist/index.js +102 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
package/dist/index.mjs
CHANGED
|
@@ -665,26 +665,49 @@ var AuthManager = class {
|
|
|
665
665
|
*
|
|
666
666
|
* better-auth defaults to `@better-auth/utils/password.node`, which calls
|
|
667
667
|
* `node:crypto.scrypt`. WebContainer polyfills that API incompletely and
|
|
668
|
-
* signup throws `TypeError: y.run is not a function`.
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
* `
|
|
668
|
+
* signup throws `TypeError: y.run is not a function`.
|
|
669
|
+
*
|
|
670
|
+
* We can't dynamic-import `@better-auth/utils/password` because that
|
|
671
|
+
* package's `exports` map gates the pure-JS build behind a non-`"node"`
|
|
672
|
+
* condition — Node-the-runtime (which WebContainer reports itself as)
|
|
673
|
+
* always resolves to `password.node.mjs`. So we reimplement the same hash
|
|
674
|
+
* here using `@noble/hashes/scrypt` directly, with byte-identical params
|
|
675
|
+
* (N=16384, r=16, p=1, dkLen=64) and the same `{saltHex}:{keyHex}` storage
|
|
676
|
+
* format. Hashes produced by either implementation verify against the
|
|
677
|
+
* other — no migration needed.
|
|
672
678
|
*
|
|
673
679
|
* Returns `undefined` outside WebContainer so production deployments keep
|
|
674
|
-
* the native (fast) hasher and never load
|
|
680
|
+
* the native (fast) hasher and never load `@noble/hashes`.
|
|
675
681
|
*/
|
|
676
682
|
async resolvePasswordHasher() {
|
|
677
683
|
const isWebContainer = typeof globalThis !== "undefined" && (Boolean(globalThis.process?.versions?.webcontainer) || Boolean(globalThis.process?.env?.SHELL?.includes?.("jsh")) || Boolean(globalThis.process?.env?.STACKBLITZ));
|
|
678
684
|
if (!isWebContainer) return void 0;
|
|
679
685
|
try {
|
|
680
|
-
const
|
|
686
|
+
const { scryptAsync } = await import("@noble/hashes/scrypt.js");
|
|
687
|
+
const PARAMS = { N: 16384, r: 16, p: 1, dkLen: 64, maxmem: 128 * 16384 * 16 * 2 };
|
|
688
|
+
const toHex = (b) => {
|
|
689
|
+
let s = "";
|
|
690
|
+
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
691
|
+
return s;
|
|
692
|
+
};
|
|
693
|
+
const generateKey = (password, saltHex) => scryptAsync(password.normalize("NFKC"), saltHex, PARAMS);
|
|
681
694
|
return {
|
|
682
|
-
hash: (password) =>
|
|
683
|
-
|
|
695
|
+
hash: async (password) => {
|
|
696
|
+
const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
697
|
+
const saltHex = toHex(saltBytes);
|
|
698
|
+
const key = await generateKey(password, saltHex);
|
|
699
|
+
return `${saltHex}:${toHex(key)}`;
|
|
700
|
+
},
|
|
701
|
+
verify: async ({ hash, password }) => {
|
|
702
|
+
const [saltHex, keyHex] = hash.split(":");
|
|
703
|
+
if (!saltHex || !keyHex) throw new Error("Invalid password hash");
|
|
704
|
+
const target = await generateKey(password, saltHex);
|
|
705
|
+
return toHex(target) === keyHex;
|
|
706
|
+
}
|
|
684
707
|
};
|
|
685
708
|
} catch (err) {
|
|
686
709
|
console.warn(
|
|
687
|
-
`[AuthManager] WebContainer detected but pure-JS
|
|
710
|
+
`[AuthManager] WebContainer detected but pure-JS scrypt unavailable: ${err?.message ?? err}. Falling back to default.`
|
|
688
711
|
);
|
|
689
712
|
return void 0;
|
|
690
713
|
}
|
|
@@ -1058,7 +1081,8 @@ var AuthManager = class {
|
|
|
1058
1081
|
*/
|
|
1059
1082
|
async handleRequest(request) {
|
|
1060
1083
|
const auth = await this.getOrCreateAuth();
|
|
1061
|
-
const
|
|
1084
|
+
const { runWithRequestState } = await import("@better-auth/core/context");
|
|
1085
|
+
const response = await runWithRequestState(/* @__PURE__ */ new WeakMap(), () => auth.handler(request));
|
|
1062
1086
|
if (response.status >= 500) {
|
|
1063
1087
|
try {
|
|
1064
1088
|
const body = await response.clone().text();
|
|
@@ -1077,6 +1101,19 @@ var AuthManager = class {
|
|
|
1077
1101
|
const auth = await this.getOrCreateAuth();
|
|
1078
1102
|
return auth.api;
|
|
1079
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Get the underlying better-auth context for low-level operations such as
|
|
1106
|
+
* `internalAdapter.createAccount` / `password.hash`.
|
|
1107
|
+
*
|
|
1108
|
+
* Used by routes that need to write to better-auth's tables outside the
|
|
1109
|
+
* normal endpoint surface — currently only `set-initial-password`, which
|
|
1110
|
+
* provisions a credential account for SSO-onboarded users so they can
|
|
1111
|
+
* sign in with email/password going forward.
|
|
1112
|
+
*/
|
|
1113
|
+
async getAuthContext() {
|
|
1114
|
+
const auth = await this.getOrCreateAuth();
|
|
1115
|
+
return auth.$context;
|
|
1116
|
+
}
|
|
1080
1117
|
// ---------------------------------------------------------------------------
|
|
1081
1118
|
// Device Flow (CLI browser-based login)
|
|
1082
1119
|
//
|
|
@@ -1393,6 +1430,61 @@ var AuthPlugin = class {
|
|
|
1393
1430
|
return c.json({ hasOwner: true });
|
|
1394
1431
|
}
|
|
1395
1432
|
});
|
|
1433
|
+
rawApp.post(`${basePath}/set-initial-password`, async (c) => {
|
|
1434
|
+
try {
|
|
1435
|
+
let body = {};
|
|
1436
|
+
try {
|
|
1437
|
+
body = await c.req.json();
|
|
1438
|
+
} catch {
|
|
1439
|
+
body = {};
|
|
1440
|
+
}
|
|
1441
|
+
const newPassword = body?.newPassword;
|
|
1442
|
+
if (typeof newPassword !== "string" || newPassword.length === 0) {
|
|
1443
|
+
return c.json({ success: false, error: { code: "invalid_request", message: "newPassword is required" } }, 400);
|
|
1444
|
+
}
|
|
1445
|
+
const authApi = await this.authManager.getApi();
|
|
1446
|
+
const session = await authApi.getSession({ headers: c.req.raw.headers });
|
|
1447
|
+
if (!session?.user?.id) {
|
|
1448
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Sign in first" } }, 401);
|
|
1449
|
+
}
|
|
1450
|
+
const userId = session.user.id;
|
|
1451
|
+
const authCtx = await this.authManager.getAuthContext();
|
|
1452
|
+
if (!authCtx?.internalAdapter || !authCtx?.password) {
|
|
1453
|
+
return c.json({ success: false, error: { code: "unavailable", message: "Auth context unavailable" } }, 503);
|
|
1454
|
+
}
|
|
1455
|
+
const minLen = authCtx.password?.config?.minPasswordLength ?? 8;
|
|
1456
|
+
const maxLen = authCtx.password?.config?.maxPasswordLength ?? 128;
|
|
1457
|
+
if (newPassword.length < minLen) {
|
|
1458
|
+
return c.json({ success: false, error: { code: "password_too_short", message: `Password must be at least ${minLen} characters` } }, 400);
|
|
1459
|
+
}
|
|
1460
|
+
if (newPassword.length > maxLen) {
|
|
1461
|
+
return c.json({ success: false, error: { code: "password_too_long", message: `Password must be at most ${maxLen} characters` } }, 400);
|
|
1462
|
+
}
|
|
1463
|
+
const accounts = await authCtx.internalAdapter.findAccounts(userId);
|
|
1464
|
+
const existingCredential = accounts?.find?.((a) => a.providerId === "credential" && a.password);
|
|
1465
|
+
if (existingCredential) {
|
|
1466
|
+
return c.json({
|
|
1467
|
+
success: false,
|
|
1468
|
+
error: {
|
|
1469
|
+
code: "credential_account_exists",
|
|
1470
|
+
message: "A local password is already set for this account. Use change-password instead."
|
|
1471
|
+
}
|
|
1472
|
+
}, 409);
|
|
1473
|
+
}
|
|
1474
|
+
const passwordHash = await authCtx.password.hash(newPassword);
|
|
1475
|
+
await authCtx.internalAdapter.createAccount({
|
|
1476
|
+
userId,
|
|
1477
|
+
providerId: "credential",
|
|
1478
|
+
accountId: userId,
|
|
1479
|
+
password: passwordHash
|
|
1480
|
+
});
|
|
1481
|
+
return c.json({ success: true });
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1484
|
+
ctx.logger.error("[AuthPlugin] set-initial-password failed", err);
|
|
1485
|
+
return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1396
1488
|
rawApp.all(`${basePath}/*`, async (c) => {
|
|
1397
1489
|
try {
|
|
1398
1490
|
const response = await this.authManager.handleRequest(c.req.raw);
|