@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,14 +1,7 @@
1
1
  import type { PluginLayer, UploadistaError } from "@uploadista/core";
2
- import {
3
- deadLetterQueueService,
4
- type Flow,
5
- FlowProvider,
6
- FlowWaitUntil,
7
- kvCircuitBreakerStoreLayer,
8
- } from "@uploadista/core/flow";
2
+ import { type Flow, FlowProvider, FlowWaitUntil } from "@uploadista/core/flow";
9
3
  import {
10
4
  createDataStoreLayer,
11
- deadLetterQueueKvStore,
12
5
  type UploadFileDataStores,
13
6
  type UploadFileKVStore,
14
7
  } from "@uploadista/core/types";
@@ -24,7 +17,6 @@ import { handleFlowError } from "../http-utils";
24
17
  import { createFlowServerLayer, createUploadServerLayer } from "../layer-utils";
25
18
  import { AuthContextServiceLive } from "../service";
26
19
  import type { AuthContext } from "../types";
27
- import { UsageHookServiceLive } from "../usage-hooks/service";
28
20
  import { handleUploadistaRequest } from "./http-handlers/http-handlers";
29
21
  import type { ExtractFlowPluginRequirements } from "./plugin-types";
30
22
  import type { NotFoundResponse } from "./routes";
@@ -193,17 +185,12 @@ export const createUploadistaServer = async <
193
185
  eventEmitter,
194
186
  eventBroadcaster = memoryEventBroadcaster,
195
187
  withTracing = false,
196
- observabilityLayer,
197
188
  baseUrl: configBaseUrl = "uploadista",
198
189
  generateId = GenerateIdLive,
199
190
  metricsLayer,
200
191
  bufferedDataStore,
201
192
  adapter,
202
193
  authCacheConfig,
203
- circuitBreaker = true,
204
- deadLetterQueue = false,
205
- healthCheck,
206
- usageHooks,
207
194
  }: UploadistaServerConfig<
208
195
  TContext,
209
196
  TResponse,
@@ -274,50 +261,20 @@ export const createUploadistaServer = async <
274
261
  // Metrics layer (defaults to NoOp if not provided)
275
262
  const effectiveMetricsLayer = metricsLayer ?? NoOpMetricsServiceLive;
276
263
 
277
- // Create circuit breaker store layer if enabled (uses the provided kvStore)
278
- const circuitBreakerStoreLayer = circuitBreaker
279
- ? kvCircuitBreakerStoreLayer.pipe(Layer.provide(kvStore))
280
- : null;
281
-
282
- // Create dead letter queue layer if enabled (uses the provided kvStore)
283
- // The DLQ layer provides both the KV store wrapper and the service
284
- const dlqLayer = deadLetterQueue
285
- ? deadLetterQueueService.pipe(
286
- Layer.provide(deadLetterQueueKvStore),
287
- Layer.provide(kvStore),
288
- )
289
- : null;
290
-
291
- // Create usage hook layer (defaults to no-op if not configured)
292
- const usageHookLayer = UsageHookServiceLive(usageHooks);
293
-
294
264
  /**
295
265
  * Merge all server layers including plugins.
296
266
  *
297
267
  * This combines the core server infrastructure (upload server, flow server,
298
- * metrics, auth cache, circuit breaker, dead letter queue, usage hooks)
299
- * with user-provided plugin layers.
268
+ * metrics, auth cache) with user-provided plugin layers.
300
269
  */
301
270
  const serverLayerRaw = Layer.mergeAll(
302
271
  uploadServerLayer,
303
272
  flowServerLayer,
304
273
  effectiveMetricsLayer,
305
274
  authCacheLayer,
306
- usageHookLayer,
307
275
  ...plugins,
308
- ...(circuitBreakerStoreLayer ? [circuitBreakerStoreLayer] : []),
309
- ...(dlqLayer ? [dlqLayer] : []),
310
276
  );
311
277
 
