@getstrata/core 1.0.9 → 1.1.1

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 (90) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/core/auth/authContext.d.ts +5 -2
  3. package/dist/core/auth/emailVerification.d.ts +1 -1
  4. package/dist/core/auth/guard.d.ts +5 -1
  5. package/dist/core/auth/jwtGuard.d.ts +6 -2
  6. package/dist/core/auth/oauth/oidcIdToken.d.ts +19 -0
  7. package/dist/core/auth/oauth/oidcProvider.d.ts +16 -4
  8. package/dist/core/auth/oauth/samlProvider.d.ts +7 -4
  9. package/dist/core/auth/oneTimeToken.d.ts +19 -0
  10. package/dist/core/auth/passwordLogin.d.ts +20 -0
  11. package/dist/core/auth/saml/samlAssertionReplay.d.ts +20 -0
  12. package/dist/core/auth/saml/samlServiceProvider.d.ts +51 -0
  13. package/dist/core/contracts/authUserDirectory.d.ts +2 -3
  14. package/dist/core/database/errors.d.ts +2 -1
  15. package/dist/core/http/response.d.ts +3 -1
  16. package/dist/core/mail/mailer.d.ts +6 -1
  17. package/dist/core/security/oauthState.d.ts +6 -2
  18. package/dist/core/security/safeFetch.d.ts +1 -0
  19. package/dist/core/security/safePath.d.ts +4 -0
  20. package/dist/core/security/safeUrl.d.ts +14 -1
  21. package/dist/core/security/totp.d.ts +1 -1
  22. package/dist/core/tenant/databaseTenantContext.d.ts +2 -1
  23. package/dist/core/tenant/enableTenantRls.d.ts +6 -0
  24. package/dist/core/tenant/postgresAppRole.d.ts +75 -0
  25. package/dist/entries/audit/exportAuditLogs.js +223 -40
  26. package/dist/entries/auth/accessControl.js +1 -0
  27. package/dist/entries/auth/basicAuthGuard.js +638 -17
  28. package/dist/entries/auth/emailVerification.js +1 -1
  29. package/dist/entries/auth/intendedUrlCookie.js +32 -2
  30. package/dist/entries/auth/jwt.js +1 -1
  31. package/dist/entries/auth/jwtGuard.js +192 -3
  32. package/dist/entries/auth/membershipMiddleware.js +1 -0
  33. package/dist/entries/auth/membershipScope.js +1 -0
  34. package/dist/entries/auth/membershipService.js +1 -0
  35. package/dist/entries/auth/oauth/oidcProvider.js +542 -20
  36. package/dist/entries/auth/oauth/providers.js +313 -5
  37. package/dist/entries/auth/oauth/samlProvider.js +552 -74
  38. package/dist/entries/auth/oneTimeToken.js +362 -0
  39. package/dist/entries/auth/password.js +1 -1
  40. package/dist/entries/auth/passwordLogin.js +548 -0
  41. package/dist/entries/auth/policy.js +1 -0
  42. package/dist/entries/auth/saml/samlServiceProvider.js +571 -0
  43. package/dist/entries/auth/scimAuthMiddleware.js +10 -3
  44. package/dist/entries/auth/sessionCookie.js +1 -1
  45. package/dist/entries/auth/sessionGuard.js +1 -1
  46. package/dist/entries/auth/tokenHash.js +2 -6
  47. package/dist/entries/crypto/fieldEncryption.js +1 -1
  48. package/dist/entries/crypto/mfaSecret.js +31 -5
  49. package/dist/entries/database/errors.js +29 -12
  50. package/dist/entries/database/model.js +9 -9
  51. package/dist/entries/facades.js +72 -9
  52. package/dist/entries/http/authMiddleware.js +7 -14
  53. package/dist/entries/http/corsMiddleware.js +69 -2
  54. package/dist/entries/http/csrfMiddleware.js +33 -43
  55. package/dist/entries/http/csrfToken.js +2 -2
  56. package/dist/entries/http/flashMiddleware.js +4 -2
  57. package/dist/entries/http/flashSession.js +4 -2
  58. package/dist/entries/http/formRequest.js +2 -2
  59. package/dist/entries/http/memoryThrottleMiddleware.js +63 -0
  60. package/dist/entries/http/pagination.js +2 -2
  61. package/dist/entries/http/requirePasswordConfirmMiddleware.js +36 -3
  62. package/dist/entries/http/requireWebAuthMiddleware.js +32 -2
  63. package/dist/entries/http/response.js +101 -45
  64. package/dist/entries/http/routeModelBinding.js +2 -2
  65. package/dist/entries/http/safeInternalPath.js +32 -2
  66. package/dist/entries/http/scimThrottleMiddleware.js +7 -1
  67. package/dist/entries/http/securedRouteModelBinding.js +2 -2
  68. package/dist/entries/http/securityHeadersMiddleware.js +3 -64
  69. package/dist/entries/http/signedUrl.js +6 -5
  70. package/dist/entries/http/throttleMiddleware.js +63 -0
  71. package/dist/entries/http/validation.js +2 -2
  72. package/dist/entries/http/webFormRequest.js +2 -2
  73. package/dist/entries/jobs/exportAuditLogsJob.js +223 -40
  74. package/dist/entries/mail/mailer.js +54 -8
  75. package/dist/entries/media/imageTransform.js +12 -1
  76. package/dist/entries/openapi/generator.js +0 -7
  77. package/dist/entries/security/oauthState.js +30 -39
  78. package/dist/entries/security/publicReads.js +1 -1
  79. package/dist/entries/security/recoveryCodes.js +2 -2
  80. package/dist/entries/security/safeFetch.js +195 -94
  81. package/dist/entries/security/safePath.js +25 -0
  82. package/dist/entries/security/scimTenantTokens.js +10 -3
  83. package/dist/entries/security/totp.js +16 -4
  84. package/dist/entries/storage/storage.js +24 -2
  85. package/dist/entries/tenant/databaseTenantContext.js +34 -12
  86. package/dist/entries/tenant/enableTenantRls.js +385 -0
  87. package/dist/entries/tracing/tracingMiddleware.js +308 -5
  88. package/dist/framework/public-api.d.ts +10 -3
  89. package/dist/index.js +2767 -1515
  90. package/package.json +32 -3
