All files storageResults.ts

91.3% Statements 21/23
92.85% Branches 26/28
100% Functions 2/2
95.23% Lines 20/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83                                                            5x 5x 5x 2x   3x 5x 2x 2x         2x 2x 2x                                             11x 11x 11x 1x   10x 9x 9x     6x 6x      
import type { StoredAsset } from './alphalib/types/storageAsset.ts'
import type {
  GetStoredAssemblyResultsOptions,
  StoredAssemblyResult,
} from './alphalib/types/storageResults.ts'
import type { Transloadit } from './Transloadit.ts'
 
import { ApiError } from './ApiError.ts'
import { robotFileServeInstructionsSchema } from './alphalib/types/robots/file-serve.ts'
import { storedAssetSchema } from './alphalib/types/storageAsset.ts'
import {
  extractStoredAssemblyResults,
  storedAssemblyResultsOptionsSchema,
} from './alphalib/types/storageResults.ts'
import InconsistentResponseError from './InconsistentResponseError.ts'
 
/** Original-byte delivery, not a preview or transcode; authorize the application's asset first. */
export interface StoredAssetUrlOptions {
  /** True uses the receipt filename; a string supplies a different attachment filename. */
  download?: boolean | string
  /** Maximum signed lifetime, from one second to 48 hours. Defaults to five minutes. */
  lifetimeMs?: number
}
 
/** Signs a retained original, including non-image media, without fetching or proxying its bytes. */
export function getStoredAssetUrl(
  client: Transloadit,
  input: StoredAsset,
  options: StoredAssetUrlOptions = {},
): string {
  const asset = storedAssetSchema.parse(input)
  const lifetimeMs = options.lifetimeMs ?? 5 * 60_000
  if (!Number.isSafeInteger(lifetimeMs) || lifetimeMs < 1000 || lifetimeMs > 48 * 3600_000) {
    throw new RangeError('lifetimeMs must be an integer between 1000 and 172800000')
  }
  const download = options.download === true ? asset.path.split('/').at(-1) : options.download
  if (download !== undefined && download !== false) {
    robotFileServeInstructionsSchema.shape.download_name.unwrap().min(1).parse(download)
    Iif (typeof download !== 'string' || /[\uD800-\uDFFF]/u.test(download))
      throw new TypeError('download must contain a valid Unicode filename')
  }
  // Bunny includes the full query in its cache key. Rotate at most once a minute, never at
  // the lifetime boundary where rounding could issue an almost-expired grant.
  const rotationMs = Math.min(60_000, Math.floor(lifetimeMs / 2))
  const expiresAt = Math.floor(Date.now() / rotationMs) * rotationMs + lifetimeMs
  return client.getSignedSmartCDNUrl({
    workspace: asset.workspace,
    template: 'builtin/storage-serve@0.0.3',
    input: asset.asset_id,
    urlParams: { v: asset.version_id, ...(typeof download === 'string' ? { download } : {}) },
    expiresAt,
  })
}
 
export type {
  GetStoredAssemblyResultsOptions,
  StoredAssemblyResult,
} from './alphalib/types/storageResults.ts'
 
/**
 * Fetches a completed Assembly and verifies all retained results, including non-image media.
 * The caller must first bind the Assembly to an authorized application upload. Register results
 * idempotently by Assembly/step/result ID (or asset/version), not by filename or notification count.
 */
export async function getStoredAssemblyResults(
  client: Transloadit,
  options: GetStoredAssemblyResultsOptions,
): Promise<StoredAssemblyResult[]> {
  const { assemblyId, workspace } = storedAssemblyResultsOptionsSchema.parse(options)
  const assembly = await client.getAssembly(assemblyId)
  if (assembly.assembly_id !== assemblyId) {
    throw new InconsistentResponseError('The response did not match the requested Assembly')
  }
  if (typeof assembly.error === 'string') throw new ApiError({ body: assembly })
  try {
    return extractStoredAssemblyResults(assembly, { assemblyId, workspace })
  } catch (error) {
    // Keep the Node SDK error contract while sharing strict extraction with Web runtimes.
    Iif (!(error instanceof Error)) throw new Error('Invalid Assembly response', { cause: error })
    throw new InconsistentResponseError(error.message, { cause: error.cause ?? error })
  }
}