@arcote.tech/arc-workspace 0.4.7 → 0.4.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-workspace",
3
3
  "type": "module",
4
- "version": "0.4.7",
4
+ "version": "0.4.9",
5
5
  "private": false,
6
6
  "description": "Reusable workspace module for Arc framework — multi-workspace with dual token architecture",
7
7
  "main": "./src/index.ts",
@@ -10,8 +10,8 @@
10
10
  "type-check": "tsc --noEmit"
11
11
  },
12
12
  "peerDependencies": {
13
- "@arcote.tech/arc": "^0.4.7",
14
- "@arcote.tech/arc-auth": "^0.4.7",
13
+ "@arcote.tech/arc": "^0.4.9",
14
+ "@arcote.tech/arc-auth": "^0.4.9",
15
15
  "typescript": "^5.0.0"
16
16
  },
17
17
  "devDependencies": {
@@ -0,0 +1,219 @@
1
+ import {
2
+ aggregate,
3
+ date,
4
+ string,
5
+ type ArcId,
6
+ } from "@arcote.tech/arc";
7
+ import type { Token } from "@arcote.tech/arc-auth";
8
+ import type { WorkspaceToken } from "../tokens/workspace-token";
9
+
10
+ export type WorkspaceInvitationAggregateData = {
11
+ name: string;
12
+ workspaceInvitationId: ArcId<any>;
13
+ workspaceId: ArcId<any>;
14
+ accountId: ArcId<any>;
15
+ workspaceToken: WorkspaceToken;
16
+ userToken: Token;
17
+ };
18
+
19
+ function generateInviteToken(): string {
20
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
21
+ let result = "";
22
+ for (let i = 0; i < 32; i++) {
23
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
24
+ }
25
+ return result;
26
+ }
27
+
28
+ export const createWorkspaceInvitationAggregate = <
29
+ const Data extends WorkspaceInvitationAggregateData,
30
+ >(
31
+ data: Data,
32
+ ) => {
33
+ const {
34
+ workspaceInvitationId,
35
+ workspaceId,
36
+ accountId,
37
+ workspaceToken,
38
+ userToken,
39
+ } = data;
40
+
41
+ return aggregate(
42
+ `${data.name}WorkspaceInvitations`,
43
+ workspaceInvitationId,
44
+ {
45
+ workspaceId,
46
+ invitedBy: accountId,
47
+ role: string(),
48
+ token: string(),
49
+ email: string().optional(),
50
+ workspaceName: string(),
51
+ workspaceType: string(),
52
+ status: string(), // "pending" | "accepted" | "revoked"
53
+ createdAt: date(),
54
+ expiresAt: date(),
55
+ },
56
+ )
57
+ .publicEvent(
58
+ "invitationCreated",
59
+ {
60
+ workspaceInvitationId,
61
+ workspaceId,
62
+ invitedBy: accountId,
63
+ role: string(),
64
+ token: string(),
65
+ email: string().optional(),
66
+ workspaceName: string(),
67
+ workspaceType: string(),
68
+ expiresAt: date(),
69
+ },
70
+ async (ctx, event) => {
71
+ const p = event.payload;
72
+ await ctx.set(p.workspaceInvitationId, {
73
+ workspaceId: p.workspaceId,
74
+ invitedBy: p.invitedBy,
75
+ role: p.role,
76
+ token: p.token,
77
+ email: p.email,
78
+ workspaceName: p.workspaceName,
79
+ workspaceType: p.workspaceType,
80
+ status: "pending",
81
+ createdAt: event.createdAt,
82
+ expiresAt: p.expiresAt,
83
+ });
84
+ },
85
+ )
86
+
87
+ .publicEvent(
88
+ "invitationAccepted",
89
+ {
90
+ workspaceInvitationId,
91
+ workspaceId,
92
+ acceptedBy: accountId,
93
+ role: string(),
94
+ workspaceName: string(),
95
+ workspaceType: string(),
96
+ },
97
+ async (ctx, event) => {
98
+ await ctx.modify(event.payload.workspaceInvitationId, {
99
+ status: "accepted",
100
+ });
101
+ },
102
+ )
103
+
104
+ .publicEvent(
105
+ "invitationRevoked",
106
+ { workspaceInvitationId },
107
+ async (ctx, event) => {
108
+ await ctx.modify(event.payload.workspaceInvitationId, {
109
+ status: "revoked",
110
+ });
111
+ },
112
+ )
113
+
114
+ .mutateMethod(
115
+ "createInvitation",
116
+ {
117
+ params: {
118
+ workspaceId,
119
+ role: string(),
120
+ workspaceName: string(),
121
+ workspaceType: string(),
122
+ email: string().optional(),
123
+ },
124
+ },
125
+ ONLY_SERVER &&
126
+ (async (ctx, params) => {
127
+ const invId = workspaceInvitationId.generate();
128
+ const token = generateInviteToken();
129
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
130
+
131
+ await ctx.invitationCreated.emit({
132
+ workspaceInvitationId: invId,
133
+ workspaceId: params.workspaceId,
134
+ invitedBy: ctx.$auth.params.accountId,
135
+ role: params.role,
136
+ token,
137
+ email: params.email,
138
+ workspaceName: params.workspaceName,
139
+ workspaceType: params.workspaceType,
140
+ expiresAt,
141
+ });
142
+
143
+ return { workspaceInvitationId: invId, token };
144
+ }),
145
+ )
146
+
147
+ .mutateMethod(
148
+ "acceptInvitation",
149
+ { params: { token: string() } },
150
+ ONLY_SERVER &&
151
+ (async (ctx, params) => {
152
+ const invitation = await ctx.$query.findOne({
153
+ token: params.token,
154
+ });
155
+
156
+ if (!invitation) {
157
+ return { error: "INVITATION_NOT_FOUND" as const };
158
+ }
159
+ if (invitation.status !== "pending") {
160
+ return { error: "INVITATION_NOT_VALID" as const };
161
+ }
162
+ if (new Date(invitation.expiresAt) < new Date()) {
163
+ return { error: "INVITATION_EXPIRED" as const };
164
+ }
165
+
166
+ // Emit accepted event — WorkspaceMember handles adding the member
167
+ await ctx.invitationAccepted.emit({
168
+ workspaceInvitationId: invitation._id,
169
+ workspaceId: invitation.workspaceId,
170
+ acceptedBy: ctx.$auth.params.accountId,
171
+ role: invitation.role,
172
+ workspaceName: invitation.workspaceName,
173
+ workspaceType: invitation.workspaceType,
174
+ });
175
+
176
+ // Generate workspace token for the new member
177
+ const wsToken = workspaceToken.generateJWT({
178
+ accountId: ctx.$auth.params.accountId,
179
+ workspaceId: invitation.workspaceId,
180
+ role: invitation.role,
181
+ workspaceType: invitation.workspaceType,
182
+ });
183
+
184
+ return {
185
+ success: true,
186
+ token: wsToken,
187
+ workspaceId: invitation.workspaceId,
188
+ };
189
+ }),
190
+ )
191
+
192
+ .mutateMethod(
193
+ "revokeInvitation",
194
+ { params: { workspaceInvitationId } },
195
+ ONLY_SERVER &&
196
+ (async (ctx, params) => {
197
+ const invitation = await ctx.$query.findOne({
198
+ _id: params.workspaceInvitationId,
199
+ });
200
+ if (!invitation || invitation.status !== "pending") {
201
+ return { error: "INVITATION_NOT_VALID" as const };
202
+ }
203
+
204
+ await ctx.invitationRevoked.emit({
205
+ workspaceInvitationId: params.workspaceInvitationId,
206
+ });
207
+
208
+ return { success: true };
209
+ }),
210
+ )
211
+
212
+ .protectBy(userToken, (p: any) => ({ invitedBy: p.accountId }))
213
+ .clientQuery("getByWorkspace", async (ctx) => ctx.$query.find({}))
214
+ ;
215
+ };
216
+
217
+ export type WorkspaceInvitationAggregate<
218
+ Data extends WorkspaceInvitationAggregateData = WorkspaceInvitationAggregateData,
219
+ > = ReturnType<typeof createWorkspaceInvitationAggregate<Data>>;
@@ -16,6 +16,7 @@ export type WorkspaceMemberAggregateData = {
16
16
  workspaceToken: WorkspaceToken;
17
17
  userToken: Token;
18
18
  WorkspaceAggregate: ArcAggregateElement<any, any, any, any, any, any>;
19
+ WorkspaceInvitationAggregate?: ArcAggregateElement<any, any, any, any, any, any>;
19
20
  };
20
21
 
21
22
  export const createWorkspaceMemberAggregate = <
@@ -30,11 +31,12 @@ export const createWorkspaceMemberAggregate = <
30
31
  workspaceToken,
31
32
  userToken,
32
33
  WorkspaceAggregate,
34
+ WorkspaceInvitationAggregate,
33
35
  } = data;
