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