@getstrata/core 0.4.0 → 0.5.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 (114) hide show
  1. package/dist/bootstrap/httpKernel.d.ts +2 -0
  2. package/dist/config/queue.d.ts +8 -0
  3. package/dist/config/rateLimit.d.ts +2 -1
  4. package/dist/core/database/baseRepository.d.ts +16 -4
  5. package/dist/core/database/index.d.ts +10 -4
  6. package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
  7. package/dist/core/database/migrations/runner.d.ts +8 -2
  8. package/dist/core/database/migrations/types.d.ts +1 -0
  9. package/dist/core/database/model.d.ts +46 -17
  10. package/dist/core/database/query.d.ts +25 -14
  11. package/dist/core/database/relationships.d.ts +32 -2
  12. package/dist/core/database/repositoryQuery.d.ts +16 -1
  13. package/dist/core/database/schema/blueprint.d.ts +47 -0
  14. package/dist/core/database/schema/columnDefinition.d.ts +36 -0
  15. package/dist/core/database/schema/driver.d.ts +8 -0
  16. package/dist/core/database/schema/errors.d.ts +4 -0
  17. package/dist/core/database/schema/grammars/compileStatements.d.ts +8 -0
  18. package/dist/core/database/schema/grammars/createGrammar.d.ts +4 -0
  19. package/dist/core/database/schema/grammars/grammar.d.ts +10 -0
  20. package/dist/core/database/schema/grammars/index.d.ts +7 -0
  21. package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +2 -0
  22. package/dist/core/database/schema/grammars/postgresGrammar.d.ts +2 -0
  23. package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +2 -0
  24. package/dist/core/database/schema/index.d.ts +12 -0
  25. package/dist/core/database/schema/schema.d.ts +21 -0
  26. package/dist/core/database/seeders/runner.d.ts +6 -0
  27. package/dist/core/database/seeders/types.d.ts +8 -0
  28. package/dist/core/database/types.d.ts +39 -2
  29. package/dist/core/database/whereBuilder.d.ts +17 -0
  30. package/dist/core/http/index.d.ts +1 -1
  31. package/dist/core/http/securedRouteModelBinding.d.ts +2 -1
  32. package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
  33. package/dist/core/pagination/index.d.ts +11 -1
  34. package/dist/core/queue/failedJobRepository.d.ts +6 -0
  35. package/dist/core/queue/failedJobService.d.ts +15 -0
  36. package/dist/core/queue/failedJobTable.d.ts +3 -0
  37. package/dist/core/queue/jobRegistry.d.ts +14 -0
  38. package/dist/core/queue/jobRunner.d.ts +9 -0
  39. package/dist/core/queue/publicQueue.d.ts +15 -0
  40. package/dist/core/queue/redisQueue.d.ts +29 -0
  41. package/dist/core/queue/resilientQueue.d.ts +9 -0
  42. package/dist/core/queue/types.d.ts +8 -0
  43. package/dist/core/scheduler/schedule.d.ts +15 -0
  44. package/dist/entries/auth/accessControl.js +113 -0
  45. package/dist/entries/auth/authContext.js +15 -0
  46. package/dist/entries/auth/guard.js +2871 -0
  47. package/dist/entries/auth/membershipContext.js +276 -0
  48. package/dist/entries/auth/membershipScope.js +390 -0
  49. package/dist/entries/auth/membershipService.js +477 -0
  50. package/dist/entries/auth/oauth/oidcProvider.js +49 -0
  51. package/dist/entries/auth/oauth/providers.js +71 -0
  52. package/dist/entries/auth/oauth/samlProvider.js +26 -0
  53. package/dist/entries/auth/oauth/types.js +1 -0
  54. package/dist/entries/auth/password.js +15 -0
  55. package/dist/entries/auth/policy.js +134 -0
  56. package/dist/entries/auth/sessionCookie.js +75 -0
  57. package/dist/entries/auth/tokenHash.js +17 -0
  58. package/dist/entries/cache/tags.js +13 -0
  59. package/dist/entries/crypto/fieldEncryption.js +93 -0
  60. package/dist/entries/crypto/mfaSecret.js +105 -0
  61. package/dist/entries/database/factory.js +17 -0
  62. package/dist/entries/database/seeders.js +30 -0
  63. package/dist/entries/database/types.js +1 -0
  64. package/dist/entries/database.js +2219 -0
  65. package/dist/entries/errors/http.js +78 -0
  66. package/dist/entries/http/contentNegotiation.js +26 -0
  67. package/dist/entries/http/csrfToken.js +147 -0
  68. package/dist/entries/http/etag.js +170 -0
  69. package/dist/entries/http/flashSession.js +106 -0
  70. package/dist/entries/http/middleware.js +68 -0
  71. package/dist/entries/http/parseFormBody.js +88 -0
  72. package/dist/entries/http/requestMetaContext.js +18 -0
  73. package/dist/entries/http/resources.js +19 -0
  74. package/dist/entries/http/webErrorResponse.js +3144 -0
  75. package/dist/entries/http/webFormRequest.js +293 -0
  76. package/dist/entries/http.js +3925 -0
  77. package/dist/entries/jobs/dispatchWebhookJob.js +342 -0
  78. package/dist/entries/lifecycle/gracefulShutdown.js +50 -0
  79. package/dist/entries/metrics/prometheus.js +70 -0
  80. package/dist/entries/pagination.js +14 -0
  81. package/dist/entries/queue/createAppQueue.js +2849 -0
  82. package/dist/entries/queue/failedJobService.js +38 -0
  83. package/dist/entries/queue/jobRegistry.js +32 -0
  84. package/dist/entries/queue/jobRunner.js +75 -0
  85. package/dist/entries/queue/publicQueue.js +2477 -0
  86. package/dist/entries/queue/queueMetrics.js +2899 -0
  87. package/dist/entries/queue/types.js +1 -0
  88. package/dist/entries/security/oauthState.js +75 -0
  89. package/dist/entries/security/publicReads.js +33 -0
  90. package/dist/entries/security/safeUrl.js +143 -0
  91. package/dist/entries/security/securityEvents.js +41 -0
  92. package/dist/entries/security/stripeWebhook.js +115 -0
  93. package/dist/entries/security/tokenExpiry.js +16 -0
  94. package/dist/entries/security/totp.js +51 -0
  95. package/dist/entries/storage/storage.js +123 -0
  96. package/dist/entries/tenant/tenantContext.js +30 -0
  97. package/dist/entries/tenant/tenantMiddleware.js +312 -0
  98. package/dist/entries/tracing/traceContext.js +15 -0
  99. package/dist/entries/validation/rules.js +232 -0
  100. package/dist/entries/view.js +3073 -0
  101. package/dist/framework/public-api.d.ts +65 -38
  102. package/dist/index.js +3199 -308
  103. package/dist/modules/user/apiTokenRepository.d.ts +1 -1
  104. package/dist/modules/user/apiTokenTable.d.ts +1 -1
  105. package/dist/modules/user/authService.d.ts +1 -1
  106. package/dist/modules/user/notificationRepository.d.ts +1 -1
  107. package/dist/modules/user/notificationService.d.ts +1 -1
  108. package/dist/modules/user/notificationTable.d.ts +1 -1
  109. package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
  110. package/dist/modules/user/provider.d.ts +1 -1
  111. package/dist/modules/user/repository.d.ts +1 -1
  112. package/dist/modules/user/table.d.ts +1 -1
  113. package/dist/modules/user/tokenService.d.ts +1 -1
  114. package/package.json +289 -3