34
36
 
35
37
  const workspaceCreatedEvent = WorkspaceAggregate.getEvent("workspaceCreated");
36
38
 
37
- const Constructor = aggregate(
39
+ let Constructor = aggregate(
38
40
  `${data.name}WorkspaceMembers`,
39
41
  workspaceMemberId,
40
42
  {
@@ -109,7 +111,48 @@ export const createWorkspaceMemberAggregate = <
109
111
  });
110
112
  })
111
113
 
112
- .mutateMethod(
114
+ // Remove all members when workspace is archived
115
+ .handleEvent(WorkspaceAggregate.getEvent("workspaceArchived"), async (ctx, event) => {
116
+ const members = await ctx.find({
117
+ where: { workspaceId: event.payload.workspaceId },
118
+ });
119
+ for (const member of members) {
120
+ await ctx.remove(member._id);
121
+ }
122
+ })
123
+
124
+ // Sync workspace name to all members when workspace is renamed
125
+ .handleEvent(WorkspaceAggregate.getEvent("workspaceRenamed"), async (ctx, event) => {
126
+ const members = await ctx.find({
127
+ where: { workspaceId: event.payload.workspaceId },
128
+ });
129
+ for (const member of members) {
130
+ await ctx.modify(member._id, {
131
+ workspaceName: event.payload.name,
132
+ });
133
+ }
134
+ })
135
+
136
+ ;
137
+
138
+ // Auto-add member when invitation is accepted
139
+ if (WorkspaceInvitationAggregate) {
140
+ const invitationAcceptedEvent = WorkspaceInvitationAggregate.getEvent("invitationAccepted");
141
+ Constructor = Constructor.handleEvent(invitationAcceptedEvent, async (ctx, event) => {
142
+ const p = event.payload;
143
+ const memberId = workspaceMemberId.generate();
144
+ await ctx.set(memberId, {
145
+ workspaceId: p.workspaceId,
146
+ accountId: p.acceptedBy,
147
+ role: p.role,
148
+ workspaceName: p.workspaceName,
149
+ workspaceType: p.workspaceType,
150
+ joinedAt: event.createdAt,
151
+ });
152
+ });
153
+ }
154
+
155
+ Constructor = Constructor.mutateMethod(
113
156
  "addMember",
114
157
  {
115
158
  params: {
@@ -205,7 +248,9 @@ export const createWorkspaceMemberAggregate = <
205
248
  )
206
249
 
207
250
  .protectBy(userToken, (params: any) => ({ accountId: params.accountId }))
251
+ .protectBy(workspaceToken, (params: any) => ({ workspaceId: params.workspaceId }))
208
252
  .clientQuery("getAll", async (ctx) => ctx.$query.find({}))
253
+ .clientQuery("getByWorkspace", async (ctx) => ctx.$query.find({}))
209
254
  ;
210
255
 
211
256
  return Constructor;
@@ -1,4 +1,4 @@
1
- import { aggregate, date, string, type ArcId } from "@arcote.tech/arc";
1
+ import { aggregate, boolean, date, string, type ArcId } from "@arcote.tech/arc";
2
2
  import type { WorkspaceToken } from "../tokens/workspace-token";
3
3
 
4
4
  export type WorkspaceAggregateData = {
@@ -20,6 +20,7 @@ export const createWorkspaceAggregate = <
20
20
  name: string().minLength(1).maxLength(100),
21
21
  type: string(),
22
22
  ownerId: accountId,
23
+ isArchived: boolean(),
23
24
  createdAt: date(),
24
25
  })
25
26
  .publicEvent(
@@ -37,6 +38,7 @@ export const createWorkspaceAggregate = <
37
38
  name,
38
39
  type,
39
40
  ownerId,
41
+ isArchived: false,
40
42
  createdAt: event.createdAt,
41
43
  });
42
44
  },
