@uploadista/server 0.0.18-beta.7 → 0.0.18-beta.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/server",
3
3
  "type": "module",
4
- "version": "0.0.18-beta.7",
4
+ "version": "0.0.18-beta.9",
5
5
  "description": "Core Server package for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -20,23 +20,23 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@uploadista/core": "0.0.18-beta.7",
24
- "@uploadista/observability": "0.0.18-beta.7",
25
- "@uploadista/event-emitter-websocket": "0.0.18-beta.7",
26
- "@uploadista/event-broadcaster-memory": "0.0.18-beta.7"
23
+ "@uploadista/core": "0.0.18-beta.9",
24
+ "@uploadista/observability": "0.0.18-beta.9",
25
+ "@uploadista/event-emitter-websocket": "0.0.18-beta.9",
26
+ "@uploadista/event-broadcaster-memory": "0.0.18-beta.9"
27
27
  },
28
28
  "devDependencies": {
29
- "@cloudflare/workers-types": "4.20251126.0",
29
+ "@cloudflare/workers-types": "4.20251202.0",
30
30
  "@effect/vitest": "0.27.0",
31
31
  "@types/express": "^5.0.0",
32
32
  "@types/node": "24.10.1",
33
- "effect": "3.19.6",
33
+ "effect": "3.19.8",
34
34
  "tsd": "0.33.0",
35
- "tsdown": "0.16.7",
35
+ "tsdown": "0.16.8",
36
36
  "typescript": "5.9.3",
37
37
  "vitest": "4.0.14",
38
38
  "zod": "4.1.13",
39
- "@uploadista/typescript-config": "0.0.18-beta.7"
39
+ "@uploadista/typescript-config": "0.0.18-beta.9"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "effect": "^3.0.0",
@@ -1,5 +1,7 @@
1
1
  import { DeadLetterQueueService } from "@uploadista/core/flow";
2
2
  import { Effect } from "effect";
3
+ import { PERMISSIONS } from "../../permissions/types";
4
+ import { AuthContextService } from "../../service";
3
5
  import type {
4
6
  DlqCleanupRequest,
5
7
  DlqCleanupResponse,
@@ -24,6 +26,11 @@ import type {
24
26
  */
25
27
  export const handleDlqList = (req: DlqListRequest) =>
26
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
+
27
34
  const dlq = yield* DeadLetterQueueService;
28
35
  const result = yield* dlq.list(req.options);
29
36
 
@@ -40,6 +47,11 @@ export const handleDlqList = (req: DlqListRequest) =>
40
47
  */
41
48
  export const handleDlqGet = (req: DlqGetRequest) =>
42
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
+
43
55
  const dlq = yield* DeadLetterQueueService;
44
56
  const item = yield* dlq.get(req.itemId);
45
57
 
@@ -56,6 +68,11 @@ export const handleDlqGet = (req: DlqGetRequest) =>
56
68
  */
57
69
  export const handleDlqRetry = (req: DlqRetryRequest) =>
58
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
+
59
76
  const dlq = yield* DeadLetterQueueService;
60
77
 
61
78
  // Mark item as retrying
@@ -79,6 +96,11 @@ export const handleDlqRetry = (req: DlqRetryRequest) =>
79
96
  */
80
97
  export const handleDlqRetryAll = (req: DlqRetryAllRequest) =>
81
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
+
82
104
  const dlq = yield* DeadLetterQueueService;
83
105
 
84
106
  // List items matching the filter
@@ -117,6 +139,11 @@ export const handleDlqRetryAll = (req: DlqRetryAllRequest) =>
117
139
  */
118
140
  export const handleDlqDelete = (req: DlqDeleteRequest) =>
119
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
+
120
147
  const dlq = yield* DeadLetterQueueService;
121
148
  yield* dlq.delete(req.itemId);
122
149
 
@@ -133,6 +160,11 @@ export const handleDlqDelete = (req: DlqDeleteRequest) =>
133
160
  */
134
161
  export const handleDlqResolve = (req: DlqResolveRequest) =>
135
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
+
136
168
  const dlq = yield* DeadLetterQueueService;
137
169
  const item = yield* dlq.markResolved(req.itemId);
138
170
 
@@ -149,6 +181,11 @@ export const handleDlqResolve = (req: DlqResolveRequest) =>
149
181
  */
150
182
  export const handleDlqCleanup = (req: DlqCleanupRequest) =>
151
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
+
152
189
  const dlq = yield* DeadLetterQueueService;
153
190
  const result = yield* dlq.cleanup(req.options);
154
191
 
@@ -165,6 +202,11 @@ export const handleDlqCleanup = (req: DlqCleanupRequest) =>
165
202
  */
166
203
  export const handleDlqStats = (_req: DlqStatsRequest) =>
167
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
+
168
210
  const dlq = yield* DeadLetterQueueService;
169
211
  const stats = yield* dlq.getStats();
170
212
 
@@ -1,7 +1,10 @@
1
1
  import { FlowServer } from "@uploadista/core/flow";
2
2
  import { Effect } from "effect";
3
3
  import { AuthCacheService } from "../../cache";
4
+ import { PERMISSIONS } from "../../permissions/types";
5
+ import { QuotaExceededError } from "../../permissions/errors";
4
6
  import { AuthContextService } from "../../service";
7
+ import { UsageHookService } from "../../usage-hooks/service";
5
8
  import type {
6
9
  CancelFlowRequest,
7
10
  CancelFlowResponse,
@@ -20,10 +23,12 @@ import type {
20
23
  export const handleGetFlow = ({ flowId }: GetFlowRequest) => {
21
24
  return Effect.gen(function* () {
22
25
  const flowServer = yield* FlowServer;
23
- // Access auth context if available
24
26
  const authService = yield* AuthContextService;
25
27
  const clientId = yield* authService.getClientId();
26
28
 
29
+ // Check permission for reading flow status
30
+ yield* authService.requirePermission(PERMISSIONS.FLOW.STATUS);
31
+
27
32
  if (clientId) {
28
33
  yield* Effect.logInfo(
29
34
  `[Flow] Getting flow data: ${flowId}, client: ${clientId}`,
@@ -46,11 +51,14 @@ export const handleRunFlow = <TRequirements>({
46
51
  }: RunFlowRequest) => {
47
52
  return Effect.gen(function* () {
48
53
  const flowServer = yield* FlowServer;
49
- // Access auth context if available
50
54
  const authService = yield* AuthContextService;
51
55
  const authCache = yield* AuthCacheService;
56
+ const usageHookService = yield* UsageHookService;
52
57
  const clientId = yield* authService.getClientId();
53
58
 
59
+ // Check permission for executing flows
60
+ yield* authService.requirePermission(PERMISSIONS.FLOW.EXECUTE);
61
+
54
62
  if (clientId) {
55
63
  yield* Effect.logInfo(
56
64
  `[Flow] Executing flow: ${flowId}, storage: ${storageId}, client: ${clientId}`,
@@ -65,7 +73,28 @@ export const handleRunFlow = <TRequirements>({
65
73
  );
66
74
  }
67
75
 
76
+ // Execute onFlowStart hook for quota checking
77
+ if (clientId) {
78
+ const hookResult = yield* usageHookService.onFlowStart({
79
+ clientId,
80
+ operation: "flow",
81
+ metadata: {
82
+ flowId,
83
+ },
84
+ });
85
+
86
+ if (hookResult.action === "abort") {
87
+ return yield* Effect.fail(
88
+ new QuotaExceededError(
89
+ hookResult.reason,
90
+ hookResult.code ?? "SUBSCRIPTION_REQUIRED",
91
+ ),
92
+ );
93
+ }
94
+ }
95
+
68
96
  // Run flow returns immediately with jobId
97
+ const startTime = Date.now();
69
98
  yield* Effect.logInfo(`[Flow] Calling flowServer.runFlow...`);
70
99
  const result = yield* flowServer
71
100
  .runFlow<TRequirements>({
@@ -91,6 +120,10 @@ export const handleRunFlow = <TRequirements>({
91
120
 
92
121
  yield* Effect.logInfo(`[Flow] Flow started with jobId: ${result.id}`);
93
122
 
123
+ // Note: onFlowComplete hook is called when the flow actually completes,
124
+ // which happens asynchronously. The hook should be invoked from the
125
+ // flow execution pipeline, not here where we just start the flow.
126
+
94
127
  return {
95
128
  status: 200,
96
129
  body: result,
@@ -101,11 +134,13 @@ export const handleRunFlow = <TRequirements>({
101
134
  export const handleJobStatus = ({ jobId }: GetJobStatusRequest) => {
102
135
  return Effect.gen(function* () {
103
136
  const flowServer = yield* FlowServer;
104
- // Access auth context if available
105
137
  const authService = yield* AuthContextService;
106
138
  const authCache = yield* AuthCacheService;
107
139
  const clientId = yield* authService.getClientId();
108
140
 
141
+ // Check permission for checking flow status
142
+ yield* authService.requirePermission(PERMISSIONS.FLOW.STATUS);
143
+
109
144
  if (!jobId) {
110
145
  throw new Error("No job id");
111
146
  }
@@ -142,10 +177,12 @@ export const handleResumeFlow = <TRequirements>({
142
177
  }: ResumeFlowRequest) => {
143
178
  return Effect.gen(function* () {
144
179
  const flowServer = yield* FlowServer;
145
- // Try to get auth from current request or cached auth
146
180
  const authService = yield* AuthContextService;
147
181
  const authCache = yield* AuthCacheService;
148
182
 
183
+ // Check permission for executing flows (resume is part of execution)
184
+ yield* authService.requirePermission(PERMISSIONS.FLOW.EXECUTE);
185
+
149
186
  // Try current auth first, fallback to cached auth
150
187
  let clientId = yield* authService.getClientId();
151
188
  if (!clientId) {
@@ -186,13 +223,16 @@ export const handleResumeFlow = <TRequirements>({
186
223
  } as ResumeFlowResponse;
187
224
  });
188
225
  };
226
+
189
227
  export const handlePauseFlow = ({ jobId }: PauseFlowRequest) => {
190
228
  return Effect.gen(function* () {
191
229
  const flowServer = yield* FlowServer;
192
- // Try to get auth from current request or cached auth
193
230
  const authService = yield* AuthContextService;
194
231
  const authCache = yield* AuthCacheService;
195
232
 
233
+ // Check permission for cancelling flows (pause is related to cancel)
234
+ yield* authService.requirePermission(PERMISSIONS.FLOW.CANCEL);
235
+
196
236
  // Try current auth first, fallback to cached auth
197
237
  let clientId = yield* authService.getClientId();
198
238
  if (!clientId) {
@@ -224,9 +264,12 @@ export const handlePauseFlow = ({ jobId }: PauseFlowRequest) => {
224
264
  export const handleCancelFlow = ({ jobId }: CancelFlowRequest) => {
225
265
  return Effect.gen(function* () {
226
266
  const flowServer = yield* FlowServer;
227
- // Try to get auth from current request or cached auth
228
267
  const authService = yield* AuthContextService;
229
268
  const authCache = yield* AuthCacheService;
269
+ const usageHookService = yield* UsageHookService;
270
+
271
+ // Check permission for cancelling flows
272
+ yield* authService.requirePermission(PERMISSIONS.FLOW.CANCEL);
230
273
 
231
274
  if (!jobId) {
232
275
  throw new Error("No job id");
@@ -253,6 +296,18 @@ export const handleCancelFlow = ({ jobId }: CancelFlowRequest) => {
253
296
  yield* Effect.logInfo(
254
297
  `[Flow] Flow cancelled, cleared auth cache: ${jobId}`,
255
298
  );
299
+
300
+ // Execute onFlowComplete hook for cancelled flow
301
+ yield* Effect.forkDaemon(
302
+ usageHookService.onFlowComplete({
303
+ clientId,
304
+ operation: "flow",
305
+ metadata: {
306
+ jobId,
307
+ status: "cancelled",
308
+ },
309
+ }),
310
+ );
256
311
  }
257
312
 
258
313
  return {
@@ -15,6 +15,8 @@ import {
15
15
  type HealthCheckConfig,
16
16
  } from "@uploadista/core/types";
17
17
  import { Effect } from "effect";
18
+ import { PERMISSIONS } from "../../permissions/types";
19
+ import { AuthContextService } from "../../service";
18
20
  import {
19
21
  createLivenessResponse,
20
22
  performComponentsCheck,
@@ -66,12 +68,19 @@ export const handleHealthLiveness = (
66
68
  * Checks all critical dependencies (storage, KV store) and returns:
67
69
  * - 200 OK if all dependencies are healthy
68
70
  * - 503 Service Unavailable if any critical dependency is unavailable
71
+ *
72
+ * Requires `engine:readiness` permission.
69
73
  */
70
74
  export const handleHealthReadiness = (
71
75
  req: HealthReadyRequest,
72
76
  config?: HealthCheckConfig,
73
77
  ) =>
74
78
  Effect.gen(function* () {
79
+ const authService = yield* AuthContextService;
80
+
81
+ // Check permission for readiness endpoint
82
+ yield* authService.requirePermission(PERMISSIONS.ENGINE.READINESS);
83
+
75
84
  const response = yield* performReadinessCheck(config);
76
85
  const format = getHealthResponseFormat(req.acceptHeader);
77
86
 
@@ -106,12 +115,19 @@ export const handleHealthReadiness = (
106
115
  * - Dead letter queue (if enabled)
107
116
  *
108
117
  * Always returns 200 OK for debugging purposes (even if components are degraded).
118
+ *
119
+ * Requires `engine:readiness` permission.
109
120
  */
110
121
  export const handleHealthComponents = (
111
122
  req: HealthComponentsRequest,
112
123
  config?: HealthCheckConfig,
113
124
  ) =>
114
125
  Effect.gen(function* () {
126
+ const authService = yield* AuthContextService;
127
+
128
+ // Check permission for components endpoint
129
+ yield* authService.requirePermission(PERMISSIONS.ENGINE.READINESS);
130
+
115
131
  const response = yield* performComponentsCheck(config);
116
132
  const format = getHealthResponseFormat(req.acceptHeader);
117
133
 
@@ -5,7 +5,10 @@ import { MetricsService } from "@uploadista/observability";
5
5
  import { Effect } from "effect";
6
6
  import { AuthCacheService } from "../../cache";
7
7
  import { ValidationError } from "../../error-types";
8
+ import { PERMISSIONS } from "../../permissions/types";
9
+ import { QuotaExceededError } from "../../permissions/errors";
8
10
  import { AuthContextService } from "../../service";
11
+ import { UsageHookService } from "../../usage-hooks/service";
9
12
  import type {
10
13
  CreateUploadRequest,
11
14
  CreateUploadResponse,
@@ -20,11 +23,14 @@ import type {
20
23
  export const handleCreateUpload = (req: CreateUploadRequest) =>
21
24
  Effect.gen(function* () {
22
25
  const server = yield* UploadServer;
23
- // Access auth context if available
24
26
  const authService = yield* AuthContextService;
25
27
  const authCache = yield* AuthCacheService;
28
+ const usageHookService = yield* UsageHookService;
26
29
  const clientId = yield* authService.getClientId();
27
30
 
31
+ // Check permission for creating uploads
32
+ yield* authService.requirePermission(PERMISSIONS.UPLOAD.CREATE);
33
+
28
34
  if (clientId) {
29
35
  yield* Effect.logInfo(`[Upload] Creating upload for client: ${clientId}`);
30
36
  }
@@ -51,6 +57,28 @@ export const handleCreateUpload = (req: CreateUploadRequest) =>
51
57
  );
52
58
  }
53
59
 
60
+ // Execute onUploadStart hook for quota checking
61
+ if (clientId) {
62
+ const hookResult = yield* usageHookService.onUploadStart({
63
+ clientId,
64
+ operation: "upload",
65
+ metadata: {
66
+ fileSize: parsedInputFile.data.size,
67
+ mimeType: parsedInputFile.data.type,
68
+ fileName: parsedInputFile.data.fileName,
69
+ },
70
+ });
71
+
72
+ if (hookResult.action === "abort") {
73
+ return yield* Effect.fail(
74
+ new QuotaExceededError(
75
+ hookResult.reason,
76
+ hookResult.code ?? "QUOTA_EXCEEDED",
77
+ ),
78
+ );
79
+ }
80
+ }
81
+
54
82
  const fileCreated = yield* server.createUpload(
55
83
  parsedInputFile.data,
56
84
  clientId,
@@ -80,6 +108,9 @@ export const handleGetCapabilities = ({ storageId }: GetCapabilitiesRequest) =>
80
108
  const authService = yield* AuthContextService;
81
109
  const clientId = yield* authService.getClientId();
82
110
 
111
+ // Check permission for reading upload capabilities
112
+ yield* authService.requirePermission(PERMISSIONS.UPLOAD.READ);
113
+
83
114
  const capabilities = yield* server.getCapabilities(storageId, clientId);
84
115
 
85
116
  return {
@@ -95,6 +126,11 @@ export const handleGetCapabilities = ({ storageId }: GetCapabilitiesRequest) =>
95
126
  export const handleGetUpload = ({ uploadId }: GetUploadRequest) =>
96
127
  Effect.gen(function* () {
97
128
  const server = yield* UploadServer;
129
+ const authService = yield* AuthContextService;
130
+
131
+ // Check permission for reading upload status
132
+ yield* authService.requirePermission(PERMISSIONS.UPLOAD.READ);
133
+
98
134
  const fileResult = yield* server.getUpload(uploadId);
99
135
 
100
136
  return {
@@ -106,13 +142,16 @@ export const handleGetUpload = ({ uploadId }: GetUploadRequest) =>
106
142
  export const handleUploadChunk = (req: UploadChunkRequest) =>
107
143
  Effect.gen(function* () {
108
144
  const server = yield* UploadServer;
109
- // Try to get auth from current request or cached auth
110
145
  const authService = yield* AuthContextService;
111
146
  const authCache = yield* AuthCacheService;
112
147
  const metricsService = yield* MetricsService;
148
+ const usageHookService = yield* UsageHookService;
113
149
 
114
150
  const { uploadId, data } = req;
115
151
 
152
+ // Check permission for creating uploads (chunks are part of creation)
153
+ yield* authService.requirePermission(PERMISSIONS.UPLOAD.CREATE);
154
+
116
155
  // Try current auth first, fallback to cached auth
117
156
  let clientId = yield* authService.getClientId();
118
157
  let authMetadata = yield* authService.getMetadata();
@@ -128,6 +167,7 @@ export const handleUploadChunk = (req: UploadChunkRequest) =>
128
167
  );
129
168
  }
130
169
 
170
+ const startTime = Date.now();
131
171
  const fileResult = yield* server.uploadChunk(uploadId, clientId, data);
132
172
 
133
173
  // Clear cache and record metrics if upload is complete
@@ -140,7 +180,6 @@ export const handleUploadChunk = (req: UploadChunkRequest) =>
140
180
  }
141
181
 
142
182
  // Record upload metrics if we have organization ID
143
-
144
183
  if (clientId && fileResult.size) {
145
184
  yield* Effect.logInfo(
146
185
  `[Upload] Recording metrics for org: ${clientId}, size: ${fileResult.size}`,
@@ -148,6 +187,20 @@ export const handleUploadChunk = (req: UploadChunkRequest) =>
148
187
  yield* Effect.forkDaemon(
149
188
  metricsService.recordUpload(clientId, fileResult.size, authMetadata),
150
189
  );
190
+
191
+ // Execute onUploadComplete hook for usage tracking
192
+ const duration = Date.now() - startTime;
193
+ yield* Effect.forkDaemon(
194
+ usageHookService.onUploadComplete({
195
+ clientId,
196
+ operation: "upload",
197
+ metadata: {
198
+ uploadId,
199
+ fileSize: fileResult.size,
200
+ duration,
201
+ },
202
+ }),
203
+ );
151
204
  } else {
152
205
  yield* Effect.logWarning(
153
206
  `[Upload] Cannot record metrics - missing organizationId or size`,
@@ -24,6 +24,7 @@ import { handleFlowError } from "../http-utils";
24
24
  import { createFlowServerLayer, createUploadServerLayer } from "../layer-utils";
25
25
  import { AuthContextServiceLive } from "../service";
26
26
  import type { AuthContext } from "../types";
27
+ import { UsageHookServiceLive } from "../usage-hooks/service";
27
28
  import { handleUploadistaRequest } from "./http-handlers/http-handlers";
28
29
  import type { ExtractFlowPluginRequirements } from "./plugin-types";
29
30
  import type { NotFoundResponse } from "./routes";
@@ -201,6 +202,7 @@ export const createUploadistaServer = async <
201
202
  circuitBreaker = true,
202
203
  deadLetterQueue = false,
203
204
  healthCheck,
205
+ usageHooks,
204
206
  }: UploadistaServerConfig<
205
207
  TContext,
206
208
  TResponse,
@@ -285,17 +287,22 @@ export const createUploadistaServer = async <
285
287
  )
286
288
  : null;
287
289
 
290
+ // Create usage hook layer (defaults to no-op if not configured)
291
+ const usageHookLayer = UsageHookServiceLive(usageHooks);
292
+
288
293
  /**
289
294
  * Merge all server layers including plugins.
290
295
  *
291
296
  * This combines the core server infrastructure (upload server, flow server,
292
- * metrics, auth cache, circuit breaker, dead letter queue) with user-provided plugin layers.
297
+ * metrics, auth cache, circuit breaker, dead letter queue, usage hooks)
298
+ * with user-provided plugin layers.
293
299
  */
294
300
  const serverLayerRaw = Layer.mergeAll(
295
301
  uploadServerLayer,
296
302
  flowServerLayer,
297
303
  effectiveMetricsLayer,
298
304
  authCacheLayer,
305
+ usageHookLayer,
299
306
  ...plugins,
300
307
  ...(circuitBreakerStoreLayer ? [circuitBreakerStoreLayer] : []),
301
308
  ...(dlqLayer ? [dlqLayer] : []),
@@ -497,12 +504,13 @@ export const createUploadistaServer = async <
497
504
  }
498
505
  }
499
506
 
500
- // Combine auth context, auth cache, metrics layers, plugins, circuit breaker, DLQ, and waitUntil
507
+ // Combine auth context, auth cache, metrics layers, usage hooks, plugins, circuit breaker, DLQ, and waitUntil
501
508
  // This ensures that flow nodes have access to all required services
502
509
  const baseRequestContextLayer = Layer.mergeAll(
503
510
  authContextLayer,
504
511
  authCacheLayer,
505
512
  effectiveMetricsLayer,
513
+ usageHookLayer,
506
514
  ...plugins,
507
515
  ...waitUntilLayers,
508
516
  );
package/src/core/types.ts CHANGED
@@ -15,6 +15,7 @@ import type { Effect, Layer } from "effect";
15
15
  import type { z } from "zod";
16
16
  import type { ServerAdapter } from "../adapter";
17
17
  import type { AuthCacheConfig } from "../cache";
18
+ import type { UsageHookConfig } from "../usage-hooks/types";
18
19
 
19
20
  /**
20
21
  * Function type for retrieving flows based on flow ID and client ID.
@@ -374,6 +375,40 @@ export interface UploadistaServerConfig<
374
375
  * ```
375
376
  */
376
377
  healthCheck?: HealthCheckConfig;
378
+
379
+ /**
380
+ * Optional: Usage hooks for tracking and billing integration.
381
+ *
382
+ * Usage hooks allow you to intercept upload and flow operations for:
383
+ * - Quota checking (e.g., verify user has subscription)
384
+ * - Usage tracking (e.g., count uploads, track bandwidth)
385
+ * - Billing integration (e.g., report usage to Stripe/Polar)
386
+ *
387
+ * Hooks follow a "fail-open" design - if a hook times out or errors,
388
+ * the operation proceeds (unless the hook explicitly aborts).
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * usageHooks: {
393
+ * hooks: {
394
+ * onUploadStart: (ctx) => Effect.gen(function* () {
395
+ * // Check quota before upload starts
396
+ * const quota = yield* checkUserQuota(ctx.clientId);
397
+ * if (quota.exceeded) {
398
+ * return { action: "abort", reason: "Storage quota exceeded" };
399
+ * }
400
+ * return { action: "continue" };
401
+ * }),
402
+ * onUploadComplete: (ctx) => Effect.gen(function* () {
403
+ * // Track usage after upload completes
404
+ * yield* reportUsage(ctx.clientId, ctx.metadata.fileSize);
405
+ * }),
406
+ * },
407
+ * timeout: 5000, // 5 second timeout for hooks
408
+ * }
409
+ * ```
410
+ */
411
+ usageHooks?: UsageHookConfig;
377
412
  }
378
413
 
379
414
  /**
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ export * from "./core";
5
5
  export * from "./error-types";
6
6
  export * from "./http-utils";
7
7
  export * from "./layer-utils";
8
+ export * from "./permissions";
8
9
  export * from "./plugins-typing";
9
10
  export * from "./service";
10
11
  export * from "./types";
12
+ export * from "./usage-hooks";