@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +123 -1
  2. package/dist/index.js +475 -380
  3. 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/integrity.ts
426
- var SHA256_HEADER = "x-devultur-sha256";
427
- var SHA256_VERIFIED_HEADER = "x-devultur-sha256-verified";
428
- var EXPIRES_AT_HEADER = "x-devultur-expires-at";
429
- var IMAGE_WIDTH_HEADER = "x-devultur-image-width";
430
- var IMAGE_HEIGHT_HEADER = "x-devultur-image-height";
431
- var REPR_DIGEST_HEADER = "Repr-Digest";
432
- var SHA256_HEX = /^[0-9a-f]{64}$/;
433
- function normalizeSha256(raw) {
434
- if (typeof raw !== "string") return null;
435
- const value = raw.trim().toLowerCase();
436
- return SHA256_HEX.test(value) ? value : null;
437
- }
438
- function bytesToHex(bytes) {
439
- return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
440
- }
441
- async function sha256Hex(data) {
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 validateRetention(input, path, errors) {
468
- if (!isObject(input)) {
469
- errors.push(`${path}: must be an object`);
470
- return void 0;
471
- }
472
- const errorCount = errors.length;
473
- rejectUnknownKeys(input, RETENTION_FIELDS, path, errors);
474
- const { mode, until, period, derivatives } = input;
475
- if (!RETENTION_MODES.includes(mode)) {
476
- errors.push(`${path}.mode: must be "governance" or "compliance"`);
477
- }
478
- if (until !== void 0 && !isIsoInstant(until)) {
479
- errors.push(`${path}.until: must be an ISO 8601 date-time with a zone`);
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
- if (errors.length > errorCount) return void 0;
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/api-error.ts
704
- var DevulturApiError = class extends Error {
705
- status;
706
- code;
707
- limit;
708
- /**
709
- * The response body's fields other than `error` and `code`, e.g. `{ key, mode, until, hold }`
710
- * on a `409 retention_active`. `undefined` when the body had none (or wasn't JSON).
711
- */
712
- details;
713
- constructor(message, status, code, limit, details) {
714
- super(message);
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
- async function errorFromResponse(response) {
727
- let message = `API error: ${response.status}`;
728
- let code;
729
- let limit;
730
- let details;
731
- try {
732
- const body = await response.json();
733
- if (typeof body === "object" && body !== null && !Array.isArray(body)) {
734
- if ("error" in body && typeof body.error === "string") message = body.error;
735
- if ("code" in body && typeof body.code === "string") code = body.code;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devultur/core",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Shared config, types, storage path conventions, media references and URL signing for the Devultur media SDKs",
5
5
  "keywords": [
6
6
  "media",