@@ -114,6 +116,36 @@ export const createWorkspaceAggregate = <
114
116
  }),
115
117
  )
116
118
 
119
+ .publicEvent(
120
+ "workspaceArchived",
121
+ { workspaceId },
122
+ async (ctx, event) => {
123
+ await ctx.modify(event.payload.workspaceId, {
124
+ isArchived: true,
125
+ });
126
+ },
127
+ )
128
+
129
+ .mutateMethod(
130
+ "archive",
131
+ { params: { workspaceId } },
132
+ ONLY_SERVER &&
133
+ (async (ctx, params) => {
134
+ const existing = await ctx.$query.findOne({
135
+ _id: params.workspaceId,
136
+ });
137
+ if (!existing) {
138
+ return { error: "WORKSPACE_NOT_FOUND" as const };
139
+ }
140
+
141
+ await ctx.workspaceArchived.emit({
142
+ workspaceId: params.workspaceId,
143
+ });
144
+
145
+ return { success: true };
146
+ }),
147
+ )
148
+
117
149
  .clientQuery("getAll", async (ctx) => ctx.$query.find({}))
118
150
  ;
119
151
  };
@@ -0,0 +1,12 @@
1
+ import { id } from "@arcote.tech/arc";
2
+
3
+ export type WorkspaceInvitationIdData = {
4
+ name: string;
5
+ };
6
+
7
+ export const createWorkspaceInvitationId = <const Data extends WorkspaceInvitationIdData>(
8
+ data: Readonly<Data>,
9
+ ) => id(`${data.name}WorkspaceInvitation`);
10
+
11
+ export type WorkspaceInvitationId<Data extends WorkspaceInvitationIdData = WorkspaceInvitationIdData> =
12
+ ReturnType<typeof createWorkspaceInvitationId<Data>>;
package/src/index.ts CHANGED
@@ -6,12 +6,16 @@ export { createWorkspaceAggregate } from "./aggregates/workspace";
6
6
  export type { WorkspaceAggregate } from "./aggregates/workspace";
