@getstrata/core 0.5.43 → 0.5.45

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 (40) hide show
  1. package/dist/entries/admin/types.js +1 -0
  2. package/dist/entries/auth/abilityChecker.js +1 -0
  3. package/dist/entries/auth/membershipMiddleware.js +298 -0
  4. package/dist/entries/auth/scimAuthMiddleware.js +10 -0
  5. package/dist/entries/auth/sessionGuard.js +14 -0
  6. package/dist/entries/database/bindConnection.js +1 -22
  7. package/dist/entries/database/boundConnection.js +1 -19
  8. package/dist/entries/database/connection.js +1 -12
  9. package/dist/entries/database/defaultConnection.js +1 -0
  10. package/dist/entries/database/migrations/types.js +1 -0
  11. package/dist/entries/database/migrations.js +127 -0
  12. package/dist/entries/database/repositoryConnection.js +1 -0
  13. package/dist/entries/database/seeders/types.js +1 -0
  14. package/dist/entries/http/conditionalResponse.js +192 -0
  15. package/dist/entries/http/corsMiddleware.js +54 -0
  16. package/dist/entries/http/csrfMiddleware.js +236 -0
  17. package/dist/entries/http/csrfToken.js +3 -0
  18. package/dist/entries/http/flashMiddleware.js +143 -0
  19. package/dist/entries/http/formRequest.js +158 -0
  20. package/dist/entries/http/loginThrottleMiddleware.js +46 -0
  21. package/dist/entries/http/memoryThrottleMiddleware.js +30 -0
  22. package/dist/entries/http/pagination.js +185 -0
  23. package/dist/entries/http/requireAbilityMiddleware.js +110 -0
  24. package/dist/entries/http/requireAuthMiddleware.js +80 -0
  25. package/dist/entries/http/requireGlobalAdminMiddleware.js +147 -0
  26. package/dist/entries/http/requireWebAuthMiddleware.js +107 -0
  27. package/dist/entries/http/route.js +8 -0
  28. package/dist/entries/http/routeMiddleware.js +32 -0
  29. package/dist/entries/http/routeModelBinding.js +147 -0
  30. package/dist/entries/http/scimThrottleMiddleware.js +23 -0
  31. package/dist/entries/http/securedRouteModelBinding.js +16 -0
  32. package/dist/entries/http/securityHeadersMiddleware.js +77 -0
  33. package/dist/entries/http/throttleMiddleware.js +87 -0
  34. package/dist/entries/http/webErrorResponse.js +14 -0
  35. package/dist/entries/http/webFormRequest.js +16 -0
  36. package/dist/entries/logging/requestLoggingMiddleware.js +89 -0
  37. package/dist/entries/tenant/tenantDatabaseScope.js +10 -0
  38. package/dist/entries/tracing/tracingMiddleware.js +103 -0
  39. package/dist/entries/view.js +14 -0
  40. package/package.json +147 -7
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,298 @@
1
+ // @bun
2
+ // ../../src/config/database.ts
3
+ function readInteger(name, fallback) {
4
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
5
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
6
+ }
7
+ var databaseConfig = {
8
+ url: process.env.DATABASE_URL ?? "",
9
+ poolMax: readInteger("DB_POOL_MAX", 10),
10
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
11
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
12
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
13
+ };
14
+
15
+ // ../../src/core/runtime/asyncContextStore.ts
16
+ import { AsyncLocalStorage } from "async_hooks";
17
+ function createAsyncContextStore(key) {
18
+ const symbol = Symbol.for(key);
19
+ const globalRecord = globalThis;
20
+ const existing = globalRecord[symbol];
21
+ if (existing) {
22
+ return existing;
23
+ }
24
+ const store = new AsyncLocalStorage;
25
+ globalRecord[symbol] = store;
26
+ return store;
27
+ }
28
+
29
+ // ../../src/core/database/connectionContext.ts
30
+ var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
31
+ function runWithDatabaseConnection(connection, callback) {
32
+ return activeConnection.run(connection, callback);
33
+ }
34
+ function getActiveDatabaseConnection(fallback) {
35
+ return activeConnection.getStore() ?? fallback;
36
+ }
37
+ function hasActiveDatabaseConnection() {
38
+ return activeConnection.getStore() !== undefined;
39
+ }
40
+
41
+ // ../../src/core/database/queryProxy.ts
42
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
43
+ function createDatabaseQueryProxy(pool) {
44
+ function resolveDatabase() {
45
+ return getActiveDatabaseConnection(pool);
46
+ }
47
+ function resolveDatabaseForProperty(property) {
48
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
49
+ return pool;
50
+ }
51
+ return resolveDatabase();
52
+ }
53
+ return new Proxy(function database() {}, {
54
+ apply(_target, _thisArg, args) {
55
+ return resolveDatabase()(...args);
56
+ },
57
+ get(_target, property) {
58
+ const connection = resolveDatabaseForProperty(property);
59
+ const value = connection[property];
60
+ return typeof value === "function" ? value.bind(connection) : value;
61
+ }
62
+ });
63
+ }
64
+
65
+ // ../../src/core/database/defaultConnection.ts
66
+ var defaultPool = {
67
+ connection: null
68
+ };
69
+ var defaultQuery = {
70
+ connection: null
71
+ };
72
+ function registerDefaultDatabasePool(connection) {
73
+ defaultPool.connection = connection;
74
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
75
+ }
76
+ function getDefaultDatabasePool() {
77
+ if (!defaultPool.connection) {
78
+ throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
79
+ }
80
+ return defaultPool.connection;
81
+ }
82
+ function getDefaultDatabaseQuery() {
83
+ if (!defaultQuery.connection) {
84
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
85
+ }
86
+ return defaultQuery.connection;
87
+ }
88
+
89
+ // ../../src/db/connection/createConnection.ts
90
+ var {SQL } = globalThis.Bun;
91
+ function createDatabaseConnection(config) {
92
+ if (!config.url) {
93
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
94
+ }
95
+ return new SQL({
96
+ url: config.url,
97
+ max: config.poolMax,
98
+ idleTimeout: config.idleTimeoutSeconds,
99
+ maxLifetime: config.maxLifetimeSeconds,
100
+ connectionTimeout: config.connectionTimeoutSeconds
101
+ });
102
+ }
103
+
104
+ // ../../src/db/connection/index.ts
105
+ var connectionHolder = {
106
+ connection: null
107
+ };
108
+ function getDatabase() {
109
+ if (!connectionHolder.connection) {
110
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
111
+ registerDefaultDatabasePool(connectionHolder.connection);
112
+ }
113
+ return connectionHolder.connection;
114
+ }
115
+ function getDb() {
116
+ getDatabase();
117
+ return getDefaultDatabaseQuery();
118
+ }
119
+ var db = new Proxy(function database() {}, {
120
+ apply(_target, _thisArg, args) {
121
+ return getDb()(...args);
122
+ },
123
+ get(_target, property) {
124
+ const connection = getDb();
125
+ const value = connection[property];
126
+ return typeof value === "function" ? value.bind(connection) : value;
127
+ }
128
+ });
129
+ var connection_default = db;
130
+
131
+ // ../../src/modules/organization/memberRepository.ts
132
+ class OrganizationMemberRepository {
133
+ constructor() {}
134
+ async findMembership(userId, organizationId) {
135
+ const rows = await connection_default`
136
+ SELECT id, organization_id, user_id, role, created_at
137
+ FROM organization_member
138
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
139
+ LIMIT 1
140
+ `;
141
+ return rows[0] ?? null;
142
+ }
143
+ async listForUser(userId) {
144
+ return await connection_default`
145
+ SELECT id, organization_id, user_id, role, created_at
146
+ FROM organization_member
147
+ WHERE user_id = ${userId}
148
+ ORDER BY organization_id
149
+ `;
150
+ }
151
+ async listForOrganization(organizationId) {
152
+ return await connection_default`
153
+ SELECT id, organization_id, user_id, role, created_at
154
+ FROM organization_member
155
+ WHERE organization_id = ${organizationId}
156
+ ORDER BY id
157
+ `;
158
+ }
159
+ async addMember(input) {
160
+ const rows = await connection_default`
161
+ INSERT INTO organization_member (organization_id, user_id, role)
162
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
163
+ RETURNING id, organization_id, user_id, role, created_at
164
+ `;
165
+ const row = rows[0];
166
+ if (!row) {
167
+ throw new Error("Organization member insert did not return a row.");
168
+ }
169
+ return row;
170
+ }
171
+ async removeMember(organizationId, userId) {
172
+ const rows = await connection_default`
173
+ DELETE FROM organization_member
174
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
175
+ RETURNING id
176
+ `;
177
+ return rows.length > 0;
178
+ }
179
+ }
180
+ var memberRepository_default = OrganizationMemberRepository;
181
+
182
+ // ../../src/core/errors/http.ts
183
+ class HttpError extends Error {
184
+ status;
185
+ details;
186
+ constructor(status, message, details) {
187
+ super(message);
188
+ this.name = new.target.name;
189
+ this.status = status;
190
+ this.details = details;
191
+ }
192
+ }
193
+
194
+ class BadRequestError extends HttpError {
195
+ constructor(message = "Bad Request", details) {
196
+ super(400, message, details);
197
+ }
198
+ }
199
+
200
+ class NotFoundError extends HttpError {
201
+ constructor(message = "Not Found", details) {
202
+ super(404, message, details);
203
+ }
204
+ }
205
+
206
+ class ConflictError extends HttpError {
207
+ constructor(message = "Conflict", details) {
208
+ super(409, message, details);
209
+ }
210
+ }
211
+
212
+ class UnprocessableEntityError extends HttpError {
213
+ constructor(message = "Unprocessable Entity", details) {
214
+ super(422, message, details);
215
+ }
216
+ }
217
+
218
+ class ValidationError extends HttpError {
219
+ constructor(message = "Validation failed", details) {
220
+ super(422, message, details);
221
+ }
222
+ }
223
+
224
+ class ForbiddenError extends HttpError {
225
+ constructor(message = "Forbidden", details) {
226
+ super(403, message, details);
227
+ }
228
+ }
229
+
230
+ class UnauthorizedError extends HttpError {
231
+ constructor(message = "Unauthorized", details) {
232
+ super(401, message, details);
233
+ }
234
+ }
235
+
236
+ class PayloadTooLargeError extends HttpError {
237
+ constructor(message = "Payload Too Large", details) {
238
+ super(413, message, details);
239
+ }
240
+ }
241
+
242
+ class PreconditionFailedError extends HttpError {
243
+ constructor(message = "Precondition Failed", details) {
244
+ super(412, message, details);
245
+ }
246
+ }
247
+
248
+ // ../../src/core/auth/authContext.ts
249
+ var authContext = createAsyncContextStore("@getstrata/authContext");
250
+ function runWithAuthUser(user, callback) {
251
+ return authContext.run(user, callback);
252
+ }
253
+ function currentAuthUser() {
254
+ return authContext.getStore() ?? null;
255
+ }
256
+
257
+ // ../../src/core/auth/accessControl.ts
258
+ function isGlobalAdmin(user) {
259
+ return user?.role === "admin";
260
+ }
261
+ function resolveUserId(user) {
262
+ const userId = typeof user.id === "number" ? user.id : Number(user.id);
263
+ if (!Number.isInteger(userId) || userId <= 0) {
264
+ throw new ForbiddenError("Invalid authenticated user.");
265
+ }
266
+ return userId;
267
+ }
268
+
269
+ // ../../src/core/auth/membershipContext.ts
270
+ var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
271
+ var membershipRepository = new memberRepository_default;
272
+ async function runWithMembershipContext(callback) {
273
+ const user = currentAuthUser();
274
+ if (!user || isGlobalAdmin(user)) {
275
+ return await callback();
276
+ }
277
+ const memberships = await membershipRepository.listForUser(resolveUserId(user));
278
+ const context = {
279
+ organizationIds: memberships.map((membership) => membership.organization_id),
280
+ rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
281
+ };
282
+ return await membershipContext.run(context, callback);
283
+ }
284
+
285
+ // ../../src/core/auth/membershipContextMiddleware.ts
286
+ function createMembershipContextMiddleware() {
287
+ return async (_request, next) => {
288
+ return await runWithMembershipContext(async () => await next());
289
+ };
290
+ }
291
+
292
+ // ../../src/core/auth/membershipMiddleware.ts
293
+ function createMembershipMiddleware() {
294
+ return createMembershipContextMiddleware();
295
+ }
296
+ export {
297
+ createMembershipMiddleware
298
+ };
@@ -177,6 +177,16 @@ function currentTenant() {
177
177
  function currentTenantId() {
178
178
  return currentTenant()?.id ?? 1;
179
179
  }
180
+ function rateLimitMultiplierForPlan(plan) {
181
+ switch (plan) {
182
+ case "enterprise":
183
+ return 4;
184
+ case "pro":
185
+ return 2;
186
+ default:
187
+ return 1;
188
+ }
189
+ }
180
190
 
181
191
  // ../../src/core/tenant/tenantDatabaseScope.ts
182
192
  async function applyTenantContextToTransaction(transaction, tenantId) {
@@ -2603,6 +2603,7 @@ var db = new Proxy(function database() {}, {
2603
2603
  return typeof value === "function" ? value.bind(connection) : value;
2604
2604
  }
2605
2605
  });
2606
+ var connection_default = db;
2606
2607
 
2607
2608
  // ../../src/modules/user/apiTokenTable.ts
2608
2609
  var apiTokenTable = defineTable({
@@ -2744,6 +2745,9 @@ function currentAuthUser() {
2744
2745
 
2745
2746
  // ../../src/core/http/requestMetaContext.ts
2746
2747
  var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
2748
+ function runWithRequestMeta(meta, callback) {
2749
+ return requestMetaContext.run(meta, callback);
2750
+ }
2747
2751
  function currentRequestMeta() {
2748
2752
  return requestMetaContext.getStore() ?? {
2749
2753
  ipAddress: null,
@@ -2837,6 +2841,16 @@ function currentTenant() {
2837
2841
  function currentTenantId() {
2838
2842
  return currentTenant()?.id ?? 1;
2839
2843
  }
2844
+ function rateLimitMultiplierForPlan(plan) {
2845
+ switch (plan) {
2846
+ case "enterprise":
2847
+ return 4;
2848
+ case "pro":
2849
+ return 2;
2850
+ default:
2851
+ return 1;
2852
+ }
2853
+ }
2840
2854
 
2841
2855
  // ../../src/modules/user/authService.ts
2842
2856
  class AuthService {
@@ -1,22 +1 @@
1
- // @bun
2
- // ../../src/core/database/boundConnection.ts
3
- var boundConnectionHolder = {
4
- connection: null
5
- };
6
- function bindDatabaseConnection(connection) {
7
- boundConnectionHolder.connection = connection;
8
- }
9
- function getBoundDatabaseConnection() {
10
- return boundConnectionHolder.connection;
11
- }
12
- function resetBoundDatabaseConnection() {
13
- boundConnectionHolder.connection = null;
14
- }
15
-
16
- // ../../src/core/database/bindConnection.ts
17
- function bindDatabaseConnection2(connection) {
18
- bindDatabaseConnection(connection);
19
- }
20
- export {
21
- bindDatabaseConnection2 as bindDatabaseConnection
22
- };
1
+ export * from "../../index.js";
@@ -1,19 +1 @@
1
- // @bun
2
- // ../../src/core/database/boundConnection.ts
3
- var boundConnectionHolder = {
4
- connection: null
5
- };
6
- function bindDatabaseConnection(connection) {
7
- boundConnectionHolder.connection = connection;
8
- }
9
- function getBoundDatabaseConnection() {
10
- return boundConnectionHolder.connection;
11
- }
12
- function resetBoundDatabaseConnection() {
13
- boundConnectionHolder.connection = null;
14
- }
15
- export {
16
- resetBoundDatabaseConnection,
17
- getBoundDatabaseConnection,
18
- bindDatabaseConnection
19
- };
1
+ export * from "../../index.js";
@@ -1,12 +1 @@
1
- // @bun
2
- // ../../src/core/database/connection.ts
3
- function createDatabaseConnection(source) {
4
- return {
5
- async unsafe(query, params = []) {
6
- return await source.unsafe(query, params);
7
- }
8
- };
9
- }
10
- export {
11
- createDatabaseConnection
12
- };
1
+ export * from "../../index.js";
@@ -0,0 +1 @@
1
+ export * from "../../index.js";
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,127 @@
1
+ // @bun
2
+ // ../../src/core/database/migrations/runner.ts
3
+ import { readdir } from "fs/promises";
4
+ import { join } from "path";
5
+ import { pathToFileURL } from "url";
6
+
7
+ // ../../src/core/database/migrations/advisoryLock.ts
8
+ var MIGRATION_LOCK_KEY = 42424242;
9
+ async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
10
+ await db.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
11
+ try {
12
+ return await callback();
13
+ } finally {
14
+ await db.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
15
+ }
16
+ }
17
+
18
+ // ../../src/core/database/migrations/runner.ts
19
+ var MIGRATIONS_TABLE = "framework_migrations";
20
+ async function ensureMigrationsTable(db) {
21
+ await db.unsafe(`
22
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
23
+ name TEXT PRIMARY KEY,
24
+ batch INTEGER NOT NULL,
25
+ run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
26
+ )
27
+ `);
28
+ }
29
+ async function getAppliedMigrations(db) {
30
+ await ensureMigrationsTable(db);
31
+ return await db.unsafe(`
32
+ SELECT name, batch
33
+ FROM ${MIGRATIONS_TABLE}
34
+ ORDER BY batch ASC, name ASC
35
+ `);
36
+ }
37
+ async function loadMigrationsFromDirectory(directory) {
38
+ const entries = await readdir(directory);
39
+ const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
40
+ const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
41
+ const moduleUrl = pathToFileURL(join(directory, fileName)).href;
42
+ const module = await import(moduleUrl);
43
+ return module.default;
44
+ }));
45
+ return loadedMigrations.filter((migration) => migration?.name !== undefined);
46
+ }
47
+ async function getMigrationStatus(db, migrations) {
48
+ const applied = await getAppliedMigrations(db);
49
+ const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
50
+ return migrations.map(({ name }) => ({
51
+ name,
52
+ status: appliedByName.has(name) ? "up" : "pending",
53
+ batch: appliedByName.get(name) ?? null
54
+ }));
55
+ }
56
+ async function runPendingMigrations(db, migrations, options = {}) {
57
+ const applied = await getAppliedMigrations(db);
58
+ const appliedNames = new Set(applied.map(({ name }) => name));
59
+ const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
60
+ const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
61
+ for (const migration of pendingMigrations) {
62
+ options.onMigration?.(migration.name);
63
+ await migration.up(db);
64
+ const inserted = await db.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
65
+ if (inserted.length === 0) {
66
+ throw new Error(`Migration ${migration.name} was applied but not recorded.`);
67
+ }
68
+ }
69
+ return pendingMigrations.length;
70
+ }
71
+ async function migrateDatabase(db, migrations, options = {}) {
72
+ const { advisoryLock = false, onMigration } = options;
73
+ if (advisoryLock) {
74
+ return withMigrationLock(db, () => runPendingMigrations(db, migrations, { onMigration }));
75
+ }
76
+ return runPendingMigrations(db, migrations, { onMigration });
77
+ }
78
+ async function rollbackDatabase(db, migrations, options = {}) {
79
+ const applied = await getAppliedMigrations(db);
80
+ if (applied.length === 0) {
81
+ return 0;
82
+ }
83
+ const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
84
+ const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
85
+ let rolledBack = 0;
86
+ for (const migration of [...migrations].reverse()) {
87
+ if (!migrationsToRollback.has(migration.name)) {
88
+ continue;
89
+ }
90
+ options.onMigration?.(migration.name);
91
+ await migration.down(db);
92
+ await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
93
+ rolledBack += 1;
94
+ }
95
+ return rolledBack;
96
+ }
97
+ async function freshDatabase(db, migrations, options = {}) {
98
+ const runFresh = async () => {
99
+ const applied = await getAppliedMigrations(db);
100
+ const appliedNames = new Set(applied.map(({ name }) => name));
101
+ const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
102
+ for (const migration of [...appliedMigrations].reverse()) {
103
+ options.onMigration?.(migration.name);
104
+ await migration.down(db);
105
+ }
106
+ if (appliedMigrations.length > 0) {
107
+ await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
108
+ }
109
+ await runPendingMigrations(db, migrations, options);
110
+ };
111
+ if (options.advisoryLock) {
112
+ await withMigrationLock(db, runFresh);
113
+ return;
114
+ }
115
+ await runFresh();
116
+ }
117
+ export {
118
+ withMigrationLock,
119
+ runPendingMigrations,
120
+ rollbackDatabase,
121
+ migrateDatabase,
122
+ loadMigrationsFromDirectory,
123
+ getMigrationStatus,
124
+ getAppliedMigrations,
125
+ freshDatabase,
126
+ ensureMigrationsTable
127
+ };
@@ -0,0 +1 @@
1
+ export * from "../../index.js";
@@ -0,0 +1 @@
1
+ // @bun