@uploadista/server 0.0.3

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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +503 -0
  5. package/dist/auth/cache.d.ts +87 -0
  6. package/dist/auth/cache.d.ts.map +1 -0
  7. package/dist/auth/cache.js +121 -0
  8. package/dist/auth/cache.test.d.ts +2 -0
  9. package/dist/auth/cache.test.d.ts.map +1 -0
  10. package/dist/auth/cache.test.js +209 -0
  11. package/dist/auth/get-auth-credentials.d.ts +73 -0
  12. package/dist/auth/get-auth-credentials.d.ts.map +1 -0
  13. package/dist/auth/get-auth-credentials.js +55 -0
  14. package/dist/auth/index.d.ts +2 -0
  15. package/dist/auth/index.d.ts.map +1 -0
  16. package/dist/auth/index.js +1 -0
  17. package/dist/auth/jwt/index.d.ts +38 -0
  18. package/dist/auth/jwt/index.d.ts.map +1 -0
  19. package/dist/auth/jwt/index.js +36 -0
  20. package/dist/auth/jwt/types.d.ts +77 -0
  21. package/dist/auth/jwt/types.d.ts.map +1 -0
  22. package/dist/auth/jwt/types.js +1 -0
  23. package/dist/auth/jwt/validate.d.ts +58 -0
  24. package/dist/auth/jwt/validate.d.ts.map +1 -0
  25. package/dist/auth/jwt/validate.js +226 -0
  26. package/dist/auth/jwt/validate.test.d.ts +2 -0
  27. package/dist/auth/jwt/validate.test.d.ts.map +1 -0
  28. package/dist/auth/jwt/validate.test.js +492 -0
  29. package/dist/auth/service.d.ts +63 -0
  30. package/dist/auth/service.d.ts.map +1 -0
  31. package/dist/auth/service.js +43 -0
  32. package/dist/auth/service.test.d.ts +2 -0
  33. package/dist/auth/service.test.d.ts.map +1 -0
  34. package/dist/auth/service.test.js +195 -0
  35. package/dist/auth/types.d.ts +38 -0
  36. package/dist/auth/types.d.ts.map +1 -0
  37. package/dist/auth/types.js +1 -0
  38. package/dist/cache.d.ts +87 -0
  39. package/dist/cache.d.ts.map +1 -0
  40. package/dist/cache.js +121 -0
  41. package/dist/cache.test.d.ts +2 -0
  42. package/dist/cache.test.d.ts.map +1 -0
  43. package/dist/cache.test.js +209 -0
  44. package/dist/cloudflare-config.d.ts +72 -0
  45. package/dist/cloudflare-config.d.ts.map +1 -0
  46. package/dist/cloudflare-config.js +67 -0
  47. package/dist/error-types.d.ts +138 -0
  48. package/dist/error-types.d.ts.map +1 -0
  49. package/dist/error-types.js +155 -0
  50. package/dist/hono-adapter.d.ts +48 -0
  51. package/dist/hono-adapter.d.ts.map +1 -0
  52. package/dist/hono-adapter.js +58 -0
  53. package/dist/http-utils.d.ts +148 -0
  54. package/dist/http-utils.d.ts.map +1 -0
  55. package/dist/http-utils.js +233 -0
  56. package/dist/index.d.ts +9 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +8 -0
  59. package/dist/layer-utils.d.ts +121 -0
  60. package/dist/layer-utils.d.ts.map +1 -0
  61. package/dist/layer-utils.js +80 -0
  62. package/dist/metrics/service.d.ts +26 -0
  63. package/dist/metrics/service.d.ts.map +1 -0
  64. package/dist/metrics/service.js +20 -0
  65. package/dist/plugins-typing.d.ts +11 -0
  66. package/dist/plugins-typing.d.ts.map +1 -0
  67. package/dist/plugins-typing.js +1 -0
  68. package/dist/service.d.ts +63 -0
  69. package/dist/service.d.ts.map +1 -0
  70. package/dist/service.js +43 -0
  71. package/dist/service.test.d.ts +2 -0
  72. package/dist/service.test.d.ts.map +1 -0
  73. package/dist/service.test.js +195 -0
  74. package/dist/types.d.ts +38 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +1 -0
  77. package/package.json +47 -0
  78. package/src/auth/get-auth-credentials.ts +97 -0
  79. package/src/auth/index.ts +1 -0
  80. package/src/cache.test.ts +306 -0
  81. package/src/cache.ts +204 -0
  82. package/src/error-types.ts +172 -0
  83. package/src/http-utils.ts +264 -0
  84. package/src/index.ts +8 -0
  85. package/src/layer-utils.ts +184 -0
  86. package/src/plugins-typing.ts +57 -0
  87. package/src/service.test.ts +275 -0
  88. package/src/service.ts +78 -0
  89. package/src/types.ts +40 -0
  90. package/tsconfig.json +13 -0
  91. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Shared HTTP utilities for server adapters
