@shipfox/api-workspaces 5.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/README.md +2 -2
  4. package/dist/config.d.ts +0 -8
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -50
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/entities/invitation.d.ts +1 -0
  9. package/dist/core/entities/invitation.d.ts.map +1 -1
  10. package/dist/core/entities/invitation.js.map +1 -1
  11. package/dist/core/invitations.d.ts +21 -2
  12. package/dist/core/invitations.d.ts.map +1 -1
  13. package/dist/core/invitations.js +49 -12
  14. package/dist/core/invitations.js.map +1 -1
  15. package/dist/core/workspaces.d.ts +1 -1
  16. package/dist/core/workspaces.d.ts.map +1 -1
  17. package/dist/core/workspaces.js +13 -1
  18. package/dist/core/workspaces.js.map +1 -1
  19. package/dist/db/db.d.ts +34 -0
  20. package/dist/db/db.d.ts.map +1 -1
  21. package/dist/db/index.d.ts +2 -2
  22. package/dist/db/index.d.ts.map +1 -1
  23. package/dist/db/index.js +1 -1
  24. package/dist/db/index.js.map +1 -1
  25. package/dist/db/invitations.d.ts +15 -8
  26. package/dist/db/invitations.d.ts.map +1 -1
  27. package/dist/db/invitations.js +70 -22
  28. package/dist/db/invitations.js.map +1 -1
  29. package/dist/db/schema/invitations.d.ts +17 -0
  30. package/dist/db/schema/invitations.d.ts.map +1 -1
  31. package/dist/db/schema/invitations.js +4 -0
  32. package/dist/db/schema/invitations.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +6 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/presentation/inter-module.d.ts +4 -0
  38. package/dist/presentation/inter-module.d.ts.map +1 -0
  39. package/dist/presentation/inter-module.js +74 -0
  40. package/dist/presentation/inter-module.js.map +1 -0
  41. package/dist/presentation/subscribers/on-invitation-send-requested.js +1 -1
  42. package/dist/presentation/subscribers/on-invitation-send-requested.js.map +1 -1
  43. package/dist/tsconfig.test.tsbuildinfo +1 -1
  44. package/drizzle/0004_tired_talon.sql +1 -0
  45. package/drizzle/meta/0004_snapshot.json +448 -0
  46. package/drizzle/meta/_journal.json +7 -0
  47. package/package.json +18 -10
  48. package/src/config.ts +1 -44
  49. package/src/core/entities/invitation.ts +1 -0
  50. package/src/core/invitations.test.ts +35 -2
  51. package/src/core/invitations.ts +72 -17
  52. package/src/core/workspaces.test.ts +28 -0
  53. package/src/core/workspaces.ts +19 -2
  54. package/src/db/index.ts +1 -6
  55. package/src/db/invitations.test.ts +237 -15
  56. package/src/db/invitations.ts +78 -50
  57. package/src/db/schema/invitations.ts +2 -0
  58. package/src/db/workspaces.test.ts +3 -2
  59. package/src/index.test.ts +10 -1
  60. package/src/index.ts +8 -1
  61. package/src/presentation/inter-module.ts +94 -0
  62. package/src/presentation/routes/invitations/create.test.ts +19 -1
  63. package/src/presentation/subscribers/invitation-send-requested.test.ts +1 -1
  64. package/src/presentation/subscribers/on-invitation-send-requested.ts +1 -1
  65. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1 @@
