@uploadista/server 0.0.18-beta.16 → 0.0.18-beta.2
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/README.md +0 -83
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +9 -794
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +9 -794
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
- package/src/core/http-handlers/flow-http-handlers.ts +6 -61
- package/src/core/http-handlers/http-handlers.ts +0 -50
- package/src/core/http-handlers/upload-http-handlers.ts +3 -56
- package/src/core/routes.ts +0 -171
- package/src/core/server.ts +11 -71
- package/src/core/types.ts +0 -150
- package/src/index.ts +0 -2
- package/src/plugins-typing.ts +1 -1
- package/src/service.ts +3 -101
- package/docs/HEALTH_CHECKS.md +0 -256
- package/src/core/health-check-service.ts +0 -367
- package/src/core/http-handlers/dlq-http-handlers.ts +0 -219
- package/src/core/http-handlers/health-http-handlers.ts +0 -150
- package/src/permissions/errors.ts +0 -105
- package/src/permissions/index.ts +0 -9
- package/src/permissions/matcher.ts +0 -139
- package/src/permissions/types.ts +0 -151
- package/src/usage-hooks/index.ts +0 -8
- package/src/usage-hooks/service.ts +0 -162
- package/src/usage-hooks/types.ts +0 -221
- package/tests/core/health-check-service.test.ts +0 -570
- package/tests/core/http-handlers/health-handlers.test.ts +0 -351
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { DeadLetterQueueService } from "@uploadista/core/flow";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import { PERMISSIONS } from "../../permissions/types";
|
|
4
|
-
import { AuthContextService } from "../../service";
|
|
5
|
-
import type {
|
|
6
|
-
DlqCleanupRequest,
|
|
7
|
-
DlqCleanupResponse,
|
|
8
|
-
DlqDeleteRequest,
|
|
9
|
-
DlqDeleteResponse,
|
|
10
|
-
DlqGetRequest,
|
|
11
|
-
DlqGetResponse,
|
|
12
|
-
DlqListRequest,
|
|
13
|
-
DlqListResponse,
|
|
14
|
-
DlqResolveRequest,
|
|
15
|
-
DlqResolveResponse,
|
|
16
|
-
DlqRetryAllRequest,
|
|
17
|
-
DlqRetryAllResponse,
|
|
18
|
-
DlqRetryRequest,
|
|
19
|
-
DlqRetryResponse,
|
|
20
|
-
DlqStatsRequest,
|
|
21
|
-
DlqStatsResponse,
|
|
22
|
-
} from "../routes";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Handle GET /api/dlq - List DLQ items
|
|
26
|
-
*/
|
|
27
|
-
export const handleDlqList = (req: DlqListRequest) =>
|
|
28
|
-
Effect.gen(function* () {
|
|
29
|
-
const authService = yield* AuthContextService;
|
|
30
|
-
|
|
31
|
-
// Check permission for reading DLQ
|
|
32
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_READ);
|
|
33
|
-
|
|
34
|
-
const dlq = yield* DeadLetterQueueService;
|
|
35
|
-
const result = yield* dlq.list(req.options);
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
type: "dlq-list",
|
|
39
|
-
status: 200,
|
|
40
|
-
headers: { "Content-Type": "application/json" },
|
|
41
|
-
body: result,
|
|
42
|
-
} satisfies DlqListResponse;
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Handle GET /api/dlq/:itemId - Get a specific DLQ item
|
|
47
|
-
*/
|
|
48
|
-
export const handleDlqGet = (req: DlqGetRequest) =>
|
|
49
|
-
Effect.gen(function* () {
|
|
50
|
-
const authService = yield* AuthContextService;
|
|
51
|
-
|
|
52
|
-
// Check permission for reading DLQ
|
|
53
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_READ);
|
|
54
|
-
|
|
55
|
-
const dlq = yield* DeadLetterQueueService;
|
|
56
|
-
const item = yield* dlq.get(req.itemId);
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
type: "dlq-get",
|
|
60
|
-
status: 200,
|
|
61
|
-
headers: { "Content-Type": "application/json" },
|
|
62
|
-
body: item,
|
|
63
|
-
} satisfies DlqGetResponse;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Handle POST /api/dlq/:itemId/retry - Retry a specific DLQ item
|
|
68
|
-
*/
|
|
69
|
-
export const handleDlqRetry = (req: DlqRetryRequest) =>
|
|
70
|
-
Effect.gen(function* () {
|
|
71
|
-
const authService = yield* AuthContextService;
|
|
72
|
-
|
|
73
|
-
// Check permission for writing to DLQ
|
|
74
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
75
|
-
|
|
76
|
-
const dlq = yield* DeadLetterQueueService;
|
|
77
|
-
|
|
78
|
-
// Mark item as retrying
|
|
79
|
-
yield* dlq.markRetrying(req.itemId);
|
|
80
|
-
|
|
81
|
-
// TODO: Implement actual retry logic by re-executing the flow
|
|
82
|
-
// This would require access to FlowServer and the original job context
|
|
83
|
-
// For now, we just mark it as retrying and return success
|
|
84
|
-
// The actual retry would be handled by a background scheduler
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
type: "dlq-retry",
|
|
88
|
-
status: 200,
|
|
89
|
-
headers: { "Content-Type": "application/json" },
|
|
90
|
-
body: { success: true },
|
|
91
|
-
} satisfies DlqRetryResponse;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Handle POST /api/dlq/retry-all - Retry all matching DLQ items
|
|
96
|
-
*/
|
|
97
|
-
export const handleDlqRetryAll = (req: DlqRetryAllRequest) =>
|
|
98
|
-
Effect.gen(function* () {
|
|
99
|
-
const authService = yield* AuthContextService;
|
|
100
|
-
|
|
101
|
-
// Check permission for writing to DLQ
|
|
102
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
103
|
-
|
|
104
|
-
const dlq = yield* DeadLetterQueueService;
|
|
105
|
-
|
|
106
|
-
// List items matching the filter
|
|
107
|
-
const { items } = yield* dlq.list({
|
|
108
|
-
status: req.options?.status,
|
|
109
|
-
flowId: req.options?.flowId,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
let succeeded = 0;
|
|
113
|
-
let failed = 0;
|
|
114
|
-
|
|
115
|
-
// Mark each item for retry
|
|
116
|
-
for (const item of items) {
|
|
117
|
-
const result = yield* Effect.either(dlq.markRetrying(item.id));
|
|
118
|
-
if (result._tag === "Right") {
|
|
119
|
-
succeeded++;
|
|
120
|
-
} else {
|
|
121
|
-
failed++;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
type: "dlq-retry-all",
|
|
127
|
-
status: 200,
|
|
128
|
-
headers: { "Content-Type": "application/json" },
|
|
129
|
-
body: {
|
|
130
|
-
retried: items.length,
|
|
131
|
-
succeeded,
|
|
132
|
-
failed,
|
|
133
|
-
},
|
|
134
|
-
} satisfies DlqRetryAllResponse;
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Handle DELETE /api/dlq/:itemId - Delete a DLQ item
|
|
139
|
-
*/
|
|
140
|
-
export const handleDlqDelete = (req: DlqDeleteRequest) =>
|
|
141
|
-
Effect.gen(function* () {
|
|
142
|
-
const authService = yield* AuthContextService;
|
|
143
|
-
|
|
144
|
-
// Check permission for writing to DLQ
|
|
145
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
146
|
-
|
|
147
|
-
const dlq = yield* DeadLetterQueueService;
|
|
148
|
-
yield* dlq.delete(req.itemId);
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
type: "dlq-delete",
|
|
152
|
-
status: 200,
|
|
153
|
-
headers: { "Content-Type": "application/json" },
|
|
154
|
-
body: { success: true },
|
|
155
|
-
} satisfies DlqDeleteResponse;
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Handle POST /api/dlq/:itemId/resolve - Manually resolve a DLQ item
|
|
160
|
-
*/
|
|
161
|
-
export const handleDlqResolve = (req: DlqResolveRequest) =>
|
|
162
|
-
Effect.gen(function* () {
|
|
163
|
-
const authService = yield* AuthContextService;
|
|
164
|
-
|
|
165
|
-
// Check permission for writing to DLQ
|
|
166
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
167
|
-
|
|
168
|
-
const dlq = yield* DeadLetterQueueService;
|
|
169
|
-
const item = yield* dlq.markResolved(req.itemId);
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
type: "dlq-resolve",
|
|
173
|
-
status: 200,
|
|
174
|
-
headers: { "Content-Type": "application/json" },
|
|
175
|
-
body: item,
|
|
176
|
-
} satisfies DlqResolveResponse;
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Handle POST /api/dlq/cleanup - Cleanup old DLQ items
|
|
181
|
-
*/
|
|
182
|
-
export const handleDlqCleanup = (req: DlqCleanupRequest) =>
|
|
183
|
-
Effect.gen(function* () {
|
|
184
|
-
const authService = yield* AuthContextService;
|
|
185
|
-
|
|
186
|
-
// Check permission for writing to DLQ
|
|
187
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_WRITE);
|
|
188
|
-
|
|
189
|
-
const dlq = yield* DeadLetterQueueService;
|
|
190
|
-
const result = yield* dlq.cleanup(req.options);
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
type: "dlq-cleanup",
|
|
194
|
-
status: 200,
|
|
195
|
-
headers: { "Content-Type": "application/json" },
|
|
196
|
-
body: result,
|
|
197
|
-
} satisfies DlqCleanupResponse;
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Handle GET /api/dlq/stats - Get DLQ statistics
|
|
202
|
-
*/
|
|
203
|
-
export const handleDlqStats = (_req: DlqStatsRequest) =>
|
|
204
|
-
Effect.gen(function* () {
|
|
205
|
-
const authService = yield* AuthContextService;
|
|
206
|
-
|
|
207
|
-
// Check permission for reading DLQ
|
|
208
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.DLQ_READ);
|
|
209
|
-
|
|
210
|
-
const dlq = yield* DeadLetterQueueService;
|
|
211
|
-
const stats = yield* dlq.getStats();
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
type: "dlq-stats",
|
|
215
|
-
status: 200,
|
|
216
|
-
headers: { "Content-Type": "application/json" },
|
|
217
|
-
body: stats,
|
|
218
|
-
} satisfies DlqStatsResponse;
|
|
219
|
-
});
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Health Check HTTP Handlers for Uploadista SDK.
|
|
3
|
-
*
|
|
4
|
-
* This module provides HTTP handlers for health check endpoints:
|
|
5
|
-
* - `/health` (liveness) - Simple alive check, no dependencies
|
|
6
|
-
* - `/ready` (readiness) - Full dependency check for accepting traffic
|
|
7
|
-
* - `/health/components` - Detailed component status for debugging
|
|
8
|
-
*
|
|
9
|
-
* @module core/http-handlers/health-http-handlers
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
formatHealthAsText,
|
|
14
|
-
getHealthResponseFormat,
|
|
15
|
-
type HealthCheckConfig,
|
|
16
|
-
} from "@uploadista/core/types";
|
|
17
|
-
import { Effect } from "effect";
|
|
18
|
-
import { PERMISSIONS } from "../../permissions/types";
|
|
19
|
-
import { AuthContextService } from "../../service";
|
|
20
|
-
import {
|
|
21
|
-
createLivenessResponse,
|
|
22
|
-
performComponentsCheck,
|
|
23
|
-
performReadinessCheck,
|
|
24
|
-
} from "../health-check-service";
|
|
25
|
-
import type {
|
|
26
|
-
HealthComponentsRequest,
|
|
27
|
-
HealthComponentsResponse,
|
|
28
|
-
HealthReadyRequest,
|
|
29
|
-
HealthReadyResponse,
|
|
30
|
-
HealthRequest,
|
|
31
|
-
HealthResponse,
|
|
32
|
-
} from "../routes";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Handle GET /health - Liveness probe
|
|
36
|
-
*
|
|
37
|
-
* Returns immediately with 200 OK if the server is alive.
|
|
38
|
-
* Does not check any dependencies.
|
|
39
|
-
*/
|
|
40
|
-
export const handleHealthLiveness = (
|
|
41
|
-
req: HealthRequest,
|
|
42
|
-
config?: HealthCheckConfig,
|
|
43
|
-
) =>
|
|
44
|
-
Effect.sync(() => {
|
|
45
|
-
const response = createLivenessResponse(config);
|
|
46
|
-
const format = getHealthResponseFormat(req.acceptHeader);
|
|
47
|
-
|
|
48
|
-
if (format === "text") {
|
|
49
|
-
return {
|
|
50
|
-
type: "health",
|
|
51
|
-
status: 200,
|
|
52
|
-
headers: { "Content-Type": "text/plain" },
|
|
53
|
-
body: formatHealthAsText(response.status),
|
|
54
|
-
} as HealthResponse;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
type: "health",
|
|
59
|
-
status: 200,
|
|
60
|
-
headers: { "Content-Type": "application/json" },
|
|
61
|
-
body: response,
|
|
62
|
-
} satisfies HealthResponse;
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Handle GET /ready - Readiness probe
|
|
67
|
-
*
|
|
68
|
-
* Checks all critical dependencies (storage, KV store) and returns:
|
|
69
|
-
* - 200 OK if all dependencies are healthy
|
|
70
|
-
* - 503 Service Unavailable if any critical dependency is unavailable
|
|
71
|
-
*
|
|
72
|
-
* Requires `engine:readiness` permission.
|
|
73
|
-
*/
|
|
74
|
-
export const handleHealthReadiness = (
|
|
75
|
-
req: HealthReadyRequest,
|
|
76
|
-
config?: HealthCheckConfig,
|
|
77
|
-
) =>
|
|
78
|
-
Effect.gen(function* () {
|
|
79
|
-
const authService = yield* AuthContextService;
|
|
80
|
-
|
|
81
|
-
// Check permission for readiness endpoint
|
|
82
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.READINESS);
|
|
83
|
-
|
|
84
|
-
const response = yield* performReadinessCheck(config);
|
|
85
|
-
const format = getHealthResponseFormat(req.acceptHeader);
|
|
86
|
-
|
|
87
|
-
// Determine HTTP status based on health status
|
|
88
|
-
const httpStatus = response.status === "unhealthy" ? 503 : 200;
|
|
89
|
-
|
|
90
|
-
if (format === "text") {
|
|
91
|
-
return {
|
|
92
|
-
type: "health-ready",
|
|
93
|
-
status: httpStatus,
|
|
94
|
-
headers: { "Content-Type": "text/plain" },
|
|
95
|
-
body: formatHealthAsText(response.status),
|
|
96
|
-
} as HealthReadyResponse;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
type: "health-ready",
|
|
101
|
-
status: httpStatus,
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: response,
|
|
104
|
-
} satisfies HealthReadyResponse;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Handle GET /health/components - Detailed component status
|
|
109
|
-
*
|
|
110
|
-
* Returns detailed health information for each component including:
|
|
111
|
-
* - Storage backend
|
|
112
|
-
* - KV store
|
|
113
|
-
* - Event broadcaster
|
|
114
|
-
* - Circuit breaker (if enabled)
|
|
115
|
-
* - Dead letter queue (if enabled)
|
|
116
|
-
*
|
|
117
|
-
* Always returns 200 OK for debugging purposes (even if components are degraded).
|
|
118
|
-
*
|
|
119
|
-
* Requires `engine:readiness` permission.
|
|
120
|
-
*/
|
|
121
|
-
export const handleHealthComponents = (
|
|
122
|
-
req: HealthComponentsRequest,
|
|
123
|
-
config?: HealthCheckConfig,
|
|
124
|
-
) =>
|
|
125
|
-
Effect.gen(function* () {
|
|
126
|
-
const authService = yield* AuthContextService;
|
|
127
|
-
|
|
128
|
-
// Check permission for components endpoint
|
|
129
|
-
yield* authService.requirePermission(PERMISSIONS.ENGINE.READINESS);
|
|
130
|
-
|
|
131
|
-
const response = yield* performComponentsCheck(config);
|
|
132
|
-
const format = getHealthResponseFormat(req.acceptHeader);
|
|
133
|
-
|
|
134
|
-
if (format === "text") {
|
|
135
|
-
// For text format, just return the overall status
|
|
136
|
-
return {
|
|
137
|
-
type: "health-components",
|
|
138
|
-
status: 200,
|
|
139
|
-
headers: { "Content-Type": "text/plain" },
|
|
140
|
-
body: formatHealthAsText(response.status),
|
|
141
|
-
} as HealthComponentsResponse;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
type: "health-components",
|
|
146
|
-
status: 200,
|
|
147
|
-
headers: { "Content-Type": "application/json" },
|
|
148
|
-
body: response,
|
|
149
|
-
} satisfies HealthComponentsResponse;
|
|
150
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authorization Error Types
|
|
3
|
-
*
|
|
4
|
-
* Error classes for permission and authorization failures.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { AdapterError } from "../error-types";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Authorization error - indicates the user lacks required permissions.
|
|
11
|
-
* Returns HTTP 403 Forbidden status.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* if (!hasPermission(permissions, "engine:metrics")) {
|
|
16
|
-
* throw new AuthorizationError("engine:metrics");
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export class AuthorizationError extends AdapterError {
|
|
21
|
-
/**
|
|
22
|
-
* The permission that was required but not granted.
|
|
23
|
-
*/
|
|
24
|
-
public readonly requiredPermission: string;
|
|
25
|
-
|
|
26
|
-
constructor(requiredPermission: string, message?: string) {
|
|
27
|
-
super(
|
|
28
|
-
message ?? `Permission denied: ${requiredPermission} required`,
|
|
29
|
-
403,
|
|
30
|
-
"PERMISSION_DENIED",
|
|
31
|
-
);
|
|
32
|
-
this.name = "AuthorizationError";
|
|
33
|
-
this.requiredPermission = requiredPermission;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Authentication required error - indicates no authentication context.
|
|
39
|
-
* Returns HTTP 401 Unauthorized status.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```typescript
|
|
43
|
-
* if (!authContext) {
|
|
44
|
-
* throw new AuthenticationRequiredError();
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
export class AuthenticationRequiredError extends AdapterError {
|
|
49
|
-
constructor(message = "Authentication required") {
|
|
50
|
-
super(message, 401, "AUTHENTICATION_REQUIRED");
|
|
51
|
-
this.name = "AuthenticationRequiredError";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Organization mismatch error - indicates accessing a resource from another organization.
|
|
57
|
-
* Returns HTTP 403 Forbidden status.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```typescript
|
|
61
|
-
* if (resource.organizationId !== clientId) {
|
|
62
|
-
* throw new OrganizationMismatchError();
|
|
63
|
-
* }
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export class OrganizationMismatchError extends AdapterError {
|
|
67
|
-
constructor(message = "Access denied: resource belongs to another organization") {
|
|
68
|
-
super(message, 403, "ORGANIZATION_MISMATCH");
|
|
69
|
-
this.name = "OrganizationMismatchError";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Quota exceeded error - indicates usage quota has been exceeded.
|
|
75
|
-
* Returns HTTP 402 Payment Required status.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```typescript
|
|
79
|
-
* if (usage > quota) {
|
|
80
|
-
* throw new QuotaExceededError("Storage quota exceeded");
|
|
81
|
-
* }
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export class QuotaExceededError extends AdapterError {
|
|
85
|
-
constructor(message = "Quota exceeded", code = "QUOTA_EXCEEDED") {
|
|
86
|
-
super(message, 402, code);
|
|
87
|
-
this.name = "QuotaExceededError";
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Creates a standardized error response body for AuthorizationError.
|
|
93
|
-
* Includes the required permission in the response.
|
|
94
|
-
*
|
|
95
|
-
* @param error - The AuthorizationError to format
|
|
96
|
-
* @returns Standardized error response body
|
|
97
|
-
*/
|
|
98
|
-
export const createAuthorizationErrorResponseBody = (
|
|
99
|
-
error: AuthorizationError,
|
|
100
|
-
) => ({
|
|
101
|
-
error: error.message,
|
|
102
|
-
code: error.errorCode,
|
|
103
|
-
requiredPermission: error.requiredPermission,
|
|
104
|
-
timestamp: new Date().toISOString(),
|
|
105
|
-
});
|
package/src/permissions/index.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission Matching Logic
|
|
3
|
-
*
|
|
4
|
-
* Implements permission matching with support for:
|
|
5
|
-
* - Exact match: `engine:health` matches `engine:health`
|
|
6
|
-
* - Wildcard match: `engine:*` matches `engine:health`, `engine:metrics`, etc.
|
|
7
|
-
* - Hierarchical match: `engine:dlq` implies `engine:dlq:read` and `engine:dlq:write`
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { PERMISSION_HIERARCHY } from "./types";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Checks if a granted permission matches a required permission.
|
|
14
|
-
*
|
|
15
|
-
* @param granted - The permission that has been granted to the user
|
|
16
|
-
* @param required - The permission that is required for the operation
|
|
17
|
-
* @returns true if the granted permission satisfies the required permission
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```typescript
|
|
21
|
-
* matchesPermission("engine:*", "engine:health") // true (wildcard)
|
|
22
|
-
* matchesPermission("engine:health", "engine:health") // true (exact)
|
|
23
|
-
* matchesPermission("engine:dlq", "engine:dlq:read") // true (hierarchical)
|
|
24
|
-
* matchesPermission("flow:execute", "engine:health") // false
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export const matchesPermission = (
|
|
28
|
-
granted: string,
|
|
29
|
-
required: string,
|
|
30
|
-
): boolean => {
|
|
31
|
-
// Exact match
|
|
32
|
-
if (granted === required) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Wildcard match: `engine:*` matches `engine:health`
|
|
37
|
-
if (granted.endsWith(":*")) {
|
|
38
|
-
const prefix = granted.slice(0, -1); // Remove the `*`, keep the `:`
|
|
39
|
-
if (required.startsWith(prefix)) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Hierarchical match: `engine:dlq` implies `engine:dlq:read`
|
|
45
|
-
const impliedPermissions = PERMISSION_HIERARCHY[granted];
|
|
46
|
-
if (impliedPermissions?.includes(required)) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return false;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Checks if any of the granted permissions satisfy the required permission.
|
|
55
|
-
*
|
|
56
|
-
* @param grantedPermissions - Array of permissions granted to the user
|
|
57
|
-
* @param required - The permission that is required for the operation
|
|
58
|
-
* @returns true if any granted permission satisfies the required permission
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```typescript
|
|
62
|
-
* hasPermission(["flow:*", "upload:create"], "flow:execute") // true
|
|
63
|
-
* hasPermission(["upload:create"], "flow:execute") // false
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export const hasPermission = (
|
|
67
|
-
grantedPermissions: readonly string[],
|
|
68
|
-
required: string,
|
|
69
|
-
): boolean => {
|
|
70
|
-
return grantedPermissions.some((granted) =>
|
|
71
|
-
matchesPermission(granted, required),
|
|
72
|
-
);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Checks if any of the granted permissions satisfy any of the required permissions.
|
|
77
|
-
*
|
|
78
|
-
* @param grantedPermissions - Array of permissions granted to the user
|
|
79
|
-
* @param requiredPermissions - Array of permissions, any of which would be sufficient
|
|
80
|
-
* @returns true if any granted permission satisfies any required permission
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* ```typescript
|
|
84
|
-
* hasAnyPermission(["upload:create"], ["flow:execute", "upload:create"]) // true
|
|
85
|
-
* hasAnyPermission(["upload:read"], ["flow:execute", "upload:create"]) // false
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
export const hasAnyPermission = (
|
|
89
|
-
grantedPermissions: readonly string[],
|
|
90
|
-
requiredPermissions: readonly string[],
|
|
91
|
-
): boolean => {
|
|
92
|
-
return requiredPermissions.some((required) =>
|
|
93
|
-
hasPermission(grantedPermissions, required),
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Checks if all of the required permissions are satisfied.
|
|
99
|
-
*
|
|
100
|
-
* @param grantedPermissions - Array of permissions granted to the user
|
|
101
|
-
* @param requiredPermissions - Array of permissions, all of which must be satisfied
|
|
102
|
-
* @returns true if all required permissions are satisfied
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```typescript
|
|
106
|
-
* hasAllPermissions(["flow:*", "upload:*"], ["flow:execute", "upload:create"]) // true
|
|
107
|
-
* hasAllPermissions(["flow:execute"], ["flow:execute", "upload:create"]) // false
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export const hasAllPermissions = (
|
|
111
|
-
grantedPermissions: readonly string[],
|
|
112
|
-
requiredPermissions: readonly string[],
|
|
113
|
-
): boolean => {
|
|
114
|
-
return requiredPermissions.every((required) =>
|
|
115
|
-
hasPermission(grantedPermissions, required),
|
|
116
|
-
);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Expands a permission to include all implied permissions.
|
|
121
|
-
* Useful for display or audit purposes.
|
|
122
|
-
*
|
|
123
|
-
* @param permission - The permission to expand
|
|
124
|
-
* @returns Array of the permission and all implied permissions
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* ```typescript
|
|
128
|
-
* expandPermission("engine:dlq") // ["engine:dlq", "engine:dlq:read", "engine:dlq:write"]
|
|
129
|
-
* expandPermission("engine:health") // ["engine:health"]
|
|
130
|
-
* ```
|
|
131
|
-
*/
|
|
132
|
-
export const expandPermission = (permission: string): string[] => {
|
|
133
|
-
const result = [permission];
|
|
134
|
-
const implied = PERMISSION_HIERARCHY[permission];
|
|
135
|
-
if (implied) {
|
|
136
|
-
result.push(...implied);
|
|
137
|
-
}
|
|
138
|
-
return result;
|
|
139
|
-
};
|