312
- /**
313
- * Determine the tracing layer to use.
314
- * This must be included in the runtime layer (not per-request) so that the
315
- * BatchSpanProcessor can aggregate spans across requests and flush them properly.
316
- */
317
- const tracingLayer = withTracing
318
- ? observabilityLayer ?? NodeSdkLive
319
- : null;
320
-
321
278
  /**
322
279
  * Type Casting Rationale for Plugin System
323
280
  *
@@ -395,29 +352,16 @@ export const createUploadistaServer = async <
395
352
  * @see validatePluginRequirements - Runtime validation helper
396
353
  * @see ValidatePlugins - Compile-time validation type utility
397
354
  */
398
- const serverLayerTyped = serverLayerRaw as unknown as Layer.Layer<
355
+ const serverLayer = serverLayerRaw as unknown as Layer.Layer<
399
356
  // biome-ignore lint/suspicious/noExplicitAny: Dynamic plugin requirements require any - see comprehensive explanation above
400
357
  any,
401
358
  never,
402
359
  never
403
360
  >;
404
361
 
405
- /**
406
- * Final server layer with optional tracing.
407
- * The tracing layer is merged at runtime level (not per-request) so that:
408
- * 1. The OpenTelemetry SDK is initialized once for the server
409
- * 2. The BatchSpanProcessor can aggregate spans across requests
410
- * 3. Spans are properly flushed when the runtime is disposed
411
- */
412
- const serverLayer = tracingLayer
413
- ? Layer.merge(serverLayerTyped, tracingLayer)
414
- : serverLayerTyped;
415
-
416
362
  // Create a shared managed runtime from the server layer
417
363
  // This ensures all requests use the same layer instances (including event broadcaster)
418
364
  // ManagedRuntime properly handles scoped resources and provides convenient run methods
419
- // When tracing is enabled, the OpenTelemetry SDK is part of this runtime and will be
420
- // properly shut down (flushing all pending spans) when dispose() is called
421
365
 
422
366
  const managedRuntime = ManagedRuntime.make(serverLayer);
423
367
 
@@ -527,22 +471,15 @@ export const createUploadistaServer = async <
527
471
  }
528
472
  }
529
473
 
530
- // Combine auth context, auth cache, metrics layers, usage hooks, plugins, circuit breaker, DLQ, and waitUntil
474
+ // Combine auth context, auth cache, metrics layers, plugins, and waitUntil
531
475
  // This ensures that flow nodes have access to all required services
532
- const baseRequestContextLayer = Layer.mergeAll(
476
+ const requestContextLayer = Layer.mergeAll(
533
477
  authContextLayer,
534
478
  authCacheLayer,
535
479
  effectiveMetricsLayer,
536
- usageHookLayer,
537
480
  ...plugins,
538
481
  ...waitUntilLayers,
539
482
  );
540
- const withCircuitBreakerContext = circuitBreakerStoreLayer
541
- ? Layer.merge(baseRequestContextLayer, circuitBreakerStoreLayer)
542
- : baseRequestContextLayer;
543
- const requestContextLayer = dlqLayer
544
- ? Layer.merge(withCircuitBreakerContext, dlqLayer)
545
- : withCircuitBreakerContext;
546
483
 
547
484
  // Check for baseUrl/api/ prefix
548
485
  if (uploadistaRequest.type === "not-found") {
@@ -558,7 +495,6 @@ export const createUploadistaServer = async <
558
495
  // Handle the request
559
496
  const response = yield* handleUploadistaRequest<TRequirements>(
560
497
  uploadistaRequest,
561
- { healthCheckConfig: healthCheck },
562
498
  ).pipe(Effect.provide(requestContextLayer));
563
499
 
564
500
  return yield* adapter.sendResponse(response, ctx);
@@ -582,8 +518,12 @@ export const createUploadistaServer = async <
582
518
  }),
583
519
  );
584
520
 
585
- // Use the shared managed runtime which includes all layers (including tracing if enabled)
586
- // Tracing is now part of the runtime layer, so spans are properly aggregated and flushed
521
+ // Use the shared managed runtime instead of creating a new one per request
522
+ if (withTracing) {
523
+ return managedRuntime.runPromise(
524
+ program.pipe(Effect.provide(NodeSdkLive)),
525
+ );
526
+ }
587
527
  return managedRuntime.runPromise(program);
588
528
  };
589
529
 
