@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
package/src/core/server.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
586
|
-
|
|
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";
|
package/src/plugins-typing.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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.
|