@rebasepro/server 0.23.0 → 0.24.1-canary.g56af4a4
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 +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/GCSStorageController-DivE9bAx.js +402 -0
- package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
- package/dist/S3StorageController-D_vKs4kP.js +480 -0
- package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +92 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +38 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +97 -11
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +9 -7
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +28 -1
- package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
- package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
- package/dist/auth/access.d.ts +121 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +16 -1
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +2 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +46 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +71 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +120 -59
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +54 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +54 -1
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +11 -6
- package/dist/auth/token-revocation.d.ts +50 -1
- package/dist/auth/user-picking.d.ts +35 -0
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
- package/dist/auth-OPq_FeMs.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +24 -3
- package/dist/backup/backup-routes.d.ts +46 -5
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +16 -0
- package/dist/backup/index.d.ts +5 -1
- package/dist/backup-5d7XhLX3.js +581 -0
- package/dist/backup-5d7XhLX3.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +56 -4
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/static-routing.d.ts +65 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
- package/dist/contract-routes-02lASyJI.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +94 -23
- package/dist/cron/cron-store.d.ts +31 -4
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
- package/dist/cron-loader-D7OmleAb.js.map +1 -0
- package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
- package/dist/cron-routes-CBfiaqEL.js.map +1 -0
- package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
- package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
- package/dist/cron-store-B2Ih0gCN.js +540 -0
- package/dist/cron-store-B2Ih0gCN.js.map +1 -0
- package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
- package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/types.d.ts +44 -0
- package/dist/env.d.ts +1 -39
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/filter-dialect-REl-TjZr.js +1628 -0
- package/dist/filter-dialect-REl-TjZr.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
- package/dist/function-loader-BDKKiBmy.js.map +1 -0
- package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
- package/dist/function-routes-DWB7BBsi.js.map +1 -0
- package/dist/functions/context.d.ts +26 -6
- package/dist/functions/define-function.d.ts +7 -4
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +114 -37
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
- package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
- package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.es.js +23363 -17552
- package/dist/index.es.js.map +1 -1
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +20 -0
- package/dist/init.d.ts +70 -6
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
- package/dist/jobs-BpD4Q0qR.js.map +1 -0
- package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
- package/dist/jwt-Bqcu7cGt.js.map +1 -0
- package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
- package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/load-failure-UvzZgGXT.js +48 -0
- package/dist/load-failure-UvzZgGXT.js.map +1 -0
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +7 -0
- package/dist/mcp/mcp-tools.d.ts +23 -9
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +7 -1
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
- package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
- package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
- package/dist/query-parser-DK9eMkb9.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +73 -3
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +20 -5
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
- package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +16 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/soft-delete-params-47yBGs3_.js +668 -0
- package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
- package/dist/sql-export-routes-BNNuGGLP.js +240 -0
- package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
- package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +48 -2
- package/dist/storage/LocalStorageController.d.ts +69 -2
- package/dist/storage/S3StorageController.d.ts +39 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-keys.d.ts +15 -0
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +52 -14
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +76 -7
- package/dist/storage/types.d.ts +101 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-CjrA4PMo.js +0 -242
- package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
- package/dist/S3StorageController-B6pKDNVj.js +0 -264
- package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-B-GIMpDG.js.map +0 -1
- package/dist/backup-D7YR94N3.js +0 -253
- package/dist/backup-D7YR94N3.js.map +0 -1
- package/dist/contract-routes-CbFjuBwa.js.map +0 -1
- package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
- package/dist/cron-routes-eE8nif_b.js.map +0 -1
- package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
- package/dist/cron-store-TcoGz-xS.js +0 -305
- package/dist/cron-store-TcoGz-xS.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-loader-xnbDAPfa.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/jobs-CazMYhyy.js.map +0 -1
- package/dist/jwt-DnQHNFCl.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
- package/dist/openapi-generator-O_O24MAT.js.map +0 -1
- package/dist/query-parser-DGRVFNM3.js.map +0 -1
- package/dist/request-timeout-C_4C2BeR.js.map +0 -1
- package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
- package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
- package/dist/src-pmvW7BFx.js +0 -6688
- package/dist/src-pmvW7BFx.js.map +0 -1
- package/dist/src-vkcwKXbT.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
package/dist/storage/routes.d.ts
CHANGED
|
@@ -10,20 +10,35 @@ import { Hono } from "hono";
|
|
|
10
10
|
import { StorageController, type StorageAuthorize, type StorageAuthorizeData } from "./types.js";
|
|
11
11
|
import { type StorageRegistry } from "./storage-registry.js";
|
|
12
12
|
import { type StorageSourceDefinition, type AuthAdapter } from "@rebasepro/types";
|
|
13
|
-
import { type ResolveUploadConstraints } from "./property-limits.js";
|
|
13
|
+
import { type ResolveUploadConstraints, type ResolveUploadPathConstraints } from "./property-limits.js";
|
|
14
14
|
import { HonoEnv } from "../api/types.js";
|
|
15
15
|
import { type StorageTrigger } from "./triggers.js";
|
|
16
16
|
import { type RenditionCacheConfig } from "./rendition-cache.js";
|
|
17
|
+
/**
|
|
18
|
+
* The policy every SVG response carries.
|
|
19
|
+
*
|
|
20
|
+
* An SVG embedded with `<img>` never runs script. Opened directly — a link, a
|
|
21
|
+
* new tab, an `<iframe>` — it is a document on the API origin, and its
|
|
22
|
+
* `<script>` would run there, next to the refresh cookie. `sandbox` gives that
|
|
23
|
+
* document an opaque origin with scripts disabled, and `default-src 'none'`
|
|
24
|
+
* stops it loading anything; `style-src 'unsafe-inline'` keeps the inline
|
|
25
|
+
* styles most SVGs are drawn with. It is what GitHub serves user content
|
|
26
|
+
* under. Serving SVG as a download instead made every SVG logo a broken
|
|
27
|
+
* thumbnail in the panel.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
|
|
17
30
|
/**
|
|
18
31
|
* Decide what to actually serve for a stored content type.
|
|
19
32
|
*
|
|
20
|
-
* Returns the type to send
|
|
33
|
+
* Returns the type to send, whether to force a download, and the
|
|
34
|
+
* `Content-Security-Policy` the response must carry, if any. The check is an
|
|
21
35
|
* allowlist rather than a blocklist of dangerous types: the set of types a
|
|
22
36
|
* browser will execute grows, and the set we want to render inline does not.
|
|
23
37
|
*/
|
|
24
38
|
export declare function resolveServedContentType(storedContentType: string): {
|
|
25
39
|
contentType: string;
|
|
26
40
|
attachment: boolean;
|
|
41
|
+
contentSecurityPolicy?: string;
|
|
27
42
|
};
|
|
28
43
|
export interface StorageRoutesConfig {
|
|
29
44
|
/**
|
|
@@ -73,6 +88,13 @@ export interface StorageRoutesConfig {
|
|
|
73
88
|
* to the global cap exactly as before.
|
|
74
89
|
*/
|
|
75
90
|
uploadConstraints?: ResolveUploadConstraints;
|
|
91
|
+
/**
|
|
92
|
+
* The limits of the properties whose `storagePath` an upload's key falls
|
|
93
|
+
* in, by source and key — see `createUploadPathResolver`. Applied to every
|
|
94
|
+
* upload, whether or not it names a property, so a property's limits hold
|
|
95
|
+
* for every file written where its files go.
|
|
96
|
+
*/
|
|
97
|
+
uploadPathConstraints?: ResolveUploadPathConstraints;
|
|
76
98
|
/**
|
|
77
99
|
* When provided, storage routes delegate auth to this adapter instead
|
|
78
100
|
* of the built-in JWT module. This mirrors how data routes use
|
|
@@ -105,25 +127,41 @@ export interface StorageRoutesConfig {
|
|
|
105
127
|
* Run something when an object lands, or when one goes. See `triggers.ts`.
|
|
106
128
|
*/
|
|
107
129
|
triggers?: StorageTrigger[];
|
|
130
|
+
/**
|
|
131
|
+
* How long the download token `/metadata` mints for a private object stays
|
|
132
|
+
* valid, in seconds. {@link DEFAULT_DOWNLOAD_TOKEN_TTL_SECONDS} unless set;
|
|
133
|
+
* at most {@link MAX_DOWNLOAD_TOKEN_TTL_SECONDS}.
|
|
134
|
+
*/
|
|
135
|
+
downloadTokenTtlSeconds?: number;
|
|
108
136
|
}
|
|
137
|
+
/** Five minutes: long enough to render a page of thumbnails, short enough to be worthless in a log. */
|
|
138
|
+
export declare const DEFAULT_DOWNLOAD_TOKEN_TTL_SECONDS = 300;
|
|
109
139
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
140
|
+
* A week. The token travels in a URL — into access logs, Referer headers and
|
|
141
|
+
* browser history — so a lifetime past this is a permanent link to a private
|
|
142
|
+
* file. A file that must be linked forever belongs under the public prefix.
|
|
143
|
+
*/
|
|
144
|
+
export declare const MAX_DOWNLOAD_TOKEN_TTL_SECONDS: number;
|
|
145
|
+
/** Most keys `POST /metadata` takes in one request. */
|
|
146
|
+
export declare const DOWNLOAD_METADATA_BATCH_MAX = 100;
|
|
147
|
+
/**
|
|
148
|
+
* What a storage request costs against the data rate limit: one, except a
|
|
149
|
+
* `POST /metadata` of N keys, which costs ⌈N/10⌉.
|
|
115
150
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
151
|
+
* One request per key let a page of thumbnails spend a hundred requests of a
|
|
152
|
+
* caller's allowance; one per batch would make a batch of a hundred keys a
|
|
153
|
+
* hundred times cheaper to scrape with than the route it replaces. A tenth
|
|
154
|
+
* keeps a budget against enumeration while a page of thumbnails costs ten.
|
|
155
|
+
* Read before the route runs (the limiter is ahead of it); the body is parsed
|
|
156
|
+
* once, and the route reads Hono's cached copy.
|
|
120
157
|
*/
|
|
121
|
-
export declare function
|
|
158
|
+
export declare function storageRateLimitCost(c: {
|
|
122
159
|
req: {
|
|
160
|
+
method: string;
|
|
123
161
|
path: string;
|
|
124
|
-
|
|
162
|
+
json(): Promise<unknown>;
|
|
125
163
|
};
|
|
126
|
-
}):
|
|
164
|
+
}): Promise<number>;
|
|
127
165
|
/**
|
|
128
166
|
* Create storage REST API routes
|
|
129
167
|
*/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The variable that binds a declared object-store source's bucket on a
|
|
3
|
+
* deployment — `S3_BUCKET__MEDIA`, `GCS_BUCKET__ARCHIVE`, `S3_BUCKET` for the
|
|
4
|
+
* default source.
|
|
5
|
+
*
|
|
6
|
+
* One spelling for the three places that tell an operator to set it: the boot
|
|
7
|
+
* warning, the 501 a request to the unbound source answers, and `rebase
|
|
8
|
+
* status`. The 501 used to say "the media suffixed environment variables".
|
|
9
|
+
*/
|
|
10
|
+
export declare function storageBucketVariable(key: string, engine: string | undefined): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Anything that hands over bytes a chunk at a time: a Node readable, an SDK body. */
|
|
2
|
+
type ByteSource = AsyncIterable<Uint8Array>;
|
|
3
|
+
/**
|
|
4
|
+
* A byte source as a web stream that reads a chunk only when the consumer asks
|
|
5
|
+
* for one, and releases the source when the consumer stops early.
|
|
6
|
+
*
|
|
7
|
+
* Pull-based with a zero high-water mark, so nothing is read ahead of the
|
|
8
|
+
* reader: a slow client holds one chunk, not a queue of them. A source given
|
|
9
|
+
* as a function is opened on the first read, which keeps a stream nobody reads
|
|
10
|
+
* — a `HEAD`, a 304 decided late — from holding a file descriptor or a
|
|
11
|
+
* connection open. Cancelling (a client that disconnects mid-download)
|
|
12
|
+
* returns the iterator, which destroys a Node stream and so closes its file or
|
|
13
|
+
* socket.
|
|
14
|
+
*/
|
|
15
|
+
export declare function webStreamFrom(source: ByteSource | (() => ByteSource)): ReadableStream<Uint8Array>;
|
|
16
|
+
/**
|
|
17
|
+
* A file on disk, or an inclusive byte range of it, as a web stream. The file
|
|
18
|
+
* is opened on the first read; see {@link webStreamFrom}.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fileWebStream(filePath: string, range?: {
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
}): ReadableStream<Uint8Array>;
|
|
24
|
+
/** A buffer already in memory as a web stream of views onto it, never copies. */
|
|
25
|
+
export declare function bufferWebStream(bytes: Uint8Array, chunkSize?: number): ReadableStream<Uint8Array>;
|
|
26
|
+
/**
|
|
27
|
+
* Read a stream into one buffer.
|
|
28
|
+
*
|
|
29
|
+
* For the one consumer that needs the whole object at once — an image decode.
|
|
30
|
+
* With the size known the bytes are copied once, into a buffer of that size,
|
|
31
|
+
* rather than collected and then concatenated (which holds them twice); a
|
|
32
|
+
* stream that turns out longer or shorter than announced still comes back
|
|
33
|
+
* whole and exact.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readStreamFully(stream: ReadableStream<Uint8Array>, expectedSize?: number): Promise<Buffer>;
|
|
36
|
+
/**
|
|
37
|
+
* The bytes of a buffer as a `Uint8Array` the response APIs accept, without
|
|
38
|
+
* copying them. `new Uint8Array(buffer)` copies; a view does not. Only a buffer
|
|
39
|
+
* over shared memory — which nothing here produces — is copied.
|
|
40
|
+
*/
|
|
41
|
+
export declare function bytesView(bytes: Uint8Array): Uint8Array<ArrayBuffer>;
|
|
42
|
+
export {};
|
|
@@ -10,13 +10,29 @@
|
|
|
10
10
|
import type { Context } from "hono";
|
|
11
11
|
import type { StorageController } from "./types.js";
|
|
12
12
|
import { type StorageRegistry } from "./storage-registry.js";
|
|
13
|
-
import { type ResolveUploadConstraints } from "./property-limits.js";
|
|
13
|
+
import { type ResolveUploadConstraints, type ResolveUploadPathConstraints } from "./property-limits.js";
|
|
14
|
+
/**
|
|
15
|
+
* The header a resumable upload's last `PATCH` answers with the stored object
|
|
16
|
+
* in — `{ key, bucket, storageUrl }`, the `data` of `POST /upload`, as
|
|
17
|
+
* URI-encoded JSON — so a client gets from the resumable path what it gets
|
|
18
|
+
* from the multipart one. `create` answers it with `on-complete`, which tells
|
|
19
|
+
* a client this server will, before it sends a byte.
|
|
20
|
+
*/
|
|
21
|
+
export declare const UPLOAD_RESULT_HEADER = "Rebase-Upload-Result";
|
|
14
22
|
/**
|
|
15
23
|
* TUS resumable upload handler.
|
|
16
24
|
*
|
|
17
25
|
* Each instance manages uploads for a single storage root. The
|
|
18
26
|
* `storageController` is used to finalize completed uploads by
|
|
19
27
|
* calling `putObject`.
|
|
28
|
+
*
|
|
29
|
+
* An upload lives in the upload directory, not in this process: its bytes in
|
|
30
|
+
* `<id>`, and what `create` decided about it in `<id>.json` beside them. Its
|
|
31
|
+
* offset is the length of the bytes file. So a restart — a rolling deploy —
|
|
32
|
+
* resumes an upload where it stopped instead of sending the client back to
|
|
33
|
+
* byte 0, and every process that shares the directory (one volume at
|
|
34
|
+
* `STORAGE_PATH`) serves every upload. The map below only remembers records
|
|
35
|
+
* already read.
|
|
20
36
|
*/
|
|
21
37
|
export declare class TusHandler {
|
|
22
38
|
private storageController?;
|
|
@@ -65,6 +81,18 @@ export declare class TusHandler {
|
|
|
65
81
|
* accept is refused before the first chunk rather than after the last.
|
|
66
82
|
*/
|
|
67
83
|
private uploadConstraints?;
|
|
84
|
+
/**
|
|
85
|
+
* The limits of the properties whose storage path the upload's key
|
|
86
|
+
* falls in — checked whether or not the upload names a property, as
|
|
87
|
+
* `POST /upload` checks them.
|
|
88
|
+
*/
|
|
89
|
+
private uploadPathConstraints?;
|
|
90
|
+
/**
|
|
91
|
+
* Upload records this process has read or written, by id. A cache of the
|
|
92
|
+
* `<id>.json` files: it never holds the offset (the bytes file's length
|
|
93
|
+
* does), and an upload another process finished or cancelled is noticed
|
|
94
|
+
* when its bytes file is gone.
|
|
95
|
+
*/
|
|
68
96
|
private uploads;
|
|
69
97
|
private tusDir;
|
|
70
98
|
private cleanupTimer?;
|
|
@@ -123,21 +151,50 @@ export declare class TusHandler {
|
|
|
123
151
|
* and the metadata's content type, so a file the property will not
|
|
124
152
|
* accept is refused before the first chunk rather than after the last.
|
|
125
153
|
*/
|
|
126
|
-
uploadConstraints?: ResolveUploadConstraints | undefined
|
|
154
|
+
uploadConstraints?: ResolveUploadConstraints | undefined,
|
|
155
|
+
/**
|
|
156
|
+
* The limits of the properties whose storage path the upload's key
|
|
157
|
+
* falls in — checked whether or not the upload names a property, as
|
|
158
|
+
* `POST /upload` checks them.
|
|
159
|
+
*/
|
|
160
|
+
uploadPathConstraints?: ResolveUploadPathConstraints | undefined);
|
|
127
161
|
/** Ensure the temp directory exists. */
|
|
128
162
|
private ensureDir;
|
|
129
163
|
/** Start periodic cleanup of stale uploads. */
|
|
130
164
|
startCleanup(): void;
|
|
131
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* Remove uploads created more than {@link UPLOAD_EXPIRY_MS} ago.
|
|
167
|
+
*
|
|
168
|
+
* From the directory rather than from this process's memory, so an upload
|
|
169
|
+
* a crashed or replaced process abandoned is collected too. A record is
|
|
170
|
+
* written once, at creation, so its file's mtime is when the upload began;
|
|
171
|
+
* a bytes file with no record (left by an earlier version) is judged by
|
|
172
|
+
* its own.
|
|
173
|
+
*/
|
|
132
174
|
private cleanupStale;
|
|
175
|
+
/** The upload's bytes and record, gone; from every process, since they are files. */
|
|
176
|
+
private forget;
|
|
133
177
|
/**
|
|
134
178
|
* Parse the `Upload-Metadata` header.
|
|
135
179
|
*
|
|
136
180
|
* Format: `key base64value,key2 base64value2`
|
|
137
181
|
*/
|
|
138
182
|
private parseMetadata;
|
|
139
|
-
/**
|
|
140
|
-
|
|
183
|
+
/** The controller an upload to this storage source is written to. */
|
|
184
|
+
private targetFor;
|
|
185
|
+
/**
|
|
186
|
+
* The largest upload this source takes: the source's own limit, under the
|
|
187
|
+
* deployment's and the protocol ceiling. One function for what `OPTIONS`
|
|
188
|
+
* advertises and what `create` enforces, which used to be 5 GB and 50 MB.
|
|
189
|
+
*/
|
|
190
|
+
private limitFor;
|
|
191
|
+
/**
|
|
192
|
+
* `OPTIONS /tus` — TUS capability discovery.
|
|
193
|
+
*
|
|
194
|
+
* `Tus-Max-Size` is the limit `create` holds this source to, for the source
|
|
195
|
+
* named by `?storageId` (the default one otherwise).
|
|
196
|
+
*/
|
|
197
|
+
options(c?: Context): Response;
|
|
141
198
|
/** `POST /tus` — Create a new upload. */
|
|
142
199
|
create(c: Context): Promise<Response>;
|
|
143
200
|
/**
|
|
@@ -160,9 +217,21 @@ export declare class TusHandler {
|
|
|
160
217
|
* the rest.
|
|
161
218
|
*/
|
|
162
219
|
private ownedUpload;
|
|
220
|
+
/**
|
|
221
|
+
* The upload with this id: from this process's memory, or from its record
|
|
222
|
+
* in the upload directory — written by another process sharing it, or by
|
|
223
|
+
* this one before a restart. `null` when there is no such upload.
|
|
224
|
+
*/
|
|
225
|
+
private lookup;
|
|
226
|
+
/**
|
|
227
|
+
* Bytes received so far: the length of the bytes file. `null` when it is
|
|
228
|
+
* gone — finished or cancelled by another process — and then this one
|
|
229
|
+
* forgets the upload too.
|
|
230
|
+
*/
|
|
231
|
+
private offsetOf;
|
|
163
232
|
/** `HEAD /tus/:id` — Query upload progress. */
|
|
164
|
-
head(c: Context, id: string): Response
|
|
165
|
-
/** `PATCH /tus/:id` —
|
|
233
|
+
head(c: Context, id: string): Promise<Response>;
|
|
234
|
+
/** `PATCH /tus/:id` — Write one chunk of an upload, at its offset. */
|
|
166
235
|
patch(c: Context, id: string): Promise<Response>;
|
|
167
236
|
/** `DELETE /tus/:id` — Cancel and remove an upload. */
|
|
168
237
|
delete(c: Context, id: string): Promise<Response>;
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -23,6 +23,14 @@ export interface LocalStorageConfig {
|
|
|
23
23
|
* production, where an unbound object store stays unbound.
|
|
24
24
|
*/
|
|
25
25
|
standsInFor?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Set when nothing chose this backend: no `STORAGE_TYPE` and no declared
|
|
28
|
+
* engine, so local disk is only the default a source falls back to.
|
|
29
|
+
* Production drops a local backend either way. This only decides how that
|
|
30
|
+
* is reported: an implicit default being dropped means the project uses no
|
|
31
|
+
* storage, which is not a misconfiguration.
|
|
32
|
+
*/
|
|
33
|
+
implicit?: boolean;
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* S3-compatible storage configuration (works with AWS S3 and MinIO)
|
|
@@ -45,8 +53,6 @@ export interface S3StorageConfig {
|
|
|
45
53
|
maxFileSize?: number;
|
|
46
54
|
/** Allowed MIME types (if not set, all types allowed) */
|
|
47
55
|
allowedMimeTypes?: string[];
|
|
48
|
-
/** URL expiration time in seconds for signed URLs (default: 3600) */
|
|
49
|
-
signedUrlExpiration?: number;
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* Google Cloud Storage configuration (works with GCS and Firebase Storage)
|
|
@@ -69,8 +75,6 @@ export interface GCSStorageConfig {
|
|
|
69
75
|
maxFileSize?: number;
|
|
70
76
|
/** Allowed MIME types */
|
|
71
77
|
allowedMimeTypes?: string[];
|
|
72
|
-
/** Signed URL expiration in seconds (default: 3600) */
|
|
73
|
-
signedUrlExpiration?: number;
|
|
74
78
|
}
|
|
75
79
|
/**
|
|
76
80
|
* Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.
|
|
@@ -88,16 +92,40 @@ export interface GCSStorageConfig {
|
|
|
88
92
|
export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;
|
|
89
93
|
/** What a storage request is trying to do to an object. */
|
|
90
94
|
export type { StorageOperation, StorageAuthorizeUser, StorageAuthorizeContext, StorageAuthorizeData, StorageAuthorize } from "@rebasepro/types";
|
|
95
|
+
/**
|
|
96
|
+
* How the server writes an object. See {@link StorageController.putObject}.
|
|
97
|
+
*/
|
|
98
|
+
export interface PutObjectOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Hold this write to the controller's upload limits: `maxFileSize` and
|
|
101
|
+
* `allowedMimeTypes`. Defaults to `true`.
|
|
102
|
+
*
|
|
103
|
+
* Those limits say what a user may upload. A write the server makes for
|
|
104
|
+
* itself, such as a database backup, passes `false`. A dump is neither of
|
|
105
|
+
* an allowed type nor, past a small database, under 50 MB. The upload
|
|
106
|
+
* routes never pass it.
|
|
107
|
+
*/
|
|
108
|
+
uploadLimits?: boolean;
|
|
109
|
+
}
|
|
91
110
|
/**
|
|
92
111
|
* Storage controller interface for backend implementations
|
|
93
112
|
*/
|
|
94
113
|
export interface StorageController {
|
|
95
114
|
/**
|
|
96
|
-
* Upload an object
|
|
115
|
+
* Upload an object.
|
|
116
|
+
*
|
|
117
|
+
* A write is held to the controller's upload limits — `maxFileSize` and
|
|
118
|
+
* `allowedMimeTypes` — unless `options.uploadLimits` is `false`. Only the
|
|
119
|
+
* server's own writes pass that; nothing a request sends reaches it.
|
|
97
120
|
*/
|
|
98
|
-
putObject(props: UploadFileProps): Promise<UploadFileResult>;
|
|
121
|
+
putObject(props: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;
|
|
99
122
|
/**
|
|
100
|
-
* Get a download URL (signed URL equivalent) for an object
|
|
123
|
+
* Get a download URL (signed URL equivalent) for an object.
|
|
124
|
+
*
|
|
125
|
+
* For server-side code only: no route hands this URL to a client. A
|
|
126
|
+
* client reads a private object through `/file/*` with the download
|
|
127
|
+
* token `GET /metadata/*` mints. The built-in S3 and GCS controllers sign
|
|
128
|
+
* it for an hour.
|
|
101
129
|
*/
|
|
102
130
|
getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;
|
|
103
131
|
/**
|
|
@@ -120,6 +148,29 @@ export interface StorageController {
|
|
|
120
148
|
* Get object as a File
|
|
121
149
|
*/
|
|
122
150
|
getObject(key: string, bucket?: string): Promise<File | null>;
|
|
151
|
+
/**
|
|
152
|
+
* Describe an object for serving it — its size, stored type and version —
|
|
153
|
+
* without reading its bytes, or `null` when there is no such object.
|
|
154
|
+
*
|
|
155
|
+
* Optional, together with {@link getObjectStream}. A controller that has
|
|
156
|
+
* both is served streamed: `GET /file/*` answers a `304`, a `416`, a `HEAD`
|
|
157
|
+
* and a cached image rendition from this alone, and otherwise reads only
|
|
158
|
+
* the bytes it sends — the requested range, a chunk at a time. One without
|
|
159
|
+
* them is served through {@link getObject}, which holds the whole object in
|
|
160
|
+
* memory for every request, a `Range` request for one megabyte included.
|
|
161
|
+
*/
|
|
162
|
+
statObject?(key: string, bucket?: string): Promise<StoredObjectInfo | null>;
|
|
163
|
+
/**
|
|
164
|
+
* Read an object, or one byte range of it, as a stream, or `null` when
|
|
165
|
+
* there is no such object. See {@link statObject}.
|
|
166
|
+
*
|
|
167
|
+
* The stream holds a file or a connection until it is read to the end or
|
|
168
|
+
* cancelled, so a caller that decides not to read it must cancel it.
|
|
169
|
+
*/
|
|
170
|
+
getObjectStream?(key: string, options?: {
|
|
171
|
+
bucket?: string;
|
|
172
|
+
range?: StoredObjectRange;
|
|
173
|
+
}): Promise<StoredObjectStream | null>;
|
|
123
174
|
/**
|
|
124
175
|
* Delete an object
|
|
125
176
|
*/
|
|
@@ -159,6 +210,49 @@ export interface StorageController {
|
|
|
159
210
|
* when the caller names none, and each provider maps it to its own.
|
|
160
211
|
*/
|
|
161
212
|
knownBuckets?(): string[];
|
|
213
|
+
/**
|
|
214
|
+
* The largest file this controller accepts, in bytes.
|
|
215
|
+
*
|
|
216
|
+
* Every door that takes a file reads it, so the number a client is told is
|
|
217
|
+
* the number it is held to: the upload route checks it before anything is
|
|
218
|
+
* stored, the resumable route checks it at creation — before the first
|
|
219
|
+
* chunk rather than after the last — and advertises it as `Tus-Max-Size`,
|
|
220
|
+
* and the server's upload body limit is the largest of them. A controller
|
|
221
|
+
* that does not implement this is held to the deployment-wide limit alone.
|
|
222
|
+
*/
|
|
223
|
+
maxFileSize?(): number;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* What serving an object needs to know before deciding whether to read it.
|
|
227
|
+
* See {@link StorageController.statObject}.
|
|
228
|
+
*/
|
|
229
|
+
export interface StoredObjectInfo {
|
|
230
|
+
/** The whole object's size in bytes. */
|
|
231
|
+
size: number;
|
|
232
|
+
/** The stored content type — the uploader's claim, not a sniffed one. */
|
|
233
|
+
contentType: string;
|
|
234
|
+
/**
|
|
235
|
+
* The object's own modification time, in milliseconds since the epoch.
|
|
236
|
+
* The routes build `ETag`, `Last-Modified` and the transform cache key from
|
|
237
|
+
* it, so it must be the object's version, not the time of the read.
|
|
238
|
+
*/
|
|
239
|
+
lastModified: number;
|
|
240
|
+
}
|
|
241
|
+
/** An inclusive byte range, as `Range: bytes=start-end` names one. */
|
|
242
|
+
export interface StoredObjectRange {
|
|
243
|
+
start: number;
|
|
244
|
+
end: number;
|
|
245
|
+
}
|
|
246
|
+
/** An object's bytes as a stream. See {@link StorageController.getObjectStream}. */
|
|
247
|
+
export interface StoredObjectStream extends StoredObjectInfo {
|
|
248
|
+
/** The bytes: the range when {@link range} is set, the whole object otherwise. */
|
|
249
|
+
stream: ReadableStream<Uint8Array>;
|
|
250
|
+
/**
|
|
251
|
+
* The range {@link stream} holds — the one asked for — or absent when it
|
|
252
|
+
* holds the whole object. A controller may answer a range with the whole
|
|
253
|
+
* object, as HTTP lets a server do; the caller then serves it whole.
|
|
254
|
+
*/
|
|
255
|
+
range?: StoredObjectRange;
|
|
162
256
|
}
|
|
163
257
|
/**
|
|
164
258
|
* Default maximum file size (50MB)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every storage controller checks about a file before it stores it: the
|
|
3
|
+
* source's own size cap and, when configured, its allowed content types.
|
|
4
|
+
*
|
|
5
|
+
* One function for the three controllers, which carried a copy each. Both
|
|
6
|
+
* refusals are `ApiError`s, so they reach the caller as what they are. A plain
|
|
7
|
+
* `Error` was a 500 "Internal Server Error" on `/upload` and a 502 on TUS —
|
|
8
|
+
* after the client had sent every byte.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateUploadedFile(file: {
|
|
11
|
+
size: number;
|
|
12
|
+
type: string;
|
|
13
|
+
}, maxSize: number, allowedMimeTypes: string[] | undefined): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import "./resumable-upload-threshold-CKv-Ry0l.js";
|
|
6
|
+
//#region ../common/src/util/resumable-upload.ts
|
|
7
|
+
/** What `create` answers in `Rebase-Upload-Result` when the last `PATCH` will carry the stored object there. */
|
|
8
|
+
var RESULT_ON_COMPLETE = "on-complete";
|
|
9
|
+
var DEFAULT_RETRY_DELAYS_MS = [
|
|
10
|
+
250,
|
|
11
|
+
1e3,
|
|
12
|
+
3e3
|
|
13
|
+
];
|
|
14
|
+
/** `value` as UTF-8, base64-encoded — what `Upload-Metadata` carries. */
|
|
15
|
+
function base64Utf8(value) {
|
|
16
|
+
const bytes = new TextEncoder().encode(value);
|
|
17
|
+
let binary = "";
|
|
18
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
/** Worth asking for the offset and sending again: an offset conflict, a lock, a rate limit, a server fault. */
|
|
22
|
+
var isRetryable = (status) => status === 409 || status === 423 || status === 429 || status >= 500;
|
|
23
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
/** The stored object a finished upload reports, or `null` when the header says nothing readable. */
|
|
25
|
+
function storedObjectOf(header) {
|
|
26
|
+
if (!header) return null;
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(decodeURIComponent(header));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "object" || value === null) return null;
|
|
34
|
+
const key = "key" in value ? value.key : void 0;
|
|
35
|
+
const bucket = "bucket" in value ? value.bucket : void 0;
|
|
36
|
+
const storageUrl = "storageUrl" in value ? value.storageUrl : void 0;
|
|
37
|
+
if (typeof key !== "string" || typeof bucket !== "string" || typeof storageUrl !== "string") return null;
|
|
38
|
+
return {
|
|
39
|
+
key,
|
|
40
|
+
bucket,
|
|
41
|
+
storageUrl
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Upload `file` through a Rebase server's TUS endpoint, one chunk at a time.
|
|
46
|
+
*
|
|
47
|
+
* The server holds every upload to what `POST /storage/upload` holds a file
|
|
48
|
+
* to — the source's and the property's limits, the authorize hook, the bucket
|
|
49
|
+
* — and stores it with the same key, type and custom metadata, and fires the
|
|
50
|
+
* same triggers. So a refusal is thrown as the multipart route would have
|
|
51
|
+
* thrown it. A chunk that fails on the way — a dropped connection, a 5xx, a
|
|
52
|
+
* conflict over the offset — is retried from the offset the server reports.
|
|
53
|
+
*
|
|
54
|
+
* Answers `unavailable` when the resumable path cannot carry this file and
|
|
55
|
+
* the multipart one can: a server without the endpoint (or one too old to
|
|
56
|
+
* report the stored object), an upload another instance cannot see (several
|
|
57
|
+
* instances, no shared volume, no sticky sessions), or a chunk over the
|
|
58
|
+
* server's body limit.
|
|
59
|
+
*/
|
|
60
|
+
async function uploadResumable(options) {
|
|
61
|
+
const { file, send } = options;
|
|
62
|
+
const chunkSize = Math.max(1, options.chunkSize ?? 8388608);
|
|
63
|
+
const retryDelays = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
64
|
+
const tus = { "Tus-Resumable": "1.0.0" };
|
|
65
|
+
const created = await send("POST", "", {
|
|
66
|
+
...tus,
|
|
67
|
+
"Upload-Length": String(file.size),
|
|
68
|
+
"Upload-Metadata": Object.entries(options.metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`).join(",")
|
|
69
|
+
});
|
|
70
|
+
if (created.status === 404 || created.status === 405 || created.status === 501) return { unavailable: `the server has no resumable upload endpoint (${created.status})` };
|
|
71
|
+
if (created.status < 200 || created.status >= 300) throw created.error ?? /* @__PURE__ */ new Error(`The upload was refused (${created.status}).`);
|
|
72
|
+
const id = created.header("Location")?.split("?")[0].split("/").filter(Boolean).pop();
|
|
73
|
+
if (!id) return { unavailable: "the server did not say where the upload lives" };
|
|
74
|
+
const path = `/${encodeURIComponent(id)}`;
|
|
75
|
+
if (created.header("Rebase-Upload-Result") !== RESULT_ON_COMPLETE) {
|
|
76
|
+
await send("DELETE", path, tus).catch(() => void 0);
|
|
77
|
+
return { unavailable: "the server does not report the object a resumable upload stores" };
|
|
78
|
+
}
|
|
79
|
+
let offset = 0;
|
|
80
|
+
let lastFailure;
|
|
81
|
+
for (let attempt = 0;;) {
|
|
82
|
+
const end = Math.min(file.size, offset + chunkSize);
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await send("PATCH", path, {
|
|
86
|
+
...tus,
|
|
87
|
+
"Upload-Offset": String(offset),
|
|
88
|
+
"Content-Type": "application/offset+octet-stream"
|
|
89
|
+
}, file.slice(offset, end));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
lastFailure = error instanceof Error ? error : new Error(String(error));
|
|
92
|
+
}
|
|
93
|
+
if (response && response.status >= 200 && response.status < 300) {
|
|
94
|
+
attempt = 0;
|
|
95
|
+
const reported = Number(response.header("Upload-Offset"));
|
|
96
|
+
offset = Number.isFinite(reported) && reported > offset ? reported : end;
|
|
97
|
+
if (offset < file.size) continue;
|
|
98
|
+
const stored = storedObjectOf(response.header("Rebase-Upload-Result"));
|
|
99
|
+
if (!stored) throw new Error("The upload finished, but the server did not say where it stored it.");
|
|
100
|
+
return { stored };
|
|
101
|
+
}
|
|
102
|
+
if (response?.status === 404) return { unavailable: "the server no longer knows this upload — another instance may have answered" };
|
|
103
|
+
if (response?.status === 413) {
|
|
104
|
+
await send("DELETE", path, tus).catch(() => void 0);
|
|
105
|
+
return { unavailable: "a chunk is over the server's request body limit" };
|
|
106
|
+
}
|
|
107
|
+
if (response && !isRetryable(response.status)) {
|
|
108
|
+
await send("DELETE", path, tus).catch(() => void 0);
|
|
109
|
+
throw response.error ?? /* @__PURE__ */ new Error(`The upload was refused (${response.status}).`);
|
|
110
|
+
}
|
|
111
|
+
if (response) lastFailure = response.error ?? /* @__PURE__ */ new Error(`The upload failed (${response.status}).`);
|
|
112
|
+
if (attempt >= retryDelays.length) throw lastFailure ?? /* @__PURE__ */ new Error("The upload failed.");
|
|
113
|
+
await sleep(retryDelays[attempt++]);
|
|
114
|
+
try {
|
|
115
|
+
const progress = await send("HEAD", path, tus);
|
|
116
|
+
if (progress.status === 404) return { unavailable: "the server no longer knows this upload — another instance may have answered" };
|
|
117
|
+
const held = Number(progress.header("Upload-Offset"));
|
|
118
|
+
if (progress.status >= 200 && progress.status < 300 && Number.isFinite(held)) offset = held;
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { uploadResumable };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=storage-resumable-VO-cMpse-C_YX71pO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-resumable-VO-cMpse-C_YX71pO.js","names":[],"sources":["../../common/src/util/resumable-upload.ts"],"sourcesContent":["import type { UploadFileResult } from \"@rebasepro/types\";\nimport { RESUMABLE_UPLOAD_THRESHOLD } from \"./resumable-upload-threshold\";\n\n/** One response, as the resumable upload reads it. */\nexport interface ResumableUploadResponse {\n status: number;\n header(name: string): string | null;\n /** The error a non-2xx response stands for, as the transport reports it. */\n error?: Error;\n}\n\n/**\n * Send one TUS request. `path` is `\"\"` for the creation request and\n * `/<upload id>` after it, relative to the source's `…/storage/tus` endpoint.\n * Answers every HTTP status; throws only when no response arrived.\n */\nexport type ResumableUploadSend = (\n method: \"POST\" | \"PATCH\" | \"HEAD\" | \"DELETE\",\n path: string,\n headers: Record<string, string>,\n body?: Blob\n) => Promise<ResumableUploadResponse>;\n\nexport interface ResumableUploadOptions {\n file: Blob;\n /** `Upload-Metadata` entries: `key`, `filename`, `filetype`, `bucket`, `storageId`, `collection`, `property`, `metadata_<name>`. */\n metadata: Record<string, string>;\n send: ResumableUploadSend;\n /** Bytes per `PATCH`. Default {@link RESUMABLE_UPLOAD_THRESHOLD}. */\n chunkSize?: number;\n /** How long to wait before each retry of a chunk; its length is the number of retries. */\n retryDelaysMs?: readonly number[];\n}\n\n/**\n * What a resumable upload came to: the stored object, or — for a reason the\n * multipart route does not share — nothing, and the caller sends the file\n * through `POST /storage/upload` instead.\n */\nexport type ResumableUploadOutcome =\n | { stored: UploadFileResult }\n | { unavailable: string };\n\n/** What `create` answers in `Rebase-Upload-Result` when the last `PATCH` will carry the stored object there. */\nconst RESULT_ON_COMPLETE = \"on-complete\";\nconst DEFAULT_RETRY_DELAYS_MS = [250, 1_000, 3_000];\n\n/** `value` as UTF-8, base64-encoded — what `Upload-Metadata` carries. */\nfunction base64Utf8(value: string): string {\n const bytes = new TextEncoder().encode(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Worth asking for the offset and sending again: an offset conflict, a lock, a rate limit, a server fault. */\nconst isRetryable = (status: number) => status === 409 || status === 423 || status === 429 || status >= 500;\n\nconst sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));\n\n/** The stored object a finished upload reports, or `null` when the header says nothing readable. */\nfunction storedObjectOf(header: string | null): UploadFileResult | null {\n if (!header) return null;\n let value: unknown;\n try {\n value = JSON.parse(decodeURIComponent(header));\n } catch {\n return null;\n }\n if (typeof value !== \"object\" || value === null) return null;\n const key = \"key\" in value ? value.key : undefined;\n const bucket = \"bucket\" in value ? value.bucket : undefined;\n const storageUrl = \"storageUrl\" in value ? value.storageUrl : undefined;\n if (typeof key !== \"string\" || typeof bucket !== \"string\" || typeof storageUrl !== \"string\") return null;\n return { key, bucket, storageUrl };\n}\n\n/**\n * Upload `file` through a Rebase server's TUS endpoint, one chunk at a time.\n *\n * The server holds every upload to what `POST /storage/upload` holds a file\n * to — the source's and the property's limits, the authorize hook, the bucket\n * — and stores it with the same key, type and custom metadata, and fires the\n * same triggers. So a refusal is thrown as the multipart route would have\n * thrown it. A chunk that fails on the way — a dropped connection, a 5xx, a\n * conflict over the offset — is retried from the offset the server reports.\n *\n * Answers `unavailable` when the resumable path cannot carry this file and\n * the multipart one can: a server without the endpoint (or one too old to\n * report the stored object), an upload another instance cannot see (several\n * instances, no shared volume, no sticky sessions), or a chunk over the\n * server's body limit.\n */\nexport async function uploadResumable(options: ResumableUploadOptions): Promise<ResumableUploadOutcome> {\n const { file, send } = options;\n const chunkSize = Math.max(1, options.chunkSize ?? RESUMABLE_UPLOAD_THRESHOLD);\n const retryDelays = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;\n const tus = { \"Tus-Resumable\": \"1.0.0\" };\n\n const created = await send(\"POST\", \"\", {\n ...tus,\n \"Upload-Length\": String(file.size),\n \"Upload-Metadata\": Object.entries(options.metadata)\n .map(([name, value]) => `${name} ${base64Utf8(value)}`)\n .join(\",\")\n });\n if (created.status === 404 || created.status === 405 || created.status === 501) {\n return { unavailable: `the server has no resumable upload endpoint (${created.status})` };\n }\n if (created.status < 200 || created.status >= 300) {\n throw created.error ?? new Error(`The upload was refused (${created.status}).`);\n }\n const id = created.header(\"Location\")?.split(\"?\")[0].split(\"/\").filter(Boolean).pop();\n if (!id) return { unavailable: \"the server did not say where the upload lives\" };\n const path = `/${encodeURIComponent(id)}`;\n if (created.header(\"Rebase-Upload-Result\") !== RESULT_ON_COMPLETE) {\n await send(\"DELETE\", path, tus).catch(() => undefined);\n return { unavailable: \"the server does not report the object a resumable upload stores\" };\n }\n\n let offset = 0;\n let lastFailure: Error | undefined;\n for (let attempt = 0; ;) {\n const end = Math.min(file.size, offset + chunkSize);\n let response: ResumableUploadResponse | undefined;\n try {\n response = await send(\"PATCH\", path, {\n ...tus,\n \"Upload-Offset\": String(offset),\n \"Content-Type\": \"application/offset+octet-stream\"\n }, file.slice(offset, end));\n } catch (error: unknown) {\n // No response: the connection dropped. Ask where the upload got to.\n lastFailure = error instanceof Error ? error : new Error(String(error));\n }\n\n if (response && response.status >= 200 && response.status < 300) {\n attempt = 0;\n const reported = Number(response.header(\"Upload-Offset\"));\n offset = Number.isFinite(reported) && reported > offset ? reported : end;\n if (offset < file.size) continue;\n const stored = storedObjectOf(response.header(\"Rebase-Upload-Result\"));\n if (!stored) throw new Error(\"The upload finished, but the server did not say where it stored it.\");\n return { stored };\n }\n if (response?.status === 404) {\n return { unavailable: \"the server no longer knows this upload — another instance may have answered\" };\n }\n if (response?.status === 413) {\n await send(\"DELETE\", path, tus).catch(() => undefined);\n return { unavailable: \"a chunk is over the server's request body limit\" };\n }\n if (response && !isRetryable(response.status)) {\n await send(\"DELETE\", path, tus).catch(() => undefined);\n throw response.error ?? new Error(`The upload was refused (${response.status}).`);\n }\n if (response) lastFailure = response.error ?? new Error(`The upload failed (${response.status}).`);\n\n if (attempt >= retryDelays.length) throw lastFailure ?? new Error(\"The upload failed.\");\n await sleep(retryDelays[attempt++]);\n // Resume from what the server holds, which may be more than was\n // acknowledged (the chunk arrived, its answer did not) or less.\n try {\n const progress = await send(\"HEAD\", path, tus);\n if (progress.status === 404) {\n return { unavailable: \"the server no longer knows this upload — another instance may have answered\" };\n }\n const held = Number(progress.header(\"Upload-Offset\"));\n if (progress.status >= 200 && progress.status < 300 && Number.isFinite(held)) offset = held;\n } catch {\n // Still no connection: the next attempt asks again.\n }\n }\n}\n"],"mappings":"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"}
|