package/src/core/types.ts CHANGED
@@ -5,7 +5,6 @@ import type {
5
5
  BaseKvStoreService,
6
6
  DataStoreConfig,
7
7
  EventBroadcasterService,
8
- HealthCheckConfig,
9
8
  UploadFileDataStore,
10
9
  UploadFileKVStore,
11
10
  } from "@uploadista/core/types";
@@ -15,7 +14,6 @@ import type { Effect, Layer } from "effect";
15
14
  import type { z } from "zod";
16
15
  import type { ServerAdapter } from "../adapter";
17
16
  import type { AuthCacheConfig } from "../cache";
18
- import type { UsageHookConfig } from "../usage-hooks/types";
19
17
 
20
18
  /**
21
19
  * Function type for retrieving flows based on flow ID and client ID.
@@ -230,42 +228,6 @@ export interface UploadistaServerConfig<
230
228
  */
231
229
  withTracing?: boolean;
232
230
 
233
- /**
234
- * Optional: Custom observability layer for distributed tracing.
235
- *
236
- * When provided, this layer will be used instead of the default NodeSdkLive.
237
- * This allows you to configure custom OTLP exporters (e.g., for Grafana Cloud,
238
- * Jaeger, or other OpenTelemetry-compatible backends).
239
- *
240
- * Requires `withTracing: true` to be effective.
241
- *
242
- * @example
243
- * ```typescript
244
- * import { OtlpNodeSdkLive, createOtlpNodeSdkLayer } from "@uploadista/observability";
245
- *
246
- * // Option 1: Use default OTLP layer (reads from env vars)
247
- * const server = await createUploadistaServer({
248
- * withTracing: true,
249
- * observabilityLayer: OtlpNodeSdkLive,
250
- * // ...
251
- * });
252
- *
253
- * // Option 2: Custom configuration with tenant attributes
254
- * const server = await createUploadistaServer({
255
- * withTracing: true,
256
- * observabilityLayer: createOtlpNodeSdkLayer({
257
- * serviceName: "uploadista-cloud-api",
258
- * resourceAttributes: {
259
- * "deployment.environment": "production",
260
- * },
261
- * }),
262
- * // ...
263
- * });
264
- * ```
265
- */
266
- // biome-ignore lint/suspicious/noExplicitAny: Observability layers from @effect/opentelemetry provide different services
267
- observabilityLayer?: Layer.Layer<any, never, never>;
268
-
269
231
  /**
270
232
  * Optional: Metrics layer for observability.
271
233
  *
@@ -333,118 +295,6 @@ export interface UploadistaServerConfig<
333
295
  * ```
334
296
  */
335
297
  authCacheConfig?: AuthCacheConfig;
