@devultur/self-host 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ pnpm add @devultur/self-host
15
15
  ```typescript
16
16
  // src/index.ts
17
17
  import { createWorker } from "@devultur/self-host";
18
- export { DownloadLink } from "@devultur/self-host";
18
+ export { DownloadGrants, DownloadLink } from "@devultur/self-host";
19
19
 
20
20
  export default createWorker();
21
21
  ```
@@ -42,9 +42,17 @@ id = "<your KV namespace id>"
42
42
  name = "DOWNLOAD_LINKS"
43
43
  class_name = "DownloadLink"
44
44
 
45
+ [[durable_objects.bindings]]
46
+ name = "DOWNLOAD_GRANTS"
47
+ class_name = "DownloadGrants"
48
+
45
49
  [[migrations]]
46
50
  tag = "v1"
47
51
  new_sqlite_classes = ["DownloadLink"]
52
+
53
+ [[migrations]]
54
+ tag = "v2"
55
+ new_sqlite_classes = ["DownloadGrants"]
48
56
  ```
49
57
 
50
58
  ```bash
@@ -420,6 +420,7 @@ async function watchContainer(exited, env) {
420
420
  }
421
421
 
422
422
  export {
423
+ isConstantTimeEqual,
423
424
  isBearerAuthorized,
424
425
  getRequestState,
425
426
  isAuthenticated,
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, RetentionMode, TranscodeOutputKeys, TranscodeOutputUrls } from '@devultur/core';
3
+ import { Visibility, ObjectLock, SecurityPolicy, CacheRule, TranscodeSettings, DownloadGrantLinkCaps, 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`). */
@@ -37,7 +37,20 @@ interface TranscodeJob {
37
37
  /** Plugin context of the request that launched the job, as it was. */
38
38
  context?: PluginContext;
39
39
  }
40
+ /** A download grant as the Worker hands it to plugins, with its public id (`dgr_…`). */
41
+ interface DownloadGrantEvent {
42
+ publicId: string;
43
+ grant: DownloadGrantInfo;
44
+ }
40
45
  interface RouteResult {
46
+ /** `POST /v1/token/grant` issued this grant. */
47
+ issuedGrant?: DownloadGrantEvent;
48
+ /** The grant was revoked: by its bearer (`DELETE /v1/grant`, or with its file) or by the owner (by public id). */
49
+ revokedGrant?: DownloadGrantEvent & {
50
+ by: "bearer" | "owner";
51
+ };
52
+ /** `DELETE /v1/grant/file` deleted this file (its key, relative to the storage prefix). */
53
+ grantDeletedKey?: string;
41
54
  /** `GET /v1/download/{id}` redeemed this link (one use spent). */
42
55
  redeemedLink?: DownloadLinkInfo;
43
56
  /** `transcode.complete` deleted the video's original (`config.transcode.deleteOriginalOnSuccess`). */
@@ -73,6 +86,7 @@ interface WorkerEnv {
73
86
  WORKER_AUTH_SECRET?: string;
74
87
  TRANSCODE_WEBHOOK_SECRET?: string;
75
88
  DOWNLOAD_LINKS?: DurableObjectNamespace;
89
+ DOWNLOAD_GRANTS?: DurableObjectNamespace;
76
90
  IMAGE_TRANSFORMS?: string;
77
91
  IMAGE_TRANSFORMS_MAX_MEGAPIXELS?: string;
78
92
  }
@@ -233,6 +247,79 @@ declare function isAuthenticated(context: AnyContext$2): boolean;
233
247
  */
234
248
  declare function updateRequestState(context: AnyContext$2, patch: RequestStatePatch): RequestState;
235
249
 
250
+ interface DownloadGrantRecord {
251
+ /** Grant id, unique within its file. */
252
+ id: string;
253
+ /** SHA-256 (hex) of the grant's secret. */
254
+ secretHash: string;
255
+ /** Full storage key (with the storage prefix). */
256
+ storageKey: string;
257
+ /** Virtual key (`{visibility}/…`). */
258
+ key: string;
259
+ /** R2 `version` of the object the grant was issued for: a file uploaded later at the same key isn't covered. */
260
+ version: string;
261
+ /** Storage prefix the grant was issued under. */
262
+ storagePrefix: string;
263
+ /** Epoch ms. */
264
+ expiresAt: number;
265
+ link: DownloadGrantLinkCaps;
266
+ maxLinks: number;
267
+ linksIssued: number;
268
+ canDelete: boolean;
269
+ metadata?: Record<string, string>;
270
+ /** Plugin context of the request that issued the grant (opaque to the Worker); its links inherit it. */
271
+ context?: PluginContext;
272
+ /** Epoch ms. */
273
+ createdAt: number;
274
+ /** Epoch ms, or null while live. */
275
+ revokedAt: number | null;
276
+ }
277
+ /** A grant without its secret hash: what the Worker hands to routes, hooks and the owner. */
278
+ type DownloadGrantInfo = Omit<DownloadGrantRecord, "secretHash">;
279
+ /** Storage surface of a DO that the class uses (the Workers one satisfies it; tests pass a Map). */
280
+ interface DownloadGrantStorage {
281
+ get<T>(key: string): Promise<T | undefined>;
282
+ put<T>(key: string, value: T): Promise<void>;
283
+ list<T>(options: {
284
+ prefix: string;
285
+ }): Promise<Map<string, T>>;
286
+ delete(key: string): Promise<boolean>;
287
+ setAlarm(scheduledTime: number): Promise<void>;
288
+ deleteAlarm(): Promise<void>;
289
+ }
290
+ interface DownloadGrantState {
291
+ storage: DownloadGrantStorage;
292
+ blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;
293
+ }
294
+ declare class DownloadGrants {
295
+ private readonly state;
296
+ private readonly storage;
297
+ constructor(state: DownloadGrantState);
298
+ fetch(request: Request): Promise<Response>;
299
+ private handlerFor;
300
+ private create;
301
+ private recordOf;
302
+ /** The record whose secret hash matches, live or not, or 401. */
303
+ private matchingRecord;
304
+ /** The live record whose secret hash matches, or the response explaining why not. */
305
+ private liveRecord;
306
+ private identify;
307
+ private authorize;
308
+ /** Spends one link: caps first (a refused link spends nothing), then the total. */
309
+ private issue;
310
+ private refund;
311
+ /** Without `secretHash` the route already authorized the owner; with it, only the grant's own secret revokes it. */
312
+ private revoke;
313
+ private getOne;
314
+ private records;
315
+ private list;
316
+ private revokeAll;
317
+ /** Alarm at the earliest expiry among the stored grants; none left: no alarm. */
318
+ private scheduleAlarm;
319
+ /** Drops the grants past their expiry (revoked ones stay listed until then) and schedules the next alarm. */
320
+ alarm(): Promise<void>;
321
+ }
322
+
236
323
  declare const LOCK_AUDIT_PREFIX = "__lock-audit/";
237
324
  type LockAuditAction = "set" | "extend" | "shorten" | "remove" | "hold" | "release" | "delete_rejected" | "overwrite_rejected" | "original_retained";
238
325
  interface LockAuditRecord {
@@ -253,13 +340,19 @@ interface LockAuditRecord {
253
340
  reason?: string;
254
341
  }
255
342
 
256
- /** What is being issued: a viewer token, an upload token, a download link or the transcode runner's token. */
257
- type CredentialKind = "viewer" | "upload" | "download-link" | "service";
343
+ /**
344
+ * What is being issued: a viewer token, an upload token, a download link, a download grant or the transcode runner's
345
+ * token.
346
+ */
347
+ type CredentialKind = "viewer" | "upload" | "download-link" | "download-grant" | "service";
258
348
  interface CredentialRequest {
259
349
  kind: CredentialKind;
260
350
  /** Scopes the credential will carry. */
261
351
  scopes: string[];
262
- /** What it is bound to: the upload prefix (`asset/…/`), the download link's key or the original being transcoded. */
352
+ /**
353
+ * What it is bound to: the upload prefix (`asset/…/`), the key of the download link or grant, or the original being
354
+ * transcoded.
355
+ */
263
356
  path?: string;
264
357
  /** Expiry the request asked for (epoch ms). */
265
358
  expiresAt: number;
@@ -286,6 +379,20 @@ type AnyContext$1 = Context<any>;
286
379
  type IssueCredentialHook = (context: AnyContext$1, request: CredentialRequest) => CredentialDecision | Promise<CredentialDecision>;
287
380
  /** A response refuses the redemption (returned as is, and the use is NOT spent); `null` lets it go on. */
288
381
  type BeforeLinkRedeemHook = (context: AnyContext$1, link: DownloadLinkInfo) => Response | null | Promise<Response | null>;
382
+ /**
383
+ * What a download grant is used for: its bearer reads the file's head, issues a link, deletes the file or revokes the
384
+ * grant; or the owner revokes it by its public id.
385
+ */
386
+ type GrantAction = "head" | "issue-link" | "delete-file" | "revoke" | "owner-revoke";
387
+ interface GrantAccessRequest {
388
+ action: GrantAction;
389
+ grant: DownloadGrantInfo;
390
+ }
391
+ /**
392
+ * Before a download grant is used (or revoked by the owner), once the Worker checked it belongs to the request's storage
393
+ * prefix. A response refuses (returned as is; a refused link spends nothing); `null` lets it go on.
394
+ */
395
+ type AuthorizeGrantHook = (context: AnyContext$1, request: GrantAccessRequest) => Response | null | Promise<Response | null>;
289
396
  /**
290
397
  * What a lock change does: `set` a first (or expired) retention, `extend` it, `shorten` or `remove` it; `hold` or
291
398
  * `release` a hold.
@@ -317,6 +424,7 @@ type OnLockAuditHook = (context: AnyContext$1, record: LockAuditRecord) => void
317
424
  interface PluginHooks {
318
425
  issueCredential: IssueCredentialHook[];
319
426
  beforeLinkRedeem: BeforeLinkRedeemHook[];
427
+ authorizeGrant: AuthorizeGrantHook[];
320
428
  authorizeLockChange: AuthorizeLockChangeHook[];
321
429
  onLockAudit: OnLockAuditHook[];
322
430
  }
@@ -349,6 +457,8 @@ interface WorkerPlugin {
349
457
  issueCredential?: IssueCredentialHook;
350
458
  /** Before `GET /v1/download/{id}` spends a use of a link that still has one. */
351
459
  beforeLinkRedeem?: BeforeLinkRedeemHook;
460
+ /** Before a download grant is used, or revoked by its public id. */
461
+ authorizeGrant?: AuthorizeGrantHook;
352
462
  /** Before `PUT /v1/lock` changes a lock: refuse it, or approve a governance bypass. */
353
463
  authorizeLockChange?: AuthorizeLockChangeHook;
354
464
  /** After every lock event, with its audit record. Runs after the response. */
@@ -374,6 +484,10 @@ interface DownloadLinkRecord {
374
484
  storagePrefix: string;
375
485
  /** Plugin context of the request that issued the link (opaque to the Worker). */
376
486
  context?: PluginContext;
487
+ /** Public id (`dgr_…`) of the download grant that issued the link. */
488
+ grantId?: string;
489
+ /** Opaque metadata the link was issued with. */
490
+ metadata?: Record<string, string>;
377
491
  }
378
492
  /** Storage surface of a DO that the class uses (the Workers one satisfies it; tests pass a Map). */
379
493
  interface DownloadLinkStorage {
@@ -618,6 +732,7 @@ declare const WorkerAuthErrorCode: {
618
732
  * - `/health` and the playback metrics endpoint: public by design.
619
733
  * - Media delivery: public media is tokenless; the route itself requires a viewer token for private media.
620
734
  * - Download links: the opaque link id is the credential.
735
+ * - The bearer's download grant routes: the grant is the credential (the route checks it).
621
736
  * - Webhooks: they check their own shared secret.
622
737
  */
623
738
  declare function isOpenRoute(method: string, path: string): boolean;
@@ -665,6 +780,18 @@ declare function isObjectExpired(customMetadata: Record<string, string> | undefi
665
780
  /** The unit's tag: a hash (Cloudflare's tags are case-insensitive but paths aren't). */
666
781
  declare function edgeCacheTag(storagePrefix: string, virtualKey: string): Promise<string>;
667
782
 
783
+ /** A grant as its bearer presented it: its public id and record (no secret). */
784
+ interface IdentifiedGrant {
785
+ publicId: string;
786
+ grant: DownloadGrantInfo;
787
+ }
788
+ /**
789
+ * The grant in the request's `Authorization: Bearer`, live or not, or the response refusing it (`401 invalid_grant`,
790
+ * `grant_in_query`, `501`). For a platform that resolves the grant's tenant before authorizing the request: the
791
+ * routes still check it's live, belongs to the request's storage prefix, and run the hooks.
792
+ */
793
+ declare function identifyDownloadGrant(anyContext: Context<any>): Promise<IdentifiedGrant | Response>;
794
+
668
795
  /** `Cache-Control` served: the one stored on the object (for private, no shared caches) or the one for a generated id. */
669
796
  declare function servedCacheControl(stored: string | undefined, isPrivate: boolean): string;
670
797
 
@@ -770,4 +897,4 @@ interface CreateWorkerOptions {
770
897
  }
771
898
  declare function createWorker(options?: CreateWorkerOptions): Hono<WorkerApp, hono_types.BlankSchema, "/">;
772
899
 
773
- export { type AllowedCredential, type AssetListEntry, type AuthorizeLockChangeHook, type BeforeLinkRedeemHook, type ComposedPlugins, type ContentSniffingMode, 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 };
900
+ export { type AllowedCredential, type AssetListEntry, type AuthorizeGrantHook, type AuthorizeLockChangeHook, type BeforeLinkRedeemHook, type ComposedPlugins, type ContentSniffingMode, type CreateWorkerOptions, type CredentialDecision, type CredentialKind, type CredentialPolicyOptions, type CredentialRequest, type DownloadGrantEvent, type DownloadGrantInfo, type DownloadGrantRecord, type DownloadGrantState, type DownloadGrantStorage, DownloadGrants, DownloadLink, type DownloadLinkInfo, type DownloadLinkRecord, type DownloadLinkStorage, ENCRYPTION_KEY_MISSING_CODE, type EncodeMetrics, type GrantAccessRequest, type GrantAction, HOLD_PAGE_SIZE, type HoldSkip, type IdentifiedGrant, 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, identifyDownloadGrant, 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 };