@@ -0,0 +1,477 @@
1
+ // @bun
2
+ // ../../src/core/logging/logger.ts
3
+ class Logger {
4
+ channel;
5
+ constructor(channel = "app") {
6
+ this.channel = channel;
7
+ }
8
+ write(level, message, context = {}) {
9
+ const entry = {
10
+ level,
11
+ channel: this.channel,
12
+ message,
13
+ timestamp: new Date().toISOString(),
14
+ ...context
15
+ };
16
+ const line = JSON.stringify(entry);
17
+ if (level === "error") {
18
+ console.error(line);
19
+ return;
20
+ }
21
+ console.log(line);
22
+ }
23
+ debug(message, context) {
24
+ this.write("debug", message, context);
25
+ }
26
+ info(message, context) {
27
+ this.write("info", message, context);
28
+ }
29
+ warn(message, context) {
30
+ this.write("warn", message, context);
31
+ }
32
+ error(message, context) {
33
+ this.write("error", message, context);
34
+ }
35
+ }
36
+ var appLogger = new Logger("app");
37
+
38
+ // ../../src/bootstrap/config.ts
39
+ var CORE_QUEUE_TOKEN = "core.queue";
40
+ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
41
+ var CORE_AUTH_TOKEN = "core.auth";
42
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
43
+ var DEFAULT_QUEUE_DRIVER = "sync";
44
+
45
+ // ../../src/bootstrap/contracts.ts
46
+ class ServiceContainer {
47
+ services = new Map;
48
+ singletonFactories = new Map;
49
+ bindings = new Map;
50
+ set(key, value) {
51
+ this.singletonFactories.delete(key);
52
+ this.bindings.delete(key);
53
+ this.services.set(key, value);
54
+ return value;
55
+ }
56
+ singleton(key, factory) {
57
+ this.bindings.delete(key);
58
+ this.services.delete(key);
59
+ this.singletonFactories.set(key, factory);
60
+ }
61
+ bind(key, factory) {
62
+ this.singletonFactories.delete(key);
63
+ this.services.delete(key);
64
+ this.bindings.set(key, factory);
65
+ }
66
+ get(key) {
67
+ if (this.services.has(key)) {
68
+ return this.services.get(key);
69
+ }
70
+ const singletonFactory = this.singletonFactories.get(key);
71
+ if (singletonFactory) {
72
+ const value = singletonFactory(this);
73
+ this.services.set(key, value);
74
+ return value;
75
+ }
76
+ const binding = this.bindings.get(key);
77
+ if (binding) {
78
+ return binding(this);
79
+ }
80
+ throw new Error(`Service "${key}" is not registered.`);
81
+ }
82
+ resolve(key) {
83
+ return this.get(key);
84
+ }
85
+ has(key) {
86
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
87
+ }
88
+ }
89
+
90
+ class ConfigStore {
91
+ values = new Map;
92
+ set(key, value) {
93
+ this.values.set(key, value);
94
+ return value;
95
+ }
96
+ get(key) {
97
+ return this.values.get(key);
98
+ }
99
+ require(key) {
100
+ if (!this.values.has(key)) {
101
+ throw new Error(`Config key "${key}" is not defined.`);
102
+ }
103
+ return this.values.get(key);
104
+ }
105
+ has(key) {
106
+ return this.values.has(key);
107
+ }
108
+ }
109
+ function getRequiredDependency(dependencies, key) {
110
+ const dependency = dependencies[key];
111
+ if (dependency === undefined) {
112
+ throw new Error(`Required dependency "${key}" is not registered.`);
113
+ }
114
+ return dependency;
115
+ }
116
+ function resolveService(dependencies, token) {
117
+ return dependencies.container.resolve(token);
118
+ }
119
+
120
+ // ../../src/bootstrap/applicationRegistry.ts
121
+ var activeContext;
122
+ function requireActiveApplicationContext() {
123
+ if (!activeContext) {
124
+ throw new Error("The application context has not been bootstrapped.");
125
+ }
126
+ return activeContext;
127
+ }
128
+ function resolveApplicationCache() {
129
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
130
+ }
131
+ function resolveApplicationQueue() {
132
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
133
+ }
134
+ function resolveApplicationAuth() {
135
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
136
+ }
137
+ function resolveApplicationPolicyGate() {
138
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
139
+ }
140
+ function resolveApplicationConfig() {
141
+ return requireActiveApplicationContext().config;
142
+ }
143
+ function resolveApplicationLogger() {
144
+ return appLogger;
145
+ }
146
+ function resolveApplicationDependencies() {
147
+ return requireActiveApplicationContext().dependencies;
148
+ }
149
+
150
+ // ../../src/core/errors/http.ts
151
+ class HttpError extends Error {
152
+ status;
153
+ details;
154
+ constructor(status, message, details) {
155
+ super(message);
156
+ this.name = new.target.name;
157
+ this.status = status;
158
+ this.details = details;
159
+ }
160
+ }
161
+
162
+ class BadRequestError extends HttpError {
163
+ constructor(message = "Bad Request", details) {
164
+ super(400, message, details);
165
+ }
166
+ }
167
+
168
+ class NotFoundError extends HttpError {
169
+ constructor(message = "Not Found", details) {
170
+ super(404, message, details);
171
+ }
172
+ }
173
+
174
+ class ConflictError extends HttpError {
175
+ constructor(message = "Conflict", details) {
176
+ super(409, message, details);
177
+ }
178
+ }
179
+
180
+ class UnprocessableEntityError extends HttpError {
181
+ constructor(message = "Unprocessable Entity", details) {
182
+ super(422, message, details);
183
+ }
184
+ }
185
+
186
+ class ValidationError extends HttpError {
187
+ constructor(message = "Validation failed", details) {
188
+ super(422, message, details);
189
+ }
190
+ }
191
+
192
+ class ForbiddenError extends HttpError {
193
+ constructor(message = "Forbidden", details) {
194
+ super(403, message, details);
195
+ }
196
+ }
197
+
198
+ class UnauthorizedError extends HttpError {
199
+ constructor(message = "Unauthorized", details) {
200
+ super(401, message, details);
201
+ }
202
+ }
203
+
204
+ class PayloadTooLargeError extends HttpError {
205
+ constructor(message = "Payload Too Large", details) {
206
+ super(413, message, details);
207
+ }
208
+ }
209
+
210
+ class PreconditionFailedError extends HttpError {
211
+ constructor(message = "Precondition Failed", details) {
212
+ super(412, message, details);
213
+ }
214
+ }
215
+
216
+ // ../../src/core/auth/authContext.ts
217
+ import { AsyncLocalStorage } from "async_hooks";
218
+ var authContext = new AsyncLocalStorage;
219
+ function runWithAuthUser(user, callback) {
220
+ return authContext.run(user, callback);
221
+ }
222
+ function currentAuthUser() {
223
+ return authContext.getStore() ?? null;
224
+ }
225
+
226
+ // ../../src/core/auth/accessControl.ts
227
+ var ROLE_RANK = {
228
+ member: 1,
229
+ admin: 2,
230
+ owner: 3
231
+ };
232
+ function isGlobalAdmin(user) {
233
+ return user?.role === "admin";
234
+ }
235
+ function hasMinimumOrgRole(role, minimum) {
236
+ if (!role) {
237
+ return false;
238
+ }
239
+ return ROLE_RANK[role] >= ROLE_RANK[minimum];
240
+ }
241
+ function requireAuthenticatedUser() {
242
+ const user = currentAuthUser();
243
+ if (!user) {
244
+ throw new ForbiddenError("Authentication required.");
245
+ }
246
+ return user;
247
+ }
248
+ function resolveUserId(user) {
249
+ const userId = typeof user.id === "number" ? user.id : Number(user.id);
250
+ if (!Number.isInteger(userId) || userId <= 0) {
251
+ throw new ForbiddenError("Invalid authenticated user.");
252
+ }
253
+ return userId;
254
+ }
255
+
256
+ // ../../src/core/auth/membershipContext.ts
257
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
258
+
259
+ // ../../src/config/database.ts
260
+ function readInteger(name, fallback) {
261
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
262
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
263
+ }
264
+ var databaseConfig = {
265
+ url: process.env.DATABASE_URL ?? "",
266
+ poolMax: readInteger("DB_POOL_MAX", 10),
267
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
268
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
269
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
270
+ };
271
+
272
+ // ../../src/core/database/connectionContext.ts
273
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
274
+ var activeConnection = new AsyncLocalStorage2;
275
+ function runWithDatabaseConnection(connection, callback) {
276
+ return activeConnection.run(connection, callback);
277
+ }
278
+ function getActiveDatabaseConnection(fallback) {
279
+ return activeConnection.getStore() ?? fallback;
280
+ }
281
+
282
+ // ../../src/db/connection/createConnection.ts
283
+ var {SQL } = globalThis.Bun;
284
+ function createDatabaseConnection(config) {
285
+ if (!config.url) {
286
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
287
+ }
288
+ return new SQL({
289
+ url: config.url,
290
+ max: config.poolMax,
291
+ idleTimeout: config.idleTimeoutSeconds,
292
+ maxLifetime: config.maxLifetimeSeconds,
293
+ connectionTimeout: config.connectionTimeoutSeconds
294
+ });
295
+ }
296
+
297
+ // ../../src/db/connection/index.ts
298
+ var connectionHolder = {
299
+ connection: null
300
+ };
301
+ function getDatabase() {
302
+ if (!connectionHolder.connection) {
303
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
304
+ }
305
+ return connectionHolder.connection;
306
+ }
307
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
308
+ function resolveDatabase() {
309
+ return getActiveDatabaseConnection(getDatabase());
310
+ }
311
+ function resolveDatabaseForProperty(property) {
312
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
313
+ return getDatabase();
314
+ }
315
+ return resolveDatabase();
316
+ }
317
+ var db = new Proxy(function database() {}, {
318
+ apply(_target, _thisArg, args) {
319
+ return resolveDatabase()(...args);
320
+ },
321
+ get(_target, property) {
322
+ const connection = resolveDatabaseForProperty(property);
323
+ const value = connection[property];
324
+ return typeof value === "function" ? value.bind(connection) : value;
325
+ }
326
+ });
327
+ var connection_default = db;
328
+
329
+ // ../../src/modules/organization/memberRepository.ts
330
+ class OrganizationMemberRepository {
331
+ constructor() {}
332
+ async findMembership(userId, organizationId) {
333
+ const rows = await connection_default`
334
+ SELECT id, organization_id, user_id, role, created_at
335
+ FROM organization_member
336
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
337
+ LIMIT 1
338
+ `;
339
+ return rows[0] ?? null;
340
+ }
341
+ async listForUser(userId) {
342
+ return await connection_default`
343
+ SELECT id, organization_id, user_id, role, created_at
344
+ FROM organization_member
345
+ WHERE user_id = ${userId}
346
+ ORDER BY organization_id
347
+ `;
348
+ }
349
+ async listForOrganization(organizationId) {
350
+ return await connection_default`
351
+ SELECT id, organization_id, user_id, role, created_at
352
+ FROM organization_member
353
+ WHERE organization_id = ${organizationId}
354
+ ORDER BY id
355
+ `;
356
+ }
357
+ async addMember(input) {
358
+ const rows = await connection_default`
359
+ INSERT INTO organization_member (organization_id, user_id, role)
360
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
361
+ RETURNING id, organization_id, user_id, role, created_at
362
+ `;
363
+ const row = rows[0];
364
+ if (!row) {
365
+ throw new Error("Organization member insert did not return a row.");
366
+ }
367
+ return row;
368
+ }
369
+ async removeMember(organizationId, userId) {
370
+ const rows = await connection_default`
371
+ DELETE FROM organization_member
372
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
373
+ RETURNING id
374
+ `;
375
+ return rows.length > 0;
376
+ }
377
+ }
378
+ var memberRepository_default = OrganizationMemberRepository;
379
+
380
+ // ../../src/core/auth/membershipContext.ts
381
+ var membershipContext = new AsyncLocalStorage3;
382
+ var membershipRepository = new memberRepository_default;
383
+ async function runWithMembershipContext(callback) {
384
+ const user = currentAuthUser();
385
+ if (!user || isGlobalAdmin(user)) {
386
+ return await callback();
387
+ }
388
+ const memberships = await membershipRepository.listForUser(resolveUserId(user));
389
+ const context = {
390
+ organizationIds: memberships.map((membership) => membership.organization_id),
391
+ rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
392
+ };
393
+ return await membershipContext.run(context, callback);
394
+ }
395
+ function currentOrgRole(organizationId) {
396
+ return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
397
+ }
398
+ function hasOrgMembership(organizationId) {
399
+ return currentOrgRole(organizationId) !== null;
400
+ }
401
+ function currentOrganizationIds() {
402
+ return membershipContext.getStore()?.organizationIds ?? [];
403
+ }
404
+ function hasMinimumOrgRole2(organizationId, minimum) {
405
+ const role = currentOrgRole(organizationId);
406
+ if (!role) {
407
+ return false;
408
+ }
409
+ const ranks = {
410
+ member: 1,
411
+ admin: 2,
412
+ owner: 3
413
+ };
414
+ return ranks[role] >= ranks[minimum];
415
+ }
416
+
417
+ // ../../src/core/auth/membershipService.ts
418
+ class MembershipService {
419
+ constructor() {}
420
+ async listOrganizationIdsForUser(userId) {
421
+ const memberships = await membershipRepository.listForUser(userId);
422
+ return memberships.map((membership) => membership.organization_id);
423
+ }
424
+ async getOrgRole(userId, organizationId) {
425
+ const membership = await membershipRepository.findMembership(userId, organizationId);
426
+ return membership?.role ?? null;
427
+ }
428
+ async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
429
+ if (!user) {
430
+ throw new ForbiddenError("Authentication required.");
431
+ }
432
+ if (isGlobalAdmin(user)) {
433
+ return "owner";
434
+ }
435
+ const role = await this.getOrgRole(resolveUserId(user), organizationId);
436
+ if (!role || !hasMinimumOrgRole(role, minimumRole)) {
437
+ throw new ForbiddenError("Organization membership required.");
438
+ }
439
+ return role;
440
+ }
441
+ async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
442
+ if (!user) {
443
+ return [];
444
+ }
445
+ if (isGlobalAdmin(user)) {
446
+ return organizationIds;
447
+ }
448
+ const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
449
+ return organizationIds.filter((organizationId) => allowed.has(organizationId));
450
+ }
451
+ async addOwnerOnOrganizationCreate(organizationId, userId) {
452
+ await membershipRepository.addMember({
453
+ organizationId,
454
+ userId,
455
+ role: "owner"
456
+ });
457
+ }
458
+ listMembersForOrganization(organizationId) {
459
+ return membershipRepository.listForOrganization(organizationId);
460
+ }
461
+ addMember(input) {
462
+ return membershipRepository.addMember(input);
463
+ }
464
+ removeMember(organizationId, userId) {
465
+ return membershipRepository.removeMember(organizationId, userId);
466
+ }
467
+ }
468
+ function resolveMembershipService() {
469
+ const dependencies = resolveApplicationDependencies();
470
+ if (dependencies.container.has("core.membership")) {
471
+ return dependencies.container.resolve("core.membership");
472
+ }
473
+ return new MembershipService;
474
+ }
475
+ export {
476
+ resolveMembershipService
477
+ };
@@ -0,0 +1,49 @@
1
+ // @bun
2
+ // ../../src/core/auth/oauth/oidcProvider.ts
3
+ class OidcProvider {
4
+ options;
5
+ name;
6
+ constructor(options) {
7
+ this.options = options;
8
+ this.name = options.name;
9
+ }
10
+ getAuthorizationUrl(state) {
11
+ const params = new URLSearchParams({
12
+ client_id: this.options.clientId,
13
+ redirect_uri: this.options.redirectUri,
14
+ response_type: "code",
15
+ scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
16
+ state
17
+ });
18
+ return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
19
+ }
20
+ async exchangeCode(code) {
21
+ const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/x-www-form-urlencoded" },
24
+ body: new URLSearchParams({
25
+ grant_type: "authorization_code",
26
+ code,
27
+ redirect_uri: this.options.redirectUri,
28
+ client_id: this.options.clientId,
29
+ client_secret: this.options.clientSecret
30
+ })
31
+ });
32
+ const tokenBody = await tokenResponse.json();
33
+ if (!tokenBody.access_token) {
34
+ throw new Error("OIDC token exchange failed.");
35
+ }
36
+ const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
37
+ headers: { authorization: `Bearer ${tokenBody.access_token}` }
38
+ });
39
+ const profile = await profileResponse.json();
40
+ return {
41
+ providerUserId: profile.sub,
42
+ email: profile.email ?? `${profile.sub}@oidc.local`,
43
+ name: profile.name ?? profile.sub
44
+ };
45
+ }
46
+ }
47
+ export {
48
+ OidcProvider
49
+ };
@@ -0,0 +1,71 @@
1
+ // @bun
2
+ // ../../src/core/auth/oauth/providers.ts
3
+ class GitHubOAuthProvider {
4
+ options;
5
+ name = "github";
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ getAuthorizationUrl(state) {
10
+ const params = new URLSearchParams({
11
+ client_id: this.options.clientId,
12
+ redirect_uri: this.options.redirectUri,
13
+ scope: "read:user user:email",
14
+ state
15
+ });
16
+ return `https://github.com/login/oauth/authorize?${params.toString()}`;
17
+ }
18
+ async exchangeCode(code) {
19
+ const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
20
+ method: "POST",
21
+ headers: {
22
+ accept: "application/json",
23
+ "content-type": "application/json"
24
+ },
25
+ body: JSON.stringify({
26
+ client_id: this.options.clientId,
27
+ client_secret: this.options.clientSecret,
28
+ code,
29
+ redirect_uri: this.options.redirectUri
30
+ })
31
+ });
32
+ const tokenBody = await tokenResponse.json();
33
+ if (!tokenBody.access_token) {
34
+ throw new Error("GitHub OAuth token exchange failed.");
35
+ }
36
+ const profileResponse = await fetch("https://api.github.com/user", {
37
+ headers: {
38
+ authorization: `Bearer ${tokenBody.access_token}`,
39
+ accept: "application/json",
40
+ "user-agent": "workhub"
41
+ }
42
+ });
43
+ const profile = await profileResponse.json();
44
+ return {
45
+ providerUserId: String(profile.id),
46
+ email: profile.email ?? `${profile.login}@users.noreply.github.com`,
47
+ name: profile.name ?? profile.login
48
+ };
49
+ }
50
+ }
51
+
52
+ class MockOAuthProvider {
53
+ profile;
54
+ name = "mock";
55
+ constructor(profile) {
56
+ this.profile = profile;
57
+ }
58
+ getAuthorizationUrl(state) {
59
+ return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
60
+ }
61
+ async exchangeCode(code) {
62
+ if (code !== "valid-code") {
63
+ throw new Error("Invalid OAuth code.");
64
+ }
65
+ return this.profile;
66
+ }
67
+ }
68
+ export {
69
+ MockOAuthProvider,
70
+ GitHubOAuthProvider
71
+ };
@@ -0,0 +1,26 @@
1
+ // @bun
2
+ // ../../src/core/auth/oauth/samlProvider.ts
3
+ class SamlProvider {
4
+ loginUrl;
5
+ name = "saml";
6
+ constructor(loginUrl) {
7
+ this.loginUrl = loginUrl;
8
+ }
9
+ getAuthorizationUrl(state) {
10
+ return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
11
+ }
12
+ async exchangeCode(code) {
13
+ if (!code.startsWith("saml:")) {
14
+ throw new Error("Invalid SAML assertion reference.");
15
+ }
16
+ const [, email, name] = code.split(":");
17
+ return {
18
+ providerUserId: email ?? "saml-user",
19
+ email: email ?? "saml-user@workhub.test",
20
+ name: name ?? "SAML User"
21
+ };
22
+ }
23
+ }
24
+ export {
25
+ SamlProvider
26
+ };
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,15 @@
1
+ // @bun
2
+ // ../../src/core/auth/password.ts
3
+ async function hashPassword(password) {
4
+ return await Bun.password.hash(password, {
5
+ algorithm: "bcrypt",
6
+ cost: 10
7
+ });
8
+ }
9
+ async function verifyPassword(password, passwordHash) {
10
+ return await Bun.password.verify(password, passwordHash);
11
+ }
12
+ export {
13
+ verifyPassword,
14
+ hashPassword
15
+ };