7
7
  export { createWorkspaceMemberAggregate } from "./aggregates/workspace-member";
8
8
  export type { WorkspaceMemberAggregate } from "./aggregates/workspace-member";
9
+ export { createWorkspaceInvitationAggregate } from "./aggregates/workspace-invitation";
10
+ export type { WorkspaceInvitationAggregate } from "./aggregates/workspace-invitation";
9
11
 
10
12
  // --- ID factories & types ---
11
13
  export { createWorkspaceId } from "./ids/workspace";
12
14
  export type { WorkspaceId } from "./ids/workspace";
13
15
  export { createWorkspaceMemberId } from "./ids/workspace-member";
14
16
  export type { WorkspaceMemberId } from "./ids/workspace-member";
17
+ export { createWorkspaceInvitationId } from "./ids/workspace-invitation";
18
+ export type { WorkspaceInvitationId } from "./ids/workspace-invitation";
15
19
 
16
20
  // --- Token factory & type ---
17
21
  export { createWorkspaceToken } from "./tokens/workspace-token";
@@ -6,16 +6,20 @@ import {
6
6
  import type { AccountId, Token } from "@arcote.tech/arc-auth";
7
7
  import { createWorkspaceId } from "./ids/workspace";
8
8
  import { createWorkspaceMemberId } from "./ids/workspace-member";
9
+ import { createWorkspaceInvitationId } from "./ids/workspace-invitation";
9
10
  import { createWorkspaceToken } from "./tokens/workspace-token";
10
11
  import { createWorkspaceAggregate } from "./aggregates/workspace";
11
12
  import { createWorkspaceMemberAggregate } from "./aggregates/workspace-member";
13
+ import { createWorkspaceInvitationAggregate } from "./aggregates/workspace-invitation";
12
14
 
13
15
  export class WorkspaceBuilder<
14
16
  WsId,
15
17
  WsMemberId,
18
+ WsInvId,
16
19
  WsToken,
17
20
  Workspace extends ArcAggregateElement,
18
21
  WorkspaceMember extends ArcAggregateElement,
22
+ WorkspaceInvitation extends ArcAggregateElement,
19
23
  Types extends readonly string[],
20
24
  Elements extends ArcContextElement<any>[],
21
25
  > {
@@ -23,9 +27,11 @@ export class WorkspaceBuilder<
23
27
  private readonly _name: string,
24
28
  readonly workspaceId: WsId,
25
29
  readonly workspaceMemberId: WsMemberId,
30
+ readonly workspaceInvitationId: WsInvId,
26
31
  readonly workspaceToken: WsToken,
27
32
  readonly Workspace: Workspace,
28
33
  readonly WorkspaceMember: WorkspaceMember,
34
+ readonly WorkspaceInvitation: WorkspaceInvitation,
29
35
  readonly types: Types,
30
36
  readonly elements: Elements,
31
37
  ) {}
@@ -35,15 +41,17 @@ export class WorkspaceBuilder<
35
41
  context: context(this.elements),
36
42
  workspaceId: this.workspaceId,
37
43
  workspaceMemberId: this.workspaceMemberId,
44
+ workspaceInvitationId: this.workspaceInvitationId,
38
45
  workspaceToken: this.workspaceToken,
39
46
  Workspace: this.Workspace,
40
47
  WorkspaceMember: this.WorkspaceMember,
48
+ WorkspaceInvitation: this.WorkspaceInvitation,
41
49
  types: this.types,
42
50
  isOwner: (params: { role: string }) => params.role === "owner",
43
51
  isMember: (params: { role: string }) =>
44
52
  params.role === "member" || params.role === "owner",
45
- isType: (type: string) => (params: { workspaceType: string }) =>
46
- params.workspaceType === type,
53
+ isType: (...types: string[]) => (token: { params?: { workspaceType?: string } }) =>
54
+ types.includes(token.params?.workspaceType ?? ""),
47
55
  };
