@fonderie/workspaces 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1339 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ EVENT_KEYS: () => EVENT_KEYS,
24
+ MESSAGE_KEYS: () => MESSAGE_KEYS,
25
+ WorkspacesModule: () => WorkspacesModule,
26
+ requireWorkspace: () => requireWorkspace,
27
+ toInvitationDTO: () => toInvitationDTO,
28
+ toMemberDTO: () => toMemberDTO,
29
+ toRoleDTO: () => toRoleDTO,
30
+ toSettingsDTO: () => toSettingsDTO,
31
+ toWorkspaceDTO: () => toWorkspaceDTO,
32
+ withWorkspace: () => withWorkspace
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/config.ts
37
+ var MESSAGE_KEYS = {
38
+ workspaceInvitation: "workspace-invitation"
39
+ };
40
+ var EVENT_KEYS = {
41
+ personalWorkspaceCreated: "fonderie.workspace.personal.created"
42
+ };
43
+
44
+ // src/routes.ts
45
+ var import_middlewares = require("@fonderie/core/middlewares");
46
+
47
+ // src/middlewares/workspace-context.ts
48
+ var import_core = require("@fonderie/core");
49
+
50
+ // src/services/members.ts
51
+ var SELECT_MEMBER = `
52
+ ruw.user_id AS "userId",
53
+ ruw.workspace_id AS "workspaceId",
54
+ ruw.role_id AS "roleId",
55
+ r.name AS "roleName",
56
+ ruw.confirmed AS "confirmed",
57
+ ruw.created_at AS "createdAt",
58
+ u.first_name AS "firstName",
59
+ u.last_name AS "lastName",
60
+ u.email AS "email",
61
+ u.profile_image_url AS "profileImageUrl"
62
+ `;
63
+ async function getMember(userId, workspaceId, store) {
64
+ const [row] = await store.query(
65
+ `SELECT ${SELECT_MEMBER}
66
+ FROM fonderie_role_user_workspaces ruw
67
+ LEFT JOIN fonderie_roles r ON r.id = ruw.role_id
68
+ LEFT JOIN fonderie_users u ON u.id = ruw.user_id
69
+ WHERE ruw.user_id = $1
70
+ AND ruw.workspace_id = $2
71
+ AND ruw.removed = false
72
+ AND ruw.suspended = false
73
+ LIMIT 1`,
74
+ [userId, workspaceId]
75
+ );
76
+ return row ?? null;
77
+ }
78
+ async function listMembers(workspaceId, store) {
79
+ return store.query(
80
+ `SELECT ${SELECT_MEMBER}
81
+ FROM fonderie_role_user_workspaces ruw
82
+ LEFT JOIN fonderie_roles r ON r.id = ruw.role_id
83
+ LEFT JOIN fonderie_users u ON u.id = ruw.user_id
84
+ WHERE ruw.workspace_id = $1
85
+ AND ruw.removed = false
86
+ AND ruw.suspended = false
87
+ ORDER BY ruw.created_at ASC`,
88
+ [workspaceId]
89
+ );
90
+ }
91
+ async function addMember(opts, store) {
92
+ await store.query(
93
+ `INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
94
+ VALUES ($1, $2, $3, $4)
95
+ ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
96
+ SET confirmed = $4, removed = false, suspended = false`,
97
+ [opts.userId, opts.workspaceId, opts.roleId, opts.confirmed ?? true]
98
+ );
99
+ }
100
+ async function removeMember(userId, workspaceId, store) {
101
+ await store.query(
102
+ `UPDATE fonderie_role_user_workspaces
103
+ SET removed = true
104
+ WHERE user_id = $1
105
+ AND workspace_id = $2`,
106
+ [userId, workspaceId]
107
+ );
108
+ }
109
+ async function getUserRoles(userId, workspaceId, store) {
110
+ return store.query(
111
+ `SELECT
112
+ r.id,
113
+ r.name,
114
+ r.is_system AS "isSystem",
115
+ r.active,
116
+ r.description,
117
+ r.workspace_id AS "workspaceId"
118
+ FROM fonderie_role_user_workspaces ruw
119
+ JOIN fonderie_roles r ON r.id = ruw.role_id
120
+ WHERE ruw.user_id = $1
121
+ AND ruw.workspace_id = $2
122
+ AND ruw.removed = false
123
+ AND ruw.suspended = false`,
124
+ [userId, workspaceId]
125
+ );
126
+ }
127
+ async function addRoleToMember(userId, workspaceId, roleId, store) {
128
+ await store.query(
129
+ `INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
130
+ VALUES ($1, $2, $3, true)
131
+ ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
132
+ SET confirmed = true, removed = false, suspended = false`,
133
+ [userId, workspaceId, roleId]
134
+ );
135
+ }
136
+ async function removeRoleFromMember(userId, workspaceId, roleId, store) {
137
+ const remaining = await store.query(
138
+ `SELECT COUNT(*) AS count
139
+ FROM fonderie_role_user_workspaces
140
+ WHERE user_id = $1
141
+ AND workspace_id = $2
142
+ AND removed = false`,
143
+ [userId, workspaceId]
144
+ );
145
+ const count = parseInt(remaining[0]?.count ?? "0", 10);
146
+ if (count <= 1) throw new Error("Cannot remove last role from member");
147
+ await store.query(
148
+ `DELETE FROM fonderie_role_user_workspaces
149
+ WHERE user_id = $1
150
+ AND workspace_id = $2
151
+ AND role_id = $3`,
152
+ [userId, workspaceId, roleId]
153
+ );
154
+ }
155
+
156
+ // src/services/workspaces.ts
157
+ var SELECT_WS = `
158
+ id,
159
+ name,
160
+ slug,
161
+ type,
162
+ description,
163
+ motto,
164
+ phone,
165
+ business_type AS "businessType",
166
+ address,
167
+ plan,
168
+ owner_id AS "ownerId",
169
+ is_personal AS "isPersonal",
170
+ archived_at AS "archivedAt",
171
+ archived_by AS "archivedBy",
172
+ created_at AS "createdAt",
173
+ updated_at AS "updatedAt"
174
+ `;
175
+ var SELECT_WS_W = `
176
+ w.id,
177
+ w.name,
178
+ w.slug,
179
+ w.type,
180
+ w.description,
181
+ w.motto,
182
+ w.phone,
183
+ w.business_type AS "businessType",
184
+ w.address,
185
+ w.plan,
186
+ w.owner_id AS "ownerId",
187
+ w.is_personal AS "isPersonal",
188
+ w.archived_at AS "archivedAt",
189
+ w.archived_by AS "archivedBy",
190
+ w.created_at AS "createdAt",
191
+ w.updated_at AS "updatedAt"
192
+ `;
193
+ async function findWorkspaceById(id, store) {
194
+ const [row] = await store.query(
195
+ `SELECT ${SELECT_WS} FROM fonderie_workspaces WHERE id = $1`,
196
+ [id]
197
+ );
198
+ return row ?? null;
199
+ }
200
+ async function findWorkspacesByUserId(userId, store) {
201
+ return store.query(
202
+ `SELECT ${SELECT_WS_W}
203
+ FROM fonderie_workspaces w
204
+ JOIN fonderie_role_user_workspaces ruw ON ruw.workspace_id = w.id
205
+ WHERE ruw.user_id = $1
206
+ AND ruw.removed = false
207
+ AND ruw.suspended = false
208
+ GROUP BY w.id
209
+ ORDER BY w.created_at ASC`,
210
+ [userId]
211
+ );
212
+ }
213
+ async function createWorkspace(opts, store) {
214
+ const [workspace] = await store.query(
215
+ `INSERT INTO fonderie_workspaces (name, slug, owner_id, type, description, plan)
216
+ VALUES ($1, $2, $3, $4, $5, $6)
217
+ RETURNING ${SELECT_WS}`,
218
+ [
219
+ opts.name,
220
+ opts.slug,
221
+ opts.ownerId,
222
+ opts.type ?? "ORGANIZATION",
223
+ opts.description ?? null,
224
+ opts.plan ?? "free"
225
+ ]
226
+ );
227
+ if (!workspace) throw new Error("Failed to create workspace");
228
+ return workspace;
229
+ }
230
+ async function createPersonalWorkspace(opts, store) {
231
+ const [workspace] = await store.query(
232
+ `INSERT INTO fonderie_workspaces (name, slug, owner_id, type, is_personal)
233
+ VALUES ($1, $2, $3, 'PERSONAL', true)
234
+ ON CONFLICT (owner_id) WHERE is_personal = true DO NOTHING
235
+ RETURNING ${SELECT_WS}`,
236
+ [opts.name, opts.slug, opts.ownerId]
237
+ );
238
+ return workspace ?? null;
239
+ }
240
+ async function findPersonalWorkspace(userId, store) {
241
+ const [row] = await store.query(
242
+ `SELECT ${SELECT_WS} FROM fonderie_workspaces
243
+ WHERE owner_id = $1 AND is_personal = true
244
+ LIMIT 1`,
245
+ [userId]
246
+ );
247
+ return row ?? null;
248
+ }
249
+ async function updateWorkspace(id, opts, store) {
250
+ const sets = ["updated_at = now()"];
251
+ const params = [id];
252
+ if (opts.name !== void 0) {
253
+ params.push(opts.name);
254
+ sets.push(`name = $${params.length}`);
255
+ }
256
+ if (opts.description !== void 0) {
257
+ params.push(opts.description);
258
+ sets.push(`description = $${params.length}`);
259
+ }
260
+ if (opts.slug !== void 0) {
261
+ params.push(opts.slug);
262
+ sets.push(`slug = $${params.length}`);
263
+ }
264
+ if (opts.motto !== void 0) {
265
+ params.push(opts.motto);
266
+ sets.push(`motto = $${params.length}`);
267
+ }
268
+ if (opts.phone !== void 0) {
269
+ params.push(opts.phone);
270
+ sets.push(`phone = $${params.length}`);
271
+ }
272
+ if (opts.businessType !== void 0) {
273
+ params.push(opts.businessType);
274
+ sets.push(`business_type = $${params.length}`);
275
+ }
276
+ if (opts.address !== void 0) {
277
+ params.push(JSON.stringify(opts.address ?? {}));
278
+ sets.push(`address = $${params.length}::jsonb`);
279
+ }
280
+ const [row] = await store.query(
281
+ `UPDATE fonderie_workspaces
282
+ SET ${sets.join(", ")}
283
+ WHERE id = $1
284
+ RETURNING ${SELECT_WS}`,
285
+ params
286
+ );
287
+ return row ?? null;
288
+ }
289
+ async function archiveWorkspace(id, byUser, store) {
290
+ await store.query(
291
+ `UPDATE fonderie_workspaces
292
+ SET archived_at = now(), archived_by = $2, updated_at = now()
293
+ WHERE id = $1 AND archived_at IS NULL`,
294
+ [id, byUser]
295
+ );
296
+ }
297
+ async function restoreWorkspace(id, store) {
298
+ await store.query(
299
+ `UPDATE fonderie_workspaces
300
+ SET archived_at = NULL, archived_by = NULL, updated_at = now()
301
+ WHERE id = $1`,
302
+ [id]
303
+ );
304
+ }
305
+ var SETTINGS_DEFAULTS = {
306
+ locale: "en-US",
307
+ timezone: "UTC",
308
+ currency: "USD",
309
+ dateFormat: "MM/DD/YYYY",
310
+ timeFormat: "hh:mm A"
311
+ };
312
+ async function getWorkspaceSettings(id, store) {
313
+ const [row] = await store.query(
314
+ `SELECT settings FROM fonderie_workspaces WHERE id = $1`,
315
+ [id]
316
+ );
317
+ const raw = row?.settings ?? {};
318
+ const s = raw["settings"] ?? raw;
319
+ return {
320
+ locale: typeof s["locale"] === "string" ? s["locale"] : SETTINGS_DEFAULTS.locale,
321
+ timezone: typeof s["timezone"] === "string" ? s["timezone"] : SETTINGS_DEFAULTS.timezone,
322
+ currency: typeof s["currency"] === "string" ? s["currency"] : SETTINGS_DEFAULTS.currency,
323
+ dateFormat: typeof s["dateFormat"] === "string" ? s["dateFormat"] : SETTINGS_DEFAULTS.dateFormat,
324
+ timeFormat: typeof s["timeFormat"] === "string" ? s["timeFormat"] : SETTINGS_DEFAULTS.timeFormat
325
+ };
326
+ }
327
+ async function updateWorkspaceSettings(id, settings, store) {
328
+ await store.query(
329
+ `UPDATE fonderie_workspaces
330
+ SET settings = settings || jsonb_build_object('settings', $2::jsonb),
331
+ updated_at = now()
332
+ WHERE id = $1`,
333
+ [id, JSON.stringify(settings)]
334
+ );
335
+ return getWorkspaceSettings(id, store);
336
+ }
337
+
338
+ // src/middlewares/workspace-context.ts
339
+ function makeHandler(store) {
340
+ return async (ctx, next) => {
341
+ const params = ctx.meta["params"];
342
+ const workspaceId = params?.["workspaceId"] ?? params?.["id"] ?? ctx.request.headers.get("x-workspace-id") ?? void 0;
343
+ if (!workspaceId) {
344
+ if (ctx.user) {
345
+ const personal = await findPersonalWorkspace(ctx.user.id, store);
346
+ if (personal) Object.assign(ctx, { workspace: personal });
347
+ }
348
+ return next();
349
+ }
350
+ const workspace = await findWorkspaceById(workspaceId, store);
351
+ if (!workspace) {
352
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
353
+ }
354
+ if (ctx.user) {
355
+ const member = await getMember(ctx.user.id, workspaceId, store);
356
+ if (!member) {
357
+ return (0, import_core.setApiResponse)(import_core.HTTP.FORBIDDEN, "FORBIDDEN", "Not a member of this workspace");
358
+ }
359
+ }
360
+ Object.assign(ctx, { workspace });
361
+ return next();
362
+ };
363
+ }
364
+ function withWorkspace(store, ctx, next) {
365
+ const handler = makeHandler(store);
366
+ if (ctx !== void 0 && next !== void 0) return handler(ctx, next);
367
+ return handler;
368
+ }
369
+
370
+ // src/controllers/workspace.controller.ts
371
+ var import_core2 = require("@fonderie/core");
372
+
373
+ // src/models/workspace.model.ts
374
+ var WorkspaceModel = class {
375
+ constructor(store) {
376
+ this.store = store;
377
+ }
378
+ store;
379
+ findById(id) {
380
+ return findWorkspaceById(id, this.store);
381
+ }
382
+ findByUserId(userId) {
383
+ return findWorkspacesByUserId(userId, this.store);
384
+ }
385
+ create(opts) {
386
+ return createWorkspace(opts, this.store);
387
+ }
388
+ createPersonal(opts) {
389
+ return createPersonalWorkspace(opts, this.store);
390
+ }
391
+ findPersonal(userId) {
392
+ return findPersonalWorkspace(userId, this.store);
393
+ }
394
+ update(id, opts) {
395
+ return updateWorkspace(id, opts, this.store);
396
+ }
397
+ archive(id, byUser) {
398
+ return archiveWorkspace(id, byUser, this.store);
399
+ }
400
+ restore(id) {
401
+ return restoreWorkspace(id, this.store);
402
+ }
403
+ getSettings(id) {
404
+ return getWorkspaceSettings(id, this.store);
405
+ }
406
+ updateSettings(id, settings) {
407
+ return updateWorkspaceSettings(id, settings, this.store);
408
+ }
409
+ };
410
+
411
+ // src/models/member.model.ts
412
+ var MemberModel = class {
413
+ constructor(store) {
414
+ this.store = store;
415
+ }
416
+ store;
417
+ get(userId, workspaceId) {
418
+ return getMember(userId, workspaceId, this.store);
419
+ }
420
+ list(workspaceId) {
421
+ return listMembers(workspaceId, this.store);
422
+ }
423
+ add(opts) {
424
+ return addMember(opts, this.store);
425
+ }
426
+ remove(userId, workspaceId) {
427
+ return removeMember(userId, workspaceId, this.store);
428
+ }
429
+ getUserRoles(userId, workspaceId) {
430
+ return getUserRoles(userId, workspaceId, this.store);
431
+ }
432
+ addRole(userId, workspaceId, roleId) {
433
+ return addRoleToMember(userId, workspaceId, roleId, this.store);
434
+ }
435
+ removeRole(userId, workspaceId, roleId) {
436
+ return removeRoleFromMember(userId, workspaceId, roleId, this.store);
437
+ }
438
+ };
439
+
440
+ // src/services/roles.ts
441
+ var SELECT_ROLE = `
442
+ id,
443
+ name,
444
+ is_system AS "isSystem",
445
+ active,
446
+ description,
447
+ workspace_id AS "workspaceId"
448
+ `;
449
+ async function createRole(opts, store) {
450
+ const [role] = await store.query(
451
+ `INSERT INTO fonderie_roles (name, workspace_id, description)
452
+ VALUES ($1, $2, $3)
453
+ RETURNING ${SELECT_ROLE}`,
454
+ [opts.name, opts.workspaceId, opts.description ?? null]
455
+ );
456
+ if (!role) throw new Error("Failed to create role");
457
+ return role;
458
+ }
459
+ async function findSystemRole(name, store) {
460
+ const [row] = await store.query(
461
+ `SELECT ${SELECT_ROLE} FROM fonderie_roles WHERE name = $1 AND is_system = true LIMIT 1`,
462
+ [name]
463
+ );
464
+ return row ?? null;
465
+ }
466
+ async function getRoleById(id, store) {
467
+ const [row] = await store.query(
468
+ `SELECT ${SELECT_ROLE} FROM fonderie_roles WHERE id = $1`,
469
+ [id]
470
+ );
471
+ return row ?? null;
472
+ }
473
+ async function listWorkspaceRoles(workspaceId, store) {
474
+ return store.query(
475
+ `SELECT ${SELECT_ROLE}
476
+ FROM fonderie_roles
477
+ WHERE workspace_id = $1 OR is_system = true
478
+ ORDER BY is_system DESC, name ASC`,
479
+ [workspaceId]
480
+ );
481
+ }
482
+ async function updateRole(id, opts, store) {
483
+ const sets = [];
484
+ const params = [id];
485
+ if (opts.name !== void 0) {
486
+ params.push(opts.name);
487
+ sets.push(`name = $${params.length}`);
488
+ }
489
+ if (opts.description !== void 0) {
490
+ params.push(opts.description);
491
+ sets.push(`description = $${params.length}`);
492
+ }
493
+ if (opts.active !== void 0) {
494
+ params.push(opts.active);
495
+ sets.push(`active = $${params.length}`);
496
+ }
497
+ if (sets.length === 0) return getRoleById(id, store);
498
+ const [row] = await store.query(
499
+ `UPDATE fonderie_roles
500
+ SET ${sets.join(", ")}
501
+ WHERE id = $1 AND is_system = false
502
+ RETURNING ${SELECT_ROLE}`,
503
+ params
504
+ );
505
+ return row ?? null;
506
+ }
507
+ async function deleteRole(id, workspaceId, store) {
508
+ await store.query(
509
+ `DELETE FROM fonderie_roles
510
+ WHERE id = $1 AND workspace_id = $2 AND is_system = false`,
511
+ [id, workspaceId]
512
+ );
513
+ }
514
+ async function setRolePermissions(roleId, workspaceId, permissions, store) {
515
+ if (permissions.length === 0) {
516
+ await store.query(
517
+ `DELETE FROM fonderie_role_permissions WHERE role_id = $1 AND workspace_id = $2`,
518
+ [roleId, workspaceId]
519
+ );
520
+ return;
521
+ }
522
+ await store.transaction(async (tx) => {
523
+ await tx.query(
524
+ `DELETE FROM fonderie_role_permissions WHERE role_id = $1 AND workspace_id = $2`,
525
+ [roleId, workspaceId]
526
+ );
527
+ for (const p of permissions) {
528
+ await tx.query(
529
+ `INSERT INTO fonderie_role_permissions
530
+ (role_id, workspace_id, permission_key, can_create, can_read, can_update, can_delete)
531
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
532
+ ON CONFLICT (role_id, permission_key)
533
+ DO UPDATE SET
534
+ can_create = $4, can_read = $5,
535
+ can_update = $6, can_delete = $7`,
536
+ [roleId, workspaceId, p.permissionKey, p.canCreate, p.canRead, p.canUpdate, p.canDelete]
537
+ );
538
+ }
539
+ });
540
+ }
541
+
542
+ // src/models/role.model.ts
543
+ var RoleModel = class {
544
+ constructor(store) {
545
+ this.store = store;
546
+ }
547
+ store;
548
+ create(opts) {
549
+ return createRole(opts, this.store);
550
+ }
551
+ findSystem(name) {
552
+ return findSystemRole(name, this.store);
553
+ }
554
+ findById(id) {
555
+ return getRoleById(id, this.store);
556
+ }
557
+ list(workspaceId) {
558
+ return listWorkspaceRoles(workspaceId, this.store);
559
+ }
560
+ update(id, opts) {
561
+ return updateRole(id, opts, this.store);
562
+ }
563
+ delete(id, workspaceId) {
564
+ return deleteRole(id, workspaceId, this.store);
565
+ }
566
+ setPermissions(roleId, workspaceId, permissions) {
567
+ return setRolePermissions(roleId, workspaceId, permissions, this.store);
568
+ }
569
+ };
570
+
571
+ // src/dtos/workspace.ts
572
+ var import_parser = require("@fonderie/core/parser");
573
+ function toWorkspaceDTO(ws) {
574
+ const addr = ws.address ?? {};
575
+ return {
576
+ id: (0, import_parser.stringOrEmpty)(ws.id),
577
+ name: (0, import_parser.stringOrEmpty)(ws.name),
578
+ slug: (0, import_parser.stringOrEmpty)(ws.slug),
579
+ type: (0, import_parser.stringOrEmpty)(ws.type),
580
+ description: (0, import_parser.stringOrEmpty)(ws.description),
581
+ motto: (0, import_parser.stringOrEmpty)(ws.motto),
582
+ phone: (0, import_parser.stringOrEmpty)(ws.phone),
583
+ businessType: (0, import_parser.stringOrEmpty)(ws.businessType),
584
+ address: {
585
+ line1: (0, import_parser.stringOrEmpty)(addr.line1),
586
+ line2: (0, import_parser.stringOrEmpty)(addr.line2),
587
+ city: (0, import_parser.stringOrEmpty)(addr.city),
588
+ state: (0, import_parser.stringOrEmpty)(addr.state),
589
+ zip: (0, import_parser.stringOrEmpty)(addr.zip),
590
+ country: (0, import_parser.stringOrEmpty)(addr.country)
591
+ },
592
+ plan: (0, import_parser.stringOrEmpty)(ws.plan),
593
+ ownerId: (0, import_parser.stringOrEmpty)(ws.ownerId),
594
+ isPersonal: (0, import_parser.booleanOrFalse)(ws.isPersonal),
595
+ isArchived: ws.archivedAt !== null,
596
+ archivedAt: (0, import_parser.stringOrEmpty)(ws.archivedAt),
597
+ createdAt: (0, import_parser.stringOrEmpty)(ws.createdAt),
598
+ updatedAt: (0, import_parser.stringOrEmpty)(ws.updatedAt)
599
+ };
600
+ }
601
+ function toRoleDTO(role) {
602
+ return {
603
+ id: (0, import_parser.stringOrEmpty)(role.id),
604
+ name: (0, import_parser.stringOrEmpty)(role.name),
605
+ isSystem: (0, import_parser.booleanOrFalse)(role.isSystem),
606
+ active: role.active !== false,
607
+ description: (0, import_parser.stringOrEmpty)(role.description),
608
+ workspaceId: (0, import_parser.stringOrEmpty)(role.workspaceId)
609
+ };
610
+ }
611
+ function toMemberDTO(m) {
612
+ return {
613
+ userId: (0, import_parser.stringOrEmpty)(m.userId),
614
+ workspaceId: (0, import_parser.stringOrEmpty)(m.workspaceId),
615
+ roleId: (0, import_parser.stringOrEmpty)(m.roleId),
616
+ roleName: (0, import_parser.stringOrEmpty)(m.roleName),
617
+ confirmed: (0, import_parser.booleanOrFalse)(m.confirmed),
618
+ createdAt: (0, import_parser.stringOrEmpty)(m.createdAt)
619
+ };
620
+ }
621
+ function toInvitationDTO(inv) {
622
+ return {
623
+ id: (0, import_parser.stringOrEmpty)(inv.id),
624
+ workspaceId: (0, import_parser.stringOrEmpty)(inv.workspaceId),
625
+ email: (0, import_parser.stringOrEmpty)(inv.email),
626
+ roleId: (0, import_parser.stringOrEmpty)(inv.roleId),
627
+ token: (0, import_parser.stringOrEmpty)(inv.token),
628
+ status: (0, import_parser.stringOrEmpty)(inv.status),
629
+ expiresAt: (0, import_parser.stringOrEmpty)(inv.expiresAt),
630
+ createdAt: (0, import_parser.stringOrEmpty)(inv.createdAt)
631
+ };
632
+ }
633
+ function toSettingsDTO(s) {
634
+ return {
635
+ locale: (0, import_parser.stringOrEmpty)(s.locale),
636
+ timezone: (0, import_parser.stringOrEmpty)(s.timezone),
637
+ currency: (0, import_parser.stringOrEmpty)(s.currency),
638
+ dateFormat: (0, import_parser.stringOrEmpty)(s.dateFormat),
639
+ timeFormat: (0, import_parser.stringOrEmpty)(s.timeFormat)
640
+ };
641
+ }
642
+
643
+ // src/controllers/workspace.controller.ts
644
+ function workspaceController(store, config) {
645
+ const workspaces = new WorkspaceModel(store);
646
+ const members = new MemberModel(store);
647
+ const roles = new RoleModel(store);
648
+ return {
649
+ async list(ctx) {
650
+ if (!ctx.user)
651
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Authentication required");
652
+ const list = await workspaces.findByUserId(ctx.user.id);
653
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACES_FETCHED", "Workspaces retrieved successfully.", {
654
+ workspaces: list.map(toWorkspaceDTO)
655
+ });
656
+ },
657
+ async create(ctx) {
658
+ const body = ctx.meta["body"];
659
+ const name = body?.["name"];
660
+ const description = body?.["description"];
661
+ const type = body?.["type"];
662
+ if (typeof name !== "string" || name.trim().length === 0) {
663
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
664
+ }
665
+ if (type === "PERSONAL") {
666
+ return (0, import_core2.setApiResponse)(
667
+ import_core2.HTTP.UNPROCESSABLE,
668
+ "INVALID_PARAMETER",
669
+ "Personal workspaces are created automatically"
670
+ );
671
+ }
672
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
673
+ const workspace = await store.transaction(async (tx) => {
674
+ const wsOpts = {
675
+ name: name.trim(),
676
+ slug,
677
+ ownerId: ctx.user.id,
678
+ type: typeof type === "string" ? type : "ORGANIZATION"
679
+ };
680
+ if (typeof description === "string") wsOpts.description = description;
681
+ const wsModel = new WorkspaceModel(tx);
682
+ const roleModel = new RoleModel(tx);
683
+ const memModel = new MemberModel(tx);
684
+ const ws = await wsModel.create(wsOpts);
685
+ const adminRole = await roleModel.findSystem("ADMIN");
686
+ if (!adminRole) throw new Error("System ADMIN role not found");
687
+ await memModel.add({ userId: ctx.user.id, workspaceId: ws.id, roleId: adminRole.id });
688
+ return ws;
689
+ });
690
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.CREATED, "WORKSPACE_CREATED", "Workspace created successfully.", {
691
+ workspace: toWorkspaceDTO(workspace)
692
+ });
693
+ },
694
+ async get(ctx) {
695
+ if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
696
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_FETCHED", "Workspace retrieved successfully.", {
697
+ workspace: toWorkspaceDTO(ctx.workspace)
698
+ });
699
+ },
700
+ async update(ctx) {
701
+ if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
702
+ const body = ctx.meta["body"];
703
+ const opts = {};
704
+ if (typeof body?.["name"] === "string") opts.name = body["name"].trim();
705
+ if (body?.["description"] !== void 0)
706
+ opts.description = typeof body["description"] === "string" ? body["description"] : null;
707
+ if (body?.["motto"] !== void 0)
708
+ opts.motto = typeof body["motto"] === "string" ? body["motto"] : null;
709
+ if (body?.["phone"] !== void 0)
710
+ opts.phone = typeof body["phone"] === "string" ? body["phone"].trim() : null;
711
+ if (body?.["businessType"] !== void 0)
712
+ opts.businessType = typeof body["businessType"] === "string" ? body["businessType"] : null;
713
+ if (body?.["address"] !== void 0 && typeof body["address"] === "object")
714
+ opts.address = body["address"];
715
+ const workspace = await workspaces.update(ctx.workspace.id, opts);
716
+ if (!workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
717
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_UPDATED", "Workspace updated successfully.", {
718
+ workspace: toWorkspaceDTO(workspace)
719
+ });
720
+ },
721
+ async archive(ctx) {
722
+ if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
723
+ if (ctx.workspace.isPersonal) {
724
+ return (0, import_core2.setApiResponse)(
725
+ import_core2.HTTP.FORBIDDEN,
726
+ "FORBIDDEN",
727
+ "Personal workspaces cannot be archived"
728
+ );
729
+ }
730
+ await workspaces.archive(ctx.workspace.id, ctx.user.id);
731
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_ARCHIVED", "Workspace archived successfully.");
732
+ },
733
+ async restore(ctx) {
734
+ if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
735
+ await workspaces.restore(ctx.workspace.id);
736
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "WORKSPACE_RESTORED", "Workspace restored successfully.");
737
+ },
738
+ async getSettings(ctx) {
739
+ if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
740
+ const settings = await workspaces.getSettings(ctx.workspace.id);
741
+ return (0, import_core2.setApiResponse)(
742
+ import_core2.HTTP.OK,
743
+ "SETTINGS_FETCHED",
744
+ "Workspace settings retrieved successfully.",
745
+ {
746
+ settings: toSettingsDTO(settings)
747
+ }
748
+ );
749
+ },
750
+ async updateSettings(ctx) {
751
+ if (!ctx.workspace) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
752
+ const body = ctx.meta["body"];
753
+ if (!body || Object.keys(body).length === 0) {
754
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "No settings provided");
755
+ }
756
+ const patch = {};
757
+ for (const key of ["locale", "timezone", "currency", "dateFormat", "timeFormat"]) {
758
+ if (typeof body[key] === "string") patch[key] = body[key];
759
+ }
760
+ const settings = await workspaces.updateSettings(ctx.workspace.id, patch);
761
+ return (0, import_core2.setApiResponse)(
762
+ import_core2.HTTP.OK,
763
+ "SETTINGS_UPDATED",
764
+ "Workspace settings updated successfully.",
765
+ {
766
+ settings: toSettingsDTO(settings)
767
+ }
768
+ );
769
+ }
770
+ };
771
+ }
772
+
773
+ // src/controllers/member.controller.ts
774
+ var import_core3 = require("@fonderie/core");
775
+ function memberController(store) {
776
+ const members = new MemberModel(store);
777
+ return {
778
+ async list(ctx) {
779
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
780
+ const list = await members.list(ctx.workspace.id);
781
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "MEMBERS_FETCHED", "Members retrieved successfully.", {
782
+ members: list.map(toMemberDTO)
783
+ });
784
+ },
785
+ async remove(ctx) {
786
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
787
+ if (ctx.workspace.isPersonal) {
788
+ return (0, import_core3.setApiResponse)(
789
+ import_core3.HTTP.FORBIDDEN,
790
+ "FORBIDDEN",
791
+ "Personal workspaces do not support member management"
792
+ );
793
+ }
794
+ const params = ctx.meta["params"];
795
+ const userId = params?.["userId"];
796
+ if (!userId)
797
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
798
+ if (userId === ctx.user?.id) {
799
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.BAD_REQUEST, "INVALID_OPERATION", "Cannot remove yourself");
800
+ }
801
+ await members.remove(userId, ctx.workspace.id);
802
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "MEMBER_REMOVED", "Member removed successfully.");
803
+ },
804
+ async getUserRoles(ctx) {
805
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
806
+ const params = ctx.meta["params"];
807
+ const userId = params?.["userId"];
808
+ if (!userId)
809
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
810
+ const roles = await members.getUserRoles(userId, ctx.workspace.id);
811
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ROLES_FETCHED", "Member roles retrieved successfully.", {
812
+ roles: roles.map(toRoleDTO)
813
+ });
814
+ },
815
+ async addRole(ctx) {
816
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
817
+ const params = ctx.meta["params"];
818
+ const body = ctx.meta["body"];
819
+ const userId = params?.["userId"];
820
+ const roleId = body?.["roleId"] ?? params?.["roleId"];
821
+ if (!userId)
822
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
823
+ if (!roleId)
824
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
825
+ await members.addRole(userId, ctx.workspace.id, roleId);
826
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ROLE_ASSIGNED", "Role assigned successfully.");
827
+ },
828
+ async removeRole(ctx) {
829
+ if (!ctx.workspace) return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
830
+ const params = ctx.meta["params"];
831
+ const userId = params?.["userId"];
832
+ const roleId = params?.["roleId"];
833
+ if (!userId)
834
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
835
+ if (!roleId)
836
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
837
+ try {
838
+ await members.removeRole(userId, ctx.workspace.id, roleId);
839
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ROLE_REMOVED", "Role removed successfully.");
840
+ } catch (err) {
841
+ const message = err instanceof Error ? err.message : "Failed";
842
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.BAD_REQUEST, "OPERATION_FAILED", message);
843
+ }
844
+ }
845
+ };
846
+ }
847
+
848
+ // src/controllers/role.controller.ts
849
+ var import_core4 = require("@fonderie/core");
850
+ function roleController(store) {
851
+ const roles = new RoleModel(store);
852
+ return {
853
+ async create(ctx) {
854
+ if (!ctx.workspace) {
855
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
856
+ }
857
+ const body = ctx.meta["body"];
858
+ const name = body?.["name"];
859
+ const description = body?.["description"];
860
+ if (typeof name !== "string" || name.trim().length === 0) {
861
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
862
+ }
863
+ try {
864
+ const opts = {
865
+ name: name.trim(),
866
+ workspaceId: ctx.workspace.id
867
+ };
868
+ if (typeof description === "string") {
869
+ opts.description = description;
870
+ }
871
+ const role = await roles.create(opts);
872
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.CREATED, "ROLE_CREATED", "Role created successfully.", {
873
+ role: toRoleDTO(role)
874
+ });
875
+ } catch (err) {
876
+ const message = err instanceof Error ? err.message : "Failed to create role";
877
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "OPERATION_FAILED", message);
878
+ }
879
+ },
880
+ async list(ctx) {
881
+ if (!ctx.workspace) {
882
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
883
+ }
884
+ const list = await roles.list(ctx.workspace.id);
885
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLES_FETCHED", "Roles retrieved successfully.", {
886
+ roles: list.map(toRoleDTO)
887
+ });
888
+ },
889
+ async get(ctx) {
890
+ if (!ctx.workspace) {
891
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
892
+ }
893
+ const params = ctx.meta["params"];
894
+ const roleId = params?.["roleId"];
895
+ if (!roleId) {
896
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
897
+ }
898
+ const role = await roles.findById(roleId);
899
+ if (!role) {
900
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found");
901
+ }
902
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_FETCHED", "Role retrieved successfully.", {
903
+ role: toRoleDTO(role)
904
+ });
905
+ },
906
+ async update(ctx) {
907
+ if (!ctx.workspace) {
908
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
909
+ }
910
+ const params = ctx.meta["params"];
911
+ const body = ctx.meta["body"];
912
+ const roleId = params?.["roleId"];
913
+ if (!roleId) {
914
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
915
+ }
916
+ const opts = {};
917
+ if (typeof body?.["name"] === "string") {
918
+ opts.name = body["name"];
919
+ }
920
+ if (typeof body?.["description"] === "string") {
921
+ opts.description = body["description"];
922
+ }
923
+ if (body?.["description"] === null) {
924
+ opts.description = null;
925
+ }
926
+ if (typeof body?.["active"] === "boolean") {
927
+ opts.active = body["active"];
928
+ }
929
+ const role = await roles.update(roleId, opts);
930
+ if (!role) {
931
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Role not found or is a system role");
932
+ }
933
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_UPDATED", "Role updated successfully.", {
934
+ role: toRoleDTO(role)
935
+ });
936
+ },
937
+ async remove(ctx) {
938
+ if (!ctx.workspace) {
939
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
940
+ }
941
+ const params = ctx.meta["params"];
942
+ const roleId = params?.["roleId"];
943
+ if (!roleId) {
944
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
945
+ }
946
+ await roles.delete(roleId, ctx.workspace.id);
947
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "ROLE_DELETED", "Role deleted successfully.");
948
+ },
949
+ async setPermissions(ctx) {
950
+ if (!ctx.workspace) {
951
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
952
+ }
953
+ const params = ctx.meta["params"];
954
+ const body = ctx.meta["body"];
955
+ const roleId = params?.["roleId"];
956
+ if (!roleId) {
957
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
958
+ }
959
+ const perms = body?.["permissions"];
960
+ if (!Array.isArray(perms)) {
961
+ return (0, import_core4.setApiResponse)(
962
+ import_core4.HTTP.UNPROCESSABLE,
963
+ "INVALID_PARAMETER",
964
+ "permissions array is required"
965
+ );
966
+ }
967
+ const normalized = perms.map((p) => {
968
+ const perm = p;
969
+ return {
970
+ permissionKey: String(perm["permissionKey"] ?? ""),
971
+ canCreate: Boolean(perm["canCreate"]),
972
+ canRead: Boolean(perm["canRead"]),
973
+ canUpdate: Boolean(perm["canUpdate"]),
974
+ canDelete: Boolean(perm["canDelete"])
975
+ };
976
+ }).filter((p) => p.permissionKey.length > 0);
977
+ await roles.setPermissions(roleId, ctx.workspace.id, normalized);
978
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "PERMISSIONS_SET", "Role permissions updated successfully.");
979
+ }
980
+ };
981
+ }
982
+
983
+ // src/controllers/invitation.controller.ts
984
+ var import_core5 = require("@fonderie/core");
985
+ var import_events = require("@fonderie/events");
986
+ var import_billing = require("@fonderie/billing");
987
+
988
+ // src/services/invitations.ts
989
+ var import_node_crypto = require("crypto");
990
+ function generateToken() {
991
+ return (0, import_node_crypto.randomBytes)(32).toString("hex");
992
+ }
993
+ function generatePin() {
994
+ return Math.floor(1e5 + Math.random() * 9e5).toString();
995
+ }
996
+ function parseTtl(ttl) {
997
+ const units = {
998
+ s: 1e3,
999
+ m: 6e4,
1000
+ h: 36e5,
1001
+ d: 864e5
1002
+ };
1003
+ const match = ttl.match(/^(\d+)([smhd])$/);
1004
+ if (!match) return 7 * 864e5;
1005
+ const [, n, unit] = match;
1006
+ return parseInt(n, 10) * (units[unit] ?? 0);
1007
+ }
1008
+ var SELECT_INV = `
1009
+ id,
1010
+ workspace_id AS "workspaceId",
1011
+ email,
1012
+ role_id AS "roleId",
1013
+ token,
1014
+ pin,
1015
+ status,
1016
+ expires_at AS "expiresAt",
1017
+ created_at AS "createdAt"
1018
+ `;
1019
+ async function createInvitation(opts, store) {
1020
+ const token = generateToken();
1021
+ const pin = generatePin();
1022
+ const expiresAt = new Date(Date.now() + parseTtl(opts.ttl ?? "7d"));
1023
+ const [invitation] = await store.query(
1024
+ `INSERT INTO fonderie_workspace_invitations
1025
+ (workspace_id, email, role_id, token, pin, expires_at)
1026
+ VALUES ($1, $2, $3, $4, $5, $6)
1027
+ ON CONFLICT DO NOTHING
1028
+ RETURNING ${SELECT_INV}`,
1029
+ [opts.workspaceId, opts.email, opts.roleId, token, pin, expiresAt]
1030
+ );
1031
+ if (!invitation) {
1032
+ const [updated] = await store.query(
1033
+ `UPDATE fonderie_workspace_invitations
1034
+ SET token = $4, pin = $5, expires_at = $6, role_id = $3, status = 'PENDING'
1035
+ WHERE workspace_id = $1 AND email = $2 AND status = 'PENDING'
1036
+ RETURNING ${SELECT_INV}`,
1037
+ [opts.workspaceId, opts.email, opts.roleId, token, pin, expiresAt]
1038
+ );
1039
+ if (!updated) throw new Error("Failed to create invitation");
1040
+ return updated;
1041
+ }
1042
+ return invitation;
1043
+ }
1044
+ async function listInvitations(workspaceId, store) {
1045
+ return store.query(
1046
+ `SELECT ${SELECT_INV}
1047
+ FROM fonderie_workspace_invitations
1048
+ WHERE workspace_id = $1 AND status = 'PENDING'
1049
+ ORDER BY created_at DESC`,
1050
+ [workspaceId]
1051
+ );
1052
+ }
1053
+ async function cancelInvitation(invitationId, workspaceId, store) {
1054
+ await store.query(
1055
+ `UPDATE fonderie_workspace_invitations
1056
+ SET status = 'CANCELLED'
1057
+ WHERE id = $1 AND workspace_id = $2 AND status = 'PENDING'`,
1058
+ [invitationId, workspaceId]
1059
+ );
1060
+ }
1061
+ async function acceptInvitationByPin(opts, store) {
1062
+ const [inv] = await store.query(
1063
+ `SELECT id, workspace_id AS "workspaceId", role_id AS "roleId", expires_at AS "expiresAt"
1064
+ FROM fonderie_workspace_invitations
1065
+ WHERE pin = $1 AND status = 'PENDING'`,
1066
+ [opts.pin]
1067
+ );
1068
+ if (!inv) throw new Error("Invalid PIN");
1069
+ if (/* @__PURE__ */ new Date() > new Date(inv.expiresAt)) throw new Error("Invitation expired");
1070
+ await store.transaction(async (tx) => {
1071
+ await Promise.all([
1072
+ tx.query(
1073
+ `INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
1074
+ VALUES ($1, $2, $3, true)
1075
+ ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
1076
+ SET confirmed = true, removed = false`,
1077
+ [opts.userId, inv.workspaceId, inv.roleId]
1078
+ ),
1079
+ tx.query(`UPDATE fonderie_workspace_invitations SET status = 'ACCEPTED' WHERE id = $1`, [
1080
+ inv.id
1081
+ ])
1082
+ ]);
1083
+ });
1084
+ return { workspaceId: inv.workspaceId, roleId: inv.roleId };
1085
+ }
1086
+
1087
+ // src/models/invitation.model.ts
1088
+ var InvitationModel = class {
1089
+ constructor(store) {
1090
+ this.store = store;
1091
+ }
1092
+ store;
1093
+ create(opts) {
1094
+ return createInvitation(opts, this.store);
1095
+ }
1096
+ list(workspaceId) {
1097
+ return listInvitations(workspaceId, this.store);
1098
+ }
1099
+ cancel(invitationId, workspaceId) {
1100
+ return cancelInvitation(invitationId, workspaceId, this.store);
1101
+ }
1102
+ acceptByPin(opts) {
1103
+ return acceptInvitationByPin(opts, this.store);
1104
+ }
1105
+ };
1106
+
1107
+ // src/controllers/invitation.controller.ts
1108
+ function invitationController(store, ttl, bus) {
1109
+ const invitations = new InvitationModel(store);
1110
+ return {
1111
+ async list(ctx) {
1112
+ if (!ctx.workspace) return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1113
+ const list = await invitations.list(ctx.workspace.id);
1114
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "INVITATIONS_FETCHED", "Invitations retrieved successfully.", {
1115
+ invitations: list.map(toInvitationDTO)
1116
+ });
1117
+ },
1118
+ async invite(ctx) {
1119
+ if (!ctx.workspace) return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1120
+ if (ctx.workspace.isPersonal) {
1121
+ return (0, import_core5.setApiResponse)(
1122
+ import_core5.HTTP.FORBIDDEN,
1123
+ "FORBIDDEN",
1124
+ "Personal workspaces do not support invitations"
1125
+ );
1126
+ }
1127
+ const body = ctx.meta["body"];
1128
+ const entries = Array.isArray(body) ? body : [body];
1129
+ if (!entries.length) {
1130
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "at least one invite is required");
1131
+ }
1132
+ for (const entry of entries) {
1133
+ if (typeof entry?.["email"] !== "string") {
1134
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required for every invite");
1135
+ }
1136
+ }
1137
+ const seatLimit = (0, import_billing.getPlanLimit)(ctx, "seats");
1138
+ if (seatLimit !== null) {
1139
+ const [countRow] = await store.query(
1140
+ `SELECT COUNT(*) AS count FROM fonderie_role_user_workspaces
1141
+ WHERE workspace_id = $1 AND removed = false AND suspended = false`,
1142
+ [ctx.workspace.id]
1143
+ );
1144
+ const total = parseInt(countRow.count, 10);
1145
+ const occupied = ctx.workspace.isPersonal ? total : Math.max(0, total - 1);
1146
+ if (occupied + entries.length > seatLimit) {
1147
+ return (0, import_core5.setApiResponse)(
1148
+ import_core5.HTTP.PAYMENT_REQUIRED,
1149
+ "SEAT_LIMIT_REACHED",
1150
+ `Your plan allows ${seatLimit} seat${seatLimit === 1 ? "" : "s"}. Upgrade to invite more members.`,
1151
+ { limit: seatLimit }
1152
+ );
1153
+ }
1154
+ }
1155
+ let defaultRoleId;
1156
+ const needsDefault = entries.some((e) => !e["roleId"]);
1157
+ if (needsDefault) {
1158
+ const [row] = await store.query(
1159
+ `SELECT id FROM fonderie_roles
1160
+ WHERE name = 'ADMIN' AND workspace_id = $1 LIMIT 1`,
1161
+ [ctx.workspace.id]
1162
+ );
1163
+ defaultRoleId = row?.id;
1164
+ if (!defaultRoleId) {
1165
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Default role not found");
1166
+ }
1167
+ }
1168
+ const results = await Promise.all(
1169
+ entries.map(async (entry) => {
1170
+ const email = entry["email"];
1171
+ const resolvedRoleId = entry["roleId"] ?? defaultRoleId;
1172
+ const invitation = await invitations.create({
1173
+ workspaceId: ctx.workspace.id,
1174
+ email,
1175
+ roleId: resolvedRoleId,
1176
+ ttl
1177
+ });
1178
+ bus?.emit(import_events.NOTIFICATION_EVENT, {
1179
+ type: MESSAGE_KEYS.workspaceInvitation,
1180
+ recipient: { email, phone: null, deviceToken: null },
1181
+ data: { token: invitation.token, pin: invitation.pin }
1182
+ }).catch(() => {
1183
+ });
1184
+ return { invitationId: invitation.id, email };
1185
+ })
1186
+ );
1187
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.CREATED, "INVITATIONS_SENT", "Invitations sent successfully.", {
1188
+ invitations: results
1189
+ });
1190
+ },
1191
+ async cancel(ctx) {
1192
+ if (!ctx.workspace) return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
1193
+ const params = ctx.meta["params"];
1194
+ const invitationId = params?.["inviteId"];
1195
+ if (!invitationId)
1196
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "inviteId is required");
1197
+ await invitations.cancel(invitationId, ctx.workspace.id);
1198
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "INVITATION_CANCELLED", "Invitation cancelled successfully.");
1199
+ },
1200
+ async accept(ctx) {
1201
+ const body = ctx.meta["body"];
1202
+ const pin = body?.["pin"];
1203
+ if (typeof pin !== "string") {
1204
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "pin is required");
1205
+ }
1206
+ try {
1207
+ const { workspaceId } = await invitations.acceptByPin({ pin, userId: ctx.user.id });
1208
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
1209
+ workspaceId
1210
+ });
1211
+ } catch (err) {
1212
+ const message = err instanceof Error ? err.message : "Invalid invitation";
1213
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "INVITATION_FAILED", message);
1214
+ }
1215
+ }
1216
+ };
1217
+ }
1218
+
1219
+ // src/routes.ts
1220
+ function buildWorkspaceRoutes(store, config, bus) {
1221
+ const ttl = config.invitationTtl ?? "7d";
1222
+ const wsCtx = withWorkspace(store);
1223
+ const workspace = workspaceController(store, config);
1224
+ const member = memberController(store);
1225
+ const role = roleController(store);
1226
+ const invitation = invitationController(store, ttl, bus);
1227
+ return [
1228
+ // ── Workspace creation + listing (no workspace context required)
1229
+ ["POST", "/workspaces", import_middlewares.requireAuth, workspace.create],
1230
+ ["GET", "/workspaces", import_middlewares.requireAuth, workspace.list],
1231
+ // ── Members (workspace resolved from X-Workspace-ID header)
1232
+ ["GET", "/workspaces/members", import_middlewares.requireAuth, wsCtx, member.list],
1233
+ ["DELETE", "/workspaces/members/:userId", import_middlewares.requireAuth, wsCtx, member.remove],
1234
+ ["GET", "/workspaces/members/:userId/roles", import_middlewares.requireAuth, wsCtx, member.getUserRoles],
1235
+ ["POST", "/workspaces/members/:userId/roles", import_middlewares.requireAuth, wsCtx, member.addRole],
1236
+ ["DELETE", "/workspaces/members/:userId/roles/:roleId", import_middlewares.requireAuth, wsCtx, member.removeRole],
1237
+ // ── Invitations
1238
+ ["GET", "/workspaces/invitations", import_middlewares.requireAuth, wsCtx, invitation.list],
1239
+ ["POST", "/workspaces/invitations", import_middlewares.requireAuth, wsCtx, invitation.invite],
1240
+ ["DELETE", "/workspaces/invitations/:inviteId", import_middlewares.requireAuth, wsCtx, invitation.cancel],
1241
+ ["POST", "/workspaces/invitations/accept", import_middlewares.requireAuth, invitation.accept],
1242
+ // ── Roles
1243
+ ["POST", "/workspaces/roles", import_middlewares.requireAuth, wsCtx, role.create],
1244
+ ["GET", "/workspaces/roles", import_middlewares.requireAuth, wsCtx, role.list],
1245
+ ["GET", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, role.get],
1246
+ ["PUT", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, role.update],
1247
+ ["DELETE", "/workspaces/roles/:roleId", import_middlewares.requireAuth, wsCtx, role.remove],
1248
+ ["POST", "/workspaces/roles/:roleId/permissions", import_middlewares.requireAuth, wsCtx, role.setPermissions],
1249
+ // ── Workspace lifecycle
1250
+ ["POST", "/workspaces/archive", import_middlewares.requireAuth, wsCtx, workspace.archive],
1251
+ ["POST", "/workspaces/restore", import_middlewares.requireAuth, wsCtx, workspace.restore],
1252
+ ["GET", "/workspaces/settings", import_middlewares.requireAuth, wsCtx, workspace.getSettings],
1253
+ ["PUT", "/workspaces/settings", import_middlewares.requireAuth, wsCtx, workspace.updateSettings],
1254
+ // ── Path-based lookup by ID (admin / cross-workspace use)
1255
+ ["GET", "/workspaces/:id", import_middlewares.requireAuth, wsCtx, workspace.get],
1256
+ // ── Update current workspace — ID resolved from X-Workspace-ID header
1257
+ // (or personal workspace fallback when header is absent)
1258
+ ["PUT", "/workspaces", import_middlewares.requireAuth, wsCtx, workspace.update]
1259
+ ];
1260
+ }
1261
+
1262
+ // src/module.ts
1263
+ var AUTH_USER_REGISTERED = "fonderie.user.registered";
1264
+ var WorkspacesModule = class {
1265
+ constructor(store, config = {}, bus) {
1266
+ this.store = store;
1267
+ this.config = config;
1268
+ this.bus = bus;
1269
+ }
1270
+ store;
1271
+ config;
1272
+ bus;
1273
+ name = "@fonderie/workspaces";
1274
+ deps = ["@fonderie/auth", "@fonderie/billing"];
1275
+ install(app) {
1276
+ if (this.bus && this.config.personalWorkspace !== false) {
1277
+ this.bus.on(
1278
+ AUTH_USER_REGISTERED,
1279
+ (payload) => this.provisionPersonalWorkspace(payload),
1280
+ "workspaces"
1281
+ );
1282
+ }
1283
+ const routes = buildWorkspaceRoutes(this.store, this.config, this.bus);
1284
+ for (const [method, path, ...handlers] of routes) {
1285
+ app.addRoute(method, path, ...handlers);
1286
+ }
1287
+ }
1288
+ async provisionPersonalWorkspace(payload) {
1289
+ const name = "My Workspace";
1290
+ const slug = `${payload.userId}-personal`;
1291
+ let workspaceId;
1292
+ await this.store.transaction(async (tx) => {
1293
+ const wsModel = new WorkspaceModel(tx);
1294
+ const roleModel = new RoleModel(tx);
1295
+ const memModel = new MemberModel(tx);
1296
+ const ws = await wsModel.createPersonal({ name, slug, ownerId: payload.userId });
1297
+ if (!ws) return;
1298
+ workspaceId = ws.id;
1299
+ const adminRole = await roleModel.findSystem("ADMIN");
1300
+ if (!adminRole) throw new Error("System ADMIN role not found");
1301
+ await memModel.add({
1302
+ userId: payload.userId,
1303
+ workspaceId: ws.id,
1304
+ roleId: adminRole.id,
1305
+ confirmed: true
1306
+ });
1307
+ });
1308
+ if (workspaceId) {
1309
+ this.bus?.emit(EVENT_KEYS.personalWorkspaceCreated, {
1310
+ workspaceId,
1311
+ userId: payload.userId
1312
+ }).catch(() => {
1313
+ });
1314
+ }
1315
+ }
1316
+ };
1317
+
1318
+ // src/middlewares/require-workspace.ts
1319
+ var import_core6 = require("@fonderie/core");
1320
+ var requireWorkspace = async (ctx, next) => {
1321
+ if (!ctx.workspace) {
1322
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
1323
+ }
1324
+ return next();
1325
+ };
1326
+ // Annotate the CommonJS export names for ESM import in node:
1327
+ 0 && (module.exports = {
1328
+ EVENT_KEYS,
1329
+ MESSAGE_KEYS,
1330
+ WorkspacesModule,
1331
+ requireWorkspace,
1332
+ toInvitationDTO,
1333
+ toMemberDTO,
1334
+ toRoleDTO,
1335
+ toSettingsDTO,
1336
+ toWorkspaceDTO,
1337
+ withWorkspace
1338
+ });
1339
+ //# sourceMappingURL=index.cjs.map