@lastshotlabs/bunshot 0.0.21 → 0.0.27
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 +3035 -1249
- package/dist/adapters/localStorage.d.ts +6 -0
- package/dist/adapters/localStorage.js +59 -0
- package/dist/adapters/memoryAuth.d.ts +13 -0
- package/dist/adapters/memoryAuth.js +261 -2
- package/dist/adapters/memoryStorage.d.ts +3 -0
- package/dist/adapters/memoryStorage.js +44 -0
- package/dist/adapters/mongoAuth.js +217 -1
- package/dist/adapters/s3Storage.d.ts +14 -0
- package/dist/adapters/s3Storage.js +126 -0
- package/dist/adapters/sqliteAuth.d.ts +30 -0
- package/dist/adapters/sqliteAuth.js +352 -2
- package/dist/app.d.ts +203 -3
- package/dist/app.js +352 -48
- package/dist/cli.js +118 -38
- package/dist/index.d.ts +69 -8
- package/dist/index.js +46 -5
- package/dist/lib/HttpError.d.ts +7 -1
- package/dist/lib/HttpError.js +10 -1
- package/dist/lib/appConfig.d.ts +157 -0
- package/dist/lib/appConfig.js +54 -0
- package/dist/lib/auditLog.d.ts +58 -0
- package/dist/lib/auditLog.js +218 -0
- package/dist/lib/authAdapter.d.ts +140 -1
- package/dist/lib/authRateLimit.js +36 -0
- package/dist/lib/breachedPassword.d.ts +13 -0
- package/dist/lib/breachedPassword.js +48 -0
- package/dist/lib/captcha.d.ts +25 -0
- package/dist/lib/captcha.js +37 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +24 -1
- package/dist/lib/context.js +17 -3
- package/dist/lib/createRoute.d.ts +28 -2
- package/dist/lib/createRoute.js +54 -3
- package/dist/lib/credentialStuffing.d.ts +31 -0
- package/dist/lib/credentialStuffing.js +77 -0
- package/dist/lib/deletionCancelToken.d.ts +12 -0
- package/dist/lib/deletionCancelToken.js +88 -0
- package/dist/lib/emailVerification.d.ts +6 -0
- package/dist/lib/emailVerification.js +46 -3
- package/dist/lib/groups.d.ts +113 -0
- package/dist/lib/groups.js +133 -0
- package/dist/lib/idempotency.d.ts +22 -0
- package/dist/lib/idempotency.js +182 -0
- package/dist/lib/jwks.d.ts +25 -0
- package/dist/lib/jwks.js +51 -0
- package/dist/lib/jwt.d.ts +15 -2
- package/dist/lib/jwt.js +92 -5
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +6 -0
- package/dist/lib/m2m.d.ts +29 -0
- package/dist/lib/m2m.js +48 -0
- package/dist/lib/metrics.d.ts +14 -0
- package/dist/lib/metrics.js +158 -0
- package/dist/lib/mfaChallenge.d.ts +14 -1
- package/dist/lib/mfaChallenge.js +111 -6
- package/dist/lib/mongo.js +1 -1
- package/dist/lib/oauthCode.js +23 -18
- package/dist/lib/pagination.d.ts +119 -0
- package/dist/lib/pagination.js +166 -0
- package/dist/lib/resetPassword.js +3 -1
- package/dist/lib/saml.d.ts +25 -0
- package/dist/lib/saml.js +64 -0
- package/dist/lib/scim.d.ts +44 -0
- package/dist/lib/scim.js +54 -0
- package/dist/lib/securityEvents.d.ts +28 -0
- package/dist/lib/securityEvents.js +26 -0
- package/dist/lib/session.d.ts +14 -0
- package/dist/lib/session.js +121 -5
- package/dist/lib/signing.d.ts +52 -0
- package/dist/lib/signing.js +183 -0
- package/dist/lib/storageAdapter.d.ts +30 -0
- package/dist/lib/storageAdapter.js +1 -0
- package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
- package/dist/lib/stripUnreferencedSchemas.js +79 -0
- package/dist/lib/suspension.d.ts +13 -0
- package/dist/lib/suspension.js +23 -0
- package/dist/lib/tenant.js +2 -2
- package/dist/lib/upload.d.ts +39 -0
- package/dist/lib/upload.js +112 -0
- package/dist/lib/uploadRegistry.d.ts +18 -0
- package/dist/lib/uploadRegistry.js +83 -0
- package/dist/lib/validate.js +2 -2
- package/dist/lib/ws.d.ts +1 -0
- package/dist/lib/ws.js +28 -0
- package/dist/lib/wsHeartbeat.d.ts +12 -0
- package/dist/lib/wsHeartbeat.js +57 -0
- package/dist/lib/wsMessages.d.ts +40 -0
- package/dist/lib/wsMessages.js +330 -0
- package/dist/lib/wsPresence.d.ts +25 -0
- package/dist/lib/wsPresence.js +99 -0
- package/dist/middleware/auditLog.d.ts +22 -0
- package/dist/middleware/auditLog.js +39 -0
- package/dist/middleware/bearerAuth.js +1 -1
- package/dist/middleware/cacheResponse.js +5 -1
- package/dist/middleware/captcha.d.ts +10 -0
- package/dist/middleware/captcha.js +36 -0
- package/dist/middleware/csrf.js +18 -4
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/identify.js +89 -14
- package/dist/middleware/metrics.d.ts +9 -0
- package/dist/middleware/metrics.js +26 -0
- package/dist/middleware/requestId.d.ts +3 -0
- package/dist/middleware/requestId.js +7 -0
- package/dist/middleware/requestLogger.d.ts +38 -0
- package/dist/middleware/requestLogger.js +68 -0
- package/dist/middleware/requestSigning.d.ts +20 -0
- package/dist/middleware/requestSigning.js +100 -0
- package/dist/middleware/requireMfaSetup.d.ts +16 -0
- package/dist/middleware/requireMfaSetup.js +37 -0
- package/dist/middleware/requireRole.d.ts +9 -3
- package/dist/middleware/requireRole.js +23 -36
- package/dist/middleware/requireScope.d.ts +10 -0
- package/dist/middleware/requireScope.js +25 -0
- package/dist/middleware/requireStepUp.d.ts +18 -0
- package/dist/middleware/requireStepUp.js +29 -0
- package/dist/middleware/scimAuth.d.ts +8 -0
- package/dist/middleware/scimAuth.js +29 -0
- package/dist/middleware/upload.d.ts +5 -0
- package/dist/middleware/upload.js +27 -0
- package/dist/middleware/webhookAuth.d.ts +30 -0
- package/dist/middleware/webhookAuth.js +58 -0
- package/dist/models/AuditLog.d.ts +30 -0
- package/dist/models/AuditLog.js +39 -0
- package/dist/models/AuthUser.d.ts +7 -0
- package/dist/models/AuthUser.js +7 -0
- package/dist/models/Group.d.ts +21 -0
- package/dist/models/Group.js +28 -0
- package/dist/models/GroupMembership.d.ts +21 -0
- package/dist/models/GroupMembership.js +25 -0
- package/dist/models/M2MClient.d.ts +18 -0
- package/dist/models/M2MClient.js +18 -0
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +238 -21
- package/dist/routes/groups.d.ts +21 -0
- package/dist/routes/groups.js +346 -0
- package/dist/routes/jobs.js +66 -46
- package/dist/routes/m2m.d.ts +2 -0
- package/dist/routes/m2m.js +72 -0
- package/dist/routes/metrics.d.ts +8 -0
- package/dist/routes/metrics.js +55 -0
- package/dist/routes/mfa.js +13 -1
- package/dist/routes/oauth.js +6 -0
- package/dist/routes/oidc.d.ts +2 -0
- package/dist/routes/oidc.js +29 -0
- package/dist/routes/passkey.d.ts +1 -0
- package/dist/routes/passkey.js +157 -0
- package/dist/routes/saml.d.ts +2 -0
- package/dist/routes/saml.js +86 -0
- package/dist/routes/scim.d.ts +2 -0
- package/dist/routes/scim.js +255 -0
- package/dist/routes/uploads.d.ts +14 -0
- package/dist/routes/uploads.js +227 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +46 -3
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +101 -22
- package/dist/services/mfa.js +2 -2
- package/dist/ws/index.js +5 -1
- package/docs/sections/auth-flow/full.md +203 -47
- package/docs/sections/auth-flow/overview.md +2 -2
- package/docs/sections/auth-security-examples/full.md +388 -0
- package/docs/sections/authentication/full.md +130 -0
- package/docs/sections/authentication/overview.md +5 -0
- package/docs/sections/cli/full.md +13 -1
- package/docs/sections/configuration/full.md +17 -0
- package/docs/sections/configuration/overview.md +1 -0
- package/docs/sections/exports/full.md +34 -3
- package/docs/sections/logging/full.md +83 -0
- package/docs/sections/metrics/full.md +131 -0
- package/docs/sections/oauth/full.md +189 -189
- package/docs/sections/oauth/overview.md +1 -1
- package/docs/sections/pagination/full.md +93 -0
- package/docs/sections/passkey-login/full.md +90 -0
- package/docs/sections/passkey-login/overview.md +1 -0
- package/docs/sections/roles/full.md +224 -135
- package/docs/sections/roles/overview.md +3 -1
- package/docs/sections/signing/full.md +203 -0
- package/docs/sections/uploads/full.md +208 -0
- package/docs/sections/versioning/full.md +85 -0
- package/docs/sections/webhook-auth/full.md +100 -0
- package/docs/sections/websocket/full.md +95 -0
- package/docs/sections/websocket-rooms/full.md +6 -1
- package/package.json +18 -5
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { AuthUser } from "../models/AuthUser";
|
|
2
2
|
import { TenantRole } from "../models/TenantRole";
|
|
3
|
+
import { Group } from "../models/Group";
|
|
4
|
+
import { GroupMembership } from "../models/GroupMembership";
|
|
5
|
+
import { M2MClient } from "../models/M2MClient";
|
|
3
6
|
import { HttpError } from "../lib/HttpError";
|
|
4
7
|
export const mongoAuthAdapter = {
|
|
5
8
|
async findByEmail(email) {
|
|
@@ -62,13 +65,19 @@ export const mongoAuthAdapter = {
|
|
|
62
65
|
await AuthUser.findByIdAndUpdate(userId, { $pull: { roles: role } });
|
|
63
66
|
},
|
|
64
67
|
async getUser(userId) {
|
|
65
|
-
const user = await AuthUser.findById(userId, "email providerIds emailVerified").lean();
|
|
68
|
+
const user = await AuthUser.findById(userId, "email providerIds emailVerified displayName firstName lastName externalId suspended suspendedReason").lean();
|
|
66
69
|
if (!user)
|
|
67
70
|
return null;
|
|
68
71
|
return {
|
|
69
72
|
email: user.email,
|
|
70
73
|
providerIds: user.providerIds,
|
|
71
74
|
emailVerified: user.emailVerified ?? false,
|
|
75
|
+
displayName: user.displayName ?? undefined,
|
|
76
|
+
firstName: user.firstName ?? undefined,
|
|
77
|
+
lastName: user.lastName ?? undefined,
|
|
78
|
+
externalId: user.externalId ?? undefined,
|
|
79
|
+
suspended: user.suspended ?? false,
|
|
80
|
+
suspendedReason: user.suspendedReason ?? undefined,
|
|
72
81
|
};
|
|
73
82
|
},
|
|
74
83
|
async unlinkProvider(userId, provider) {
|
|
@@ -184,4 +193,211 @@ export const mongoAuthAdapter = {
|
|
|
184
193
|
async removeTenantRole(userId, tenantId, role) {
|
|
185
194
|
await TenantRole.findOneAndUpdate({ userId, tenantId }, { $pull: { roles: role } });
|
|
186
195
|
},
|
|
196
|
+
async setSuspended(userId, suspended, reason) {
|
|
197
|
+
const update = { suspended };
|
|
198
|
+
if (suspended) {
|
|
199
|
+
update.suspendedAt = new Date();
|
|
200
|
+
update.suspendedReason = reason ?? null;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
update.suspendedAt = null;
|
|
204
|
+
update.suspendedReason = null;
|
|
205
|
+
}
|
|
206
|
+
await AuthUser.updateOne({ _id: userId }, { $set: update });
|
|
207
|
+
},
|
|
208
|
+
async getSuspended(userId) {
|
|
209
|
+
const user = await AuthUser.findById(userId, { suspended: 1, suspendedReason: 1 }).lean();
|
|
210
|
+
if (!user)
|
|
211
|
+
return null;
|
|
212
|
+
return { suspended: user.suspended ?? false, suspendedReason: user.suspendedReason ?? undefined };
|
|
213
|
+
},
|
|
214
|
+
async updateProfile(userId, fields) {
|
|
215
|
+
await AuthUser.updateOne({ _id: userId }, { $set: fields });
|
|
216
|
+
},
|
|
217
|
+
async listUsers(query) {
|
|
218
|
+
const filter = {};
|
|
219
|
+
if (query.email !== undefined)
|
|
220
|
+
filter.email = query.email;
|
|
221
|
+
if (query.externalId !== undefined)
|
|
222
|
+
filter.externalId = query.externalId;
|
|
223
|
+
if (query.suspended !== undefined)
|
|
224
|
+
filter.suspended = query.suspended;
|
|
225
|
+
const startIndex = query.startIndex ?? 0;
|
|
226
|
+
const count = query.count ?? 100;
|
|
227
|
+
const [users, totalResults] = await Promise.all([
|
|
228
|
+
AuthUser.find(filter, {
|
|
229
|
+
_id: 1, email: 1, displayName: 1, firstName: 1, lastName: 1,
|
|
230
|
+
externalId: 1, suspended: 1, suspendedAt: 1, suspendedReason: 1,
|
|
231
|
+
emailVerified: 1, providerIds: 1,
|
|
232
|
+
}).skip(startIndex).limit(count).lean(),
|
|
233
|
+
AuthUser.countDocuments(filter),
|
|
234
|
+
]);
|
|
235
|
+
return {
|
|
236
|
+
users: users.map((u) => ({
|
|
237
|
+
id: String(u._id),
|
|
238
|
+
email: u.email ?? undefined,
|
|
239
|
+
displayName: u.displayName ?? undefined,
|
|
240
|
+
firstName: u.firstName ?? undefined,
|
|
241
|
+
lastName: u.lastName ?? undefined,
|
|
242
|
+
externalId: u.externalId ?? undefined,
|
|
243
|
+
suspended: u.suspended ?? false,
|
|
244
|
+
suspendedAt: u.suspendedAt ?? undefined,
|
|
245
|
+
suspendedReason: u.suspendedReason ?? undefined,
|
|
246
|
+
emailVerified: u.emailVerified ?? undefined,
|
|
247
|
+
providerIds: u.providerIds ?? undefined,
|
|
248
|
+
})),
|
|
249
|
+
totalResults,
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Groups
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
async createGroup(group) {
|
|
256
|
+
try {
|
|
257
|
+
const doc = await Group.create(group);
|
|
258
|
+
return { id: String(doc._id) };
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
if (err?.code === 11000)
|
|
262
|
+
throw new HttpError(409, "A group with this name already exists in this scope");
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
async deleteGroup(groupId) {
|
|
267
|
+
await Group.findByIdAndDelete(groupId);
|
|
268
|
+
// Explicit cascade (Mongoose middleware hooks are intentionally avoided for visibility)
|
|
269
|
+
await GroupMembership.deleteMany({ groupId });
|
|
270
|
+
},
|
|
271
|
+
async getGroup(groupId) {
|
|
272
|
+
const doc = await Group.findById(groupId).lean();
|
|
273
|
+
if (!doc)
|
|
274
|
+
return null;
|
|
275
|
+
return mongoGroupToRecord(doc);
|
|
276
|
+
},
|
|
277
|
+
async listGroups(tenantId, opts) {
|
|
278
|
+
const limit = Math.min(opts?.limit ?? 50, 200);
|
|
279
|
+
const offset = opts?.offset ?? 0;
|
|
280
|
+
const filter = { tenantId: tenantId ?? null };
|
|
281
|
+
const [docs, total] = await Promise.all([
|
|
282
|
+
Group.find(filter).skip(offset).limit(limit).lean(),
|
|
283
|
+
Group.countDocuments(filter),
|
|
284
|
+
]);
|
|
285
|
+
return { items: docs.map(mongoGroupToRecord), total, limit, offset };
|
|
286
|
+
},
|
|
287
|
+
async updateGroup(groupId, updates) {
|
|
288
|
+
await Group.findByIdAndUpdate(groupId, { $set: updates });
|
|
289
|
+
},
|
|
290
|
+
async addGroupMember(groupId, userId, roles = []) {
|
|
291
|
+
// Look up group to get tenantId for denormalization
|
|
292
|
+
const group = await Group.findById(groupId, "tenantId").lean();
|
|
293
|
+
if (!group)
|
|
294
|
+
throw new HttpError(404, "Group not found");
|
|
295
|
+
try {
|
|
296
|
+
await GroupMembership.create({ groupId, userId, roles, tenantId: group.tenantId ?? null });
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (err?.code === 11000)
|
|
300
|
+
throw new HttpError(409, "User is already a member of this group");
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async updateGroupMembership(groupId, userId, roles) {
|
|
305
|
+
await GroupMembership.findOneAndUpdate({ groupId, userId }, { $set: { roles } });
|
|
306
|
+
},
|
|
307
|
+
async removeGroupMember(groupId, userId) {
|
|
308
|
+
await GroupMembership.deleteOne({ groupId, userId });
|
|
309
|
+
},
|
|
310
|
+
async getGroupMembers(groupId, opts) {
|
|
311
|
+
const limit = Math.min(opts?.limit ?? 50, 200);
|
|
312
|
+
const offset = opts?.offset ?? 0;
|
|
313
|
+
const [docs, total] = await Promise.all([
|
|
314
|
+
GroupMembership.find({ groupId }, "userId roles").skip(offset).limit(limit).lean(),
|
|
315
|
+
GroupMembership.countDocuments({ groupId }),
|
|
316
|
+
]);
|
|
317
|
+
return {
|
|
318
|
+
items: docs.map((d) => ({ userId: d.userId, roles: d.roles ?? [] })),
|
|
319
|
+
total, limit, offset,
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
async getUserGroups(userId, tenantId) {
|
|
323
|
+
const memberships = await GroupMembership.find({ userId, tenantId: tenantId ?? null }, "groupId roles").lean();
|
|
324
|
+
if (memberships.length === 0)
|
|
325
|
+
return [];
|
|
326
|
+
const groupIds = memberships.map((m) => m.groupId);
|
|
327
|
+
const groups = await Group.find({ _id: { $in: groupIds } }).lean();
|
|
328
|
+
const groupMap = new Map(groups.map((g) => [String(g._id), g]));
|
|
329
|
+
return memberships.map((m) => ({
|
|
330
|
+
group: mongoGroupToRecord(groupMap.get(m.groupId)),
|
|
331
|
+
membershipRoles: m.roles ?? [],
|
|
332
|
+
})).filter((r) => r.group);
|
|
333
|
+
},
|
|
334
|
+
async getEffectiveRoles(userId, tenantId) {
|
|
335
|
+
// Direct roles
|
|
336
|
+
let direct = [];
|
|
337
|
+
if (tenantId) {
|
|
338
|
+
const doc = await TenantRole.findOne({ userId, tenantId }, "roles").lean();
|
|
339
|
+
direct = doc?.roles ?? [];
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const user = await AuthUser.findById(userId, "roles").lean();
|
|
343
|
+
direct = user?.roles ?? [];
|
|
344
|
+
}
|
|
345
|
+
// Group roles via memberships
|
|
346
|
+
const memberships = await GroupMembership.find({ userId, tenantId: tenantId ?? null }, "groupId roles").lean();
|
|
347
|
+
if (memberships.length === 0)
|
|
348
|
+
return [...new Set(direct)];
|
|
349
|
+
const groupIds = memberships.map((m) => m.groupId);
|
|
350
|
+
const groups = await Group.find({ _id: { $in: groupIds } }, "roles").lean();
|
|
351
|
+
const groupMap = new Map(groups.map((g) => [String(g._id), g.roles ?? []]));
|
|
352
|
+
const groupRoles = memberships.flatMap((m) => [
|
|
353
|
+
...(groupMap.get(m.groupId) ?? []),
|
|
354
|
+
...(m.roles ?? []),
|
|
355
|
+
]);
|
|
356
|
+
return [...new Set([...direct, ...groupRoles])];
|
|
357
|
+
},
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// M2M client credentials
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
async getM2MClient(clientId) {
|
|
362
|
+
const client = await M2MClient.findOne({ clientId, active: true }).lean();
|
|
363
|
+
if (!client)
|
|
364
|
+
return null;
|
|
365
|
+
return {
|
|
366
|
+
id: String(client._id),
|
|
367
|
+
clientId: client.clientId,
|
|
368
|
+
name: client.name,
|
|
369
|
+
scopes: client.scopes,
|
|
370
|
+
active: client.active,
|
|
371
|
+
clientSecretHash: client.clientSecretHash,
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
async createM2MClient(data) {
|
|
375
|
+
const client = await M2MClient.create(data);
|
|
376
|
+
return { id: String(client._id) };
|
|
377
|
+
},
|
|
378
|
+
async deleteM2MClient(clientId) {
|
|
379
|
+
await M2MClient.deleteOne({ clientId });
|
|
380
|
+
},
|
|
381
|
+
async listM2MClients() {
|
|
382
|
+
const clients = await M2MClient.find({}).lean();
|
|
383
|
+
return clients.map((c) => ({
|
|
384
|
+
id: String(c._id),
|
|
385
|
+
clientId: c.clientId,
|
|
386
|
+
name: c.name,
|
|
387
|
+
scopes: c.scopes,
|
|
388
|
+
active: c.active,
|
|
389
|
+
}));
|
|
390
|
+
},
|
|
187
391
|
};
|
|
392
|
+
function mongoGroupToRecord(doc) {
|
|
393
|
+
return {
|
|
394
|
+
id: String(doc._id),
|
|
395
|
+
name: doc.name,
|
|
396
|
+
displayName: doc.displayName,
|
|
397
|
+
description: doc.description,
|
|
398
|
+
roles: doc.roles ?? [],
|
|
399
|
+
tenantId: doc.tenantId ?? null,
|
|
400
|
+
createdAt: doc.createdAt instanceof Date ? doc.createdAt.getTime() : doc.createdAt,
|
|
401
|
+
updatedAt: doc.updatedAt instanceof Date ? doc.updatedAt.getTime() : doc.updatedAt,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageAdapter } from "../lib/storageAdapter";
|
|
2
|
+
export interface S3StorageConfig {
|
|
3
|
+
bucket: string;
|
|
4
|
+
region?: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
credentials?: {
|
|
7
|
+
accessKeyId: string;
|
|
8
|
+
secretAccessKey: string;
|
|
9
|
+
};
|
|
10
|
+
publicUrl?: string;
|
|
11
|
+
forcePathStyle?: boolean;
|
|
12
|
+
streaming?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const s3Storage: (config: S3StorageConfig) => StorageAdapter;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
function requireS3Client() {
|
|
2
|
+
try {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4
|
+
return require("@aws-sdk/client-s3");
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
throw new Error("@aws-sdk/client-s3 is not installed. Run: bun add @aws-sdk/client-s3");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function requirePresigner() {
|
|
11
|
+
try {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
return require("@aws-sdk/s3-request-presigner");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error("@aws-sdk/s3-request-presigner is not installed. Run: bun add @aws-sdk/s3-request-presigner");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function requireLibStorage() {
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
return require("@aws-sdk/lib-storage");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error("@aws-sdk/lib-storage is not installed. Run: bun add @aws-sdk/lib-storage");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const s3Storage = (config) => {
|
|
29
|
+
let _client = null;
|
|
30
|
+
const getClient = () => {
|
|
31
|
+
if (!_client) {
|
|
32
|
+
const { S3Client } = requireS3Client();
|
|
33
|
+
_client = new S3Client({
|
|
34
|
+
region: config.region ?? "us-east-1",
|
|
35
|
+
...(config.endpoint ? { endpoint: config.endpoint } : {}),
|
|
36
|
+
...(config.credentials ? { credentials: config.credentials } : {}),
|
|
37
|
+
...(config.forcePathStyle !== undefined ? { forcePathStyle: config.forcePathStyle } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return _client;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
async put(key, data, meta) {
|
|
44
|
+
const bucket = meta.bucket ?? config.bucket;
|
|
45
|
+
const client = getClient();
|
|
46
|
+
if (config.streaming && data instanceof ReadableStream) {
|
|
47
|
+
const { Upload } = requireLibStorage();
|
|
48
|
+
const upload = new Upload({
|
|
49
|
+
client,
|
|
50
|
+
params: {
|
|
51
|
+
Bucket: bucket,
|
|
52
|
+
Key: key,
|
|
53
|
+
Body: data,
|
|
54
|
+
ContentType: meta.mimeType,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
await upload.done();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const { PutObjectCommand } = requireS3Client();
|
|
61
|
+
let body;
|
|
62
|
+
if (data instanceof ReadableStream) {
|
|
63
|
+
const response = new Response(data);
|
|
64
|
+
body = Buffer.from(await response.arrayBuffer());
|
|
65
|
+
}
|
|
66
|
+
else if (data instanceof Blob) {
|
|
67
|
+
body = Buffer.from(await data.arrayBuffer());
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
body = data;
|
|
71
|
+
}
|
|
72
|
+
await client.send(new PutObjectCommand({
|
|
73
|
+
Bucket: bucket,
|
|
74
|
+
Key: key,
|
|
75
|
+
Body: body,
|
|
76
|
+
ContentType: meta.mimeType,
|
|
77
|
+
ContentLength: meta.size,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
const url = config.publicUrl
|
|
81
|
+
? `${config.publicUrl.replace(/\/$/, "")}/${key}`
|
|
82
|
+
: undefined;
|
|
83
|
+
return { ...(url !== undefined ? { url } : {}) };
|
|
84
|
+
},
|
|
85
|
+
async get(key) {
|
|
86
|
+
const { GetObjectCommand } = requireS3Client();
|
|
87
|
+
const client = getClient();
|
|
88
|
+
const bucket = config.bucket;
|
|
89
|
+
try {
|
|
90
|
+
const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
91
|
+
return {
|
|
92
|
+
stream: result.Body,
|
|
93
|
+
mimeType: result.ContentType,
|
|
94
|
+
size: result.ContentLength,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err?.name === "NoSuchKey" || err?.$metadata?.httpStatusCode === 404)
|
|
99
|
+
return null;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async delete(key) {
|
|
104
|
+
const { DeleteObjectCommand } = requireS3Client();
|
|
105
|
+
const client = getClient();
|
|
106
|
+
await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: key }));
|
|
107
|
+
},
|
|
108
|
+
async presignPut(key, opts) {
|
|
109
|
+
const { PutObjectCommand } = requireS3Client();
|
|
110
|
+
const { getSignedUrl } = requirePresigner();
|
|
111
|
+
const client = getClient();
|
|
112
|
+
const params = {
|
|
113
|
+
Bucket: config.bucket,
|
|
114
|
+
Key: key,
|
|
115
|
+
...(opts.mimeType ? { ContentType: opts.mimeType } : {}),
|
|
116
|
+
};
|
|
117
|
+
return getSignedUrl(client, new PutObjectCommand(params), { expiresIn: opts.expirySeconds });
|
|
118
|
+
},
|
|
119
|
+
async presignGet(key, opts) {
|
|
120
|
+
const { GetObjectCommand } = requireS3Client();
|
|
121
|
+
const { getSignedUrl } = requirePresigner();
|
|
122
|
+
const client = getClient();
|
|
123
|
+
return getSignedUrl(client, new GetObjectCommand({ Bucket: config.bucket, Key: key }), { expiresIn: opts.expirySeconds });
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
@@ -15,6 +15,10 @@ import type { RefreshResult } from "../lib/session";
|
|
|
15
15
|
export declare const sqliteSetRefreshToken: (sessionId: string, refreshToken: string) => void;
|
|
16
16
|
export declare const sqliteGetSessionByRefreshToken: (refreshToken: string) => RefreshResult | null;
|
|
17
17
|
export declare const sqliteRotateRefreshToken: (sessionId: string, newRefreshToken: string, newAccessToken: string) => void;
|
|
18
|
+
export declare const sqliteGetSessionFingerprint: (sessionId: string) => string | null;
|
|
19
|
+
export declare const sqliteSetSessionFingerprint: (sessionId: string, fingerprint: string) => void;
|
|
20
|
+
export declare const sqliteGetMfaVerifiedAt: (sessionId: string) => number | null;
|
|
21
|
+
export declare const sqliteSetMfaVerifiedAt: (sessionId: string, ts: number) => void;
|
|
18
22
|
export declare const sqliteStoreOAuthState: (state: string, codeVerifier?: string, linkUserId?: string) => void;
|
|
19
23
|
export declare const sqliteConsumeOAuthState: (state: string) => {
|
|
20
24
|
codeVerifier?: string;
|
|
@@ -31,12 +35,38 @@ export declare const sqliteGetVerificationToken: (token: string) => {
|
|
|
31
35
|
email: string;
|
|
32
36
|
} | null;
|
|
33
37
|
export declare const sqliteDeleteVerificationToken: (token: string) => void;
|
|
38
|
+
export declare const sqliteConsumeVerificationToken: (token: string) => {
|
|
39
|
+
userId: string;
|
|
40
|
+
email: string;
|
|
41
|
+
} | null;
|
|
34
42
|
export declare const sqliteCreateResetToken: (token: string, userId: string, email: string, ttlSeconds: number) => void;
|
|
35
43
|
export declare const sqliteConsumeResetToken: (hash: string) => {
|
|
36
44
|
userId: string;
|
|
37
45
|
email: string;
|
|
38
46
|
} | null;
|
|
47
|
+
export declare const sqliteCreateDeletionCancelToken: (token: string, userId: string, jobId: string, ttlSeconds: number) => void;
|
|
48
|
+
export declare const sqliteConsumeDeletionCancelToken: (hash: string) => {
|
|
49
|
+
userId: string;
|
|
50
|
+
jobId: string;
|
|
51
|
+
} | null;
|
|
39
52
|
import type { OAuthCodePayload } from "../lib/oauthCode";
|
|
40
53
|
export declare const sqliteStoreOAuthCode: (hash: string, payload: OAuthCodePayload, ttlSeconds: number) => void;
|
|
41
54
|
export declare const sqliteConsumeOAuthCode: (hash: string) => OAuthCodePayload | null;
|
|
42
55
|
export declare const startSqliteCleanup: (intervalMs?: number) => ReturnType<typeof setInterval>;
|
|
56
|
+
export declare const sqliteRegisterUpload: (record: {
|
|
57
|
+
key: string;
|
|
58
|
+
ownerUserId?: string;
|
|
59
|
+
tenantId?: string;
|
|
60
|
+
mimeType?: string;
|
|
61
|
+
bucket?: string;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
}) => void;
|
|
64
|
+
export declare const sqliteGetUploadRecord: (key: string) => {
|
|
65
|
+
key: string;
|
|
66
|
+
ownerUserId?: string;
|
|
67
|
+
tenantId?: string;
|
|
68
|
+
mimeType?: string;
|
|
69
|
+
bucket?: string;
|
|
70
|
+
createdAt: number;
|
|
71
|
+
} | null;
|
|
72
|
+
export declare const sqliteDeleteUploadRecord: (key: string) => boolean;
|