@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getMfaVerifiedAt } from "../lib/session";
|
|
2
|
+
import { HttpError } from "../lib/HttpError";
|
|
3
|
+
/**
|
|
4
|
+
* Middleware that requires the user to have recently completed step-up MFA.
|
|
5
|
+
*
|
|
6
|
+
* Attach to sensitive routes that require fresh MFA verification:
|
|
7
|
+
* ```
|
|
8
|
+
* router.post("/transfer", userAuth, requireStepUp(), transferHandler);
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The user completes step-up via POST /auth/step-up.
|
|
12
|
+
* After successful step-up, mfaVerifiedAt is stored in their session.
|
|
13
|
+
*/
|
|
14
|
+
export const requireStepUp = (opts) => async (c, next) => {
|
|
15
|
+
const sessionId = c.get("sessionId");
|
|
16
|
+
if (!sessionId) {
|
|
17
|
+
throw new HttpError(401, "Authentication required");
|
|
18
|
+
}
|
|
19
|
+
const maxAge = opts?.maxAge ?? 300;
|
|
20
|
+
const verifiedAt = await getMfaVerifiedAt(sessionId);
|
|
21
|
+
if (verifiedAt === null) {
|
|
22
|
+
throw new HttpError(403, "Step-up authentication required", "STEP_UP_REQUIRED");
|
|
23
|
+
}
|
|
24
|
+
const now = Math.floor(Date.now() / 1000);
|
|
25
|
+
if (now - verifiedAt > maxAge) {
|
|
26
|
+
throw new HttpError(403, "Step-up authentication expired", "STEP_UP_REQUIRED");
|
|
27
|
+
}
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import type { AppEnv } from "../lib/context";
|
|
3
|
+
export declare function setScimTokens(tokens: string | string[]): void;
|
|
4
|
+
/**
|
|
5
|
+
* Middleware that validates SCIM bearer tokens.
|
|
6
|
+
* Tokens are checked with timingSafeEqual to prevent timing attacks.
|
|
7
|
+
*/
|
|
8
|
+
export declare const scimAuth: MiddlewareHandler<AppEnv>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { timingSafeEqual } from "../lib/crypto";
|
|
2
|
+
import { HttpError } from "../lib/HttpError";
|
|
3
|
+
let _scimTokens = [];
|
|
4
|
+
export function setScimTokens(tokens) {
|
|
5
|
+
_scimTokens = Array.isArray(tokens) ? tokens : [tokens];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Middleware that validates SCIM bearer tokens.
|
|
9
|
+
* Tokens are checked with timingSafeEqual to prevent timing attacks.
|
|
10
|
+
*/
|
|
11
|
+
export const scimAuth = async (c, next) => {
|
|
12
|
+
const authHeader = c.req.header("authorization") ?? "";
|
|
13
|
+
if (!authHeader.startsWith("Bearer ")) {
|
|
14
|
+
throw new HttpError(401, "SCIM bearer token required");
|
|
15
|
+
}
|
|
16
|
+
const provided = authHeader.slice(7);
|
|
17
|
+
const valid = _scimTokens.some((token) => {
|
|
18
|
+
try {
|
|
19
|
+
return timingSafeEqual(provided, token);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (!valid) {
|
|
26
|
+
throw new HttpError(401, "Invalid SCIM token");
|
|
27
|
+
}
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import type { AppEnv } from "../lib/context";
|
|
3
|
+
import type { UploadOpts } from "../lib/upload";
|
|
4
|
+
export type UploadMiddlewareOptions = UploadOpts;
|
|
5
|
+
export declare const handleUpload: (opts?: UploadMiddlewareOptions) => MiddlewareHandler<AppEnv>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseUpload, getUploadConfig } from "../lib/upload";
|
|
2
|
+
export const handleUpload = (opts) => {
|
|
3
|
+
return async (c, next) => {
|
|
4
|
+
const config = getUploadConfig();
|
|
5
|
+
const merged = { ...config, ...opts };
|
|
6
|
+
const maxFileSize = merged.maxFileSize ?? 10 * 1024 * 1024;
|
|
7
|
+
const maxFiles = merged.maxFiles ?? 10;
|
|
8
|
+
// Content-Length pre-check to avoid Bun killing the connection
|
|
9
|
+
const contentLength = Number(c.req.header("content-length") ?? 0);
|
|
10
|
+
if (contentLength > 0 && contentLength > maxFileSize * maxFiles) {
|
|
11
|
+
return c.json({ error: `Request body too large. Maximum is ${maxFileSize * maxFiles} bytes` }, 413);
|
|
12
|
+
}
|
|
13
|
+
let results;
|
|
14
|
+
try {
|
|
15
|
+
results = await parseUpload(c, opts);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (err?.status === 400)
|
|
19
|
+
return c.json({ error: err.message }, 400);
|
|
20
|
+
if (err?.status === 413)
|
|
21
|
+
return c.json({ error: err.message }, 413);
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
c.set("uploadResults", results);
|
|
25
|
+
await next();
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MiddlewareHandler, Context } from "hono";
|
|
2
|
+
import type { AppEnv } from "../lib/context";
|
|
3
|
+
export interface WebhookTimestampOptions {
|
|
4
|
+
/** Header name containing the Unix timestamp (seconds or ms). */
|
|
5
|
+
header: string;
|
|
6
|
+
/**
|
|
7
|
+
* Allowed age of the timestamp in milliseconds.
|
|
8
|
+
* Values below 1e10 in the header are treated as Unix seconds and auto-converted.
|
|
9
|
+
*/
|
|
10
|
+
tolerance: number;
|
|
11
|
+
}
|
|
12
|
+
export interface WebhookAuthOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Shared HMAC secret. Pass a function for dynamic resolution
|
|
15
|
+
* (e.g. per-tenant secret lookup). If the function throws, a 500 is returned.
|
|
16
|
+
*/
|
|
17
|
+
secret: string | ((c: Context<AppEnv>) => string | Promise<string>);
|
|
18
|
+
/** Header that carries the signature. Default: `"x-webhook-signature"`. */
|
|
19
|
+
header?: string;
|
|
20
|
+
/** HMAC algorithm. Default: `"sha256"`. */
|
|
21
|
+
algorithm?: "sha256" | "sha512";
|
|
22
|
+
/**
|
|
23
|
+
* Strip this prefix from the signature header value before comparing.
|
|
24
|
+
* e.g. `"sha256="` for GitHub-style `X-Hub-Signature-256: sha256=<hex>`.
|
|
25
|
+
*/
|
|
26
|
+
prefix?: string;
|
|
27
|
+
/** Optional replay-protection via a timestamp header. */
|
|
28
|
+
timestamp?: WebhookTimestampOptions;
|
|
29
|
+
}
|
|
30
|
+
export declare const webhookAuth: (options: WebhookAuthOptions) => MiddlewareHandler<AppEnv>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHmac } from "crypto";
|
|
2
|
+
import { timingSafeEqual } from "../lib/crypto";
|
|
3
|
+
import { HttpError } from "../lib/HttpError";
|
|
4
|
+
export const webhookAuth = (options) => async (c, next) => {
|
|
5
|
+
const algorithm = options.algorithm ?? "sha256";
|
|
6
|
+
const sigHeader = options.header ?? "x-webhook-signature";
|
|
7
|
+
// --- Optional timestamp replay protection ---
|
|
8
|
+
if (options.timestamp) {
|
|
9
|
+
const { header: tsHeader, tolerance } = options.timestamp;
|
|
10
|
+
const rawTs = c.req.header(tsHeader);
|
|
11
|
+
const tsNum = rawTs !== undefined ? parseInt(rawTs, 10) : NaN;
|
|
12
|
+
if (isNaN(tsNum)) {
|
|
13
|
+
throw new HttpError(401, "Unauthorized", "EXPIRED_TIMESTAMP");
|
|
14
|
+
}
|
|
15
|
+
// Auto-detect Unix seconds (< 1e10) vs milliseconds
|
|
16
|
+
const tsMs = tsNum < 1e10 ? tsNum * 1000 : tsNum;
|
|
17
|
+
if (Math.abs(Date.now() - tsMs) > tolerance) {
|
|
18
|
+
throw new HttpError(401, "Unauthorized", "EXPIRED_TIMESTAMP");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// --- Signature header ---
|
|
22
|
+
const rawSig = c.req.header(sigHeader);
|
|
23
|
+
if (!rawSig) {
|
|
24
|
+
throw new HttpError(401, "Unauthorized", "INVALID_SIGNATURE");
|
|
25
|
+
}
|
|
26
|
+
const provided = options.prefix && rawSig.startsWith(options.prefix)
|
|
27
|
+
? rawSig.slice(options.prefix.length)
|
|
28
|
+
: rawSig;
|
|
29
|
+
// --- Secret resolution ---
|
|
30
|
+
let secret;
|
|
31
|
+
if (typeof options.secret === "function") {
|
|
32
|
+
try {
|
|
33
|
+
secret = await options.secret(c);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new HttpError(500, "Internal Server Error", "WEBHOOK_SECRET_ERROR");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
secret = options.secret;
|
|
41
|
+
}
|
|
42
|
+
// --- Body reading (Hono caches this — downstream c.req.json() still works) ---
|
|
43
|
+
const body = await c.req.text();
|
|
44
|
+
// --- HMAC computation & comparison ---
|
|
45
|
+
const computed = createHmac(algorithm, secret).update(body).digest("hex");
|
|
46
|
+
let valid;
|
|
47
|
+
try {
|
|
48
|
+
valid = timingSafeEqual(computed, provided);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// timingSafeEqual can throw if buffer byte lengths differ (e.g. multi-byte Unicode in sig)
|
|
52
|
+
valid = false;
|
|
53
|
+
}
|
|
54
|
+
if (!valid) {
|
|
55
|
+
throw new HttpError(401, "Unauthorized", "INVALID_SIGNATURE");
|
|
56
|
+
}
|
|
57
|
+
await next();
|
|
58
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IAuditLog {
|
|
3
|
+
/** UUID assigned by the caller — stable cross-store identifier. */
|
|
4
|
+
id: string;
|
|
5
|
+
userId: string | null;
|
|
6
|
+
sessionId: string | null;
|
|
7
|
+
tenantId: string | null;
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
status: number;
|
|
11
|
+
ip: string | null;
|
|
12
|
+
userAgent: string | null;
|
|
13
|
+
action?: string;
|
|
14
|
+
resource?: string;
|
|
15
|
+
resourceId?: string;
|
|
16
|
+
meta?: Record<string, unknown>;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
/**
|
|
19
|
+
* Optional TTL field. MongoDB will automatically delete the document
|
|
20
|
+
* once this date is in the past (via `expireAfterSeconds: 0` index).
|
|
21
|
+
*/
|
|
22
|
+
expiresAt?: Date;
|
|
23
|
+
}
|
|
24
|
+
type AuditLogDocument = IAuditLog & Document;
|
|
25
|
+
export declare const AuditLog: Model<AuditLogDocument, {}, {}, {}, Document<unknown, {}, AuditLogDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuditLog & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
26
|
+
_id: import("mongoose").Types.ObjectId;
|
|
27
|
+
}> & {
|
|
28
|
+
__v: number;
|
|
29
|
+
}, any, AuditLogDocument>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
let _AuditLog = null;
|
|
3
|
+
function getAuditLogModel() {
|
|
4
|
+
if (!_AuditLog) {
|
|
5
|
+
const { Schema } = mongoose;
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
id: { type: String, required: true, unique: true },
|
|
8
|
+
userId: { type: String, default: null },
|
|
9
|
+
sessionId: { type: String, default: null },
|
|
10
|
+
tenantId: { type: String, default: null },
|
|
11
|
+
method: { type: String, required: true },
|
|
12
|
+
path: { type: String, required: true },
|
|
13
|
+
status: { type: Number, required: true },
|
|
14
|
+
ip: { type: String, default: null },
|
|
15
|
+
userAgent: { type: String, default: null },
|
|
16
|
+
action: { type: String },
|
|
17
|
+
resource: { type: String },
|
|
18
|
+
resourceId: { type: String },
|
|
19
|
+
meta: { type: Schema.Types.Mixed },
|
|
20
|
+
expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
|
|
21
|
+
}, {
|
|
22
|
+
collection: "audit_logs",
|
|
23
|
+
// Mongoose manages createdAt; no updatedAt needed for immutable log entries.
|
|
24
|
+
timestamps: { createdAt: "createdAt", updatedAt: false },
|
|
25
|
+
});
|
|
26
|
+
schema.index({ userId: 1, createdAt: 1 });
|
|
27
|
+
schema.index({ tenantId: 1, createdAt: 1 });
|
|
28
|
+
schema.index({ path: 1 });
|
|
29
|
+
_AuditLog = authConnection.model("AuditLog", schema);
|
|
30
|
+
}
|
|
31
|
+
return _AuditLog;
|
|
32
|
+
}
|
|
33
|
+
export const AuditLog = new Proxy({}, {
|
|
34
|
+
get(_, prop) {
|
|
35
|
+
const model = getAuditLogModel();
|
|
36
|
+
const val = model[prop];
|
|
37
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -25,6 +25,13 @@ interface IAuthUser {
|
|
|
25
25
|
name?: string;
|
|
26
26
|
createdAt: Date;
|
|
27
27
|
}>;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
firstName?: string;
|
|
30
|
+
lastName?: string;
|
|
31
|
+
externalId?: string;
|
|
32
|
+
suspended: boolean;
|
|
33
|
+
suspendedAt?: Date;
|
|
34
|
+
suspendedReason?: string;
|
|
28
35
|
}
|
|
29
36
|
type AuthUserDocument = IAuthUser & Document;
|
|
30
37
|
export declare const AuthUser: Model<AuthUserDocument, {}, {}, {}, Document<unknown, {}, AuthUserDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuthUser & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
package/dist/models/AuthUser.js
CHANGED
|
@@ -31,6 +31,13 @@ function getAuthUser() {
|
|
|
31
31
|
name: { type: String },
|
|
32
32
|
createdAt: { type: Date, default: Date.now },
|
|
33
33
|
}],
|
|
34
|
+
displayName: { type: String, default: null },
|
|
35
|
+
firstName: { type: String, default: null },
|
|
36
|
+
lastName: { type: String, default: null },
|
|
37
|
+
externalId: { type: String, default: null, index: true, sparse: true },
|
|
38
|
+
suspended: { type: Boolean, default: false },
|
|
39
|
+
suspendedAt: { type: Date, default: null },
|
|
40
|
+
suspendedReason: { type: String, default: null },
|
|
34
41
|
}, { timestamps: true });
|
|
35
42
|
schema.index({ providerIds: 1 });
|
|
36
43
|
_AuthUser = authConnection.model("AuthUser", schema);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IGroup {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
roles: string[];
|
|
7
|
+
/**
|
|
8
|
+
* null = app-wide group, string = tenant-scoped group.
|
|
9
|
+
* Immutable after creation — adapters must reject updates that include tenantId.
|
|
10
|
+
*/
|
|
11
|
+
tenantId: string | null;
|
|
12
|
+
}
|
|
13
|
+
type GroupDocument = IGroup & Document;
|
|
14
|
+
export declare const Group: Model<GroupDocument, {}, {}, {}, Document<unknown, {}, GroupDocument, {}, import("mongoose").DefaultSchemaOptions> & IGroup & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
15
|
+
_id: import("mongoose").Types.ObjectId;
|
|
16
|
+
}> & {
|
|
17
|
+
__v: number;
|
|
18
|
+
} & {
|
|
19
|
+
id: string;
|
|
20
|
+
}, any, GroupDocument>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
let _Group = null;
|
|
3
|
+
function getGroup() {
|
|
4
|
+
if (!_Group) {
|
|
5
|
+
const { Schema } = mongoose;
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
name: { type: String, required: true },
|
|
8
|
+
displayName: { type: String },
|
|
9
|
+
description: { type: String },
|
|
10
|
+
roles: [{ type: String }],
|
|
11
|
+
tenantId: { type: String, default: null },
|
|
12
|
+
}, { timestamps: true });
|
|
13
|
+
// Name is unique within scope (app-wide or per-tenant).
|
|
14
|
+
// MongoDB treats null as a value, so this compound index correctly enforces uniqueness
|
|
15
|
+
// for app-wide groups (both have tenantId: null) and per-tenant groups separately.
|
|
16
|
+
schema.index({ name: 1, tenantId: 1 }, { unique: true });
|
|
17
|
+
schema.index({ tenantId: 1 });
|
|
18
|
+
_Group = authConnection.model("Group", schema);
|
|
19
|
+
}
|
|
20
|
+
return _Group;
|
|
21
|
+
}
|
|
22
|
+
export const Group = new Proxy({}, {
|
|
23
|
+
get(_, prop) {
|
|
24
|
+
const model = getGroup();
|
|
25
|
+
const val = model[prop];
|
|
26
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IGroupMembership {
|
|
3
|
+
userId: string;
|
|
4
|
+
groupId: string;
|
|
5
|
+
/** Per-member extra roles on top of the group's baseline roles. */
|
|
6
|
+
roles: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Denormalized from the group at insert time for efficient tenant-scoped queries.
|
|
9
|
+
* Immutable: group.tenantId cannot change after creation, so this is always consistent.
|
|
10
|
+
*/
|
|
11
|
+
tenantId: string | null;
|
|
12
|
+
}
|
|
13
|
+
type GroupMembershipDocument = IGroupMembership & Document;
|
|
14
|
+
export declare const GroupMembership: Model<GroupMembershipDocument, {}, {}, {}, Document<unknown, {}, GroupMembershipDocument, {}, import("mongoose").DefaultSchemaOptions> & IGroupMembership & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
15
|
+
_id: import("mongoose").Types.ObjectId;
|
|
16
|
+
}> & {
|
|
17
|
+
__v: number;
|
|
18
|
+
} & {
|
|
19
|
+
id: string;
|
|
20
|
+
}, any, GroupMembershipDocument>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
let _GroupMembership = null;
|
|
3
|
+
function getGroupMembership() {
|
|
4
|
+
if (!_GroupMembership) {
|
|
5
|
+
const { Schema } = mongoose;
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
userId: { type: String, required: true },
|
|
8
|
+
groupId: { type: String, required: true },
|
|
9
|
+
roles: [{ type: String }],
|
|
10
|
+
tenantId: { type: String, default: null },
|
|
11
|
+
}, { timestamps: { createdAt: true, updatedAt: false } });
|
|
12
|
+
schema.index({ userId: 1, groupId: 1 }, { unique: true });
|
|
13
|
+
schema.index({ groupId: 1 });
|
|
14
|
+
schema.index({ userId: 1, tenantId: 1 });
|
|
15
|
+
_GroupMembership = authConnection.model("GroupMembership", schema);
|
|
16
|
+
}
|
|
17
|
+
return _GroupMembership;
|
|
18
|
+
}
|
|
19
|
+
export const GroupMembership = new Proxy({}, {
|
|
20
|
+
get(_, prop) {
|
|
21
|
+
const model = getGroupMembership();
|
|
22
|
+
const val = model[prop];
|
|
23
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
export interface IM2MClient {
|
|
3
|
+
_id: string;
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecretHash: string;
|
|
6
|
+
name: string;
|
|
7
|
+
scopes: string[];
|
|
8
|
+
active: boolean;
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
updatedAt: Date;
|
|
11
|
+
}
|
|
12
|
+
export declare const M2MClient: mongoose.Model<IM2MClient, {}, {}, {}, mongoose.Document<unknown, {}, IM2MClient, {}, mongoose.DefaultSchemaOptions> & IM2MClient & Required<{
|
|
13
|
+
_id: string;
|
|
14
|
+
}> & {
|
|
15
|
+
__v: number;
|
|
16
|
+
} & {
|
|
17
|
+
id: string;
|
|
18
|
+
}, any, IM2MClient>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
const m2mClientSchema = new mongoose.Schema({
|
|
3
|
+
clientId: { type: String, required: true, unique: true },
|
|
4
|
+
clientSecretHash: { type: String, required: true },
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
scopes: { type: [String], default: [] },
|
|
7
|
+
active: { type: Boolean, default: true },
|
|
8
|
+
}, { timestamps: true });
|
|
9
|
+
// Lazy proxy pattern (same as AuthUser.ts)
|
|
10
|
+
export const M2MClient = new Proxy({}, {
|
|
11
|
+
get(_, prop) {
|
|
12
|
+
const { authConnection } = require("../lib/mongo");
|
|
13
|
+
if (!authConnection)
|
|
14
|
+
throw new Error("authConnection not initialized — call connectAuthMongo() or connectMongo() first");
|
|
15
|
+
const model = authConnection.models["M2MClient"] ?? authConnection.model("M2MClient", m2mClientSchema);
|
|
16
|
+
return model[prop];
|
|
17
|
+
},
|
|
18
|
+
});
|
package/dist/routes/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig } from "../lib/appConfig";
|
|
1
|
+
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, StepUpConfig } from "../lib/appConfig";
|
|
2
2
|
import type { AuthRateLimitConfig, AccountDeletionConfig } from "../app";
|
|
3
3
|
export interface AuthRouterOptions {
|
|
4
4
|
primaryField: PrimaryField;
|
|
@@ -7,5 +7,6 @@ export interface AuthRouterOptions {
|
|
|
7
7
|
rateLimit?: AuthRateLimitConfig;
|
|
8
8
|
accountDeletion?: AccountDeletionConfig;
|
|
9
9
|
refreshTokens?: RefreshTokenConfig;
|
|
10
|
+
stepUp?: StepUpConfig;
|
|
10
11
|
}
|
|
11
|
-
export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit, accountDeletion, refreshTokens }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
|
|
12
|
+
export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit, accountDeletion, refreshTokens, stepUp }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
|