1
+ ALTER TABLE "workspaces_invitations" ADD COLUMN "revoked_at" timestamp with time zone;
@@ -0,0 +1,448 @@
1
+ {
2
+ "id": "8eaf4dda-d82b-4389-9d02-2c2da9258419",
3
+ "prevId": "86af97dc-249d-4cc9-98bc-52681d4ca30f",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.workspaces_invitations": {
8
+ "name": "workspaces_invitations",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "uuidv7()"
17
+ },
18
+ "workspace_id": {
19
+ "name": "workspace_id",
20
+ "type": "uuid",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "email": {
25
+ "name": "email",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "hashed_token": {
31
+ "name": "hashed_token",
32
+ "type": "text",
33
+ "primaryKey": false,
34
+ "notNull": true
35
+ },
36
+ "expires_at": {
37
+ "name": "expires_at",
38
+ "type": "timestamp with time zone",
39
+ "primaryKey": false,
40
+ "notNull": true
41
+ },
42
+ "revoked_at": {
43
+ "name": "revoked_at",
44
+ "type": "timestamp with time zone",
45
+ "primaryKey": false,
46
+ "notNull": false
47
+ },
48
+ "accepted_at": {
49
+ "name": "accepted_at",
50
+ "type": "timestamp with time zone",
51
+ "primaryKey": false,
52
+ "notNull": false
53
+ },
54
+ "accepted_by_user_id": {
55
+ "name": "accepted_by_user_id",
56
+ "type": "uuid",
57
+ "primaryKey": false,
58
+ "notNull": false
59
+ },
60
+ "invited_by_user_id": {
61
+ "name": "invited_by_user_id",
62
+ "type": "uuid",
63
+ "primaryKey": false,
64
+ "notNull": true
65
+ },
66
+ "invited_by_display": {
67
+ "name": "invited_by_display",
68
+ "type": "text",
69
+ "primaryKey": false,
70
+ "notNull": false
71
+ },
72
+ "created_at": {
73
+ "name": "created_at",
74
+ "type": "timestamp with time zone",
75
+ "primaryKey": false,
76
+ "notNull": true,
77
+ "default": "now()"
78
+ },
79
+ "updated_at": {
80
+ "name": "updated_at",
81
+ "type": "timestamp with time zone",
82
+ "primaryKey": false,
83
+ "notNull": true,
84
+ "default": "now()"
85
+ }
86
+ },
87
+ "indexes": {
88
+ "workspaces_invitations_hashed_token_unique": {
89
+ "name": "workspaces_invitations_hashed_token_unique",
90
+ "columns": [
91
+ {
92
+ "expression": "hashed_token",
93
+ "isExpression": false,
94
+ "asc": true,
95
+ "nulls": "last"
96
+ }
97
+ ],
98
+ "isUnique": true,
99
+ "concurrently": false,
100
+ "method": "btree",
101
+ "with": {}
102
+ },
103
+ "workspaces_invitations_workspace_email_idx": {
104
+ "name": "workspaces_invitations_workspace_email_idx",
105
+ "columns": [
106
+ {
107
+ "expression": "workspace_id",
108
+ "isExpression": false,
109
+ "asc": true,
110
+ "nulls": "last"
111
+ },
112
+ {
113
+ "expression": "email",
114
+ "isExpression": false,
115
+ "asc": true,
116
+ "nulls": "last"
117
+ }
118
+ ],
119
+ "isUnique": false,
120
+ "concurrently": false,
121
+ "method": "btree",
122
+ "with": {}
123
+ }
124
+ },
125
+ "foreignKeys": {
126
+ "workspaces_invitations_workspace_id_workspaces_id_fk": {
127
+ "name": "workspaces_invitations_workspace_id_workspaces_id_fk",
128
+ "tableFrom": "workspaces_invitations",
129
+ "tableTo": "workspaces",
130
+ "columnsFrom": ["workspace_id"],
131
+ "columnsTo": ["id"],
132
+ "onDelete": "cascade",
133
+ "onUpdate": "no action"
134
+ }
135
+ },
136
+ "compositePrimaryKeys": {},
137
+ "uniqueConstraints": {},
138
+ "policies": {},
139
+ "checkConstraints": {},
140
+ "isRLSEnabled": false
141
+ },
142
+ "public.workspaces_memberships": {
143
+ "name": "workspaces_memberships",
144
+ "schema": "",
145
+ "columns": {
146
+ "id": {
147
+ "name": "id",
148
+ "type": "uuid",
149
+ "primaryKey": true,
150
+ "notNull": true,
151
+ "default": "uuidv7()"
152
+ },
153
+ "user_id": {
154
+ "name": "user_id",
155
+ "type": "uuid",
156
+ "primaryKey": false,
157
+ "notNull": true
158
+ },
159
+ "user_email": {
160
+ "name": "user_email",
161
+ "type": "text",
162
+ "primaryKey": false,
163
+ "notNull": true
164
+ },
165
+ "user_name": {
166
+ "name": "user_name",
167
+ "type": "text",
168
+ "primaryKey": false,
169
+ "notNull": false
170
+ },
171
+ "workspace_id": {
172
+ "name": "workspace_id",
173
+ "type": "uuid",
174
+ "primaryKey": false,
175
+ "notNull": true
176
+ },
177
+ "created_at": {
178
+ "name": "created_at",
179
+ "type": "timestamp with time zone",
180
+ "primaryKey": false,
181
+ "notNull": true,
182
+ "default": "now()"
183
+ },
184
+ "updated_at": {
185
+ "name": "updated_at",
186
+ "type": "timestamp with time zone",
187
+ "primaryKey": false,
188
+ "notNull": true,
189
+ "default": "now()"
190
+ }
191
+ },
192
+ "indexes": {
193
+ "workspaces_memberships_user_workspace_unique": {
194
+ "name": "workspaces_memberships_user_workspace_unique",
195
+ "columns": [
196
+ {
197
+ "expression": "user_id",
198
+ "isExpression": false,
199
+ "asc": true,
200
+ "nulls": "last"
201
+ },
202
+ {
203
+ "expression": "workspace_id",
204
+ "isExpression": false,
205
+ "asc": true,
206
+ "nulls": "last"
207
+ }
208
+ ],
209
+ "isUnique": true,
210
+ "concurrently": false,
211
+ "method": "btree",
212
+ "with": {}
213
+ },
214
+ "workspaces_memberships_workspace_id_idx": {
215
+ "name": "workspaces_memberships_workspace_id_idx",
216
+ "columns": [
217
+ {
218
+ "expression": "workspace_id",
219
+ "isExpression": false,
220
+ "asc": true,
221
+ "nulls": "last"
222
+ }
223
+ ],
224
+ "isUnique": false,
225
+ "concurrently": false,
226
+ "method": "btree",
227
+ "with": {}
228
+ }
229
+ },
230
+ "foreignKeys": {
231
+ "workspaces_memberships_workspace_id_workspaces_id_fk": {
232
+ "name": "workspaces_memberships_workspace_id_workspaces_id_fk",
233
+ "tableFrom": "workspaces_memberships",
234
+ "tableTo": "workspaces",
235
+ "columnsFrom": ["workspace_id"],
236
+ "columnsTo": ["id"],
237
+ "onDelete": "cascade",
238
+ "onUpdate": "no action"
239
+ }
240
+ },
241
+ "compositePrimaryKeys": {},
242
+ "uniqueConstraints": {},
243
+ "policies": {},
244
+ "checkConstraints": {},
245
+ "isRLSEnabled": false
246
+ },
247
+ "public.workspaces_outbox": {
248
+ "name": "workspaces_outbox",
249
+ "schema": "",
250
+ "columns": {
251
+ "id": {
252
+ "name": "id",
253
+ "type": "uuid",
254
+ "primaryKey": true,
255
+ "notNull": true,
256
+ "default": "uuidv7()"
257
+ },
258
+ "event_type": {
259
+ "name": "event_type",
260
+ "type": "text",
261
+ "primaryKey": false,
262
+ "notNull": true
263
+ },
264
+ "ordering_key": {
265
+ "name": "ordering_key",
266
+ "type": "text",
267
+ "primaryKey": false,
268
+ "notNull": false
269
+ },
270
+ "payload": {
271
+ "name": "payload",
272
+ "type": "jsonb",
273
+ "primaryKey": false,
274
+ "notNull": true
275
+ },
276
+ "created_at": {
277
+ "name": "created_at",
278
+ "type": "timestamp with time zone",
279
+ "primaryKey": false,
280
+ "notNull": true,
281
+ "default": "now()"
282
+ },
283
+ "dispatched_at": {
284
+ "name": "dispatched_at",
285
+ "type": "timestamp with time zone",
286
+ "primaryKey": false,
287
+ "notNull": false
288
+ },
289
+ "dispatch_attempts": {
290
+ "name": "dispatch_attempts",
291
+ "type": "integer",
292
+ "primaryKey": false,
293
+ "notNull": true,
294
+ "default": 0
295
+ },
296
+ "next_dispatch_at": {
297
+ "name": "next_dispatch_at",
298
+ "type": "timestamp with time zone",
299
+ "primaryKey": false,
300
+ "notNull": true,
301
+ "default": "now()"
302
+ },
303
+ "last_dispatch_error": {
304
+ "name": "last_dispatch_error",
305
+ "type": "jsonb",
306
+ "primaryKey": false,
307
+ "notNull": false
308
+ },
309
+ "last_dispatch_failed_at": {
310
+ "name": "last_dispatch_failed_at",
311
+ "type": "timestamp with time zone",
312
+ "primaryKey": false,
313
+ "notNull": false
314
+ },
315
+ "dead_lettered_at": {
316
+ "name": "dead_lettered_at",
317
+ "type": "timestamp with time zone",
318
+ "primaryKey": false,
319
+ "notNull": false
320
+ }
321
+ },
322
+ "indexes": {
323
+ "workspaces_outbox_pending_idx": {
324
+ "name": "workspaces_outbox_pending_idx",
325
+ "columns": [
326
+ {
327
+ "expression": "next_dispatch_at",
328
+ "isExpression": false,
329
+ "asc": true,
330
+ "nulls": "last"
331
+ },
332
+ {
333
+ "expression": "created_at",
334
+ "isExpression": false,
335
+ "asc": true,
336
+ "nulls": "last"
337
+ }
338
+ ],
339
+ "isUnique": false,
340
+ "where": "\"dispatched_at\" IS NULL AND \"dead_lettered_at\" IS NULL",
341
+ "concurrently": false,
342
+ "method": "btree",
343
+ "with": {}
344
+ },
345
+ "workspaces_outbox_dispatched_retention_idx": {
346
+ "name": "workspaces_outbox_dispatched_retention_idx",
347
+ "columns": [
348
+ {
349
+ "expression": "dispatched_at",
350
+ "isExpression": false,
351
+ "asc": true,
352
+ "nulls": "last"
353
+ },
354
+ {
355
+ "expression": "id",
356
+ "isExpression": false,
357
+ "asc": true,
358
+ "nulls": "last"
359
+ }
360
+ ],
361
+ "isUnique": false,
362
+ "where": "\"dispatched_at\" IS NOT NULL",
363
+ "concurrently": false,
364
+ "method": "btree",
365
+ "with": {}
366
+ }
367
+ },
368
+ "foreignKeys": {},
369
+ "compositePrimaryKeys": {},
370
+ "uniqueConstraints": {},
371
+ "policies": {},
372
+ "checkConstraints": {},
373
+ "isRLSEnabled": false
374
+ },
375
+ "public.workspaces": {
376
+ "name": "workspaces",
377
+ "schema": "",
378
+ "columns": {
379
+ "id": {
380
+ "name": "id",
381
+ "type": "uuid",
382
+ "primaryKey": true,
383
+ "notNull": true,
384
+ "default": "uuidv7()"
385
+ },
386
+ "name": {
387
+ "name": "name",
388
+ "type": "text",
389
+ "primaryKey": false,
390
+ "notNull": true
391
+ },
392
+ "status": {
393
+ "name": "status",
394
+ "type": "workspaces_workspace_status",
395
+ "typeSchema": "public",
396
+ "primaryKey": false,
397
+ "notNull": true,
398
+ "default": "'active'"
399
+ },
400
+ "settings": {
401
+ "name": "settings",
402
+ "type": "jsonb",
403
+ "primaryKey": false,
404
+ "notNull": true,
405
+ "default": "'{}'::jsonb"
406
+ },
407
+ "created_at": {
408
+ "name": "created_at",
409
+ "type": "timestamp with time zone",
410
+ "primaryKey": false,
411
+ "notNull": true,
412
+ "default": "now()"
413
+ },
414
+ "updated_at": {
415
+ "name": "updated_at",
416
+ "type": "timestamp with time zone",
417
+ "primaryKey": false,
418
+ "notNull": true,
419
+ "default": "now()"
420
+ }
421
+ },
422
+ "indexes": {},
423
+ "foreignKeys": {},
424
+ "compositePrimaryKeys": {},
425
+ "uniqueConstraints": {},
426
+ "policies": {},
427
+ "checkConstraints": {},
428
+ "isRLSEnabled": false
429
+ }
430
+ },
431
+ "enums": {
432
+ "public.workspaces_workspace_status": {
433
+ "name": "workspaces_workspace_status",
434
+ "schema": "public",
435
+ "values": ["active", "suspended", "deleted"]
436
+ }
437
+ },
438
+ "schemas": {},
439
+ "sequences": {},
440
+ "roles": {},
441
+ "policies": {},
442
+ "views": {},
443
+ "_meta": {
444
+ "columns": {},
445
+ "schemas": {},
446
+ "tables": {}
447
+ }
448
+ }
@@ -29,6 +29,13 @@
29
29
  "when": 1782352229611,
