@lunora/auth 1.0.0-alpha.4 → 1.0.0-alpha.41
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/LICENSE.md +6 -0
- package/README.md +55 -0
- package/dist/adapter.d.mts +35 -35
- package/dist/adapter.d.ts +35 -35
- package/dist/adapter.mjs +1 -47
- package/dist/audit.d.mts +114 -0
- package/dist/audit.d.ts +114 -0
- package/dist/audit.mjs +11 -0
- package/dist/email-guard.d.mts +122 -0
- package/dist/email-guard.d.ts +122 -0
- package/dist/email-guard.mjs +1 -0
- package/dist/index.d.mts +460 -146
- package/dist/index.d.ts +460 -146
- package/dist/index.mjs +1 -12
- package/dist/middleware.d.mts +156 -155
- package/dist/middleware.d.ts +156 -155
- package/dist/middleware.mjs +1 -53
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs +1 -0
- package/dist/packem_shared/LunoraAuthAdminError-BiNYZM9j.mjs +1 -0
- package/dist/packem_shared/authAuditHook-Dx3sqf3G.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-ChiudSmt.mjs +1 -0
- package/dist/packem_shared/create-auth.d-De6IOirt.d.mts +128 -0
- package/dist/packem_shared/create-auth.d-De6IOirt.d.ts +128 -0
- package/dist/packem_shared/createAuth-DS6PL8Mb.mjs +1 -0
- package/dist/packem_shared/emailGateDatabaseHooks-DzBD1Qoq.mjs +1 -0
- package/dist/packem_shared/sessionPresets-DpEFjXKV.mjs +1 -0
- package/dist/plugins-client.mjs +1 -2
- package/dist/plugins.mjs +1 -22
- package/dist/schema.d.mts +39 -39
- package/dist/schema.d.ts +39 -39
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.d.mts +28 -28
- package/dist/sql-store.d.ts +28 -28
- package/dist/sql-store.mjs +1 -162
- package/dist/store.d.mts +49 -31
- package/dist/store.d.ts +49 -31
- package/dist/store.mjs +1 -170
- package/dist/turnstile-middleware.d.mts +55 -55
- package/dist/turnstile-middleware.d.ts +55 -55
- package/dist/turnstile-middleware.mjs +1 -45
- package/dist/turnstile.d.mts +42 -59
- package/dist/turnstile.d.ts +42 -59
- package/dist/turnstile.mjs +1 -61
- package/package.json +19 -6
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs +0 -249
- package/dist/packem_shared/compileMigrationsSql-wZH3oXDu.mjs +0 -28
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.mts +0 -58
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.ts +0 -58
- package/dist/packem_shared/createAuth-B-tvsvQU.mjs +0 -56
- package/dist/packem_shared/sessionPresets-B95rXrd8.mjs +0 -35
package/dist/turnstile.mjs
CHANGED
|
@@ -1,61 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const asString = (value) => typeof value === "string" ? value : void 0;
|
|
3
|
-
const asStringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
4
|
-
const verifyTurnstile = async ({
|
|
5
|
-
expectedAction,
|
|
6
|
-
expectedHostname,
|
|
7
|
-
fetch = globalThis.fetch,
|
|
8
|
-
remoteip,
|
|
9
|
-
secret,
|
|
10
|
-
token
|
|
11
|
-
}) => {
|
|
12
|
-
const body = new URLSearchParams({ response: token, secret });
|
|
13
|
-
if (remoteip !== void 0 && remoteip !== "") {
|
|
14
|
-
body.set("remoteip", remoteip);
|
|
15
|
-
}
|
|
16
|
-
let response;
|
|
17
|
-
try {
|
|
18
|
-
response = await fetch(TURNSTILE_VERIFY_ENDPOINT, {
|
|
19
|
-
body: body.toString(),
|
|
20
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
21
|
-
method: "POST"
|
|
22
|
-
});
|
|
23
|
-
} catch (error) {
|
|
24
|
-
throw Object.assign(new Error("turnstile siteverify request failed"), {
|
|
25
|
-
cause: error,
|
|
26
|
-
code: "SERVICE_UNAVAILABLE",
|
|
27
|
-
name: "LunoraError",
|
|
28
|
-
status: 503
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw Object.assign(new Error(`turnstile siteverify returned ${String(response.status)}`), {
|
|
33
|
-
code: "SERVICE_UNAVAILABLE",
|
|
34
|
-
name: "LunoraError",
|
|
35
|
-
status: 503
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
const raw = await response.json();
|
|
39
|
-
const action = asString(raw.action);
|
|
40
|
-
const hostname = asString(raw.hostname);
|
|
41
|
-
const errorCodes = asStringArray(raw["error-codes"]);
|
|
42
|
-
let success = raw.success === true;
|
|
43
|
-
if (success && expectedHostname !== void 0 && hostname !== expectedHostname) {
|
|
44
|
-
success = false;
|
|
45
|
-
errorCodes.push("hostname-mismatch");
|
|
46
|
-
}
|
|
47
|
-
if (success && expectedAction !== void 0 && action !== expectedAction) {
|
|
48
|
-
success = false;
|
|
49
|
-
errorCodes.push("action-mismatch");
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
action,
|
|
53
|
-
cdata: asString(raw.cdata),
|
|
54
|
-
challengeTs: asString(raw.challenge_ts),
|
|
55
|
-
errorCodes,
|
|
56
|
-
hostname,
|
|
57
|
-
success
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export { TURNSTILE_VERIFY_ENDPOINT, verifyTurnstile };
|
|
1
|
+
import{LunoraError as l}from"@lunora/errors";const y="https://challenges.cloudflare.com/turnstile/v0/siteverify",o=t=>typeof t=="string"?t:void 0,w=t=>Array.isArray(t)?t.filter(r=>typeof r=="string"):[],A=async({expectedAction:t,expectedHostname:r,fetch:f=globalThis.fetch,remoteip:n,secret:m,token:p})=>{const h=new URLSearchParams({response:p,secret:m});n!==void 0&&n!==""&&h.set("remoteip",n);let a;try{a=await f(y,{body:h.toString(),headers:{"content-type":"application/x-www-form-urlencoded"},method:"POST"})}catch(c){throw new l("SERVICE_UNAVAILABLE","turnstile siteverify request failed",{cause:c,status:503})}if(!a.ok)throw new l("SERVICE_UNAVAILABLE",`turnstile siteverify returned ${String(a.status)}`,{status:503});let e;try{e=await a.json()}catch(c){throw new l("SERVICE_UNAVAILABLE","turnstile siteverify returned a non-JSON body",{cause:c,status:503})}const u=o(e.action),d=o(e.hostname),i=w(e["error-codes"]);let s=e.success===!0;return s&&r!==void 0&&d!==r&&(s=!1,i.push("hostname-mismatch")),s&&t!==void 0&&u!==t&&(s=!1,i.push("action-mismatch")),{action:u,cdata:o(e.cdata),challengeTs:o(e.challenge_ts),errorCodes:i,hostname:d,success:s}};export{y as TURNSTILE_VERIFY_ENDPOINT,A as verifyTurnstile};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/auth",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.41",
|
|
4
4
|
"description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/auth"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist",
|
|
28
|
+
"./dist",
|
|
29
29
|
"__assets__",
|
|
30
30
|
"README.md",
|
|
31
31
|
"LICENSE.md"
|
|
@@ -52,6 +52,14 @@
|
|
|
52
52
|
"types": "./dist/middleware.d.ts",
|
|
53
53
|
"import": "./dist/middleware.mjs"
|
|
54
54
|
},
|
|
55
|
+
"./audit": {
|
|
56
|
+
"types": "./dist/audit.d.ts",
|
|
57
|
+
"import": "./dist/audit.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./email-guard": {
|
|
60
|
+
"types": "./dist/email-guard.d.ts",
|
|
61
|
+
"import": "./dist/email-guard.mjs"
|
|
62
|
+
},
|
|
55
63
|
"./turnstile": {
|
|
56
64
|
"types": "./dist/turnstile.d.ts",
|
|
57
65
|
"import": "./dist/turnstile.mjs"
|
|
@@ -82,10 +90,15 @@
|
|
|
82
90
|
"access": "public"
|
|
83
91
|
},
|
|
84
92
|
"dependencies": {
|
|
85
|
-
"@better-auth/passkey": "
|
|
86
|
-
"@lunora/
|
|
87
|
-
"@lunora/
|
|
88
|
-
"
|
|
93
|
+
"@better-auth/passkey": "1.6.23",
|
|
94
|
+
"@lunora/errors": "1.0.0-alpha.8",
|
|
95
|
+
"@lunora/server": "1.0.0-alpha.35",
|
|
96
|
+
"@lunora/values": "1.0.0-alpha.11",
|
|
97
|
+
"@visulima/disposable-email-domains": "1.0.1",
|
|
98
|
+
"@visulima/email-verifier": "1.0.1",
|
|
99
|
+
"@visulima/free-email-domains": "1.0.0",
|
|
100
|
+
"@visulima/redact": "3.0.0",
|
|
101
|
+
"better-auth": "1.6.23"
|
|
89
102
|
},
|
|
90
103
|
"engines": {
|
|
91
104
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const DEFAULT_AUTH_BASE_PATH = "/api/auth";
|
|
2
|
-
const handleAuthRequest = async (auth, request, basePath = DEFAULT_AUTH_BASE_PATH) => {
|
|
3
|
-
const url = new URL(request.url);
|
|
4
|
-
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
5
|
-
if (url.pathname !== base && !url.pathname.startsWith(`${base}/`)) {
|
|
6
|
-
return void 0;
|
|
7
|
-
}
|
|
8
|
-
return auth.handler(request);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export { DEFAULT_AUTH_BASE_PATH, handleAuthRequest };
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
class LunoraAuthAdminError extends Error {
|
|
2
|
-
code;
|
|
3
|
-
constructor(message, code) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.name = "LunoraAuthAdminError";
|
|
6
|
-
this.code = code;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
const DEFAULT_LIMIT = 50;
|
|
10
|
-
const MAX_LIMIT = 500;
|
|
11
|
-
const DEFAULT_IMPERSONATION_SECONDS = 3600;
|
|
12
|
-
const MAX_IMPERSONATION_SECONDS = DEFAULT_IMPERSONATION_SECONDS * 24;
|
|
13
|
-
const MAX_BAN_SECONDS = 100 * 365 * 24 * 60 * 60;
|
|
14
|
-
const SENSITIVE_FIELDS = /* @__PURE__ */ new Set(["accessToken", "backupCodes", "idToken", "password", "publicKey", "refreshToken", "secret", "token"]);
|
|
15
|
-
const clampLimit = (limit) => Math.min(Math.max(Math.trunc(limit ?? DEFAULT_LIMIT), 1), MAX_LIMIT);
|
|
16
|
-
const clampOffset = (offset) => Math.max(0, Math.trunc(offset ?? 0));
|
|
17
|
-
const normalizeRow = (row) => {
|
|
18
|
-
const out = {};
|
|
19
|
-
for (const [key, value] of Object.entries(row)) {
|
|
20
|
-
if (SENSITIVE_FIELDS.has(key)) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
out[key] = value instanceof Date ? value.getTime() : value;
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
};
|
|
27
|
-
const serializeRole = (role) => Array.isArray(role) ? role.join(",") : role;
|
|
28
|
-
const asAdminError = (error) => {
|
|
29
|
-
if (error instanceof LunoraAuthAdminError) {
|
|
30
|
-
return error;
|
|
31
|
-
}
|
|
32
|
-
const candidate = error;
|
|
33
|
-
const code = candidate?.body?.code ?? candidate?.code ?? "AUTH_ADMIN_ERROR";
|
|
34
|
-
const message = candidate?.body?.message ?? candidate?.message ?? "auth admin operation failed";
|
|
35
|
-
return new LunoraAuthAdminError(message, code);
|
|
36
|
-
};
|
|
37
|
-
const createAuthAdmin = (auth, options = {}) => {
|
|
38
|
-
const context = auth.$context;
|
|
39
|
-
const features = options.features ?? {};
|
|
40
|
-
const withContext = async (function_) => {
|
|
41
|
-
try {
|
|
42
|
-
return await function_(await context);
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw asAdminError(error);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const toUser = (row) => normalizeRow(row);
|
|
48
|
-
const page = async (context_, model, options_) => {
|
|
49
|
-
const where = options_.where && options_.where.length > 0 ? options_.where : void 0;
|
|
50
|
-
const [rows, total] = await Promise.all([
|
|
51
|
-
context_.adapter.findMany({
|
|
52
|
-
limit: clampLimit(options_.limit),
|
|
53
|
-
model,
|
|
54
|
-
offset: clampOffset(options_.offset),
|
|
55
|
-
sortBy: options_.sortBy,
|
|
56
|
-
where
|
|
57
|
-
}),
|
|
58
|
-
context_.adapter.count({ model, where })
|
|
59
|
-
]);
|
|
60
|
-
return { rows: rows.map((row) => normalizeRow(row)), total };
|
|
61
|
-
};
|
|
62
|
-
return {
|
|
63
|
-
banUser: ({ expiresInSeconds, reason, userId }) => withContext(async (context_) => {
|
|
64
|
-
const seconds = typeof expiresInSeconds === "number" && Number.isFinite(expiresInSeconds) ? Math.min(Math.trunc(expiresInSeconds), MAX_BAN_SECONDS) : 0;
|
|
65
|
-
const banExpires = seconds > 0 ? new Date(Date.now() + seconds * 1e3) : void 0;
|
|
66
|
-
const user = await context_.internalAdapter.updateUser(userId, {
|
|
67
|
-
banExpires,
|
|
68
|
-
banned: true,
|
|
69
|
-
banReason: reason ?? "No reason"
|
|
70
|
-
});
|
|
71
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
72
|
-
return toUser(user);
|
|
73
|
-
}),
|
|
74
|
-
cancelInvitation: ({ invitationId }) => withContext(async (context_) => {
|
|
75
|
-
await context_.adapter.delete({ model: "invitation", where: [{ field: "id", value: invitationId }] });
|
|
76
|
-
}),
|
|
77
|
-
capabilities: () => withContext((context_) => {
|
|
78
|
-
const ids = new Set((context_.options.plugins ?? []).map((plugin) => plugin.id));
|
|
79
|
-
const has = (id) => ids.has(id);
|
|
80
|
-
return Promise.resolve({
|
|
81
|
-
accounts: features.accounts ?? true,
|
|
82
|
-
admin: features.admin ?? has("admin"),
|
|
83
|
-
organization: features.organization ?? has("organization"),
|
|
84
|
-
passkey: features.passkey ?? has("passkey"),
|
|
85
|
-
twoFactor: features.twoFactor ?? has("two-factor")
|
|
86
|
-
});
|
|
87
|
-
}),
|
|
88
|
-
// The one op that genuinely builds a row rather than mutating one. Replicates
|
|
89
|
-
// the plugin's create-user handler over `internalAdapter` (lowercase + dedupe
|
|
90
|
-
// email, create the row, then link a credential account when a password is given).
|
|
91
|
-
createUser: ({ data, email, name, password, role }) => withContext(async (context_) => {
|
|
92
|
-
const normalizedEmail = email.toLowerCase();
|
|
93
|
-
if (await context_.internalAdapter.findUserByEmail(normalizedEmail)) {
|
|
94
|
-
throw new LunoraAuthAdminError("a user with this email already exists", "USER_ALREADY_EXISTS");
|
|
95
|
-
}
|
|
96
|
-
const user = await context_.internalAdapter.createUser({
|
|
97
|
-
email: normalizedEmail,
|
|
98
|
-
name,
|
|
99
|
-
role: role === void 0 ? void 0 : serializeRole(role),
|
|
100
|
-
...data
|
|
101
|
-
});
|
|
102
|
-
if (password !== void 0 && password !== "") {
|
|
103
|
-
const hashed = await context_.password.hash(password);
|
|
104
|
-
await context_.internalAdapter.linkAccount({
|
|
105
|
-
accountId: user.id,
|
|
106
|
-
password: hashed,
|
|
107
|
-
providerId: "credential",
|
|
108
|
-
userId: user.id
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return toUser(user);
|
|
112
|
-
}),
|
|
113
|
-
deletePasskey: ({ passkeyId }) => withContext(async (context_) => {
|
|
114
|
-
await context_.adapter.delete({ model: "passkey", where: [{ field: "id", value: passkeyId }] });
|
|
115
|
-
}),
|
|
116
|
-
disableTwoFactor: ({ userId }) => withContext(async (context_) => {
|
|
117
|
-
await context_.adapter.deleteMany({ model: "twoFactor", where: [{ field: "userId", value: userId }] });
|
|
118
|
-
await context_.internalAdapter.updateUser(userId, { twoFactorEnabled: false });
|
|
119
|
-
}),
|
|
120
|
-
impersonateUser: ({ userId }) => withContext(async (context_) => {
|
|
121
|
-
const user = await context_.internalAdapter.findUserById(userId);
|
|
122
|
-
if (!user) {
|
|
123
|
-
throw new LunoraAuthAdminError("user not found", "USER_NOT_FOUND");
|
|
124
|
-
}
|
|
125
|
-
const rawSeconds = options.impersonationSeconds;
|
|
126
|
-
let ttlSeconds = DEFAULT_IMPERSONATION_SECONDS;
|
|
127
|
-
if (rawSeconds !== void 0) {
|
|
128
|
-
if (!Number.isInteger(rawSeconds) || !Number.isFinite(rawSeconds) || rawSeconds <= 0) {
|
|
129
|
-
throw new LunoraAuthAdminError("impersonationSeconds must be a positive finite integer", "INVALID_IMPERSONATION_SECONDS");
|
|
130
|
-
}
|
|
131
|
-
ttlSeconds = Math.min(rawSeconds, MAX_IMPERSONATION_SECONDS);
|
|
132
|
-
}
|
|
133
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
134
|
-
const session = await context_.internalAdapter.createSession(
|
|
135
|
-
userId,
|
|
136
|
-
true,
|
|
137
|
-
{ expiresAt, impersonatedBy: options.impersonatedBy ?? userId },
|
|
138
|
-
true
|
|
139
|
-
);
|
|
140
|
-
return {
|
|
141
|
-
expiresAt: session.expiresAt instanceof Date ? session.expiresAt.getTime() : expiresAt.getTime(),
|
|
142
|
-
token: session.token,
|
|
143
|
-
user: toUser(user)
|
|
144
|
-
};
|
|
145
|
-
}),
|
|
146
|
-
listAccounts: ({ userId }) => withContext(async (context_) => {
|
|
147
|
-
const rows = await context_.adapter.findMany({ model: "account", where: [{ field: "userId", value: userId }] });
|
|
148
|
-
return rows.map((row) => normalizeRow(row));
|
|
149
|
-
}),
|
|
150
|
-
listInvitations: ({ limit, offset, organizationId }) => withContext(
|
|
151
|
-
(context_) => page(context_, "invitation", {
|
|
152
|
-
limit,
|
|
153
|
-
offset,
|
|
154
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
155
|
-
})
|
|
156
|
-
),
|
|
157
|
-
listMembers: ({ limit, offset, organizationId }) => withContext(
|
|
158
|
-
(context_) => page(context_, "member", {
|
|
159
|
-
limit,
|
|
160
|
-
offset,
|
|
161
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
162
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
163
|
-
})
|
|
164
|
-
),
|
|
165
|
-
listOrganizations: ({ limit, offset }) => withContext((context_) => page(context_, "organization", { limit, offset, sortBy: { direction: "desc", field: "createdAt" } })),
|
|
166
|
-
listPasskeys: ({ userId }) => withContext(async (context_) => {
|
|
167
|
-
const rows = await context_.adapter.findMany({ model: "passkey", where: [{ field: "userId", value: userId }] });
|
|
168
|
-
return rows.map((row) => normalizeRow(row));
|
|
169
|
-
}),
|
|
170
|
-
listSessions: ({ limit, offset, userId }) => withContext(
|
|
171
|
-
(context_) => page(context_, "session", {
|
|
172
|
-
limit,
|
|
173
|
-
offset,
|
|
174
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
175
|
-
where: userId === void 0 || userId === "" ? void 0 : [{ field: "userId", value: userId }]
|
|
176
|
-
})
|
|
177
|
-
),
|
|
178
|
-
listUsers: ({ filterField, filterValue, limit, offset, search, searchField, sortBy, sortDirection }) => withContext((context_) => {
|
|
179
|
-
const where = [];
|
|
180
|
-
if (search !== void 0 && search !== "") {
|
|
181
|
-
where.push({ field: searchField ?? "email", operator: "contains", value: search });
|
|
182
|
-
}
|
|
183
|
-
if (filterValue !== void 0) {
|
|
184
|
-
where.push({ field: filterField ?? "email", operator: "eq", value: filterValue });
|
|
185
|
-
}
|
|
186
|
-
return page(context_, "user", {
|
|
187
|
-
limit,
|
|
188
|
-
offset,
|
|
189
|
-
sortBy: { direction: sortDirection ?? "desc", field: sortBy ?? "createdAt" },
|
|
190
|
-
where
|
|
191
|
-
});
|
|
192
|
-
}),
|
|
193
|
-
removeMember: ({ memberId }) => withContext(async (context_) => {
|
|
194
|
-
await context_.adapter.delete({ model: "member", where: [{ field: "id", value: memberId }] });
|
|
195
|
-
}),
|
|
196
|
-
removeUser: ({ userId }) => withContext(async (context_) => {
|
|
197
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
198
|
-
await context_.internalAdapter.deleteUser(userId);
|
|
199
|
-
}),
|
|
200
|
-
// Keyed on the session *id*, not its token: tokens are bearer credentials we
|
|
201
|
-
// deliberately never surface to the studio. Resolve the row to recover its
|
|
202
|
-
// token, then delete via `internalAdapter.deleteSession` — which also clears
|
|
203
|
-
// secondary (KV) storage, unlike a raw `adapter.delete` on the DB row.
|
|
204
|
-
revokeUserSession: ({ sessionId }) => withContext(async (context_) => {
|
|
205
|
-
const session = await context_.adapter.findOne({ model: "session", where: [{ field: "id", value: sessionId }] });
|
|
206
|
-
if (session?.token) {
|
|
207
|
-
await context_.internalAdapter.deleteSession(session.token);
|
|
208
|
-
}
|
|
209
|
-
}),
|
|
210
|
-
revokeUserSessions: ({ userId }) => withContext(async (context_) => {
|
|
211
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
212
|
-
}),
|
|
213
|
-
setRole: ({ role, userId }) => withContext(async (context_) => {
|
|
214
|
-
const user = await context_.internalAdapter.updateUser(userId, { role: serializeRole(role) });
|
|
215
|
-
return toUser(user);
|
|
216
|
-
}),
|
|
217
|
-
setUserPassword: ({ newPassword, userId }) => withContext(async (context_) => {
|
|
218
|
-
const min = context_.password.config.minPasswordLength;
|
|
219
|
-
const max = context_.password.config.maxPasswordLength;
|
|
220
|
-
if (newPassword.length < min) {
|
|
221
|
-
throw new LunoraAuthAdminError(`password must be at least ${min.toString()} characters`, "PASSWORD_TOO_SHORT");
|
|
222
|
-
}
|
|
223
|
-
if (newPassword.length > max) {
|
|
224
|
-
throw new LunoraAuthAdminError(`password must be at most ${max.toString()} characters`, "PASSWORD_TOO_LONG");
|
|
225
|
-
}
|
|
226
|
-
const hashed = await context_.password.hash(newPassword);
|
|
227
|
-
await context_.internalAdapter.updatePassword(userId, hashed);
|
|
228
|
-
}),
|
|
229
|
-
unbanUser: ({ userId }) => withContext(async (context_) => {
|
|
230
|
-
const user = await context_.internalAdapter.updateUser(userId, { banExpires: null, banned: false, banReason: null });
|
|
231
|
-
return toUser(user);
|
|
232
|
-
}),
|
|
233
|
-
unlinkAccount: ({ accountId, userId }) => withContext(async (context_) => {
|
|
234
|
-
await context_.adapter.delete({
|
|
235
|
-
model: "account",
|
|
236
|
-
where: [
|
|
237
|
-
{ field: "id", value: accountId },
|
|
238
|
-
{ connector: "AND", field: "userId", value: userId }
|
|
239
|
-
]
|
|
240
|
-
});
|
|
241
|
-
}),
|
|
242
|
-
updateUser: ({ data, userId }) => withContext(async (context_) => {
|
|
243
|
-
const user = await context_.internalAdapter.updateUser(userId, data);
|
|
244
|
-
return toUser(user);
|
|
245
|
-
})
|
|
246
|
-
};
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
export { LunoraAuthAdminError, createAuthAdmin };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { getMigrations } from 'better-auth/db/migration';
|
|
2
|
-
|
|
3
|
-
const migrating = /* @__PURE__ */ new WeakMap();
|
|
4
|
-
const ensureMigrated = async (auth) => {
|
|
5
|
-
const { options } = auth;
|
|
6
|
-
const inFlight = migrating.get(options);
|
|
7
|
-
if (inFlight) {
|
|
8
|
-
await inFlight;
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
const run = (async () => {
|
|
12
|
-
const { runMigrations } = await getMigrations(options);
|
|
13
|
-
await runMigrations();
|
|
14
|
-
})();
|
|
15
|
-
migrating.set(options, run);
|
|
16
|
-
try {
|
|
17
|
-
await run;
|
|
18
|
-
} catch (error) {
|
|
19
|
-
migrating.delete(options);
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const compileMigrationsSql = async (options) => {
|
|
24
|
-
const { compileMigrations } = await getMigrations(options);
|
|
25
|
-
return compileMigrations();
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export { compileMigrationsSql, ensureMigrated };
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { betterAuth, BetterAuthOptions } from 'better-auth';
|
|
2
|
-
/**
|
|
3
|
-
* Lunora's options pass straight through to better-auth — the only thing we add
|
|
4
|
-
* is requiring `secret` up front so a misconfigured deployment fails loudly
|
|
5
|
-
* instead of at the first sign-in.
|
|
6
|
-
*
|
|
7
|
-
* For `database`, prefer `lunoraD1Adapter` (`database: lunoraD1Adapter(env.DB)`)
|
|
8
|
-
* over passing the raw `env.DB`. better-auth *does* accept a D1Database directly,
|
|
9
|
-
* but it then resolves its Kysely adapter via a runtime `await import(...)` inside
|
|
10
|
-
* `auth.$context` — and that import never settles under `@cloudflare/vite-plugin`'s
|
|
11
|
-
* worker runner, hanging every auth request in `pnpm dev`. The explicit adapter
|
|
12
|
-
* skips it, so dev and prod behave the same. (Raw `env.DB` is still correct for
|
|
13
|
-
* the migration-only instance — see `lunoraD1Adapter`'s note.)
|
|
14
|
-
*
|
|
15
|
-
* Session rotation / richer session policies are configured via the `session`
|
|
16
|
-
* field (a `SessionPolicy`); Lunora validates it for obviously-broken
|
|
17
|
-
* durations and forwards it verbatim to better-auth. See `sessionPresets`
|
|
18
|
-
* for ready-made rotation/expiry trade-offs.
|
|
19
|
-
*
|
|
20
|
-
* ## Serverless background tasks (Cloudflare Workers)
|
|
21
|
-
*
|
|
22
|
-
* better-auth runs some work *after* sending the response — most importantly the
|
|
23
|
-
* password-reset email, whose background send is what keeps reset responses
|
|
24
|
-
* constant-time (a timing-attack defence: the response doesn't reveal whether
|
|
25
|
-
* the account exists). On Cloudflare Workers a promise that isn't handed to
|
|
26
|
-
* `ctx.waitUntil` can be cancelled the moment the response returns, dropping
|
|
27
|
-
* that send and weakening the guarantee. Wire your request's `ctx.waitUntil`
|
|
28
|
-
* into better-auth's background handler so the work survives:
|
|
29
|
-
*
|
|
30
|
-
* ```ts
|
|
31
|
-
* // in your worker fetch handler, where `ctx: ExecutionContext` is in scope
|
|
32
|
-
* const auth = createAuth({
|
|
33
|
-
* secret: env.AUTH_SECRET,
|
|
34
|
-
* database: lunoraD1Adapter(env.DB),
|
|
35
|
-
* advanced: {
|
|
36
|
-
* backgroundTasks: { handler: (promise) => ctx.waitUntil(promise) },
|
|
37
|
-
* },
|
|
38
|
-
* });
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* (Lunora can't set this for you — `ctx.waitUntil` is per-request, but
|
|
42
|
-
* `createAuth` runs once at worker setup.)
|
|
43
|
-
*/
|
|
44
|
-
type LunoraAuthOptions = BetterAuthOptions;
|
|
45
|
-
/**
|
|
46
|
-
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
47
|
-
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
48
|
-
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
49
|
-
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
50
|
-
*/
|
|
51
|
-
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
52
|
-
/**
|
|
53
|
-
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
54
|
-
* the `secret` requirement at construction time so misconfigured deployments
|
|
55
|
-
* fail loudly at the first fetch rather than the first sign-in attempt.
|
|
56
|
-
*/
|
|
57
|
-
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
58
|
-
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c };
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { betterAuth, BetterAuthOptions } from 'better-auth';
|
|
2
|
-
/**
|
|
3
|
-
* Lunora's options pass straight through to better-auth — the only thing we add
|
|
4
|
-
* is requiring `secret` up front so a misconfigured deployment fails loudly
|
|
5
|
-
* instead of at the first sign-in.
|
|
6
|
-
*
|
|
7
|
-
* For `database`, prefer `lunoraD1Adapter` (`database: lunoraD1Adapter(env.DB)`)
|
|
8
|
-
* over passing the raw `env.DB`. better-auth *does* accept a D1Database directly,
|
|
9
|
-
* but it then resolves its Kysely adapter via a runtime `await import(...)` inside
|
|
10
|
-
* `auth.$context` — and that import never settles under `@cloudflare/vite-plugin`'s
|
|
11
|
-
* worker runner, hanging every auth request in `pnpm dev`. The explicit adapter
|
|
12
|
-
* skips it, so dev and prod behave the same. (Raw `env.DB` is still correct for
|
|
13
|
-
* the migration-only instance — see `lunoraD1Adapter`'s note.)
|
|
14
|
-
*
|
|
15
|
-
* Session rotation / richer session policies are configured via the `session`
|
|
16
|
-
* field (a `SessionPolicy`); Lunora validates it for obviously-broken
|
|
17
|
-
* durations and forwards it verbatim to better-auth. See `sessionPresets`
|
|
18
|
-
* for ready-made rotation/expiry trade-offs.
|
|
19
|
-
*
|
|
20
|
-
* ## Serverless background tasks (Cloudflare Workers)
|
|
21
|
-
*
|
|
22
|
-
* better-auth runs some work *after* sending the response — most importantly the
|
|
23
|
-
* password-reset email, whose background send is what keeps reset responses
|
|
24
|
-
* constant-time (a timing-attack defence: the response doesn't reveal whether
|
|
25
|
-
* the account exists). On Cloudflare Workers a promise that isn't handed to
|
|
26
|
-
* `ctx.waitUntil` can be cancelled the moment the response returns, dropping
|
|
27
|
-
* that send and weakening the guarantee. Wire your request's `ctx.waitUntil`
|
|
28
|
-
* into better-auth's background handler so the work survives:
|
|
29
|
-
*
|
|
30
|
-
* ```ts
|
|
31
|
-
* // in your worker fetch handler, where `ctx: ExecutionContext` is in scope
|
|
32
|
-
* const auth = createAuth({
|
|
33
|
-
* secret: env.AUTH_SECRET,
|
|
34
|
-
* database: lunoraD1Adapter(env.DB),
|
|
35
|
-
* advanced: {
|
|
36
|
-
* backgroundTasks: { handler: (promise) => ctx.waitUntil(promise) },
|
|
37
|
-
* },
|
|
38
|
-
* });
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* (Lunora can't set this for you — `ctx.waitUntil` is per-request, but
|
|
42
|
-
* `createAuth` runs once at worker setup.)
|
|
43
|
-
*/
|
|
44
|
-
type LunoraAuthOptions = BetterAuthOptions;
|
|
45
|
-
/**
|
|
46
|
-
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
47
|
-
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
48
|
-
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
49
|
-
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
50
|
-
*/
|
|
51
|
-
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
52
|
-
/**
|
|
53
|
-
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
54
|
-
* the `secret` requirement at construction time so misconfigured deployments
|
|
55
|
-
* fail loudly at the first fetch rather than the first sign-in attempt.
|
|
56
|
-
*/
|
|
57
|
-
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
58
|
-
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { betterAuth } from 'better-auth';
|
|
2
|
-
import { validateSessionPolicy } from './sessionPresets-B95rXrd8.mjs';
|
|
3
|
-
|
|
4
|
-
const MIN_SECRET_LENGTH = 32;
|
|
5
|
-
const isWeakSecret = (secret) => {
|
|
6
|
-
const trimmedLength = typeof secret === "string" ? secret.trim().length : 0;
|
|
7
|
-
return trimmedLength > 0 && trimmedLength < MIN_SECRET_LENGTH;
|
|
8
|
-
};
|
|
9
|
-
const isHttpsBaseUrl = (baseURL) => {
|
|
10
|
-
if (typeof baseURL === "string") {
|
|
11
|
-
return baseURL.startsWith("https://");
|
|
12
|
-
}
|
|
13
|
-
if (baseURL && typeof baseURL === "object") {
|
|
14
|
-
if (baseURL.protocol === "https") {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
if (baseURL.protocol === "http") {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
return typeof baseURL.fallback === "string" && baseURL.fallback.startsWith("https://");
|
|
21
|
-
}
|
|
22
|
-
return false;
|
|
23
|
-
};
|
|
24
|
-
const hardenAuthOptions = (options) => {
|
|
25
|
-
if (isWeakSecret(options.secret)) {
|
|
26
|
-
const message = `@lunora/auth: AUTH_SECRET is only ${String(options.secret?.trim().length)} characters. Use at least ${String(MIN_SECRET_LENGTH)} for a brute-force-resistant secret — generate one with \`openssl rand -hex 32\`.`;
|
|
27
|
-
if (isHttpsBaseUrl(options.baseURL)) {
|
|
28
|
-
throw new Error(message);
|
|
29
|
-
}
|
|
30
|
-
console.warn(message);
|
|
31
|
-
}
|
|
32
|
-
const advanced = options.advanced ?? {};
|
|
33
|
-
return {
|
|
34
|
-
...options,
|
|
35
|
-
advanced: {
|
|
36
|
-
...advanced,
|
|
37
|
-
defaultCookieAttributes: advanced.defaultCookieAttributes ?? { httpOnly: true, path: "/", sameSite: "lax" },
|
|
38
|
-
...advanced.useSecureCookies === void 0 && isHttpsBaseUrl(options.baseURL) ? { useSecureCookies: true } : {}
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
const createAuth = (options) => {
|
|
43
|
-
if (!options.secret || options.secret.trim() === "") {
|
|
44
|
-
throw new Error(
|
|
45
|
-
'@lunora/auth: `secret` is required. Set AUTH_SECRET locally in .dev.vars (`lunora env set AUTH_SECRET "$(openssl rand -hex 32)"`), and in production with `wrangler secret put AUTH_SECRET`.'
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
if (options.session) {
|
|
49
|
-
validateSessionPolicy(options.session);
|
|
50
|
-
}
|
|
51
|
-
const hardened = hardenAuthOptions(options);
|
|
52
|
-
const resolvedOptions = hardened.rateLimit?.enabled === void 0 ? { ...hardened, rateLimit: { ...hardened.rateLimit, enabled: true } } : hardened;
|
|
53
|
-
return betterAuth(resolvedOptions);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export { createAuth };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
const MINUTE = 60;
|
|
2
|
-
const HOUR = 60 * MINUTE;
|
|
3
|
-
const DAY = 24 * HOUR;
|
|
4
|
-
const validateSessionPolicy = (policy) => {
|
|
5
|
-
const durationFields = ["expiresIn", "updateAge", "freshAge"];
|
|
6
|
-
for (const field of durationFields) {
|
|
7
|
-
const value = policy[field];
|
|
8
|
-
if (value === void 0) {
|
|
9
|
-
continue;
|
|
10
|
-
}
|
|
11
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
12
|
-
throw new TypeError(`@lunora/auth: \`session.${field}\` must be a non-negative, finite number of seconds`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return policy;
|
|
16
|
-
};
|
|
17
|
-
const sessionPresets = {
|
|
18
|
-
longLived: {
|
|
19
|
-
expiresIn: 30 * DAY,
|
|
20
|
-
freshAge: DAY,
|
|
21
|
-
updateAge: DAY
|
|
22
|
-
},
|
|
23
|
-
rolling: {
|
|
24
|
-
expiresIn: 7 * DAY,
|
|
25
|
-
freshAge: DAY,
|
|
26
|
-
updateAge: DAY
|
|
27
|
-
},
|
|
28
|
-
strict: {
|
|
29
|
-
expiresIn: HOUR,
|
|
30
|
-
freshAge: 5 * MINUTE,
|
|
31
|
-
updateAge: 15 * MINUTE
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export { sessionPresets, validateSessionPolicy };
|