@uploadbox/nextjs 0.1.0

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 (57) hide show
  1. package/dist/analytics.d.ts +11 -0
  2. package/dist/analytics.d.ts.map +1 -0
  3. package/dist/analytics.js +50 -0
  4. package/dist/analytics.js.map +1 -0
  5. package/dist/auth.d.ts +4 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +46 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/create-hosted-handler.d.ts +17 -0
  10. package/dist/create-hosted-handler.d.ts.map +1 -0
  11. package/dist/create-hosted-handler.js +13 -0
  12. package/dist/create-hosted-handler.js.map +1 -0
  13. package/dist/create-route-handler.d.ts +7 -0
  14. package/dist/create-route-handler.d.ts.map +1 -0
  15. package/dist/create-route-handler.js +469 -0
  16. package/dist/create-route-handler.js.map +1 -0
  17. package/dist/extract-router-config.d.ts +3 -0
  18. package/dist/extract-router-config.d.ts.map +1 -0
  19. package/dist/extract-router-config.js +8 -0
  20. package/dist/extract-router-config.js.map +1 -0
  21. package/dist/hosted-hooks.d.ts +9 -0
  22. package/dist/hosted-hooks.d.ts.map +1 -0
  23. package/dist/hosted-hooks.js +105 -0
  24. package/dist/hosted-hooks.js.map +1 -0
  25. package/dist/index.d.ts +10 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +7 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/processing-pipeline.d.ts +3 -0
  30. package/dist/processing-pipeline.d.ts.map +1 -0
  31. package/dist/processing-pipeline.js +68 -0
  32. package/dist/processing-pipeline.js.map +1 -0
  33. package/dist/quota.d.ts +5 -0
  34. package/dist/quota.d.ts.map +1 -0
  35. package/dist/quota.js +68 -0
  36. package/dist/quota.js.map +1 -0
  37. package/dist/rate-limiter.d.ts +17 -0
  38. package/dist/rate-limiter.d.ts.map +1 -0
  39. package/dist/rate-limiter.js +47 -0
  40. package/dist/rate-limiter.js.map +1 -0
  41. package/dist/types.d.ts +104 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +2 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/webhooks.d.ts +7 -0
  46. package/dist/webhooks.d.ts.map +1 -0
  47. package/dist/webhooks.js +151 -0
  48. package/dist/webhooks.js.map +1 -0
  49. package/package.json +53 -0
  50. package/src/create-hosted-handler.ts +27 -0
  51. package/src/create-route-handler.ts +654 -0
  52. package/src/extract-router-config.ts +9 -0
  53. package/src/hosted-hooks.ts +132 -0
  54. package/src/index.ts +19 -0
  55. package/src/processing-pipeline.ts +77 -0
  56. package/src/rate-limiter.ts +64 -0
  57. package/src/types.ts +129 -0
