@hiliosai/sdk 0.1.12 → 0.1.15
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/index.d.ts +904 -0
- package/dist/index.js +1809 -0
- package/package.json +10 -2
- package/src/configs/constants.ts +0 -135
- package/src/configs/index.ts +0 -2
- package/src/configs/moleculer/bulkhead.ts +0 -8
- package/src/configs/moleculer/channels.ts +0 -102
- package/src/configs/moleculer/circuit-breaker.ts +0 -17
- package/src/configs/moleculer/index.ts +0 -98
- package/src/configs/moleculer/logger.ts +0 -17
- package/src/configs/moleculer/metrics.ts +0 -20
- package/src/configs/moleculer/registry.ts +0 -7
- package/src/configs/moleculer/retry-policy.ts +0 -17
- package/src/configs/moleculer/tracing.ts +0 -6
- package/src/configs/moleculer/tracking.ts +0 -6
- package/src/configs/permissions.ts +0 -109
- package/src/datasources/base.datasource.ts +0 -111
- package/src/datasources/extensions/index.ts +0 -11
- package/src/datasources/extensions/retry.extension.ts +0 -91
- package/src/datasources/extensions/soft-delete.extension.ts +0 -114
- package/src/datasources/extensions/tenant.extension.ts +0 -105
- package/src/datasources/index.ts +0 -3
- package/src/datasources/prisma.datasource.ts +0 -317
- package/src/env.ts +0 -12
- package/src/errors/auth.error.ts +0 -33
- package/src/errors/index.ts +0 -2
- package/src/errors/permission.error.ts +0 -17
- package/src/index.ts +0 -10
- package/src/middlewares/context-helpers.middleware.ts +0 -162
- package/src/middlewares/datasource.middleware.ts +0 -73
- package/src/middlewares/health.middleware.ts +0 -134
- package/src/middlewares/index.ts +0 -5
- package/src/middlewares/memoize.middleware.ts +0 -33
- package/src/middlewares/permissions.middleware.ts +0 -162
- package/src/mixins/datasource.mixin.ts +0 -111
- package/src/mixins/index.ts +0 -1
- package/src/service/define-integration.ts +0 -404
- package/src/service/define-service.ts +0 -58
- package/src/types/channels.ts +0 -60
- package/src/types/context.ts +0 -64
- package/src/types/datasource.ts +0 -23
- package/src/types/index.ts +0 -9
- package/src/types/integration.ts +0 -28
- package/src/types/message.ts +0 -128
- package/src/types/platform.ts +0 -39
- package/src/types/service.ts +0 -209
- package/src/types/tenant.ts +0 -4
- package/src/types/user.ts +0 -16
- package/src/utils/context-cache.ts +0 -70
- package/src/utils/index.ts +0 -8
- package/src/utils/permission-calculator.ts +0 -62
- package/tsconfig.json +0 -13
- package/tsup.config.ts +0 -5
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export class PermissionError extends Error {
|
|
2
|
-
public readonly code = 'PERMISSION_DENIED';
|
|
3
|
-
public readonly statusCode = 403;
|
|
4
|
-
public readonly data: Record<string, unknown> | undefined;
|
|
5
|
-
|
|
6
|
-
constructor(message: string, data?: Record<string, unknown>) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = 'PermissionError';
|
|
9
|
-
this.data = data;
|
|
10
|
-
|
|
11
|
-
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
13
|
-
if (Error.captureStackTrace) {
|
|
14
|
-
Error.captureStackTrace(this, PermissionError);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export * from './middlewares';
|
|
2
|
-
export * from './service/define-service';
|
|
3
|
-
export * from './service/define-integration';
|
|
4
|
-
export * from './types';
|
|
5
|
-
export * from './configs';
|
|
6
|
-
export * from './env';
|
|
7
|
-
export * from './datasources';
|
|
8
|
-
export * from './mixins';
|
|
9
|
-
export * from './utils';
|
|
10
|
-
export {default as env} from './env';
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type {AppContext, Tenant, User, UserRole} from '../types';
|
|
2
|
-
import {AuthenticationError, TenantError} from '../errors';
|
|
3
|
-
import {ContextCache} from '../utils/context-cache';
|
|
4
|
-
import {PermissionCalculator} from '../utils/permission-calculator';
|
|
5
|
-
|
|
6
|
-
export const ContextHelpersMiddleware = {
|
|
7
|
-
// Add helper functions to context before action handlers
|
|
8
|
-
localAction(handler: (...args: unknown[]) => unknown) {
|
|
9
|
-
return function ContextHelpersWrapper(this: unknown, ctx: AppContext) {
|
|
10
|
-
const cache = ContextCache.getInstance();
|
|
11
|
-
|
|
12
|
-
// Memoized permission checking with caching
|
|
13
|
-
const memoizedPermissions = new Map<string, boolean>();
|
|
14
|
-
|
|
15
|
-
ctx.hasPermission = function (permission: string): boolean {
|
|
16
|
-
// Check in-request memoization first
|
|
17
|
-
if (memoizedPermissions.has(permission)) {
|
|
18
|
-
const cachedResult = memoizedPermissions.get(permission);
|
|
19
|
-
return cachedResult === true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const user = ctx.meta.user;
|
|
23
|
-
if (!user) {
|
|
24
|
-
memoizedPermissions.set(permission, false);
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Use optimized permission calculator
|
|
29
|
-
const result = PermissionCalculator.hasPermission(user, permission);
|
|
30
|
-
memoizedPermissions.set(permission, result);
|
|
31
|
-
return result;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Cached user permissions getter
|
|
35
|
-
ctx.getUserPermissions = async function (): Promise<string[]> {
|
|
36
|
-
const user = ctx.meta.user;
|
|
37
|
-
if (!user) return [];
|
|
38
|
-
|
|
39
|
-
const cacheKey = `permissions:${user.id}:${JSON.stringify(user.roles)}`;
|
|
40
|
-
return cache.get(cacheKey, () => {
|
|
41
|
-
return PermissionCalculator.calculateUserPermissions(user);
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
ctx.hasRole = function (role: keyof typeof UserRole): boolean {
|
|
46
|
-
const user = ctx.ensureUser();
|
|
47
|
-
return Array.isArray(user.roles) && user.roles.includes(role);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
ctx.isTenantMember = function (): boolean {
|
|
51
|
-
const user = ctx.ensureUser();
|
|
52
|
-
return !!(
|
|
53
|
-
user.tenantId &&
|
|
54
|
-
ctx.meta.tenantId &&
|
|
55
|
-
user.tenantId === ctx.meta.tenantId
|
|
56
|
-
);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
ctx.isTenantOwner = function (): boolean {
|
|
60
|
-
return ctx.isTenantMember() && ctx.hasRole('OWNER');
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
ctx.ensureUser = (): User => {
|
|
64
|
-
if (!ctx.meta.user) {
|
|
65
|
-
ctx.broker.logger.error('Authentication required', {
|
|
66
|
-
action: ctx.action?.name,
|
|
67
|
-
requestId: ctx.meta.requestId,
|
|
68
|
-
userAgent: ctx.meta.userAgent,
|
|
69
|
-
ip: ctx.meta.clientIP,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
throw new AuthenticationError('Authentication required', {
|
|
73
|
-
code: 'AUTH_REQUIRED',
|
|
74
|
-
statusCode: 401,
|
|
75
|
-
requestId: ctx.meta.requestId,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return ctx.meta.user;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
ctx.ensureTenant = (): Tenant => {
|
|
82
|
-
if (!ctx.meta.tenantId) {
|
|
83
|
-
ctx.broker.logger.error('Tenant required', {
|
|
84
|
-
action: ctx.action?.name,
|
|
85
|
-
userId: ctx.meta.user?.id,
|
|
86
|
-
requestId: ctx.meta.requestId,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
throw new TenantError('Tenant required', {
|
|
90
|
-
code: 'TENANT_REQUIRED',
|
|
91
|
-
statusCode: 401,
|
|
92
|
-
tenantId: ctx.meta.tenantId,
|
|
93
|
-
requestId: ctx.meta.requestId,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Return a proper Tenant object with the ID
|
|
98
|
-
return {
|
|
99
|
-
id: ctx.meta.tenantId,
|
|
100
|
-
name: ctx.meta.tenantName ?? '',
|
|
101
|
-
} as Tenant;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Enhanced audit logging function
|
|
105
|
-
ctx.auditLog = function (
|
|
106
|
-
action: string,
|
|
107
|
-
resource?: unknown,
|
|
108
|
-
metadata?: Record<string, unknown>
|
|
109
|
-
): void {
|
|
110
|
-
ctx.broker.logger.info('Audit log', {
|
|
111
|
-
action,
|
|
112
|
-
resource: resource
|
|
113
|
-
? {
|
|
114
|
-
type: typeof resource,
|
|
115
|
-
id: (resource as Record<string, unknown>).id,
|
|
116
|
-
}
|
|
117
|
-
: undefined,
|
|
118
|
-
userId: ctx.meta.user?.id,
|
|
119
|
-
tenantId: ctx.meta.tenantId,
|
|
120
|
-
requestId: ctx.meta.requestId,
|
|
121
|
-
timestamp: new Date().toISOString(),
|
|
122
|
-
...metadata,
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Enhanced error creation with context
|
|
127
|
-
ctx.createError = function (
|
|
128
|
-
message: string,
|
|
129
|
-
code: string,
|
|
130
|
-
statusCode = 400
|
|
131
|
-
): Error {
|
|
132
|
-
const errorData = {
|
|
133
|
-
code,
|
|
134
|
-
statusCode,
|
|
135
|
-
userId: ctx.meta.user?.id,
|
|
136
|
-
tenantId: ctx.meta.tenantId,
|
|
137
|
-
requestId: ctx.meta.requestId,
|
|
138
|
-
action: ctx.action?.name,
|
|
139
|
-
timestamp: new Date().toISOString(),
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
ctx.broker.logger.warn('Context error created', {
|
|
143
|
-
message,
|
|
144
|
-
...errorData,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
if (code === 'AUTH_REQUIRED') {
|
|
148
|
-
return new AuthenticationError(message, errorData);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (code === 'TENANT_REQUIRED') {
|
|
152
|
-
return new TenantError(message, errorData);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return new Error(message);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Call the original handler
|
|
159
|
-
return handler.call(this, ctx);
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type {Context} from 'moleculer';
|
|
2
|
-
import type {BaseDatasource} from '../datasources';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Datasource constructor registry type
|
|
6
|
-
* All datasources should implement BaseDatasource interface
|
|
7
|
-
*/
|
|
8
|
-
export interface DatasourceConstructorRegistry {
|
|
9
|
-
[key: string]: new () => BaseDatasource | object;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Datasource instance registry type
|
|
14
|
-
*/
|
|
15
|
-
export interface DatasourceInstanceRegistry {
|
|
16
|
-
[key: string]: object;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Datasource context extension
|
|
21
|
-
*/
|
|
22
|
-
export interface DatasourceContext extends Context {
|
|
23
|
-
datasources: DatasourceInstanceRegistry;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Initialize all datasources
|
|
28
|
-
*/
|
|
29
|
-
function initializeDatasources(
|
|
30
|
-
constructorRegistry: DatasourceConstructorRegistry
|
|
31
|
-
): DatasourceInstanceRegistry {
|
|
32
|
-
const initializedDatasources: DatasourceInstanceRegistry = {};
|
|
33
|
-
|
|
34
|
-
for (const [key, DatasourceClass] of Object.entries(constructorRegistry)) {
|
|
35
|
-
initializedDatasources[key] = new DatasourceClass();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return initializedDatasources;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Datasource middleware that injects datasources into the context
|
|
43
|
-
*/
|
|
44
|
-
export function createDatasourceMiddleware(
|
|
45
|
-
datasources: DatasourceConstructorRegistry
|
|
46
|
-
) {
|
|
47
|
-
const initializedDatasources = initializeDatasources(datasources);
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
localAction(handler: (...args: unknown[]) => unknown) {
|
|
51
|
-
return function DatasourceWrapper(this: unknown, ctx: Context) {
|
|
52
|
-
// Inject datasources into context
|
|
53
|
-
(ctx as DatasourceContext).datasources = initializedDatasources;
|
|
54
|
-
return handler.call(this, ctx);
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
remoteAction(handler: (...args: unknown[]) => unknown) {
|
|
59
|
-
return function DatasourceWrapper(this: unknown, ctx: Context) {
|
|
60
|
-
// Inject datasources into context for remote actions too
|
|
61
|
-
(ctx as DatasourceContext).datasources = initializedDatasources;
|
|
62
|
-
return handler.call(this, ctx);
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Type helper for services that use datasources
|
|
70
|
-
*/
|
|
71
|
-
export type ServiceWithDatasources<T extends DatasourceInstanceRegistry> = {
|
|
72
|
-
datasources: T;
|
|
73
|
-
};
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import env from '@ltv/env';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import type {Service} from 'moleculer';
|
|
4
|
-
|
|
5
|
-
const HEALTH_CHECK_PORT = env.int('HEALTH_CHECK_PORT', 3301);
|
|
6
|
-
const HEALTH_CHECK_READINESS_PATH = env.string(
|
|
7
|
-
'HEALTH_CHECK_READINESS_PATH',
|
|
8
|
-
'/readyz'
|
|
9
|
-
);
|
|
10
|
-
const HEALTH_CHECK_LIVENESS_PATH = env.string(
|
|
11
|
-
'HEALTH_CHECK_LIVENESS_PATH',
|
|
12
|
-
'/livez'
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
interface HealthCheckOptions {
|
|
16
|
-
port?: number;
|
|
17
|
-
readiness?: {
|
|
18
|
-
path?: string;
|
|
19
|
-
};
|
|
20
|
-
liveness?: {
|
|
21
|
-
path?: string;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface HealthCheckConfig {
|
|
26
|
-
port: number;
|
|
27
|
-
readiness: {
|
|
28
|
-
path: string;
|
|
29
|
-
};
|
|
30
|
-
liveness: {
|
|
31
|
-
path: string;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Default configuration constants
|
|
36
|
-
export const HEALTH_CHECK_DEFAULTS = {
|
|
37
|
-
PORT: HEALTH_CHECK_PORT,
|
|
38
|
-
READINESS_PATH: HEALTH_CHECK_READINESS_PATH,
|
|
39
|
-
LIVENESS_PATH: HEALTH_CHECK_LIVENESS_PATH,
|
|
40
|
-
} as const;
|
|
41
|
-
|
|
42
|
-
export function CreateHealthCheckMiddleware(opts: HealthCheckOptions = {}) {
|
|
43
|
-
// Merge user options with defaults
|
|
44
|
-
const config: HealthCheckConfig = {
|
|
45
|
-
port: opts.port ?? HEALTH_CHECK_DEFAULTS.PORT,
|
|
46
|
-
readiness: {
|
|
47
|
-
path: opts.readiness?.path ?? HEALTH_CHECK_DEFAULTS.READINESS_PATH,
|
|
48
|
-
},
|
|
49
|
-
liveness: {
|
|
50
|
-
path: opts.liveness?.path ?? HEALTH_CHECK_DEFAULTS.LIVENESS_PATH,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type HealthState = 'down' | 'starting' | 'up' | 'stopping';
|
|
55
|
-
|
|
56
|
-
let state: HealthState = 'down';
|
|
57
|
-
let server: http.Server;
|
|
58
|
-
|
|
59
|
-
function handler(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
60
|
-
// Prevent headers from being sent multiple times
|
|
61
|
-
if (res.headersSent) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (req.url === config.readiness.path || req.url === config.liveness.path) {
|
|
66
|
-
const resHeader = {
|
|
67
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const content = {
|
|
71
|
-
state,
|
|
72
|
-
uptime: process.uptime(),
|
|
73
|
-
timestamp: Date.now(),
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
if (req.url === config.readiness.path) {
|
|
77
|
-
// Readiness if the broker started successfully.
|
|
78
|
-
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes
|
|
79
|
-
res.writeHead(state === 'up' ? 200 : 503, resHeader);
|
|
80
|
-
} else {
|
|
81
|
-
// Liveness if the broker is not stopped.
|
|
82
|
-
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-command
|
|
83
|
-
res.writeHead(state !== 'down' ? 200 : 503, resHeader);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
res.end(JSON.stringify(content, null, 2));
|
|
87
|
-
} else {
|
|
88
|
-
res.writeHead(404, http.STATUS_CODES[404] ?? 'Not Found', {});
|
|
89
|
-
res.end();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
created(broker: Service) {
|
|
95
|
-
state = 'starting';
|
|
96
|
-
|
|
97
|
-
server = http.createServer(handler);
|
|
98
|
-
server.listen(config.port, (err?: Error | undefined) => {
|
|
99
|
-
if (err) {
|
|
100
|
-
return broker.logger.error(
|
|
101
|
-
'Unable to start health-check server',
|
|
102
|
-
err
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
broker.logger.info('');
|
|
107
|
-
broker.logger.info('K8s health-check server listening on');
|
|
108
|
-
broker.logger.info(
|
|
109
|
-
` http://localhost:${config.port}${config.readiness.path}`
|
|
110
|
-
);
|
|
111
|
-
broker.logger.info(
|
|
112
|
-
` http://localhost:${config.port}${config.liveness.path}`
|
|
113
|
-
);
|
|
114
|
-
broker.logger.info('');
|
|
115
|
-
});
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
// After broker started
|
|
119
|
-
started() {
|
|
120
|
-
state = 'up';
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
// Before broker stopping
|
|
124
|
-
stopping() {
|
|
125
|
-
state = 'stopping';
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
// After broker stopped
|
|
129
|
-
stopped() {
|
|
130
|
-
state = 'down';
|
|
131
|
-
server.close();
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
package/src/middlewares/index.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type {ServiceSchema, ServiceSettingSchema} from 'moleculer';
|
|
2
|
-
|
|
3
|
-
export interface MemoizeMixinOptions {
|
|
4
|
-
ttl?: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function MemoizeMixin(
|
|
8
|
-
options?: MemoizeMixinOptions
|
|
9
|
-
): ServiceSchema<ServiceSettingSchema> {
|
|
10
|
-
return {
|
|
11
|
-
name: '',
|
|
12
|
-
methods: {
|
|
13
|
-
async memoize(name: string, params: unknown, fn: () => Promise<unknown>) {
|
|
14
|
-
if (!this.broker.cacher) return fn();
|
|
15
|
-
|
|
16
|
-
const key = this.broker.cacher.defaultKeygen(
|
|
17
|
-
`${this.name}:memoize-${name}`,
|
|
18
|
-
params as object | null,
|
|
19
|
-
{},
|
|
20
|
-
[]
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
let res = await this.broker.cacher.get(key);
|
|
24
|
-
if (res) return res;
|
|
25
|
-
|
|
26
|
-
res = (await fn()) as unknown as object;
|
|
27
|
-
this.broker.cacher.set(key, res, options?.ttl);
|
|
28
|
-
|
|
29
|
-
return res;
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type {ActionSchema, Service} from 'moleculer';
|
|
2
|
-
|
|
3
|
-
import {PERMISSIONS, ROLE_PERMISSIONS} from '../configs';
|
|
4
|
-
import {isDev} from '../env';
|
|
5
|
-
|
|
6
|
-
import {PermissionError} from '../errors';
|
|
7
|
-
import type {AppContext} from '../types';
|
|
8
|
-
|
|
9
|
-
export type Permission =
|
|
10
|
-
| keyof typeof PERMISSIONS
|
|
11
|
-
| string
|
|
12
|
-
| ((ctx: AppContext, action: ActionSchema) => Promise<boolean> | boolean);
|
|
13
|
-
|
|
14
|
-
// Middleware interface for type safety
|
|
15
|
-
export interface ActionWithPermissions extends ActionSchema {
|
|
16
|
-
permissions?: Permission | Permission[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const permissionHandlers = {
|
|
20
|
-
[PERMISSIONS.AUTHENTICATED]: async (ctx: AppContext) => !!ctx.meta.user?.id,
|
|
21
|
-
|
|
22
|
-
[PERMISSIONS.TENANT_OWNER]: async (ctx: AppContext) =>
|
|
23
|
-
ctx.meta.user?.roles.includes(PERMISSIONS.OWNER) ?? false,
|
|
24
|
-
|
|
25
|
-
[PERMISSIONS.TENANT_MEMBER]: async (ctx: AppContext) =>
|
|
26
|
-
!!(
|
|
27
|
-
ctx.meta.user?.tenantId &&
|
|
28
|
-
ctx.meta.tenantId &&
|
|
29
|
-
ctx.meta.user.tenantId === ctx.meta.tenantId
|
|
30
|
-
),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const PermissionsMiddleware = {
|
|
34
|
-
// Wrap local action handlers
|
|
35
|
-
localAction(
|
|
36
|
-
handler: (...args: unknown[]) => unknown,
|
|
37
|
-
action: ActionWithPermissions
|
|
38
|
-
) {
|
|
39
|
-
// If permissions are not defined, return original handler
|
|
40
|
-
if (!action.permissions) {
|
|
41
|
-
return handler;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const permissions = Array.isArray(action.permissions)
|
|
45
|
-
? action.permissions
|
|
46
|
-
: [action.permissions];
|
|
47
|
-
|
|
48
|
-
const permissionNames: string[] = [];
|
|
49
|
-
const permissionFunctions: Array<
|
|
50
|
-
(ctx: AppContext, action: ActionSchema) => Promise<boolean> | boolean
|
|
51
|
-
> = [];
|
|
52
|
-
|
|
53
|
-
// Process each permission
|
|
54
|
-
permissions.forEach((permission) => {
|
|
55
|
-
if (typeof permission === 'function') {
|
|
56
|
-
// Add custom permission function
|
|
57
|
-
permissionFunctions.push(permission);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (typeof permission === 'string') {
|
|
62
|
-
// Check if it's a built-in permission handler
|
|
63
|
-
if (permission in permissionHandlers) {
|
|
64
|
-
const handler =
|
|
65
|
-
permissionHandlers[permission as keyof typeof permissionHandlers];
|
|
66
|
-
permissionFunctions.push(handler);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Otherwise, treat as a permission name to check against user roles
|
|
71
|
-
permissionNames.push(permission);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return async function CheckPermissionsMiddleware(
|
|
77
|
-
this: Service,
|
|
78
|
-
ctx: AppContext
|
|
79
|
-
) {
|
|
80
|
-
let hasAccess = false;
|
|
81
|
-
|
|
82
|
-
// Check if user has OWNER role (super admin within tenant)
|
|
83
|
-
if (ctx.meta.user?.roles.includes(PERMISSIONS.OWNER)) {
|
|
84
|
-
hasAccess = true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// REMOVED: DEVELOPER role bypass - security vulnerability
|
|
88
|
-
// Developers must have explicit permissions like any other role
|
|
89
|
-
|
|
90
|
-
// Check custom permission functions
|
|
91
|
-
if (!hasAccess && permissionFunctions.length > 0) {
|
|
92
|
-
const results = await Promise.allSettled(
|
|
93
|
-
permissionFunctions.map((fn) => fn(ctx, action))
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
hasAccess = results.some(
|
|
97
|
-
(result) => result.status === 'fulfilled' && !!result.value
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Log failed permissions without exposing details
|
|
101
|
-
const failures = results.filter((r) => r.status === 'rejected');
|
|
102
|
-
if (failures.length > 0) {
|
|
103
|
-
ctx.broker.logger.warn(
|
|
104
|
-
`${failures.length} permission functions failed`,
|
|
105
|
-
{
|
|
106
|
-
action: action.name,
|
|
107
|
-
userId: ctx.meta.user?.id,
|
|
108
|
-
}
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check named permissions against user roles
|
|
114
|
-
if (!hasAccess && permissionNames.length > 0) {
|
|
115
|
-
const userRoles = ctx.meta.user?.roles ?? [];
|
|
116
|
-
|
|
117
|
-
// Check if user has any role that includes the required permissions
|
|
118
|
-
hasAccess = userRoles.some((role: string) => {
|
|
119
|
-
const rolePermissions = ROLE_PERMISSIONS[role] ?? [];
|
|
120
|
-
return permissionNames.some((permName) =>
|
|
121
|
-
rolePermissions.includes(permName)
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Throw error if access denied
|
|
127
|
-
if (!hasAccess) {
|
|
128
|
-
const user = ctx.meta.user;
|
|
129
|
-
ctx.broker.logger.warn('Access denied:', {
|
|
130
|
-
action: action.name,
|
|
131
|
-
userId: user?.id,
|
|
132
|
-
userRoles: user?.roles,
|
|
133
|
-
tenantId: ctx.meta.tenantId,
|
|
134
|
-
requiredPermissions: permissions,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Sanitize error details for production
|
|
138
|
-
const errorDetails = isDev
|
|
139
|
-
? {
|
|
140
|
-
action: action.name,
|
|
141
|
-
requiredPermissions: permissions.map((p) =>
|
|
142
|
-
typeof p === 'function' ? '[Function]' : String(p)
|
|
143
|
-
),
|
|
144
|
-
userRoles: user?.roles ?? [],
|
|
145
|
-
userId: user?.id,
|
|
146
|
-
tenantId: ctx.meta.tenantId,
|
|
147
|
-
}
|
|
148
|
-
: {
|
|
149
|
-
action: action.name,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
throw new PermissionError(
|
|
153
|
-
'You do not have permission to perform this action',
|
|
154
|
-
errorDetails
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Call the original handler
|
|
159
|
-
return handler.call(this, ctx);
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
};
|