@shipfox/api-workspaces 5.0.0 → 6.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.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +39 -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 +17 -9
  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
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 47 files with swc (250.48ms)
2
+ Successfully compiled: 48 files with swc (373.58ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @shipfox/api-workspaces
2
2
 
3
+ ## 6.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6bdf24b: Adds idempotent workspace invitation reconciliation for retry-safe external acceptance flows.
8
+ - 326f4c0: Exposes Workspaces inter-module operations and moves Auth and OAuth providers onto injected clients.
9
+ - 1820feb: Adds Slack Settings installation and callback recovery while returning stable workspace access errors.
10
+
11
+ ### Patch Changes
12
+
13
+ - 7366f04: Adds a configured shared mailer that owns SMTP delivery settings. `@shipfox/api-auth` and `@shipfox/api-workspaces` drop their own mailer environment variables and factory logic and use the shared `mailer` from `@shipfox/node-mailer` instead.
14
+ - c2db8c3: Adds workspace member invitation and join lifecycle events to the workspaces outbox.
15
+ - f73da5d: Enforces bounded API context imports and routes inter-module consumers through producer contracts.
16
+ - Updated dependencies [b70f920]
17
+ - Updated dependencies [7366f04]
18
+ - Updated dependencies [54ce48b]
19
+ - Updated dependencies [c0162b0]
20
+ - Updated dependencies [7ac43a4]
21
+ - Updated dependencies [a01e917]
22
+ - Updated dependencies [1b79cda]
23
+ - Updated dependencies [c2db8c3]
24
+ - Updated dependencies [8bdc149]
25
+ - Updated dependencies [3810996]
26
+ - Updated dependencies [b00ed29]
27
+ - Updated dependencies [8aa7cd3]
28
+ - Updated dependencies [326f4c0]
29
+ - Updated dependencies [4a91956]
30
+ - Updated dependencies [81f9544]
31
+ - @shipfox/node-tokens@0.3.0
32
+ - @shipfox/node-mailer@0.2.0
33
+ - @shipfox/node-module@0.4.0
34
+ - @shipfox/node-drizzle@0.3.2
35
+ - @shipfox/node-outbox@0.2.4
36
+ - @shipfox/api-workspaces-dto@6.0.0
37
+ - @shipfox/api-auth-context@6.0.0
38
+ - @shipfox/node-fastify@0.2.4
39
+ - @shipfox/api-common-dto@6.0.0
40
+ - @shipfox/inter-module@0.2.0
41
+
3
42
  ## 5.0.0
4
43
 
5
44
  ### Patch Changes
package/README.md CHANGED
@@ -16,7 +16,7 @@ pnpm add @shipfox/api-workspaces
16
16
  ```
17
17
 
18
18
  Set `CLIENT_BASE_URL` to the public client URL. Invite emails use this URL for
19
- their accept links. Set up the shared mailer when invite email must be sent.
19
+ their accept links. Configure `@shipfox/node-mailer` when invite email must be sent.
20
20
 
21
21
  ## Usage
22
22
 
@@ -39,7 +39,7 @@ const membership = await ensureMembership({
39
39
  | --- | --- | --- |
40
40
  | `CLIENT_BASE_URL` | `http://localhost:5173` | Base URL used in workspace invitation links. |
41
41
 
42
- The package also uses the shared mailer configuration when it sends invitation email.
42
+ Invitation email uses the shared `@shipfox/node-mailer` configuration.
43
43
 
44
44
  ## Routes / API / Data Model
45
45
 
package/dist/config.d.ts CHANGED
@@ -1,12 +1,4 @@
1
- import { type Mailer } from '@shipfox/node-mailer';
2
1
  export declare const config: Readonly<{
3
2
  CLIENT_BASE_URL: string;
4
- MAILER_TRANSPORT: "console" | "smtp";
5
- MAILER_FROM: string;
6
- SMTP_HOST: string | undefined;
7
- SMTP_PORT: number;
8
- SMTP_USER: string | undefined;
9
- SMTP_PASSWORD: string | undefined;
10
3
  } & import("@shipfox/config").CleanedEnvAccessors>;
11
- export declare const mailer: Mailer;
12
4
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAwC,KAAK,MAAM,EAAC,MAAM,sBAAsB,CAAC;AAExF,eAAO,MAAM,MAAM;;;;;;;;kDA8BjB,CAAC;AAiBH,eAAO,MAAM,MAAM,QAAiB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;kDAKjB,CAAC"}
package/dist/config.js CHANGED
@@ -1,58 +1,9 @@
1
- import { createConfig, num, str } from '@shipfox/config';
2
- import { createConsoleMailer, createSmtpMailer } from '@shipfox/node-mailer';
1
+ import { createConfig, str } from '@shipfox/config';
3
2
  export const config = createConfig({
4
3
  CLIENT_BASE_URL: str({
5
4
  desc: 'Base URL of the client app. Used to build links in workspace invitation emails.',
6
5
  default: 'http://localhost:5173'
7
- }),
8
- MAILER_TRANSPORT: str({
9
- desc: 'How emails are delivered. Use console to print them to the log, or smtp to send them through an SMTP server.',
10
- choices: [
11
- 'console',
12
- 'smtp'
13
- ],
14
- default: 'console'
15
- }),
16
- MAILER_FROM: str({
17
- desc: 'Sender address shown on outgoing emails.',
18
- default: 'noreply@shipfox.local'
19
- }),
20
- SMTP_HOST: str({
21
- desc: 'Hostname of the SMTP server. Required when MAILER_TRANSPORT is smtp.',
22
- default: undefined
23
- }),
24
- SMTP_PORT: num({
25
- desc: 'Port of the SMTP server.',
26
- default: 587
27
- }),
28
- SMTP_USER: str({
29
- desc: 'Username for SMTP authentication. Leave it unset if the server needs no login.',
30
- default: undefined
31
- }),
32
- SMTP_PASSWORD: str({
33
- desc: 'Password for SMTP authentication. Leave it unset if the server needs no login.',
34
- default: undefined
35
6
  })
36
7
  });
37
- function createMailer() {
38
- if (config.MAILER_TRANSPORT === 'smtp') {
39
- if (!config.SMTP_HOST) throw new Error('SMTP_HOST is required when MAILER_TRANSPORT=smtp');
40
- return createSmtpMailer({
41
- host: config.SMTP_HOST,
42
- port: config.SMTP_PORT,
43
- ...config.SMTP_USER ? {
44
- user: config.SMTP_USER
45
- } : {},
46
- ...config.SMTP_PASSWORD ? {
47
- password: config.SMTP_PASSWORD
48
- } : {},
49
- from: config.MAILER_FROM
50
- });
51
- }
52
- return createConsoleMailer({
53
- from: config.MAILER_FROM
54
- });
55
- }
56
- export const mailer = createMailer();
57
8
 
58
9
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, num, str} from '@shipfox/config';\nimport {createConsoleMailer, createSmtpMailer, type Mailer} from '@shipfox/node-mailer';\n\nexport const config = createConfig({\n CLIENT_BASE_URL: str({\n desc: 'Base URL of the client app. Used to build links in workspace invitation emails.',\n default: 'http://localhost:5173',\n }),\n MAILER_TRANSPORT: str({\n desc: 'How emails are delivered. Use console to print them to the log, or smtp to send them through an SMTP server.',\n choices: ['console', 'smtp'],\n default: 'console',\n }),\n MAILER_FROM: str({\n desc: 'Sender address shown on outgoing emails.',\n default: 'noreply@shipfox.local',\n }),\n SMTP_HOST: str({\n desc: 'Hostname of the SMTP server. Required when MAILER_TRANSPORT is smtp.',\n default: undefined,\n }),\n SMTP_PORT: num({\n desc: 'Port of the SMTP server.',\n default: 587,\n }),\n SMTP_USER: str({\n desc: 'Username for SMTP authentication. Leave it unset if the server needs no login.',\n default: undefined,\n }),\n SMTP_PASSWORD: str({\n desc: 'Password for SMTP authentication. Leave it unset if the server needs no login.',\n default: undefined,\n }),\n});\n\nfunction createMailer(): Mailer {\n if (config.MAILER_TRANSPORT === 'smtp') {\n if (!config.SMTP_HOST) throw new Error('SMTP_HOST is required when MAILER_TRANSPORT=smtp');\n return createSmtpMailer({\n host: config.SMTP_HOST,\n port: config.SMTP_PORT,\n ...(config.SMTP_USER ? {user: config.SMTP_USER} : {}),\n ...(config.SMTP_PASSWORD ? {password: config.SMTP_PASSWORD} : {}),\n from: config.MAILER_FROM,\n });\n }\n\n return createConsoleMailer({from: config.MAILER_FROM});\n}\n\nexport const mailer = createMailer();\n"],"names":["createConfig","num","str","createConsoleMailer","createSmtpMailer","config","CLIENT_BASE_URL","desc","default","MAILER_TRANSPORT","choices","MAILER_FROM","SMTP_HOST","undefined","SMTP_PORT","SMTP_USER","SMTP_PASSWORD","createMailer","Error","host","port","user","password","from","mailer"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AACvD,SAAQC,mBAAmB,EAAEC,gBAAgB,QAAoB,uBAAuB;AAExF,OAAO,MAAMC,SAASL,aAAa;IACjCM,iBAAiBJ,IAAI;QACnBK,MAAM;QACNC,SAAS;IACX;IACAC,kBAAkBP,IAAI;QACpBK,MAAM;QACNG,SAAS;YAAC;YAAW;SAAO;QAC5BF,SAAS;IACX;IACAG,aAAaT,IAAI;QACfK,MAAM;QACNC,SAAS;IACX;IACAI,WAAWV,IAAI;QACbK,MAAM;QACNC,SAASK;IACX;IACAC,WAAWb,IAAI;QACbM,MAAM;QACNC,SAAS;IACX;IACAO,WAAWb,IAAI;QACbK,MAAM;QACNC,SAASK;IACX;IACAG,eAAed,IAAI;QACjBK,MAAM;QACNC,SAASK;IACX;AACF,GAAG;AAEH,SAASI;IACP,IAAIZ,OAAOI,gBAAgB,KAAK,QAAQ;QACtC,IAAI,CAACJ,OAAOO,SAAS,EAAE,MAAM,IAAIM,MAAM;QACvC,OAAOd,iBAAiB;YACtBe,MAAMd,OAAOO,SAAS;YACtBQ,MAAMf,OAAOS,SAAS;YACtB,GAAIT,OAAOU,SAAS,GAAG;gBAACM,MAAMhB,OAAOU,SAAS;YAAA,IAAI,CAAC,CAAC;YACpD,GAAIV,OAAOW,aAAa,GAAG;gBAACM,UAAUjB,OAAOW,aAAa;YAAA,IAAI,CAAC,CAAC;YAChEO,MAAMlB,OAAOM,WAAW;QAC1B;IACF;IAEA,OAAOR,oBAAoB;QAACoB,MAAMlB,OAAOM,WAAW;IAAA;AACtD;AAEA,OAAO,MAAMa,SAASP,eAAe"}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str} from '@shipfox/config';\n\nexport const config = createConfig({\n CLIENT_BASE_URL: str({\n desc: 'Base URL of the client app. Used to build links in workspace invitation emails.',\n default: 'http://localhost:5173',\n }),\n});\n"],"names":["createConfig","str","config","CLIENT_BASE_URL","desc","default"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,QAAO,kBAAkB;AAElD,OAAO,MAAMC,SAASF,aAAa;IACjCG,iBAAiBF,IAAI;QACnBG,MAAM;QACNC,SAAS;IACX;AACF,GAAG"}
@@ -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 +1 @@
1
- {"version":3,"file":"invitation.d.ts","sourceRoot":"","sources":["../../../src/core/entities/invitation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB"}
1
+ {"version":3,"file":"invitation.d.ts","sourceRoot":"","sources":["../../../src/core/entities/invitation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/core/entities/invitation.ts"],"sourcesContent":["export interface Invitation {\n id: string;\n workspaceId: string;\n email: string;\n hashedToken: string;\n expiresAt: Date;\n acceptedAt: Date | null;\n acceptedByUserId: string | null;\n invitedByUserId: string;\n invitedByDisplay: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n"],"names":[],"mappings":"AAAA,WAYC"}
1
+ {"version":3,"sources":["../../../src/core/entities/invitation.ts"],"sourcesContent":["export interface Invitation {\n id: string;\n workspaceId: string;\n email: string;\n hashedToken: string;\n expiresAt: Date;\n revokedAt: Date | null;\n acceptedAt: Date | null;\n acceptedByUserId: string | null;\n invitedByUserId: string;\n invitedByDisplay: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n"],"names":[],"mappings":"AAAA,WAaC"}
@@ -1,6 +1,6 @@
1
1
  import type { UserContextMembership } from '@shipfox/api-auth-context';
2
- import { type AcceptInvitationResult } from '#db/invitations.js';
3
2
  import type { Invitation } from './entities/invitation.js';
3
+ import type { Membership } from './entities/membership.js';
4
4
  export declare function createWorkspaceInvitation(params: {
5
5
  workspaceId: string;
6
6
  email: string;
@@ -28,6 +28,11 @@ export type PreviewInvitationResult = {
28
28
  } | {
29
29
  status: 'invalid';
30
30
  };
31
+ export interface AcceptedWorkspaceInvitation {
32
+ invitation: Invitation;
33
+ membership: Membership;
34
+ alreadyMember: boolean;
35
+ }
31
36
  export declare function previewInvitation(params: {
32
37
  token: string;
33
38
  }): Promise<PreviewInvitationResult>;
@@ -36,7 +41,21 @@ export declare function acceptWorkspaceInvitation(params: {
36
41
  userId: string;
37
42
  email: string;
38
43
  name?: string | null | undefined;
39
- }): Promise<AcceptInvitationResult>;
44
+ }): Promise<AcceptedWorkspaceInvitation>;
45
+ export type WorkspaceInvitationReconciliation = {
46
+ status: 'accepted' | 'already_accepted';
47
+ invitation: Invitation;
48
+ membership: Membership;
49
+ alreadyMember: boolean;
50
+ } | {
51
+ status: 'invalid' | 'expired' | 'revoked' | 'consumed_by_another_user' | 'email_mismatch';
52
+ };
53
+ export declare function reconcileWorkspaceInvitationAcceptance(params: {
54
+ token: string;
55
+ userId: string;
56
+ email: string;
57
+ name?: string | null | undefined;
58
+ }): Promise<WorkspaceInvitationReconciliation>;
40
59
  export declare function listWorkspaceInvitations(params: {
41
60
  workspaceId: string;
42
61
  requesterUserId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../src/core/invitations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,2BAA2B,CAAC;AAGrE,OAAO,EACL,KAAK,sBAAsB,EAO5B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAsBzD,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CAC5D,GAAG,OAAO,CAAC,UAAU,CAAC,CAuBtB;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAElC;AAED,MAAM,MAAM,uBAAuB,GAC/B;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,IAAI,CAAC;CACjB,GACD;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAC,GAC3D;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAC,GAC/C;IAAC,MAAM,EAAE,SAAS,CAAA;CAAC,CAAC;AAExB,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAgCjG;AAED,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAClC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA4BlC;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CAC5D,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQxB;AAED,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAC3D,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB"}
1
+ {"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../src/core/invitations.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
+ import { emailSchema } from '@shipfox/api-common-dto';
1
2
  import { generateOpaqueToken, hashOpaqueToken } from '@shipfox/node-tokens';
2
3
  import { config } from '#config.js';
3
- import { acceptInvitation, createInvitation, findInvitationById, findInvitationByToken, listOpenInvitationsByWorkspace, revokeInvitation } from '#db/invitations.js';
4
+ import { createInvitation, findInvitationById, findInvitationByToken, listOpenInvitationsByWorkspace, reconcileInvitationAcceptance, revokeInvitation } from '#db/invitations.js';
4
5
  import { getWorkspaceById } from '#db/workspaces.js';
5
6
  import { InvitationEmailMismatchError, InvitationNotFoundError, InvitationWorkspaceMismatchError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError } from './errors.js';
6
7
  import { requireWorkspaceMembership } from './workspaces.js';
@@ -13,6 +14,7 @@ function nonBlankOrFallback(value, fallback) {
13
14
  return trimmed ? trimmed : fallback;
14
15
  }
15
16
  export async function createWorkspaceInvitation(params) {
17
+ const email = emailSchema.parse(params.email);
16
18
  const { workspace } = await requireWorkspaceMembership({
17
19
  workspaceId: params.workspaceId,
18
20
  userId: params.invitedByUserId,
@@ -21,7 +23,7 @@ export async function createWorkspaceInvitation(params) {
21
23
  const rawToken = generateOpaqueToken('invitation');
22
24
  const invitation = await createInvitation({
23
25
  workspaceId: params.workspaceId,
24
- email: params.email,
26
+ email,
25
27
  hashedToken: hashOpaqueToken(rawToken),
26
28
  expiresAt: daysFromNow(INVITATION_TTL_DAYS),
27
29
  invitedByUserId: params.invitedByUserId,
@@ -55,6 +57,11 @@ export async function previewInvitation(params) {
55
57
  status: 'invalid'
56
58
  };
57
59
  }
60
+ if (invitation.revokedAt !== null) {
61
+ return {
62
+ status: 'invalid'
63
+ };
64
+ }
58
65
  if (invitation.acceptedAt !== null) {
59
66
  return {
60
67
  status: 'already_used',
@@ -78,28 +85,58 @@ export async function previewInvitation(params) {
78
85
  };
79
86
  }
80
87
  export async function acceptWorkspaceInvitation(params) {
81
- const invitation = await peekInvitationByRawToken({
82
- token: params.token
88
+ const result = await reconcileWorkspaceInvitationAcceptance({
89
+ token: params.token,
90
+ email: params.email,
91
+ userId: params.userId,
92
+ name: params.name
83
93
  });
84
- if (!invitation) {
85
- throw new TokenInvalidError('Invitation token is invalid');
94
+ if (result.status === 'accepted') {
95
+ return {
96
+ invitation: result.invitation,
97
+ membership: result.membership,
98
+ alreadyMember: result.alreadyMember
99
+ };
86
100
  }
87
- if (invitation.acceptedAt !== null) {
101
+ if (result.status === 'already_accepted' || result.status === 'consumed_by_another_user') {
88
102
  throw new TokenAlreadyUsedError();
89
103
  }
90
- if (invitation.expiresAt.getTime() <= Date.now()) {
104
+ if (result.status === 'expired') {
91
105
  throw new TokenExpiredError();
92
106
  }
93
- if (params.email !== invitation.email) {
107
+ if (result.status === 'email_mismatch') {
94
108
  throw new InvitationEmailMismatchError();
95
109
  }
96
- const result = await acceptInvitation({
110
+ throw new TokenInvalidError(result.status === 'revoked' ? 'Invitation token has been revoked' : 'Invitation is invalid');
111
+ }
112
+ export async function reconcileWorkspaceInvitationAcceptance(params) {
113
+ const invitation = await peekInvitationByRawToken({
114
+ token: params.token
115
+ });
116
+ if (!invitation) return {
117
+ status: 'invalid'
118
+ };
119
+ const result = await reconcileInvitationAcceptance({
97
120
  invitationId: invitation.id,
98
121
  acceptedByUserId: params.userId,
122
+ email: emailSchema.parse(params.email),
99
123
  acceptedByUserName: params.name
100
124
  });
101
- if (!result) {
102
- throw new TokenInvalidError('Invitation is no longer valid');
125
+ if (result.status === 'accepted') {
126
+ return {
127
+ status: result.status,
128
+ invitation: result.invitation,
129
+ membership: result.membership,
130
+ alreadyMember: result.alreadyMember
131
+ };
132
+ }
133
+ if (result.status === 'already_accepted') {
134
+ return {
135
+ status: result.status,
136
+ invitation: result.invitation,
137
+ membership: result.membership,
138
+ alreadyMember: true
139
+ };
103
140
  }
104
141
  return result;
105
142
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/invitations.ts"],"sourcesContent":["import type {UserContextMembership} from '@shipfox/api-auth-context';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {\n type AcceptInvitationResult,\n acceptInvitation,\n createInvitation,\n findInvitationById,\n findInvitationByToken,\n listOpenInvitationsByWorkspace,\n revokeInvitation,\n} from '#db/invitations.js';\nimport {getWorkspaceById} from '#db/workspaces.js';\nimport type {Invitation} from './entities/invitation.js';\nimport {\n InvitationEmailMismatchError,\n InvitationNotFoundError,\n InvitationWorkspaceMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n} from './errors.js';\nimport {requireWorkspaceMembership} from './workspaces.js';\n\nconst INVITATION_TTL_DAYS = 7;\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction nonBlankOrFallback(value: string | null | undefined, fallback: string): string {\n const trimmed = value?.trim();\n return trimmed ? trimmed : fallback;\n}\n\nexport async function createWorkspaceInvitation(params: {\n workspaceId: string;\n email: string;\n invitedByUserId: string;\n invitedByDisplay?: string | null;\n invitedByMemberships: ReadonlyArray<UserContextMembership>;\n}): Promise<Invitation> {\n const {workspace} = await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.invitedByUserId,\n memberships: params.invitedByMemberships,\n });\n\n const rawToken = generateOpaqueToken('invitation');\n const invitation = await createInvitation({\n workspaceId: params.workspaceId,\n email: params.email,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: daysFromNow(INVITATION_TTL_DAYS),\n invitedByUserId: params.invitedByUserId,\n invitedByDisplay: params.invitedByDisplay ?? null,\n sendEmail: {\n workspaceName: nonBlankOrFallback(workspace.name, 'your workspace'),\n inviterName: nonBlankOrFallback(params.invitedByDisplay, 'A teammate'),\n inviteLink: `${config.CLIENT_BASE_URL}/invitations/accept?token=${rawToken}`,\n },\n });\n\n return invitation;\n}\n\nexport async function peekInvitationByRawToken(params: {\n token: string;\n}): Promise<Invitation | undefined> {\n return await findInvitationByToken({hashedToken: hashOpaqueToken(params.token)});\n}\n\nexport type PreviewInvitationResult =\n | {\n status: 'pending';\n workspaceId: string;\n workspaceName: string;\n email: string;\n invitedByDisplay: string | null;\n expiresAt: Date;\n }\n | {status: 'expired'; workspaceName: string; expiresAt: Date}\n | {status: 'already_used'; workspaceName: string}\n | {status: 'invalid'};\n\nexport async function previewInvitation(params: {token: string}): Promise<PreviewInvitationResult> {\n const invitation = await peekInvitationByRawToken({token: params.token});\n if (!invitation) {\n return {status: 'invalid'};\n }\n\n const workspace = await getWorkspaceById(invitation.workspaceId);\n if (!workspace) {\n // Workspace cascade-deleted — the invitation row is orphaned. Treat as invalid.\n return {status: 'invalid'};\n }\n\n if (invitation.acceptedAt !== null) {\n return {status: 'already_used', workspaceName: workspace.name};\n }\n\n if (invitation.expiresAt.getTime() <= Date.now()) {\n return {\n status: 'expired',\n workspaceName: workspace.name,\n expiresAt: invitation.expiresAt,\n };\n }\n\n return {\n status: 'pending',\n workspaceId: workspace.id,\n workspaceName: workspace.name,\n email: invitation.email,\n invitedByDisplay: invitation.invitedByDisplay,\n expiresAt: invitation.expiresAt,\n };\n}\n\nexport async function acceptWorkspaceInvitation(params: {\n token: string;\n userId: string;\n email: string;\n name?: string | null | undefined;\n}): Promise<AcceptInvitationResult> {\n const invitation = await peekInvitationByRawToken({token: params.token});\n if (!invitation) {\n throw new TokenInvalidError('Invitation token is invalid');\n }\n\n if (invitation.acceptedAt !== null) {\n throw new TokenAlreadyUsedError();\n }\n\n if (invitation.expiresAt.getTime() <= Date.now()) {\n throw new TokenExpiredError();\n }\n\n if (params.email !== invitation.email) {\n throw new InvitationEmailMismatchError();\n }\n\n const result = await acceptInvitation({\n invitationId: invitation.id,\n acceptedByUserId: params.userId,\n acceptedByUserName: params.name,\n });\n if (!result) {\n throw new TokenInvalidError('Invitation is no longer valid');\n }\n\n return result;\n}\n\nexport async function listWorkspaceInvitations(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n}): Promise<Invitation[]> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n return listOpenInvitationsByWorkspace({workspaceId: params.workspaceId});\n}\n\nexport async function revokeWorkspaceInvitation(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n invitationId: string;\n}): Promise<void> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n const invitation = await findInvitationById({id: params.invitationId});\n if (!invitation) {\n throw new InvitationNotFoundError(params.invitationId);\n }\n\n if (invitation.workspaceId !== params.workspaceId) {\n throw new InvitationWorkspaceMismatchError();\n }\n\n await revokeInvitation({invitationId: params.invitationId});\n}\n"],"names":["generateOpaqueToken","hashOpaqueToken","config","acceptInvitation","createInvitation","findInvitationById","findInvitationByToken","listOpenInvitationsByWorkspace","revokeInvitation","getWorkspaceById","InvitationEmailMismatchError","InvitationNotFoundError","InvitationWorkspaceMismatchError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","requireWorkspaceMembership","INVITATION_TTL_DAYS","daysFromNow","days","Date","now","nonBlankOrFallback","value","fallback","trimmed","trim","createWorkspaceInvitation","params","workspace","workspaceId","userId","invitedByUserId","memberships","invitedByMemberships","rawToken","invitation","email","hashedToken","expiresAt","invitedByDisplay","sendEmail","workspaceName","name","inviterName","inviteLink","CLIENT_BASE_URL","peekInvitationByRawToken","token","previewInvitation","status","acceptedAt","getTime","id","acceptWorkspaceInvitation","result","invitationId","acceptedByUserId","acceptedByUserName","listWorkspaceInvitations","requesterUserId","requesterMemberships","revokeWorkspaceInvitation"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/invitations.ts"],"sourcesContent":["import type {UserContextMembership} from '@shipfox/api-auth-context';\nimport {emailSchema} from '@shipfox/api-common-dto';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {\n createInvitation,\n findInvitationById,\n findInvitationByToken,\n listOpenInvitationsByWorkspace,\n reconcileInvitationAcceptance,\n revokeInvitation,\n} from '#db/invitations.js';\nimport {getWorkspaceById} from '#db/workspaces.js';\nimport type {Invitation} from './entities/invitation.js';\nimport type {Membership} from './entities/membership.js';\nimport {\n InvitationEmailMismatchError,\n InvitationNotFoundError,\n InvitationWorkspaceMismatchError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n} from './errors.js';\nimport {requireWorkspaceMembership} from './workspaces.js';\n\nconst INVITATION_TTL_DAYS = 7;\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction nonBlankOrFallback(value: string | null | undefined, fallback: string): string {\n const trimmed = value?.trim();\n return trimmed ? trimmed : fallback;\n}\n\nexport async function createWorkspaceInvitation(params: {\n workspaceId: string;\n email: string;\n invitedByUserId: string;\n invitedByDisplay?: string | null;\n invitedByMemberships: ReadonlyArray<UserContextMembership>;\n}): Promise<Invitation> {\n const email = emailSchema.parse(params.email);\n const {workspace} = await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.invitedByUserId,\n memberships: params.invitedByMemberships,\n });\n\n const rawToken = generateOpaqueToken('invitation');\n const invitation = await createInvitation({\n workspaceId: params.workspaceId,\n email,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: daysFromNow(INVITATION_TTL_DAYS),\n invitedByUserId: params.invitedByUserId,\n invitedByDisplay: params.invitedByDisplay ?? null,\n sendEmail: {\n workspaceName: nonBlankOrFallback(workspace.name, 'your workspace'),\n inviterName: nonBlankOrFallback(params.invitedByDisplay, 'A teammate'),\n inviteLink: `${config.CLIENT_BASE_URL}/invitations/accept?token=${rawToken}`,\n },\n });\n\n return invitation;\n}\n\nexport async function peekInvitationByRawToken(params: {\n token: string;\n}): Promise<Invitation | undefined> {\n return await findInvitationByToken({hashedToken: hashOpaqueToken(params.token)});\n}\n\nexport type PreviewInvitationResult =\n | {\n status: 'pending';\n workspaceId: string;\n workspaceName: string;\n email: string;\n invitedByDisplay: string | null;\n expiresAt: Date;\n }\n | {status: 'expired'; workspaceName: string; expiresAt: Date}\n | {status: 'already_used'; workspaceName: string}\n | {status: 'invalid'};\n\nexport interface AcceptedWorkspaceInvitation {\n invitation: Invitation;\n membership: Membership;\n alreadyMember: boolean;\n}\n\nexport async function previewInvitation(params: {token: string}): Promise<PreviewInvitationResult> {\n const invitation = await peekInvitationByRawToken({token: params.token});\n if (!invitation) {\n return {status: 'invalid'};\n }\n\n const workspace = await getWorkspaceById(invitation.workspaceId);\n if (!workspace) {\n // Workspace cascade-deleted — the invitation row is orphaned. Treat as invalid.\n return {status: 'invalid'};\n }\n\n if (invitation.revokedAt !== null) {\n return {status: 'invalid'};\n }\n\n if (invitation.acceptedAt !== null) {\n return {status: 'already_used', workspaceName: workspace.name};\n }\n\n if (invitation.expiresAt.getTime() <= Date.now()) {\n return {\n status: 'expired',\n workspaceName: workspace.name,\n expiresAt: invitation.expiresAt,\n };\n }\n\n return {\n status: 'pending',\n workspaceId: workspace.id,\n workspaceName: workspace.name,\n email: invitation.email,\n invitedByDisplay: invitation.invitedByDisplay,\n expiresAt: invitation.expiresAt,\n };\n}\n\nexport async function acceptWorkspaceInvitation(params: {\n token: string;\n userId: string;\n email: string;\n name?: string | null | undefined;\n}): Promise<AcceptedWorkspaceInvitation> {\n const result = await reconcileWorkspaceInvitationAcceptance({\n token: params.token,\n email: params.email,\n userId: params.userId,\n name: params.name,\n });\n if (result.status === 'accepted') {\n return {\n invitation: result.invitation,\n membership: result.membership,\n alreadyMember: result.alreadyMember,\n };\n }\n if (result.status === 'already_accepted' || result.status === 'consumed_by_another_user') {\n throw new TokenAlreadyUsedError();\n }\n if (result.status === 'expired') {\n throw new TokenExpiredError();\n }\n if (result.status === 'email_mismatch') {\n throw new InvitationEmailMismatchError();\n }\n throw new TokenInvalidError(\n result.status === 'revoked' ? 'Invitation token has been revoked' : 'Invitation is invalid',\n );\n}\n\nexport type WorkspaceInvitationReconciliation =\n | {\n status: 'accepted' | 'already_accepted';\n invitation: Invitation;\n membership: Membership;\n alreadyMember: boolean;\n }\n | {\n status: 'invalid' | 'expired' | 'revoked' | 'consumed_by_another_user' | 'email_mismatch';\n };\n\nexport async function reconcileWorkspaceInvitationAcceptance(params: {\n token: string;\n userId: string;\n email: string;\n name?: string | null | undefined;\n}): Promise<WorkspaceInvitationReconciliation> {\n const invitation = await peekInvitationByRawToken({token: params.token});\n if (!invitation) return {status: 'invalid'};\n\n const result = await reconcileInvitationAcceptance({\n invitationId: invitation.id,\n acceptedByUserId: params.userId,\n email: emailSchema.parse(params.email),\n acceptedByUserName: params.name,\n });\n if (result.status === 'accepted') {\n return {\n status: result.status,\n invitation: result.invitation,\n membership: result.membership,\n alreadyMember: result.alreadyMember,\n };\n }\n if (result.status === 'already_accepted') {\n return {\n status: result.status,\n invitation: result.invitation,\n membership: result.membership,\n alreadyMember: true,\n };\n }\n return result;\n}\n\nexport async function listWorkspaceInvitations(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n}): Promise<Invitation[]> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n return listOpenInvitationsByWorkspace({workspaceId: params.workspaceId});\n}\n\nexport async function revokeWorkspaceInvitation(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n invitationId: string;\n}): Promise<void> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n const invitation = await findInvitationById({id: params.invitationId});\n if (!invitation) {\n throw new InvitationNotFoundError(params.invitationId);\n }\n\n if (invitation.workspaceId !== params.workspaceId) {\n throw new InvitationWorkspaceMismatchError();\n }\n\n await revokeInvitation({invitationId: params.invitationId});\n}\n"],"names":["emailSchema","generateOpaqueToken","hashOpaqueToken","config","createInvitation","findInvitationById","findInvitationByToken","listOpenInvitationsByWorkspace","reconcileInvitationAcceptance","revokeInvitation","getWorkspaceById","InvitationEmailMismatchError","InvitationNotFoundError","InvitationWorkspaceMismatchError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","requireWorkspaceMembership","INVITATION_TTL_DAYS","daysFromNow","days","Date","now","nonBlankOrFallback","value","fallback","trimmed","trim","createWorkspaceInvitation","params","email","parse","workspace","workspaceId","userId","invitedByUserId","memberships","invitedByMemberships","rawToken","invitation","hashedToken","expiresAt","invitedByDisplay","sendEmail","workspaceName","name","inviterName","inviteLink","CLIENT_BASE_URL","peekInvitationByRawToken","token","previewInvitation","status","revokedAt","acceptedAt","getTime","id","acceptWorkspaceInvitation","result","reconcileWorkspaceInvitationAcceptance","membership","alreadyMember","invitationId","acceptedByUserId","acceptedByUserName","listWorkspaceInvitations","requesterUserId","requesterMemberships","revokeWorkspaceInvitation"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { UserContextMembership } from '@shipfox/api-auth-context';
2
- import type { WorkspaceRole } from '@shipfox/api-workspaces-dto';
2
+ import { type WorkspaceRole } from '@shipfox/api-workspaces-dto';
3
3
  import { type MembershipWithUser, type MembershipWithWorkspace } from '#db/memberships.js';
4
4
  import type { Workspace } from './entities/workspace.js';
5
5
  export interface RequireWorkspaceMembershipParams {
@@ -1 +1 @@
1
- {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/core/workspaces.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAE/D,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAE7B,MAAM,oBAAoB,CAAC;AAI5B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yBAAyB,CAAC;AASvD,MAAM,WAAW,gCAAgC;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,gCAAgC;IAC/C,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,gCAAgC,GACvC,OAAO,CAAC,gCAAgC,CAAC,CAe3C;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC,GAAG,OAAO,CAAC,SAAS,CAAC,CAYrB;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAOpF;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAErC;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CAC5D,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAQhC;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB"}
1
+ {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/core/workspaces.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAE7B,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yBAAyB,CAAC;AASvD,MAAM,WAAW,gCAAgC;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,gCAAgC;IAC/C,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,gCAAgC,GACvC,OAAO,CAAC,gCAAgC,CAAC,CAe3C;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC,GAAG,OAAO,CAAC,SAAS,CAAC,CAuBrB;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAOpF;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAErC;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CAC5D,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAQhC;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB"}
@@ -1,6 +1,9 @@
1
+ import { WORKSPACES_WORKSPACE_CREATED } from '@shipfox/api-workspaces-dto';
2
+ import { writeOutboxEvent } from '@shipfox/node-outbox';
1
3
  import { db } from '#db/db.js';
2
4
  import { findMembership, listMembershipsByUser, listMembershipsByWorkspace, removeMembership } from '#db/memberships.js';
3
5
  import { memberships } from '#db/schema/memberships.js';
6
+ import { workspacesOutbox } from '#db/schema/outbox.js';
4
7
  import { toWorkspace, workspaces } from '#db/schema/workspaces.js';
5
8
  import { getWorkspaceById } from '#db/workspaces.js';
6
9
  import { MembershipNotFoundError, MembershipRequiredError, SelfRemovalNotAllowedError, WorkspaceInactiveError, WorkspaceNotFoundError } from './errors.js';
@@ -35,7 +38,16 @@ export async function createWorkspaceForUser(params) {
35
38
  userName: params.userName ?? null,
36
39
  workspaceId: workspaceRow.id
37
40
  });
38
- return toWorkspace(workspaceRow);
41
+ const workspace = toWorkspace(workspaceRow);
42
+ await writeOutboxEvent(tx, workspacesOutbox, {
43
+ type: WORKSPACES_WORKSPACE_CREATED,
44
+ payload: {
45
+ workspaceId: workspace.id,
46
+ name: workspace.name,
47
+ creatorUserId: params.userId
48
+ }
49
+ });
50
+ return workspace;
39
51
  });
40
52
  }
41
53
  export async function getWorkspace(params) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/workspaces.ts"],"sourcesContent":["import type {UserContextMembership} from '@shipfox/api-auth-context';\nimport type {WorkspaceRole} from '@shipfox/api-workspaces-dto';\nimport {db} from '#db/db.js';\nimport {\n findMembership,\n listMembershipsByUser,\n listMembershipsByWorkspace,\n type MembershipWithUser,\n type MembershipWithWorkspace,\n removeMembership,\n} from '#db/memberships.js';\nimport {memberships} from '#db/schema/memberships.js';\nimport {toWorkspace, workspaces} from '#db/schema/workspaces.js';\nimport {getWorkspaceById} from '#db/workspaces.js';\nimport type {Workspace} from './entities/workspace.js';\nimport {\n MembershipNotFoundError,\n MembershipRequiredError,\n SelfRemovalNotAllowedError,\n WorkspaceInactiveError,\n WorkspaceNotFoundError,\n} from './errors.js';\n\nexport interface RequireWorkspaceMembershipParams {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<UserContextMembership>;\n}\n\nexport interface RequireWorkspaceMembershipResult {\n workspace: Workspace;\n workspaceId: string;\n userId: string;\n role: WorkspaceRole;\n}\n\nexport async function requireWorkspaceMembership(\n params: RequireWorkspaceMembershipParams,\n): Promise<RequireWorkspaceMembershipResult> {\n const membership = params.memberships.find((m) => m.workspaceId === params.workspaceId);\n if (!membership) {\n throw new MembershipRequiredError(params.workspaceId);\n }\n\n const workspace = await getWorkspaceById(params.workspaceId);\n if (!workspace) {\n throw new WorkspaceNotFoundError(params.workspaceId);\n }\n if (workspace.status !== 'active') {\n throw new WorkspaceInactiveError(params.workspaceId);\n }\n\n return {workspace, workspaceId: workspace.id, userId: params.userId, role: membership.role};\n}\n\nexport async function createWorkspaceForUser(params: {\n name: string;\n userId: string;\n userEmail?: string | undefined;\n userName?: string | null | undefined;\n}): Promise<Workspace> {\n return await db().transaction(async (tx) => {\n const [workspaceRow] = await tx.insert(workspaces).values({name: params.name}).returning();\n if (!workspaceRow) throw new Error('Insert returned no rows');\n await tx.insert(memberships).values({\n userId: params.userId,\n userEmail: params.userEmail ?? `user-${params.userId}@example.local`,\n userName: params.userName ?? null,\n workspaceId: workspaceRow.id,\n });\n return toWorkspace(workspaceRow);\n });\n}\n\nexport async function getWorkspace(params: {workspaceId: string}): Promise<Workspace> {\n const workspace = await getWorkspaceById(params.workspaceId);\n if (!workspace) {\n throw new WorkspaceNotFoundError(params.workspaceId);\n }\n\n return workspace;\n}\n\nexport async function listUserWorkspaceMemberships(params: {\n userId: string;\n}): Promise<MembershipWithWorkspace[]> {\n return await listMembershipsByUser({userId: params.userId});\n}\n\nexport async function listWorkspaceMembers(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n}): Promise<MembershipWithUser[]> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n return listMembershipsByWorkspace({workspaceId: params.workspaceId});\n}\n\nexport async function removeWorkspaceMember(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n userId: string;\n}): Promise<void> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n if (params.userId === params.requesterUserId) {\n throw new SelfRemovalNotAllowedError();\n }\n\n const target = await findMembership({userId: params.userId, workspaceId: params.workspaceId});\n if (!target) {\n throw new MembershipNotFoundError(params.userId, params.workspaceId);\n }\n\n await removeMembership({userId: params.userId, workspaceId: params.workspaceId});\n}\n"],"names":["db","findMembership","listMembershipsByUser","listMembershipsByWorkspace","removeMembership","memberships","toWorkspace","workspaces","getWorkspaceById","MembershipNotFoundError","MembershipRequiredError","SelfRemovalNotAllowedError","WorkspaceInactiveError","WorkspaceNotFoundError","requireWorkspaceMembership","params","membership","find","m","workspaceId","workspace","status","id","userId","role","createWorkspaceForUser","transaction","tx","workspaceRow","insert","values","name","returning","Error","userEmail","userName","getWorkspace","listUserWorkspaceMemberships","listWorkspaceMembers","requesterUserId","requesterMemberships","removeWorkspaceMember","target"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/workspaces.ts"],"sourcesContent":["import type {UserContextMembership} from '@shipfox/api-auth-context';\nimport {\n WORKSPACES_WORKSPACE_CREATED,\n type WorkspaceRole,\n type WorkspacesEventMap,\n} from '@shipfox/api-workspaces-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {db} from '#db/db.js';\nimport {\n findMembership,\n listMembershipsByUser,\n listMembershipsByWorkspace,\n type MembershipWithUser,\n type MembershipWithWorkspace,\n removeMembership,\n} from '#db/memberships.js';\nimport {memberships} from '#db/schema/memberships.js';\nimport {workspacesOutbox} from '#db/schema/outbox.js';\nimport {toWorkspace, workspaces} from '#db/schema/workspaces.js';\nimport {getWorkspaceById} from '#db/workspaces.js';\nimport type {Workspace} from './entities/workspace.js';\nimport {\n MembershipNotFoundError,\n MembershipRequiredError,\n SelfRemovalNotAllowedError,\n WorkspaceInactiveError,\n WorkspaceNotFoundError,\n} from './errors.js';\n\nexport interface RequireWorkspaceMembershipParams {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<UserContextMembership>;\n}\n\nexport interface RequireWorkspaceMembershipResult {\n workspace: Workspace;\n workspaceId: string;\n userId: string;\n role: WorkspaceRole;\n}\n\nexport async function requireWorkspaceMembership(\n params: RequireWorkspaceMembershipParams,\n): Promise<RequireWorkspaceMembershipResult> {\n const membership = params.memberships.find((m) => m.workspaceId === params.workspaceId);\n if (!membership) {\n throw new MembershipRequiredError(params.workspaceId);\n }\n\n const workspace = await getWorkspaceById(params.workspaceId);\n if (!workspace) {\n throw new WorkspaceNotFoundError(params.workspaceId);\n }\n if (workspace.status !== 'active') {\n throw new WorkspaceInactiveError(params.workspaceId);\n }\n\n return {workspace, workspaceId: workspace.id, userId: params.userId, role: membership.role};\n}\n\nexport async function createWorkspaceForUser(params: {\n name: string;\n userId: string;\n userEmail?: string | undefined;\n userName?: string | null | undefined;\n}): Promise<Workspace> {\n return await db().transaction(async (tx) => {\n const [workspaceRow] = await tx.insert(workspaces).values({name: params.name}).returning();\n if (!workspaceRow) throw new Error('Insert returned no rows');\n await tx.insert(memberships).values({\n userId: params.userId,\n userEmail: params.userEmail ?? `user-${params.userId}@example.local`,\n userName: params.userName ?? null,\n workspaceId: workspaceRow.id,\n });\n const workspace = toWorkspace(workspaceRow);\n\n await writeOutboxEvent<WorkspacesEventMap>(tx, workspacesOutbox, {\n type: WORKSPACES_WORKSPACE_CREATED,\n payload: {\n workspaceId: workspace.id,\n name: workspace.name,\n creatorUserId: params.userId,\n },\n });\n\n return workspace;\n });\n}\n\nexport async function getWorkspace(params: {workspaceId: string}): Promise<Workspace> {\n const workspace = await getWorkspaceById(params.workspaceId);\n if (!workspace) {\n throw new WorkspaceNotFoundError(params.workspaceId);\n }\n\n return workspace;\n}\n\nexport async function listUserWorkspaceMemberships(params: {\n userId: string;\n}): Promise<MembershipWithWorkspace[]> {\n return await listMembershipsByUser({userId: params.userId});\n}\n\nexport async function listWorkspaceMembers(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n}): Promise<MembershipWithUser[]> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n return listMembershipsByWorkspace({workspaceId: params.workspaceId});\n}\n\nexport async function removeWorkspaceMember(params: {\n workspaceId: string;\n requesterUserId: string;\n requesterMemberships: ReadonlyArray<UserContextMembership>;\n userId: string;\n}): Promise<void> {\n await requireWorkspaceMembership({\n workspaceId: params.workspaceId,\n userId: params.requesterUserId,\n memberships: params.requesterMemberships,\n });\n\n if (params.userId === params.requesterUserId) {\n throw new SelfRemovalNotAllowedError();\n }\n\n const target = await findMembership({userId: params.userId, workspaceId: params.workspaceId});\n if (!target) {\n throw new MembershipNotFoundError(params.userId, params.workspaceId);\n }\n\n await removeMembership({userId: params.userId, workspaceId: params.workspaceId});\n}\n"],"names":["WORKSPACES_WORKSPACE_CREATED","writeOutboxEvent","db","findMembership","listMembershipsByUser","listMembershipsByWorkspace","removeMembership","memberships","workspacesOutbox","toWorkspace","workspaces","getWorkspaceById","MembershipNotFoundError","MembershipRequiredError","SelfRemovalNotAllowedError","WorkspaceInactiveError","WorkspaceNotFoundError","requireWorkspaceMembership","params","membership","find","m","workspaceId","workspace","status","id","userId","role","createWorkspaceForUser","transaction","tx","workspaceRow","insert","values","name","returning","Error","userEmail","userName","type","payload","creatorUserId","getWorkspace","listUserWorkspaceMemberships","listWorkspaceMembers","requesterUserId","requesterMemberships","removeWorkspaceMember","target"],"mappings":"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"}
package/dist/db/db.d.ts CHANGED
@@ -324,6 +324,23 @@ export declare const schema: {
324
324
  identity: undefined;
325
325
  generated: undefined;
326
326
  }, {}, {}>;
327
+ revokedAt: import("drizzle-orm/pg-core").PgColumn<{
328
+ name: "revoked_at";
329
+ tableName: "invitations";
330
+ dataType: "date";
331
+ columnType: "PgTimestamp";
332
+ data: Date;
333
+ driverParam: string;
334
+ notNull: false;
335
+ hasDefault: false;
336
+ isPrimaryKey: false;
337
+ isAutoincrement: false;
338
+ hasRuntimeDefault: false;
339
+ enumValues: undefined;
340
+ baseColumn: never;
341
+ identity: undefined;
342
+ generated: undefined;
343
+ }, {}, {}>;
327
344
  acceptedAt: import("drizzle-orm/pg-core").PgColumn<{
328
345
  name: "accepted_at";
329
346
  tableName: "invitations";
@@ -949,6 +966,23 @@ export declare function db(): NodePgDatabase<{
949
966
  identity: undefined;
950
967
  generated: undefined;
951
968
  }, {}, {}>;
969
+ revokedAt: import("drizzle-orm/pg-core").PgColumn<{
970
+ name: "revoked_at";
971
+ tableName: "invitations";
972
+ dataType: "date";
973
+ columnType: "PgTimestamp";
974
+ data: Date;
975
+ driverParam: string;
976
+ notNull: false;
977
+ hasDefault: false;
978
+ isPrimaryKey: false;
979
+ isAutoincrement: false;
980
+ hasRuntimeDefault: false;
981
+ enumValues: undefined;
982
+ baseColumn: never;
983
+ identity: undefined;
984
+ generated: undefined;
985
+ }, {}, {}>;
952
986
  acceptedAt: import("drizzle-orm/pg-core").PgColumn<{
953
987
  name: "accepted_at";
954
988
  tableName: "invitations";
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAOnE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKlB,CAAC;AAIF,wBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGjB;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAOnE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKlB,CAAC;AAIF,wBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGjB;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B"}
@@ -1,6 +1,6 @@
1
1
  export { closeDb, db, schema } from './db.js';
2
- export type { AcceptInvitationParams, AcceptInvitationResult, CreateInvitationParams, } from './invitations.js';
3
- export { acceptInvitation, createInvitation, findInvitationByToken, listOpenInvitationsByWorkspace, revokeInvitation, } from './invitations.js';
2
+ export type { CreateInvitationParams } from './invitations.js';
3
+ export { createInvitation, findInvitationByToken, listOpenInvitationsByWorkspace, revokeInvitation, } from './invitations.js';
4
4
  export type { CreateMembershipParams, MembershipWithUser, MembershipWithWorkspace, RemoveMembershipParams, } from './memberships.js';
5
5
  export { createMembership, findMembership, listMembershipsByUser, listMembershipsByWorkspace, removeMembership, } from './memberships.js';
6
6
  export { workspacesOutbox } from './schema/outbox.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,OAAO,EAAE,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAC5C,YAAY,EACV,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,8BAA8B,EAC9B,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,cAAc,QAAoE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,OAAO,EAAE,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAC5C,YAAY,EAAC,sBAAsB,EAAC,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,8BAA8B,EAC9B,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,cAAc,QAAoE,CAAC"}
package/dist/db/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { dirname, resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  export { closeDb, db, schema } from './db.js';
4
- export { acceptInvitation, createInvitation, findInvitationByToken, listOpenInvitationsByWorkspace, revokeInvitation } from './invitations.js';
4
+ export { createInvitation, findInvitationByToken, listOpenInvitationsByWorkspace, revokeInvitation } from './invitations.js';
5
5
  export { createMembership, findMembership, listMembershipsByUser, listMembershipsByWorkspace, removeMembership } from './memberships.js';
6
6
  export { workspacesOutbox } from './schema/outbox.js';
7
7
  export { createWorkspace, getWorkspaceById, getWorkspaceServiceMetrics, updateWorkspace } from './workspaces.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nexport {closeDb, db, schema} from './db.js';\nexport type {\n AcceptInvitationParams,\n AcceptInvitationResult,\n CreateInvitationParams,\n} from './invitations.js';\nexport {\n acceptInvitation,\n createInvitation,\n findInvitationByToken,\n listOpenInvitationsByWorkspace,\n revokeInvitation,\n} from './invitations.js';\nexport type {\n CreateMembershipParams,\n MembershipWithUser,\n MembershipWithWorkspace,\n RemoveMembershipParams,\n} from './memberships.js';\nexport {\n createMembership,\n findMembership,\n listMembershipsByUser,\n listMembershipsByWorkspace,\n removeMembership,\n} from './memberships.js';\nexport {workspacesOutbox} from './schema/outbox.js';\nexport type {\n CreateWorkspaceParams,\n UpdateWorkspaceParams,\n WorkspaceServiceMetrics,\n} from './workspaces.js';\nexport {\n createWorkspace,\n getWorkspaceById,\n getWorkspaceServiceMetrics,\n updateWorkspace,\n} from './workspaces.js';\n\nexport const migrationsPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle');\n"],"names":["dirname","resolve","fileURLToPath","closeDb","db","schema","acceptInvitation","createInvitation","findInvitationByToken","listOpenInvitationsByWorkspace","revokeInvitation","createMembership","findMembership","listMembershipsByUser","listMembershipsByWorkspace","removeMembership","workspacesOutbox","createWorkspace","getWorkspaceById","getWorkspaceServiceMetrics","updateWorkspace","migrationsPath","url"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AAEvC,SAAQC,OAAO,EAAEC,EAAE,EAAEC,MAAM,QAAO,UAAU;AAM5C,SACEC,gBAAgB,EAChBC,gBAAgB,EAChBC,qBAAqB,EACrBC,8BAA8B,EAC9BC,gBAAgB,QACX,mBAAmB;AAO1B,SACEC,gBAAgB,EAChBC,cAAc,EACdC,qBAAqB,EACrBC,0BAA0B,EAC1BC,gBAAgB,QACX,mBAAmB;AAC1B,SAAQC,gBAAgB,QAAO,qBAAqB;AAMpD,SACEC,eAAe,EACfC,gBAAgB,EAChBC,0BAA0B,EAC1BC,eAAe,QACV,kBAAkB;AAEzB,OAAO,MAAMC,iBAAiBpB,QAAQD,QAAQE,cAAc,YAAYoB,GAAG,IAAI,iBAAiB"}
1
+ {"version":3,"sources":["../../src/db/index.ts"],"sourcesContent":["import {dirname, resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nexport {closeDb, db, schema} from './db.js';\nexport type {CreateInvitationParams} from './invitations.js';\nexport {\n createInvitation,\n findInvitationByToken,\n listOpenInvitationsByWorkspace,\n revokeInvitation,\n} from './invitations.js';\nexport type {\n CreateMembershipParams,\n MembershipWithUser,\n MembershipWithWorkspace,\n RemoveMembershipParams,\n} from './memberships.js';\nexport {\n createMembership,\n findMembership,\n listMembershipsByUser,\n listMembershipsByWorkspace,\n removeMembership,\n} from './memberships.js';\nexport {workspacesOutbox} from './schema/outbox.js';\nexport type {\n CreateWorkspaceParams,\n UpdateWorkspaceParams,\n WorkspaceServiceMetrics,\n} from './workspaces.js';\nexport {\n createWorkspace,\n getWorkspaceById,\n getWorkspaceServiceMetrics,\n updateWorkspace,\n} from './workspaces.js';\n\nexport const migrationsPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle');\n"],"names":["dirname","resolve","fileURLToPath","closeDb","db","schema","createInvitation","findInvitationByToken","listOpenInvitationsByWorkspace","revokeInvitation","createMembership","findMembership","listMembershipsByUser","listMembershipsByWorkspace","removeMembership","workspacesOutbox","createWorkspace","getWorkspaceById","getWorkspaceServiceMetrics","updateWorkspace","migrationsPath","url"],"mappings":"AAAA,SAAQA,OAAO,EAAEC,OAAO,QAAO,YAAY;AAC3C,SAAQC,aAAa,QAAO,WAAW;AAEvC,SAAQC,OAAO,EAAEC,EAAE,EAAEC,MAAM,QAAO,UAAU;AAE5C,SACEC,gBAAgB,EAChBC,qBAAqB,EACrBC,8BAA8B,EAC9BC,gBAAgB,QACX,mBAAmB;AAO1B,SACEC,gBAAgB,EAChBC,cAAc,EACdC,qBAAqB,EACrBC,0BAA0B,EAC1BC,gBAAgB,QACX,mBAAmB;AAC1B,SAAQC,gBAAgB,QAAO,qBAAqB;AAMpD,SACEC,eAAe,EACfC,gBAAgB,EAChBC,0BAA0B,EAC1BC,eAAe,QACV,kBAAkB;AAEzB,OAAO,MAAMC,iBAAiBnB,QAAQD,QAAQE,cAAc,YAAYmB,GAAG,IAAI,iBAAiB"}
@@ -32,16 +32,23 @@ export declare function listOpenInvitationsByWorkspace(params: {
32
32
  export declare function revokeInvitation(params: {
33
33
  invitationId: string;
34
34
  }): Promise<void>;
35
- export interface AcceptInvitationParams {
36
- invitationId: string;
37
- acceptedByUserId: string;
38
- acceptedByUserName?: string | null | undefined;
39
- }
40
- export interface AcceptInvitationResult {
35
+ export type ReconcileInvitationAcceptanceResult = {
36
+ status: 'accepted';
41
37
  invitation: Invitation;
42
38
  membership: Membership;
43
39
  alreadyMember: boolean;
44
- }
45
- export declare function acceptInvitation(params: AcceptInvitationParams): Promise<AcceptInvitationResult | undefined>;
40
+ } | {
41
+ status: 'already_accepted';
42
+ invitation: Invitation;
43
+ membership: Membership;
44
+ } | {
45
+ status: 'invalid' | 'expired' | 'revoked' | 'consumed_by_another_user' | 'email_mismatch';
46
+ };
47
+ export declare function reconcileInvitationAcceptance(params: {
48
+ invitationId: string;
49
+ acceptedByUserId: string;
50
+ email: string;
51
+ acceptedByUserName?: string | null | undefined;
52
+ }): Promise<ReconcileInvitationAcceptanceResult>;
46
53
  export {};
47
54
  //# sourceMappingURL=invitations.d.ts.map