@lunora/auth 1.0.0-alpha.37 → 1.0.0-alpha.39
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/adapter.mjs +1 -48
- package/dist/audit.mjs +2 -102
- package/dist/email-guard.mjs +1 -71
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -16
- package/dist/middleware.d.mts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.mjs +1 -56
- 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-COcIS_KU.d.mts → create-auth.d-De6IOirt.d.mts} +1 -1
- package/dist/packem_shared/{create-auth.d-COcIS_KU.d.ts → create-auth.d-De6IOirt.d.ts} +1 -1
- 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 +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.mjs +1 -184
- package/dist/store.mjs +1 -183
- package/dist/turnstile-middleware.mjs +1 -34
- package/dist/turnstile.mjs +1 -59
- package/package.json +2 -2
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-CReJPMkx.mjs +0 -513
- package/dist/packem_shared/authAuditHook-3OJKhpQV.mjs +0 -119
- package/dist/packem_shared/compileMigrationsSql-Dl5N8z5q.mjs +0 -29
- package/dist/packem_shared/createAuth-s4i7WhAh.mjs +0 -72
- package/dist/packem_shared/emailGateDatabaseHooks-BGS4uJM9.mjs +0 -64
- package/dist/packem_shared/sessionPresets-Dwwd74_J.mjs +0 -38
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { getAuthTables } from 'better-auth/db';
|
|
3
|
-
|
|
4
|
-
class LunoraAuthAdminError extends LunoraError {
|
|
5
|
-
constructor(message, code) {
|
|
6
|
-
super(code, message, { name: "LunoraAuthAdminError" });
|
|
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 INVITATION_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
15
|
-
const SENSITIVE_FIELDS = /* @__PURE__ */ new Set(["accessToken", "backupCodes", "idToken", "password", "publicKey", "refreshToken", "secret", "token"]);
|
|
16
|
-
const clampLimit = (limit) => Math.min(Math.max(Math.trunc(limit ?? DEFAULT_LIMIT), 1), MAX_LIMIT);
|
|
17
|
-
const clampOffset = (offset) => Math.max(0, Math.trunc(offset ?? 0));
|
|
18
|
-
const normalizeRow = (row) => {
|
|
19
|
-
const out = {};
|
|
20
|
-
for (const [key, value] of Object.entries(row)) {
|
|
21
|
-
if (SENSITIVE_FIELDS.has(key)) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
out[key] = value instanceof Date ? value.getTime() : value;
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
};
|
|
28
|
-
const serializeRole = (role) => Array.isArray(role) ? role.join(",") : role;
|
|
29
|
-
const slugify = (value) => value.toLowerCase().replaceAll(/[^\da-z]+/g, "-").replaceAll(/^-|-$/g, "");
|
|
30
|
-
const CORE_USER_FIELDS = /* @__PURE__ */ new Set(["banExpires", "banned", "banReason", "createdAt", "email", "emailVerified", "id", "name", "role", "updatedAt"]);
|
|
31
|
-
const USER_FIELD_PLUGIN = {
|
|
32
|
-
displayUsername: "username",
|
|
33
|
-
phoneNumber: "phone-number",
|
|
34
|
-
phoneNumberVerified: "phone-number",
|
|
35
|
-
username: "username"
|
|
36
|
-
};
|
|
37
|
-
const mapUserFieldType = (type) => {
|
|
38
|
-
if (type === "boolean") {
|
|
39
|
-
return "boolean";
|
|
40
|
-
}
|
|
41
|
-
if (type === "date") {
|
|
42
|
-
return "date";
|
|
43
|
-
}
|
|
44
|
-
if (type === "number") {
|
|
45
|
-
return "number";
|
|
46
|
-
}
|
|
47
|
-
return "string";
|
|
48
|
-
};
|
|
49
|
-
const buildUserFields = (userFields) => {
|
|
50
|
-
const out = [];
|
|
51
|
-
for (const [name, attribute] of Object.entries(userFields)) {
|
|
52
|
-
if (attribute.input === false || attribute.references !== void 0 || CORE_USER_FIELDS.has(name)) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
out.push({
|
|
56
|
-
name,
|
|
57
|
-
plugin: USER_FIELD_PLUGIN[name],
|
|
58
|
-
required: attribute.required === true,
|
|
59
|
-
type: mapUserFieldType(attribute.type),
|
|
60
|
-
unique: attribute.unique === true
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
};
|
|
65
|
-
const asAdminError = (error) => {
|
|
66
|
-
if (error instanceof LunoraAuthAdminError) {
|
|
67
|
-
return error;
|
|
68
|
-
}
|
|
69
|
-
const candidate = error;
|
|
70
|
-
const code = candidate?.body?.code ?? candidate?.code ?? "AUTH_ADMIN_ERROR";
|
|
71
|
-
const message = candidate?.body?.message ?? candidate?.message ?? "auth admin operation failed";
|
|
72
|
-
return new LunoraAuthAdminError(message, code);
|
|
73
|
-
};
|
|
74
|
-
const createAuthAdmin = (auth, options = {}) => {
|
|
75
|
-
const context = auth.$context;
|
|
76
|
-
const features = options.features ?? {};
|
|
77
|
-
const deriveCapabilities = (authOptions) => {
|
|
78
|
-
const ids = new Set((authOptions.plugins ?? []).map((plugin) => plugin.id));
|
|
79
|
-
const has = (id) => ids.has(id);
|
|
80
|
-
return {
|
|
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
|
-
const withContext = async (function_) => {
|
|
89
|
-
try {
|
|
90
|
-
return await function_(await context);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
throw asAdminError(error);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
const toUser = (row) => normalizeRow(row);
|
|
96
|
-
const page = async (context_, model, options_) => {
|
|
97
|
-
const where = options_.where && options_.where.length > 0 ? options_.where : void 0;
|
|
98
|
-
const [rows, total] = await Promise.all([
|
|
99
|
-
context_.adapter.findMany({
|
|
100
|
-
limit: clampLimit(options_.limit),
|
|
101
|
-
model,
|
|
102
|
-
offset: clampOffset(options_.offset),
|
|
103
|
-
sortBy: options_.sortBy,
|
|
104
|
-
where
|
|
105
|
-
}),
|
|
106
|
-
context_.adapter.count({ model, where })
|
|
107
|
-
]);
|
|
108
|
-
return { rows: rows.map((row) => normalizeRow(row)), total };
|
|
109
|
-
};
|
|
110
|
-
return {
|
|
111
|
-
banUser: ({ expiresInSeconds, reason, userId }) => withContext(async (context_) => {
|
|
112
|
-
let banExpires = null;
|
|
113
|
-
if (expiresInSeconds !== void 0) {
|
|
114
|
-
if (!Number.isInteger(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
115
|
-
throw new LunoraAuthAdminError("expiresInSeconds must be a positive finite integer", "INVALID_BAN_SECONDS");
|
|
116
|
-
}
|
|
117
|
-
const seconds = Math.min(expiresInSeconds, MAX_BAN_SECONDS);
|
|
118
|
-
banExpires = new Date(Date.now() + seconds * 1e3);
|
|
119
|
-
}
|
|
120
|
-
const user = await context_.internalAdapter.updateUser(userId, {
|
|
121
|
-
// `null` (not `undefined`) for a permanent ban so the adapter clears any prior
|
|
122
|
-
// `banExpires` rather than skipping it — otherwise a temp-ban-then-permanent-ban
|
|
123
|
-
// escalation leaves the old expiry and the "permanent" ban silently lapses.
|
|
124
|
-
banExpires,
|
|
125
|
-
banned: true,
|
|
126
|
-
banReason: reason ?? "No reason"
|
|
127
|
-
});
|
|
128
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
129
|
-
return toUser(user);
|
|
130
|
-
}),
|
|
131
|
-
cancelInvitation: ({ invitationId }) => withContext(async (context_) => {
|
|
132
|
-
await context_.adapter.delete({ model: "invitation", where: [{ field: "id", value: invitationId }] });
|
|
133
|
-
}),
|
|
134
|
-
capabilities: () => withContext((context_) => Promise.resolve(deriveCapabilities(context_.options))),
|
|
135
|
-
// ── Directly add an existing user to an org (no invitation/acceptance). ──
|
|
136
|
-
addMember: ({ organizationId, role, userId }) => withContext(async (context_) => {
|
|
137
|
-
const member = await context_.adapter.create({
|
|
138
|
-
data: { createdAt: /* @__PURE__ */ new Date(), organizationId, role: role === void 0 || role === "" ? "member" : role, userId },
|
|
139
|
-
model: "member"
|
|
140
|
-
});
|
|
141
|
-
return normalizeRow(member);
|
|
142
|
-
}),
|
|
143
|
-
addTeamMember: ({ teamId, userId }) => withContext(async (context_) => {
|
|
144
|
-
const teamMember = await context_.adapter.create({
|
|
145
|
-
data: { createdAt: /* @__PURE__ */ new Date(), teamId, userId },
|
|
146
|
-
model: "teamMember"
|
|
147
|
-
});
|
|
148
|
-
return normalizeRow(teamMember);
|
|
149
|
-
}),
|
|
150
|
-
// Rich introspection for the config panel + dynamic create-user form. Reads
|
|
151
|
-
// only from the resolved better-auth options (no DB, no secrets).
|
|
152
|
-
config: () => withContext((context_) => {
|
|
153
|
-
const authOptions = context_.options;
|
|
154
|
-
const capabilities = deriveCapabilities(authOptions);
|
|
155
|
-
const ids = new Set((authOptions.plugins ?? []).map((plugin) => plugin.id));
|
|
156
|
-
const tables = getAuthTables(authOptions);
|
|
157
|
-
const session = authOptions.session ?? {};
|
|
158
|
-
const rateLimit = authOptions.rateLimit ?? {};
|
|
159
|
-
return Promise.resolve({
|
|
160
|
-
capabilities,
|
|
161
|
-
emailAndPassword: authOptions.emailAndPassword?.enabled ?? false,
|
|
162
|
-
organization: {
|
|
163
|
-
enabled: capabilities.organization,
|
|
164
|
-
roles: Boolean(tables["organizationRole"]),
|
|
165
|
-
teams: Boolean(tables["team"])
|
|
166
|
-
},
|
|
167
|
-
plugins: [...ids].toSorted((a, b) => a.localeCompare(b)),
|
|
168
|
-
rateLimit: { enabled: rateLimit.enabled ?? false, max: rateLimit.max, window: rateLimit.window },
|
|
169
|
-
session: {
|
|
170
|
-
cookieCache: session.cookieCache?.enabled,
|
|
171
|
-
expiresIn: session.expiresIn,
|
|
172
|
-
freshAge: session.freshAge,
|
|
173
|
-
updateAge: session.updateAge
|
|
174
|
-
},
|
|
175
|
-
socialProviders: Object.keys(authOptions.socialProviders ?? {}).toSorted((a, b) => a.localeCompare(b)),
|
|
176
|
-
userFields: buildUserFields(tables["user"]?.fields ?? {})
|
|
177
|
-
});
|
|
178
|
-
}),
|
|
179
|
-
createOrganization: ({ logo, metadata, name, ownerId, slug }) => withContext(async (context_) => {
|
|
180
|
-
const finalSlug = slug !== void 0 && slug !== "" ? slugify(slug) : slugify(name);
|
|
181
|
-
if (finalSlug === "") {
|
|
182
|
-
throw new LunoraAuthAdminError("could not derive a slug from the organization name", "ORG_SLUG_INVALID");
|
|
183
|
-
}
|
|
184
|
-
const existing = await context_.adapter.findOne({
|
|
185
|
-
model: "organization",
|
|
186
|
-
where: [{ field: "slug", value: finalSlug }]
|
|
187
|
-
});
|
|
188
|
-
if (existing) {
|
|
189
|
-
throw new LunoraAuthAdminError("an organization with this slug already exists", "ORG_SLUG_TAKEN");
|
|
190
|
-
}
|
|
191
|
-
const organization = await context_.adapter.create({
|
|
192
|
-
data: {
|
|
193
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
194
|
-
logo: logo === void 0 || logo === "" ? void 0 : logo,
|
|
195
|
-
metadata: metadata === void 0 ? void 0 : JSON.stringify(metadata),
|
|
196
|
-
name,
|
|
197
|
-
slug: finalSlug
|
|
198
|
-
},
|
|
199
|
-
model: "organization"
|
|
200
|
-
});
|
|
201
|
-
if (ownerId !== void 0 && ownerId !== "") {
|
|
202
|
-
await context_.adapter.create({
|
|
203
|
-
data: { createdAt: /* @__PURE__ */ new Date(), organizationId: organization.id, role: "owner", userId: ownerId },
|
|
204
|
-
model: "member"
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
return normalizeRow(organization);
|
|
208
|
-
}),
|
|
209
|
-
createOrgRole: ({ organizationId, permission, role }) => withContext(async (context_) => {
|
|
210
|
-
const created = await context_.adapter.create({
|
|
211
|
-
data: { createdAt: /* @__PURE__ */ new Date(), organizationId, permission: JSON.stringify(permission), role },
|
|
212
|
-
model: "organizationRole"
|
|
213
|
-
});
|
|
214
|
-
return normalizeRow(created);
|
|
215
|
-
}),
|
|
216
|
-
createTeam: ({ name, organizationId }) => withContext(async (context_) => {
|
|
217
|
-
const team = await context_.adapter.create({
|
|
218
|
-
data: { createdAt: /* @__PURE__ */ new Date(), name, organizationId },
|
|
219
|
-
model: "team"
|
|
220
|
-
});
|
|
221
|
-
return normalizeRow(team);
|
|
222
|
-
}),
|
|
223
|
-
deleteOrganization: ({ organizationId }) => withContext(async (context_) => {
|
|
224
|
-
const tables = getAuthTables(context_.options);
|
|
225
|
-
await context_.adapter.deleteMany({ model: "member", where: [{ field: "organizationId", value: organizationId }] });
|
|
226
|
-
await context_.adapter.deleteMany({ model: "invitation", where: [{ field: "organizationId", value: organizationId }] });
|
|
227
|
-
if (tables["team"]) {
|
|
228
|
-
const teams = await context_.adapter.findMany({
|
|
229
|
-
model: "team",
|
|
230
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
231
|
-
});
|
|
232
|
-
for (const team of teams) {
|
|
233
|
-
await context_.adapter.deleteMany({ model: "teamMember", where: [{ field: "teamId", value: team.id }] });
|
|
234
|
-
}
|
|
235
|
-
await context_.adapter.deleteMany({ model: "team", where: [{ field: "organizationId", value: organizationId }] });
|
|
236
|
-
}
|
|
237
|
-
if (tables["organizationRole"]) {
|
|
238
|
-
await context_.adapter.deleteMany({ model: "organizationRole", where: [{ field: "organizationId", value: organizationId }] });
|
|
239
|
-
}
|
|
240
|
-
await context_.adapter.delete({ model: "organization", where: [{ field: "id", value: organizationId }] });
|
|
241
|
-
}),
|
|
242
|
-
deleteOrgRole: ({ roleId }) => withContext(async (context_) => {
|
|
243
|
-
await context_.adapter.delete({ model: "organizationRole", where: [{ field: "id", value: roleId }] });
|
|
244
|
-
}),
|
|
245
|
-
// Create a pending email invitation. `inviterId` is DB-required; when the
|
|
246
|
-
// caller omits it, attribute the invite to the org's owner (else any member).
|
|
247
|
-
inviteMember: ({ email, inviterId, organizationId, role }) => withContext(async (context_) => {
|
|
248
|
-
let resolvedInviter = inviterId;
|
|
249
|
-
if (resolvedInviter === void 0 || resolvedInviter === "") {
|
|
250
|
-
const members = await context_.adapter.findMany({
|
|
251
|
-
model: "member",
|
|
252
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
253
|
-
});
|
|
254
|
-
const owner = members.find((member) => typeof member.role === "string" && member.role.includes("owner"));
|
|
255
|
-
resolvedInviter = (owner ?? members[0])?.userId;
|
|
256
|
-
}
|
|
257
|
-
if (resolvedInviter === void 0 || resolvedInviter === "") {
|
|
258
|
-
throw new LunoraAuthAdminError("provide an inviter — the organization has no members to attribute the invitation to", "INVITER_REQUIRED");
|
|
259
|
-
}
|
|
260
|
-
const invitation = await context_.adapter.create({
|
|
261
|
-
data: {
|
|
262
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
263
|
-
email: email.toLowerCase(),
|
|
264
|
-
expiresAt: new Date(Date.now() + INVITATION_TTL_MS),
|
|
265
|
-
inviterId: resolvedInviter,
|
|
266
|
-
organizationId,
|
|
267
|
-
role: role === void 0 || role === "" ? "member" : role,
|
|
268
|
-
status: "pending"
|
|
269
|
-
},
|
|
270
|
-
model: "invitation"
|
|
271
|
-
});
|
|
272
|
-
return normalizeRow(invitation);
|
|
273
|
-
}),
|
|
274
|
-
listOrgRoles: ({ limit, offset, organizationId }) => withContext(
|
|
275
|
-
(context_) => page(context_, "organizationRole", {
|
|
276
|
-
limit,
|
|
277
|
-
offset,
|
|
278
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
279
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
280
|
-
})
|
|
281
|
-
),
|
|
282
|
-
listTeamMembers: ({ limit, offset, teamId }) => withContext(
|
|
283
|
-
(context_) => page(context_, "teamMember", {
|
|
284
|
-
limit,
|
|
285
|
-
offset,
|
|
286
|
-
where: [{ field: "teamId", value: teamId }]
|
|
287
|
-
})
|
|
288
|
-
),
|
|
289
|
-
listTeams: ({ limit, offset, organizationId }) => withContext(
|
|
290
|
-
(context_) => page(context_, "team", {
|
|
291
|
-
limit,
|
|
292
|
-
offset,
|
|
293
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
294
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
295
|
-
})
|
|
296
|
-
),
|
|
297
|
-
removeTeam: ({ teamId }) => withContext(async (context_) => {
|
|
298
|
-
await context_.adapter.deleteMany({ model: "teamMember", where: [{ field: "teamId", value: teamId }] });
|
|
299
|
-
await context_.adapter.delete({ model: "team", where: [{ field: "id", value: teamId }] });
|
|
300
|
-
}),
|
|
301
|
-
removeTeamMember: ({ teamMemberId }) => withContext(async (context_) => {
|
|
302
|
-
await context_.adapter.delete({ model: "teamMember", where: [{ field: "id", value: teamMemberId }] });
|
|
303
|
-
}),
|
|
304
|
-
updateMemberRole: ({ memberId, role }) => withContext(async (context_) => {
|
|
305
|
-
const member = await context_.adapter.update({
|
|
306
|
-
model: "member",
|
|
307
|
-
update: { role: serializeRole(role) },
|
|
308
|
-
where: [{ field: "id", value: memberId }]
|
|
309
|
-
});
|
|
310
|
-
return normalizeRow(member ?? { id: memberId, role: serializeRole(role) });
|
|
311
|
-
}),
|
|
312
|
-
updateOrganization: ({ logo, metadata, name, organizationId, slug }) => withContext(async (context_) => {
|
|
313
|
-
const update = {};
|
|
314
|
-
if (name !== void 0) {
|
|
315
|
-
update["name"] = name;
|
|
316
|
-
}
|
|
317
|
-
if (slug !== void 0 && slug !== "") {
|
|
318
|
-
update["slug"] = slugify(slug);
|
|
319
|
-
}
|
|
320
|
-
if (logo !== void 0) {
|
|
321
|
-
update["logo"] = logo === "" ? void 0 : logo;
|
|
322
|
-
}
|
|
323
|
-
if (metadata !== void 0) {
|
|
324
|
-
update["metadata"] = JSON.stringify(metadata);
|
|
325
|
-
}
|
|
326
|
-
if (Object.keys(update).length === 0) {
|
|
327
|
-
return normalizeRow({ id: organizationId });
|
|
328
|
-
}
|
|
329
|
-
const organization = await context_.adapter.update({
|
|
330
|
-
model: "organization",
|
|
331
|
-
update,
|
|
332
|
-
where: [{ field: "id", value: organizationId }]
|
|
333
|
-
});
|
|
334
|
-
return normalizeRow(organization ?? { id: organizationId });
|
|
335
|
-
}),
|
|
336
|
-
updateOrgRole: ({ permission, roleId }) => withContext(async (context_) => {
|
|
337
|
-
const updated = await context_.adapter.update({
|
|
338
|
-
model: "organizationRole",
|
|
339
|
-
update: { permission: JSON.stringify(permission), updatedAt: /* @__PURE__ */ new Date() },
|
|
340
|
-
where: [{ field: "id", value: roleId }]
|
|
341
|
-
});
|
|
342
|
-
return normalizeRow(updated ?? { id: roleId, permission: JSON.stringify(permission) });
|
|
343
|
-
}),
|
|
344
|
-
updateTeam: ({ name, teamId }) => withContext(async (context_) => {
|
|
345
|
-
const team = await context_.adapter.update({
|
|
346
|
-
model: "team",
|
|
347
|
-
update: { name, updatedAt: /* @__PURE__ */ new Date() },
|
|
348
|
-
where: [{ field: "id", value: teamId }]
|
|
349
|
-
});
|
|
350
|
-
return normalizeRow(team ?? { id: teamId, name });
|
|
351
|
-
}),
|
|
352
|
-
// The one op that genuinely builds a row rather than mutating one. Replicates
|
|
353
|
-
// the plugin's create-user handler over `internalAdapter` (lowercase + dedupe
|
|
354
|
-
// email, create the row, then link a credential account when a password is given).
|
|
355
|
-
createUser: ({ data, email, name, password, role }) => withContext(async (context_) => {
|
|
356
|
-
const normalizedEmail = email.toLowerCase();
|
|
357
|
-
if (await context_.internalAdapter.findUserByEmail(normalizedEmail)) {
|
|
358
|
-
throw new LunoraAuthAdminError("a user with this email already exists", "USER_ALREADY_EXISTS");
|
|
359
|
-
}
|
|
360
|
-
const user = await context_.internalAdapter.createUser({
|
|
361
|
-
email: normalizedEmail,
|
|
362
|
-
name,
|
|
363
|
-
role: role === void 0 ? void 0 : serializeRole(role),
|
|
364
|
-
...data
|
|
365
|
-
});
|
|
366
|
-
if (password !== void 0 && password !== "") {
|
|
367
|
-
const hashed = await context_.password.hash(password);
|
|
368
|
-
await context_.internalAdapter.linkAccount({
|
|
369
|
-
accountId: user.id,
|
|
370
|
-
password: hashed,
|
|
371
|
-
providerId: "credential",
|
|
372
|
-
userId: user.id
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
return toUser(user);
|
|
376
|
-
}),
|
|
377
|
-
deletePasskey: ({ passkeyId }) => withContext(async (context_) => {
|
|
378
|
-
await context_.adapter.delete({ model: "passkey", where: [{ field: "id", value: passkeyId }] });
|
|
379
|
-
}),
|
|
380
|
-
disableTwoFactor: ({ userId }) => withContext(async (context_) => {
|
|
381
|
-
await context_.adapter.deleteMany({ model: "twoFactor", where: [{ field: "userId", value: userId }] });
|
|
382
|
-
await context_.internalAdapter.updateUser(userId, { twoFactorEnabled: false });
|
|
383
|
-
}),
|
|
384
|
-
impersonateUser: ({ userId }) => withContext(async (context_) => {
|
|
385
|
-
const user = await context_.internalAdapter.findUserById(userId);
|
|
386
|
-
if (!user) {
|
|
387
|
-
throw new LunoraAuthAdminError("user not found", "USER_NOT_FOUND");
|
|
388
|
-
}
|
|
389
|
-
const rawSeconds = options.impersonationSeconds;
|
|
390
|
-
let ttlSeconds = DEFAULT_IMPERSONATION_SECONDS;
|
|
391
|
-
if (rawSeconds !== void 0) {
|
|
392
|
-
if (!Number.isInteger(rawSeconds) || !Number.isFinite(rawSeconds) || rawSeconds <= 0) {
|
|
393
|
-
throw new LunoraAuthAdminError("impersonationSeconds must be a positive finite integer", "INVALID_IMPERSONATION_SECONDS");
|
|
394
|
-
}
|
|
395
|
-
ttlSeconds = Math.min(rawSeconds, MAX_IMPERSONATION_SECONDS);
|
|
396
|
-
}
|
|
397
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
398
|
-
const session = await context_.internalAdapter.createSession(
|
|
399
|
-
userId,
|
|
400
|
-
true,
|
|
401
|
-
{ expiresAt, impersonatedBy: options.impersonatedBy ?? userId },
|
|
402
|
-
true
|
|
403
|
-
);
|
|
404
|
-
return {
|
|
405
|
-
expiresAt: session.expiresAt instanceof Date ? session.expiresAt.getTime() : expiresAt.getTime(),
|
|
406
|
-
token: session.token,
|
|
407
|
-
user: toUser(user)
|
|
408
|
-
};
|
|
409
|
-
}),
|
|
410
|
-
listAccounts: ({ userId }) => withContext(async (context_) => {
|
|
411
|
-
const rows = await context_.adapter.findMany({ model: "account", where: [{ field: "userId", value: userId }] });
|
|
412
|
-
return rows.map((row) => normalizeRow(row));
|
|
413
|
-
}),
|
|
414
|
-
listInvitations: ({ limit, offset, organizationId }) => withContext(
|
|
415
|
-
(context_) => page(context_, "invitation", {
|
|
416
|
-
limit,
|
|
417
|
-
offset,
|
|
418
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
419
|
-
})
|
|
420
|
-
),
|
|
421
|
-
listMembers: ({ limit, offset, organizationId }) => withContext(
|
|
422
|
-
(context_) => page(context_, "member", {
|
|
423
|
-
limit,
|
|
424
|
-
offset,
|
|
425
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
426
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
427
|
-
})
|
|
428
|
-
),
|
|
429
|
-
listOrganizations: ({ limit, offset }) => withContext((context_) => page(context_, "organization", { limit, offset, sortBy: { direction: "desc", field: "createdAt" } })),
|
|
430
|
-
listPasskeys: ({ userId }) => withContext(async (context_) => {
|
|
431
|
-
const rows = await context_.adapter.findMany({ model: "passkey", where: [{ field: "userId", value: userId }] });
|
|
432
|
-
return rows.map((row) => normalizeRow(row));
|
|
433
|
-
}),
|
|
434
|
-
listSessions: ({ limit, offset, userId }) => withContext(
|
|
435
|
-
(context_) => page(context_, "session", {
|
|
436
|
-
limit,
|
|
437
|
-
offset,
|
|
438
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
439
|
-
where: userId === void 0 || userId === "" ? void 0 : [{ field: "userId", value: userId }]
|
|
440
|
-
})
|
|
441
|
-
),
|
|
442
|
-
listUsers: ({ filterField, filterValue, limit, offset, search, searchField, sortBy, sortDirection }) => withContext((context_) => {
|
|
443
|
-
const where = [];
|
|
444
|
-
if (search !== void 0 && search !== "") {
|
|
445
|
-
where.push({ field: searchField ?? "email", operator: "contains", value: search });
|
|
446
|
-
}
|
|
447
|
-
if (filterValue !== void 0) {
|
|
448
|
-
where.push({ field: filterField ?? "email", operator: "eq", value: filterValue });
|
|
449
|
-
}
|
|
450
|
-
return page(context_, "user", {
|
|
451
|
-
limit,
|
|
452
|
-
offset,
|
|
453
|
-
sortBy: { direction: sortDirection ?? "desc", field: sortBy ?? "createdAt" },
|
|
454
|
-
where
|
|
455
|
-
});
|
|
456
|
-
}),
|
|
457
|
-
removeMember: ({ memberId }) => withContext(async (context_) => {
|
|
458
|
-
await context_.adapter.delete({ model: "member", where: [{ field: "id", value: memberId }] });
|
|
459
|
-
}),
|
|
460
|
-
removeUser: ({ userId }) => withContext(async (context_) => {
|
|
461
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
462
|
-
await context_.internalAdapter.deleteUser(userId);
|
|
463
|
-
}),
|
|
464
|
-
// Keyed on the session *id*, not its token: tokens are bearer credentials we
|
|
465
|
-
// deliberately never surface to the studio. Resolve the row to recover its
|
|
466
|
-
// token, then delete via `internalAdapter.deleteSession` — which also clears
|
|
467
|
-
// secondary (KV) storage, unlike a raw `adapter.delete` on the DB row.
|
|
468
|
-
revokeUserSession: ({ sessionId }) => withContext(async (context_) => {
|
|
469
|
-
const session = await context_.adapter.findOne({ model: "session", where: [{ field: "id", value: sessionId }] });
|
|
470
|
-
if (session?.token) {
|
|
471
|
-
await context_.internalAdapter.deleteSession(session.token);
|
|
472
|
-
}
|
|
473
|
-
}),
|
|
474
|
-
revokeUserSessions: ({ userId }) => withContext(async (context_) => {
|
|
475
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
476
|
-
}),
|
|
477
|
-
setRole: ({ role, userId }) => withContext(async (context_) => {
|
|
478
|
-
const user = await context_.internalAdapter.updateUser(userId, { role: serializeRole(role) });
|
|
479
|
-
return toUser(user);
|
|
480
|
-
}),
|
|
481
|
-
setUserPassword: ({ newPassword, userId }) => withContext(async (context_) => {
|
|
482
|
-
const min = context_.password.config.minPasswordLength;
|
|
483
|
-
const max = context_.password.config.maxPasswordLength;
|
|
484
|
-
if (newPassword.length < min) {
|
|
485
|
-
throw new LunoraAuthAdminError(`password must be at least ${min.toString()} characters`, "PASSWORD_TOO_SHORT");
|
|
486
|
-
}
|
|
487
|
-
if (newPassword.length > max) {
|
|
488
|
-
throw new LunoraAuthAdminError(`password must be at most ${max.toString()} characters`, "PASSWORD_TOO_LONG");
|
|
489
|
-
}
|
|
490
|
-
const hashed = await context_.password.hash(newPassword);
|
|
491
|
-
await context_.internalAdapter.updatePassword(userId, hashed);
|
|
492
|
-
}),
|
|
493
|
-
unbanUser: ({ userId }) => withContext(async (context_) => {
|
|
494
|
-
const user = await context_.internalAdapter.updateUser(userId, { banExpires: null, banned: false, banReason: null });
|
|
495
|
-
return toUser(user);
|
|
496
|
-
}),
|
|
497
|
-
unlinkAccount: ({ accountId, userId }) => withContext(async (context_) => {
|
|
498
|
-
await context_.adapter.delete({
|
|
499
|
-
model: "account",
|
|
500
|
-
where: [
|
|
501
|
-
{ field: "id", value: accountId },
|
|
502
|
-
{ connector: "AND", field: "userId", value: userId }
|
|
503
|
-
]
|
|
504
|
-
});
|
|
505
|
-
}),
|
|
506
|
-
updateUser: ({ data, userId }) => withContext(async (context_) => {
|
|
507
|
-
const user = await context_.internalAdapter.updateUser(userId, data);
|
|
508
|
-
return toUser(user);
|
|
509
|
-
})
|
|
510
|
-
};
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
export { LunoraAuthAdminError, createAuthAdmin };
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { createAuthMiddleware } from 'better-auth/api';
|
|
2
|
-
import { appendAuthAuditEntry } from '../audit.mjs';
|
|
3
|
-
|
|
4
|
-
const eventForPath = (path) => {
|
|
5
|
-
const normalized = path.toLowerCase();
|
|
6
|
-
const ends = (suffix) => normalized === suffix || normalized.endsWith(suffix);
|
|
7
|
-
if (ends("/sign-up/email") || ends("/sign-up")) {
|
|
8
|
-
return "sign-up";
|
|
9
|
-
}
|
|
10
|
-
if (normalized.includes("/sign-in/")) {
|
|
11
|
-
return "sign-in";
|
|
12
|
-
}
|
|
13
|
-
if (ends("/sign-out")) {
|
|
14
|
-
return "sign-out";
|
|
15
|
-
}
|
|
16
|
-
if (ends("/change-password") || ends("/set-password")) {
|
|
17
|
-
return "password-change";
|
|
18
|
-
}
|
|
19
|
-
if (ends("/reset-password") || ends("/request-password-reset") || ends("/forget-password")) {
|
|
20
|
-
return "password-reset";
|
|
21
|
-
}
|
|
22
|
-
if (ends("/verify-email")) {
|
|
23
|
-
return "email-verification";
|
|
24
|
-
}
|
|
25
|
-
if (normalized.includes("/two-factor/enable") || normalized.includes("/totp/enable")) {
|
|
26
|
-
return "mfa-enable";
|
|
27
|
-
}
|
|
28
|
-
if (normalized.includes("/two-factor/disable") || normalized.includes("/totp/disable")) {
|
|
29
|
-
return "mfa-disable";
|
|
30
|
-
}
|
|
31
|
-
if (ends("/refresh-token") || ends("/token")) {
|
|
32
|
-
return "token-refresh";
|
|
33
|
-
}
|
|
34
|
-
if (ends("/revoke-session") || ends("/revoke-sessions") || ends("/revoke-other-sessions")) {
|
|
35
|
-
return "session-revoke";
|
|
36
|
-
}
|
|
37
|
-
if (ends("/link-social")) {
|
|
38
|
-
return "account-link";
|
|
39
|
-
}
|
|
40
|
-
if (ends("/unlink-account")) {
|
|
41
|
-
return "account-unlink";
|
|
42
|
-
}
|
|
43
|
-
return void 0;
|
|
44
|
-
};
|
|
45
|
-
const header = (context, name) => {
|
|
46
|
-
const value = context.headers?.get(name) ?? context.request?.headers.get(name);
|
|
47
|
-
return value ?? void 0;
|
|
48
|
-
};
|
|
49
|
-
const resolveIp = (context) => {
|
|
50
|
-
const forwarded = header(context, "x-forwarded-for");
|
|
51
|
-
return header(context, "cf-connecting-ip") ?? (forwarded === void 0 ? void 0 : forwarded.split(",")[0]?.trim()) ?? header(context, "x-real-ip");
|
|
52
|
-
};
|
|
53
|
-
const resolveActor = (context) => {
|
|
54
|
-
const source = context.context?.newSession ?? context.context?.session;
|
|
55
|
-
const actorId = source?.user?.id ?? source?.session?.userId;
|
|
56
|
-
const actorEmail = source?.user?.email;
|
|
57
|
-
return {
|
|
58
|
-
...actorId === void 0 ? {} : { actorId },
|
|
59
|
-
...actorEmail === void 0 ? {} : { actorEmail }
|
|
60
|
-
};
|
|
61
|
-
};
|
|
62
|
-
const resolveOutcome = (context) => {
|
|
63
|
-
const returned = context.context?.returned;
|
|
64
|
-
if (returned instanceof Error) {
|
|
65
|
-
return "failure";
|
|
66
|
-
}
|
|
67
|
-
if (typeof returned === "object" && returned !== null && "status" in returned) {
|
|
68
|
-
const status = Number(returned.status);
|
|
69
|
-
if (Number.isFinite(status) && status >= 400) {
|
|
70
|
-
return "failure";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return "success";
|
|
74
|
-
};
|
|
75
|
-
const buildAuditEntry = (context, now = Date.now()) => {
|
|
76
|
-
const event = context.path === void 0 ? void 0 : eventForPath(context.path);
|
|
77
|
-
if (event === void 0) {
|
|
78
|
-
return void 0;
|
|
79
|
-
}
|
|
80
|
-
const ip = resolveIp(context);
|
|
81
|
-
const userAgent = header(context, "user-agent");
|
|
82
|
-
return {
|
|
83
|
-
...resolveActor(context),
|
|
84
|
-
event,
|
|
85
|
-
outcome: resolveOutcome(context),
|
|
86
|
-
ts: now,
|
|
87
|
-
...ip === void 0 ? {} : { ip },
|
|
88
|
-
...userAgent === void 0 ? {} : { userAgent },
|
|
89
|
-
detail: { path: context.path }
|
|
90
|
-
};
|
|
91
|
-
};
|
|
92
|
-
const authAuditHook = (config) => createAuthMiddleware(async (context) => {
|
|
93
|
-
try {
|
|
94
|
-
const entry = buildAuditEntry(context);
|
|
95
|
-
if (entry !== void 0) {
|
|
96
|
-
const persisted = await appendAuthAuditEntry(config.executor, entry, {
|
|
97
|
-
redactDetail: config.redactDetail,
|
|
98
|
-
retention: config.retention
|
|
99
|
-
});
|
|
100
|
-
if (config.onRecord !== void 0) {
|
|
101
|
-
await config.onRecord(persisted);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error("@lunora/auth: audit hook failed to record event", error);
|
|
106
|
-
}
|
|
107
|
-
return {};
|
|
108
|
-
});
|
|
109
|
-
const withAuthAudit = (options, config) => {
|
|
110
|
-
const audit = authAuditHook(config);
|
|
111
|
-
const existing = options.hooks?.after;
|
|
112
|
-
const after = existing ? async (context) => {
|
|
113
|
-
await existing(context);
|
|
114
|
-
return audit(context);
|
|
115
|
-
} : audit;
|
|
116
|
-
return { ...options, hooks: { ...options.hooks, after } };
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export { authAuditHook, buildAuditEntry, eventForPath, withAuthAudit };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { getMigrations } from 'better-auth/db/migration';
|
|
2
|
-
import { resolveAuthOptions } from './createAuth-s4i7WhAh.mjs';
|
|
3
|
-
|
|
4
|
-
const migrating = /* @__PURE__ */ new WeakMap();
|
|
5
|
-
const ensureMigrated = async (auth) => {
|
|
6
|
-
const { options } = auth;
|
|
7
|
-
const inFlight = migrating.get(options);
|
|
8
|
-
if (inFlight) {
|
|
9
|
-
await inFlight;
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
const run = (async () => {
|
|
13
|
-
const { runMigrations } = await getMigrations(options);
|
|
14
|
-
await runMigrations();
|
|
15
|
-
})();
|
|
16
|
-
migrating.set(options, run);
|
|
17
|
-
try {
|
|
18
|
-
await run;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
migrating.delete(options);
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
const compileMigrationsSql = async (options) => {
|
|
25
|
-
const { compileMigrations } = await getMigrations(resolveAuthOptions(options));
|
|
26
|
-
return compileMigrations();
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export { compileMigrationsSql, ensureMigrated };
|