@m5kdev/backend 0.1.1 → 0.1.3
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/src/lib/posthog.js +7 -0
- package/dist/src/lib/sentry.js +9 -0
- package/dist/src/modules/access/access.repository.js +32 -0
- package/dist/src/modules/access/access.service.js +51 -0
- package/dist/src/modules/access/access.test.js +182 -0
- package/dist/src/modules/access/access.utils.js +20 -0
- package/dist/src/modules/ai/ai.db.js +39 -0
- package/dist/src/modules/ai/ai.prompt.js +30 -0
- package/dist/src/modules/ai/ai.repository.js +26 -0
- package/dist/src/modules/ai/ai.router.js +132 -0
- package/dist/src/modules/ai/ai.service.js +207 -0
- package/dist/src/modules/ai/ai.trpc.d.ts +5 -5
- package/dist/src/modules/ai/ai.trpc.js +20 -0
- package/dist/src/modules/ai/ideogram/ideogram.constants.js +167 -0
- package/dist/src/modules/ai/ideogram/ideogram.dto.js +49 -0
- package/dist/src/modules/ai/ideogram/ideogram.prompt.js +860 -0
- package/dist/src/modules/ai/ideogram/ideogram.repository.js +46 -0
- package/dist/src/modules/ai/ideogram/ideogram.service.js +11 -0
- package/dist/src/modules/auth/auth.db.js +215 -0
- package/dist/src/modules/auth/auth.dto.js +38 -0
- package/dist/src/modules/auth/auth.lib.d.ts +4 -4
- package/dist/src/modules/auth/auth.lib.js +284 -0
- package/dist/src/modules/auth/auth.middleware.js +52 -0
- package/dist/src/modules/auth/auth.repository.js +541 -0
- package/dist/src/modules/auth/auth.service.js +201 -0
- package/dist/src/modules/auth/auth.trpc.d.ts +18 -18
- package/dist/src/modules/auth/auth.trpc.js +157 -0
- package/dist/src/modules/auth/auth.utils.js +97 -0
- package/dist/src/modules/base/base.abstract.js +53 -0
- package/dist/src/modules/base/base.dto.js +112 -0
- package/dist/src/modules/base/base.grants.js +123 -0
- package/dist/src/modules/base/base.grants.test.js +668 -0
- package/dist/src/modules/base/base.repository.js +307 -0
- package/dist/src/modules/base/base.service.js +109 -0
- package/dist/src/modules/base/base.types.js +2 -0
- package/dist/src/modules/billing/billing.db.js +29 -0
- package/dist/src/modules/billing/billing.repository.js +235 -0
- package/dist/src/modules/billing/billing.router.js +56 -0
- package/dist/src/modules/billing/billing.service.js +147 -0
- package/dist/src/modules/billing/billing.trpc.d.ts +5 -5
- package/dist/src/modules/billing/billing.trpc.js +17 -0
- package/dist/src/modules/clay/clay.repository.js +26 -0
- package/dist/src/modules/clay/clay.service.js +24 -0
- package/dist/src/modules/connect/connect.db.js +30 -0
- package/dist/src/modules/connect/connect.dto.js +36 -0
- package/dist/src/modules/connect/connect.linkedin.js +53 -0
- package/dist/src/modules/connect/connect.oauth.js +198 -0
- package/dist/src/modules/connect/connect.repository.d.ts +7 -7
- package/dist/src/modules/connect/connect.repository.js +54 -0
- package/dist/src/modules/connect/connect.router.js +54 -0
- package/dist/src/modules/connect/connect.service.d.ts +14 -14
- package/dist/src/modules/connect/connect.service.js +114 -0
- package/dist/src/modules/connect/connect.trpc.d.ts +10 -10
- package/dist/src/modules/connect/connect.trpc.js +21 -0
- package/dist/src/modules/connect/connect.types.js +2 -0
- package/dist/src/modules/crypto/crypto.db.js +17 -0
- package/dist/src/modules/crypto/crypto.repository.js +10 -0
- package/dist/src/modules/crypto/crypto.service.js +52 -0
- package/dist/src/modules/email/email.service.js +107 -0
- package/dist/src/modules/file/file.repository.js +79 -0
- package/dist/src/modules/file/file.router.js +99 -0
- package/dist/src/modules/file/file.service.js +150 -0
- package/dist/src/modules/recurrence/recurrence.db.js +66 -0
- package/dist/src/modules/recurrence/recurrence.repository.js +39 -0
- package/dist/src/modules/recurrence/recurrence.service.js +70 -0
- package/dist/src/modules/recurrence/recurrence.trpc.d.ts +15 -15
- package/dist/src/modules/recurrence/recurrence.trpc.js +65 -0
- package/dist/src/modules/social/social.dto.js +18 -0
- package/dist/src/modules/social/social.linkedin.js +427 -0
- package/dist/src/modules/social/social.linkedin.test.js +235 -0
- package/dist/src/modules/social/social.service.js +76 -0
- package/dist/src/modules/social/social.types.js +2 -0
- package/dist/src/modules/tag/tag.db.js +42 -0
- package/dist/src/modules/tag/tag.dto.js +9 -0
- package/dist/src/modules/tag/tag.repository.js +154 -0
- package/dist/src/modules/tag/tag.service.js +31 -0
- package/dist/src/modules/tag/tag.trpc.d.ts +5 -5
- package/dist/src/modules/tag/tag.trpc.js +47 -0
- package/dist/src/modules/utils/applyPagination.js +16 -0
- package/dist/src/modules/utils/applySorting.js +18 -0
- package/dist/src/modules/utils/getConditionsFromFilters.js +200 -0
- package/dist/src/modules/video/video.service.js +84 -0
- package/dist/src/modules/webhook/webhook.constants.js +10 -0
- package/dist/src/modules/webhook/webhook.db.js +17 -0
- package/dist/src/modules/webhook/webhook.dto.js +7 -0
- package/dist/src/modules/webhook/webhook.repository.js +56 -0
- package/dist/src/modules/webhook/webhook.router.js +30 -0
- package/dist/src/modules/webhook/webhook.service.js +68 -0
- package/dist/src/modules/workflow/workflow.db.js +30 -0
- package/dist/src/modules/workflow/workflow.repository.js +105 -0
- package/dist/src/modules/workflow/workflow.service.js +37 -0
- package/dist/src/modules/workflow/workflow.trpc.d.ts +5 -5
- package/dist/src/modules/workflow/workflow.trpc.js +21 -0
- package/dist/src/modules/workflow/workflow.types.js +2 -0
- package/dist/src/modules/workflow/workflow.utils.js +173 -0
- package/dist/src/test/stubs/utils.js +5 -0
- package/dist/src/trpc/context.d.ts +5 -5
- package/dist/src/trpc/context.js +17 -0
- package/dist/src/trpc/index.js +6 -0
- package/dist/src/trpc/procedures.d.ts +56 -56
- package/dist/src/trpc/procedures.js +32 -0
- package/dist/src/trpc/utils.js +20 -0
- package/dist/src/types.d.ts +33 -33
- package/dist/src/types.js +13 -0
- package/dist/src/utils/errors.js +104 -0
- package/dist/src/utils/logger.js +11 -0
- package/dist/src/utils/posthog.js +31 -0
- package/dist/src/utils/types.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/tsconfig.json +2 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const neverthrow_1 = require("neverthrow");
|
|
4
|
+
const base_grants_1 = require("#modules/base/base.grants");
|
|
5
|
+
const errors_1 = require("#utils/errors");
|
|
6
|
+
// ============================================
|
|
7
|
+
// Mock Factories
|
|
8
|
+
// ============================================
|
|
9
|
+
function createMockUser(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
id: "user-123",
|
|
12
|
+
role: "member",
|
|
13
|
+
email: "test@example.com",
|
|
14
|
+
emailVerified: true,
|
|
15
|
+
name: "Test User",
|
|
16
|
+
createdAt: new Date(),
|
|
17
|
+
updatedAt: new Date(),
|
|
18
|
+
image: null,
|
|
19
|
+
onboarding: null,
|
|
20
|
+
preferences: null,
|
|
21
|
+
flags: null,
|
|
22
|
+
stripeCustomerId: null,
|
|
23
|
+
paymentCustomerId: null,
|
|
24
|
+
paymentPlanTier: null,
|
|
25
|
+
paymentPlanExpiresAt: null,
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createMockSession(overrides = {}) {
|
|
30
|
+
return {
|
|
31
|
+
id: "session-123",
|
|
32
|
+
userId: "user-123",
|
|
33
|
+
token: "token-123",
|
|
34
|
+
expiresAt: new Date(Date.now() + 86400000),
|
|
35
|
+
createdAt: new Date(),
|
|
36
|
+
updatedAt: new Date(),
|
|
37
|
+
ipAddress: null,
|
|
38
|
+
userAgent: null,
|
|
39
|
+
activeOrganizationId: null,
|
|
40
|
+
activeTeamId: null,
|
|
41
|
+
activeOrganizationRole: null,
|
|
42
|
+
activeTeamRole: null,
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createMockContext(userOverrides = {}, sessionOverrides = {}) {
|
|
47
|
+
return {
|
|
48
|
+
user: createMockUser(userOverrides),
|
|
49
|
+
session: createMockSession(sessionOverrides),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function createMockEntity(overrides = {}) {
|
|
53
|
+
return {
|
|
54
|
+
userId: "user-123",
|
|
55
|
+
teamId: undefined,
|
|
56
|
+
organizationId: undefined,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ============================================
|
|
61
|
+
// flattenNestedGrants
|
|
62
|
+
// ============================================
|
|
63
|
+
describe("flattenNestedGrants", () => {
|
|
64
|
+
it("converts permission object to grants array", () => {
|
|
65
|
+
const permission = {
|
|
66
|
+
posts: {
|
|
67
|
+
user: {
|
|
68
|
+
member: { read: "own", create: "own" },
|
|
69
|
+
},
|
|
70
|
+
team: {
|
|
71
|
+
admin: { write: "all" },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const result = (0, base_grants_1.flattenNestedGrants)(permission);
|
|
76
|
+
expect(result).toHaveLength(3);
|
|
77
|
+
expect(result).toContainEqual({
|
|
78
|
+
resource: "posts",
|
|
79
|
+
level: "user",
|
|
80
|
+
role: "member",
|
|
81
|
+
action: "read",
|
|
82
|
+
access: "own",
|
|
83
|
+
});
|
|
84
|
+
expect(result).toContainEqual({
|
|
85
|
+
resource: "posts",
|
|
86
|
+
level: "user",
|
|
87
|
+
role: "member",
|
|
88
|
+
action: "create",
|
|
89
|
+
access: "own",
|
|
90
|
+
});
|
|
91
|
+
expect(result).toContainEqual({
|
|
92
|
+
resource: "posts",
|
|
93
|
+
level: "team",
|
|
94
|
+
role: "admin",
|
|
95
|
+
action: "write",
|
|
96
|
+
access: "all",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("handles multiple resources", () => {
|
|
100
|
+
const permission = {
|
|
101
|
+
posts: {
|
|
102
|
+
user: { member: { read: "own" } },
|
|
103
|
+
},
|
|
104
|
+
comments: {
|
|
105
|
+
team: { admin: { delete: "all" } },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const result = (0, base_grants_1.flattenNestedGrants)(permission);
|
|
109
|
+
expect(result).toHaveLength(2);
|
|
110
|
+
expect(result).toContainEqual({
|
|
111
|
+
resource: "posts",
|
|
112
|
+
level: "user",
|
|
113
|
+
role: "member",
|
|
114
|
+
action: "read",
|
|
115
|
+
access: "own",
|
|
116
|
+
});
|
|
117
|
+
expect(result).toContainEqual({
|
|
118
|
+
resource: "comments",
|
|
119
|
+
level: "team",
|
|
120
|
+
role: "admin",
|
|
121
|
+
action: "delete",
|
|
122
|
+
access: "all",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it("handles permission with only some levels defined", () => {
|
|
126
|
+
const permission = {
|
|
127
|
+
posts: {
|
|
128
|
+
organization: {
|
|
129
|
+
owner: { delete: "all" },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const result = (0, base_grants_1.flattenNestedGrants)(permission);
|
|
134
|
+
expect(result).toHaveLength(1);
|
|
135
|
+
expect(result[0]).toEqual({
|
|
136
|
+
resource: "posts",
|
|
137
|
+
level: "organization",
|
|
138
|
+
role: "owner",
|
|
139
|
+
action: "delete",
|
|
140
|
+
access: "all",
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it("returns empty array for empty permission", () => {
|
|
144
|
+
const result = (0, base_grants_1.flattenNestedGrants)({});
|
|
145
|
+
expect(result).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
it("handles multiple roles per level", () => {
|
|
148
|
+
const permission = {
|
|
149
|
+
posts: {
|
|
150
|
+
user: {
|
|
151
|
+
member: { read: "own" },
|
|
152
|
+
admin: { read: "all", write: "all" },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const result = (0, base_grants_1.flattenNestedGrants)(permission);
|
|
157
|
+
expect(result).toHaveLength(3);
|
|
158
|
+
expect(result).toContainEqual({
|
|
159
|
+
resource: "posts",
|
|
160
|
+
level: "user",
|
|
161
|
+
role: "member",
|
|
162
|
+
action: "read",
|
|
163
|
+
access: "own",
|
|
164
|
+
});
|
|
165
|
+
expect(result).toContainEqual({
|
|
166
|
+
resource: "posts",
|
|
167
|
+
level: "user",
|
|
168
|
+
role: "admin",
|
|
169
|
+
action: "read",
|
|
170
|
+
access: "all",
|
|
171
|
+
});
|
|
172
|
+
expect(result).toContainEqual({
|
|
173
|
+
resource: "posts",
|
|
174
|
+
level: "user",
|
|
175
|
+
role: "admin",
|
|
176
|
+
action: "write",
|
|
177
|
+
access: "all",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ============================================
|
|
182
|
+
// checkPermissionSync
|
|
183
|
+
// ============================================
|
|
184
|
+
describe("checkPermissionSync", () => {
|
|
185
|
+
describe("edge cases", () => {
|
|
186
|
+
it("returns false for empty grants array", () => {
|
|
187
|
+
const ctx = createMockContext();
|
|
188
|
+
const result = (0, base_grants_1.checkPermissionSync)(ctx, []);
|
|
189
|
+
expect(result).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
it("returns false for undefined grants", () => {
|
|
192
|
+
const ctx = createMockContext();
|
|
193
|
+
const result = (0, base_grants_1.checkPermissionSync)(ctx, undefined);
|
|
194
|
+
expect(result).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe("user-level permissions", () => {
|
|
198
|
+
it("grants access with 'all' access regardless of entity", () => {
|
|
199
|
+
const ctx = createMockContext({ role: "member" });
|
|
200
|
+
const grants = [{ level: "user", role: "member", access: "all" }];
|
|
201
|
+
// No entity provided
|
|
202
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
203
|
+
// Entity with different userId - still allowed because "all" access
|
|
204
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, { userId: "other-user" })).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
it("grants access with 'own' access when userId matches", () => {
|
|
207
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
208
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
209
|
+
const entity = createMockEntity({ userId: "user-123" });
|
|
210
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it("denies access with 'own' access when userId does not match", () => {
|
|
213
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
214
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
215
|
+
const entity = createMockEntity({ userId: "other-user" });
|
|
216
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
it("denies access with 'own' access when no entity provided", () => {
|
|
219
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
220
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
221
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
it("denies access when user role does not match grant role", () => {
|
|
224
|
+
const ctx = createMockContext({ role: "viewer" });
|
|
225
|
+
const grants = [{ level: "user", role: "admin", access: "all" }];
|
|
226
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe("team-level permissions", () => {
|
|
230
|
+
it("grants access with 'all' access when team role matches", () => {
|
|
231
|
+
const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "admin" });
|
|
232
|
+
const grants = [{ level: "team", role: "admin", access: "all" }];
|
|
233
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
it("grants access with 'own' access when teamId matches", () => {
|
|
236
|
+
const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
|
|
237
|
+
const grants = [{ level: "team", role: "member", access: "own" }];
|
|
238
|
+
const entity = createMockEntity({ teamId: "team-1" });
|
|
239
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
it("denies access with 'own' access when teamId does not match", () => {
|
|
242
|
+
const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
|
|
243
|
+
const grants = [{ level: "team", role: "member", access: "own" }];
|
|
244
|
+
const entity = createMockEntity({ teamId: "team-2" });
|
|
245
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
it("denies access when team role does not match", () => {
|
|
248
|
+
const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "viewer" });
|
|
249
|
+
const grants = [{ level: "team", role: "admin", access: "all" }];
|
|
250
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
it("denies access when no active team", () => {
|
|
253
|
+
const ctx = createMockContext({}, { activeTeamId: null, activeTeamRole: null });
|
|
254
|
+
const grants = [{ level: "team", role: "admin", access: "all" }];
|
|
255
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe("organization-level permissions", () => {
|
|
259
|
+
it("grants access with 'all' access when organization role matches", () => {
|
|
260
|
+
const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "owner" });
|
|
261
|
+
const grants = [
|
|
262
|
+
{ level: "organization", role: "owner", access: "all" },
|
|
263
|
+
];
|
|
264
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
it("grants access with 'own' access when organizationId matches", () => {
|
|
267
|
+
const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "member" });
|
|
268
|
+
const grants = [
|
|
269
|
+
{ level: "organization", role: "member", access: "own" },
|
|
270
|
+
];
|
|
271
|
+
const entity = createMockEntity({ organizationId: "org-1" });
|
|
272
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
it("denies access with 'own' access when organizationId does not match", () => {
|
|
275
|
+
const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "member" });
|
|
276
|
+
const grants = [
|
|
277
|
+
{ level: "organization", role: "member", access: "own" },
|
|
278
|
+
];
|
|
279
|
+
const entity = createMockEntity({ organizationId: "org-2" });
|
|
280
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe("multiple grants", () => {
|
|
284
|
+
it("checks 'all' access before 'own' access (optimization)", () => {
|
|
285
|
+
const ctx = createMockContext({ id: "user-123", role: "admin" });
|
|
286
|
+
const grants = [
|
|
287
|
+
{ level: "user", role: "member", access: "own" }, // Would need entity check
|
|
288
|
+
{ level: "user", role: "admin", access: "all" }, // Should match first in pass 1
|
|
289
|
+
];
|
|
290
|
+
// No entity provided, but should still pass because "all" is checked first
|
|
291
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
it("falls back to 'own' access if no 'all' access matches", () => {
|
|
294
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
295
|
+
const grants = [
|
|
296
|
+
{ level: "user", role: "admin", access: "all" }, // Role doesn't match
|
|
297
|
+
{ level: "user", role: "member", access: "own" }, // Should match in pass 2
|
|
298
|
+
];
|
|
299
|
+
const entity = createMockEntity({ userId: "user-123" });
|
|
300
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
it("grants access if any level matches with 'all'", () => {
|
|
303
|
+
const ctx = createMockContext({ role: "viewer" }, { activeTeamId: "team-1", activeTeamRole: "admin" });
|
|
304
|
+
const grants = [
|
|
305
|
+
{ level: "user", role: "member", access: "all" }, // User role doesn't match
|
|
306
|
+
{ level: "team", role: "admin", access: "all" }, // Team role matches
|
|
307
|
+
];
|
|
308
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
describe("multiple entities", () => {
|
|
312
|
+
it("requires all entities to match for 'own' access", () => {
|
|
313
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
314
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
315
|
+
const matchingEntities = [{ userId: "user-123" }, { userId: "user-123" }];
|
|
316
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, matchingEntities)).toBe(true);
|
|
317
|
+
const mixedEntities = [{ userId: "user-123" }, { userId: "other-user" }];
|
|
318
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, mixedEntities)).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
it("denies access if any entity does not match", () => {
|
|
321
|
+
const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
|
|
322
|
+
const grants = [{ level: "team", role: "member", access: "own" }];
|
|
323
|
+
const entities = [{ teamId: "team-1" }, { teamId: "team-1" }, { teamId: "team-2" }];
|
|
324
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entities)).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
describe("level priority (user -> team -> organization)", () => {
|
|
328
|
+
it("checks user level before team level", () => {
|
|
329
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, { activeTeamId: "team-1", activeTeamRole: "member" });
|
|
330
|
+
// Both levels have matching grants, but user should be checked first
|
|
331
|
+
const grants = [
|
|
332
|
+
{ level: "team", role: "member", access: "own" },
|
|
333
|
+
{ level: "user", role: "member", access: "all" },
|
|
334
|
+
];
|
|
335
|
+
// Should return true from user-level "all" without needing entity
|
|
336
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
it("checks team level before organization level", () => {
|
|
339
|
+
const ctx = createMockContext({ role: "viewer" }, {
|
|
340
|
+
activeTeamId: "team-1",
|
|
341
|
+
activeTeamRole: "admin",
|
|
342
|
+
activeOrganizationId: "org-1",
|
|
343
|
+
activeOrganizationRole: "admin",
|
|
344
|
+
});
|
|
345
|
+
const grants = [
|
|
346
|
+
{ level: "organization", role: "admin", access: "all" },
|
|
347
|
+
{ level: "team", role: "admin", access: "all" },
|
|
348
|
+
];
|
|
349
|
+
// Both would match, but team is checked before org in the priority
|
|
350
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
describe("multi-level grants with different roles per level", () => {
|
|
354
|
+
it("user with different roles at each level - matches user level", () => {
|
|
355
|
+
const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
|
|
356
|
+
activeTeamId: "team-1",
|
|
357
|
+
activeTeamRole: "member",
|
|
358
|
+
activeOrganizationId: "org-1",
|
|
359
|
+
activeOrganizationRole: "admin",
|
|
360
|
+
});
|
|
361
|
+
// Grant requires "viewer" at user level
|
|
362
|
+
const grants = [{ level: "user", role: "viewer", access: "all" }];
|
|
363
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
it("user with different roles at each level - matches team level only", () => {
|
|
366
|
+
const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
|
|
367
|
+
activeTeamId: "team-1",
|
|
368
|
+
activeTeamRole: "manager",
|
|
369
|
+
activeOrganizationId: "org-1",
|
|
370
|
+
activeOrganizationRole: "member",
|
|
371
|
+
});
|
|
372
|
+
// Grant requires "admin" at user level (no match) or "manager" at team level (match)
|
|
373
|
+
const grants = [
|
|
374
|
+
{ level: "user", role: "admin", access: "all" },
|
|
375
|
+
{ level: "team", role: "manager", access: "all" },
|
|
376
|
+
];
|
|
377
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
it("user with different roles at each level - matches organization level only", () => {
|
|
380
|
+
const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
|
|
381
|
+
activeTeamId: "team-1",
|
|
382
|
+
activeTeamRole: "member",
|
|
383
|
+
activeOrganizationId: "org-1",
|
|
384
|
+
activeOrganizationRole: "owner",
|
|
385
|
+
});
|
|
386
|
+
// Grant requires roles that only match at organization level
|
|
387
|
+
const grants = [
|
|
388
|
+
{ level: "user", role: "admin", access: "all" },
|
|
389
|
+
{ level: "team", role: "admin", access: "all" },
|
|
390
|
+
{ level: "organization", role: "owner", access: "all" },
|
|
391
|
+
];
|
|
392
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
it("user with different roles at each level - no level matches", () => {
|
|
395
|
+
const ctx = createMockContext({ id: "user-123", role: "viewer" }, {
|
|
396
|
+
activeTeamId: "team-1",
|
|
397
|
+
activeTeamRole: "member",
|
|
398
|
+
activeOrganizationId: "org-1",
|
|
399
|
+
activeOrganizationRole: "member",
|
|
400
|
+
});
|
|
401
|
+
// Grant requires roles that don't match any level
|
|
402
|
+
const grants = [
|
|
403
|
+
{ level: "user", role: "admin", access: "all" },
|
|
404
|
+
{ level: "team", role: "admin", access: "all" },
|
|
405
|
+
{ level: "organization", role: "owner", access: "all" },
|
|
406
|
+
];
|
|
407
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
it("user with mixed 'all' and 'own' grants across levels - 'all' wins", () => {
|
|
410
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
411
|
+
activeTeamId: "team-1",
|
|
412
|
+
activeTeamRole: "admin",
|
|
413
|
+
activeOrganizationId: "org-1",
|
|
414
|
+
activeOrganizationRole: "member",
|
|
415
|
+
});
|
|
416
|
+
// User level has "own" (would need entity), team level has "all" (no entity needed)
|
|
417
|
+
const grants = [
|
|
418
|
+
{ level: "user", role: "member", access: "own" },
|
|
419
|
+
{ level: "team", role: "admin", access: "all" },
|
|
420
|
+
{ level: "organization", role: "owner", access: "own" },
|
|
421
|
+
];
|
|
422
|
+
// Should pass because team-level "all" is checked in pass 1
|
|
423
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants)).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
it("user with 'own' grants at multiple levels - first matching level wins", () => {
|
|
426
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
427
|
+
activeTeamId: "team-1",
|
|
428
|
+
activeTeamRole: "member",
|
|
429
|
+
activeOrganizationId: "org-1",
|
|
430
|
+
activeOrganizationRole: "member",
|
|
431
|
+
});
|
|
432
|
+
// All levels have "own" access, entity matches user level
|
|
433
|
+
const grants = [
|
|
434
|
+
{ level: "user", role: "member", access: "own" },
|
|
435
|
+
{ level: "team", role: "member", access: "own" },
|
|
436
|
+
{ level: "organization", role: "member", access: "own" },
|
|
437
|
+
];
|
|
438
|
+
const entity = createMockEntity({
|
|
439
|
+
userId: "user-123",
|
|
440
|
+
teamId: "team-2",
|
|
441
|
+
organizationId: "org-2",
|
|
442
|
+
});
|
|
443
|
+
// Should pass because user-level "own" matches (checked first in pass 2)
|
|
444
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
it("user with 'own' grants - team level matches when user level does not", () => {
|
|
447
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
448
|
+
activeTeamId: "team-1",
|
|
449
|
+
activeTeamRole: "member",
|
|
450
|
+
activeOrganizationId: "org-1",
|
|
451
|
+
activeOrganizationRole: "member",
|
|
452
|
+
});
|
|
453
|
+
const grants = [
|
|
454
|
+
{ level: "user", role: "member", access: "own" },
|
|
455
|
+
{ level: "team", role: "member", access: "own" },
|
|
456
|
+
{ level: "organization", role: "member", access: "own" },
|
|
457
|
+
];
|
|
458
|
+
// Entity belongs to a different user but same team
|
|
459
|
+
const entity = createMockEntity({
|
|
460
|
+
userId: "other-user",
|
|
461
|
+
teamId: "team-1",
|
|
462
|
+
organizationId: "org-2",
|
|
463
|
+
});
|
|
464
|
+
// User-level fails (userId mismatch), team-level passes (teamId matches)
|
|
465
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
it("user with 'own' grants - organization level matches when user and team do not", () => {
|
|
468
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
469
|
+
activeTeamId: "team-1",
|
|
470
|
+
activeTeamRole: "member",
|
|
471
|
+
activeOrganizationId: "org-1",
|
|
472
|
+
activeOrganizationRole: "member",
|
|
473
|
+
});
|
|
474
|
+
const grants = [
|
|
475
|
+
{ level: "user", role: "member", access: "own" },
|
|
476
|
+
{ level: "team", role: "member", access: "own" },
|
|
477
|
+
{ level: "organization", role: "member", access: "own" },
|
|
478
|
+
];
|
|
479
|
+
// Entity belongs to a different user and team, but same organization
|
|
480
|
+
const entity = createMockEntity({
|
|
481
|
+
userId: "other-user",
|
|
482
|
+
teamId: "team-2",
|
|
483
|
+
organizationId: "org-1",
|
|
484
|
+
});
|
|
485
|
+
// User-level fails, team-level fails, organization-level passes
|
|
486
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
487
|
+
});
|
|
488
|
+
it("complex scenario: admin user bypasses ownership checks", () => {
|
|
489
|
+
const ctx = createMockContext({ id: "admin-user", role: "admin" }, {
|
|
490
|
+
activeTeamId: "team-1",
|
|
491
|
+
activeTeamRole: "member",
|
|
492
|
+
activeOrganizationId: "org-1",
|
|
493
|
+
activeOrganizationRole: "member",
|
|
494
|
+
});
|
|
495
|
+
// Grant allows admins to access all, or regular members to access own
|
|
496
|
+
const grants = [
|
|
497
|
+
{ level: "user", role: "admin", access: "all" },
|
|
498
|
+
{ level: "user", role: "member", access: "own" },
|
|
499
|
+
];
|
|
500
|
+
// Entity belongs to someone else, but admin has "all" access
|
|
501
|
+
const entity = createMockEntity({ userId: "other-user" });
|
|
502
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
it("complex scenario: regular user limited to own resources", () => {
|
|
505
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
506
|
+
activeTeamId: "team-1",
|
|
507
|
+
activeTeamRole: "member",
|
|
508
|
+
activeOrganizationId: "org-1",
|
|
509
|
+
activeOrganizationRole: "member",
|
|
510
|
+
});
|
|
511
|
+
// Grant allows admins to access all, or regular members to access own
|
|
512
|
+
const grants = [
|
|
513
|
+
{ level: "user", role: "admin", access: "all" },
|
|
514
|
+
{ level: "user", role: "member", access: "own" },
|
|
515
|
+
];
|
|
516
|
+
// Entity belongs to someone else - member can't access
|
|
517
|
+
const otherEntity = createMockEntity({ userId: "other-user" });
|
|
518
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, otherEntity)).toBe(false);
|
|
519
|
+
// Entity belongs to the user - member can access
|
|
520
|
+
const ownEntity = createMockEntity({ userId: "user-123" });
|
|
521
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, ownEntity)).toBe(true);
|
|
522
|
+
});
|
|
523
|
+
it("team admin can access all team resources regardless of user ownership", () => {
|
|
524
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
525
|
+
activeTeamId: "team-1",
|
|
526
|
+
activeTeamRole: "admin",
|
|
527
|
+
activeOrganizationId: "org-1",
|
|
528
|
+
activeOrganizationRole: "member",
|
|
529
|
+
});
|
|
530
|
+
// Grant: user-level own OR team-level all for admins
|
|
531
|
+
const grants = [
|
|
532
|
+
{ level: "user", role: "member", access: "own" },
|
|
533
|
+
{ level: "team", role: "admin", access: "all" },
|
|
534
|
+
];
|
|
535
|
+
// Entity belongs to another user in the same team
|
|
536
|
+
const entity = createMockEntity({ userId: "other-user", teamId: "team-1" });
|
|
537
|
+
// Team admin has "all" access, so ownership doesn't matter
|
|
538
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
539
|
+
});
|
|
540
|
+
it("organization owner can access all organization resources", () => {
|
|
541
|
+
const ctx = createMockContext({ id: "user-123", role: "member" }, {
|
|
542
|
+
activeTeamId: "team-1",
|
|
543
|
+
activeTeamRole: "member",
|
|
544
|
+
activeOrganizationId: "org-1",
|
|
545
|
+
activeOrganizationRole: "owner",
|
|
546
|
+
});
|
|
547
|
+
// Grant: owner at org level has all access
|
|
548
|
+
const grants = [
|
|
549
|
+
{ level: "user", role: "member", access: "own" },
|
|
550
|
+
{ level: "team", role: "admin", access: "own" },
|
|
551
|
+
{ level: "organization", role: "owner", access: "all" },
|
|
552
|
+
];
|
|
553
|
+
// Entity belongs to another user and team, but in the same org
|
|
554
|
+
const entity = createMockEntity({
|
|
555
|
+
userId: "other-user",
|
|
556
|
+
teamId: "team-2",
|
|
557
|
+
organizationId: "org-1",
|
|
558
|
+
});
|
|
559
|
+
// Org owner has "all" access
|
|
560
|
+
expect((0, base_grants_1.checkPermissionSync)(ctx, grants, entity)).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
// ============================================
|
|
565
|
+
// checkPermissionAsync
|
|
566
|
+
// ============================================
|
|
567
|
+
describe("checkPermissionAsync", () => {
|
|
568
|
+
describe("edge cases", () => {
|
|
569
|
+
it("returns ok(false) for empty grants array", async () => {
|
|
570
|
+
const ctx = createMockContext();
|
|
571
|
+
const getEntities = jest.fn();
|
|
572
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, [], getEntities);
|
|
573
|
+
expect(result.isOk()).toBe(true);
|
|
574
|
+
if (result.isOk())
|
|
575
|
+
expect(result.value).toBe(false);
|
|
576
|
+
expect(getEntities).not.toHaveBeenCalled();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
describe("'all' access optimization", () => {
|
|
580
|
+
it("returns ok(true) without calling getEntities when 'all' access matches", async () => {
|
|
581
|
+
const ctx = createMockContext({ role: "admin" });
|
|
582
|
+
const grants = [{ level: "user", role: "admin", access: "all" }];
|
|
583
|
+
const getEntities = jest.fn();
|
|
584
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
585
|
+
expect(result.isOk()).toBe(true);
|
|
586
|
+
if (result.isOk())
|
|
587
|
+
expect(result.value).toBe(true);
|
|
588
|
+
expect(getEntities).not.toHaveBeenCalled();
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
describe("'own' access with entity fetch", () => {
|
|
592
|
+
it("calls getEntities and grants access when ownership matches", async () => {
|
|
593
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
594
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
595
|
+
const entity = createMockEntity({ userId: "user-123" });
|
|
596
|
+
const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(entity));
|
|
597
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
598
|
+
expect(result.isOk()).toBe(true);
|
|
599
|
+
if (result.isOk())
|
|
600
|
+
expect(result.value).toBe(true);
|
|
601
|
+
expect(getEntities).toHaveBeenCalledTimes(1);
|
|
602
|
+
});
|
|
603
|
+
it("calls getEntities and denies access when ownership does not match", async () => {
|
|
604
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
605
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
606
|
+
const entity = createMockEntity({ userId: "other-user" });
|
|
607
|
+
const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(entity));
|
|
608
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
609
|
+
expect(result.isOk()).toBe(true);
|
|
610
|
+
if (result.isOk())
|
|
611
|
+
expect(result.value).toBe(false);
|
|
612
|
+
expect(getEntities).toHaveBeenCalledTimes(1);
|
|
613
|
+
});
|
|
614
|
+
it("handles undefined entities from getEntities", async () => {
|
|
615
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
616
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
617
|
+
const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(undefined));
|
|
618
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
619
|
+
expect(result.isOk()).toBe(true);
|
|
620
|
+
if (result.isOk())
|
|
621
|
+
expect(result.value).toBe(false);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
describe("error propagation", () => {
|
|
625
|
+
it("propagates errors from getEntities", async () => {
|
|
626
|
+
const ctx = createMockContext({ id: "user-123", role: "member" });
|
|
627
|
+
const grants = [{ level: "user", role: "member", access: "own" }];
|
|
628
|
+
const mockError = new errors_1.ServerError({
|
|
629
|
+
layer: "service",
|
|
630
|
+
layerName: "BasePermissionService",
|
|
631
|
+
code: "NOT_FOUND",
|
|
632
|
+
message: "Entity not found",
|
|
633
|
+
cause: null,
|
|
634
|
+
});
|
|
635
|
+
const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.err)(mockError));
|
|
636
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
637
|
+
expect(result.isErr()).toBe(true);
|
|
638
|
+
if (result.isErr()) {
|
|
639
|
+
expect(result.error.code).toBe("NOT_FOUND");
|
|
640
|
+
expect(result.error.message).toBe("Entity not found");
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
describe("team and organization levels", () => {
|
|
645
|
+
it("checks team-level 'own' access correctly", async () => {
|
|
646
|
+
const ctx = createMockContext({}, { activeTeamId: "team-1", activeTeamRole: "member" });
|
|
647
|
+
const grants = [{ level: "team", role: "member", access: "own" }];
|
|
648
|
+
const entity = createMockEntity({ teamId: "team-1" });
|
|
649
|
+
const getEntities = jest.fn().mockResolvedValue((0, neverthrow_1.ok)(entity));
|
|
650
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
651
|
+
expect(result.isOk()).toBe(true);
|
|
652
|
+
if (result.isOk())
|
|
653
|
+
expect(result.value).toBe(true);
|
|
654
|
+
});
|
|
655
|
+
it("checks organization-level 'all' access without fetching entities", async () => {
|
|
656
|
+
const ctx = createMockContext({}, { activeOrganizationId: "org-1", activeOrganizationRole: "owner" });
|
|
657
|
+
const grants = [
|
|
658
|
+
{ level: "organization", role: "owner", access: "all" },
|
|
659
|
+
];
|
|
660
|
+
const getEntities = jest.fn();
|
|
661
|
+
const result = await (0, base_grants_1.checkPermissionAsync)(ctx, grants, getEntities);
|
|
662
|
+
expect(result.isOk()).toBe(true);
|
|
663
|
+
if (result.isOk())
|
|
664
|
+
expect(result.value).toBe(true);
|
|
665
|
+
expect(getEntities).not.toHaveBeenCalled();
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
});
|