@devultur/self-host 1.1.0 → 1.2.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 +112 -4
- package/dist/index.js +377 -41
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import { Context, MiddlewareHandler, Hono } from 'hono';
|
|
3
|
-
import { Visibility, ObjectLock, SecurityPolicy, CacheRule, TranscodeSettings, TranscodeOutputKeys, TranscodeOutputUrls } from '@devultur/core';
|
|
3
|
+
import { Visibility, ObjectLock, SecurityPolicy, CacheRule, TranscodeSettings, RetentionMode, TranscodeOutputKeys, TranscodeOutputUrls } from '@devultur/core';
|
|
4
4
|
import { T as TranscodeContainerNamespace } from './cloudflare-kZ7J3LQ2.js';
|
|
5
5
|
|
|
6
6
|
/** What an upload token allows (claims `res`, `visibility`, `max_bytes`, `max_files`, `file_ttl`, `lock`). */
|
|
@@ -444,6 +444,9 @@ declare const LOCK_UNTIL_HEADER = "x-devultur-lock-until";
|
|
|
444
444
|
/** `true` while a hold is set. */
|
|
445
445
|
declare const LOCK_HOLD_HEADER = "x-devultur-lock-hold";
|
|
446
446
|
|
|
447
|
+
/** `customMetadata` key holding the lock set at upload (JSON `{ retention?, hold }`). Owned by the Worker. */
|
|
448
|
+
declare const LOCK_METADATA_KEY = "lock";
|
|
449
|
+
|
|
447
450
|
/** Prefixes the Worker keeps for lock sidecars and audit records: no route reads or writes a key under them. */
|
|
448
451
|
declare const RESERVED_LOCK_PREFIXES: readonly ["__lock/", "__lock-audit/"];
|
|
449
452
|
|
|
@@ -458,8 +461,113 @@ interface HoldSkip {
|
|
|
458
461
|
reason: "not_found" | "unchanged" | "lock_conflict" | "lock_record_unreadable";
|
|
459
462
|
}
|
|
460
463
|
|
|
461
|
-
/**
|
|
462
|
-
|
|
464
|
+
/** The lock in force on one stored object. */
|
|
465
|
+
interface StoredObjectLock {
|
|
466
|
+
/** True while the object can't be deleted or overwritten. */
|
|
467
|
+
locked: boolean;
|
|
468
|
+
/** A hold is set (an unreadable lock record reads as one). */
|
|
469
|
+
hold: boolean;
|
|
470
|
+
/** ISO time the retention ends; left out with no retention, or while a hold keeps a `period` from starting. */
|
|
471
|
+
until?: string;
|
|
472
|
+
mode?: RetentionMode;
|
|
473
|
+
/** The lock record can't be read, so it's held until an operator repairs it. */
|
|
474
|
+
unreadable?: true;
|
|
475
|
+
/**
|
|
476
|
+
* The key isn't laid out like the Worker's (`{storage prefix}{public|private}/…`), so its lock record can't be
|
|
477
|
+
* found: it's kept, never deleted.
|
|
478
|
+
*/
|
|
479
|
+
unrecognized?: true;
|
|
480
|
+
/** Storage key of the original the lock belongs to, when it isn't the object itself (a video's raw, say). */
|
|
481
|
+
lockedBy?: string;
|
|
482
|
+
}
|
|
483
|
+
interface LockLookupOptions {
|
|
484
|
+
mediaBucket: R2Bucket;
|
|
485
|
+
/**
|
|
486
|
+
* The storage prefix the keys live under (the Worker's `storagePrefix`, `""` for the bucket root). Omitted: read
|
|
487
|
+
* from each key, as everything before its first `public` or `private` segment. Pass it when a prefix of yours
|
|
488
|
+
* has a segment named `public` or `private`.
|
|
489
|
+
*/
|
|
490
|
+
root?: string;
|
|
491
|
+
/** Default: now. */
|
|
492
|
+
now?: Date;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The lock in force on the object at `storageKey` (the full R2 key), or `null` when there's no such object (or it's
|
|
496
|
+
* a lock record). A file under a video's folder (an HLS segment, a caption) is locked by the video's raws unless
|
|
497
|
+
* they're `original-only`; an image variant by its original, likewise.
|
|
498
|
+
*/
|
|
499
|
+
declare function readObjectLock(options: LockLookupOptions, storageKey: string): Promise<StoredObjectLock | null>;
|
|
500
|
+
|
|
501
|
+
interface PrefixWalkOptions extends LockLookupOptions {
|
|
502
|
+
/** Storage prefix to walk (full R2 keys, the storage prefix of the keys included). */
|
|
503
|
+
prefix: string;
|
|
504
|
+
/** From a previous call's result, to go on where it stopped. */
|
|
505
|
+
cursor?: string;
|
|
506
|
+
/** Objects listed per page. Default 100, at most 1000. */
|
|
507
|
+
batchSize?: number;
|
|
508
|
+
/**
|
|
509
|
+
* Bucket calls this call may make. Default 40: under the free plan's 50 subrequests per request, with room for the
|
|
510
|
+
* caller's own. Raise it on a paid plan.
|
|
511
|
+
*/
|
|
512
|
+
maxSubrequests?: number;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* No `now`: locks are judged by the real clock, so no caller can make a retention read as expired. (The read-only
|
|
517
|
+
* `readObjectLock` and `retainedSummary` take one, to ask "what about then?".)
|
|
518
|
+
*/
|
|
519
|
+
interface PrefixDeleteOptions extends Omit<PrefixWalkOptions, "now"> {
|
|
520
|
+
/** The Worker's `KEYS_BUCKET`, when it has one: the encryption keys of what's deleted go too. */
|
|
521
|
+
keysBucket?: R2Bucket;
|
|
522
|
+
}
|
|
523
|
+
/** An object kept because it's locked (or its original is, or its key isn't laid out like the Worker's). */
|
|
524
|
+
interface RetainedObject extends StoredObjectLock {
|
|
525
|
+
/** Its storage key. */
|
|
526
|
+
key: string;
|
|
527
|
+
size: number;
|
|
528
|
+
}
|
|
529
|
+
interface PrefixDeleteResult {
|
|
530
|
+
deletedCount: number;
|
|
531
|
+
/** The sizes of the versions deleted, read right before each delete. */
|
|
532
|
+
deletedBytes: number;
|
|
533
|
+
/** Encryption keys deleted from the keys bucket. */
|
|
534
|
+
deletedKeyCount: number;
|
|
535
|
+
retainedCount: number;
|
|
536
|
+
retainedBytes: number;
|
|
537
|
+
/** What this call kept, in key order. */
|
|
538
|
+
retained: RetainedObject[];
|
|
539
|
+
/** Lock records and audit objects passed over (never deleted). */
|
|
540
|
+
skippedCount: number;
|
|
541
|
+
/** Set when the prefix isn't done: pass it back to go on. */
|
|
542
|
+
cursor?: string;
|
|
543
|
+
/** Bucket calls this call made. */
|
|
544
|
+
subrequests: number;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Deletes what's free under `options.prefix` (full storage keys) and keeps what's locked. Bounded: it never makes more
|
|
548
|
+
* than `maxSubrequests` bucket calls, and returns a `cursor` to call it again with. Nothing is audited: log `retained`
|
|
549
|
+
* if you need a trail. R2 deletes aren't conditional: a lock set between an object's check and its delete isn't seen
|
|
550
|
+
* (milliseconds for its own lock; for a lock it inherits, up to `LOCK_CACHE_MAX_AGE_MS`, as that lock is cached).
|
|
551
|
+
*/
|
|
552
|
+
declare function deletePrefixRespectingLocks(options: PrefixDeleteOptions): Promise<PrefixDeleteResult>;
|
|
553
|
+
|
|
554
|
+
interface RetainedSummary {
|
|
555
|
+
/** Objects looked at (lock records and audit not included). */
|
|
556
|
+
scannedCount: number;
|
|
557
|
+
/** What a delete would keep, keys not laid out like the Worker's included. */
|
|
558
|
+
retainedCount: number;
|
|
559
|
+
retainedBytes: number;
|
|
560
|
+
/** The latest retention end among what's retained (ISO). Holds have none: see `hasHold`. */
|
|
561
|
+
latestUntil?: string;
|
|
562
|
+
/** Something retained is held (or has an unreadable lock record): it stays until released, not until a date. */
|
|
563
|
+
hasHold: boolean;
|
|
564
|
+
/** Set when the prefix isn't done: pass it back to go on. Combine the counts of every call. */
|
|
565
|
+
cursor?: string;
|
|
566
|
+
/** Bucket calls this call made. */
|
|
567
|
+
subrequests: number;
|
|
568
|
+
}
|
|
569
|
+
/** What's retained under `options.prefix` (full storage keys). Bounded and resumable like the delete. */
|
|
570
|
+
declare function retainedSummary(options: PrefixWalkOptions): Promise<RetainedSummary>;
|
|
463
571
|
|
|
464
572
|
declare const LOCK_SIDECAR_PREFIX = "__lock/";
|
|
465
573
|
interface LockSidecar {
|
|
@@ -644,4 +752,4 @@ interface CreateWorkerOptions {
|
|
|
644
752
|
}
|
|
645
753
|
declare function createWorker(options?: CreateWorkerOptions): Hono<WorkerApp, hono_types.BlankSchema, "/">;
|
|
646
754
|
|
|
647
|
-
export { type AllowedCredential, type AssetListEntry, type AuthorizeLockChangeHook, type BeforeLinkRedeemHook, type ComposedPlugins, type CreateWorkerOptions, type CredentialDecision, type CredentialKind, type CredentialPolicyOptions, type CredentialRequest, DownloadLink, type DownloadLinkInfo, type DownloadLinkRecord, type DownloadLinkStorage, ENCRYPTION_KEY_MISSING_CODE, type EncodeMetrics, HOLD_PAGE_SIZE, type HoldSkip, type ImageTransformParams, type IssueCredentialHook, type Json, KEYS_BUCKET_MISSING_CODE, LOCK_AUDIT_PREFIX, LOCK_HOLD_HEADER, LOCK_METADATA_KEY, LOCK_SIDECAR_PREFIX, LOCK_UNTIL_HEADER, type LockAuditAction, type LockAuditRecord, type LockChangeAction, type LockChangeDecision, type LockChangeRequest, type LockSidecar, type LockSource, MAX_ASSET_LIST_LIMIT, MAX_EXPIRED_LOOKUPS, MAX_UPLOAD_TOKEN_TTL_SECONDS, MaxResolution, ObjectMeta, type OnLockAuditHook, type PendingUploadInfo, type PluginContext, type PluginMiddleware, type PluginRoute, RESERVED_LOCK_PREFIXES, type RawStorageClass, type RequestAccess, type RequestState, type RequestStatePatch, type RouteResult, TOKEN_REJECTED_CODE, type TranscodeConfig, type TranscodeJob, UPLOAD_TOKEN_TYPE, type UploadGrant, type UploadRules, type UploadTypeRule, type WorkerApp, WorkerAuthErrorCode, type WorkerConfig, type WorkerEnv, type WorkerPlugin, type WorkerVariables, cancelPendingUpload, createWorker, credentialPolicy, defaultPlugins, deletePrefix, downloadLinks, edgeCacheTag, getPendingUpload, getRequestState, getRouteResult, hashKey, imageOriginAuthorization, isAuthenticated, isImageOriginRequest, isObjectExpired, isOpenRoute, isServiceToken, isUploadUrl, isVariantKey, logEncodeMetrics, parseEncodeMetrics, parseTransformParams, readImageOriginScope, readStoryboardKey, readTokenScopes, readTranscodeOutputKeys, readTranscodeOutputs, servedCacheControl, staticConfig, storageKey, updateRequestState, uploadGrantFromClaims, variantPrefix, vcpuSecondsPerVideoMinute, verifyViewerToken, withOutputUrls, workerSecretAuth };
|
|
755
|
+
export { type AllowedCredential, type AssetListEntry, type AuthorizeLockChangeHook, type BeforeLinkRedeemHook, type ComposedPlugins, type CreateWorkerOptions, type CredentialDecision, type CredentialKind, type CredentialPolicyOptions, type CredentialRequest, DownloadLink, type DownloadLinkInfo, type DownloadLinkRecord, type DownloadLinkStorage, ENCRYPTION_KEY_MISSING_CODE, type EncodeMetrics, HOLD_PAGE_SIZE, type HoldSkip, type ImageTransformParams, type IssueCredentialHook, type Json, KEYS_BUCKET_MISSING_CODE, LOCK_AUDIT_PREFIX, LOCK_HOLD_HEADER, LOCK_METADATA_KEY, LOCK_SIDECAR_PREFIX, LOCK_UNTIL_HEADER, type LockAuditAction, type LockAuditRecord, type LockChangeAction, type LockChangeDecision, type LockChangeRequest, type LockLookupOptions, type LockSidecar, type LockSource, MAX_ASSET_LIST_LIMIT, MAX_EXPIRED_LOOKUPS, MAX_UPLOAD_TOKEN_TTL_SECONDS, MaxResolution, ObjectMeta, type OnLockAuditHook, type PendingUploadInfo, type PluginContext, type PluginMiddleware, type PluginRoute, type PrefixDeleteOptions, type PrefixDeleteResult, type PrefixWalkOptions, RESERVED_LOCK_PREFIXES, type RawStorageClass, type RequestAccess, type RequestState, type RequestStatePatch, type RetainedObject, type RetainedSummary, type RouteResult, type StoredObjectLock, TOKEN_REJECTED_CODE, type TranscodeConfig, type TranscodeJob, UPLOAD_TOKEN_TYPE, type UploadGrant, type UploadRules, type UploadTypeRule, type WorkerApp, WorkerAuthErrorCode, type WorkerConfig, type WorkerEnv, type WorkerPlugin, type WorkerVariables, cancelPendingUpload, createWorker, credentialPolicy, defaultPlugins, deletePrefix, deletePrefixRespectingLocks, downloadLinks, edgeCacheTag, getPendingUpload, getRequestState, getRouteResult, hashKey, imageOriginAuthorization, isAuthenticated, isImageOriginRequest, isObjectExpired, isOpenRoute, isServiceToken, isUploadUrl, isVariantKey, logEncodeMetrics, parseEncodeMetrics, parseTransformParams, readImageOriginScope, readObjectLock, readStoryboardKey, readTokenScopes, readTranscodeOutputKeys, readTranscodeOutputs, retainedSummary, servedCacheControl, staticConfig, storageKey, updateRequestState, uploadGrantFromClaims, variantPrefix, vcpuSecondsPerVideoMinute, verifyViewerToken, withOutputUrls, workerSecretAuth };
|
package/dist/index.js
CHANGED
|
@@ -73,7 +73,7 @@ import { cors } from "hono/cors";
|
|
|
73
73
|
import { logger } from "hono/logger";
|
|
74
74
|
|
|
75
75
|
// src/object-lock/guard.ts
|
|
76
|
-
import {
|
|
76
|
+
import { isVideoRawKey } from "@devultur/core";
|
|
77
77
|
|
|
78
78
|
// src/plugin-hooks.ts
|
|
79
79
|
var PLUGIN_HOOKS_VARIABLE = "pluginHooks";
|
|
@@ -152,11 +152,47 @@ async function recordLockAudit(context, input) {
|
|
|
152
152
|
await inBackground(context, deliver(context, root, record));
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// src/object-lock/object-keys.ts
|
|
156
|
+
import { decodeEncryptionMeta } from "@devultur/core";
|
|
157
|
+
var DELETE_BATCH_SIZE = 1e3;
|
|
158
|
+
async function listAll(bucket, options) {
|
|
159
|
+
const objects = [];
|
|
160
|
+
let cursor;
|
|
161
|
+
do {
|
|
162
|
+
const listing = await bucket.list({ ...options, ...cursor ? { cursor } : {} });
|
|
163
|
+
objects.push(...listing.objects);
|
|
164
|
+
cursor = listing.truncated ? listing.cursor : void 0;
|
|
165
|
+
} while (cursor);
|
|
166
|
+
return objects;
|
|
167
|
+
}
|
|
168
|
+
var isWrittenBy = (key, writtenBy) => key.uploaded.getTime() <= writtenBy.getTime();
|
|
169
|
+
async function keysWrittenBy(keysBucket, prefix, writtenBy) {
|
|
170
|
+
return (await listAll(keysBucket, { prefix })).filter((key) => isWrittenBy(key, writtenBy)).map((key) => key.key);
|
|
171
|
+
}
|
|
172
|
+
async function objectKeysToDelete(keysBucket, fullKey, deleted) {
|
|
173
|
+
const own = await listAll(keysBucket, { prefix: `${fullKey}/`, delimiter: "/" });
|
|
174
|
+
const keys = new Set(own.filter((key) => isWrittenBy(key, deleted.uploaded)).map((key) => key.key));
|
|
175
|
+
const meta = decodeEncryptionMeta(deleted.customMetadata?.enc);
|
|
176
|
+
if (meta) keys.add(`${fullKey}/${meta.keyId}`);
|
|
177
|
+
for (const variantKey of await listAll(keysBucket, { prefix: variantPrefix(fullKey) })) keys.add(variantKey.key);
|
|
178
|
+
return [...keys];
|
|
179
|
+
}
|
|
180
|
+
var deleteCallsFor = (keys) => Math.ceil(keys.length / DELETE_BATCH_SIZE);
|
|
181
|
+
async function deleteKeys(keysBucket, keys) {
|
|
182
|
+
for (let start = 0; start < keys.length; start += DELETE_BATCH_SIZE) {
|
|
183
|
+
await keysBucket.delete(keys.slice(start, start + DELETE_BATCH_SIZE));
|
|
184
|
+
}
|
|
185
|
+
return keys.length;
|
|
186
|
+
}
|
|
187
|
+
async function sweepObjectKeys(keysBucket, fullKey, deleted) {
|
|
188
|
+
return deleteKeys(keysBucket, await objectKeysToDelete(keysBucket, fullKey, deleted));
|
|
189
|
+
}
|
|
190
|
+
|
|
155
191
|
// src/object-lock/sidecar.ts
|
|
156
192
|
import { isIsoInstant, validateLock as validateLock2 } from "@devultur/core";
|
|
157
193
|
|
|
158
194
|
// src/object-lock/state.ts
|
|
159
|
-
import { isLocked, validateLock } from "@devultur/core";
|
|
195
|
+
import { effectiveUntil, isLocked, validateLock } from "@devultur/core";
|
|
160
196
|
var LOCK_METADATA_KEY = "lock";
|
|
161
197
|
function encodeLockMetadata(lock) {
|
|
162
198
|
if (!lock || !lock.retention && !lock.hold) return void 0;
|
|
@@ -187,6 +223,13 @@ function isStateLocked(state, now) {
|
|
|
187
223
|
return true;
|
|
188
224
|
}
|
|
189
225
|
}
|
|
226
|
+
function untilOf(state) {
|
|
227
|
+
try {
|
|
228
|
+
return effectiveUntil(state.lock, { uploadedAt: state.uploadedAt, holdReleasedAt: state.holdReleasedAt });
|
|
229
|
+
} catch {
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
190
233
|
|
|
191
234
|
// src/object-lock/sidecar.ts
|
|
192
235
|
var LOCK_SIDECAR_PREFIX = "__lock/";
|
|
@@ -289,32 +332,34 @@ function protectsDerived(state) {
|
|
|
289
332
|
const derivatives = state.lock.retention?.derivatives ?? "inherit";
|
|
290
333
|
return derivatives === "inherit";
|
|
291
334
|
}
|
|
292
|
-
async function
|
|
293
|
-
const
|
|
294
|
-
const state = object ? (await effectiveLock(context.env.MEDIA_BUCKET, root, key, object)).state : null;
|
|
335
|
+
async function lockedObject(bucket, root, key, object, now) {
|
|
336
|
+
const { state } = await effectiveLock(bucket, root, key, object);
|
|
295
337
|
return state && isStateLocked(state, now) ? { key, state } : null;
|
|
296
338
|
}
|
|
297
|
-
async function
|
|
339
|
+
async function lockedOriginal(bucket, root, key, now) {
|
|
340
|
+
const object = await bucket.head(`${root}${key}`);
|
|
341
|
+
return object ? lockedObject(bucket, root, key, object, now) : null;
|
|
342
|
+
}
|
|
343
|
+
async function lockedRaw(bucket, root, rawPrefix, now) {
|
|
298
344
|
let cursor;
|
|
299
345
|
do {
|
|
300
|
-
const listing = await
|
|
346
|
+
const listing = await bucket.list({ prefix: `${root}${rawPrefix}`, cursor });
|
|
301
347
|
for (const listed of listing.objects) {
|
|
302
348
|
const key = listed.key.slice(root.length);
|
|
303
|
-
const object = await
|
|
304
|
-
const state = object ? (await effectiveLock(
|
|
349
|
+
const object = await bucket.head(listed.key);
|
|
350
|
+
const state = object ? (await effectiveLock(bucket, root, key, object)).state : null;
|
|
305
351
|
if (state && protectsDerived(state) && isStateLocked(state, now)) return { key, state };
|
|
306
352
|
}
|
|
307
353
|
cursor = listing.truncated ? listing.cursor : void 0;
|
|
308
354
|
} while (cursor);
|
|
309
355
|
return null;
|
|
310
356
|
}
|
|
357
|
+
function findLockedOriginal(bucket, root, subject, now) {
|
|
358
|
+
return subject.kind === "original" ? lockedOriginal(bucket, root, subject.key, now) : lockedRaw(bucket, root, subject.rawPrefix, now);
|
|
359
|
+
}
|
|
311
360
|
function retentionActive(context, locked, op) {
|
|
312
|
-
const { lock
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
until = effectiveUntil(lock, { uploadedAt, holdReleasedAt });
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
361
|
+
const { lock } = locked.state;
|
|
362
|
+
const until = untilOf(locked.state);
|
|
318
363
|
const verb = op === "delete" ? "deleted" : "overwritten";
|
|
319
364
|
return context.json(
|
|
320
365
|
{
|
|
@@ -339,7 +384,7 @@ async function assertWritable(context, keys, op, root = getRequestState(context)
|
|
|
339
384
|
const subjectId = subject.kind === "original" ? subject.key : subject.rawPrefix;
|
|
340
385
|
if (checked.has(subjectId)) continue;
|
|
341
386
|
checked.add(subjectId);
|
|
342
|
-
const locked =
|
|
387
|
+
const locked = await findLockedOriginal(context.env.MEDIA_BUCKET, root, subject, now);
|
|
343
388
|
if (!locked) continue;
|
|
344
389
|
const requested = key === locked.key ? [key] : [key, locked.key];
|
|
345
390
|
await recordLockAudit(context, {
|
|
@@ -355,7 +400,7 @@ async function assertWritable(context, keys, op, root = getRequestState(context)
|
|
|
355
400
|
}
|
|
356
401
|
return null;
|
|
357
402
|
}
|
|
358
|
-
var
|
|
403
|
+
var DELETE_BATCH_SIZE2 = 1e3;
|
|
359
404
|
async function guardedDeletePrefix(context, prefix) {
|
|
360
405
|
const root = getRequestState(context).storagePrefix;
|
|
361
406
|
const storageKeys = [];
|
|
@@ -368,38 +413,17 @@ async function guardedDeletePrefix(context, prefix) {
|
|
|
368
413
|
const keys = storageKeys.map((key) => key.slice(root.length));
|
|
369
414
|
const blocked = await assertWritable(context, keys, "delete");
|
|
370
415
|
if (blocked) return { blocked };
|
|
371
|
-
for (let start = 0; start < storageKeys.length; start +=
|
|
372
|
-
await context.env.MEDIA_BUCKET.delete(storageKeys.slice(start, start +
|
|
416
|
+
for (let start = 0; start < storageKeys.length; start += DELETE_BATCH_SIZE2) {
|
|
417
|
+
await context.env.MEDIA_BUCKET.delete(storageKeys.slice(start, start + DELETE_BATCH_SIZE2));
|
|
373
418
|
}
|
|
374
419
|
return { deletedCount: storageKeys.length };
|
|
375
420
|
}
|
|
376
|
-
async function ownKeysOf(keysBucket, fullKey) {
|
|
377
|
-
const keys = [];
|
|
378
|
-
let cursor;
|
|
379
|
-
do {
|
|
380
|
-
const listing = await keysBucket.list({ prefix: `${fullKey}/`, delimiter: "/", cursor });
|
|
381
|
-
keys.push(...listing.objects);
|
|
382
|
-
cursor = listing.truncated ? listing.cursor : void 0;
|
|
383
|
-
} while (cursor);
|
|
384
|
-
return keys;
|
|
385
|
-
}
|
|
386
|
-
function isKeyOfDeletedVersion(key, deleted) {
|
|
387
|
-
return key.uploaded.getTime() <= deleted.uploaded.getTime();
|
|
388
|
-
}
|
|
389
421
|
async function deleteKeysFor(context, objectKey, deleted) {
|
|
390
422
|
const blocked = await assertWritable(context, [objectKey], "delete");
|
|
391
423
|
if (blocked) return { blocked };
|
|
392
424
|
const keysBucket = context.env.KEYS_BUCKET;
|
|
393
425
|
if (!keysBucket) return { deletedCount: 0 };
|
|
394
|
-
|
|
395
|
-
const ownKeys = new Set(
|
|
396
|
-
(await ownKeysOf(keysBucket, fullKey)).filter((key) => isKeyOfDeletedVersion(key, deleted)).map((key) => key.key)
|
|
397
|
-
);
|
|
398
|
-
const meta = decodeEncryptionMeta(deleted.customMetadata?.enc);
|
|
399
|
-
if (meta) ownKeys.add(`${fullKey}/${meta.keyId}`);
|
|
400
|
-
if (ownKeys.size > 0) await keysBucket.delete([...ownKeys]);
|
|
401
|
-
const variantKeyCount = await deletePrefix(keysBucket, variantPrefix(fullKey));
|
|
402
|
-
return { deletedCount: variantKeyCount + ownKeys.size };
|
|
426
|
+
return { deletedCount: await sweepObjectKeys(keysBucket, storageKey(context, objectKey), deleted) };
|
|
403
427
|
}
|
|
404
428
|
async function deleteFolderKeys(context, videoFolder) {
|
|
405
429
|
const blocked = await assertWritable(context, [videoFolder], "delete");
|
|
@@ -4923,6 +4947,315 @@ upload.put("/:key{.+}", async (context) => {
|
|
|
4923
4947
|
return context.json({ key, size: body.byteLength, sha256: digest.sha256, sha256Verified: true });
|
|
4924
4948
|
});
|
|
4925
4949
|
|
|
4950
|
+
// src/object-lock/lock-status.ts
|
|
4951
|
+
var VISIBILITY_SEGMENTS = /* @__PURE__ */ new Set(["public", "private"]);
|
|
4952
|
+
var RESERVED_SEGMENT = /__lock(?:-audit)?$/;
|
|
4953
|
+
function placeOfStorageKey(storageKey2, root) {
|
|
4954
|
+
const segments = storageKey2.split("/");
|
|
4955
|
+
if (segments.some((segment) => RESERVED_SEGMENT.test(segment))) return { kind: "reserved" };
|
|
4956
|
+
if (root !== void 0) {
|
|
4957
|
+
const key = storageKey2.slice(root.length);
|
|
4958
|
+
const isWorkerKey = storageKey2.startsWith(root) && VISIBILITY_SEGMENTS.has(key.split("/")[0] ?? "");
|
|
4959
|
+
return isWorkerKey ? { kind: "object", root, key } : { kind: "unrecognized" };
|
|
4960
|
+
}
|
|
4961
|
+
const first = segments.findIndex((segment) => VISIBILITY_SEGMENTS.has(segment));
|
|
4962
|
+
if (first < 0) return { kind: "unrecognized" };
|
|
4963
|
+
const inferredRoot = first === 0 ? "" : `${segments.slice(0, first).join("/")}/`;
|
|
4964
|
+
return { kind: "object", root: inferredRoot, key: storageKey2.slice(inferredRoot.length) };
|
|
4965
|
+
}
|
|
4966
|
+
var UNRECOGNIZED_LOCK = { locked: true, hold: false, unrecognized: true };
|
|
4967
|
+
var VARIANTS_SEGMENT = "/__variants/";
|
|
4968
|
+
function retentionSubjectOf(key) {
|
|
4969
|
+
const isVariant = isVariantKey(key);
|
|
4970
|
+
const original = isVariant ? key.slice(0, key.indexOf(VARIANTS_SEGMENT)) : key;
|
|
4971
|
+
return { subject: lockSubjectOf(original) ?? { kind: "original", key: original }, isVariant };
|
|
4972
|
+
}
|
|
4973
|
+
function toStoredObjectLock(locked, place) {
|
|
4974
|
+
if (!locked) return { locked: false, hold: false };
|
|
4975
|
+
const { lock, isUnreadable } = locked.state;
|
|
4976
|
+
const until = untilOf(locked.state);
|
|
4977
|
+
return {
|
|
4978
|
+
locked: true,
|
|
4979
|
+
hold: lock.hold === true,
|
|
4980
|
+
...until ? { until } : {},
|
|
4981
|
+
...lock.retention ? { mode: lock.retention.mode } : {},
|
|
4982
|
+
...isUnreadable ? { unreadable: true } : {},
|
|
4983
|
+
...locked.key === place.key ? {} : { lockedBy: `${place.root}${locked.key}` }
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
var LOCK_CACHE_MAX_USES = 50;
|
|
4987
|
+
var LOCK_CACHE_MAX_AGE_MS = 5e3;
|
|
4988
|
+
function lockResolver(mediaBucket, now) {
|
|
4989
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4990
|
+
const resolveSubject = (root, subject) => {
|
|
4991
|
+
const id = `${root}\0${subject.kind === "original" ? subject.key : subject.rawPrefix}`;
|
|
4992
|
+
const cached = cache.get(id);
|
|
4993
|
+
const isFresh = cached && cached.uses < LOCK_CACHE_MAX_USES && Date.now() - cached.readAt < LOCK_CACHE_MAX_AGE_MS;
|
|
4994
|
+
if (cached && isFresh) {
|
|
4995
|
+
cached.uses += 1;
|
|
4996
|
+
return cached.pending;
|
|
4997
|
+
}
|
|
4998
|
+
const pending = findLockedOriginal(mediaBucket, root, subject, now);
|
|
4999
|
+
pending.catch(() => cache.delete(id));
|
|
5000
|
+
cache.set(id, { pending, readAt: Date.now(), uses: 1 });
|
|
5001
|
+
return pending;
|
|
5002
|
+
};
|
|
5003
|
+
return async (place, object) => {
|
|
5004
|
+
const { subject, isVariant } = retentionSubjectOf(place.key);
|
|
5005
|
+
if (subject.kind === "original" && subject.key === place.key) {
|
|
5006
|
+
return lockedObject(mediaBucket, place.root, place.key, object, now);
|
|
5007
|
+
}
|
|
5008
|
+
const locked = await resolveSubject(place.root, subject);
|
|
5009
|
+
if (isVariant && subject.kind === "original" && locked && !protectsDerived(locked.state)) return null;
|
|
5010
|
+
return locked;
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
async function readObjectLock(options, storageKey2) {
|
|
5014
|
+
const place = placeOfStorageKey(storageKey2, options.root);
|
|
5015
|
+
if (place.kind === "reserved") return null;
|
|
5016
|
+
const object = await options.mediaBucket.head(storageKey2);
|
|
5017
|
+
if (!object) return null;
|
|
5018
|
+
if (place.kind === "unrecognized") return UNRECOGNIZED_LOCK;
|
|
5019
|
+
const locked = await lockResolver(options.mediaBucket, options.now ?? /* @__PURE__ */ new Date())(place, object);
|
|
5020
|
+
return toStoredObjectLock(locked, place);
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
// src/object-lock/prefix-walk.ts
|
|
5024
|
+
var DEFAULT_MAX_SUBREQUESTS = 40;
|
|
5025
|
+
var DEFAULT_BATCH_SIZE = 100;
|
|
5026
|
+
var MAX_BATCH_SIZE = 1e3;
|
|
5027
|
+
var BudgetExhausted = class extends Error {
|
|
5028
|
+
constructor() {
|
|
5029
|
+
super("subrequest budget exhausted");
|
|
5030
|
+
}
|
|
5031
|
+
};
|
|
5032
|
+
function subrequestBudget(max) {
|
|
5033
|
+
if (!Number.isInteger(max) || max < 1) throw new RangeError("maxSubrequests must be a positive integer");
|
|
5034
|
+
let used = 0;
|
|
5035
|
+
return {
|
|
5036
|
+
get used() {
|
|
5037
|
+
return used;
|
|
5038
|
+
},
|
|
5039
|
+
canAfford: (count) => used + count <= max,
|
|
5040
|
+
track: (bucket) => new Proxy(bucket, {
|
|
5041
|
+
get(target, property) {
|
|
5042
|
+
const value = Reflect.get(target, property);
|
|
5043
|
+
if (typeof value !== "function") return value;
|
|
5044
|
+
return (...args) => {
|
|
5045
|
+
if (used >= max) throw new BudgetExhausted();
|
|
5046
|
+
used += 1;
|
|
5047
|
+
return value.apply(target, args);
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
})
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
function encodeCursor(prefix, cursor) {
|
|
5054
|
+
return btoa(JSON.stringify({ prefix, ...cursor }));
|
|
5055
|
+
}
|
|
5056
|
+
var VIDEO_FOLDER_SHAPE = /^(?:public|private)\/video\/[^/]+$/;
|
|
5057
|
+
function invalidCursor(reason) {
|
|
5058
|
+
throw new RangeError(`cursor isn't one this function returned for this prefix: ${reason}`);
|
|
5059
|
+
}
|
|
5060
|
+
function parseCursor(cursor) {
|
|
5061
|
+
try {
|
|
5062
|
+
const parsed = JSON.parse(atob(cursor));
|
|
5063
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
|
|
5064
|
+
return parsed;
|
|
5065
|
+
} catch {
|
|
5066
|
+
}
|
|
5067
|
+
return invalidCursor("not a cursor");
|
|
5068
|
+
}
|
|
5069
|
+
var isOptionalString = (value) => value === void 0 || typeof value === "string";
|
|
5070
|
+
function validFolder(value, options) {
|
|
5071
|
+
if (value === void 0) return void 0;
|
|
5072
|
+
const { root, folder } = typeof value === "object" && value !== null ? value : {};
|
|
5073
|
+
if (typeof root !== "string" || typeof folder !== "string" || !VIDEO_FOLDER_SHAPE.test(folder)) {
|
|
5074
|
+
return invalidCursor("folder");
|
|
5075
|
+
}
|
|
5076
|
+
const folderPrefix = `${root}${folder}/`;
|
|
5077
|
+
const place = placeOfStorageKey(`${folderPrefix}x`, options.root);
|
|
5078
|
+
const isPlaced = place.kind === "object" && place.root === root;
|
|
5079
|
+
if (!isPlaced || !folderPrefix.startsWith(options.prefix)) return invalidCursor("folder outside the prefix");
|
|
5080
|
+
return { root, folder };
|
|
5081
|
+
}
|
|
5082
|
+
function decodeCursor(cursor, options) {
|
|
5083
|
+
if (cursor === void 0) return {};
|
|
5084
|
+
const { prefix, after, listCursor, folder } = parseCursor(cursor);
|
|
5085
|
+
if (prefix !== options.prefix) return invalidCursor("another prefix");
|
|
5086
|
+
if (!isOptionalString(after) || !isOptionalString(listCursor)) return invalidCursor("position");
|
|
5087
|
+
if (typeof after === "string" && !after.startsWith(options.prefix))
|
|
5088
|
+
return invalidCursor("position outside the prefix");
|
|
5089
|
+
const pendingFolder = validFolder(folder, options);
|
|
5090
|
+
return {
|
|
5091
|
+
...typeof after === "string" ? { after } : {},
|
|
5092
|
+
...typeof listCursor === "string" ? { listCursor } : {},
|
|
5093
|
+
...pendingFolder ? { folder: pendingFolder } : {}
|
|
5094
|
+
};
|
|
5095
|
+
}
|
|
5096
|
+
async function walkPrefix(options, mediaBucket, start, visit) {
|
|
5097
|
+
const limit = Math.min(Math.max(1, Math.floor(options.batchSize ?? DEFAULT_BATCH_SIZE)), MAX_BATCH_SIZE);
|
|
5098
|
+
let position = { ...start };
|
|
5099
|
+
let hasProgressed = false;
|
|
5100
|
+
const stopped = () => ({ position, isDone: false, hasProgressed });
|
|
5101
|
+
for (; ; ) {
|
|
5102
|
+
let listing;
|
|
5103
|
+
try {
|
|
5104
|
+
listing = await mediaBucket.list({ prefix: options.prefix, limit, ...listFrom(position) });
|
|
5105
|
+
} catch (error) {
|
|
5106
|
+
if (error instanceof BudgetExhausted) return stopped();
|
|
5107
|
+
throw error;
|
|
5108
|
+
}
|
|
5109
|
+
const stray = listing.objects.find((listed) => !listed.key.startsWith(options.prefix));
|
|
5110
|
+
if (stray) throw new RangeError(`listing returned ${stray.key}, outside the prefix ${options.prefix}`);
|
|
5111
|
+
for (const listed of listing.objects) {
|
|
5112
|
+
try {
|
|
5113
|
+
await visit({ listed, place: placeOfStorageKey(listed.key, options.root) });
|
|
5114
|
+
} catch (error) {
|
|
5115
|
+
if (error instanceof BudgetExhausted) return stopped();
|
|
5116
|
+
throw error;
|
|
5117
|
+
}
|
|
5118
|
+
position = { after: listed.key };
|
|
5119
|
+
hasProgressed = true;
|
|
5120
|
+
}
|
|
5121
|
+
if (!listing.truncated) return { position, isDone: true, hasProgressed };
|
|
5122
|
+
if (listing.objects.length > 0) continue;
|
|
5123
|
+
position = { ...position, listCursor: listing.cursor };
|
|
5124
|
+
hasProgressed = true;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
function listFrom(position) {
|
|
5128
|
+
if (position.listCursor) return { cursor: position.listCursor };
|
|
5129
|
+
return position.after ? { startAfter: position.after } : {};
|
|
5130
|
+
}
|
|
5131
|
+
function assertProgress(hasProgressed) {
|
|
5132
|
+
if (hasProgressed) return;
|
|
5133
|
+
throw new RangeError("maxSubrequests is too small to finish the next object: raise it");
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
// src/object-lock/prefix-delete.ts
|
|
5137
|
+
var VIDEO_FOLDER = /^((?:public|private)\/video\/[^/]+)\//;
|
|
5138
|
+
var isPending = (folder) => folder?.hasDeleted === true && !folder.hasRetained;
|
|
5139
|
+
async function deletePrefixRespectingLocks(options) {
|
|
5140
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
5141
|
+
const budget = subrequestBudget(options.maxSubrequests ?? DEFAULT_MAX_SUBREQUESTS);
|
|
5142
|
+
const mediaBucket = budget.track(options.mediaBucket);
|
|
5143
|
+
const keysBucket = options.keysBucket ? budget.track(options.keysBucket) : void 0;
|
|
5144
|
+
const resolveLock3 = lockResolver(mediaBucket, startedAt);
|
|
5145
|
+
const start = decodeCursor(options.cursor, options);
|
|
5146
|
+
const result = {
|
|
5147
|
+
deletedCount: 0,
|
|
5148
|
+
deletedBytes: 0,
|
|
5149
|
+
deletedKeyCount: 0,
|
|
5150
|
+
retainedCount: 0,
|
|
5151
|
+
retainedBytes: 0,
|
|
5152
|
+
retained: [],
|
|
5153
|
+
skippedCount: 0,
|
|
5154
|
+
subrequests: 0
|
|
5155
|
+
};
|
|
5156
|
+
let openFolder = start.folder ? { ...start.folder, hasDeleted: true, hasRetained: false } : null;
|
|
5157
|
+
let hasClosed = false;
|
|
5158
|
+
const closeFolder = async () => {
|
|
5159
|
+
if (isPending(openFolder)) {
|
|
5160
|
+
const { root, folder } = openFolder;
|
|
5161
|
+
const folderPrefix = `${root}${folder}/`;
|
|
5162
|
+
const remaining = await mediaBucket.list({ prefix: folderPrefix, limit: 1 });
|
|
5163
|
+
if (remaining.objects.length === 0) {
|
|
5164
|
+
if (keysBucket) {
|
|
5165
|
+
const keys = await keysWrittenBy(keysBucket, folderPrefix, startedAt);
|
|
5166
|
+
result.deletedKeyCount += await deleteKeys(keysBucket, keys);
|
|
5167
|
+
}
|
|
5168
|
+
await forgetTranscodeCompleted(mediaBucket, root, folder);
|
|
5169
|
+
}
|
|
5170
|
+
hasClosed = true;
|
|
5171
|
+
}
|
|
5172
|
+
openFolder = null;
|
|
5173
|
+
};
|
|
5174
|
+
const enterFolder = async (place) => {
|
|
5175
|
+
const videoFolder = VIDEO_FOLDER.exec(place.key)?.[1];
|
|
5176
|
+
const folder = videoFolder && `${place.root}${videoFolder}/`.startsWith(options.prefix) ? videoFolder : void 0;
|
|
5177
|
+
if (openFolder && openFolder.root === place.root && openFolder.folder === folder) return openFolder;
|
|
5178
|
+
await closeFolder();
|
|
5179
|
+
openFolder = folder ? { root: place.root, folder, hasDeleted: false, hasRetained: false } : null;
|
|
5180
|
+
return openFolder;
|
|
5181
|
+
};
|
|
5182
|
+
const retain = (key, size, lock) => {
|
|
5183
|
+
result.retained.push({ key, size, ...lock });
|
|
5184
|
+
result.retainedCount += 1;
|
|
5185
|
+
result.retainedBytes += size;
|
|
5186
|
+
};
|
|
5187
|
+
const visit = async ({ listed, place }) => {
|
|
5188
|
+
if (place.kind === "reserved") {
|
|
5189
|
+
result.skippedCount += 1;
|
|
5190
|
+
return;
|
|
5191
|
+
}
|
|
5192
|
+
if (place.kind === "unrecognized") return retain(listed.key, listed.size, UNRECOGNIZED_LOCK);
|
|
5193
|
+
const folder = await enterFolder(place);
|
|
5194
|
+
const object = await mediaBucket.head(listed.key);
|
|
5195
|
+
if (!object) return;
|
|
5196
|
+
const locked = await resolveLock3(place, object);
|
|
5197
|
+
if (locked) {
|
|
5198
|
+
if (folder) folder.hasRetained = true;
|
|
5199
|
+
return retain(listed.key, object.size, toStoredObjectLock(locked, place));
|
|
5200
|
+
}
|
|
5201
|
+
const keys = keysBucket ? await objectKeysToDelete(keysBucket, listed.key, object) : [];
|
|
5202
|
+
if (!budget.canAfford(1 + deleteCallsFor(keys))) throw new BudgetExhausted();
|
|
5203
|
+
await mediaBucket.delete(listed.key);
|
|
5204
|
+
result.deletedCount += 1;
|
|
5205
|
+
result.deletedBytes += object.size;
|
|
5206
|
+
if (folder) folder.hasDeleted = true;
|
|
5207
|
+
if (keysBucket) result.deletedKeyCount += await deleteKeys(keysBucket, keys);
|
|
5208
|
+
};
|
|
5209
|
+
const walk = await walkPrefix(options, mediaBucket, start, visit);
|
|
5210
|
+
if (walk.isDone) {
|
|
5211
|
+
try {
|
|
5212
|
+
await closeFolder();
|
|
5213
|
+
} catch (error) {
|
|
5214
|
+
if (!(error instanceof BudgetExhausted)) throw error;
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
const pendingFolder = isPending(openFolder) ? { root: openFolder.root, folder: openFolder.folder } : void 0;
|
|
5218
|
+
const isFinished = walk.isDone && !pendingFolder;
|
|
5219
|
+
assertProgress(walk.hasProgressed || hasClosed || isFinished);
|
|
5220
|
+
const cursor = isFinished ? void 0 : encodeCursor(options.prefix, { ...walk.position, ...pendingFolder ? { folder: pendingFolder } : {} });
|
|
5221
|
+
return { ...result, ...cursor ? { cursor } : {}, subrequests: budget.used };
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
// src/object-lock/retained-summary.ts
|
|
5225
|
+
async function retainedSummary(options) {
|
|
5226
|
+
const budget = subrequestBudget(options.maxSubrequests ?? DEFAULT_MAX_SUBREQUESTS);
|
|
5227
|
+
const mediaBucket = budget.track(options.mediaBucket);
|
|
5228
|
+
const resolveLock3 = lockResolver(mediaBucket, options.now ?? /* @__PURE__ */ new Date());
|
|
5229
|
+
const summary = {
|
|
5230
|
+
scannedCount: 0,
|
|
5231
|
+
retainedCount: 0,
|
|
5232
|
+
retainedBytes: 0,
|
|
5233
|
+
hasHold: false,
|
|
5234
|
+
subrequests: 0
|
|
5235
|
+
};
|
|
5236
|
+
const visit = async ({ listed, place }) => {
|
|
5237
|
+
if (place.kind === "reserved") return;
|
|
5238
|
+
let lock = UNRECOGNIZED_LOCK;
|
|
5239
|
+
let size = listed.size;
|
|
5240
|
+
if (place.kind === "object") {
|
|
5241
|
+
const object = await mediaBucket.head(listed.key);
|
|
5242
|
+
if (!object) return;
|
|
5243
|
+
lock = toStoredObjectLock(await resolveLock3(place, object), place);
|
|
5244
|
+
size = object.size;
|
|
5245
|
+
}
|
|
5246
|
+
summary.scannedCount += 1;
|
|
5247
|
+
if (!lock.locked) return;
|
|
5248
|
+
summary.retainedCount += 1;
|
|
5249
|
+
summary.retainedBytes += size;
|
|
5250
|
+
if (lock.hold) summary.hasHold = true;
|
|
5251
|
+
if (lock.until && (!summary.latestUntil || lock.until > summary.latestUntil)) summary.latestUntil = lock.until;
|
|
5252
|
+
};
|
|
5253
|
+
const walk = await walkPrefix(options, mediaBucket, decodeCursor(options.cursor, options), visit);
|
|
5254
|
+
assertProgress(walk.hasProgressed || walk.isDone);
|
|
5255
|
+
const cursor = walk.isDone ? void 0 : encodeCursor(options.prefix, walk.position);
|
|
5256
|
+
return { ...summary, ...cursor ? { cursor } : {}, subrequests: budget.used };
|
|
5257
|
+
}
|
|
5258
|
+
|
|
4926
5259
|
// src/index.ts
|
|
4927
5260
|
function createWorker(options) {
|
|
4928
5261
|
const { middleware, routes, hooks } = composePlugins(options?.plugins ?? defaultPlugins());
|
|
@@ -4985,6 +5318,7 @@ export {
|
|
|
4985
5318
|
credentialPolicy,
|
|
4986
5319
|
defaultPlugins,
|
|
4987
5320
|
deletePrefix,
|
|
5321
|
+
deletePrefixRespectingLocks,
|
|
4988
5322
|
downloadLinks,
|
|
4989
5323
|
edgeCacheTag,
|
|
4990
5324
|
getPendingUpload,
|
|
@@ -5003,10 +5337,12 @@ export {
|
|
|
5003
5337
|
parseEncodeMetrics,
|
|
5004
5338
|
parseTransformParams,
|
|
5005
5339
|
readImageOriginScope,
|
|
5340
|
+
readObjectLock,
|
|
5006
5341
|
readStoryboardKey,
|
|
5007
5342
|
readTokenScopes,
|
|
5008
5343
|
readTranscodeOutputKeys,
|
|
5009
5344
|
readTranscodeOutputs,
|
|
5345
|
+
retainedSummary,
|
|
5010
5346
|
servedCacheControl,
|
|
5011
5347
|
staticConfig,
|
|
5012
5348
|
storageKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devultur/self-host",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Self-hosted media Worker for Cloudflare: R2 uploads, edge-cached delivery, image transforms, HLS transcode and captions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@jsquash/resize": "2.1.1",
|
|
47
47
|
"@jsquash/webp": "1.5.0",
|
|
48
48
|
"hono": "^4.7.0",
|
|
49
|
-
"@devultur/core": "1.
|
|
49
|
+
"@devultur/core": "1.2.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@cf-wasm/photon": "0.4.0",
|