48
56
  }
49
57
  }
@@ -61,6 +69,7 @@ export function workspace<
61
69
  }) {
62
70
  const workspaceId = createWorkspaceId({ name: config.name });
63
71
  const workspaceMemberId = createWorkspaceMemberId({ name: config.name });
72
+ const workspaceInvitationId = createWorkspaceInvitationId({ name: config.name });
64
73
  const workspaceToken = createWorkspaceToken({
65
74
  name: config.name,
66
75
  secret: config.secret,
@@ -74,6 +83,15 @@ export function workspace<
74
83
  workspaceToken,
75
84
  });
76
85
 
86
+ const WorkspaceInvitation = createWorkspaceInvitationAggregate({
87
+ name: config.name,
88
+ workspaceInvitationId,
89
+ workspaceId,
90
+ accountId: config.accountId,
91
+ workspaceToken,
92
+ userToken: config.userToken,
93
+ });
94
+
77
95
  const WorkspaceMember = createWorkspaceMemberAggregate({
78
96
  name: config.name,
79
97
  workspaceMemberId,
@@ -82,16 +100,19 @@ export function workspace<
82
100
  workspaceToken,
83
101
  userToken: config.userToken,
84
102
  WorkspaceAggregate: Workspace,
103
+ WorkspaceInvitationAggregate: WorkspaceInvitation,
85
104
  });
86
105
 
87
106
  return new WorkspaceBuilder(
88
107
  config.name,
89
108
  workspaceId,
90
109
  workspaceMemberId,
110
+ workspaceInvitationId,
91
111
  workspaceToken,
92
112
  Workspace,
93
113
  WorkspaceMember,
114
+ WorkspaceInvitation,
94
115
  config.types,
95
- [Workspace, WorkspaceMember],
116
+ [Workspace, WorkspaceMember, WorkspaceInvitation],
96
117
  );
97
118
  }