@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.d.mts
CHANGED
|
@@ -217,13 +217,19 @@ declare class AuthManager {
|
|
|
217
217
|
*
|
|
218
218
|
* better-auth defaults to `@better-auth/utils/password.node`, which calls
|
|
219
219
|
* `node:crypto.scrypt`. WebContainer polyfills that API incompletely and
|
|
220
|
-
* signup throws `TypeError: y.run is not a function`.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* `
|
|
220
|
+
* signup throws `TypeError: y.run is not a function`.
|
|
221
|
+
*
|
|
222
|
+
* We can't dynamic-import `@better-auth/utils/password` because that
|
|
223
|
+
* package's `exports` map gates the pure-JS build behind a non-`"node"`
|
|
224
|
+
* condition — Node-the-runtime (which WebContainer reports itself as)
|
|
225
|
+
* always resolves to `password.node.mjs`. So we reimplement the same hash
|
|
226
|
+
* here using `@noble/hashes/scrypt` directly, with byte-identical params
|
|
227
|
+
* (N=16384, r=16, p=1, dkLen=64) and the same `{saltHex}:{keyHex}` storage
|
|
228
|
+
* format. Hashes produced by either implementation verify against the
|
|
229
|
+
* other — no migration needed.
|
|
224
230
|
*
|
|
225
231
|
* Returns `undefined` outside WebContainer so production deployments keep
|
|
226
|
-
* the native (fast) hasher and never load
|
|
232
|
+
* the native (fast) hasher and never load `@noble/hashes`.
|
|
227
233
|
*/
|
|
228
234
|
private resolvePasswordHasher;
|
|
229
235
|
/**
|
|
@@ -298,6 +304,16 @@ declare class AuthManager {
|
|
|
298
304
|
* Use this for server-side operations (e.g., creating users, checking sessions)
|
|
299
305
|
*/
|
|
300
306
|
getApi(): Promise<Auth<any>['api']>;
|
|
307
|
+
/**
|
|
308
|
+
* Get the underlying better-auth context for low-level operations such as
|
|
309
|
+
* `internalAdapter.createAccount` / `password.hash`.
|
|
310
|
+
*
|
|
311
|
+
* Used by routes that need to write to better-auth's tables outside the
|
|
312
|
+
* normal endpoint surface — currently only `set-initial-password`, which
|
|
313
|
+
* provisions a credential account for SSO-onboarded users so they can
|
|
314
|
+
* sign in with email/password going forward.
|
|
315
|
+
*/
|
|
316
|
+
getAuthContext(): Promise<any>;
|
|
301
317
|
getPublicConfig(): {
|
|
302
318
|
emailPassword: {
|
|
303
319
|
enabled: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -217,13 +217,19 @@ declare class AuthManager {
|
|
|
217
217
|
*
|
|
218
218
|
* better-auth defaults to `@better-auth/utils/password.node`, which calls
|
|
219
219
|
* `node:crypto.scrypt`. WebContainer polyfills that API incompletely and
|
|
220
|
-
* signup throws `TypeError: y.run is not a function`.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* `
|
|
220
|
+
* signup throws `TypeError: y.run is not a function`.
|
|
221
|
+
*
|
|
222
|
+
* We can't dynamic-import `@better-auth/utils/password` because that
|
|
223
|
+
* package's `exports` map gates the pure-JS build behind a non-`"node"`
|
|
224
|
+
* condition — Node-the-runtime (which WebContainer reports itself as)
|
|
225
|
+
* always resolves to `password.node.mjs`. So we reimplement the same hash
|
|
226
|
+
* here using `@noble/hashes/scrypt` directly, with byte-identical params
|
|
227
|
+
* (N=16384, r=16, p=1, dkLen=64) and the same `{saltHex}:{keyHex}` storage
|
|
228
|
+
* format. Hashes produced by either implementation verify against the
|
|
229
|
+
* other — no migration needed.
|
|
224
230
|
*
|
|
225
231
|
* Returns `undefined` outside WebContainer so production deployments keep
|
|
226
|
-
* the native (fast) hasher and never load
|
|
232
|
+
* the native (fast) hasher and never load `@noble/hashes`.
|
|
227
233
|
*/
|
|
228
234
|
private resolvePasswordHasher;
|
|
229
235
|
/**
|
|
@@ -298,6 +304,16 @@ declare class AuthManager {
|
|
|
298
304
|
* Use this for server-side operations (e.g., creating users, checking sessions)
|
|
299
305
|
*/
|
|
300
306
|
getApi(): Promise<Auth<any>['api']>;
|
|
307
|
+
/**
|
|
308
|
+
* Get the underlying better-auth context for low-level operations such as
|
|
309
|
+
* `internalAdapter.createAccount` / `password.hash`.
|
|
310
|
+
*
|
|
311
|
+
* Used by routes that need to write to better-auth's tables outside the
|
|
312
|
+
* normal endpoint surface — currently only `set-initial-password`, which
|
|
313
|
+
* provisions a credential account for SSO-onboarded users so they can
|
|
314
|
+
* sign in with email/password going forward.
|
|
315
|
+
*/
|
|
316
|
+
getAuthContext(): Promise<any>;
|
|
301
317
|
getPublicConfig(): {
|
|
302
318
|
emailPassword: {
|
|
303
319
|
enabled: boolean;
|
package/dist/index.js
CHANGED
|
@@ -729,26 +729,49 @@ var AuthManager = class {
|
|
|
729
729
|
*
|
|
730
730
|
* better-auth defaults to `@better-auth/utils/password.node`, which calls
|
|
731
731
|
* `node:crypto.scrypt`. WebContainer polyfills that API incompletely and
|
|
732
|
-
* signup throws `TypeError: y.run is not a function`.
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
* `
|
|
732
|
+
* signup throws `TypeError: y.run is not a function`.
|
|
733
|
+
*
|
|
734
|
+
* We can't dynamic-import `@better-auth/utils/password` because that
|
|
735
|
+
* package's `exports` map gates the pure-JS build behind a non-`"node"`
|
|
736
|
+
* condition — Node-the-runtime (which WebContainer reports itself as)
|
|
737
|
+
* always resolves to `password.node.mjs`. So we reimplement the same hash
|
|
738
|
+
* here using `@noble/hashes/scrypt` directly, with byte-identical params
|
|
739
|
+
* (N=16384, r=16, p=1, dkLen=64) and the same `{saltHex}:{keyHex}` storage
|
|
740
|
+
* format. Hashes produced by either implementation verify against the
|
|
741
|
+
* other — no migration needed.
|
|
736
742
|
*
|
|
737
743
|
* Returns `undefined` outside WebContainer so production deployments keep
|
|
738
|
-
* the native (fast) hasher and never load
|
|
744
|
+
* the native (fast) hasher and never load `@noble/hashes`.
|
|
739
745
|
*/
|
|
740
746
|
async resolvePasswordHasher() {
|
|
741
747
|
const isWebContainer = typeof globalThis !== "undefined" && (Boolean(globalThis.process?.versions?.webcontainer) || Boolean(globalThis.process?.env?.SHELL?.includes?.("jsh")) || Boolean(globalThis.process?.env?.STACKBLITZ));
|
|
742
748
|
if (!isWebContainer) return void 0;
|
|
743
749
|
try {
|
|
744
|
-
const
|
|
750
|
+
const { scryptAsync } = await import("@noble/hashes/scrypt.js");
|
|
751
|
+
const PARAMS = { N: 16384, r: 16, p: 1, dkLen: 64, maxmem: 128 * 16384 * 16 * 2 };
|
|
752
|
+
const toHex = (b) => {
|
|
753
|
+
let s = "";
|
|
754
|
+
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
755
|
+
return s;
|
|
756
|
+
};
|
|
757
|
+
const generateKey = (password, saltHex) => scryptAsync(password.normalize("NFKC"), saltHex, PARAMS);
|
|
745
758
|
return {
|
|
746
|
-
hash: (password) =>
|
|
747
|
-
|
|
759
|
+
hash: async (password) => {
|
|
760
|
+
const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
761
|
+
const saltHex = toHex(saltBytes);
|
|
762
|
+
const key = await generateKey(password, saltHex);
|
|
763
|
+
return `${saltHex}:${toHex(key)}`;
|
|
764
|
+
},
|
|
765
|
+
verify: async ({ hash, password }) => {
|
|
766
|
+
const [saltHex, keyHex] = hash.split(":");
|
|
767
|
+
if (!saltHex || !keyHex) throw new Error("Invalid password hash");
|
|
768
|
+
const target = await generateKey(password, saltHex);
|
|
769
|
+
return toHex(target) === keyHex;
|
|
770
|
+
}
|
|
748
771
|
};
|
|
749
772
|
} catch (err) {
|
|
750
773
|
console.warn(
|
|
751
|
-
`[AuthManager] WebContainer detected but pure-JS
|
|
774
|
+
`[AuthManager] WebContainer detected but pure-JS scrypt unavailable: ${err?.message ?? err}. Falling back to default.`
|
|
752
775
|
);
|
|
753
776
|
return void 0;
|
|
754
777
|
}
|
|
@@ -1122,7 +1145,8 @@ var AuthManager = class {
|
|
|
1122
1145
|
*/
|
|
1123
1146
|
async handleRequest(request) {
|
|
1124
1147
|
const auth = await this.getOrCreateAuth();
|
|
1125
|
-
const
|
|
1148
|
+
const { runWithRequestState } = await import("@better-auth/core/context");
|
|
1149
|
+
const response = await runWithRequestState(/* @__PURE__ */ new WeakMap(), () => auth.handler(request));
|
|
1126
1150
|
if (response.status >= 500) {
|
|
1127
1151
|
try {
|
|
1128
1152
|
const body = await response.clone().text();
|
|
@@ -1141,6 +1165,19 @@ var AuthManager = class {
|
|
|
1141
1165
|
const auth = await this.getOrCreateAuth();
|
|
1142
1166
|
return auth.api;
|
|
1143
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Get the underlying better-auth context for low-level operations such as
|
|
1170
|
+
* `internalAdapter.createAccount` / `password.hash`.
|
|
1171
|
+
*
|
|
1172
|
+
* Used by routes that need to write to better-auth's tables outside the
|
|
1173
|
+
* normal endpoint surface — currently only `set-initial-password`, which
|
|
1174
|
+
* provisions a credential account for SSO-onboarded users so they can
|
|
1175
|
+
* sign in with email/password going forward.
|
|
1176
|
+
*/
|
|
1177
|
+
async getAuthContext() {
|
|
1178
|
+
const auth = await this.getOrCreateAuth();
|
|
1179
|
+
return auth.$context;
|
|
1180
|
+
}
|
|
1144
1181
|
// ---------------------------------------------------------------------------
|
|
1145
1182
|
// Device Flow (CLI browser-based login)
|
|
1146
1183
|
//
|
|
@@ -1438,6 +1475,61 @@ var AuthPlugin = class {
|
|
|
1438
1475
|
return c.json({ hasOwner: true });
|
|
1439
1476
|
}
|
|
1440
1477
|
});
|
|
1478
|
+
rawApp.post(`${basePath}/set-initial-password`, async (c) => {
|
|
1479
|
+
try {
|
|
1480
|
+
let body = {};
|
|
1481
|
+
try {
|
|
1482
|
+
body = await c.req.json();
|
|
1483
|
+
} catch {
|
|
1484
|
+
body = {};
|
|
1485
|
+
}
|
|
1486
|
+
const newPassword = body?.newPassword;
|
|
1487
|
+
if (typeof newPassword !== "string" || newPassword.length === 0) {
|
|
1488
|
+
return c.json({ success: false, error: { code: "invalid_request", message: "newPassword is required" } }, 400);
|
|
1489
|
+
}
|
|
1490
|
+
const authApi = await this.authManager.getApi();
|
|
1491
|
+
const session = await authApi.getSession({ headers: c.req.raw.headers });
|
|
1492
|
+
if (!session?.user?.id) {
|
|
1493
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Sign in first" } }, 401);
|
|
1494
|
+
}
|
|
1495
|
+
const userId = session.user.id;
|
|
1496
|
+
const authCtx = await this.authManager.getAuthContext();
|
|
1497
|
+
if (!authCtx?.internalAdapter || !authCtx?.password) {
|
|
1498
|
+
return c.json({ success: false, error: { code: "unavailable", message: "Auth context unavailable" } }, 503);
|
|
1499
|
+
}
|
|
1500
|
+
const minLen = authCtx.password?.config?.minPasswordLength ?? 8;
|
|
1501
|
+
const maxLen = authCtx.password?.config?.maxPasswordLength ?? 128;
|
|
1502
|
+
if (newPassword.length < minLen) {
|
|
1503
|
+
return c.json({ success: false, error: { code: "password_too_short", message: `Password must be at least ${minLen} characters` } }, 400);
|
|
1504
|
+
}
|
|
1505
|
+
if (newPassword.length > maxLen) {
|
|
1506
|
+
return c.json({ success: false, error: { code: "password_too_long", message: `Password must be at most ${maxLen} characters` } }, 400);
|
|
1507
|
+
}
|
|
1508
|
+
const accounts = await authCtx.internalAdapter.findAccounts(userId);
|
|
1509
|
+
const existingCredential = accounts?.find?.((a) => a.providerId === "credential" && a.password);
|
|
1510
|
+
if (existingCredential) {
|
|
1511
|
+
return c.json({
|
|
1512
|
+
success: false,
|
|
1513
|
+
error: {
|
|
1514
|
+
code: "credential_account_exists",
|
|
1515
|
+
message: "A local password is already set for this account. Use change-password instead."
|
|
1516
|
+
}
|
|
1517
|
+
}, 409);
|
|
1518
|
+
}
|
|
1519
|
+
const passwordHash = await authCtx.password.hash(newPassword);
|
|
1520
|
+
await authCtx.internalAdapter.createAccount({
|
|
1521
|
+
userId,
|
|
1522
|
+
providerId: "credential",
|
|
1523
|
+
accountId: userId,
|
|
1524
|
+
password: passwordHash
|
|
1525
|
+
});
|
|
1526
|
+
return c.json({ success: true });
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1529
|
+
ctx.logger.error("[AuthPlugin] set-initial-password failed", err);
|
|
1530
|
+
return c.json({ success: false, error: { code: "internal", message: err.message } }, 500);
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1441
1533
|
rawApp.all(`${basePath}/*`, async (c) => {
|
|
1442
1534
|
try {
|
|
1443
1535
|
const response = await this.authManager.handleRequest(c.req.raw);
|