30
30
  "tag": "0003_outgoing_enchantress",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1784571790437,
37
+ "tag": "0004_tired_talon",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-workspaces",
3
3
  "license": "MIT",
4
- "version": "5.0.0",
4
+ "version": "7.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -19,23 +19,29 @@
19
19
  ".": {
20
20
  "types": "./dist/index.d.ts",
21
21
  "default": "./dist/index.js"
22
+ },
23
+ "./errors": {
24
+ "types": "./dist/core/errors.d.ts",
25
+ "default": "./dist/core/errors.js"
22
26
  }
23
27
  },
24
28
  "dependencies": {
25
29
  "drizzle-orm": "^0.45.2",
26
30
  "zod": "^4.4.3",
27
- "@shipfox/api-auth-context": "5.0.0",
28
- "@shipfox/api-workspaces-dto": "5.0.0",
31
+ "@shipfox/api-common-dto": "6.0.0",
32
+ "@shipfox/api-auth-context": "6.0.0",
33
+ "@shipfox/api-workspaces-dto": "6.0.0",
29
34
  "@shipfox/config": "1.2.2",
30
- "@shipfox/node-drizzle": "0.3.1",
31
- "@shipfox/node-fastify": "0.2.3",
32
- "@shipfox/node-email": "0.2.2",
33
- "@shipfox/node-mailer": "0.1.4",
34
- "@shipfox/node-module": "0.3.2",
35
+ "@shipfox/inter-module": "0.2.0",
36
+ "@shipfox/node-drizzle": "0.3.2",
37
+ "@shipfox/node-fastify": "0.2.4",
38
+ "@shipfox/node-email": "0.3.1",
39
+ "@shipfox/node-mailer": "0.2.0",
40
+ "@shipfox/node-module": "0.4.0",
35
41
  "@shipfox/node-opentelemetry": "0.5.2",
36
- "@shipfox/node-outbox": "0.2.3",
42
+ "@shipfox/node-outbox": "0.2.4",
37
43
  "@shipfox/node-postgres": "0.4.2",
38
- "@shipfox/node-tokens": "0.2.1"
44
+ "@shipfox/node-tokens": "0.3.0"
39
45
  },