@@ -0,0 +1,362 @@
1
+ // @bun
2
+ // ../../src/core/auth/oneTimeToken.ts
3
+ import { randomBytes } from "crypto";
4
+
5
+ // ../../src/core/runtime/asyncContextStore.ts
6
+ import { AsyncLocalStorage } from "async_hooks";
7
+ function createAsyncContextStore(key) {
8
+ const symbol = Symbol.for(key);
9
+ const globalRecord = globalThis;
10
+ const existing = globalRecord[symbol];
11
+ if (existing) {
12
+ return existing;
13
+ }
14
+ const store = new AsyncLocalStorage;
15
+ globalRecord[symbol] = store;
16
+ return store;
17
+ }
18
+
19
+ // ../../src/core/database/schema/driver.ts
20
+ function normalizeConnectionName(connection) {
21
+ const normalized = connection.trim().toLowerCase();
22
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
23
+ return "pgsql";
24
+ }
25
+ if (normalized === "mysql" || normalized === "mariadb") {
26
+ return "mysql";
27
+ }
28
+ if (normalized === "sqlite") {
29
+ return "sqlite";
30
+ }
31
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
32
+ }
33
+ function resolveDriverFromUrl(url) {
34
+ const normalized = url.trim().toLowerCase();
35
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
36
+ return "pgsql";
37
+ }
38
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
39
+ return "mysql";
40
+ }
41
+ if (normalized.startsWith("sqlite:")) {
42
+ return "sqlite";
43
+ }
44
+ return null;
45
+ }
46
+ function resolveDatabaseDriver(options = {}) {
47
+ const connection = options.connection ?? process.env.DB_CONNECTION;
48
+ if (connection) {
49
+ return normalizeConnectionName(connection);
50
+ }
51
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
52
+ const fromUrl = resolveDriverFromUrl(url);
53
+ if (fromUrl) {
54
+ return fromUrl;
55
+ }
56
+ return "pgsql";
57
+ }
58
+
59
+ // ../../src/core/database/dialect.ts
60
+ function assertSafeIdentifier(identifier) {
61
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
62
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
63
+ }
64
+ return identifier;
65
+ }
66
+ function quoteIdentifierFor(dialect, column) {
67
+ return dialect.quoteIdentifier(column);
68
+ }
69
+ var postgresDialect = {
70
+ driver: "pgsql",
71
+ placeholder(index) {
72
+ return `$${index}`;
73
+ },
74
+ quoteIdentifier(identifier) {
75
+ return `"${assertSafeIdentifier(identifier)}"`;
76
+ },
77
+ nowExpression() {
78
+ return "NOW()";
79
+ },
80
+ timestampValue(value) {
81
+ return value.toISOString();
82
+ },
83
+ returningClause(columns) {
84
+ return ` RETURNING ${columns}`;
85
+ },
86
+ ilikeOperator() {
87
+ return "ILIKE";
88
+ },
89
+ nullsLastSuffix() {
90
+ return " NULLS LAST";
91
+ },
92
+ castToText(expression) {
93
+ return `${expression}::text`;
94
+ },
95
+ upsertSuffix(conflictColumns, updateColumns) {
96
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
97
+ if (updateColumns.length === 0) {
98
+ return ` ON CONFLICT (${target}) DO NOTHING`;
99
+ }
100
+ const assignments = updateColumns.map((column) => {
101
+ const quoted = quoteIdentifierFor(this, column);
102
+ return `${quoted} = excluded.${quoted}`;
103
+ }).join(", ");
104
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
105
+ }
106
+ };
107
+ var mysqlDialect = {
108
+ driver: "mysql",
109
+ placeholder() {
110
+ return "?";
111
+ },
112
+ quoteIdentifier(identifier) {
113
+ return `\`${assertSafeIdentifier(identifier)}\``;
114
+ },
115
+ nowExpression() {
116
+ return "CURRENT_TIMESTAMP";
117
+ },
118
+ timestampValue(value) {
119
+ return value.toISOString().slice(0, 19).replace("T", " ");
120
+ },
121
+ returningClause() {
122
+ return "";
123
+ },
124
+ ilikeOperator() {
125
+ return "LIKE";
126
+ },
127
+ nullsLastSuffix() {
128
+ return "";
129
+ },
130
+ castToText(expression) {
131
+ return `CAST(${expression} AS CHAR)`;
132
+ },
133
+ upsertSuffix(conflictColumns, updateColumns) {
134
+ if (updateColumns.length === 0) {
135
+ const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
136
+ return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
137
+ }
138
+ const assignments = updateColumns.map((column) => {
139
+ const quoted = quoteIdentifierFor(this, column);
140
+ return `${quoted} = VALUES(${quoted})`;
141
+ }).join(", ");
142
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
143
+ }
144
+ };
145
+ var sqliteDialect = {
146
+ driver: "sqlite",
147
+ placeholder() {
148
+ return "?";
149
+ },
150
+ quoteIdentifier(identifier) {
151
+ return `"${assertSafeIdentifier(identifier)}"`;
152
+ },
153
+ nowExpression() {
154
+ return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
155
+ },
156
+ timestampValue(value) {
157
+ return value.toISOString();
158
+ },
159
+ returningClause(columns) {
160
+ return ` RETURNING ${columns}`;
161
+ },
162
+ ilikeOperator() {
163
+ return "LIKE";
164
+ },
165
+ nullsLastSuffix() {
166
+ return "";
167
+ },
168
+ castToText(expression) {
169
+ return `CAST(${expression} AS TEXT)`;
170
+ },
171
+ upsertSuffix(conflictColumns, updateColumns) {
172
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
173
+ if (updateColumns.length === 0) {
174
+ return ` ON CONFLICT (${target}) DO NOTHING`;
175
+ }
176
+ const assignments = updateColumns.map((column) => {
177
+ const quoted = quoteIdentifierFor(this, column);
178
+ return `${quoted} = excluded.${quoted}`;
179
+ }).join(", ");
180
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
181
+ }
182
+ };
183
+ var dialects = {
184
+ pgsql: postgresDialect,
185
+ mysql: mysqlDialect,
186
+ sqlite: sqliteDialect
187
+ };
188
+ var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
189
+ var dialectOverride = null;
190
+ function dialectFor(driver) {
191
+ return dialects[driver];
192
+ }
193
+ function currentSqlDialect() {
194
+ return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
195
+ }
196
+
197
+ // ../../src/core/auth/tokenHash.ts
198
+ import { createHmac } from "crypto";
199
+
200
+ // ../../src/core/runtime/appEnv.ts
201
+ var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
202
+ function normalizeEnvValue(value) {
203
+ return (value ?? "").trim().toLowerCase();
204
+ }
205
+ function isProductionEnv(env = process.env) {
206
+ const appEnv = normalizeEnvValue(env.APP_ENV);
207
+ const nodeEnv = normalizeEnvValue(env.NODE_ENV);
208
+ if (appEnv === "production" || nodeEnv === "production") {
209
+ return true;
210
+ }
211
+ if (appEnv === "") {
212
+ return false;
213
+ }
214
+ return !NON_PRODUCTION_APP_ENVS.has(appEnv);
215
+ }
216
+ function envFlagEnabled(value) {
217
+ return value === "true";
218
+ }
219
+
220
+ // ../../src/core/runtime/appKeyPrefix.ts
221
+ function appKeyPrefix() {
222
+ return process.env.APP_KEY_PREFIX?.trim() || "strata";
223
+ }
224
+ function appCookieName(kind) {
225
+ return `${appKeyPrefix()}_${kind}`;
226
+ }
227
+ function appDevSecret(kind) {
228
+ return `${appKeyPrefix()}-dev-${kind}`;
229
+ }
230
+ function requireConfiguredSecret(names, devKind, env = process.env) {
231
+ for (const name of names) {
232
+ const value = env[name]?.trim();
233
+ if (value) {
234
+ return value;
235
+ }
236
+ }
237
+ if (isProductionEnv(env)) {
238
+ throw new Error(`${names[0]} must be set outside development. Do not derive secrets from the app name.`);
239
+ }
240
+ return appDevSecret(devKind);
241
+ }
242
+ function namespacedRedisKey(kind) {
243
+ return `${appKeyPrefix()}:${kind}`;
244
+ }
245
+ function smtpEhloHost() {
246
+ const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
247
+ const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
248
+ return safe || "strata.local";
249
+ }
250
+ function siemEventType() {
251
+ return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
252
+ }
253
+ function appUserAgent() {
254
+ return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
255
+ }
256
+ function otelServiceName() {
257
+ return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
258
+ }
259
+ function appDisplayName() {
260
+ return process.env.APP_NAME?.trim() || "Strata";
261
+ }
262
+ function appEnv() {
263
+ return process.env.APP_ENV?.trim() || "local";
264
+ }
265
+ function appUrl() {
266
+ return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
267
+ }
268
+ function apiPrefix() {
269
+ const raw = process.env.API_PREFIX?.trim() || "/api/v1";
270
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
271
+ const trimmed = withSlash.replace(/\/+$/, "");
272
+ return trimmed || "/api/v1";
273
+ }
274
+ function sdkClientClassName() {
275
+ const override = process.env.APP_SDK_CLASS?.trim();
276
+ if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
277
+ return override;
278
+ }
279
+ const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
280
+ return fromName ? `${fromName}Client` : "AppClient";
281
+ }
282
+
283
+ // ../../src/core/auth/tokenHash.ts
284
+ function resolveTokenPepper() {
285
+ return requireConfiguredSecret(["TOKEN_HASH_PEPPER"], "token-pepper");
286
+ }
287
+ function hashApiToken(token) {
288
+ return createHmac("sha256", resolveTokenPepper()).update(token).digest("hex");
289
+ }
290
+
291
+ // ../../src/core/auth/oneTimeToken.ts
292
+ var AUTH_ONE_TIME_PURPOSES = {
293
+ passwordReset: "password_reset",
294
+ emailVerify: "email_verify"
295
+ };
296
+ function generateOneTimeToken() {
297
+ const plain = randomBytes(32).toString("hex");
298
+ return { plain, hash: hashApiToken(plain) };
299
+ }
300
+ function hashOneTimeToken(plain) {
301
+ return hashApiToken(plain.trim());
302
+ }
303
+ function isMissingRelation(error) {
304
+ const message = error instanceof Error ? error.message : String(error);
305
+ return /no such table|does not exist|unknown table|er_no_such_table/i.test(message);
306
+ }
307
+ async function consumeOneTimeToken(sql, purpose, plain, now = new Date) {
308
+ const token = plain.trim();
309
+ if (!token) {
310
+ return null;
311
+ }
312
+ const dialect = currentSqlDialect();
313
+ const consumedAt = now.toISOString();
314
+ const hash = hashOneTimeToken(token);
315
+ const returning = dialect.returningClause("user_id");
316
+ const updated = await sql.unsafe(`UPDATE auth_one_time_tokens
317
+ SET consumed_at = ${dialect.placeholder(1)}
318
+ WHERE purpose = ${dialect.placeholder(2)}
319
+ AND token_hash = ${dialect.placeholder(3)}
320
+ AND consumed_at IS NULL
321
+ AND expires_at > ${dialect.placeholder(4)}${returning}`, [consumedAt, purpose, hash, consumedAt]);
322
+ if (returning) {
323
+ const userId = updated[0]?.user_id;
324
+ return userId == null ? null : Number(userId);
325
+ }
326
+ const header = updated[0];
327
+ const affected = Number(header?.affectedRows ?? header?.changes ?? 0);
328
+ if (affected !== 1) {
329
+ return null;
330
+ }
331
+ const rows = await sql.unsafe(`SELECT user_id FROM auth_one_time_tokens
332
+ WHERE purpose = ${dialect.placeholder(1)} AND token_hash = ${dialect.placeholder(2)}`, [purpose, hash]);
333
+ return rows[0] ? Number(rows[0].user_id) : null;
334
+ }
335
+ async function revokeUserSessions(sql, userId, options = {}) {
336
+ const dialect = currentSqlDialect();
337
+ await sql.unsafe(`UPDATE users SET session_valid_after = ${dialect.placeholder(1)} WHERE id = ${dialect.placeholder(2)}`, [new Date().toISOString(), userId]);
338
+ try {
339
+ await sql.unsafe(`DELETE FROM sessions WHERE user_id = ${dialect.placeholder(1)}`, [userId]);
340
+ } catch (error) {
341
+ if (!isMissingRelation(error)) {
342
+ throw error;
343
+ }
344
+ }
345
+ if (options.revokeApiTokens === false) {
346
+ return;
347
+ }
348
+ try {
349
+ await sql.unsafe(`DELETE FROM api_tokens WHERE user_id = ${dialect.placeholder(1)}`, [userId]);
350
+ } catch (error) {
351
+ if (!isMissingRelation(error)) {
352
+ throw error;
353
+ }
354
+ }
355
+ }
356
+ export {
357
+ AUTH_ONE_TIME_PURPOSES,
358
+ consumeOneTimeToken,
359
+ generateOneTimeToken,
360
+ hashOneTimeToken,
361
+ revokeUserSessions
362
+ };
@@ -3,7 +3,7 @@
3
3
  async function hashPassword(password) {
4
4
  return await Bun.password.hash(password, {
5
5
  algorithm: "bcrypt",
6
- cost: 10
6
+ cost: 12
7
7
  });
8
8
  }
9
9
  async function verifyPassword(password, passwordHash) {