@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.
@@ -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
- });
@@ -1,9 +0,0 @@
1
- /**
2
- * Permissions Module
3
- *
4
- * Exports all permission-related types, constants, and utilities.
5
- */
6
-
7
- export * from "./types";
8
- export * from "./matcher";
9
- export * from "./errors";
@@ -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
- };