40
46
  "devDependencies": {
41
47
  "@types/pg": "^8.15.5",
@@ -44,6 +50,7 @@
44
50
  "fastify-type-provider-zod": "^6.0.0",
45
51
  "fishery": "^2.4.0",
46
52
  "@shipfox/biome": "1.8.2",
53
+ "@shipfox/depcruise": "1.0.2",
47
54
  "@shipfox/swc": "1.2.6",
48
55
  "@shipfox/ts-config": "1.3.8",
49
56
  "@shipfox/typescript": "1.1.7",
@@ -53,6 +60,7 @@
53
60
  "build": "shipfox-swc",
54
61
  "check": "shipfox-biome-check",
55
62
  "check:fix": "shipfox-biome-check --write",
63
+ "depcruise": "shipfox-depcruise",
56
64
  "test": "shipfox-vitest-run",
57
65
  "test:watch": "shipfox-vitest-watch",
58
66
  "type": "shipfox-tsc-check",
package/src/config.ts CHANGED
@@ -1,51 +1,8 @@
1
- import {createConfig, num, str} from '@shipfox/config';
2
- import {createConsoleMailer, createSmtpMailer, type Mailer} from '@shipfox/node-mailer';
1
+ import {createConfig, str} from '@shipfox/config';
3
2
 
4
3
  export const config = createConfig({
5
4
  CLIENT_BASE_URL: str({
6
5
  desc: 'Base URL of the client app. Used to build links in workspace invitation emails.',
7
6
  default: 'http://localhost:5173',
8
7
  }),
9
- MAILER_TRANSPORT: str({
10
- desc: 'How emails are delivered. Use console to print them to the log, or smtp to send them through an SMTP server.',
11
- choices: ['console', 'smtp'],
12
- default: 'console',
13
- }),
14
- MAILER_FROM: str({
15
- desc: 'Sender address shown on outgoing emails.',
16
- default: 'noreply@shipfox.local',
17
- }),
18
- SMTP_HOST: str({
19
- desc: 'Hostname of the SMTP server. Required when MAILER_TRANSPORT is smtp.',
20
- default: undefined,
21
- }),
22
- SMTP_PORT: num({
23
- desc: 'Port of the SMTP server.',
24
- default: 587,
25
- }),
26
- SMTP_USER: str({
27
- desc: 'Username for SMTP authentication. Leave it unset if the server needs no login.',
28
- default: undefined,
29
- }),
30
- SMTP_PASSWORD: str({
31
- desc: 'Password for SMTP authentication. Leave it unset if the server needs no login.',
32
- default: undefined,
33
- }),
34
8
  });
35
-
36
- function createMailer(): Mailer {
37
- if (config.MAILER_TRANSPORT === 'smtp') {
38
- if (!config.SMTP_HOST) throw new Error('SMTP_HOST is required when MAILER_TRANSPORT=smtp');
39
- return createSmtpMailer({
40
- host: config.SMTP_HOST,
41
- port: config.SMTP_PORT,
42
- ...(config.SMTP_USER ? {user: config.SMTP_USER} : {}),
43
- ...(config.SMTP_PASSWORD ? {password: config.SMTP_PASSWORD} : {}),
44
- from: config.MAILER_FROM,
45
- });
46
- }
47
-
48
- return createConsoleMailer({from: config.MAILER_FROM});
49
- }
50
-
51
- export const mailer = createMailer();
@@ -4,6 +4,7 @@ export interface Invitation {
4
4
  email: string;
5
5
  hashedToken: string;
6
6
  expiresAt: Date;
7
+ revokedAt: Date | null;
7
8
  acceptedAt: Date | null;
8
9
  acceptedByUserId: string | null;
9
10
  invitedByUserId: string;
@@ -1,7 +1,9 @@
1
1
  import {WORKSPACES_INVITATION_SEND_REQUESTED} from '@shipfox/api-workspaces-dto';
2
2
  import type {Mailer, MailMessage} from '@shipfox/node-mailer';
3
+ import {hashOpaqueToken} from '@shipfox/node-tokens';
3
4
  import {and, desc, eq, sql} from 'drizzle-orm';
4
5
  import {db} from '#db/db.js';
6
+ import {findInvitationByToken, revokeInvitation} from '#db/invitations.js';
5
7
  import {createMembership} from '#db/memberships.js';
6
8
  import {workspacesOutbox} from '#db/schema/outbox.js';
7
9
  import {userFactory, workspaceFactory} from '#test/index.js';
@@ -10,7 +12,11 @@ import {
10
12
  OpenInvitationExistsError,
11
13
  TokenAlreadyUsedError,
12
14
  } from './errors.js';
13
- import {acceptWorkspaceInvitation, createWorkspaceInvitation} from './invitations.js';
15
+ import {
16
+ acceptWorkspaceInvitation,
17
+ createWorkspaceInvitation,
18
+ previewInvitation,
19
+ } from './invitations.js';
14
20
 
15
21
  const testConfig = vi.hoisted(
16
22
  (): {
@@ -241,11 +247,38 @@ describe('invitations core', () => {
241
247
  const result = await acceptWorkspaceInvitation({
242
248
  token,
243
249
  userId: invitee.userId,
244
- email: invitee.email,
250
+ email: ` ${invitee.email.toUpperCase()} `,
245
251
  });
246
252
  expect(result.membership.workspaceId).toBe(workspace.id);
247
253
 
248
254
  const reused = acceptWorkspaceInvitation({token, userId: invitee.userId, email: invitee.email});
249
255
  await expect(reused).rejects.toBeInstanceOf(TokenAlreadyUsedError);
250
256
  });
257
+
258
+ test('previewInvitation treats a revoked invitation as invalid', async () => {
259
+ const inviter = userFactory.build();
260
+ const workspace = await workspaceFactory.create();
261
+ await createMembership({
262
+ userId: inviter.userId,
263
+ userEmail: inviter.email,
264
+ userName: inviter.name,
265
+ workspaceId: workspace.id,
266
+ });
267
+ const email = `revoked-${crypto.randomUUID()}@example.com`;
268
+ await createWorkspaceInvitation({
269
+ workspaceId: workspace.id,
270
+ email,
271
+ invitedByUserId: inviter.userId,
272
+ invitedByMemberships: [{workspaceId: workspace.id, role: 'admin'}],
273
+ });
274
+ const payload = await latestInvitationPayload(email);
275
+ const token = extractToken(payload.inviteLink);
276
+ const invitation = await findInvitationByToken({hashedToken: hashOpaqueToken(token)});
277
+ if (!invitation) throw new Error('Expected stored invitation');
278
+ await revokeInvitation({invitationId: invitation.id});
279
+
280
+ const result = await previewInvitation({token});
281
+
282
+ expect(result).toEqual({status: 'invalid'});
283
+ });
251
284
  });