3
+ *
4
+ * This module provides routing and error handling utilities used across
5
+ * Hono, Express, and Fastify adapters for request parsing and response formatting.
6
+ */
7
+
8
+ /**
9
+ * Parses URL segments from a pathname, filtering out empty segments.
10
+ * Useful for extracting route components from request paths.
11
+ *
12
+ * @param pathname - The URL pathname (e.g., "/uploadista/api/upload/abc123")
13
+ * @returns Array of non-empty path segments
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const segments = parseUrlSegments("/uploadista/api/upload/abc123");
18
+ * // => ["uploadista", "api", "upload", "abc123"]
19
+ * ```
20
+ */
21
+ export const parseUrlSegments = (pathname: string): string[] => {
22
+ return pathname.split("/").filter(Boolean);
23
+ };
24
+
25
+ /**
26
+ * Extracts the last segment from a URL pathname.
27
+ *
28
+ * @param pathname - The URL pathname to parse
29
+ * @returns The last non-empty segment, or undefined if none exists
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const id = getLastSegment("/uploadista/api/upload/abc123");
34
+ * // => "abc123"
35
+ * ```
36
+ */
37
+ export const getLastSegment = (pathname: string): string | undefined => {
38
+ const segments = parseUrlSegments(pathname);
39
+ return segments[segments.length - 1];
40
+ };
41
+
42
+ /**
43
+ * Checks if a pathname includes a specific base path and API prefix.
44
+ * Used to determine if a request should be handled by the Uploadista adapter.
45
+ *
46
+ * @param pathname - The request pathname
47
+ * @param basePath - The base path configured for the adapter (e.g., "uploadista")
48
+ * @returns true if the path includes `{basePath}/api/`
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const isUploadistaPath = hasBasePath("/uploadista/api/upload", "uploadista");
53
+ * // => true
54
+ * ```
55
+ */
56
+ export const hasBasePath = (pathname: string, basePath: string): boolean => {
57
+ return pathname.includes(`${basePath}/api/`);
58
+ };
59
+
60
+ /**
61
+ * Removes the base path prefix and returns clean route segments.
62
+ * Transforms "/uploadista/api/upload/abc123" → ["upload", "abc123"]
63
+ *
64
+ * @param pathname - The full request pathname
65
+ * @param basePath - The base path to remove (e.g., "uploadista")
66
+ * @returns Array of route segments without base path prefix
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const route = getRouteSegments("/uploadista/api/upload/abc123", "uploadista");
71
+ * // => ["upload", "abc123"]
72
+ * ```
73
+ */
74
+ export const getRouteSegments = (
75
+ pathname: string,
76
+ basePath: string,
77
+ ): string[] => {
78
+ return pathname.replace(`${basePath}/api/`, "").split("/").filter(Boolean);
79
+ };
80
+
81
+ /**
82
+ * Standard error handler for flow and job operations.
83
+ * Maps application errors to appropriate HTTP status codes and error formats.
84
+ *
85
+ * Supports errors with `code`, `message`, `status`, and `details` properties.
86
+ * Maps error codes to HTTP status codes (e.g., NOT_FOUND → 404, VALIDATION_ERROR → 400).
87
+ *
88
+ * @param error - The error object to handle (can be any type)
89
+ * @returns Standardized error response with status, code, message, and optional details
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { handleFlowError } from "@uploadista/server";
94
+ *
95
+ * const response = handleFlowError({
96
+ * code: "FLOW_JOB_NOT_FOUND",
97
+ * message: "Job not found",
98
+ * });
99
+ * // => { status: 404, code: "FLOW_JOB_NOT_FOUND", message: "Job not found" }
100
+ * ```
101
+ */
102
+ export const handleFlowError = (
103
+ error: unknown,
104
+ ): { status: number; code: string; message: string; details?: unknown } => {
105
+ let status = 500;
106
+ let code = "UNKNOWN_ERROR";
107
+ let message = "Internal server error";
108
+ let details: unknown;
109
+
110
+ if (typeof error === "object" && error !== null) {
111
+ const errorObj = error as Record<string, unknown>;
112
+
113
+ // Extract error code
114
+ if ("code" in errorObj && typeof errorObj.code === "string") {
115
+ code = errorObj.code;
116
+ }
117
+
118
+ // Extract message
119
+ if ("message" in errorObj && typeof errorObj.message === "string") {
120
+ message = errorObj.message;
121
+ } else if ("body" in errorObj && typeof errorObj.body === "string") {
122
+ // Support UploadistaError's body property
123
+ message = errorObj.body;
124
+ }
125
+
126
+ // Extract details if present
127
+ if ("details" in errorObj) {
128
+ details = errorObj.details;
129
+ }
130
+
131
+ // Map error codes to HTTP status codes
132
+ if ("status" in errorObj && typeof errorObj.status === "number") {
133
+ status = errorObj.status;
134
+ } else if ("code" in errorObj) {
135
+ // Fallback: derive status from common error codes
136
+ switch (errorObj.code) {
137
+ case "FILE_NOT_FOUND":
138
+ case "FLOW_JOB_NOT_FOUND":
139
+ case "UPLOAD_ID_NOT_FOUND":
140
+ status = 404;
141
+ break;
142
+ case "FLOW_JOB_ERROR":
143
+ case "VALIDATION_ERROR":
144
+ case "INVALID_METADATA":
145
+ case "INVALID_LENGTH":
146
+ case "ABORTED":
147
+ case "INVALID_TERMINATION":
148
+ status = 400;
149
+ break;
150
+ case "INVALID_OFFSET":
151
+ status = 409;
152
+ break;
153
+ case "ERR_SIZE_EXCEEDED":
154
+ case "ERR_MAX_SIZE_EXCEEDED":
155
+ status = 413;
156
+ break;
157
+ case "FILE_NO_LONGER_EXISTS":
158
+ status = 410;
159
+ break;
160
+ case "MISSING_OFFSET":
161
+ case "INVALID_CONTENT_TYPE":
162
+ status = 403;
163
+ break;
164
+ default:
165
+ status = 500;
166
+ }
167
+ }
168
+
169
+ // Special handling for specific error messages
170
+ if ("message" in errorObj && errorObj.message === "Invalid JSON body") {
171
+ status = 400;
172
+ code = "VALIDATION_ERROR";
173
+ }
174
+ }
175
+
176
+ const result: {
177
+ status: number;
178
+ code: string;
179
+ message: string;
180
+ details?: unknown;
181
+ } = {
182
+ status,
183
+ code,
184
+ message,
185
+ };
186
+
187
+ if (details !== undefined) {
188
+ result.details = details;
189
+ }
190
+
191
+ return result;
192
+ };
193
+
194
+ /**
195
+ * Extracts job ID from URL segments for job status endpoint.
196
+ * Expected URL format: `/uploadista/api/jobs/:jobId/status`
197
+ *
198
+ * @param urlSegments - Parsed URL segments (without base path)
199
+ * @returns The job ID if found, or undefined
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const jobId = extractJobIdFromStatus(["jobs", "job-123", "status"]);
204
+ * // => "job-123"
205
+ * ```
206
+ */
207
+ export const extractJobIdFromStatus = (
208
+ urlSegments: string[],
209
+ ): string | undefined => {
210
+ return urlSegments[urlSegments.length - 2];
211
+ };
212
+
213
+ /**
214
+ * Extracts job ID and node ID from URL segments for continue flow endpoint.
215
+ * Expected URL format: `/uploadista/api/jobs/:jobId/continue/:nodeId`
216
+ *
217
+ * @param urlSegments - Parsed URL segments (without base path)
218
+ * @returns Object with extracted jobId and nodeId (either can be undefined if not found)
219
+ *
220
+ * @example
221
+ * ```typescript
222
+ * const { jobId, nodeId } = extractJobAndNodeId([
223
+ * "jobs",
224
+ * "job-123",
225
+ * "continue",
226
+ * "node-456",
227
+ * ]);
228
+ * // => { jobId: "job-123", nodeId: "node-456" }
229
+ * ```
230
+ */
231
+ export const extractJobAndNodeId = (
232
+ urlSegments: string[],
233
+ ): { jobId: string | undefined; nodeId: string | undefined } => {
234
+ return {
235
+ jobId: urlSegments[urlSegments.length - 3],
236
+ nodeId: urlSegments[urlSegments.length - 1],
237
+ };
238
+ };
239
+
240
+ /**
241
+ * Extracts flow ID and storage ID from URL segments.
242
+ * Expected URL format: `/uploadista/api/flow/:flowId/:storageId`
243
+ *
244
+ * Mutates the input array (removes last 2 elements).
245
+ *
246
+ * @param urlSegments - Parsed URL segments (will be mutated)
247
+ * @returns Object with extracted flowId and storageId
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const segments = ["flow", "flow-123", "storage-456"];
252
+ * const { flowId, storageId } = extractFlowAndStorageId(segments);
253
+ * // => { flowId: "flow-123", storageId: "storage-456" }
254
+ * // segments is now ["flow"]
255
+ * ```
256
+ */
257
+ export const extractFlowAndStorageId = (
258
+ urlSegments: string[],
259
+ ): { flowId: string | undefined; storageId: string | undefined } => {
260
+ return {
261
+ storageId: urlSegments.pop(),
262
+ flowId: urlSegments.pop(),
263
+ };
264
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./auth";
2
+ export * from "./cache";
3
+ export * from "./error-types";
4
+ export * from "./http-utils";
5
+ export * from "./layer-utils";
6
+ export * from "./plugins-typing";
7
+ export * from "./service";
8
+ export * from "./types";
@@ -0,0 +1,184 @@
1
+ import type { FlowProvider } from "@uploadista/core/flow";
2
+ import { flowServer } from "@uploadista/core/flow";
3
+ import {
4
+ type BaseEventEmitterService,
5
+ type BaseKvStoreService,
6
+ flowEventEmitter,
7
+ flowJobKvStore,
8
+ type UploadFileDataStore,
9
+ type UploadFileDataStores,
10
+ type UploadFileKVStore,
11
+ uploadEventEmitter,
12
+ uploadFileKvStore,
13
+ } from "@uploadista/core/types";
14
+ import { type UploadServer, uploadServer } from "@uploadista/core/upload";
15
+ import type { GenerateId } from "@uploadista/core/utils";
16
+ import { Layer } from "effect";
17
+
18
+ /**
19
+ * Configuration for creating upload server layers.
20
+ * Specifies all dependencies needed by the upload server Effect Layer.
21
+ *
22
+ * @property kvStore - Key-value store for upload metadata
23
+ * @property eventEmitter - Event emitter for upload progress events
24
+ * @property dataStore - File data storage implementation
25
+ * @property bufferedDataStore - Optional buffered storage for performance optimization
26
+ * @property generateId - Optional custom ID generator (uses default if omitted)
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { createUploadServerLayer } from "@uploadista/server";
31
+ *
32
+ * const uploadLayerConfig: UploadServerLayerConfig = {
33
+ * kvStore: redisKvStore,
34
+ * eventEmitter: webSocketEventEmitter,
35
+ * dataStore: s3DataStore,
36
+ * };
37
+ * ```
38
+ */
39
+ export interface UploadServerLayerConfig {
40
+ kvStore: Layer.Layer<BaseKvStoreService>;
41
+ eventEmitter: Layer.Layer<BaseEventEmitterService>;
42
+ dataStore: Layer.Layer<UploadFileDataStores, never, UploadFileKVStore>;
43
+ bufferedDataStore?: Layer.Layer<
44
+ UploadFileDataStore,
45
+ never,
46
+ UploadFileKVStore
47
+ >;
48
+ generateId?: Layer.Layer<GenerateId>;
49
+ }
50
+
51
+ /**
52
+ * Configuration for creating flow server layers.
53
+ * Specifies all dependencies needed by the flow processing server.
54
+ *
55
+ * @property kvStore - Key-value store for flow job metadata
56
+ * @property eventEmitter - Event emitter for flow progress events
57
+ * @property flowProvider - Factory function for creating flows
58
+ * @property uploadServer - Upload server layer (used by flows for uploads)
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * import { createFlowServerLayer } from "@uploadista/server";
63
+ *
64
+ * const flowLayerConfig: FlowServerLayerConfig = {
65
+ * kvStore: redisKvStore,
66
+ * eventEmitter: webSocketEventEmitter,
67
+ * flowProvider: createFlowsEffect,
68
+ * uploadServer: uploadServerLayer,
69
+ * };
70
+ * ```
71
+ */
72
+ export interface FlowServerLayerConfig {
73
+ kvStore: Layer.Layer<BaseKvStoreService>;
74
+ eventEmitter: Layer.Layer<BaseEventEmitterService>;
75
+ flowProvider: Layer.Layer<FlowProvider>;
76
+ uploadServer: Layer.Layer<UploadServer>;
77
+ }
78
+
79
+ /**
80
+ * Creates the upload server layer with all dependencies composed.
81
+ * This layer handles file uploads with chunked transfer, resumption, and metadata tracking.
82
+ *
83
+ * The created layer includes:
84
+ * - Upload KV store (metadata tracking)
85
+ * - Data store (file storage)
86
+ * - Event emitter (progress notifications)
87
+ * - Optional buffered data store (performance optimization)
88
+ * - Optional custom ID generator
89
+ *
90
+ * @param config - Upload server layer configuration
91
+ * @returns Effect Layer providing UploadServer
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * import { createUploadServerLayer } from "@uploadista/server";
96
+ * import { Layer } from "effect";
97
+ *
98
+ * const uploadServerLayer = createUploadServerLayer({
99
+ * kvStore: redisKvStore,
100
+ * eventEmitter: webSocketEventEmitter,
101
+ * dataStore: s3DataStore,
102
+ * });
103
+ *
104
+ * // Use in application
105
+ * const app = Layer.provide(appLogic, uploadServerLayer);
106
+ * ```
107
+ */
108
+ export const createUploadServerLayer = ({
109
+ kvStore,
110
+ eventEmitter,
111
+ dataStore,
112
+ bufferedDataStore,
113
+ generateId,
114
+ }: UploadServerLayerConfig) => {
115
+ // Set up upload server dependencies
116
+ const uploadFileKVStoreLayer = Layer.provide(uploadFileKvStore, kvStore);
117
+ const uploadDataStoreLayer = Layer.provide(dataStore, uploadFileKVStoreLayer);
118
+ const uploadBufferedDataStoreLayer = bufferedDataStore
119
+ ? Layer.provide(bufferedDataStore, uploadFileKVStoreLayer)
120
+ : Layer.empty;
121
+ const uploadEventEmitterLayer = Layer.provide(
122
+ uploadEventEmitter,
123
+ eventEmitter,
124
+ );
125
+
126
+ const uploadServerLayers = Layer.mergeAll(
127
+ uploadDataStoreLayer,
128
+ uploadFileKVStoreLayer,
129
+ uploadEventEmitterLayer,
130
+ ...(generateId ? [generateId] : []),
131
+ uploadBufferedDataStoreLayer,
132
+ );
133
+
134
+ return Layer.provide(uploadServer, uploadServerLayers);
135
+ };
136
+
137
+ /**
138
+ * Creates the flow server layer with all dependencies composed.
139
+ * This layer handles file processing workflows with multi-stage pipelines.
140
+ *
141
+ * The created layer includes:
142
+ * - Flow job KV store (job metadata and state)
143
+ * - Event emitter (progress notifications)
144
+ * - Flow provider (flow definitions)
145
+ * - Upload server (for uploads within flows)
146
+ *
147
+ * @param config - Flow server layer configuration
148
+ * @returns Effect Layer providing FlowServer
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * import { createFlowServerLayer } from "@uploadista/server";
153
+ * import { Layer } from "effect";
154
+ *
155
+ * const flowServerLayer = createFlowServerLayer({
156
+ * kvStore: redisKvStore,
157
+ * eventEmitter: webSocketEventEmitter,
158
+ * flowProvider: createFlowsEffect,
159
+ * uploadServer: uploadServerLayer,
160
+ * });
161
+ *
162
+ * // Use in application
163
+ * const app = Layer.provide(appLogic, flowServerLayer);
164
+ * ```
165
+ */
166
+ export const createFlowServerLayer = ({
167
+ kvStore,
168
+ eventEmitter,
169
+ flowProvider,
170
+ uploadServer,
171
+ }: FlowServerLayerConfig) => {
172
+ // Set up flow server dependencies
173
+ const flowJobKVStoreLayer = Layer.provide(flowJobKvStore, kvStore);
174
+ const flowEventEmitterLayer = Layer.provide(flowEventEmitter, eventEmitter);
175
+
176
+ const flowServerLayers = Layer.mergeAll(
177
+ flowProvider,
178
+ flowEventEmitterLayer,
179
+ flowJobKVStoreLayer,
180
+ uploadServer,
181
+ );
182
+
183
+ return Layer.provide(flowServer, flowServerLayers);
184
+ };
@@ -0,0 +1,57 @@
1
+ import type { Flow, UploadServer } from "@uploadista/core";
2
+ import type { Effect, Layer } from "effect";
3
+ import type z from "zod";
4
+
5
+ export type LayerSuccessUnion<
6
+ Layers extends readonly Layer.Layer<any, never, never>[],
7
+ > = Layers[number] extends Layer.Layer<infer Success, never, never>
8
+ ? Success
9
+ : never;
10
+
11
+ export type FlowSuccess<
12
+ TFlows extends (
13
+ flowId: string,
14
+ clientId: string | null,
15
+ ) => Effect.Effect<unknown, unknown, unknown>,
16
+ > = ReturnType<TFlows> extends Effect.Effect<infer Success, unknown, unknown>
17
+ ? Success
18
+ : never;
19
+
20
+ export type FlowRequirementsOf<
21
+ TFlows extends (
22
+ flowId: string,
23
+ clientId: string | null,
24
+ ) => Effect.Effect<unknown, unknown, unknown>,
25
+ > = FlowSuccess<TFlows> extends Flow<
26
+ z.ZodSchema<unknown>,
27
+ z.ZodSchema<unknown>,
28
+ infer R
29
+ >
30
+ ? Exclude<R, UploadServer>
31
+ : never;
32
+
33
+ export type RequiredPluginsOf<
34
+ TFlows extends (
35
+ flowId: string,
36
+ clientId: string | null,
37
+ ) => Effect.Effect<unknown, unknown, unknown>,
38
+ > = Exclude<FlowRequirementsOf<TFlows>, UploadServer>;
39
+
40
+ export type PluginAssertion<
41
+ TFlows extends (
42
+ flowId: string,
43
+ clientId: string | null,
44
+ ) => Effect.Effect<unknown, unknown, unknown>,
45
+ // biome-ignore lint/suspicious/noExplicitAny: Permissive constraint allows plugin tuples where each plugin provides subset of requirements
46
+ TPlugins extends readonly Layer.Layer<any, never, never>[],
47
+ > = Exclude<
48
+ RequiredPluginsOf<TFlows>,
49
+ LayerSuccessUnion<TPlugins>
50
+ > extends never
51
+ ? unknown
52
+ : {
53
+ __missingPlugins: Exclude<
54
+ RequiredPluginsOf<TFlows>,
55
+ LayerSuccessUnion<TPlugins>
56
+ >;
57
+ };