@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.
Files changed (185) hide show
  1. package/README.md +3035 -1249
  2. package/dist/adapters/localStorage.d.ts +6 -0
  3. package/dist/adapters/localStorage.js +59 -0
  4. package/dist/adapters/memoryAuth.d.ts +13 -0
  5. package/dist/adapters/memoryAuth.js +261 -2
  6. package/dist/adapters/memoryStorage.d.ts +3 -0
  7. package/dist/adapters/memoryStorage.js +44 -0
  8. package/dist/adapters/mongoAuth.js +217 -1
  9. package/dist/adapters/s3Storage.d.ts +14 -0
  10. package/dist/adapters/s3Storage.js +126 -0
  11. package/dist/adapters/sqliteAuth.d.ts +30 -0
  12. package/dist/adapters/sqliteAuth.js +352 -2
  13. package/dist/app.d.ts +203 -3
  14. package/dist/app.js +352 -48
  15. package/dist/cli.js +118 -38
  16. package/dist/index.d.ts +69 -8
  17. package/dist/index.js +46 -5
  18. package/dist/lib/HttpError.d.ts +7 -1
  19. package/dist/lib/HttpError.js +10 -1
  20. package/dist/lib/appConfig.d.ts +157 -0
  21. package/dist/lib/appConfig.js +54 -0
  22. package/dist/lib/auditLog.d.ts +58 -0
  23. package/dist/lib/auditLog.js +218 -0
  24. package/dist/lib/authAdapter.d.ts +140 -1
  25. package/dist/lib/authRateLimit.js +36 -0
  26. package/dist/lib/breachedPassword.d.ts +13 -0
  27. package/dist/lib/breachedPassword.js +48 -0
  28. package/dist/lib/captcha.d.ts +25 -0
  29. package/dist/lib/captcha.js +37 -0
  30. package/dist/lib/constants.d.ts +4 -0
  31. package/dist/lib/constants.js +4 -0
  32. package/dist/lib/context.d.ts +24 -1
  33. package/dist/lib/context.js +17 -3
  34. package/dist/lib/createRoute.d.ts +28 -2
  35. package/dist/lib/createRoute.js +54 -3
  36. package/dist/lib/credentialStuffing.d.ts +31 -0
  37. package/dist/lib/credentialStuffing.js +77 -0
  38. package/dist/lib/deletionCancelToken.d.ts +12 -0
  39. package/dist/lib/deletionCancelToken.js +88 -0
  40. package/dist/lib/emailVerification.d.ts +6 -0
  41. package/dist/lib/emailVerification.js +46 -3
  42. package/dist/lib/groups.d.ts +113 -0
  43. package/dist/lib/groups.js +133 -0
  44. package/dist/lib/idempotency.d.ts +22 -0
  45. package/dist/lib/idempotency.js +182 -0
  46. package/dist/lib/jwks.d.ts +25 -0
  47. package/dist/lib/jwks.js +51 -0
  48. package/dist/lib/jwt.d.ts +15 -2
  49. package/dist/lib/jwt.js +92 -5
  50. package/dist/lib/logger.d.ts +2 -0
  51. package/dist/lib/logger.js +6 -0
  52. package/dist/lib/m2m.d.ts +29 -0
  53. package/dist/lib/m2m.js +48 -0
  54. package/dist/lib/metrics.d.ts +14 -0
  55. package/dist/lib/metrics.js +158 -0
  56. package/dist/lib/mfaChallenge.d.ts +14 -1
  57. package/dist/lib/mfaChallenge.js +111 -6
  58. package/dist/lib/mongo.js +1 -1
  59. package/dist/lib/oauthCode.js +23 -18
  60. package/dist/lib/pagination.d.ts +119 -0
  61. package/dist/lib/pagination.js +166 -0
  62. package/dist/lib/resetPassword.js +3 -1
  63. package/dist/lib/saml.d.ts +25 -0
  64. package/dist/lib/saml.js +64 -0
  65. package/dist/lib/scim.d.ts +44 -0
  66. package/dist/lib/scim.js +54 -0
  67. package/dist/lib/securityEvents.d.ts +28 -0
  68. package/dist/lib/securityEvents.js +26 -0
  69. package/dist/lib/session.d.ts +14 -0
  70. package/dist/lib/session.js +121 -5
  71. package/dist/lib/signing.d.ts +52 -0
  72. package/dist/lib/signing.js +183 -0
  73. package/dist/lib/storageAdapter.d.ts +30 -0
  74. package/dist/lib/storageAdapter.js +1 -0
  75. package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
  76. package/dist/lib/stripUnreferencedSchemas.js +79 -0
  77. package/dist/lib/suspension.d.ts +13 -0
  78. package/dist/lib/suspension.js +23 -0
  79. package/dist/lib/tenant.js +2 -2
  80. package/dist/lib/upload.d.ts +39 -0
  81. package/dist/lib/upload.js +112 -0
  82. package/dist/lib/uploadRegistry.d.ts +18 -0
  83. package/dist/lib/uploadRegistry.js +83 -0
  84. package/dist/lib/validate.js +2 -2
  85. package/dist/lib/ws.d.ts +1 -0
  86. package/dist/lib/ws.js +28 -0
  87. package/dist/lib/wsHeartbeat.d.ts +12 -0
  88. package/dist/lib/wsHeartbeat.js +57 -0
  89. package/dist/lib/wsMessages.d.ts +40 -0
  90. package/dist/lib/wsMessages.js +330 -0
  91. package/dist/lib/wsPresence.d.ts +25 -0
  92. package/dist/lib/wsPresence.js +99 -0
  93. package/dist/middleware/auditLog.d.ts +22 -0
  94. package/dist/middleware/auditLog.js +39 -0
  95. package/dist/middleware/bearerAuth.js +1 -1
  96. package/dist/middleware/cacheResponse.js +5 -1
  97. package/dist/middleware/captcha.d.ts +10 -0
  98. package/dist/middleware/captcha.js +36 -0
  99. package/dist/middleware/csrf.js +18 -4
  100. package/dist/middleware/errorHandler.js +4 -1
  101. package/dist/middleware/identify.js +89 -14
  102. package/dist/middleware/metrics.d.ts +9 -0
  103. package/dist/middleware/metrics.js +26 -0
  104. package/dist/middleware/requestId.d.ts +3 -0
  105. package/dist/middleware/requestId.js +7 -0
  106. package/dist/middleware/requestLogger.d.ts +38 -0
  107. package/dist/middleware/requestLogger.js +68 -0
  108. package/dist/middleware/requestSigning.d.ts +20 -0
  109. package/dist/middleware/requestSigning.js +100 -0
  110. package/dist/middleware/requireMfaSetup.d.ts +16 -0
  111. package/dist/middleware/requireMfaSetup.js +37 -0
  112. package/dist/middleware/requireRole.d.ts +9 -3
  113. package/dist/middleware/requireRole.js +23 -36
  114. package/dist/middleware/requireScope.d.ts +10 -0
  115. package/dist/middleware/requireScope.js +25 -0
  116. package/dist/middleware/requireStepUp.d.ts +18 -0
  117. package/dist/middleware/requireStepUp.js +29 -0
  118. package/dist/middleware/scimAuth.d.ts +8 -0
  119. package/dist/middleware/scimAuth.js +29 -0
  120. package/dist/middleware/upload.d.ts +5 -0
  121. package/dist/middleware/upload.js +27 -0
  122. package/dist/middleware/webhookAuth.d.ts +30 -0
  123. package/dist/middleware/webhookAuth.js +58 -0
  124. package/dist/models/AuditLog.d.ts +30 -0
  125. package/dist/models/AuditLog.js +39 -0
  126. package/dist/models/AuthUser.d.ts +7 -0
  127. package/dist/models/AuthUser.js +7 -0
  128. package/dist/models/Group.d.ts +21 -0
  129. package/dist/models/Group.js +28 -0
  130. package/dist/models/GroupMembership.d.ts +21 -0
  131. package/dist/models/GroupMembership.js +25 -0
  132. package/dist/models/M2MClient.d.ts +18 -0
  133. package/dist/models/M2MClient.js +18 -0
  134. package/dist/routes/auth.d.ts +3 -2
  135. package/dist/routes/auth.js +238 -21
  136. package/dist/routes/groups.d.ts +21 -0
  137. package/dist/routes/groups.js +346 -0
  138. package/dist/routes/jobs.js +66 -46
  139. package/dist/routes/m2m.d.ts +2 -0
  140. package/dist/routes/m2m.js +72 -0
  141. package/dist/routes/metrics.d.ts +8 -0
  142. package/dist/routes/metrics.js +55 -0
  143. package/dist/routes/mfa.js +13 -1
  144. package/dist/routes/oauth.js +6 -0
  145. package/dist/routes/oidc.d.ts +2 -0
  146. package/dist/routes/oidc.js +29 -0
  147. package/dist/routes/passkey.d.ts +1 -0
  148. package/dist/routes/passkey.js +157 -0
  149. package/dist/routes/saml.d.ts +2 -0
  150. package/dist/routes/saml.js +86 -0
  151. package/dist/routes/scim.d.ts +2 -0
  152. package/dist/routes/scim.js +255 -0
  153. package/dist/routes/uploads.d.ts +14 -0
  154. package/dist/routes/uploads.js +227 -0
  155. package/dist/server.d.ts +26 -0
  156. package/dist/server.js +46 -3
  157. package/dist/services/auth.d.ts +2 -0
  158. package/dist/services/auth.js +101 -22
  159. package/dist/services/mfa.js +2 -2
  160. package/dist/ws/index.js +5 -1
  161. package/docs/sections/auth-flow/full.md +203 -47
  162. package/docs/sections/auth-flow/overview.md +2 -2
  163. package/docs/sections/auth-security-examples/full.md +388 -0
  164. package/docs/sections/authentication/full.md +130 -0
  165. package/docs/sections/authentication/overview.md +5 -0
  166. package/docs/sections/cli/full.md +13 -1
  167. package/docs/sections/configuration/full.md +17 -0
  168. package/docs/sections/configuration/overview.md +1 -0
  169. package/docs/sections/exports/full.md +34 -3
  170. package/docs/sections/logging/full.md +83 -0
  171. package/docs/sections/metrics/full.md +131 -0
  172. package/docs/sections/oauth/full.md +189 -189
  173. package/docs/sections/oauth/overview.md +1 -1
  174. package/docs/sections/pagination/full.md +93 -0
  175. package/docs/sections/passkey-login/full.md +90 -0
  176. package/docs/sections/passkey-login/overview.md +1 -0
  177. package/docs/sections/roles/full.md +224 -135
  178. package/docs/sections/roles/overview.md +3 -1
  179. package/docs/sections/signing/full.md +203 -0
  180. package/docs/sections/uploads/full.md +208 -0
  181. package/docs/sections/versioning/full.md +85 -0
  182. package/docs/sections/webhook-auth/full.md +100 -0
  183. package/docs/sections/websocket/full.md +95 -0
  184. package/docs/sections/websocket-rooms/full.md +6 -1
  185. package/package.json +18 -5
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ import type { ZodType } from "zod";
3
+ export type { PaginationOpts, PaginatedResult } from "./groups";
4
+ export interface OffsetParamDefaults {
5
+ /** Default: 50 */
6
+ limit?: number;
7
+ /** Default: 200 */
8
+ maxLimit?: number;
9
+ /** Default: 0 */
10
+ offset?: number;
11
+ }
12
+ export interface ParsedOffsetParams {
13
+ limit: number;
14
+ offset: number;
15
+ }
16
+ /**
17
+ * Zod schema for offset pagination query params.
18
+ * Fields are `z.string().optional()` — matches the existing query param
19
+ * convention. Parse the values with `parseOffsetParams`.
20
+ *
21
+ * @example
22
+ * createRoute({ ..., request: { query: offsetParams({ limit: 20 }) }, ... })
23
+ */
24
+ export declare function offsetParams(defaults?: OffsetParamDefaults): z.ZodObject<{
25
+ limit: z.ZodOptional<z.ZodString>;
26
+ offset: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>;
28
+ /**
29
+ * Parses raw string query values into clamped integers.
30
+ * - NaN (non-numeric strings) falls back to defaults
31
+ * - limit clamped to [1, maxLimit]
32
+ * - offset clamped to [0, ∞)
33
+ *
34
+ * @example
35
+ * const { limit, offset } = parseOffsetParams(c.req.query(), { maxLimit: 100 });
36
+ */
37
+ export declare function parseOffsetParams(raw: {
38
+ limit?: string;
39
+ offset?: string;
40
+ }, defaults?: OffsetParamDefaults): ParsedOffsetParams;
41
+ /**
42
+ * Zod schema factory for paginated offset responses.
43
+ * Wraps `itemSchema` in `{ items, total, limit, offset }` and registers
44
+ * the result as a named OpenAPI component.
45
+ *
46
+ * Throws if `name` was previously registered to a different schema instance.
47
+ * Calling with the same `name` + `schema` pair is idempotent.
48
+ *
49
+ * @example
50
+ * const PaginatedUsersResponse = paginatedResponse(UserSchema, "PaginatedUsers");
51
+ */
52
+ export declare function paginatedResponse<T extends ZodType>(itemSchema: T, name: string): z.ZodObject<{
53
+ items: z.ZodArray<T>;
54
+ total: z.ZodNumber;
55
+ limit: z.ZodNumber;
56
+ offset: z.ZodNumber;
57
+ }, z.core.$strip>;
58
+ export interface CursorParamDefaults {
59
+ /** Default: 50 */
60
+ limit?: number;
61
+ /** Default: 200 */
62
+ maxLimit?: number;
63
+ }
64
+ export interface ParsedCursorParams {
65
+ limit: number;
66
+ cursor: string | undefined;
67
+ }
68
+ export interface CursorResult<T> {
69
+ items: T[];
70
+ nextCursor: string | null;
71
+ hasMore: boolean;
72
+ }
73
+ /**
74
+ * Zod schema for cursor pagination query params.
75
+ * Fields are `z.string().optional()`. Parse the values with `parseCursorParams`.
76
+ *
77
+ * @example
78
+ * createRoute({ ..., request: { query: cursorParams() }, ... })
79
+ */
80
+ export declare function cursorParams(defaults?: CursorParamDefaults): z.ZodObject<{
81
+ limit: z.ZodOptional<z.ZodString>;
82
+ cursor: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strip>;
84
+ /**
85
+ * Parses raw string query values into typed cursor params.
86
+ * - limit: NaN falls back to default, clamped to [1, maxLimit]
87
+ * - cursor: empty string normalized to `undefined`; non-empty is pass-through
88
+ * - When `signing.cursors: true`, verifies the cursor HMAC — invalid cursor returns null
89
+ *
90
+ * @example
91
+ * const { limit, cursor } = parseCursorParams(c.req.query());
92
+ */
93
+ export declare function parseCursorParams(raw: {
94
+ limit?: string;
95
+ cursor?: string;
96
+ }, defaults?: CursorParamDefaults): ParsedCursorParams & {
97
+ invalidCursor?: true;
98
+ };
99
+ /**
100
+ * Sign a cursor value if `signing.cursors: true`. Otherwise returns the
101
+ * cursor unchanged (current behavior).
102
+ */
103
+ export declare function maybeSignCursor(cursor: string | null): string | null;
104
+ /**
105
+ * Zod schema factory for cursor-paginated responses.
106
+ * Wraps `itemSchema` in `{ items, nextCursor, hasMore }` and registers
107
+ * the result as a named OpenAPI component.
108
+ *
109
+ * Throws if `name` was previously registered to a different schema instance.
110
+ * Calling with the same `name` + `schema` pair is idempotent.
111
+ *
112
+ * @example
113
+ * const PostsPage = cursorResponse(PostSchema, "PostsPage");
114
+ */
115
+ export declare function cursorResponse<T extends ZodType>(itemSchema: T, name: string): z.ZodObject<{
116
+ items: z.ZodArray<T>;
117
+ nextCursor: z.ZodNullable<z.ZodString>;
118
+ hasMore: z.ZodBoolean;
119
+ }, z.core.$strip>;
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import { registerSchema } from "./createRoute";
3
+ import { getSigningConfig, getSigningSecret } from "./appConfig";
4
+ import { signCursor, verifyCursor } from "./signing";
5
+ const _registered = new Map();
6
+ function guardedRegister(name, itemSchema, buildWrapper) {
7
+ const existing = _registered.get(name);
8
+ if (existing !== undefined) {
9
+ if (existing.itemSchema !== itemSchema) {
10
+ throw new Error(`Pagination schema name "${name}" is already registered to a different schema`);
11
+ }
12
+ // Same item schema → idempotent, return the cached wrapper
13
+ return existing.wrapper;
14
+ }
15
+ const wrapper = buildWrapper();
16
+ _registered.set(name, { itemSchema, wrapper });
17
+ registerSchema(name, wrapper);
18
+ return wrapper;
19
+ }
20
+ /**
21
+ * Zod schema for offset pagination query params.
22
+ * Fields are `z.string().optional()` — matches the existing query param
23
+ * convention. Parse the values with `parseOffsetParams`.
24
+ *
25
+ * @example
26
+ * createRoute({ ..., request: { query: offsetParams({ limit: 20 }) }, ... })
27
+ */
28
+ export function offsetParams(defaults) {
29
+ const defaultLimit = defaults?.limit ?? 50;
30
+ const defaultOffset = defaults?.offset ?? 0;
31
+ const maxLimit = defaults?.maxLimit ?? 200;
32
+ return z.object({
33
+ limit: z
34
+ .string()
35
+ .optional()
36
+ .describe(`Number of items to return (1–${maxLimit}, default ${defaultLimit})`),
37
+ offset: z
38
+ .string()
39
+ .optional()
40
+ .describe(`Number of items to skip (default ${defaultOffset})`),
41
+ });
42
+ }
43
+ /**
44
+ * Parses raw string query values into clamped integers.
45
+ * - NaN (non-numeric strings) falls back to defaults
46
+ * - limit clamped to [1, maxLimit]
47
+ * - offset clamped to [0, ∞)
48
+ *
49
+ * @example
50
+ * const { limit, offset } = parseOffsetParams(c.req.query(), { maxLimit: 100 });
51
+ */
52
+ export function parseOffsetParams(raw, defaults) {
53
+ const defaultLimit = defaults?.limit ?? 50;
54
+ const maxLimit = defaults?.maxLimit ?? 200;
55
+ const defaultOffset = defaults?.offset ?? 0;
56
+ const rawLimit = parseInt(raw.limit ?? "", 10);
57
+ const rawOffset = parseInt(raw.offset ?? "", 10);
58
+ const limit = isNaN(rawLimit)
59
+ ? defaultLimit
60
+ : Math.min(Math.max(rawLimit, 1), maxLimit);
61
+ const offset = isNaN(rawOffset) ? defaultOffset : Math.max(rawOffset, 0);
62
+ return { limit, offset };
63
+ }
64
+ /**
65
+ * Zod schema factory for paginated offset responses.
66
+ * Wraps `itemSchema` in `{ items, total, limit, offset }` and registers
67
+ * the result as a named OpenAPI component.
68
+ *
69
+ * Throws if `name` was previously registered to a different schema instance.
70
+ * Calling with the same `name` + `schema` pair is idempotent.
71
+ *
72
+ * @example
73
+ * const PaginatedUsersResponse = paginatedResponse(UserSchema, "PaginatedUsers");
74
+ */
75
+ export function paginatedResponse(itemSchema, name) {
76
+ return guardedRegister(name, itemSchema, () => z.object({
77
+ items: z.array(itemSchema),
78
+ total: z.number().int().nonnegative(),
79
+ limit: z.number().int().positive(),
80
+ offset: z.number().int().nonnegative(),
81
+ }));
82
+ }
83
+ /**
84
+ * Zod schema for cursor pagination query params.
85
+ * Fields are `z.string().optional()`. Parse the values with `parseCursorParams`.
86
+ *
87
+ * @example
88
+ * createRoute({ ..., request: { query: cursorParams() }, ... })
89
+ */
90
+ export function cursorParams(defaults) {
91
+ const defaultLimit = defaults?.limit ?? 50;
92
+ const maxLimit = defaults?.maxLimit ?? 200;
93
+ return z.object({
94
+ limit: z
95
+ .string()
96
+ .optional()
97
+ .describe(`Number of items to return (1–${maxLimit}, default ${defaultLimit})`),
98
+ cursor: z
99
+ .string()
100
+ .optional()
101
+ .describe("Opaque cursor from a previous response's nextCursor field"),
102
+ });
103
+ }
104
+ /**
105
+ * Parses raw string query values into typed cursor params.
106
+ * - limit: NaN falls back to default, clamped to [1, maxLimit]
107
+ * - cursor: empty string normalized to `undefined`; non-empty is pass-through
108
+ * - When `signing.cursors: true`, verifies the cursor HMAC — invalid cursor returns null
109
+ *
110
+ * @example
111
+ * const { limit, cursor } = parseCursorParams(c.req.query());
112
+ */
113
+ export function parseCursorParams(raw, defaults) {
114
+ const defaultLimit = defaults?.limit ?? 50;
115
+ const maxLimit = defaults?.maxLimit ?? 200;
116
+ const rawLimit = parseInt(raw.limit ?? "", 10);
117
+ const limit = isNaN(rawLimit)
118
+ ? defaultLimit
119
+ : Math.min(Math.max(rawLimit, 1), maxLimit);
120
+ if (!raw.cursor)
121
+ return { limit, cursor: undefined };
122
+ const cfg = getSigningConfig();
123
+ if (cfg?.cursors) {
124
+ const secret = getSigningSecret();
125
+ if (secret) {
126
+ const verified = verifyCursor(raw.cursor, secret);
127
+ if (verified === null)
128
+ return { limit, cursor: undefined, invalidCursor: true };
129
+ return { limit, cursor: verified };
130
+ }
131
+ }
132
+ return { limit, cursor: raw.cursor };
133
+ }
134
+ /**
135
+ * Sign a cursor value if `signing.cursors: true`. Otherwise returns the
136
+ * cursor unchanged (current behavior).
137
+ */
138
+ export function maybeSignCursor(cursor) {
139
+ if (!cursor)
140
+ return cursor;
141
+ const cfg = getSigningConfig();
142
+ if (cfg?.cursors) {
143
+ const secret = getSigningSecret();
144
+ if (secret)
145
+ return signCursor(cursor, secret);
146
+ }
147
+ return cursor;
148
+ }
149
+ /**
150
+ * Zod schema factory for cursor-paginated responses.
151
+ * Wraps `itemSchema` in `{ items, nextCursor, hasMore }` and registers
152
+ * the result as a named OpenAPI component.
153
+ *
154
+ * Throws if `name` was previously registered to a different schema instance.
155
+ * Calling with the same `name` + `schema` pair is idempotent.
156
+ *
157
+ * @example
158
+ * const PostsPage = cursorResponse(PostSchema, "PostsPage");
159
+ */
160
+ export function cursorResponse(itemSchema, name) {
161
+ return guardedRegister(name, itemSchema, () => z.object({
162
+ items: z.array(itemSchema),
163
+ nextCursor: z.string().nullable(),
164
+ hasMore: z.boolean(),
165
+ }));
166
+ }
@@ -44,7 +44,9 @@ export const setPasswordResetStore = (store) => { _store = store; };
44
44
  /** Create a reset token. Returns the raw token (to embed in the email link).
45
45
  * Only the SHA-256 hash is persisted in the store. */
46
46
  export const createResetToken = async (userId, email) => {
47
- const token = crypto.randomUUID();
47
+ const bytes = new Uint8Array(32);
48
+ crypto.getRandomValues(bytes);
49
+ const token = Buffer.from(bytes).toString("base64url");
48
50
  const hash = hashToken(token);
49
51
  const ttl = getResetTokenExpiry();
50
52
  if (_store === "memory") {
@@ -0,0 +1,25 @@
1
+ import type { IdentityProfile } from "./authAdapter";
2
+ export interface SamlProfile {
3
+ nameId: string;
4
+ nameIdFormat?: string;
5
+ email?: string;
6
+ firstName?: string;
7
+ lastName?: string;
8
+ displayName?: string;
9
+ groups?: string[];
10
+ attributes: Record<string, string | string[]>;
11
+ }
12
+ export interface SamlAttributeMapping {
13
+ email?: string;
14
+ firstName?: string;
15
+ lastName?: string;
16
+ groups?: string;
17
+ }
18
+ export declare function initSaml(config: import("./appConfig").SamlConfig): Promise<void>;
19
+ export declare function createAuthnRequest(): {
20
+ redirectUrl: string;
21
+ id: string;
22
+ };
23
+ export declare function validateSamlResponse(body: string, config: import("./appConfig").SamlConfig): Promise<SamlProfile>;
24
+ export declare function samlProfileToIdentityProfile(profile: SamlProfile): IdentityProfile;
25
+ export declare function getSamlSpMetadata(): string;
@@ -0,0 +1,64 @@
1
+ let _sp = null;
2
+ let _idp = null;
3
+ export async function initSaml(config) {
4
+ const samlify = await import("samlify");
5
+ _sp = samlify.ServiceProvider({
6
+ entityID: config.entityId,
7
+ assertionConsumerService: [{
8
+ Binding: samlify.Constants.BindingNamespace.Post,
9
+ Location: config.acsUrl,
10
+ }],
11
+ signingCert: config.signingCert,
12
+ privateKey: config.signingKey,
13
+ allowCreate: true,
14
+ });
15
+ // Load IdP metadata
16
+ if (config.idpMetadata.startsWith("http")) {
17
+ // URL — fetch it
18
+ const res = await fetch(config.idpMetadata);
19
+ const xml = await res.text();
20
+ _idp = samlify.IdentityProvider({ metadata: xml });
21
+ }
22
+ else {
23
+ // XML string
24
+ _idp = samlify.IdentityProvider({ metadata: config.idpMetadata });
25
+ }
26
+ }
27
+ export function createAuthnRequest() {
28
+ if (!_sp || !_idp)
29
+ throw new Error("SAML not initialized");
30
+ const { context, entityEndpoint } = _sp.createLoginRequest(_idp, "redirect");
31
+ return { redirectUrl: entityEndpoint + "?" + context, id: context };
32
+ }
33
+ export async function validateSamlResponse(body, config) {
34
+ if (!_sp || !_idp)
35
+ throw new Error("SAML not initialized");
36
+ const { extract } = await _sp.parseLoginResponse(_idp, "post", { body: { SAMLResponse: body } });
37
+ const mapping = config.attributeMapping ?? {};
38
+ const attrs = extract.attributes ?? {};
39
+ const emailKey = mapping.email ?? "email";
40
+ const firstNameKey = mapping.firstName ?? "firstName";
41
+ const lastNameKey = mapping.lastName ?? "lastName";
42
+ const groupsKey = mapping.groups ?? "groups";
43
+ const nameId = extract.nameID;
44
+ const email = attrs[emailKey] ?? nameId;
45
+ const firstName = attrs[firstNameKey];
46
+ const lastName = attrs[lastNameKey];
47
+ const displayName = firstName && lastName ? `${firstName} ${lastName}` : undefined;
48
+ const rawGroups = attrs[groupsKey];
49
+ const groups = rawGroups ? (Array.isArray(rawGroups) ? rawGroups : [rawGroups]) : undefined;
50
+ return { nameId, email, firstName, lastName, displayName, groups, attributes: attrs };
51
+ }
52
+ export function samlProfileToIdentityProfile(profile) {
53
+ return {
54
+ email: profile.email,
55
+ displayName: profile.displayName,
56
+ firstName: profile.firstName,
57
+ lastName: profile.lastName,
58
+ };
59
+ }
60
+ export function getSamlSpMetadata() {
61
+ if (!_sp)
62
+ throw new Error("SAML not initialized");
63
+ return _sp.getMetadata();
64
+ }
@@ -0,0 +1,44 @@
1
+ import type { UserRecord } from "./authAdapter";
2
+ export interface ScimUser {
3
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"];
4
+ id: string;
5
+ externalId?: string;
6
+ userName: string;
7
+ displayName?: string;
8
+ name?: {
9
+ givenName?: string;
10
+ familyName?: string;
11
+ formatted?: string;
12
+ };
13
+ emails?: Array<{
14
+ value: string;
15
+ primary: boolean;
16
+ }>;
17
+ active: boolean;
18
+ meta: {
19
+ resourceType: "User";
20
+ created?: string;
21
+ lastModified?: string;
22
+ };
23
+ }
24
+ export interface ScimListResponse {
25
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
26
+ totalResults: number;
27
+ startIndex: number;
28
+ itemsPerPage: number;
29
+ Resources: ScimUser[];
30
+ }
31
+ export interface ScimError {
32
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"];
33
+ status: string;
34
+ detail: string;
35
+ }
36
+ export declare function userRecordToScim(user: UserRecord, config?: {
37
+ userName?: "email" | "username";
38
+ }): ScimUser;
39
+ /**
40
+ * Parse a simple SCIM filter string into a UserQuery object.
41
+ * Supports: userName eq "val", email eq "val", externalId eq "val", active eq true/false
42
+ */
43
+ export declare function parseScimFilter(filter?: string): import("./authAdapter").UserQuery;
44
+ export declare function scimError(status: number, detail: string): Response;
@@ -0,0 +1,54 @@
1
+ export function userRecordToScim(user, config) {
2
+ const userName = user.email ?? user.id;
3
+ return {
4
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
5
+ id: user.id,
6
+ externalId: user.externalId,
7
+ userName,
8
+ displayName: user.displayName,
9
+ name: (user.firstName || user.lastName) ? {
10
+ givenName: user.firstName,
11
+ familyName: user.lastName,
12
+ formatted: [user.firstName, user.lastName].filter(Boolean).join(" ") || undefined,
13
+ } : undefined,
14
+ emails: user.email ? [{ value: user.email, primary: true }] : undefined,
15
+ active: !user.suspended,
16
+ meta: { resourceType: "User" },
17
+ };
18
+ }
19
+ /**
20
+ * Parse a simple SCIM filter string into a UserQuery object.
21
+ * Supports: userName eq "val", email eq "val", externalId eq "val", active eq true/false
22
+ */
23
+ export function parseScimFilter(filter) {
24
+ if (!filter)
25
+ return {};
26
+ const query = {};
27
+ // Simple single-clause filter: `attr op "value"`
28
+ const match = filter.trim().match(/^(\w+)\s+eq\s+"?([^"]*)"?$/i);
29
+ if (!match)
30
+ return {};
31
+ const [, attr, value] = match;
32
+ const attrLower = attr.toLowerCase();
33
+ if (attrLower === "username" || attrLower === "email") {
34
+ query.email = value;
35
+ }
36
+ else if (attrLower === "externalid") {
37
+ query.externalId = value;
38
+ }
39
+ else if (attrLower === "active") {
40
+ query.suspended = value.toLowerCase() !== "true"; // active=true means suspended=false
41
+ }
42
+ return query;
43
+ }
44
+ export function scimError(status, detail) {
45
+ const body = {
46
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
47
+ status: String(status),
48
+ detail,
49
+ };
50
+ return new Response(JSON.stringify(body), {
51
+ status,
52
+ headers: { "Content-Type": "application/scim+json" },
53
+ });
54
+ }
@@ -0,0 +1,28 @@
1
+ export type SecurityEventType = "auth.login.success" | "auth.login.failure" | "auth.login.blocked" | "auth.register.success" | "auth.register.failure" | "auth.logout" | "auth.password.reset" | "auth.password.change" | "auth.mfa.setup" | "auth.mfa.verify.success" | "auth.mfa.verify.failure" | "auth.step_up.success" | "auth.step_up.failure" | "auth.session.created" | "auth.session.revoked" | "auth.account.suspended" | "auth.account.deleted" | "auth.oauth.link" | "auth.oauth.unlink" | "security.rate_limit.exceeded" | "security.credential_stuffing.detected" | "security.captcha.failed" | "security.csrf.failed" | "security.breached_password.detected" | "admin.role.changed" | "admin.user.modified";
2
+ export interface SecurityEvent {
3
+ eventType: SecurityEventType;
4
+ severity: "info" | "warn" | "critical";
5
+ timestamp: string;
6
+ requestId?: string;
7
+ userId?: string;
8
+ sessionId?: string;
9
+ tenantId?: string;
10
+ ip?: string;
11
+ userAgent?: string;
12
+ meta?: Record<string, unknown>;
13
+ }
14
+ export interface SecurityEventConfig {
15
+ /** Called for each security event. Non-blocking — errors are swallowed. */
16
+ onEvent: (event: SecurityEvent) => void | Promise<void>;
17
+ /** Only emit events of these types. If omitted, all events are emitted. */
18
+ include?: SecurityEventType[];
19
+ /** Skip events of these types. Applied after include. */
20
+ exclude?: SecurityEventType[];
21
+ }
22
+ export declare function setSecurityEventConfig(config: SecurityEventConfig | null): void;
23
+ export declare function getSecurityEventConfig(): SecurityEventConfig | null;
24
+ /**
25
+ * Emit a security event. Non-blocking — the call returns immediately.
26
+ * If onEvent throws, the error is silently swallowed (never propagates to the caller).
27
+ */
28
+ export declare function emitSecurityEvent(event: SecurityEvent): void;
@@ -0,0 +1,26 @@
1
+ let _config = null;
2
+ export function setSecurityEventConfig(config) {
3
+ _config = config;
4
+ }
5
+ export function getSecurityEventConfig() {
6
+ return _config;
7
+ }
8
+ /**
9
+ * Emit a security event. Non-blocking — the call returns immediately.
10
+ * If onEvent throws, the error is silently swallowed (never propagates to the caller).
11
+ */
12
+ export function emitSecurityEvent(event) {
13
+ if (!_config)
14
+ return;
15
+ const { onEvent, include, exclude } = _config;
16
+ if (include && include.length > 0 && !include.includes(event.eventType))
17
+ return;
18
+ if (exclude && exclude.includes(event.eventType))
19
+ return;
20
+ // Fire and forget — never block the request
21
+ Promise.resolve()
22
+ .then(() => onEvent({ ...event, timestamp: event.timestamp ?? new Date().toISOString() }))
23
+ .catch(() => {
24
+ // Swallow errors — security event emission must never crash the app
25
+ });
26
+ }
@@ -32,4 +32,18 @@ export declare const setRefreshToken: (sessionId: string, refreshToken: string)
32
32
  export declare const getSessionByRefreshToken: (refreshToken: string) => Promise<RefreshResult | null>;
33
33
  /** Rotate the refresh token: move current to prev with grace window, set new token + access token. */
34
34
  export declare const rotateRefreshToken: (sessionId: string, newRefreshToken: string, newAccessToken: string) => Promise<void>;
35
+ /** Read the stored fingerprint for a session. Returns null if not yet set. */
36
+ export declare const getSessionFingerprint: (sessionId: string) => Promise<string | null>;
37
+ /** Store a fingerprint on an existing session. No-op if the session does not exist. */
38
+ export declare const setSessionFingerprint: (sessionId: string, fingerprint: string) => Promise<void>;
39
+ /**
40
+ * Store the timestamp when MFA was last verified in the session metadata.
41
+ * Used by requireStepUp middleware.
42
+ */
43
+ export declare const setMfaVerifiedAt: (sessionId: string) => Promise<void>;
44
+ /**
45
+ * Get the Unix timestamp (seconds) when MFA was last verified for this session.
46
+ * Returns null if MFA has never been verified or session not found.
47
+ */
48
+ export declare const getMfaVerifiedAt: (sessionId: string) => Promise<number | null>;
35
49
  export {};