336
-
337
- /**
338
- * Optional: Enable circuit breakers for flow nodes.
339
- *
340
- * When enabled (default), circuit breaker state is stored in the KV store,
341
- * allowing circuit breaker state to be shared across multiple server instances
342
- * in a cluster deployment.
343
- *
344
- * Set to `false` to disable circuit breakers entirely.
345
- *
346
- * @default true
347
- *
348
- * @example
349
- * ```typescript
350
- * // Circuit breakers enabled by default (uses the provided kvStore)
351
- * const server = await createUploadistaServer({
352
- * kvStore: redisKvStore,
353
- * // circuitBreaker: true (default)
354
- * });
355
- *
356
- * // Disable circuit breakers
357
- * const server = await createUploadistaServer({
358
- * kvStore: redisKvStore,
359
- * circuitBreaker: false
360
- * });
361
- * ```
362
- */
363
- circuitBreaker?: boolean;
364
-
365
- /**
366
- * Optional: Enable dead letter queue for failed flow jobs.
367
- *
368
- * When enabled, failed flow jobs are captured in a DLQ with full context
369
- * for debugging and retry. The DLQ state is stored in the KV store,
370
- * allowing it to be shared across multiple server instances.
371
- *
372
- * Set to `false` to disable the DLQ entirely.
373
- *
374
- * @default false
375
- *
376
- * @example
377
- * ```typescript
378
- * // Enable DLQ (uses the provided kvStore)
379
- * const server = await createUploadistaServer({
380
- * kvStore: redisKvStore,
381
- * deadLetterQueue: true
382
- * });
383
- *
384
- * // DLQ is disabled by default
385
- * const server = await createUploadistaServer({
386
- * kvStore: redisKvStore,
387
- * // deadLetterQueue: false (default)
388
- * });
389
- * ```
390
- */
391
- deadLetterQueue?: boolean;
392
-
393
- /**
394
- * Optional: Health check configuration.
395
- *
396
- * Configures the behavior of health check endpoints (`/health`, `/ready`, `/health/components`).
397
- * When not provided, default values are used:
398
- * - timeout: 5000ms
399
- * - checkStorage: true
400
- * - checkKvStore: true
401
- * - checkEventBroadcaster: true
402
- *
403
- * @example
404
- * ```typescript
405
- * healthCheck: {
406
- * timeout: 3000, // 3 second timeout for dependency checks
407
- * checkStorage: true, // Check storage backend health
408
- * checkKvStore: true, // Check KV store health
409
- * version: "1.2.3" // Include version in health responses
410
- * }
411
- * ```
412
- */
413
- healthCheck?: HealthCheckConfig;
414
-
415
- /**
416
- * Optional: Usage hooks for tracking and billing integration.
417
- *
418
- * Usage hooks allow you to intercept upload and flow operations for:
419
- * - Quota checking (e.g., verify user has subscription)
420
- * - Usage tracking (e.g., count uploads, track bandwidth)
421
- * - Billing integration (e.g., report usage to Stripe/Polar)
422
- *
423
- * Hooks follow a "fail-open" design - if a hook times out or errors,
424
- * the operation proceeds (unless the hook explicitly aborts).
425
- *
426
- * @example
427
- * ```typescript
428
- * usageHooks: {
429
- * hooks: {
430
- * onUploadStart: (ctx) => Effect.gen(function* () {
431
- * // Check quota before upload starts
432
- * const quota = yield* checkUserQuota(ctx.clientId);
433
- * if (quota.exceeded) {
434
- * return { action: "abort", reason: "Storage quota exceeded" };
435
- * }
436
- * return { action: "continue" };
437
- * }),
438
- * onUploadComplete: (ctx) => Effect.gen(function* () {
439
- * // Track usage after upload completes
440
- * yield* reportUsage(ctx.clientId, ctx.metadata.fileSize);
441
- * }),
442
- * },
443
- * timeout: 5000, // 5 second timeout for hooks
444
- * }
445
- * ```
446
- */
447
- usageHooks?: UsageHookConfig;
448
298
  }
449
299
 
450
300
  /**
package/src/index.ts CHANGED
@@ -5,8 +5,6 @@ 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";
9
8
  export * from "./plugins-typing";
10
9
  export * from "./service";
11
10
  export * from "./types";
12
- export * from "./usage-hooks";
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { Flow, UploadServer } from "@uploadista/core";
18
- import type { ExtractLayerServices } from "@uploadista/core/flow";
18
+ import type { ExtractLayerServices } from "@uploadista/core/flow/types";
19
19
  import type { Effect, Layer } from "effect";
20
20
  import type z from "zod";
21
21
 
package/src/service.ts CHANGED
@@ -1,10 +1,5 @@
1
1
  import { Context, Effect, Layer } from "effect";
2
2
  import type { AuthContext } from "./types";
3
- import {
4
- hasPermission as matchHasPermission,
5
- hasAnyPermission as matchHasAnyPermission,
6
- } from "./permissions/matcher";
7
- import { AuthorizationError, AuthenticationRequiredError } from "./permissions/errors";
8
3
 
9
4
  /**
10
5
  * Authentication Context Service
@@ -44,68 +39,10 @@ export class AuthContextService extends Context.Tag("AuthContextService")<
44
39
 
45
40
  /**
46
41
  * Check if the current client has a specific permission.
47
- * Supports exact match, wildcard match, and hierarchical match.
48
42
  * Returns false if no authentication context or permission not found.
49
- *
50
- * @example
51
- * ```typescript
52
- * // Exact match
53
- * yield* authService.hasPermission("engine:health")
54
- *
55
- * // Wildcard: user with "engine:*" will match "engine:health"
56
- * yield* authService.hasPermission("engine:health")
57
- *
58
- * // Hierarchical: user with "engine:dlq" will match "engine:dlq:read"
59
- * yield* authService.hasPermission("engine:dlq:read")
60
- * ```
61
43
  */