@@ -0,0 +1,132 @@
1
+ import type { LifecycleHooks } from "./types.js";
2
+ import type { AuthContext } from "@uploadbox/core";
3
+
4
+ export interface HostedModeConfig {
5
+ /** URL of the Uploadbox platform, e.g. "https://uploadbox.dev" */
6
+ platformUrl: string;
7
+ /** Project ID on the platform */
8
+ projectId: string;
9
+ }
10
+
11
+ async function callPlatformHook(
12
+ config: HostedModeConfig,
13
+ hook: string,
14
+ payload: Record<string, unknown>
15
+ ): Promise<{ ok: boolean; data?: unknown; error?: string }> {
16
+ const url = `${config.platformUrl.replace(/\/$/, "")}/api/platform/hooks`;
17
+ const res = await fetch(url, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ "X-Uploadbox-Project-Id": config.projectId,
22
+ },
23
+ body: JSON.stringify({ hook, payload }),
24
+ });
25
+ return res.json() as Promise<{ ok: boolean; data?: unknown; error?: string }>;
26
+ }
27
+
28
+ function serializeDates(obj: Record<string, unknown>): Record<string, unknown> {
29
+ const result: Record<string, unknown> = {};
30
+ for (const [key, value] of Object.entries(obj)) {
31
+ if (value instanceof Date) {
32
+ result[key] = value.toISOString();
33
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
34
+ result[key] = serializeDates(value as Record<string, unknown>);
35
+ } else {
36
+ result[key] = value;
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+
42
+ export function createHostedHooks(config: HostedModeConfig): LifecycleHooks {
43
+ return {
44
+ onAuthenticate: async (request) => {
45
+ const authHeader = request.headers.get("authorization");
46
+ if (!authHeader) return undefined;
47
+
48
+ const result = await callPlatformHook(config, "authenticate", {
49
+ authorizationHeader: authHeader,
50
+ });
51
+
52
+ if (!result.ok) {
53
+ const { UploadboxError } = await import("@uploadbox/core");
54
+ throw UploadboxError.unauthorized(result.error ?? "Authentication failed");
55
+ }
56
+
57
+ return result.data as AuthContext;
58
+ },
59
+
60
+ onQuotaCheck: async ({ auth, totalSize }) => {
61
+ const result = await callPlatformHook(config, "quota-check", {
62
+ apiKeyId: auth?.apiKeyId,
63
+ totalSize,
64
+ });
65
+
66
+ if (!result.ok) {
67
+ const { UploadboxError } = await import("@uploadbox/core");
68
+ throw UploadboxError.quotaExceeded();
69
+ }
70
+ },
71
+
72
+ onUploadStarted: async (events) => {
73
+ callPlatformHook(config, "upload-started", {
74
+ events: events.map((e) => ({
75
+ ...serializeDates(e as unknown as Record<string, unknown>),
76
+ apiKeyId: e.auth?.apiKeyId,
77
+ })),
78
+ }).catch(console.error);
79
+ },
80
+
81
+ onFileVerified: async (fileKey) => {
82
+ const result = await callPlatformHook(config, "file-verified", {
83
+ key: fileKey,
84
+ });
85
+ if (!result.ok || !result.data) return null;
86
+ return result.data as any;
87
+ },
88
+
89
+ onUploadCompleted: async (event) => {
90
+ callPlatformHook(config, "upload-completed", {
91
+ file: event.file,
92
+ routeKey: event.routeKey,
93
+ apiKeyId: event.auth?.apiKeyId,
94
+ metadata: event.metadata,
95
+ }).catch(console.error);
96
+ },
97
+
98
+ onUploadFailed: async (event) => {
99
+ callPlatformHook(config, "upload-failed", {
100
+ key: event.key,
101
+ routeKey: event.routeKey,
102
+ apiKeyId: event.auth?.apiKeyId,
103
+ }).catch(console.error);
104
+ },
105
+
106
+ onMultipartStarted: async (event) => {
107
+ callPlatformHook(
108
+ config,
109
+ "multipart-started",
110
+ serializeDates({
111
+ ...event,
112
+ apiKeyId: event.auth?.apiKeyId,
113
+ } as unknown as Record<string, unknown>)
114
+ ).catch(console.error);
115
+ },
116
+
117
+ onMultipartCompleted: async (event) => {
118
+ callPlatformHook(config, "multipart-completed", {
119
+ key: event.key,
120
+ s3UploadId: event.s3UploadId,
121
+ parts: event.parts,
122
+ }).catch(console.error);
123
+ },
124
+
125
+ onMultipartAborted: async (event) => {
126
+ callPlatformHook(config, "multipart-aborted", {
127
+ key: event.key,
128
+ s3UploadId: event.s3UploadId,
129
+ }).catch(console.error);
130
+ },
131
+ };
132
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export { createRouteHandler } from "./create-route-handler.js";
2
+ export type {
3
+ RouteHandlerOpts,
4
+ LifecycleHooks,
5
+ UploadStartedEvent,
6
+ FileVerifiedData,
7
+ UploadCompletedEvent,
8
+ UploadFailedEvent,
9
+ MultipartStartedEvent,
10
+ MultipartCompletedEvent,
11
+ MultipartAbortedEvent,
12
+ } from "./types.js";
13
+ export { extractRouterConfig } from "./extract-router-config.js";
14
+ export { RateLimiter } from "./rate-limiter.js";
15
+ export type { RateLimitConfig } from "./rate-limiter.js";
16
+ export { runProcessingPipeline } from "./processing-pipeline.js";
17
+ export { createHostedHandler } from "./create-hosted-handler.js";
18
+ export { createHostedHooks } from "./hosted-hooks.js";
19
+ export type { HostedModeConfig } from "./hosted-hooks.js";
@@ -0,0 +1,77 @@
1
+ import type { ProcessingPipelineConfig, ProcessingContext, ProcessingHookResult, UploadedFileData, UploadboxConfig } from "@uploadbox/core";
2
+
3
+ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
4
+ return Promise.race([
5
+ promise,
6
+ new Promise<T>((_, reject) =>
7
+ setTimeout(() => reject(new Error(`Processing hook timed out after ${ms}ms`)), ms)
8
+ ),
9
+ ]);
10
+ }
11
+
12
+ export async function runProcessingPipeline(
13
+ config: ProcessingPipelineConfig,
14
+ file: UploadedFileData,
15
+ metadata: Record<string, unknown>,
16
+ s3Client: unknown,
17
+ uploadboxConfig: UploadboxConfig
18
+ ): Promise<ProcessingHookResult[]> {
19
+ const ctx: ProcessingContext = {
20
+ file,
21
+ metadata,
22
+ s3Client: s3Client as any,
23
+ config: uploadboxConfig,
24
+ };
25
+
26
+ const applicableHooks = config.hooks.filter((hook) => hook.shouldRun(ctx));
27
+
28
+ if (applicableHooks.length === 0) return [];
29
+
30
+ const results: ProcessingHookResult[] = [];
31
+
32
+ if (config.mode === "parallel") {
33
+ const promises = applicableHooks.map(async (hook) => {
34
+ try {
35
+ const timeout = hook.timeoutMs ?? 30000;
36
+ return await withTimeout(hook.run(ctx), timeout);
37
+ } catch (err) {
38
+ const result: ProcessingHookResult = {
39
+ success: false,
40
+ error: `Hook "${hook.name}" failed: ${(err as Error).message}`,
41
+ };
42
+ if (!config.continueOnError) throw err;
43
+ return result;
44
+ }
45
+ });
46
+
47
+ const settled = await Promise.allSettled(promises);
48
+ for (const result of settled) {
49
+ if (result.status === "fulfilled") {
50
+ results.push(result.value);
51
+ } else {
52
+ results.push({
53
+ success: false,
54
+ error: result.reason?.message ?? "Unknown error",
55
+ });
56
+ }
57
+ }
58
+ } else {
59
+ // Sequential (default)
60
+ for (const hook of applicableHooks) {
61
+ try {
62
+ const timeout = hook.timeoutMs ?? 30000;
63
+ const result = await withTimeout(hook.run(ctx), timeout);
64
+ results.push(result);
65
+ } catch (err) {
66
+ const result: ProcessingHookResult = {
67
+ success: false,
68
+ error: `Hook "${hook.name}" failed: ${(err as Error).message}`,
69
+ };
70
+ results.push(result);
71
+ if (!config.continueOnError) break;
72
+ }
73
+ }
74
+ }
75
+
76
+ return results;
77
+ }
@@ -0,0 +1,64 @@
1
+ export interface RateLimitConfig {
2
+ requestsPerMinute?: number;
3
+ uploadsPerHour?: number;
4
+ }
5
+
6
+ interface SlidingWindow {
7
+ timestamps: number[];
8
+ }
9
+
10
+ export class RateLimiter {
11
+ private windows = new Map<string, SlidingWindow>();
12
+ private config: RateLimitConfig;
13
+ private cleanupInterval: ReturnType<typeof setInterval>;
14
+
15
+ constructor(config: RateLimitConfig) {
16
+ this.config = config;
17
+ // Cleanup stale entries every 5 minutes
18
+ this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000);
19
+ }
20
+
21
+ check(key: string, type: "request" | "upload"): { allowed: boolean; retryAfter: number } {
22
+ const limit = type === "request" ? this.config.requestsPerMinute : this.config.uploadsPerHour;
23
+ if (!limit) return { allowed: true, retryAfter: 0 };
24
+
25
+ const windowMs = type === "request" ? 60_000 : 3_600_000;
26
+ const windowKey = `${key}:${type}`;
27
+ const now = Date.now();
28
+
29
+ let window = this.windows.get(windowKey);
30
+ if (!window) {
31
+ window = { timestamps: [] };
32
+ this.windows.set(windowKey, window);
33
+ }
34
+
35
+ // Remove expired timestamps
36
+ const cutoff = now - windowMs;
37
+ window.timestamps = window.timestamps.filter((t) => t > cutoff);
38
+
39
+ if (window.timestamps.length >= limit) {
40
+ const oldestInWindow = window.timestamps[0]!;
41
+ const retryAfter = Math.ceil((oldestInWindow + windowMs - now) / 1000);
42
+ return { allowed: false, retryAfter };
43
+ }
44
+
45
+ window.timestamps.push(now);
46
+ return { allowed: true, retryAfter: 0 };
47
+ }
48
+
49
+ private cleanup(): void {
50
+ const now = Date.now();
51
+ const maxWindow = 3_600_000; // 1 hour
52
+
53
+ for (const [key, window] of this.windows.entries()) {
54
+ window.timestamps = window.timestamps.filter((t) => t > now - maxWindow);
55
+ if (window.timestamps.length === 0) {
56
+ this.windows.delete(key);
57
+ }
58
+ }
59
+ }
60
+
61
+ destroy(): void {
62
+ clearInterval(this.cleanupInterval);
63
+ }
64
+ }
package/src/types.ts ADDED
@@ -0,0 +1,129 @@
1
+ import type {
2
+ AuthContext,
3
+ UploadedFileData,
4
+ FileRouter,
5
+ UploadboxConfig,
6
+ CompletedPartInfo,
7
+ } from "@uploadbox/core";
8
+ import type { ProcessingPipelineConfig } from "@uploadbox/core";
9
+ import type { RateLimitConfig } from "./rate-limiter.js";
10
+
11
+ // ---------- Event types ----------
12
+
13
+ export interface UploadStartedEvent {
14
+ key: string;
15
+ name: string;
16
+ size: number;
17
+ type: string;
18
+ url: string;
19
+ routeKey: string;
20
+ auth?: AuthContext;
21
+ metadata: Record<string, unknown>;
22
+ customMetadata?: Record<string, string> | null;
23
+ expiresAt: Date | null;
24
+ }
25
+
26
+ export interface FileVerifiedData {
27
+ key: string;
28
+ name: string;
29
+ size: number;
30
+ type: string;
31
+ url: string;
32
+ acl: string;
33
+ metadata: Record<string, unknown>;
34
+ customMetadata?: Record<string, string>;
35
+ }
36
+
37
+ export interface UploadCompletedEvent {
38
+ file: UploadedFileData;
39
+ routeKey: string;
40
+ auth?: AuthContext;
41
+ metadata: Record<string, unknown>;
42
+ serverData: unknown;
43
+ }
44
+
45
+ export interface UploadFailedEvent {
46
+ key: string;
47
+ routeKey: string;
48
+ auth?: AuthContext;
49
+ }
50
+
51
+ export interface MultipartStartedEvent {
52
+ key: string;
53
+ name: string;
54
+ size: number;
55
+ type: string;
56
+ url: string;
57
+ routeKey: string;
58
+ s3UploadId: string;
59
+ bucket: string;
60
+ totalParts: number;
61
+ partSize: number;
62
+ auth?: AuthContext;
63
+ metadata: Record<string, unknown>;
64
+ customMetadata?: Record<string, string> | null;
65
+ expiresAt: Date | null;
66
+ uploadExpiresAt: Date;
67
+ }
68
+
69
+ export interface MultipartCompletedEvent {
70
+ key: string;
71
+ s3UploadId: string;
72
+ routeKey: string;
73
+ auth?: AuthContext;
74
+ parts: CompletedPartInfo[];
75
+ }
76
+
77
+ export interface MultipartAbortedEvent {
78
+ key: string;
79
+ s3UploadId: string;
80
+ }
81
+
82
+ // ---------- Lifecycle hooks ----------
83
+
84
+ export interface LifecycleHooks {
85
+ /** Custom authentication. Return AuthContext or undefined for anonymous. Throw UploadboxError to deny. */
86
+ onAuthenticate?: (request: Request) => Promise<AuthContext | undefined>;
87
+
88
+ /** Quota check before upload. Throw UploadboxError.quotaExceeded() to deny. */
89
+ onQuotaCheck?: (ctx: { auth?: AuthContext; totalSize: number; fileCount: number }) => Promise<void>;
90
+
91
+ /** Called after presigned URLs are generated (fire-and-forget). */
92
+ onUploadStarted?: (events: UploadStartedEvent[]) => Promise<void>;
93
+
94
+ /** Fallback to look up file data when in-memory state is lost (e.g. multi-instance). */
95
+ onFileVerified?: (fileKey: string) => Promise<FileVerifiedData | null>;
96
+
97
+ /** Called after onUploadComplete runs successfully (fire-and-forget). */
98
+ onUploadCompleted?: (event: UploadCompletedEvent) => Promise<void>;
99
+
100
+ /** Called when an upload fails (fire-and-forget). */
101
+ onUploadFailed?: (event: UploadFailedEvent) => Promise<void>;
102
+
103
+ /** Called when multipart upload is created (fire-and-forget). */
104
+ onMultipartStarted?: (event: MultipartStartedEvent) => Promise<void>;
105
+
106
+ /** Called when multipart upload completes (fire-and-forget). */
107
+ onMultipartCompleted?: (event: MultipartCompletedEvent) => Promise<void>;
108
+
109
+ /** Called when multipart upload is aborted (fire-and-forget). */
110
+ onMultipartAborted?: (event: MultipartAbortedEvent) => Promise<void>;
111
+
112
+ /** Resolve per-request S3 config (e.g. per-region or BYOB). Return null to use default. */
113
+ onResolveConfig?: (auth?: AuthContext) => Promise<{
114
+ config: UploadboxConfig;
115
+ keyPrefix?: string;
116
+ } | null>;
117
+ }
118
+
119
+ // ---------- Route handler options ----------
120
+
121
+ export interface RouteHandlerOpts<TRouter extends FileRouter> {
122
+ router: TRouter;
123
+ config?: Partial<UploadboxConfig>;
124
+ hooks?: LifecycleHooks;
125
+ rateLimit?: RateLimitConfig;
126
+ processing?: ProcessingPipelineConfig;
127
+ /** Custom S3 client factory (e.g. for caching). Must return an S3Client instance. */
128
+ s3ClientFactory?: (config: UploadboxConfig) => unknown;
129
+ }