@devultur/core 1.5.2 → 1.6.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.
- package/dist/index.d.ts +123 -1
- package/dist/index.js +475 -380
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -389,6 +389,89 @@ interface AssetsClient {
|
|
|
389
389
|
listAll(options?: Omit<ListAssetsOptions, "cursor">): AsyncGenerator<AssetEntry>;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/** Prefix of a grant's secret. */
|
|
393
|
+
declare const DOWNLOAD_GRANT_PREFIX = "dvg_";
|
|
394
|
+
/** Prefix of a grant's public id (to revoke and audit it; not a credential). */
|
|
395
|
+
declare const DOWNLOAD_GRANT_ID_PREFIX = "dgr_";
|
|
396
|
+
/** `HEAD /v1/grant/file`: whether the file is encrypted at rest (`true`/`false`). */
|
|
397
|
+
declare const ENCRYPTED_HEADER = "x-devultur-encrypted";
|
|
398
|
+
/** `HEAD /v1/grant/file`: when the grant expires (epoch ms). */
|
|
399
|
+
declare const GRANT_EXPIRES_AT_HEADER = "x-devultur-grant-expires-at";
|
|
400
|
+
/** `HEAD /v1/grant/file`: the key of the grant's file. */
|
|
401
|
+
declare const GRANT_KEY_HEADER = "x-devultur-grant-key";
|
|
402
|
+
/** `HEAD /v1/grant/file`: the grant's metadata, as URI-encoded JSON. */
|
|
403
|
+
declare const GRANT_METADATA_HEADER = "x-devultur-grant-metadata";
|
|
404
|
+
declare const DownloadGrantLimits: {
|
|
405
|
+
/** Longest grant lifetime of a file without a TTL. A file with one: its `expiresAt`. */
|
|
406
|
+
readonly MAX_LIFETIME_SECONDS: number;
|
|
407
|
+
readonly DEFAULT_LINK_EXPIRES_IN: 900;
|
|
408
|
+
readonly MAX_LINK_EXPIRES_IN: 86400;
|
|
409
|
+
readonly DEFAULT_LINK_MAX_USES: 1;
|
|
410
|
+
readonly MAX_LINK_MAX_USES: 10;
|
|
411
|
+
readonly DEFAULT_MAX_LINKS: 20;
|
|
412
|
+
readonly MAX_MAX_LINKS: 100;
|
|
413
|
+
/** Size of the metadata as JSON, in UTF-8 bytes. */
|
|
414
|
+
readonly MAX_METADATA_BYTES: 1024;
|
|
415
|
+
};
|
|
416
|
+
declare const DownloadGrantErrorCode: {
|
|
417
|
+
/** Unknown, malformed or not this Worker's grant. */
|
|
418
|
+
readonly INVALID: "invalid_grant";
|
|
419
|
+
/** Expired, revoked, or its file is gone. */
|
|
420
|
+
readonly GONE: "grant_gone";
|
|
421
|
+
/** It issued all of its links. */
|
|
422
|
+
readonly EXHAUSTED: "grant_exhausted";
|
|
423
|
+
/** A link above `link.maxExpiresIn` or `link.maxUses`. */
|
|
424
|
+
readonly LINK_EXCEEDS_GRANT: "link_exceeds_grant";
|
|
425
|
+
/** The grant was sent in `?token=`. */
|
|
426
|
+
readonly IN_QUERY: "grant_in_query";
|
|
427
|
+
/** `delete()` on a grant issued without `canDelete`. */
|
|
428
|
+
readonly CANNOT_DELETE: "grant_cannot_delete";
|
|
429
|
+
readonly NOT_FOUND: "grant_not_found";
|
|
430
|
+
};
|
|
431
|
+
interface DownloadGrantLinkCaps {
|
|
432
|
+
/** Longest `expiresIn` (seconds) of each link. */
|
|
433
|
+
maxExpiresIn: number;
|
|
434
|
+
/** Most `maxUses` of each link. */
|
|
435
|
+
maxUses: number;
|
|
436
|
+
}
|
|
437
|
+
interface CreateDownloadGrantOptions {
|
|
438
|
+
/** Grant lifetime in seconds. Default and cap: the file's `expiresAt`; without a TTL, 30 days. */
|
|
439
|
+
expiresIn?: number;
|
|
440
|
+
/** Caps of every link the grant issues. Default `{ maxExpiresIn: 900, maxUses: 1 }`. */
|
|
441
|
+
link?: Partial<DownloadGrantLinkCaps>;
|
|
442
|
+
/** Links the grant may issue in total (default 20, max 100). */
|
|
443
|
+
maxLinks?: number;
|
|
444
|
+
/** Allows `delete()` (default false). Needs a credential that can delete the file. */
|
|
445
|
+
canDelete?: boolean;
|
|
446
|
+
/** Opaque string → string metadata (≤ 1 KB as JSON), returned by `head()` and stored with every link. */
|
|
447
|
+
metadata?: Record<string, string>;
|
|
448
|
+
}
|
|
449
|
+
/** A grant as listed: everything but its secret. */
|
|
450
|
+
interface DownloadGrantSummary {
|
|
451
|
+
/** Public id (`dgr_…`): revokes and identifies the grant, grants nothing. */
|
|
452
|
+
id: string;
|
|
453
|
+
key: string;
|
|
454
|
+
/** Epoch ms. */
|
|
455
|
+
expiresAt: number;
|
|
456
|
+
link: DownloadGrantLinkCaps;
|
|
457
|
+
maxLinks: number;
|
|
458
|
+
canDelete: boolean;
|
|
459
|
+
metadata?: Record<string, string>;
|
|
460
|
+
}
|
|
461
|
+
interface DownloadGrant extends DownloadGrantSummary {
|
|
462
|
+
/** The secret (`dvg_…`). Returned once. */
|
|
463
|
+
grant: string;
|
|
464
|
+
}
|
|
465
|
+
interface DownloadGrantListEntry extends DownloadGrantSummary {
|
|
466
|
+
linksIssued: number;
|
|
467
|
+
/** Epoch ms. */
|
|
468
|
+
createdAt: number;
|
|
469
|
+
/** Epoch ms, or null while live. */
|
|
470
|
+
revokedAt: number | null;
|
|
471
|
+
}
|
|
472
|
+
/** Is `value` shaped like a grant's secret? Pure. */
|
|
473
|
+
declare function isDownloadGrant(value: string): boolean;
|
|
474
|
+
|
|
392
475
|
interface CreateUploadTokenOptions {
|
|
393
476
|
/** Prefix within `asset/` where the token can create files (`exports/inst-1/`). */
|
|
394
477
|
prefix: string;
|
|
@@ -471,6 +554,8 @@ interface DownloadLinkOptions {
|
|
|
471
554
|
filename?: string;
|
|
472
555
|
}
|
|
473
556
|
interface DownloadLink {
|
|
557
|
+
/** Opaque id of the link (the last segment of `url`), to correlate it with delivery logs and events. */
|
|
558
|
+
id: string;
|
|
474
559
|
url: string;
|
|
475
560
|
expiresAt: number;
|
|
476
561
|
maxUses: number;
|
|
@@ -486,6 +571,15 @@ interface FilesClient {
|
|
|
486
571
|
head(key: string): Promise<FileHead | null>;
|
|
487
572
|
/** Single-use download link: the link doesn't carry the credential. */
|
|
488
573
|
createDownloadLink(key: string, options?: DownloadLinkOptions): Promise<DownloadLink>;
|
|
574
|
+
/**
|
|
575
|
+
* A download grant over this one file: hand its `grant` to another system (see `createGrantClient`) so it can issue
|
|
576
|
+
* links within the caps you set, without your credential. The secret is returned once.
|
|
577
|
+
*/
|
|
578
|
+
createDownloadGrant(key: string, options?: CreateDownloadGrantOptions): Promise<DownloadGrant>;
|
|
579
|
+
/** Revokes a grant by its public id (`dgr_…`). Idempotent. */
|
|
580
|
+
revokeDownloadGrant(id: string): Promise<void>;
|
|
581
|
+
/** The file's grants (live, revoked or expired until cleaned up), without their secrets. */
|
|
582
|
+
listDownloadGrants(key: string): Promise<DownloadGrantListEntry[]>;
|
|
489
583
|
/** `Response` of the file to pipe from your backend to your user (without exposing a token). */
|
|
490
584
|
stream(key: string, options?: {
|
|
491
585
|
range?: string;
|
|
@@ -1040,6 +1134,34 @@ declare function sanitizeFilename(filename: string): string;
|
|
|
1040
1134
|
declare function contentDispositionInline(filename: string): string;
|
|
1041
1135
|
declare function contentDispositionAttachment(filename: string): string;
|
|
1042
1136
|
|
|
1137
|
+
interface GrantFileHead extends FileHead {
|
|
1138
|
+
/** Whether the file is encrypted at rest. */
|
|
1139
|
+
encrypted: boolean;
|
|
1140
|
+
/** The grant's metadata, as its owner set it. */
|
|
1141
|
+
metadata: Record<string, string>;
|
|
1142
|
+
/** When the grant expires (epoch ms). */
|
|
1143
|
+
grantExpiresAt: number;
|
|
1144
|
+
}
|
|
1145
|
+
interface GrantLinkOptions extends DownloadLinkOptions {
|
|
1146
|
+
/** Opaque string → string metadata (≤ 1 KB as JSON) stored with the link. */
|
|
1147
|
+
metadata?: Record<string, string>;
|
|
1148
|
+
}
|
|
1149
|
+
interface GrantClient {
|
|
1150
|
+
/** The file's head, plus the grant's metadata and expiry. `null` once the grant or its file is gone. */
|
|
1151
|
+
head(): Promise<GrantFileHead | null>;
|
|
1152
|
+
/**
|
|
1153
|
+
* A download link of the file. Above the grant's caps → `400 link_exceeds_grant` (nothing spent); every link
|
|
1154
|
+
* already issued → `410 grant_exhausted`.
|
|
1155
|
+
*/
|
|
1156
|
+
createDownloadLink(options?: GrantLinkOptions): Promise<DownloadLink>;
|
|
1157
|
+
/** Deletes the file (`409 retention_active` while it's retained) and its grants. Only a grant with `canDelete`. */
|
|
1158
|
+
delete(): Promise<void>;
|
|
1159
|
+
/** Revokes the grant. Idempotent. */
|
|
1160
|
+
revoke(): Promise<void>;
|
|
1161
|
+
}
|
|
1162
|
+
/** Client for the file a download grant covers. `baseUrl` is the Worker's origin. */
|
|
1163
|
+
declare function createGrantClient(grant: string, baseUrl: string): GrantClient;
|
|
1164
|
+
|
|
1043
1165
|
/** sha256 (hex) of the file: the client declares it on upload and the worker returns it on GET/HEAD. */
|
|
1044
1166
|
declare const SHA256_HEADER = "x-devultur-sha256";
|
|
1045
1167
|
/** `true` = the worker computed the sha256 over the bytes; `false` = the client declared it (multipart). */
|
|
@@ -1369,4 +1491,4 @@ declare function createHTTPWebhookHandler(options: {
|
|
|
1369
1491
|
}>;
|
|
1370
1492
|
};
|
|
1371
1493
|
|
|
1372
|
-
export { AES_BLOCK_BYTES, ASSET_NAMESPACE, type AssetEntry, type AssetListing, type AssetUploadResult, type AssetsClient, BYTES_PER_UNIT, ByteUnit, CACHE_CONTROL_PRESETS, CacheControlPreset, type CacheRule, type CapabilityUploadResult, CaptionLocale, CaptionLocaleLabel, CaptionStatus, type CaptionsJobResult, type CaptionsStatusResult, type CreateUploadTokenOptions, type CreateUploadUrlOptions, DEFAULT_ASSET_CACHE_CONTROL, DEFAULT_PRESIGN_EXPIRY_SECONDS, DEFAULT_RETENTION_DERIVATIVES, DEFAULT_SECURITY, DEFAULT_TOKEN_EXPIRY_SECONDS, DEFAULT_VISIBILITY, type DeleteVideoResult, type DeliveryConfig, DevulturApiError, type DevulturConfig, type DevulturKind, type DevulturMedia, type DirectStorageConfig, type DownloadLink, type DownloadLinkOptions, type DownloadUrlOptions, ENCRYPTION_ALG, ENCRYPTION_VERSION, EXPIRES_AT_HEADER, type EncryptionMeta, FILE_MIMES, type FailureReason, type FetchApi, type FileBuilderConfig, type FileHead, type FilesClient, GENERATED_PRIVATE_CACHE_CONTROL, GENERATED_PUBLIC_CACHE_CONTROL, HOLD_PAGE_SIZE, type HoldPage, type HoldSkip, type HoldTarget, IMAGE_HEIGHT_HEADER, IMAGE_MIMES, IMAGE_WIDTH_HEADER, type ImageBuilderConfig, ImageFit, ImageFormat, type IssueTokenOptions, type JsonRequest, LEGACY_PRESET_SPECS, type ListAssetsOptions, type LockChange, type LockChangeOptions, type LockChangeReason, type LockClient, type LockInstant, type LockResolution, type LockTiming, type LockValidation, type LockView, MAIN_VARIANT, MAX_ASSET_PATH_DEPTH, MAX_ASSET_PATH_LENGTH, MAX_BITRATE_KBPS, MAX_CACHE_CONTROL_LENGTH, MAX_CACHE_RULES, MAX_RETENTION_PERIOD_SECONDS, MAX_VARIANTS, MIN_BITRATE_KBPS, MULTIPART_PART_SIZE_BYTES, MULTIPART_QUERY, MULTIPART_THRESHOLD_BYTES, type MediaEvent, MediaEventName, type MediaEventNameValue, type MediaEventType, type MediaRouter, type MediaRouterConfig, MediaStatus, type MediaStatusValue, MediaType, type MediaUrlOptions, type Mp4Output, MultipartAction, type MultipartPart, type MultipartSession, type MultipartUploadOptions, type ObjectLock, POSTER_FORMATS, type PartUploader, type PosterAt, type PosterFormat, type PosterUrls, type PresetResolution, type PresetSource, type ProcessingPhase, REPR_DIGEST_HEADER, RETENTION_DERIVATIVES, RETENTION_MODES, type ResolvedLock, type ResolvedRetention, type Retention, type RetentionDerivatives, type RetentionMode, SESSION_HEADER, SHA256_HEADER, SHA256_VERIFIED_HEADER, type ScopeCoverage, SecurityLevel, type SecurityPolicy, type SetLockOptions, type SignOptions, type SpecValidation, type SpeechConnectorConfig, SpeechProvider, type StorageClient, type StorageConfig, type StorageConnectorConfig, StorageProvider, type StoredRendition, TRANSCODE_CROPS, TRANSCODE_RUNGS, TRANSCODE_TEMPLATES, type TokenResult, TokenScope, type TranscodeConnectorConfig, type TranscodeCrop, type TranscodeJobResult, type TranscodeOptions, type TranscodeOutputKeys, type TranscodeOutputUrls, type TranscodeOutputs, TranscodePreset, TranscodeProvider, type TranscodeRendition, type TranscodeRung, type TranscodeSettings, type TranscodeSpec, TranscodeStatus, type TranscodeStatusResult, TranscodeTemplate, type TranscodeVariant, type TranslationConnectorConfig, TranslationProvider, type UploadAssetOptions, type UploadFileOptions, type UploadFileResult, UploadRequestKind, type UploadToken, type UploadUrlResult, type UploadsClient, VIDEO_MIMES, type VideoBuilderConfig, type VideoModuleConfig, type VideoOutputRef, type Visibility, type WebhookVerifyOptions, type WorkerClient, type WorkerConfig, type WorkerTransport, type WorkerUploadResult, assetKey, assetRelativePath, buildMediaUrl, buildPosterUrls, buildStoryboardUrl, buildTranscodeOutputUrls, buildVideoMp4Url, bytesToHex, canChange, captionPath, classifyUploadRequest, computeFingerprint, contentDispositionAttachment, contentDispositionInline, counterForBlock, coversScope, createDecryptStream, createHTTPWebhookHandler, createLockClient, createMediaRouter, createStorageClient, createWorkerClient, createWorkerTransport, decodeEncryptionMeta, decryptChunk, edgeTtlSeconds, effectiveUntil, encodeEncryptionMeta, encryptBytes, exportEncryptionKey, extractId, extractKeyFromUrl, file, filePath, formatSize, generateEncryptionKey, generateId, generateIv, getDownloadUrl, handleWebhook, image, imagePath, importEncryptionKey, isAssetKey, isBearerOf, isIsoInstant, isLocked, isPrefixWithin, isValidMime, isValidOutputName, isVideoRawKey, isWithinPathPrefix, isWorkerUploadUrl, matchCacheRule, mediaCaptionPath, mediaPath, mediaThumbnailPath, mediaUrl, missingScopes, multipartAbortUrl, multipartCompleteUrl, multipartCreateUrl, multipartPartCount, multipartPartRange, multipartPartUrl, normalizeAssetPath, normalizeAssetPrefix, normalizeCacheRules, normalizePathPrefix, normalizeSha256, parseSize, publicUrl, relativeMediaPath, reprDigest, resolveCacheControl, resolveLock, resolveSecurity, resolveTranscodePreset, resolveVisibility, sanitizeFilename, sha256Hex, shouldUseMultipart, signPayload, signUrl, timingSafeEqual, toPrivateCacheControl, uploadMultipart, uploadToCapabilityUrl, validateLock, validateTranscodeSpec, verifySignature, verifySignedUrl, video, videoBasePath, videoHlsPath, videoHlsPlaylistPath, videoMp4Dir, videoMp4Path, videoPosterPath, videoRawPath, videoStoryboardDir, videoStoryboardPath, videoThumbnailPath, visibilityFromKey };
|
|
1494
|
+
export { AES_BLOCK_BYTES, ASSET_NAMESPACE, type AssetEntry, type AssetListing, type AssetUploadResult, type AssetsClient, BYTES_PER_UNIT, ByteUnit, CACHE_CONTROL_PRESETS, CacheControlPreset, type CacheRule, type CapabilityUploadResult, CaptionLocale, CaptionLocaleLabel, CaptionStatus, type CaptionsJobResult, type CaptionsStatusResult, type CreateDownloadGrantOptions, type CreateUploadTokenOptions, type CreateUploadUrlOptions, DEFAULT_ASSET_CACHE_CONTROL, DEFAULT_PRESIGN_EXPIRY_SECONDS, DEFAULT_RETENTION_DERIVATIVES, DEFAULT_SECURITY, DEFAULT_TOKEN_EXPIRY_SECONDS, DEFAULT_VISIBILITY, DOWNLOAD_GRANT_ID_PREFIX, DOWNLOAD_GRANT_PREFIX, type DeleteVideoResult, type DeliveryConfig, DevulturApiError, type DevulturConfig, type DevulturKind, type DevulturMedia, type DirectStorageConfig, type DownloadGrant, DownloadGrantErrorCode, DownloadGrantLimits, type DownloadGrantLinkCaps, type DownloadGrantListEntry, type DownloadGrantSummary, type DownloadLink, type DownloadLinkOptions, type DownloadUrlOptions, ENCRYPTED_HEADER, ENCRYPTION_ALG, ENCRYPTION_VERSION, EXPIRES_AT_HEADER, type EncryptionMeta, FILE_MIMES, type FailureReason, type FetchApi, type FileBuilderConfig, type FileHead, type FilesClient, GENERATED_PRIVATE_CACHE_CONTROL, GENERATED_PUBLIC_CACHE_CONTROL, GRANT_EXPIRES_AT_HEADER, GRANT_KEY_HEADER, GRANT_METADATA_HEADER, type GrantClient, type GrantFileHead, type GrantLinkOptions, HOLD_PAGE_SIZE, type HoldPage, type HoldSkip, type HoldTarget, IMAGE_HEIGHT_HEADER, IMAGE_MIMES, IMAGE_WIDTH_HEADER, type ImageBuilderConfig, ImageFit, ImageFormat, type IssueTokenOptions, type JsonRequest, LEGACY_PRESET_SPECS, type ListAssetsOptions, type LockChange, type LockChangeOptions, type LockChangeReason, type LockClient, type LockInstant, type LockResolution, type LockTiming, type LockValidation, type LockView, MAIN_VARIANT, MAX_ASSET_PATH_DEPTH, MAX_ASSET_PATH_LENGTH, MAX_BITRATE_KBPS, MAX_CACHE_CONTROL_LENGTH, MAX_CACHE_RULES, MAX_RETENTION_PERIOD_SECONDS, MAX_VARIANTS, MIN_BITRATE_KBPS, MULTIPART_PART_SIZE_BYTES, MULTIPART_QUERY, MULTIPART_THRESHOLD_BYTES, type MediaEvent, MediaEventName, type MediaEventNameValue, type MediaEventType, type MediaRouter, type MediaRouterConfig, MediaStatus, type MediaStatusValue, MediaType, type MediaUrlOptions, type Mp4Output, MultipartAction, type MultipartPart, type MultipartSession, type MultipartUploadOptions, type ObjectLock, POSTER_FORMATS, type PartUploader, type PosterAt, type PosterFormat, type PosterUrls, type PresetResolution, type PresetSource, type ProcessingPhase, REPR_DIGEST_HEADER, RETENTION_DERIVATIVES, RETENTION_MODES, type ResolvedLock, type ResolvedRetention, type Retention, type RetentionDerivatives, type RetentionMode, SESSION_HEADER, SHA256_HEADER, SHA256_VERIFIED_HEADER, type ScopeCoverage, SecurityLevel, type SecurityPolicy, type SetLockOptions, type SignOptions, type SpecValidation, type SpeechConnectorConfig, SpeechProvider, type StorageClient, type StorageConfig, type StorageConnectorConfig, StorageProvider, type StoredRendition, TRANSCODE_CROPS, TRANSCODE_RUNGS, TRANSCODE_TEMPLATES, type TokenResult, TokenScope, type TranscodeConnectorConfig, type TranscodeCrop, type TranscodeJobResult, type TranscodeOptions, type TranscodeOutputKeys, type TranscodeOutputUrls, type TranscodeOutputs, TranscodePreset, TranscodeProvider, type TranscodeRendition, type TranscodeRung, type TranscodeSettings, type TranscodeSpec, TranscodeStatus, type TranscodeStatusResult, TranscodeTemplate, type TranscodeVariant, type TranslationConnectorConfig, TranslationProvider, type UploadAssetOptions, type UploadFileOptions, type UploadFileResult, UploadRequestKind, type UploadToken, type UploadUrlResult, type UploadsClient, VIDEO_MIMES, type VideoBuilderConfig, type VideoModuleConfig, type VideoOutputRef, type Visibility, type WebhookVerifyOptions, type WorkerClient, type WorkerConfig, type WorkerTransport, type WorkerUploadResult, assetKey, assetRelativePath, buildMediaUrl, buildPosterUrls, buildStoryboardUrl, buildTranscodeOutputUrls, buildVideoMp4Url, bytesToHex, canChange, captionPath, classifyUploadRequest, computeFingerprint, contentDispositionAttachment, contentDispositionInline, counterForBlock, coversScope, createDecryptStream, createGrantClient, createHTTPWebhookHandler, createLockClient, createMediaRouter, createStorageClient, createWorkerClient, createWorkerTransport, decodeEncryptionMeta, decryptChunk, edgeTtlSeconds, effectiveUntil, encodeEncryptionMeta, encryptBytes, exportEncryptionKey, extractId, extractKeyFromUrl, file, filePath, formatSize, generateEncryptionKey, generateId, generateIv, getDownloadUrl, handleWebhook, image, imagePath, importEncryptionKey, isAssetKey, isBearerOf, isDownloadGrant, isIsoInstant, isLocked, isPrefixWithin, isValidMime, isValidOutputName, isVideoRawKey, isWithinPathPrefix, isWorkerUploadUrl, matchCacheRule, mediaCaptionPath, mediaPath, mediaThumbnailPath, mediaUrl, missingScopes, multipartAbortUrl, multipartCompleteUrl, multipartCreateUrl, multipartPartCount, multipartPartRange, multipartPartUrl, normalizeAssetPath, normalizeAssetPrefix, normalizeCacheRules, normalizePathPrefix, normalizeSha256, parseSize, publicUrl, relativeMediaPath, reprDigest, resolveCacheControl, resolveLock, resolveSecurity, resolveTranscodePreset, resolveVisibility, sanitizeFilename, sha256Hex, shouldUseMultipart, signPayload, signUrl, timingSafeEqual, toPrivateCacheControl, uploadMultipart, uploadToCapabilityUrl, validateLock, validateTranscodeSpec, verifySignature, verifySignedUrl, video, videoBasePath, videoHlsPath, videoHlsPlaylistPath, videoMp4Dir, videoMp4Path, videoPosterPath, videoRawPath, videoStoryboardDir, videoStoryboardPath, videoThumbnailPath, visibilityFromKey };
|
package/dist/index.js
CHANGED
|
@@ -401,6 +401,44 @@ function getDownloadUrl(mediaUrl2, options) {
|
|
|
401
401
|
return `${mediaUrl2}${separator}download${value}`;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
// src/download-grant.ts
|
|
405
|
+
var DOWNLOAD_GRANT_PREFIX = "dvg_";
|
|
406
|
+
var DOWNLOAD_GRANT_ID_PREFIX = "dgr_";
|
|
407
|
+
var ENCRYPTED_HEADER = "x-devultur-encrypted";
|
|
408
|
+
var GRANT_EXPIRES_AT_HEADER = "x-devultur-grant-expires-at";
|
|
409
|
+
var GRANT_KEY_HEADER = "x-devultur-grant-key";
|
|
410
|
+
var GRANT_METADATA_HEADER = "x-devultur-grant-metadata";
|
|
411
|
+
var DownloadGrantLimits = {
|
|
412
|
+
/** Longest grant lifetime of a file without a TTL. A file with one: its `expiresAt`. */
|
|
413
|
+
MAX_LIFETIME_SECONDS: 30 * 24 * 60 * 60,
|
|
414
|
+
DEFAULT_LINK_EXPIRES_IN: 900,
|
|
415
|
+
MAX_LINK_EXPIRES_IN: 86400,
|
|
416
|
+
DEFAULT_LINK_MAX_USES: 1,
|
|
417
|
+
MAX_LINK_MAX_USES: 10,
|
|
418
|
+
DEFAULT_MAX_LINKS: 20,
|
|
419
|
+
MAX_MAX_LINKS: 100,
|
|
420
|
+
/** Size of the metadata as JSON, in UTF-8 bytes. */
|
|
421
|
+
MAX_METADATA_BYTES: 1024
|
|
422
|
+
};
|
|
423
|
+
var DownloadGrantErrorCode = {
|
|
424
|
+
/** Unknown, malformed or not this Worker's grant. */
|
|
425
|
+
INVALID: "invalid_grant",
|
|
426
|
+
/** Expired, revoked, or its file is gone. */
|
|
427
|
+
GONE: "grant_gone",
|
|
428
|
+
/** It issued all of its links. */
|
|
429
|
+
EXHAUSTED: "grant_exhausted",
|
|
430
|
+
/** A link above `link.maxExpiresIn` or `link.maxUses`. */
|
|
431
|
+
LINK_EXCEEDS_GRANT: "link_exceeds_grant",
|
|
432
|
+
/** The grant was sent in `?token=`. */
|
|
433
|
+
IN_QUERY: "grant_in_query",
|
|
434
|
+
/** `delete()` on a grant issued without `canDelete`. */
|
|
435
|
+
CANNOT_DELETE: "grant_cannot_delete",
|
|
436
|
+
NOT_FOUND: "grant_not_found"
|
|
437
|
+
};
|
|
438
|
+
function isDownloadGrant(value) {
|
|
439
|
+
return value.startsWith(DOWNLOAD_GRANT_PREFIX);
|
|
440
|
+
}
|
|
441
|
+
|
|
404
442
|
// src/filenames.ts
|
|
405
443
|
function sanitizeFilename(filename) {
|
|
406
444
|
const lastDot = filename.lastIndexOf(".");
|
|
@@ -422,161 +460,45 @@ function contentDispositionAttachment(filename) {
|
|
|
422
460
|
return contentDisposition("attachment", filename);
|
|
423
461
|
}
|
|
424
462
|
|
|
425
|
-
// src/
|
|
426
|
-
var
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return bytesToHex(await crypto.subtle.digest("SHA-256", data));
|
|
443
|
-
}
|
|
444
|
-
function reprDigest(sha256) {
|
|
445
|
-
const pairs = sha256.match(/.{2}/g) ?? [];
|
|
446
|
-
const binary = pairs.map((pair) => String.fromCharCode(Number.parseInt(pair, 16))).join("");
|
|
447
|
-
return `sha-256=:${btoa(binary)}:`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// src/lock.ts
|
|
451
|
-
var RETENTION_MODES = ["governance", "compliance"];
|
|
452
|
-
var RETENTION_DERIVATIVES = ["inherit", "original-only"];
|
|
453
|
-
var DEFAULT_RETENTION_DERIVATIVES = "inherit";
|
|
454
|
-
var MAX_RETENTION_PERIOD_SECONDS = 100 * 366 * 24 * 60 * 60;
|
|
455
|
-
var LOCK_FIELDS = /* @__PURE__ */ new Set(["retention", "hold"]);
|
|
456
|
-
var RETENTION_FIELDS = /* @__PURE__ */ new Set(["mode", "until", "period", "derivatives"]);
|
|
457
|
-
var ISO_8601_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
458
|
-
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
459
|
-
function isIsoInstant(value) {
|
|
460
|
-
return typeof value === "string" && ISO_8601_INSTANT.test(value) && !Number.isNaN(Date.parse(value));
|
|
461
|
-
}
|
|
462
|
-
function rejectUnknownKeys(input, allowed, path, errors) {
|
|
463
|
-
for (const key of Object.keys(input)) {
|
|
464
|
-
if (!allowed.has(key)) errors.push(`${path}.${key}: unknown field`);
|
|
463
|
+
// src/api-error.ts
|
|
464
|
+
var DevulturApiError = class extends Error {
|
|
465
|
+
status;
|
|
466
|
+
code;
|
|
467
|
+
limit;
|
|
468
|
+
/**
|
|
469
|
+
* The response body's fields other than `error` and `code`, e.g. `{ key, mode, until, hold }`
|
|
470
|
+
* on a `409 retention_active`. `undefined` when the body had none (or wasn't JSON).
|
|
471
|
+
*/
|
|
472
|
+
details;
|
|
473
|
+
constructor(message, status, code, limit, details) {
|
|
474
|
+
super(message);
|
|
475
|
+
this.name = "DevulturApiError";
|
|
476
|
+
this.status = status;
|
|
477
|
+
this.code = code;
|
|
478
|
+
this.limit = limit;
|
|
479
|
+
this.details = details;
|
|
465
480
|
}
|
|
481
|
+
};
|
|
482
|
+
function detailsFrom(body) {
|
|
483
|
+
const { error: _error, code: _code, ...rest } = body;
|
|
484
|
+
return Object.keys(rest).length > 0 ? rest : void 0;
|
|
466
485
|
}
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
const isPeriodValid = Number.isSafeInteger(period) && period > 0 && period <= MAX_RETENTION_PERIOD_SECONDS;
|
|
482
|
-
if (period !== void 0 && !isPeriodValid) {
|
|
483
|
-
errors.push(`${path}.period: must be a positive integer number of seconds, at most 100 years`);
|
|
484
|
-
}
|
|
485
|
-
if (until !== void 0 && period !== void 0) errors.push(`${path}: set either until or period, not both`);
|
|
486
|
-
if (until === void 0 && period === void 0) errors.push(`${path}: until or period is required`);
|
|
487
|
-
if (derivatives !== void 0 && !RETENTION_DERIVATIVES.includes(derivatives)) {
|
|
488
|
-
errors.push(`${path}.derivatives: must be "inherit" or "original-only"`);
|
|
486
|
+
async function errorFromResponse(response) {
|
|
487
|
+
let message = `API error: ${response.status}`;
|
|
488
|
+
let code;
|
|
489
|
+
let limit;
|
|
490
|
+
let details;
|
|
491
|
+
try {
|
|
492
|
+
const body = await response.json();
|
|
493
|
+
if (typeof body === "object" && body !== null && !Array.isArray(body)) {
|
|
494
|
+
if ("error" in body && typeof body.error === "string") message = body.error;
|
|
495
|
+
if ("code" in body && typeof body.code === "string") code = body.code;
|
|
496
|
+
if ("limit" in body && typeof body.limit === "number") limit = body.limit;
|
|
497
|
+
details = detailsFrom(body);
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
489
500
|
}
|
|
490
|
-
|
|
491
|
-
const retention = { mode };
|
|
492
|
-
if (until !== void 0) retention.until = until;
|
|
493
|
-
if (period !== void 0) retention.period = period;
|
|
494
|
-
if (derivatives !== void 0) retention.derivatives = derivatives;
|
|
495
|
-
return retention;
|
|
496
|
-
}
|
|
497
|
-
function validateLock(input, path = "lock") {
|
|
498
|
-
if (!isObject(input)) return { ok: false, errors: [`${path}: must be an object`] };
|
|
499
|
-
const errors = [];
|
|
500
|
-
rejectUnknownKeys(input, LOCK_FIELDS, path, errors);
|
|
501
|
-
const lock = {};
|
|
502
|
-
if (input.retention !== void 0) lock.retention = validateRetention(input.retention, `${path}.retention`, errors);
|
|
503
|
-
if (input.hold !== void 0 && typeof input.hold !== "boolean") errors.push(`${path}.hold: must be a boolean`);
|
|
504
|
-
if (typeof input.hold === "boolean") lock.hold = input.hold;
|
|
505
|
-
if (errors.length > 0) return { ok: false, errors };
|
|
506
|
-
return { ok: true, lock };
|
|
507
|
-
}
|
|
508
|
-
function resolveLock(resource, appDefault) {
|
|
509
|
-
const errors = [];
|
|
510
|
-
const resourceLock = resource === void 0 ? {} : collect(validateLock(resource, "lock"), errors);
|
|
511
|
-
const defaultLock = appDefault === void 0 ? {} : collect(validateLock(appDefault, "default"), errors);
|
|
512
|
-
if (errors.length > 0) return { ok: false, errors };
|
|
513
|
-
const lock = { hold: resourceLock.hold ?? defaultLock.hold ?? false };
|
|
514
|
-
const retention = resourceLock.retention ?? defaultLock.retention;
|
|
515
|
-
if (!retention) return { ok: true, lock };
|
|
516
|
-
const derivatives = resourceLock.retention?.derivatives ?? defaultLock.retention?.derivatives ?? DEFAULT_RETENTION_DERIVATIVES;
|
|
517
|
-
lock.retention = { ...retention, derivatives };
|
|
518
|
-
return { ok: true, lock };
|
|
519
|
-
}
|
|
520
|
-
function collect(validation, errors) {
|
|
521
|
-
if (validation.ok) return validation.lock;
|
|
522
|
-
errors.push(...validation.errors);
|
|
523
|
-
return {};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/lock-change.ts
|
|
527
|
-
var toMs = (instant) => instant instanceof Date ? instant.getTime() : new Date(instant).getTime();
|
|
528
|
-
function retentionEnd(lock, timing) {
|
|
529
|
-
const { retention } = lock;
|
|
530
|
-
if (!retention) return { kind: "none" };
|
|
531
|
-
if (retention.until !== void 0) return { kind: "at", ms: toMs(retention.until) };
|
|
532
|
-
const period = retention.period ?? 0;
|
|
533
|
-
if (lock.hold) return { kind: "pending", period };
|
|
534
|
-
const start = timing?.holdReleasedAt ?? timing?.uploadedAt;
|
|
535
|
-
if (start === void 0) throw new RangeError("lock: a retention period needs uploadedAt or holdReleasedAt");
|
|
536
|
-
return { kind: "at", ms: toMs(start) + period * 1e3 };
|
|
537
|
-
}
|
|
538
|
-
function effectiveUntil(lock, timing) {
|
|
539
|
-
const end = retentionEnd(lock, timing);
|
|
540
|
-
return end.kind === "at" ? new Date(end.ms).toISOString() : void 0;
|
|
541
|
-
}
|
|
542
|
-
function isLocked(lock, now, timing) {
|
|
543
|
-
if (lock.hold) return true;
|
|
544
|
-
const end = retentionEnd(lock, timing);
|
|
545
|
-
return end.kind === "at" && toMs(now) < end.ms;
|
|
546
|
-
}
|
|
547
|
-
function afterTiming(before, after, options) {
|
|
548
|
-
if (!before.hold || after.hold) return options.timing;
|
|
549
|
-
return { ...options.timing, holdReleasedAt: options.now };
|
|
550
|
-
}
|
|
551
|
-
function earliestEndMs(end, nowMs) {
|
|
552
|
-
if (end.kind === "none") return Number.NEGATIVE_INFINITY;
|
|
553
|
-
return end.kind === "at" ? end.ms : nowMs + end.period * 1e3;
|
|
554
|
-
}
|
|
555
|
-
function isShortened(beforeEnd, afterEnd, nowMs, isStillHeld) {
|
|
556
|
-
if (afterEnd.kind === "none") return true;
|
|
557
|
-
if (beforeEnd.kind === "pending" && afterEnd.kind === "at" && isStillHeld) return true;
|
|
558
|
-
return earliestEndMs(afterEnd, nowMs) < earliestEndMs(beforeEnd, nowMs);
|
|
559
|
-
}
|
|
560
|
-
function isWeakerSetting(before, after) {
|
|
561
|
-
if (!after) return true;
|
|
562
|
-
if (before.mode === "compliance" && after.mode === "governance") return true;
|
|
563
|
-
return (before.derivatives ?? "inherit") === "inherit" && after.derivatives === "original-only";
|
|
564
|
-
}
|
|
565
|
-
function isRetentionActive(end, nowMs) {
|
|
566
|
-
if (end.kind === "pending") return true;
|
|
567
|
-
return end.kind === "at" && nowMs < end.ms;
|
|
568
|
-
}
|
|
569
|
-
function canChange(before, after, options) {
|
|
570
|
-
const nowMs = toMs(options.now);
|
|
571
|
-
const beforeEnd = retentionEnd(before, options.timing);
|
|
572
|
-
if (!before.retention || !isRetentionActive(beforeEnd, nowMs)) return { ok: true, bypass: false };
|
|
573
|
-
const afterEnd = retentionEnd(after, afterTiming(before, after, options));
|
|
574
|
-
const isShortenedEnd = isShortened(beforeEnd, afterEnd, nowMs, after.hold === true);
|
|
575
|
-
const isWeakened = isShortenedEnd || isWeakerSetting(before.retention, after.retention);
|
|
576
|
-
if (!isWeakened) return { ok: true, bypass: false };
|
|
577
|
-
if (before.retention.mode === "compliance") return { ok: false, reason: "compliance_extend_only" };
|
|
578
|
-
if (!options.privileged) return { ok: false, reason: "lock_privilege_required" };
|
|
579
|
-
return { ok: true, bypass: true };
|
|
501
|
+
return new DevulturApiError(message, response.status, code, limit, details);
|
|
580
502
|
}
|
|
581
503
|
|
|
582
504
|
// src/paths.ts
|
|
@@ -700,45 +622,29 @@ function publicUrl(baseUrl, media, options) {
|
|
|
700
622
|
return buildMediaUrl(baseUrl, mediaPath(media), options);
|
|
701
623
|
}
|
|
702
624
|
|
|
703
|
-
// src/
|
|
704
|
-
var
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
this.name = "DevulturApiError";
|
|
716
|
-
this.status = status;
|
|
717
|
-
this.code = code;
|
|
718
|
-
this.limit = limit;
|
|
719
|
-
this.details = details;
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
-
function detailsFrom(body) {
|
|
723
|
-
const { error: _error, code: _code, ...rest } = body;
|
|
724
|
-
return Object.keys(rest).length > 0 ? rest : void 0;
|
|
625
|
+
// src/integrity.ts
|
|
626
|
+
var SHA256_HEADER = "x-devultur-sha256";
|
|
627
|
+
var SHA256_VERIFIED_HEADER = "x-devultur-sha256-verified";
|
|
628
|
+
var EXPIRES_AT_HEADER = "x-devultur-expires-at";
|
|
629
|
+
var IMAGE_WIDTH_HEADER = "x-devultur-image-width";
|
|
630
|
+
var IMAGE_HEIGHT_HEADER = "x-devultur-image-height";
|
|
631
|
+
var REPR_DIGEST_HEADER = "Repr-Digest";
|
|
632
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
633
|
+
function normalizeSha256(raw) {
|
|
634
|
+
if (typeof raw !== "string") return null;
|
|
635
|
+
const value = raw.trim().toLowerCase();
|
|
636
|
+
return SHA256_HEX.test(value) ? value : null;
|
|
725
637
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if ("limit" in body && typeof body.limit === "number") limit = body.limit;
|
|
737
|
-
details = detailsFrom(body);
|
|
738
|
-
}
|
|
739
|
-
} catch {
|
|
740
|
-
}
|
|
741
|
-
return new DevulturApiError(message, response.status, code, limit, details);
|
|
638
|
+
function bytesToHex(bytes) {
|
|
639
|
+
return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
640
|
+
}
|
|
641
|
+
async function sha256Hex(data) {
|
|
642
|
+
return bytesToHex(await crypto.subtle.digest("SHA-256", data));
|
|
643
|
+
}
|
|
644
|
+
function reprDigest(sha256) {
|
|
645
|
+
const pairs = sha256.match(/.{2}/g) ?? [];
|
|
646
|
+
const binary = pairs.map((pair) => String.fromCharCode(Number.parseInt(pair, 16))).join("");
|
|
647
|
+
return `sha-256=:${btoa(binary)}:`;
|
|
742
648
|
}
|
|
743
649
|
|
|
744
650
|
// src/multipart.ts
|
|
@@ -912,196 +818,6 @@ async function uploadToCapabilityUrl(uploadUrl, file2, options = {}) {
|
|
|
912
818
|
options.onProgress?.(100);
|
|
913
819
|
}
|
|
914
820
|
|
|
915
|
-
// src/path-prefix.ts
|
|
916
|
-
var VISIBILITY_SEGMENTS2 = /* @__PURE__ */ new Set(["public", "private"]);
|
|
917
|
-
var SAFE_SEGMENT2 = /^[A-Za-z0-9._-]+$/;
|
|
918
|
-
var MAX_PATH_PREFIX_LENGTH = 256;
|
|
919
|
-
function isSafeSegment2(segment) {
|
|
920
|
-
return SAFE_SEGMENT2.test(segment) && segment !== "." && segment !== "..";
|
|
921
|
-
}
|
|
922
|
-
function normalizePathPrefix(raw) {
|
|
923
|
-
if (typeof raw !== "string" || raw.length > MAX_PATH_PREFIX_LENGTH) return null;
|
|
924
|
-
const segments = raw.trim().replace(/^\/+|\/+$/g, "").split("/");
|
|
925
|
-
if (segments.length === 0 || segments[0] === "") return null;
|
|
926
|
-
if (VISIBILITY_SEGMENTS2.has(segments[0])) return null;
|
|
927
|
-
if (!segments.every(isSafeSegment2)) return null;
|
|
928
|
-
return `${segments.join("/")}/`;
|
|
929
|
-
}
|
|
930
|
-
function relativeMediaPath(virtualKey) {
|
|
931
|
-
const segments = virtualKey.split("/");
|
|
932
|
-
if (!VISIBILITY_SEGMENTS2.has(segments[0] ?? "")) return null;
|
|
933
|
-
const rest = segments.slice(1);
|
|
934
|
-
const isTrailingSlash = rest.at(-1) === "";
|
|
935
|
-
const named = isTrailingSlash ? rest.slice(0, -1) : rest;
|
|
936
|
-
if (named.length === 0 || !named.every(isSafeSegment2)) return null;
|
|
937
|
-
return `${named.join("/")}${isTrailingSlash ? "/" : ""}`;
|
|
938
|
-
}
|
|
939
|
-
function isWithinPathPrefix(relativePath, pathPrefix) {
|
|
940
|
-
if (!pathPrefix) return true;
|
|
941
|
-
return relativePath.startsWith(pathPrefix);
|
|
942
|
-
}
|
|
943
|
-
function isPrefixWithin(child, parent) {
|
|
944
|
-
if (!parent) return true;
|
|
945
|
-
if (!child) return false;
|
|
946
|
-
return child.startsWith(parent);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// src/providers.ts
|
|
950
|
-
var StorageProvider = {
|
|
951
|
-
R2: "r2"
|
|
952
|
-
};
|
|
953
|
-
var TranscodeProvider = {
|
|
954
|
-
FLY_IO: "fly.io"
|
|
955
|
-
};
|
|
956
|
-
var SpeechProvider = {
|
|
957
|
-
GROQ_WHISPER: "groq-whisper"
|
|
958
|
-
};
|
|
959
|
-
var TranslationProvider = {
|
|
960
|
-
MISTRAL: "mistral"
|
|
961
|
-
};
|
|
962
|
-
var ImageFit = {
|
|
963
|
-
COVER: "cover",
|
|
964
|
-
CONTAIN: "contain",
|
|
965
|
-
FILL: "fill",
|
|
966
|
-
INSIDE: "inside"
|
|
967
|
-
};
|
|
968
|
-
var ImageFormat = {
|
|
969
|
-
WEBP: "webp",
|
|
970
|
-
AVIF: "avif",
|
|
971
|
-
JPEG: "jpeg",
|
|
972
|
-
PNG: "png",
|
|
973
|
-
AUTO: "auto"
|
|
974
|
-
};
|
|
975
|
-
var SecurityLevel = {
|
|
976
|
-
SIGNED_URLS: 1,
|
|
977
|
-
TOKEN_BINDING: 2,
|
|
978
|
-
IP_FINGERPRINT: 3,
|
|
979
|
-
AES_128_HLS: 4
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
// src/signing.ts
|
|
983
|
-
async function importHmacKey(secret) {
|
|
984
|
-
const encoder2 = new TextEncoder();
|
|
985
|
-
return crypto.subtle.importKey("raw", encoder2.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
986
|
-
"sign",
|
|
987
|
-
"verify"
|
|
988
|
-
]);
|
|
989
|
-
}
|
|
990
|
-
function bufferToHex(buffer) {
|
|
991
|
-
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
992
|
-
}
|
|
993
|
-
function hexToBuffer(hex) {
|
|
994
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
995
|
-
for (let i = 0; i < hex.length; i += 2) {
|
|
996
|
-
bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
|
|
997
|
-
}
|
|
998
|
-
return bytes.buffer;
|
|
999
|
-
}
|
|
1000
|
-
async function signPayload(payload, secret) {
|
|
1001
|
-
const key = await importHmacKey(secret);
|
|
1002
|
-
const encoder2 = new TextEncoder();
|
|
1003
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder2.encode(payload));
|
|
1004
|
-
return bufferToHex(signature);
|
|
1005
|
-
}
|
|
1006
|
-
async function verifySignature(payload, signature, secret) {
|
|
1007
|
-
const key = await importHmacKey(secret);
|
|
1008
|
-
const encoder2 = new TextEncoder();
|
|
1009
|
-
const signatureBuffer = hexToBuffer(signature);
|
|
1010
|
-
return crypto.subtle.verify("HMAC", key, signatureBuffer, encoder2.encode(payload));
|
|
1011
|
-
}
|
|
1012
|
-
async function signUrl(url, secret, expiresIn) {
|
|
1013
|
-
const expiry = expiresIn ?? DEFAULT_PRESIGN_EXPIRY_SECONDS;
|
|
1014
|
-
const expires = Math.floor(Date.now() / 1e3) + expiry;
|
|
1015
|
-
const parsed = new URL(url);
|
|
1016
|
-
parsed.searchParams.set("expires", String(expires));
|
|
1017
|
-
const payload = `${parsed.origin}${parsed.pathname}?expires=${expires}`;
|
|
1018
|
-
const signature = await signPayload(payload, secret);
|
|
1019
|
-
parsed.searchParams.set("signature", signature);
|
|
1020
|
-
return parsed.toString();
|
|
1021
|
-
}
|
|
1022
|
-
async function verifySignedUrl(signedUrl, secret) {
|
|
1023
|
-
const parsed = new URL(signedUrl);
|
|
1024
|
-
const expires = parsed.searchParams.get("expires");
|
|
1025
|
-
const signature = parsed.searchParams.get("signature");
|
|
1026
|
-
if (!expires || !signature) return false;
|
|
1027
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1028
|
-
if (now > Number(expires)) return false;
|
|
1029
|
-
const payload = `${parsed.origin}${parsed.pathname}?expires=${expires}`;
|
|
1030
|
-
return verifySignature(payload, signature, secret);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// src/storage.ts
|
|
1034
|
-
import {
|
|
1035
|
-
DeleteObjectCommand,
|
|
1036
|
-
GetObjectCommand,
|
|
1037
|
-
HeadObjectCommand,
|
|
1038
|
-
PutObjectCommand,
|
|
1039
|
-
S3Client
|
|
1040
|
-
} from "@aws-sdk/client-s3";
|
|
1041
|
-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
1042
|
-
function createStorageClient(config) {
|
|
1043
|
-
const client = new S3Client({
|
|
1044
|
-
region: "auto",
|
|
1045
|
-
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
1046
|
-
credentials: {
|
|
1047
|
-
accessKeyId: config.accessKeyId,
|
|
1048
|
-
secretAccessKey: config.secretAccessKey
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
return {
|
|
1052
|
-
async createUploadUrl(options) {
|
|
1053
|
-
const command = new PutObjectCommand({
|
|
1054
|
-
Bucket: config.bucket,
|
|
1055
|
-
Key: options.key,
|
|
1056
|
-
ContentType: options.contentType,
|
|
1057
|
-
Metadata: options.metadata,
|
|
1058
|
-
...options.storageClass ? { StorageClass: options.storageClass } : {},
|
|
1059
|
-
...options.contentDisposition ? { ContentDisposition: options.contentDisposition } : {}
|
|
1060
|
-
});
|
|
1061
|
-
return getSignedUrl(client, command, {
|
|
1062
|
-
expiresIn: options.expiresIn ?? DEFAULT_PRESIGN_EXPIRY_SECONDS
|
|
1063
|
-
});
|
|
1064
|
-
},
|
|
1065
|
-
async createDownloadUrl(options) {
|
|
1066
|
-
const command = new GetObjectCommand({
|
|
1067
|
-
Bucket: config.bucket,
|
|
1068
|
-
Key: options.key
|
|
1069
|
-
});
|
|
1070
|
-
return getSignedUrl(client, command, {
|
|
1071
|
-
expiresIn: options.expiresIn ?? DEFAULT_PRESIGN_EXPIRY_SECONDS
|
|
1072
|
-
});
|
|
1073
|
-
},
|
|
1074
|
-
async deleteObject(key) {
|
|
1075
|
-
await client.send(
|
|
1076
|
-
new DeleteObjectCommand({
|
|
1077
|
-
Bucket: config.bucket,
|
|
1078
|
-
Key: key
|
|
1079
|
-
})
|
|
1080
|
-
);
|
|
1081
|
-
},
|
|
1082
|
-
async headObject(key) {
|
|
1083
|
-
try {
|
|
1084
|
-
const response = await client.send(
|
|
1085
|
-
new HeadObjectCommand({
|
|
1086
|
-
Bucket: config.bucket,
|
|
1087
|
-
Key: key
|
|
1088
|
-
})
|
|
1089
|
-
);
|
|
1090
|
-
return {
|
|
1091
|
-
size: response.ContentLength ?? 0,
|
|
1092
|
-
contentType: response.ContentType ?? "application/octet-stream",
|
|
1093
|
-
lastModified: response.LastModified ?? /* @__PURE__ */ new Date()
|
|
1094
|
-
};
|
|
1095
|
-
} catch (error) {
|
|
1096
|
-
if (error instanceof Error && "name" in error && error.name === "NotFound") {
|
|
1097
|
-
return null;
|
|
1098
|
-
}
|
|
1099
|
-
throw error;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
821
|
// src/worker-assets.ts
|
|
1106
822
|
function assetListQuery(options) {
|
|
1107
823
|
const params = new URLSearchParams();
|
|
@@ -1216,6 +932,18 @@ function createFilesClient(fetchApi, request) {
|
|
|
1216
932
|
body: JSON.stringify({ key, ...options })
|
|
1217
933
|
});
|
|
1218
934
|
},
|
|
935
|
+
createDownloadGrant(key, options = {}) {
|
|
936
|
+
return request("/v1/token/grant", {
|
|
937
|
+
method: "POST",
|
|
938
|
+
body: JSON.stringify({ key, ...options })
|
|
939
|
+
});
|
|
940
|
+
},
|
|
941
|
+
async revokeDownloadGrant(id) {
|
|
942
|
+
await fetchApi(`/v1/token/grant/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
943
|
+
},
|
|
944
|
+
listDownloadGrants(key) {
|
|
945
|
+
return request(`/v1/token/grant?key=${encodeURIComponent(key)}`);
|
|
946
|
+
},
|
|
1219
947
|
stream(key, options = {}) {
|
|
1220
948
|
return fetchApi(`/v1/media/${encodeKey(key)}`, {
|
|
1221
949
|
headers: options.range ? { Range: options.range } : {},
|
|
@@ -1392,6 +1120,363 @@ function createWorkerClient(credential, baseUrl) {
|
|
|
1392
1120
|
};
|
|
1393
1121
|
}
|
|
1394
1122
|
|
|
1123
|
+
// src/grant-client.ts
|
|
1124
|
+
function grantMetadata(response) {
|
|
1125
|
+
const header = response.headers.get(GRANT_METADATA_HEADER);
|
|
1126
|
+
return header ? JSON.parse(decodeURIComponent(header)) : {};
|
|
1127
|
+
}
|
|
1128
|
+
var isGone = (error) => error instanceof DevulturApiError && (error.status === 404 || error.status === 410);
|
|
1129
|
+
function createGrantClient(grant, baseUrl) {
|
|
1130
|
+
const { fetchApi, request, requestVoid } = createWorkerTransport(grant, baseUrl);
|
|
1131
|
+
return {
|
|
1132
|
+
async head() {
|
|
1133
|
+
try {
|
|
1134
|
+
const response = await fetchApi("/v1/grant/file", { method: "HEAD" });
|
|
1135
|
+
return {
|
|
1136
|
+
...headFromResponse(response.headers.get(GRANT_KEY_HEADER) ?? "", response),
|
|
1137
|
+
encrypted: response.headers.get(ENCRYPTED_HEADER) === "true",
|
|
1138
|
+
metadata: grantMetadata(response),
|
|
1139
|
+
grantExpiresAt: Number(response.headers.get(GRANT_EXPIRES_AT_HEADER))
|
|
1140
|
+
};
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
if (isGone(error)) return null;
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
createDownloadLink(options = {}) {
|
|
1147
|
+
return request("/v1/grant/links", { method: "POST", body: JSON.stringify(options) });
|
|
1148
|
+
},
|
|
1149
|
+
delete() {
|
|
1150
|
+
return requestVoid("/v1/grant/file", { method: "DELETE" });
|
|
1151
|
+
},
|
|
1152
|
+
revoke() {
|
|
1153
|
+
return requestVoid("/v1/grant", { method: "DELETE" });
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/lock.ts
|
|
1159
|
+
var RETENTION_MODES = ["governance", "compliance"];
|
|
1160
|
+
var RETENTION_DERIVATIVES = ["inherit", "original-only"];
|
|
1161
|
+
var DEFAULT_RETENTION_DERIVATIVES = "inherit";
|
|
1162
|
+
var MAX_RETENTION_PERIOD_SECONDS = 100 * 366 * 24 * 60 * 60;
|
|
1163
|
+
var LOCK_FIELDS = /* @__PURE__ */ new Set(["retention", "hold"]);
|
|
1164
|
+
var RETENTION_FIELDS = /* @__PURE__ */ new Set(["mode", "until", "period", "derivatives"]);
|
|
1165
|
+
var ISO_8601_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1166
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1167
|
+
function isIsoInstant(value) {
|
|
1168
|
+
return typeof value === "string" && ISO_8601_INSTANT.test(value) && !Number.isNaN(Date.parse(value));
|
|
1169
|
+
}
|
|
1170
|
+
function rejectUnknownKeys(input, allowed, path, errors) {
|
|
1171
|
+
for (const key of Object.keys(input)) {
|
|
1172
|
+
if (!allowed.has(key)) errors.push(`${path}.${key}: unknown field`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function validateRetention(input, path, errors) {
|
|
1176
|
+
if (!isObject(input)) {
|
|
1177
|
+
errors.push(`${path}: must be an object`);
|
|
1178
|
+
return void 0;
|
|
1179
|
+
}
|
|
1180
|
+
const errorCount = errors.length;
|
|
1181
|
+
rejectUnknownKeys(input, RETENTION_FIELDS, path, errors);
|
|
1182
|
+
const { mode, until, period, derivatives } = input;
|
|
1183
|
+
if (!RETENTION_MODES.includes(mode)) {
|
|
1184
|
+
errors.push(`${path}.mode: must be "governance" or "compliance"`);
|
|
1185
|
+
}
|
|
1186
|
+
if (until !== void 0 && !isIsoInstant(until)) {
|
|
1187
|
+
errors.push(`${path}.until: must be an ISO 8601 date-time with a zone`);
|
|
1188
|
+
}
|
|
1189
|
+
const isPeriodValid = Number.isSafeInteger(period) && period > 0 && period <= MAX_RETENTION_PERIOD_SECONDS;
|
|
1190
|
+
if (period !== void 0 && !isPeriodValid) {
|
|
1191
|
+
errors.push(`${path}.period: must be a positive integer number of seconds, at most 100 years`);
|
|
1192
|
+
}
|
|
1193
|
+
if (until !== void 0 && period !== void 0) errors.push(`${path}: set either until or period, not both`);
|
|
1194
|
+
if (until === void 0 && period === void 0) errors.push(`${path}: until or period is required`);
|
|
1195
|
+
if (derivatives !== void 0 && !RETENTION_DERIVATIVES.includes(derivatives)) {
|
|
1196
|
+
errors.push(`${path}.derivatives: must be "inherit" or "original-only"`);
|
|
1197
|
+
}
|
|
1198
|
+
if (errors.length > errorCount) return void 0;
|
|
1199
|
+
const retention = { mode };
|
|
1200
|
+
if (until !== void 0) retention.until = until;
|
|
1201
|
+
if (period !== void 0) retention.period = period;
|
|
1202
|
+
if (derivatives !== void 0) retention.derivatives = derivatives;
|
|
1203
|
+
return retention;
|
|
1204
|
+
}
|
|
1205
|
+
function validateLock(input, path = "lock") {
|
|
1206
|
+
if (!isObject(input)) return { ok: false, errors: [`${path}: must be an object`] };
|
|
1207
|
+
const errors = [];
|
|
1208
|
+
rejectUnknownKeys(input, LOCK_FIELDS, path, errors);
|
|
1209
|
+
const lock = {};
|
|
1210
|
+
if (input.retention !== void 0) lock.retention = validateRetention(input.retention, `${path}.retention`, errors);
|
|
1211
|
+
if (input.hold !== void 0 && typeof input.hold !== "boolean") errors.push(`${path}.hold: must be a boolean`);
|
|
1212
|
+
if (typeof input.hold === "boolean") lock.hold = input.hold;
|
|
1213
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
1214
|
+
return { ok: true, lock };
|
|
1215
|
+
}
|
|
1216
|
+
function resolveLock(resource, appDefault) {
|
|
1217
|
+
const errors = [];
|
|
1218
|
+
const resourceLock = resource === void 0 ? {} : collect(validateLock(resource, "lock"), errors);
|
|
1219
|
+
const defaultLock = appDefault === void 0 ? {} : collect(validateLock(appDefault, "default"), errors);
|
|
1220
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
1221
|
+
const lock = { hold: resourceLock.hold ?? defaultLock.hold ?? false };
|
|
1222
|
+
const retention = resourceLock.retention ?? defaultLock.retention;
|
|
1223
|
+
if (!retention) return { ok: true, lock };
|
|
1224
|
+
const derivatives = resourceLock.retention?.derivatives ?? defaultLock.retention?.derivatives ?? DEFAULT_RETENTION_DERIVATIVES;
|
|
1225
|
+
lock.retention = { ...retention, derivatives };
|
|
1226
|
+
return { ok: true, lock };
|
|
1227
|
+
}
|
|
1228
|
+
function collect(validation, errors) {
|
|
1229
|
+
if (validation.ok) return validation.lock;
|
|
1230
|
+
errors.push(...validation.errors);
|
|
1231
|
+
return {};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/lock-change.ts
|
|
1235
|
+
var toMs = (instant) => instant instanceof Date ? instant.getTime() : new Date(instant).getTime();
|
|
1236
|
+
function retentionEnd(lock, timing) {
|
|
1237
|
+
const { retention } = lock;
|
|
1238
|
+
if (!retention) return { kind: "none" };
|
|
1239
|
+
if (retention.until !== void 0) return { kind: "at", ms: toMs(retention.until) };
|
|
1240
|
+
const period = retention.period ?? 0;
|
|
1241
|
+
if (lock.hold) return { kind: "pending", period };
|
|
1242
|
+
const start = timing?.holdReleasedAt ?? timing?.uploadedAt;
|
|
1243
|
+
if (start === void 0) throw new RangeError("lock: a retention period needs uploadedAt or holdReleasedAt");
|
|
1244
|
+
return { kind: "at", ms: toMs(start) + period * 1e3 };
|
|
1245
|
+
}
|
|
1246
|
+
function effectiveUntil(lock, timing) {
|
|
1247
|
+
const end = retentionEnd(lock, timing);
|
|
1248
|
+
return end.kind === "at" ? new Date(end.ms).toISOString() : void 0;
|
|
1249
|
+
}
|
|
1250
|
+
function isLocked(lock, now, timing) {
|
|
1251
|
+
if (lock.hold) return true;
|
|
1252
|
+
const end = retentionEnd(lock, timing);
|
|
1253
|
+
return end.kind === "at" && toMs(now) < end.ms;
|
|
1254
|
+
}
|
|
1255
|
+
function afterTiming(before, after, options) {
|
|
1256
|
+
if (!before.hold || after.hold) return options.timing;
|
|
1257
|
+
return { ...options.timing, holdReleasedAt: options.now };
|
|
1258
|
+
}
|
|
1259
|
+
function earliestEndMs(end, nowMs) {
|
|
1260
|
+
if (end.kind === "none") return Number.NEGATIVE_INFINITY;
|
|
1261
|
+
return end.kind === "at" ? end.ms : nowMs + end.period * 1e3;
|
|
1262
|
+
}
|
|
1263
|
+
function isShortened(beforeEnd, afterEnd, nowMs, isStillHeld) {
|
|
1264
|
+
if (afterEnd.kind === "none") return true;
|
|
1265
|
+
if (beforeEnd.kind === "pending" && afterEnd.kind === "at" && isStillHeld) return true;
|
|
1266
|
+
return earliestEndMs(afterEnd, nowMs) < earliestEndMs(beforeEnd, nowMs);
|
|
1267
|
+
}
|
|
1268
|
+
function isWeakerSetting(before, after) {
|
|
1269
|
+
if (!after) return true;
|
|
1270
|
+
if (before.mode === "compliance" && after.mode === "governance") return true;
|
|
1271
|
+
return (before.derivatives ?? "inherit") === "inherit" && after.derivatives === "original-only";
|
|
1272
|
+
}
|
|
1273
|
+
function isRetentionActive(end, nowMs) {
|
|
1274
|
+
if (end.kind === "pending") return true;
|
|
1275
|
+
return end.kind === "at" && nowMs < end.ms;
|
|
1276
|
+
}
|
|
1277
|
+
function canChange(before, after, options) {
|
|
1278
|
+
const nowMs = toMs(options.now);
|
|
1279
|
+
const beforeEnd = retentionEnd(before, options.timing);
|
|
1280
|
+
if (!before.retention || !isRetentionActive(beforeEnd, nowMs)) return { ok: true, bypass: false };
|
|
1281
|
+
const afterEnd = retentionEnd(after, afterTiming(before, after, options));
|
|
1282
|
+
const isShortenedEnd = isShortened(beforeEnd, afterEnd, nowMs, after.hold === true);
|
|
1283
|
+
const isWeakened = isShortenedEnd || isWeakerSetting(before.retention, after.retention);
|
|
1284
|
+
if (!isWeakened) return { ok: true, bypass: false };
|
|
1285
|
+
if (before.retention.mode === "compliance") return { ok: false, reason: "compliance_extend_only" };
|
|
1286
|
+
if (!options.privileged) return { ok: false, reason: "lock_privilege_required" };
|
|
1287
|
+
return { ok: true, bypass: true };
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/path-prefix.ts
|
|
1291
|
+
var VISIBILITY_SEGMENTS2 = /* @__PURE__ */ new Set(["public", "private"]);
|
|
1292
|
+
var SAFE_SEGMENT2 = /^[A-Za-z0-9._-]+$/;
|
|
1293
|
+
var MAX_PATH_PREFIX_LENGTH = 256;
|
|
1294
|
+
function isSafeSegment2(segment) {
|
|
1295
|
+
return SAFE_SEGMENT2.test(segment) && segment !== "." && segment !== "..";
|
|
1296
|
+
}
|
|
1297
|
+
function normalizePathPrefix(raw) {
|
|
1298
|
+
if (typeof raw !== "string" || raw.length > MAX_PATH_PREFIX_LENGTH) return null;
|
|
1299
|
+
const segments = raw.trim().replace(/^\/+|\/+$/g, "").split("/");
|
|
1300
|
+
if (segments.length === 0 || segments[0] === "") return null;
|
|
1301
|
+
if (VISIBILITY_SEGMENTS2.has(segments[0])) return null;
|
|
1302
|
+
if (!segments.every(isSafeSegment2)) return null;
|
|
1303
|
+
return `${segments.join("/")}/`;
|
|
1304
|
+
}
|
|
1305
|
+
function relativeMediaPath(virtualKey) {
|
|
1306
|
+
const segments = virtualKey.split("/");
|
|
1307
|
+
if (!VISIBILITY_SEGMENTS2.has(segments[0] ?? "")) return null;
|
|
1308
|
+
const rest = segments.slice(1);
|
|
1309
|
+
const isTrailingSlash = rest.at(-1) === "";
|
|
1310
|
+
const named = isTrailingSlash ? rest.slice(0, -1) : rest;
|
|
1311
|
+
if (named.length === 0 || !named.every(isSafeSegment2)) return null;
|
|
1312
|
+
return `${named.join("/")}${isTrailingSlash ? "/" : ""}`;
|
|
1313
|
+
}
|
|
1314
|
+
function isWithinPathPrefix(relativePath, pathPrefix) {
|
|
1315
|
+
if (!pathPrefix) return true;
|
|
1316
|
+
return relativePath.startsWith(pathPrefix);
|
|
1317
|
+
}
|
|
1318
|
+
function isPrefixWithin(child, parent) {
|
|
1319
|
+
if (!parent) return true;
|
|
1320
|
+
if (!child) return false;
|
|
1321
|
+
return child.startsWith(parent);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/providers.ts
|
|
1325
|
+
var StorageProvider = {
|
|
1326
|
+
R2: "r2"
|
|
1327
|
+
};
|
|
1328
|
+
var TranscodeProvider = {
|
|
1329
|
+
FLY_IO: "fly.io"
|
|
1330
|
+
};
|
|
1331
|
+
var SpeechProvider = {
|
|
1332
|
+
GROQ_WHISPER: "groq-whisper"
|
|
1333
|
+
};
|
|
1334
|
+
var TranslationProvider = {
|
|
1335
|
+
MISTRAL: "mistral"
|
|
1336
|
+
};
|
|
1337
|
+
var ImageFit = {
|
|
1338
|
+
COVER: "cover",
|
|
1339
|
+
CONTAIN: "contain",
|
|
1340
|
+
FILL: "fill",
|
|
1341
|
+
INSIDE: "inside"
|
|
1342
|
+
};
|
|
1343
|
+
var ImageFormat = {
|
|
1344
|
+
WEBP: "webp",
|
|
1345
|
+
AVIF: "avif",
|
|
1346
|
+
JPEG: "jpeg",
|
|
1347
|
+
PNG: "png",
|
|
1348
|
+
AUTO: "auto"
|
|
1349
|
+
};
|
|
1350
|
+
var SecurityLevel = {
|
|
1351
|
+
SIGNED_URLS: 1,
|
|
1352
|
+
TOKEN_BINDING: 2,
|
|
1353
|
+
IP_FINGERPRINT: 3,
|
|
1354
|
+
AES_128_HLS: 4
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
// src/signing.ts
|
|
1358
|
+
async function importHmacKey(secret) {
|
|
1359
|
+
const encoder2 = new TextEncoder();
|
|
1360
|
+
return crypto.subtle.importKey("raw", encoder2.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
1361
|
+
"sign",
|
|
1362
|
+
"verify"
|
|
1363
|
+
]);
|
|
1364
|
+
}
|
|
1365
|
+
function bufferToHex(buffer) {
|
|
1366
|
+
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1367
|
+
}
|
|
1368
|
+
function hexToBuffer(hex) {
|
|
1369
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
1370
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
1371
|
+
bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
|
|
1372
|
+
}
|
|
1373
|
+
return bytes.buffer;
|
|
1374
|
+
}
|
|
1375
|
+
async function signPayload(payload, secret) {
|
|
1376
|
+
const key = await importHmacKey(secret);
|
|
1377
|
+
const encoder2 = new TextEncoder();
|
|
1378
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder2.encode(payload));
|
|
1379
|
+
return bufferToHex(signature);
|
|
1380
|
+
}
|
|
1381
|
+
async function verifySignature(payload, signature, secret) {
|
|
1382
|
+
const key = await importHmacKey(secret);
|
|
1383
|
+
const encoder2 = new TextEncoder();
|
|
1384
|
+
const signatureBuffer = hexToBuffer(signature);
|
|
1385
|
+
return crypto.subtle.verify("HMAC", key, signatureBuffer, encoder2.encode(payload));
|
|
1386
|
+
}
|
|
1387
|
+
async function signUrl(url, secret, expiresIn) {
|
|
1388
|
+
const expiry = expiresIn ?? DEFAULT_PRESIGN_EXPIRY_SECONDS;
|
|
1389
|
+
const expires = Math.floor(Date.now() / 1e3) + expiry;
|
|
1390
|
+
const parsed = new URL(url);
|
|
1391
|
+
parsed.searchParams.set("expires", String(expires));
|
|
1392
|
+
const payload = `${parsed.origin}${parsed.pathname}?expires=${expires}`;
|
|
1393
|
+
const signature = await signPayload(payload, secret);
|
|
1394
|
+
parsed.searchParams.set("signature", signature);
|
|
1395
|
+
return parsed.toString();
|
|
1396
|
+
}
|
|
1397
|
+
async function verifySignedUrl(signedUrl, secret) {
|
|
1398
|
+
const parsed = new URL(signedUrl);
|
|
1399
|
+
const expires = parsed.searchParams.get("expires");
|
|
1400
|
+
const signature = parsed.searchParams.get("signature");
|
|
1401
|
+
if (!expires || !signature) return false;
|
|
1402
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1403
|
+
if (now > Number(expires)) return false;
|
|
1404
|
+
const payload = `${parsed.origin}${parsed.pathname}?expires=${expires}`;
|
|
1405
|
+
return verifySignature(payload, signature, secret);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/storage.ts
|
|
1409
|
+
import {
|
|
1410
|
+
DeleteObjectCommand,
|
|
1411
|
+
GetObjectCommand,
|
|
1412
|
+
HeadObjectCommand,
|
|
1413
|
+
PutObjectCommand,
|
|
1414
|
+
S3Client
|
|
1415
|
+
} from "@aws-sdk/client-s3";
|
|
1416
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
1417
|
+
function createStorageClient(config) {
|
|
1418
|
+
const client = new S3Client({
|
|
1419
|
+
region: "auto",
|
|
1420
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
1421
|
+
credentials: {
|
|
1422
|
+
accessKeyId: config.accessKeyId,
|
|
1423
|
+
secretAccessKey: config.secretAccessKey
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
return {
|
|
1427
|
+
async createUploadUrl(options) {
|
|
1428
|
+
const command = new PutObjectCommand({
|
|
1429
|
+
Bucket: config.bucket,
|
|
1430
|
+
Key: options.key,
|
|
1431
|
+
ContentType: options.contentType,
|
|
1432
|
+
Metadata: options.metadata,
|
|
1433
|
+
...options.storageClass ? { StorageClass: options.storageClass } : {},
|
|
1434
|
+
...options.contentDisposition ? { ContentDisposition: options.contentDisposition } : {}
|
|
1435
|
+
});
|
|
1436
|
+
return getSignedUrl(client, command, {
|
|
1437
|
+
expiresIn: options.expiresIn ?? DEFAULT_PRESIGN_EXPIRY_SECONDS
|
|
1438
|
+
});
|
|
1439
|
+
},
|
|
1440
|
+
async createDownloadUrl(options) {
|
|
1441
|
+
const command = new GetObjectCommand({
|
|
1442
|
+
Bucket: config.bucket,
|
|
1443
|
+
Key: options.key
|
|
1444
|
+
});
|
|
1445
|
+
return getSignedUrl(client, command, {
|
|
1446
|
+
expiresIn: options.expiresIn ?? DEFAULT_PRESIGN_EXPIRY_SECONDS
|
|
1447
|
+
});
|
|
1448
|
+
},
|
|
1449
|
+
async deleteObject(key) {
|
|
1450
|
+
await client.send(
|
|
1451
|
+
new DeleteObjectCommand({
|
|
1452
|
+
Bucket: config.bucket,
|
|
1453
|
+
Key: key
|
|
1454
|
+
})
|
|
1455
|
+
);
|
|
1456
|
+
},
|
|
1457
|
+
async headObject(key) {
|
|
1458
|
+
try {
|
|
1459
|
+
const response = await client.send(
|
|
1460
|
+
new HeadObjectCommand({
|
|
1461
|
+
Bucket: config.bucket,
|
|
1462
|
+
Key: key
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1465
|
+
return {
|
|
1466
|
+
size: response.ContentLength ?? 0,
|
|
1467
|
+
contentType: response.ContentType ?? "application/octet-stream",
|
|
1468
|
+
lastModified: response.LastModified ?? /* @__PURE__ */ new Date()
|
|
1469
|
+
};
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (error instanceof Error && "name" in error && error.name === "NotFound") {
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1395
1480
|
// src/router.ts
|
|
1396
1481
|
function isDirectStorageConfig(config) {
|
|
1397
1482
|
return "storage" in config;
|
|
@@ -1876,13 +1961,21 @@ export {
|
|
|
1876
1961
|
DEFAULT_SECURITY,
|
|
1877
1962
|
DEFAULT_TOKEN_EXPIRY_SECONDS,
|
|
1878
1963
|
DEFAULT_VISIBILITY,
|
|
1964
|
+
DOWNLOAD_GRANT_ID_PREFIX,
|
|
1965
|
+
DOWNLOAD_GRANT_PREFIX,
|
|
1879
1966
|
DevulturApiError,
|
|
1967
|
+
DownloadGrantErrorCode,
|
|
1968
|
+
DownloadGrantLimits,
|
|
1969
|
+
ENCRYPTED_HEADER,
|
|
1880
1970
|
ENCRYPTION_ALG,
|
|
1881
1971
|
ENCRYPTION_VERSION,
|
|
1882
1972
|
EXPIRES_AT_HEADER,
|
|
1883
1973
|
FILE_MIMES,
|
|
1884
1974
|
GENERATED_PRIVATE_CACHE_CONTROL,
|
|
1885
1975
|
GENERATED_PUBLIC_CACHE_CONTROL,
|
|
1976
|
+
GRANT_EXPIRES_AT_HEADER,
|
|
1977
|
+
GRANT_KEY_HEADER,
|
|
1978
|
+
GRANT_METADATA_HEADER,
|
|
1886
1979
|
HOLD_PAGE_SIZE,
|
|
1887
1980
|
IMAGE_HEIGHT_HEADER,
|
|
1888
1981
|
IMAGE_MIMES,
|
|
@@ -1944,6 +2037,7 @@ export {
|
|
|
1944
2037
|
counterForBlock,
|
|
1945
2038
|
coversScope,
|
|
1946
2039
|
createDecryptStream,
|
|
2040
|
+
createGrantClient,
|
|
1947
2041
|
createHTTPWebhookHandler,
|
|
1948
2042
|
createLockClient,
|
|
1949
2043
|
createMediaRouter,
|
|
@@ -1972,6 +2066,7 @@ export {
|
|
|
1972
2066
|
importEncryptionKey,
|
|
1973
2067
|
isAssetKey,
|
|
1974
2068
|
isBearerOf,
|
|
2069
|
+
isDownloadGrant,
|
|
1975
2070
|
isIsoInstant,
|
|
1976
2071
|
isLocked,
|
|
1977
2072
|
isPrefixWithin,
|