62
44
  readonly hasPermission: (permission: string) => Effect.Effect<boolean>;
63
45
 
64
- /**
65
- * Check if the current client has any of the specified permissions.
66
- * Returns true if at least one permission is granted.
67
- */
68
- readonly hasAnyPermission: (
69
- permissions: readonly string[],
70
- ) => Effect.Effect<boolean>;
71
-
72
- /**
73
- * Require a specific permission, failing with AuthorizationError if not granted.
74
- * Use this when you want to fail fast on missing permissions.
75
- *
76
- * @throws AuthorizationError if permission is not granted
77
- * @throws AuthenticationRequiredError if no auth context
78
- *
79
- * @example
80
- * ```typescript
81
- * const protectedHandler = Effect.gen(function* () {
82
- * const authService = yield* AuthContextService;
83
- * yield* authService.requirePermission("engine:metrics");
84
- * // Only reaches here if permission is granted
85
- * return yield* getMetrics();
86
- * });
87
- * ```
88
- */
89
- readonly requirePermission: (
90
- permission: string,
91
- ) => Effect.Effect<void, AuthorizationError | AuthenticationRequiredError>;
92
-
93
- /**
94
- * Require authentication, failing with AuthenticationRequiredError if not authenticated.
95
- *
96
- * @throws AuthenticationRequiredError if no auth context
97
- */
98
- readonly requireAuthentication: () => Effect.Effect<
99
- AuthContext,
100
- AuthenticationRequiredError
101
- >;
102
-
103
- /**
104
- * Get all permissions granted to the current client.
105
- * Returns empty array if no authentication context or no permissions.
106
- */
107
- readonly getPermissions: () => Effect.Effect<readonly string[]>;
108
-
109
46
  /**
110
47
  * Get the full authentication context if available.
111
48
  * Returns null if no authentication context is available.
@@ -123,49 +60,14 @@ export class AuthContextService extends Context.Tag("AuthContextService")<
123
60
  */
124
61
  export const AuthContextServiceLive = (
125
62
  authContext: AuthContext | null,
126
- ): Layer.Layer<AuthContextService> => {
127
- const permissions = authContext?.permissions ?? [];
128
-
129
- return Layer.succeed(AuthContextService, {
63
+ ): Layer.Layer<AuthContextService> =>
64
+ Layer.succeed(AuthContextService, {
130
65
  getClientId: () => Effect.succeed(authContext?.clientId ?? null),
131
-
132
66
  getMetadata: () => Effect.succeed(authContext?.metadata ?? {}),
133
-
134
67
  hasPermission: (permission: string) =>
135
- Effect.succeed(matchHasPermission(permissions, permission)),
136
-
137
- hasAnyPermission: (requiredPermissions: readonly string[]) =>
138
- Effect.succeed(matchHasAnyPermission(permissions, requiredPermissions)),
139
-
140
- requirePermission: (permission: string) =>
141
- Effect.gen(function* () {
142
- if (!authContext) {
143
- yield* Effect.logDebug(
144
- `[Auth] Permission check failed: authentication required for '${permission}'`,
145
- );
146
- return yield* Effect.fail(new AuthenticationRequiredError());
147
- }
148
- if (!matchHasPermission(permissions, permission)) {
149
- yield* Effect.logDebug(
150
- `[Auth] Permission denied: '${permission}' for client '${authContext.clientId}'`,
151
- );
152
- return yield* Effect.fail(new AuthorizationError(permission));
153
- }
154
- yield* Effect.logDebug(
155
- `[Auth] Permission granted: '${permission}' for client '${authContext.clientId}'`,
156
- );
157
- }),
158
-
159
- requireAuthentication: () =>
160
- authContext
161
- ? Effect.succeed(authContext)
162
- : Effect.fail(new AuthenticationRequiredError()),
163
-
164
- getPermissions: () => Effect.succeed(permissions),
165
-
68
+ Effect.succeed(authContext?.permissions?.includes(permission) ?? false),
166
69
  getAuthContext: () => Effect.succeed(authContext),
167
70
  });
168
- };
169
71
 
170
72
  /**
171
73
  * No-auth implementation of AuthContextService.