@m5kdev/backend 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/modules/ai/ai.service.d.ts +11 -13
- package/dist/src/modules/ai/ai.service.js +6 -6
- package/dist/src/modules/ai/ai.trpc.d.ts +1 -1
- package/dist/src/modules/auth/auth.lib.d.ts +8 -12
- package/dist/src/modules/auth/auth.lib.js +2 -2
- package/dist/src/modules/auth/auth.service.d.ts +17 -47
- package/dist/src/modules/auth/auth.service.js +79 -66
- package/dist/src/modules/auth/auth.trpc.d.ts +16 -16
- package/dist/src/modules/base/base.abstract.d.ts +3 -2
- package/dist/src/modules/base/base.abstract.js +10 -1
- package/dist/src/modules/base/base.actor.d.ts +68 -0
- package/dist/src/modules/base/base.actor.js +99 -0
- package/dist/src/modules/base/base.actor.test.d.ts +1 -0
- package/dist/src/modules/base/base.actor.test.js +58 -0
- package/dist/src/modules/base/base.grants.d.ts +3 -7
- package/dist/src/modules/base/base.grants.js +22 -10
- package/dist/src/modules/base/base.grants.test.js +16 -45
- package/dist/src/modules/base/base.procedure.d.ts +109 -0
- package/dist/src/modules/base/base.procedure.js +301 -0
- package/dist/src/modules/base/base.repository.d.ts +1 -0
- package/dist/src/modules/base/base.repository.js +12 -2
- package/dist/src/modules/base/base.service.d.ts +23 -23
- package/dist/src/modules/base/base.service.js +26 -12
- package/dist/src/modules/base/base.service.test.d.ts +1 -0
- package/dist/src/modules/base/base.service.test.js +443 -0
- package/dist/src/modules/billing/billing.service.d.ts +4 -25
- package/dist/src/modules/billing/billing.service.js +6 -6
- package/dist/src/modules/billing/billing.trpc.d.ts +2 -2
- package/dist/src/modules/billing/billing.trpc.js +4 -6
- package/dist/src/modules/connect/connect.repository.d.ts +3 -3
- package/dist/src/modules/connect/connect.service.d.ts +21 -13
- package/dist/src/modules/connect/connect.service.js +10 -8
- package/dist/src/modules/connect/connect.trpc.d.ts +2 -2
- package/dist/src/modules/recurrence/recurrence.service.d.ts +59 -8
- package/dist/src/modules/recurrence/recurrence.service.js +16 -14
- package/dist/src/modules/recurrence/recurrence.trpc.d.ts +3 -3
- package/dist/src/modules/recurrence/recurrence.trpc.js +1 -1
- package/dist/src/modules/social/social.service.d.ts +3 -4
- package/dist/src/modules/social/social.service.js +3 -3
- package/dist/src/modules/tag/tag.repository.js +27 -26
- package/dist/src/modules/tag/tag.service.d.ts +90 -15
- package/dist/src/modules/tag/tag.service.js +20 -12
- package/dist/src/modules/tag/tag.trpc.d.ts +3 -3
- package/dist/src/modules/workflow/workflow.service.d.ts +48 -8
- package/dist/src/modules/workflow/workflow.service.js +6 -6
- package/dist/src/modules/workflow/workflow.trpc.d.ts +2 -2
- package/dist/src/types.d.ts +19 -19
- package/dist/src/utils/trpc.d.ts +31 -41
- package/dist/src/utils/trpc.js +95 -0
- package/dist/src/utils/trpc.test.d.ts +1 -0
- package/dist/src/utils/trpc.test.js +154 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TRPCMethods } from "../../utils/trpc";
|
|
2
2
|
import type { AuthService } from "./auth.service";
|
|
3
3
|
export declare function createAuthTRPC({ router, publicProcedure, privateProcedure: procedure, adminProcedure }: TRPCMethods, authService: AuthService): import("@trpc/server").TRPCBuiltRouter<{
|
|
4
|
-
ctx: import("
|
|
4
|
+
ctx: import("../../utils/trpc").Context;
|
|
5
5
|
meta: any;
|
|
6
6
|
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
7
7
|
transformer: true;
|
|
@@ -49,10 +49,10 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
49
49
|
input: void;
|
|
50
50
|
output: {
|
|
51
51
|
id: string;
|
|
52
|
-
status: string;
|
|
53
52
|
createdAt: Date;
|
|
54
53
|
updatedAt: Date | null;
|
|
55
54
|
expiresAt: Date | null;
|
|
55
|
+
status: string;
|
|
56
56
|
claimUserId: string | null;
|
|
57
57
|
claimedAt: Date | null;
|
|
58
58
|
claimedEmail: string | null;
|
|
@@ -67,11 +67,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
67
67
|
output: {
|
|
68
68
|
id: string;
|
|
69
69
|
email: string;
|
|
70
|
-
url: string;
|
|
71
70
|
createdAt: Date;
|
|
72
|
-
userId: string;
|
|
73
71
|
expiresAt: Date | null;
|
|
72
|
+
userId: string;
|
|
74
73
|
claimId: string;
|
|
74
|
+
url: string;
|
|
75
75
|
};
|
|
76
76
|
meta: any;
|
|
77
77
|
}>;
|
|
@@ -82,11 +82,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
82
82
|
output: {
|
|
83
83
|
id: string;
|
|
84
84
|
email: string;
|
|
85
|
-
url: string;
|
|
86
85
|
createdAt: Date;
|
|
87
|
-
userId: string;
|
|
88
86
|
expiresAt: Date | null;
|
|
87
|
+
userId: string;
|
|
89
88
|
claimId: string;
|
|
89
|
+
url: string;
|
|
90
90
|
}[];
|
|
91
91
|
meta: any;
|
|
92
92
|
}>;
|
|
@@ -139,11 +139,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
139
139
|
input: void;
|
|
140
140
|
output: {
|
|
141
141
|
id: string;
|
|
142
|
-
email: string | null;
|
|
143
142
|
name: string | null;
|
|
144
|
-
|
|
143
|
+
email: string | null;
|
|
145
144
|
createdAt: Date;
|
|
146
145
|
updatedAt: Date | null;
|
|
146
|
+
status: string;
|
|
147
147
|
}[];
|
|
148
148
|
meta: any;
|
|
149
149
|
}>;
|
|
@@ -153,11 +153,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
153
153
|
};
|
|
154
154
|
output: {
|
|
155
155
|
id: string;
|
|
156
|
-
email: string | null;
|
|
157
156
|
name: string | null;
|
|
158
|
-
|
|
157
|
+
email: string | null;
|
|
159
158
|
createdAt: Date;
|
|
160
159
|
updatedAt: Date | null;
|
|
160
|
+
status: string;
|
|
161
161
|
};
|
|
162
162
|
meta: any;
|
|
163
163
|
}>;
|
|
@@ -184,11 +184,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
184
184
|
};
|
|
185
185
|
output: {
|
|
186
186
|
id: string;
|
|
187
|
-
email: string | null;
|
|
188
187
|
name: string | null;
|
|
189
|
-
|
|
188
|
+
email: string | null;
|
|
190
189
|
createdAt: Date;
|
|
191
190
|
updatedAt: Date | null;
|
|
191
|
+
status: string;
|
|
192
192
|
};
|
|
193
193
|
meta: any;
|
|
194
194
|
}>;
|
|
@@ -198,11 +198,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
198
198
|
};
|
|
199
199
|
output: {
|
|
200
200
|
id: string;
|
|
201
|
-
email: string | null;
|
|
202
201
|
name: string | null;
|
|
203
|
-
|
|
202
|
+
email: string | null;
|
|
204
203
|
createdAt: Date;
|
|
205
204
|
updatedAt: Date | null;
|
|
205
|
+
status: string;
|
|
206
206
|
};
|
|
207
207
|
meta: any;
|
|
208
208
|
}>;
|
|
@@ -212,11 +212,11 @@ export declare function createAuthTRPC({ router, publicProcedure, privateProcedu
|
|
|
212
212
|
};
|
|
213
213
|
output: {
|
|
214
214
|
id: string;
|
|
215
|
-
email: string | null;
|
|
216
215
|
name: string | null;
|
|
217
|
-
|
|
216
|
+
email: string | null;
|
|
218
217
|
createdAt: Date;
|
|
219
218
|
updatedAt: Date | null;
|
|
219
|
+
status: string;
|
|
220
220
|
};
|
|
221
221
|
meta: any;
|
|
222
222
|
}>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { TRPC_ERROR_CODE_KEY } from "@trpc/server";
|
|
2
|
-
import type { ServerResult, ServerResultAsync } from "./base.dto";
|
|
3
|
-
import type { ServerErrorLayer } from "./base.types";
|
|
4
2
|
import { ServerError } from "../../utils/errors";
|
|
5
3
|
import { logger } from "../../utils/logger";
|
|
4
|
+
import type { ServerResult, ServerResultAsync } from "./base.dto";
|
|
5
|
+
import type { ServerErrorLayer } from "./base.types";
|
|
6
6
|
export declare abstract class Base {
|
|
7
7
|
layer: ServerErrorLayer;
|
|
8
8
|
logger: ReturnType<typeof logger.child>;
|
|
@@ -15,4 +15,5 @@ export declare abstract class Base {
|
|
|
15
15
|
handleUnknownError(error: unknown): ServerError;
|
|
16
16
|
throwable<T>(fn: () => ServerResult<T>): ServerResult<T>;
|
|
17
17
|
throwableAsync<T>(fn: () => ServerResultAsync<T>): ServerResultAsync<T>;
|
|
18
|
+
throwablePromise<T>(fn: () => Promise<T>, errorHandler?: (error: unknown) => ServerError): ServerResultAsync<T>;
|
|
18
19
|
}
|
|
@@ -43,11 +43,20 @@ class Base {
|
|
|
43
43
|
}
|
|
44
44
|
async throwableAsync(fn) {
|
|
45
45
|
try {
|
|
46
|
-
return fn();
|
|
46
|
+
return await fn();
|
|
47
47
|
}
|
|
48
48
|
catch (error) {
|
|
49
49
|
return (0, neverthrow_1.err)(this.handleUnknownError(error));
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
async throwablePromise(fn, errorHandler) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await fn();
|
|
55
|
+
return (0, neverthrow_1.ok)(result);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return (0, neverthrow_1.err)(errorHandler ? errorHandler(error) : this.handleUnknownError(error));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
exports.Base = Base;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Session, User } from "../auth/auth.lib";
|
|
2
|
+
export type UserActor = {
|
|
3
|
+
userId: string;
|
|
4
|
+
userRole: string;
|
|
5
|
+
organizationId: string | null;
|
|
6
|
+
organizationRole: string | null;
|
|
7
|
+
teamId: string | null;
|
|
8
|
+
teamRole: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type OrganizationActor = {
|
|
11
|
+
userId: string;
|
|
12
|
+
userRole: string;
|
|
13
|
+
organizationId: string;
|
|
14
|
+
organizationRole: string;
|
|
15
|
+
teamId: string | null;
|
|
16
|
+
teamRole: string | null;
|
|
17
|
+
};
|
|
18
|
+
export type TeamActor = {
|
|
19
|
+
userId: string;
|
|
20
|
+
userRole: string;
|
|
21
|
+
organizationId: string;
|
|
22
|
+
organizationRole: string;
|
|
23
|
+
teamId: string;
|
|
24
|
+
teamRole: string;
|
|
25
|
+
};
|
|
26
|
+
export type AuthenticatedActor = UserActor | OrganizationActor | TeamActor;
|
|
27
|
+
/** @deprecated Prefer `AuthenticatedActor` — kept for grants and legacy call sites */
|
|
28
|
+
export type ServiceActor = AuthenticatedActor;
|
|
29
|
+
export type Actor = {
|
|
30
|
+
user: UserActor;
|
|
31
|
+
organization: OrganizationActor;
|
|
32
|
+
team: TeamActor;
|
|
33
|
+
authenticated: AuthenticatedActor;
|
|
34
|
+
};
|
|
35
|
+
export type ActorScope = "user" | "organization" | "team";
|
|
36
|
+
export type RequiredServiceActor<Scope extends ActorScope> = Actor[Scope];
|
|
37
|
+
/** Claims shape used by tests and factories */
|
|
38
|
+
export type ServiceActorClaims = {
|
|
39
|
+
userId: string;
|
|
40
|
+
userRole: string;
|
|
41
|
+
organizationId?: string | null;
|
|
42
|
+
organizationRole?: string | null;
|
|
43
|
+
teamId?: string | null;
|
|
44
|
+
teamRole?: string | null;
|
|
45
|
+
};
|
|
46
|
+
/** @deprecated Prefer `OrganizationActor` */
|
|
47
|
+
export type ServiceOrganizationActor = OrganizationActor;
|
|
48
|
+
/** @deprecated Prefer `TeamActor` */
|
|
49
|
+
export type ServiceTeamActor = TeamActor;
|
|
50
|
+
export declare function createActorFromContext(context: {
|
|
51
|
+
user: User;
|
|
52
|
+
session: Session;
|
|
53
|
+
}, scope: "team"): TeamActor;
|
|
54
|
+
export declare function createActorFromContext(context: {
|
|
55
|
+
user: User;
|
|
56
|
+
session: Session;
|
|
57
|
+
}, scope: "organization"): OrganizationActor;
|
|
58
|
+
export declare function createActorFromContext(context: {
|
|
59
|
+
user: User;
|
|
60
|
+
session: Session;
|
|
61
|
+
}, scope: "user"): UserActor;
|
|
62
|
+
export declare function validateActor(actor: AuthenticatedActor, scope: ActorScope): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Builds a flat actor for tests / grants without session. Validates that team scope implies organization.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createServiceActor(claims: ServiceActorClaims): AuthenticatedActor;
|
|
67
|
+
export declare function getServiceActorScope(actor: AuthenticatedActor): ActorScope;
|
|
68
|
+
export declare function hasServiceActorScope(actor: AuthenticatedActor, scope: ActorScope): boolean;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createActorFromContext = createActorFromContext;
|
|
4
|
+
exports.validateActor = validateActor;
|
|
5
|
+
exports.createServiceActor = createServiceActor;
|
|
6
|
+
exports.getServiceActorScope = getServiceActorScope;
|
|
7
|
+
exports.hasServiceActorScope = hasServiceActorScope;
|
|
8
|
+
const errors_1 = require("../../utils/errors");
|
|
9
|
+
function createActorFromContext(context, scope) {
|
|
10
|
+
if (!context.user.role) {
|
|
11
|
+
throw new errors_1.ServerError({
|
|
12
|
+
code: "BAD_REQUEST",
|
|
13
|
+
message: "User role not found in context",
|
|
14
|
+
layer: "controller",
|
|
15
|
+
layerName: "ActorValidation",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (scope === "organization") {
|
|
19
|
+
if (!context.session.activeOrganizationId || !context.session.activeOrganizationRole) {
|
|
20
|
+
throw new errors_1.ServerError({
|
|
21
|
+
code: "FORBIDDEN",
|
|
22
|
+
message: "Active organization context required",
|
|
23
|
+
layer: "controller",
|
|
24
|
+
layerName: "ActorValidation",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (scope === "team") {
|
|
29
|
+
if (!context.session.activeOrganizationId || !context.session.activeOrganizationRole) {
|
|
30
|
+
throw new errors_1.ServerError({
|
|
31
|
+
code: "FORBIDDEN",
|
|
32
|
+
message: "Active organization context required for team scope",
|
|
33
|
+
layer: "controller",
|
|
34
|
+
layerName: "ActorValidation",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!context.session.activeTeamId || !context.session.activeTeamRole) {
|
|
38
|
+
throw new errors_1.ServerError({
|
|
39
|
+
code: "FORBIDDEN",
|
|
40
|
+
message: "Active team context required",
|
|
41
|
+
layer: "controller",
|
|
42
|
+
layerName: "ActorValidation",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
userId: context.user.id,
|
|
48
|
+
userRole: context.user.role,
|
|
49
|
+
organizationId: context.session.activeOrganizationId,
|
|
50
|
+
organizationRole: context.session.activeOrganizationRole,
|
|
51
|
+
teamId: context.session.activeTeamId,
|
|
52
|
+
teamRole: context.session.activeTeamRole,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function validateActor(actor, scope) {
|
|
56
|
+
if (!actor.userId || !actor.userRole)
|
|
57
|
+
return false;
|
|
58
|
+
if (scope === "user")
|
|
59
|
+
return true;
|
|
60
|
+
if (scope === "organization") {
|
|
61
|
+
return Boolean(actor.organizationId && actor.organizationRole);
|
|
62
|
+
}
|
|
63
|
+
return Boolean(actor.organizationId && actor.organizationRole && actor.teamId && actor.teamRole);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Builds a flat actor for tests / grants without session. Validates that team scope implies organization.
|
|
67
|
+
*/
|
|
68
|
+
function createServiceActor(claims) {
|
|
69
|
+
const organizationId = claims.organizationId ?? null;
|
|
70
|
+
const organizationRole = claims.organizationRole ?? null;
|
|
71
|
+
const teamId = claims.teamId ?? null;
|
|
72
|
+
const teamRole = claims.teamRole ?? null;
|
|
73
|
+
if ((teamId || teamRole) && (!organizationId || !organizationRole)) {
|
|
74
|
+
throw new Error("organization access before team access");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
userId: claims.userId,
|
|
78
|
+
userRole: claims.userRole,
|
|
79
|
+
organizationId,
|
|
80
|
+
organizationRole,
|
|
81
|
+
teamId,
|
|
82
|
+
teamRole,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getServiceActorScope(actor) {
|
|
86
|
+
if (validateActor(actor, "team"))
|
|
87
|
+
return "team";
|
|
88
|
+
if (validateActor(actor, "organization"))
|
|
89
|
+
return "organization";
|
|
90
|
+
return "user";
|
|
91
|
+
}
|
|
92
|
+
function hasServiceActorScope(actor, scope) {
|
|
93
|
+
if (scope === "team")
|
|
94
|
+
return validateActor(actor, "team");
|
|
95
|
+
if (scope === "organization") {
|
|
96
|
+
return validateActor(actor, "organization") || validateActor(actor, "team");
|
|
97
|
+
}
|
|
98
|
+
return validateActor(actor, "user");
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const base_actor_1 = require("./base.actor");
|
|
4
|
+
describe("base.actor", () => {
|
|
5
|
+
it("creates a user actor when only user claims are present", () => {
|
|
6
|
+
const actor = (0, base_actor_1.createServiceActor)({
|
|
7
|
+
userId: "user-1",
|
|
8
|
+
userRole: "member",
|
|
9
|
+
});
|
|
10
|
+
expect(actor).toEqual({
|
|
11
|
+
userId: "user-1",
|
|
12
|
+
userRole: "member",
|
|
13
|
+
organizationId: null,
|
|
14
|
+
organizationRole: null,
|
|
15
|
+
teamId: null,
|
|
16
|
+
teamRole: null,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it("derives the highest available scope", () => {
|
|
20
|
+
expect((0, base_actor_1.getServiceActorScope)({
|
|
21
|
+
userId: "user-1",
|
|
22
|
+
userRole: "member",
|
|
23
|
+
organizationId: "org-1",
|
|
24
|
+
organizationRole: "owner",
|
|
25
|
+
teamId: null,
|
|
26
|
+
teamRole: null,
|
|
27
|
+
})).toBe("organization");
|
|
28
|
+
expect((0, base_actor_1.getServiceActorScope)({
|
|
29
|
+
userId: "user-1",
|
|
30
|
+
userRole: "member",
|
|
31
|
+
organizationId: "org-1",
|
|
32
|
+
organizationRole: "owner",
|
|
33
|
+
teamId: "team-1",
|
|
34
|
+
teamRole: "manager",
|
|
35
|
+
})).toBe("team");
|
|
36
|
+
});
|
|
37
|
+
it("validates hierarchy before building a team actor", () => {
|
|
38
|
+
expect(() => (0, base_actor_1.createServiceActor)({
|
|
39
|
+
userId: "user-1",
|
|
40
|
+
userRole: "member",
|
|
41
|
+
teamId: "team-1",
|
|
42
|
+
teamRole: "manager",
|
|
43
|
+
})).toThrow("organization access before team access");
|
|
44
|
+
});
|
|
45
|
+
it("checks required scope against broader actors", () => {
|
|
46
|
+
const actor = (0, base_actor_1.createServiceActor)({
|
|
47
|
+
userId: "user-1",
|
|
48
|
+
userRole: "member",
|
|
49
|
+
organizationId: "org-1",
|
|
50
|
+
organizationRole: "owner",
|
|
51
|
+
teamId: "team-1",
|
|
52
|
+
teamRole: "manager",
|
|
53
|
+
});
|
|
54
|
+
expect((0, base_actor_1.hasServiceActorScope)(actor, "user")).toBe(true);
|
|
55
|
+
expect((0, base_actor_1.hasServiceActorScope)(actor, "organization")).toBe(true);
|
|
56
|
+
expect((0, base_actor_1.hasServiceActorScope)(actor, "team")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceActor } from "./base.actor";
|
|
2
2
|
import type { ServerResultAsync } from "./base.dto";
|
|
3
3
|
type Level = "user" | "team" | "organization";
|
|
4
4
|
type Access = "all" | "own";
|
|
@@ -19,10 +19,6 @@ export type NestedGrants = Record<string, Partial<Record<Level, Record<string, R
|
|
|
19
19
|
export type ResourceGrant = Omit<Grant, "resource">;
|
|
20
20
|
export type ResourceActionGrant = Omit<ResourceGrant, "action">;
|
|
21
21
|
export declare function flattenNestedGrants(nestedGrants: NestedGrants): Grant[];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
user: User;
|
|
25
|
-
}
|
|
26
|
-
export declare function checkPermissionSync<T extends Entity>(ctx: PermissionContext, grants: ResourceActionGrant[], entities?: T | T[]): boolean;
|
|
27
|
-
export declare function checkPermissionAsync<T extends Entity>(ctx: PermissionContext, grants: ResourceActionGrant[], getEntities: () => ServerResultAsync<T | T[] | undefined>): ServerResultAsync<boolean>;
|
|
22
|
+
export declare function checkPermissionSync<T extends Entity>(actor: ServiceActor, grants: ResourceActionGrant[], entities?: T | T[]): boolean;
|
|
23
|
+
export declare function checkPermissionAsync<T extends Entity>(actor: ServiceActor, grants: ResourceActionGrant[], getEntities: () => ServerResultAsync<T | T[] | undefined>): ServerResultAsync<boolean>;
|
|
28
24
|
export {};
|
|
@@ -92,26 +92,38 @@ function checkOwnAccess(grants, roles, contextValues, entities) {
|
|
|
92
92
|
}
|
|
93
93
|
return false;
|
|
94
94
|
}
|
|
95
|
-
function checkPermissionSync(
|
|
95
|
+
function checkPermissionSync(actor, grants, entities) {
|
|
96
96
|
if (!grants || grants.length === 0)
|
|
97
97
|
return false;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const roles = {
|
|
99
|
+
userRole: actor.userRole,
|
|
100
|
+
teamRole: actor.teamRole,
|
|
101
|
+
organizationRole: actor.organizationRole,
|
|
102
|
+
};
|
|
103
|
+
const contextValues = {
|
|
104
|
+
userId: actor.userId,
|
|
105
|
+
teamId: actor.teamId,
|
|
106
|
+
organizationId: actor.organizationId,
|
|
107
|
+
};
|
|
102
108
|
// Pass 1: Check for "all" access first (no ownership check needed)
|
|
103
109
|
if (hasAllAccess(grants, roles))
|
|
104
110
|
return true;
|
|
105
111
|
// Pass 2: Check "own" access with ownership validation
|
|
106
112
|
return checkOwnAccess(grants, roles, contextValues, entities);
|
|
107
113
|
}
|
|
108
|
-
async function checkPermissionAsync(
|
|
114
|
+
async function checkPermissionAsync(actor, grants, getEntities) {
|
|
109
115
|
if (!grants || grants.length === 0)
|
|
110
116
|
return (0, neverthrow_1.ok)(false);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const roles = {
|
|
118
|
+
userRole: actor.userRole,
|
|
119
|
+
teamRole: actor.teamRole,
|
|
120
|
+
organizationRole: actor.organizationRole,
|
|
121
|
+
};
|
|
122
|
+
const contextValues = {
|
|
123
|
+
userId: actor.userId,
|
|
124
|
+
teamId: actor.teamId,
|
|
125
|
+
organizationId: actor.organizationId,
|
|
126
|
+
};
|
|
115
127
|
// Pass 1: Check for "all" access first (no entity fetch needed)
|
|
116
128
|
if (hasAllAccess(grants, roles))
|
|
117
129
|
return (0, neverthrow_1.ok)(true);
|
|
@@ -1,53 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const neverthrow_1 = require("neverthrow");
|
|
4
|
-
const base_grants_1 = require("./base.grants");
|
|
5
4
|
const errors_1 = require("../../utils/errors");
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}
|
|
5
|
+
const base_actor_1 = require("./base.actor");
|
|
6
|
+
const base_grants_1 = require("./base.grants");
|
|
46
7
|
function createMockContext(userOverrides = {}, sessionOverrides = {}) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
8
|
+
const organizationId = sessionOverrides.activeOrganizationId ?? (sessionOverrides.activeTeamId ? "org-123" : null);
|
|
9
|
+
const organizationRole = sessionOverrides.activeOrganizationRole ?? (sessionOverrides.activeTeamRole ? "member" : null);
|
|
10
|
+
const actor = (0, base_actor_1.createServiceActor)({
|
|
11
|
+
userId: userOverrides.id ?? "user-123",
|
|
12
|
+
userRole: userOverrides.role ?? "member",
|
|
13
|
+
organizationId,
|
|
14
|
+
organizationRole,
|
|
15
|
+
teamId: sessionOverrides.activeTeamId ?? null,
|
|
16
|
+
teamRole: sessionOverrides.activeTeamRole ?? null,
|
|
17
|
+
});
|
|
18
|
+
if (!actor) {
|
|
19
|
+
throw new Error("Expected actor");
|
|
20
|
+
}
|
|
21
|
+
return actor;
|
|
51
22
|
}
|
|
52
23
|
function createMockEntity(overrides = {}) {
|
|
53
24
|
return {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
2
|
+
import type { TRPC_ERROR_CODE_KEY } from "@trpc/server";
|
|
3
|
+
import type { ServerError } from "../../utils/errors";
|
|
4
|
+
import type { logger } from "../../utils/logger";
|
|
5
|
+
import type { Base } from "./base.abstract";
|
|
6
|
+
import { type Actor, type ActorScope, type AuthenticatedActor } from "./base.actor";
|
|
7
|
+
import type { ServerResult, ServerResultAsync } from "./base.dto";
|
|
8
|
+
import type { Entity, ResourceActionGrant } from "./base.grants";
|
|
9
|
+
type ServiceLogger = ReturnType<typeof logger.child>;
|
|
10
|
+
type RepositoryMap = Record<string, Base>;
|
|
11
|
+
type ServiceMap = Record<string, Base>;
|
|
12
|
+
export type ServiceProcedureContext = {
|
|
13
|
+
actor?: AuthenticatedActor | null;
|
|
14
|
+
} & Record<string, unknown>;
|
|
15
|
+
export type ServiceProcedureState = Record<string, unknown>;
|
|
16
|
+
export type ServiceProcedureStoredValue<T> = [T] extends [undefined] ? undefined : Awaited<T>;
|
|
17
|
+
export type ServiceProcedureResultLike<T> = T | ServerResult<T> | Promise<T | ServerResult<T>>;
|
|
18
|
+
export type ServiceProcedureContextFilterScope = ActorScope;
|
|
19
|
+
export type ServiceProcedureContextFilteredInput<TInput> = Extract<NonNullable<TInput>, QueryInput>;
|
|
20
|
+
type ServiceProcedureAuthContext<Scope extends ActorScope> = {
|
|
21
|
+
actor: Actor[Scope];
|
|
22
|
+
};
|
|
23
|
+
type ServiceProcedureRequiredScopeFromFilter<TInclude extends readonly ServiceProcedureContextFilterScope[] | undefined> = TInclude extends readonly ServiceProcedureContextFilterScope[] ? "team" extends TInclude[number] ? "team" : "organization" extends TInclude[number] ? "organization" : "user" : "user";
|
|
24
|
+
export type ServiceProcedure<TInput, TCtx extends ServiceProcedureContext, TOutput> = (input: TInput, ctx: TCtx) => ServerResultAsync<TOutput>;
|
|
25
|
+
export type ServiceProcedureArgs<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState> = {
|
|
26
|
+
input: TInput;
|
|
27
|
+
ctx: TCtx;
|
|
28
|
+
state: State;
|
|
29
|
+
repository: Repositories;
|
|
30
|
+
service: Services;
|
|
31
|
+
logger: ServiceLogger;
|
|
32
|
+
};
|
|
33
|
+
export type ServiceProcedureStep<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState, TOutput = undefined> = (args: ServiceProcedureArgs<TInput, TCtx, Repositories, Services, State>) => ServiceProcedureResultLike<ServiceProcedureStoredValue<TOutput>>;
|
|
34
|
+
export type ServiceProcedureInputMapper<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState, TNextInput> = (args: ServiceProcedureArgs<TInput, TCtx, Repositories, Services, State>) => ServiceProcedureResultLike<ServiceProcedureStoredValue<TNextInput>>;
|
|
35
|
+
export type ServiceProcedureHandler<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState, TOutput> = (args: ServiceProcedureArgs<TInput, TCtx, Repositories, Services, State>) => ServiceProcedureResultLike<TOutput>;
|
|
36
|
+
export type ServiceProcedureEntityResolver<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState, TEntities extends Entity | Entity[] | undefined> = TEntities | ((args: ServiceProcedureArgs<TInput, TCtx, Repositories, Services, State>) => ServiceProcedureResultLike<TEntities>);
|
|
37
|
+
type ServiceProcedureAccessBaseConfig = {
|
|
38
|
+
action: string;
|
|
39
|
+
grants?: ResourceActionGrant[];
|
|
40
|
+
};
|
|
41
|
+
export type ServiceProcedureEntityStepName<State extends ServiceProcedureState> = Extract<{
|
|
42
|
+
[Key in keyof State]: State[Key] extends Entity | Entity[] | undefined ? Key : never;
|
|
43
|
+
}[keyof State], string>;
|
|
44
|
+
export type ServiceProcedureAccessEntitiesConfig<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState, TEntities extends Entity | Entity[] | undefined = undefined> = ServiceProcedureAccessBaseConfig & {
|
|
45
|
+
entities?: ServiceProcedureEntityResolver<TInput, TCtx, Repositories, Services, State, TEntities>;
|
|
46
|
+
entityStep?: never;
|
|
47
|
+
};
|
|
48
|
+
export type ServiceProcedureAccessStateConfig<State extends ServiceProcedureState, StepName extends ServiceProcedureEntityStepName<State>> = ServiceProcedureAccessBaseConfig & {
|
|
49
|
+
entityStep: StepName;
|
|
50
|
+
entities?: never;
|
|
51
|
+
};
|
|
52
|
+
export type ServiceProcedureAccessConfig<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState, TEntities extends Entity | Entity[] | undefined = undefined> = ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State, TEntities> | ServiceProcedureAccessStateConfig<State, ServiceProcedureEntityStepName<State>>;
|
|
53
|
+
export interface ServiceProcedureBuilder<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState = Record<string, never>> {
|
|
54
|
+
use<StepName extends string, TOutput = void>(stepName: StepName, step: ServiceProcedureStep<TInput, TCtx, Repositories, Services, State, TOutput>): ServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TOutput>>>;
|
|
55
|
+
mapInput<StepName extends string, TNextInput>(stepName: StepName, step: ServiceProcedureInputMapper<TInput, TCtx, Repositories, Services, State, TNextInput>): ServiceProcedureBuilder<ServiceProcedureStoredValue<TNextInput>, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TNextInput>>>;
|
|
56
|
+
addContextFilter<TInclude extends readonly ServiceProcedureContextFilterScope[] | undefined = undefined>(include?: TInclude): ServiceProcedureBuilder<ServiceProcedureContextFilteredInput<TInput>, TCtx & ServiceProcedureAuthContext<ServiceProcedureRequiredScopeFromFilter<TInclude>>, Repositories, Services, State & {
|
|
57
|
+
contextFilter: ServiceProcedureContextFilteredInput<TInput>;
|
|
58
|
+
}>;
|
|
59
|
+
requireAuth<Scope extends ActorScope = "user">(scope?: Scope): ServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<Scope>, Repositories, Services, State>;
|
|
60
|
+
handle<TOutput>(handler: ServiceProcedureHandler<TInput, TCtx, Repositories, Services, State, TOutput>): ServiceProcedure<TInput, TCtx, TOutput>;
|
|
61
|
+
}
|
|
62
|
+
export interface PermissionServiceProcedureBuilder<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState = Record<string, never>> extends ServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State> {
|
|
63
|
+
use<StepName extends string, TOutput = void>(stepName: StepName, step: ServiceProcedureStep<TInput, TCtx, Repositories, Services, State, TOutput>): PermissionServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TOutput>>>;
|
|
64
|
+
mapInput<StepName extends string, TNextInput>(stepName: StepName, step: ServiceProcedureInputMapper<TInput, TCtx, Repositories, Services, State, TNextInput>): PermissionServiceProcedureBuilder<ServiceProcedureStoredValue<TNextInput>, TCtx, Repositories, Services, State & Record<StepName, ServiceProcedureStoredValue<TNextInput>>>;
|
|
65
|
+
addContextFilter<TInclude extends readonly ServiceProcedureContextFilterScope[] | undefined = undefined>(include?: TInclude): PermissionServiceProcedureBuilder<ServiceProcedureContextFilteredInput<TInput>, TCtx & ServiceProcedureAuthContext<ServiceProcedureRequiredScopeFromFilter<TInclude>>, Repositories, Services, State & {
|
|
66
|
+
contextFilter: ServiceProcedureContextFilteredInput<TInput>;
|
|
67
|
+
}>;
|
|
68
|
+
requireAuth<Scope extends ActorScope = "user">(scope?: Scope): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<Scope>, Repositories, Services, State>;
|
|
69
|
+
access(config: ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State>): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<"user">, Repositories, Services, State>;
|
|
70
|
+
access<TEntities extends Entity | Entity[] | undefined>(config: ServiceProcedureAccessEntitiesConfig<TInput, TCtx, Repositories, Services, State, TEntities>): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<"user">, Repositories, Services, State & {
|
|
71
|
+
access: TEntities;
|
|
72
|
+
}>;
|
|
73
|
+
access<StepName extends ServiceProcedureEntityStepName<State>>(config: ServiceProcedureAccessStateConfig<State, StepName>): PermissionServiceProcedureBuilder<TInput, TCtx & ServiceProcedureAuthContext<"user">, Repositories, Services, State & {
|
|
74
|
+
access: State[StepName];
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
type BaseServiceProcedureHost<Repositories extends RepositoryMap, Services extends ServiceMap> = {
|
|
78
|
+
repository: Repositories;
|
|
79
|
+
service: Services;
|
|
80
|
+
logger: ServiceLogger;
|
|
81
|
+
addContextFilter(actor: AuthenticatedActor, include?: {
|
|
82
|
+
user?: boolean;
|
|
83
|
+
organization?: boolean;
|
|
84
|
+
team?: boolean;
|
|
85
|
+
}, query?: QueryInput): QueryInput;
|
|
86
|
+
error(code: TRPC_ERROR_CODE_KEY, message?: string, options?: {
|
|
87
|
+
cause?: unknown;
|
|
88
|
+
clientMessage?: string;
|
|
89
|
+
log?: boolean;
|
|
90
|
+
}): ServerResult<never>;
|
|
91
|
+
throwableAsync<T>(fn: () => ServerResultAsync<T>): ServerResultAsync<T>;
|
|
92
|
+
handleUnknownError(error: unknown): ServerError;
|
|
93
|
+
};
|
|
94
|
+
type PermissionServiceProcedureHost<Repositories extends RepositoryMap, Services extends ServiceMap> = BaseServiceProcedureHost<Repositories, Services> & {
|
|
95
|
+
checkPermission<T extends Entity>(actor: AuthenticatedActor, action: string, entities?: T | T[], grants?: ResourceActionGrant[]): boolean;
|
|
96
|
+
checkPermissionAsync<T extends Entity>(actor: AuthenticatedActor, action: string, getEntities: () => ServerResultAsync<T | T[] | undefined>, grants?: ResourceActionGrant[]): ServerResultAsync<boolean>;
|
|
97
|
+
};
|
|
98
|
+
type ProcedureRuntimeStep<Repositories extends RepositoryMap, Services extends ServiceMap> = {
|
|
99
|
+
stage: "use" | "input" | "auth" | "access";
|
|
100
|
+
stepName: string;
|
|
101
|
+
run: (args: ServiceProcedureArgs<unknown, ServiceProcedureContext, Repositories, Services, ServiceProcedureState>) => Promise<ServerResult<unknown>>;
|
|
102
|
+
};
|
|
103
|
+
type ProcedureBuilderConfig<Repositories extends RepositoryMap, Services extends ServiceMap> = {
|
|
104
|
+
name: string;
|
|
105
|
+
steps: ProcedureRuntimeStep<Repositories, Services>[];
|
|
106
|
+
};
|
|
107
|
+
export declare function createServiceProcedureBuilder<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState = Record<string, never>>(host: BaseServiceProcedureHost<Repositories, Services>, config: ProcedureBuilderConfig<Repositories, Services>): ServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State>;
|
|
108
|
+
export declare function createPermissionServiceProcedureBuilder<TInput, TCtx extends ServiceProcedureContext, Repositories extends RepositoryMap, Services extends ServiceMap, State extends ServiceProcedureState = Record<string, never>>(host: PermissionServiceProcedureHost<Repositories, Services>, config: ProcedureBuilderConfig<Repositories, Services>): PermissionServiceProcedureBuilder<TInput, TCtx, Repositories, Services, State>;
|
|
109
|
+
export {};
|