@vritti/api-sdk 0.3.9 → 0.3.10

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/storage.cjs CHANGED
@@ -25,6 +25,7 @@ __export(storage_exports, {
25
25
  R2BucketProvisioner: () => R2BucketProvisioner,
26
26
  R2StorageProvider: () => R2StorageProvider,
27
27
  StorageFactory: () => StorageFactory,
28
+ StorageProvisionerFactory: () => StorageProvisionerFactory,
28
29
  readConfigSource: () => readConfigSource
29
30
  });
30
31
  module.exports = __toCommonJS(storage_exports);
@@ -473,12 +474,41 @@ var StorageFactory = class {
473
474
  }
474
475
  }
475
476
  };
477
+
478
+ // src/storage/storage-provisioner.factory.ts
479
+ var StorageProvisionerFactory = class {
480
+ static {
481
+ __name(this, "StorageProvisionerFactory");
482
+ }
483
+ options;
484
+ cache = /* @__PURE__ */ new Map();
485
+ constructor(options) {
486
+ this.options = options;
487
+ }
488
+ // Resolves a provisioner by provider name, constructing it once on first use
489
+ resolve(provider) {
490
+ const cached = this.cache.get(provider);
491
+ if (cached) return cached;
492
+ const created = this.create(provider);
493
+ this.cache.set(provider, created);
494
+ return created;
495
+ }
496
+ create(provider) {
497
+ switch (provider) {
498
+ case "r2":
499
+ return new R2BucketProvisioner(readConfigSource(this.options.r2, "r2"));
500
+ default:
501
+ throw new Error(`Unsupported storage provisioner: ${provider}`);
502
+ }
503
+ }
504
+ };
476
505
  // Annotate the CommonJS export names for ESM import in node:
477
506
  0 && (module.exports = {
478
507
  BucketUsageReader,
479
508
  R2BucketProvisioner,
480
509
  R2StorageProvider,
481
510
  StorageFactory,
511
+ StorageProvisionerFactory,
482
512
  readConfigSource
483
513
  });
484
514
  //# sourceMappingURL=storage.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storage/index.ts","../src/storage/bucket-usage.reader.ts","../src/storage/config-source.ts","../src/storage/providers/r2-storage.provider.ts","../src/exceptions/bad-gateway.exception.ts","../src/exceptions/base-field.exception.ts","../src/exceptions/bad-request.exception.ts","../src/exceptions/conflict.exception.ts","../src/exceptions/forbidden.exception.ts","../src/exceptions/gone.exception.ts","../src/exceptions/internal-server-error.exception.ts","../src/exceptions/method-not-allowed.exception.ts","../src/exceptions/not-acceptable.exception.ts","../src/exceptions/not-found.exception.ts","../src/exceptions/not-implemented.exception.ts","../src/exceptions/payload-too-large.exception.ts","../src/exceptions/request-timeout.exception.ts","../src/exceptions/service-unavailable.exception.ts","../src/exceptions/too-many-requests.exception.ts","../src/exceptions/unauthorized.exception.ts","../src/exceptions/unprocessable-entity.exception.ts","../src/exceptions/unsupported-media-type.exception.ts","../src/exceptions/validation.exception.ts","../src/storage/provisioners/r2-bucket.provisioner.ts","../src/storage/storage.factory.ts"],"sourcesContent":["export { type BucketUsage, BucketUsageReader, type BucketUsageReaderConfig } from './bucket-usage.reader';\nexport { readConfigSource, type StorageConfigSource } from './config-source';\nexport { R2StorageProvider } from './providers/r2-storage.provider';\nexport { R2BucketProvisioner } from './provisioners/r2-bucket.provisioner';\nexport { StorageFactory, type StorageFactoryOptions } from './storage.factory';\nexport type {\n ListObjectsPage,\n R2ProvisionerConfig,\n R2StorageConfig,\n StorageProvider,\n StoredObject,\n UploadParams,\n} from './types';\n","import { Logger } from '@nestjs/common';\n\nconst CF_GRAPHQL = 'https://api.cloudflare.com/client/v4/graphql';\n\nexport interface BucketUsage {\n bytes: number;\n objectCount: number;\n}\n\nexport interface BucketUsageReaderConfig {\n accountId: string;\n // Account-level token with Analytics Read. NOT the org's scoped S3 credential and NOT the R2 admin token: the\n // analytics dataset is filtered by accountTag and is not reachable with object-level credentials.\n analyticsToken: string;\n}\n\n// r2StorageAdaptiveGroups is a time series, so the newest bucketed sample is the closest thing to \"current\". It lags\n// real writes by minutes — fine for a periodic quota check, useless for gating an individual upload.\n//\n// `dimensions { datetime }` is required, not decorative: ordering by a field that is neither aggregated nor selected\n// as a dimension is rejected. Keep the query free of `#` comments too — Cloudflare's parser rejects them.\nconst QUERY = `query BucketUsage($accountTag: string!, $bucketName: string, $start: Time, $end: Time) {\n viewer {\n accounts(filter: { accountTag: $accountTag }) {\n r2StorageAdaptiveGroups(\n limit: 1\n filter: { datetime_geq: $start, datetime_leq: $end, bucketName: $bucketName }\n orderBy: [datetime_DESC]\n ) {\n max { objectCount payloadSize metadataSize }\n dimensions { datetime }\n }\n }\n }\n}`;\n\ninterface UsageResponse {\n errors?: { message: string }[];\n data?: {\n viewer?: {\n accounts?: {\n r2StorageAdaptiveGroups?: {\n max?: { objectCount?: number; payloadSize?: number; metadataSize?: number };\n }[];\n }[];\n };\n };\n}\n\n// Reads how much an org's bucket actually holds, straight from the provider — the authoritative figure a locally\n// maintained counter would only ever approximate.\nexport class BucketUsageReader {\n private readonly logger = new Logger(BucketUsageReader.name);\n\n constructor(private readonly config: BucketUsageReaderConfig) {}\n\n // Returns the newest reported sample, or zeroes for a bucket the dataset has not reported on yet (a new or\n // empty bucket produces no rows at all rather than a row of zeroes)\n async getBucketUsage(bucketName: string, windowHours = 24): Promise<BucketUsage> {\n const end = new Date();\n const start = new Date(end.getTime() - windowHours * 60 * 60 * 1000);\n\n const response = await fetch(CF_GRAPHQL, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.analyticsToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n query: QUERY,\n variables: {\n accountTag: this.config.accountId,\n bucketName,\n start: start.toISOString(),\n end: end.toISOString(),\n },\n }),\n });\n\n const body = (await response.json()) as UsageResponse;\n if (body.errors?.length) {\n throw new Error(\n `Cloudflare usage query failed for ${bucketName}: ${body.errors.map((e) => e.message).join('; ')}`,\n );\n }\n\n const sample = body.data?.viewer?.accounts?.[0]?.r2StorageAdaptiveGroups?.[0]?.max;\n if (!sample) {\n this.logger.debug(`No usage samples yet for bucket ${bucketName}`);\n return { bytes: 0, objectCount: 0 };\n }\n\n // Billed storage is payload plus per-object metadata, so both count against a quota\n return {\n bytes: (sample.payloadSize ?? 0) + (sample.metadataSize ?? 0),\n objectCount: sample.objectCount ?? 0,\n };\n }\n}\n","// Config may be a value or a thunk. Servers pass a thunk when the credentials come from required-only-if-selected\n// env keys, so reading them is deferred to the first resolve() instead of running at module construction.\nexport type StorageConfigSource<T> = T | (() => T);\n\n// A backend configured nowhere is a deployment asking for something it was never given credentials for\nexport function readConfigSource<T>(source: StorageConfigSource<T> | undefined, provider: string): T {\n if (source === undefined) {\n throw new Error(`Storage provider '${provider}' is not configured.`);\n }\n return typeof source === 'function' ? (source as () => T)() : source;\n}\n","import type { Readable } from 'node:stream';\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n S3Client,\n} from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Logger } from '@nestjs/common';\nimport { NotFoundException } from '../../exceptions';\nimport type { ListObjectsPage, R2StorageConfig, StorageProvider, UploadParams } from '../types';\n\nexport class R2StorageProvider implements StorageProvider {\n private readonly logger = new Logger(R2StorageProvider.name);\n private readonly client: S3Client;\n\n constructor(private readonly config: R2StorageConfig) {\n this.client = new S3Client({\n region: 'auto',\n endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey },\n });\n }\n\n // Uploads a file buffer or stream\n async upload(params: UploadParams): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(params.bucket, this.config.defaultBucket),\n Key: params.key,\n Body: params.body,\n ContentType: params.contentType,\n }),\n );\n\n this.logger.log(`Uploaded file to R2: ${params.key}`);\n return params.key;\n }\n\n // Uploads a file to a public bucket and returns its permanent URL\n async uploadPublic(key: string, body: Buffer, contentType: string, bucket?: string): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(bucket, this.config.publicBucket),\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n\n const url = this.getPublicUrl(key);\n this.logger.log(`Uploaded public file to R2: ${key} → ${url}`);\n return url;\n }\n\n // Public URLs come from the configured custom domain, which maps to one bucket — R2 has no per-bucket URL to derive,\n // so a multi-tenant caller with a bucket per org cannot build one at all\n getPublicUrl(key: string): string {\n if (!this.config.publicUrl) {\n throw new Error('R2 storage has no publicUrl configured; public URLs cannot be built.');\n }\n return `${this.config.publicUrl}/${key}`;\n }\n\n // Deletes a file\n async delete(key: string, bucket?: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({ Bucket: bucket ?? this.config.defaultBucket, Key: key }));\n\n this.logger.log(`Deleted file from R2: ${key}`);\n }\n\n // Generates a presigned download URL (default 1 hour)\n async getSignedUrl(key: string, expiresInSeconds = 3600, bucket?: string): Promise<string> {\n const command = new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key });\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n }\n\n // Returns a readable stream\n async getStream(key: string, bucket?: string): Promise<Readable> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key }),\n );\n\n if (!response.Body) {\n throw new NotFoundException('File not found in storage.');\n }\n\n return response.Body as Readable;\n }\n\n // One page of a bucket's contents. S3 has no \"list everything\" call and no bucket-size call — a full inventory is\n // this looped until nextToken is absent, and each page is a Class A request.\n async listObjects(bucket: string, continuationToken?: string): Promise<ListObjectsPage> {\n const response = await this.client.send(\n new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: continuationToken }),\n );\n\n return {\n objects: (response.Contents ?? []).map((o) => ({\n key: o.Key ?? '',\n size: o.Size ?? 0,\n lastModified: o.LastModified ?? new Date(0),\n })),\n nextToken: response.NextContinuationToken,\n };\n }\n\n // Failing loudly here beats letting a missing org bucket fall through to some other tenant's default\n private requireBucket(bucket: string | undefined, fallback: string | undefined): string {\n const resolved = bucket ?? fallback;\n if (!resolved) {\n throw new Error('No bucket supplied and R2 storage has no default bucket configured.');\n }\n return resolved;\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadGatewayException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Gateway', HttpStatus.BAD_GATEWAY);\n }\n}\n","import { HttpException, HttpStatus } from '@nestjs/common';\nimport type { FieldError } from '../types/error-response.types';\n\n// Re-export FieldError for backwards compatibility\nexport type { FieldError } from '../types/error-response.types';\n\nexport interface ProblemOptions {\n type?: string;\n label?: string;\n detail?: string;\n errors?: FieldError[];\n}\n\nexport abstract class HttpProblemException extends HttpException {\n constructor(detailOrOptions: string | ProblemOptions, httpStatus: HttpStatus) {\n const options = typeof detailOrOptions === 'string' ? { detail: detailOrOptions } : detailOrOptions;\n\n super(\n {\n type: options.type ?? 'about:blank',\n label: options.label,\n detail: options.detail,\n errors: options.errors ?? [],\n },\n httpStatus,\n );\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadRequestException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Request', HttpStatus.BAD_REQUEST);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ConflictException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Conflict', HttpStatus.CONFLICT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ForbiddenException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Forbidden', HttpStatus.FORBIDDEN);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class GoneException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Gone', HttpStatus.GONE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class InternalServerErrorException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class MethodNotAllowedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Method Not Allowed', HttpStatus.METHOD_NOT_ALLOWED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotAcceptableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Acceptable', HttpStatus.NOT_ACCEPTABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotFoundException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Found', HttpStatus.NOT_FOUND);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotImplementedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Implemented', HttpStatus.NOT_IMPLEMENTED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class PayloadTooLargeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Payload Too Large', HttpStatus.PAYLOAD_TOO_LARGE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class RequestTimeoutException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Request Timeout', HttpStatus.REQUEST_TIMEOUT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ServiceUnavailableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Service Unavailable', HttpStatus.SERVICE_UNAVAILABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class TooManyRequestsException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnauthorizedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unauthorized', HttpStatus.UNAUTHORIZED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnprocessableEntityException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unprocessable Entity', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnsupportedMediaTypeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unsupported Media Type', HttpStatus.UNSUPPORTED_MEDIA_TYPE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ValidationException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Validation Failed', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { Logger } from '@nestjs/common';\nimport type { R2ProvisionerConfig } from '../types';\n\nconst CF_API = 'https://api.cloudflare.com/client/v4';\n\n// Bucket-level group: read/write/list objects in named buckets. Deliberately NOT 'Workers R2 Storage Write', which is\n// account-level and can create and delete buckets — exactly what a tenant credential must never do.\nconst BUCKET_ITEM_WRITE = 'Workers R2 Storage Bucket Item Write';\n\ninterface CloudflareEnvelope<T> {\n success: boolean;\n errors: { code: number; message: string }[];\n result: T;\n}\n\n// Generic R2 admin client: create/delete buckets, toggle a bucket's Cloudflare-managed public domain, and mint/revoke\n// bucket-scoped credentials. It has NO notion of organizations — bucket naming, which buckets a tenant gets, and how\n// the stored descriptor is assembled all live in the caller. Every operation is a Cloudflare REST call.\nexport class R2BucketProvisioner {\n private readonly logger = new Logger(R2BucketProvisioner.name);\n private readonly jurisdiction: string;\n private bucketItemWriteGroupId: string | null = null;\n\n constructor(private readonly config: R2ProvisionerConfig) {\n this.jurisdiction = config.jurisdiction ?? 'default';\n }\n\n // Creates one bucket, treating an existing bucket as success so provisioning can be re-run to reconcile\n async createBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.adminToken}`,\n 'Content-Type': 'application/json',\n 'cf-r2-jurisdiction': this.jurisdiction,\n },\n body: JSON.stringify({\n name,\n ...(this.config.locationHint && { locationHint: this.config.locationHint }),\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (body.success) {\n this.logger.log(`Created bucket ${name}`);\n return;\n }\n\n // 10004 is \"bucket already exists\" — two provisioning attempts racing, or a reconcile pass\n if (body.errors?.some((e) => e.code === 10004)) return;\n throw new Error(`Cloudflare bucket create failed for ${name}: ${this.describe(body)}`);\n }\n\n // Deletes one bucket. R2 refuses to delete a bucket that still holds objects, so the caller must have emptied it\n // first — a 'not empty' failure here means that step did not finish. A missing bucket counts as success.\n async deleteBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${name}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.adminToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n // 10006 is \"bucket not found\" — already gone, which is the state we wanted\n if (body.success || body.errors?.some((e) => e.code === 10006)) {\n this.logger.log(`Deleted bucket ${name}`);\n return;\n }\n throw new Error(`Cloudflare bucket delete failed for ${name}: ${this.describe(body)}`);\n }\n\n // Turns on the bucket's Cloudflare-managed domain and returns its https URL. NOTE: r2.dev is rate limited and\n // documented as non-production — sustained traffic gets 429s. A custom domain per bucket is the production answer,\n // and drops into this same field. Throws on failure; the caller decides whether that is fatal.\n async enablePublicAccess(bucket: string): Promise<string> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${bucket}/domains/managed`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${this.config.adminToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ enabled: true }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ domain?: string; enabled?: boolean }>;\n if (!body.success || !body.result?.domain) {\n throw new Error(`Cloudflare enable public access failed for ${bucket}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Enabled public access for ${bucket} → ${body.result.domain}`);\n return `https://${body.result.domain}`;\n }\n\n // Mints an ACCOUNT-owned token that can only touch the named buckets. Account-owned rather than user-owned so a\n // tenant's credential does not die with whichever Cloudflare user happened to create the parent token. Requires the\n // parent to hold `Account API Tokens Write`; a user-scoped `API Tokens: Edit` token is rejected here with 9109.\n // `name` is the token's display label; the value is returned by R2 exactly once.\n async createScopedToken(name: string, buckets: string[]): Promise<{ id: string; value: string }> {\n const permissionGroupId = await this.resolveBucketItemWriteGroupId();\n\n // The jurisdiction is embedded in the resource key and must match the bucket's, or the token authenticates fine\n // and then 403s on every object because it is scoped to a bucket that does not exist\n const resources = Object.fromEntries(\n buckets.map((bucket) => [\n `com.cloudflare.edge.r2.bucket.${this.config.accountId}_${this.jurisdiction}_${bucket}`,\n '*',\n ]),\n );\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.tokensToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n name,\n policies: [{ effect: 'allow', permission_groups: [{ id: permissionGroupId }], resources }],\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; value: string }>;\n if (!body.success || !body.result?.value) {\n throw new Error(`Cloudflare token create failed for ${name}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Minted storage credential ${name} scoped to ${buckets.length} bucket(s)`);\n return body.result;\n }\n\n // Revokes a credential by its access key id, which on R2 is the Cloudflare token id\n async deleteCredential(accessKeyId: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/${accessKeyId}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (!body.success) {\n throw new Error(`Cloudflare token delete failed for ${accessKeyId}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Revoked storage credential ${accessKeyId}`);\n }\n\n // The create-token API takes permission group UUIDs, not names. Looked up once rather than hardcoded: a UUID copied\n // from docs fails much later with an error that says nothing useful. Same account-scoped endpoint as creation.\n private async resolveBucketItemWriteGroupId(): Promise<string> {\n if (this.bucketItemWriteGroupId) return this.bucketItemWriteGroupId;\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/permission_groups`, {\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; name: string }[]>;\n const group = body.result?.find((g) => g.name === BUCKET_ITEM_WRITE);\n if (!group) {\n throw new Error(`Cloudflare permission group '${BUCKET_ITEM_WRITE}' not found: ${this.describe(body)}`);\n }\n\n this.bucketItemWriteGroupId = group.id;\n return group.id;\n }\n\n private describe(body: CloudflareEnvelope<unknown>): string {\n return body.errors?.map((e) => `${e.code} ${e.message}`).join('; ') || 'unknown error';\n }\n}\n","import { readConfigSource, type StorageConfigSource } from './config-source';\nimport { R2StorageProvider } from './providers/r2-storage.provider';\nimport type { R2StorageConfig, StorageProvider } from './types';\n\nexport interface StorageFactoryOptions {\n r2?: StorageConfigSource<R2StorageConfig>;\n}\n\nexport class StorageFactory {\n private readonly cache = new Map<string, StorageProvider>();\n\n constructor(private readonly options: StorageFactoryOptions) {}\n\n // Resolves a provider by name, constructing it once on first use\n resolve(provider: string): StorageProvider {\n const cached = this.cache.get(provider);\n if (cached) return cached;\n\n const created = this.create(provider);\n this.cache.set(provider, created);\n return created;\n }\n\n private create(provider: string): StorageProvider {\n switch (provider) {\n case 'r2':\n return new R2StorageProvider(readConfigSource(this.options.r2, 'r2'));\n default:\n throw new Error(`Unsupported storage provider: ${provider}`);\n }\n }\n}\n"],"mappings":"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","names":["CF_GRAPHQL","QUERY","BucketUsageReader","logger","Logger","name","config","getBucketUsage","bucketName","windowHours","end","Date","start","getTime","response","fetch","method","headers","Authorization","analyticsToken","body","JSON","stringify","query","variables","accountTag","accountId","toISOString","json","errors","length","Error","map","e","message","join","sample","data","viewer","accounts","r2StorageAdaptiveGroups","max","debug","bytes","objectCount","payloadSize","metadataSize","readConfigSource","source","provider","undefined","Error","import_common","import_common","import_common","HttpProblemException","HttpException","detailOrOptions","httpStatus","options","detail","type","label","errors","import_common","import_common","import_common","import_common","import_common","import_common","import_common","import_common","NotFoundException","HttpProblemException","detailOrOptions","HttpStatus","NOT_FOUND","import_common","import_common","import_common","import_common","import_common","import_common","import_common","import_common","import_common","R2StorageProvider","logger","Logger","name","client","config","S3Client","region","endpoint","accountId","credentials","accessKeyId","secretAccessKey","upload","params","send","PutObjectCommand","Bucket","requireBucket","bucket","defaultBucket","Key","key","Body","body","ContentType","contentType","log","uploadPublic","publicBucket","url","getPublicUrl","publicUrl","Error","delete","DeleteObjectCommand","getSignedUrl","expiresInSeconds","command","GetObjectCommand","expiresIn","getStream","response","NotFoundException","listObjects","continuationToken","ListObjectsV2Command","ContinuationToken","objects","Contents","map","o","size","Size","lastModified","LastModified","Date","nextToken","NextContinuationToken","fallback","resolved","import_common","CF_API","BUCKET_ITEM_WRITE","R2BucketProvisioner","logger","Logger","name","jurisdiction","bucketItemWriteGroupId","config","createBucket","response","fetch","accountId","method","headers","Authorization","adminToken","body","JSON","stringify","locationHint","json","success","log","errors","some","e","code","Error","describe","deleteBucket","enablePublicAccess","bucket","enabled","result","domain","createScopedToken","buckets","permissionGroupId","resolveBucketItemWriteGroupId","resources","Object","fromEntries","map","tokensToken","policies","effect","permission_groups","id","value","length","deleteCredential","accessKeyId","group","find","g","message","join","StorageFactory","cache","Map","options","resolve","provider","cached","get","created","create","set","R2StorageProvider","readConfigSource","r2","Error"]}
1
+ {"version":3,"sources":["../src/storage/index.ts","../src/storage/bucket-usage.reader.ts","../src/storage/config-source.ts","../src/storage/providers/r2-storage.provider.ts","../src/exceptions/bad-gateway.exception.ts","../src/exceptions/base-field.exception.ts","../src/exceptions/bad-request.exception.ts","../src/exceptions/conflict.exception.ts","../src/exceptions/forbidden.exception.ts","../src/exceptions/gone.exception.ts","../src/exceptions/internal-server-error.exception.ts","../src/exceptions/method-not-allowed.exception.ts","../src/exceptions/not-acceptable.exception.ts","../src/exceptions/not-found.exception.ts","../src/exceptions/not-implemented.exception.ts","../src/exceptions/payload-too-large.exception.ts","../src/exceptions/request-timeout.exception.ts","../src/exceptions/service-unavailable.exception.ts","../src/exceptions/too-many-requests.exception.ts","../src/exceptions/unauthorized.exception.ts","../src/exceptions/unprocessable-entity.exception.ts","../src/exceptions/unsupported-media-type.exception.ts","../src/exceptions/validation.exception.ts","../src/storage/provisioners/r2-bucket.provisioner.ts","../src/storage/storage.factory.ts","../src/storage/storage-provisioner.factory.ts"],"sourcesContent":["export { type BucketUsage, BucketUsageReader, type BucketUsageReaderConfig } from './bucket-usage.reader';\nexport { readConfigSource, type StorageConfigSource } from './config-source';\nexport { R2StorageProvider } from './providers/r2-storage.provider';\nexport { R2BucketProvisioner } from './provisioners/r2-bucket.provisioner';\nexport { StorageFactory, type StorageFactoryOptions } from './storage.factory';\nexport { StorageProvisionerFactory, type StorageProvisionerFactoryOptions } from './storage-provisioner.factory';\nexport type {\n ListObjectsPage,\n R2ProvisionerConfig,\n R2StorageConfig,\n ScopedTokenResult,\n StorageProvider,\n StorageProvisioner,\n StoredObject,\n UploadParams,\n} from './types';\n","import { Logger } from '@nestjs/common';\n\nconst CF_GRAPHQL = 'https://api.cloudflare.com/client/v4/graphql';\n\nexport interface BucketUsage {\n bytes: number;\n objectCount: number;\n}\n\nexport interface BucketUsageReaderConfig {\n accountId: string;\n // Account-level token with Analytics Read. NOT the org's scoped S3 credential and NOT the R2 admin token: the\n // analytics dataset is filtered by accountTag and is not reachable with object-level credentials.\n analyticsToken: string;\n}\n\n// r2StorageAdaptiveGroups is a time series, so the newest bucketed sample is the closest thing to \"current\". It lags\n// real writes by minutes — fine for a periodic quota check, useless for gating an individual upload.\n//\n// `dimensions { datetime }` is required, not decorative: ordering by a field that is neither aggregated nor selected\n// as a dimension is rejected. Keep the query free of `#` comments too — Cloudflare's parser rejects them.\nconst QUERY = `query BucketUsage($accountTag: string!, $bucketName: string, $start: Time, $end: Time) {\n viewer {\n accounts(filter: { accountTag: $accountTag }) {\n r2StorageAdaptiveGroups(\n limit: 1\n filter: { datetime_geq: $start, datetime_leq: $end, bucketName: $bucketName }\n orderBy: [datetime_DESC]\n ) {\n max { objectCount payloadSize metadataSize }\n dimensions { datetime }\n }\n }\n }\n}`;\n\ninterface UsageResponse {\n errors?: { message: string }[];\n data?: {\n viewer?: {\n accounts?: {\n r2StorageAdaptiveGroups?: {\n max?: { objectCount?: number; payloadSize?: number; metadataSize?: number };\n }[];\n }[];\n };\n };\n}\n\n// Reads how much an org's bucket actually holds, straight from the provider — the authoritative figure a locally\n// maintained counter would only ever approximate.\nexport class BucketUsageReader {\n private readonly logger = new Logger(BucketUsageReader.name);\n\n constructor(private readonly config: BucketUsageReaderConfig) {}\n\n // Returns the newest reported sample, or zeroes for a bucket the dataset has not reported on yet (a new or\n // empty bucket produces no rows at all rather than a row of zeroes)\n async getBucketUsage(bucketName: string, windowHours = 24): Promise<BucketUsage> {\n const end = new Date();\n const start = new Date(end.getTime() - windowHours * 60 * 60 * 1000);\n\n const response = await fetch(CF_GRAPHQL, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.analyticsToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n query: QUERY,\n variables: {\n accountTag: this.config.accountId,\n bucketName,\n start: start.toISOString(),\n end: end.toISOString(),\n },\n }),\n });\n\n const body = (await response.json()) as UsageResponse;\n if (body.errors?.length) {\n throw new Error(\n `Cloudflare usage query failed for ${bucketName}: ${body.errors.map((e) => e.message).join('; ')}`,\n );\n }\n\n const sample = body.data?.viewer?.accounts?.[0]?.r2StorageAdaptiveGroups?.[0]?.max;\n if (!sample) {\n this.logger.debug(`No usage samples yet for bucket ${bucketName}`);\n return { bytes: 0, objectCount: 0 };\n }\n\n // Billed storage is payload plus per-object metadata, so both count against a quota\n return {\n bytes: (sample.payloadSize ?? 0) + (sample.metadataSize ?? 0),\n objectCount: sample.objectCount ?? 0,\n };\n }\n}\n","// Config may be a value or a thunk. Servers pass a thunk when the credentials come from required-only-if-selected\n// env keys, so reading them is deferred to the first resolve() instead of running at module construction.\nexport type StorageConfigSource<T> = T | (() => T);\n\n// A backend configured nowhere is a deployment asking for something it was never given credentials for\nexport function readConfigSource<T>(source: StorageConfigSource<T> | undefined, provider: string): T {\n if (source === undefined) {\n throw new Error(`Storage provider '${provider}' is not configured.`);\n }\n return typeof source === 'function' ? (source as () => T)() : source;\n}\n","import type { Readable } from 'node:stream';\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n S3Client,\n} from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Logger } from '@nestjs/common';\nimport { NotFoundException } from '../../exceptions';\nimport type { ListObjectsPage, R2StorageConfig, StorageProvider, UploadParams } from '../types';\n\nexport class R2StorageProvider implements StorageProvider {\n private readonly logger = new Logger(R2StorageProvider.name);\n private readonly client: S3Client;\n\n constructor(private readonly config: R2StorageConfig) {\n this.client = new S3Client({\n region: 'auto',\n endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey },\n });\n }\n\n // Uploads a file buffer or stream\n async upload(params: UploadParams): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(params.bucket, this.config.defaultBucket),\n Key: params.key,\n Body: params.body,\n ContentType: params.contentType,\n }),\n );\n\n this.logger.log(`Uploaded file to R2: ${params.key}`);\n return params.key;\n }\n\n // Uploads a file to a public bucket and returns its permanent URL\n async uploadPublic(key: string, body: Buffer, contentType: string, bucket?: string): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(bucket, this.config.publicBucket),\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n\n const url = this.getPublicUrl(key);\n this.logger.log(`Uploaded public file to R2: ${key} → ${url}`);\n return url;\n }\n\n // Public URLs come from the configured custom domain, which maps to one bucket — R2 has no per-bucket URL to derive,\n // so a multi-tenant caller with a bucket per org cannot build one at all\n getPublicUrl(key: string): string {\n if (!this.config.publicUrl) {\n throw new Error('R2 storage has no publicUrl configured; public URLs cannot be built.');\n }\n return `${this.config.publicUrl}/${key}`;\n }\n\n // Deletes a file\n async delete(key: string, bucket?: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({ Bucket: bucket ?? this.config.defaultBucket, Key: key }));\n\n this.logger.log(`Deleted file from R2: ${key}`);\n }\n\n // Generates a presigned download URL (default 1 hour)\n async getSignedUrl(key: string, expiresInSeconds = 3600, bucket?: string): Promise<string> {\n const command = new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key });\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n }\n\n // Returns a readable stream\n async getStream(key: string, bucket?: string): Promise<Readable> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key }),\n );\n\n if (!response.Body) {\n throw new NotFoundException('File not found in storage.');\n }\n\n return response.Body as Readable;\n }\n\n // One page of a bucket's contents. S3 has no \"list everything\" call and no bucket-size call — a full inventory is\n // this looped until nextToken is absent, and each page is a Class A request.\n async listObjects(bucket: string, continuationToken?: string): Promise<ListObjectsPage> {\n const response = await this.client.send(\n new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: continuationToken }),\n );\n\n return {\n objects: (response.Contents ?? []).map((o) => ({\n key: o.Key ?? '',\n size: o.Size ?? 0,\n lastModified: o.LastModified ?? new Date(0),\n })),\n nextToken: response.NextContinuationToken,\n };\n }\n\n // Failing loudly here beats letting a missing org bucket fall through to some other tenant's default\n private requireBucket(bucket: string | undefined, fallback: string | undefined): string {\n const resolved = bucket ?? fallback;\n if (!resolved) {\n throw new Error('No bucket supplied and R2 storage has no default bucket configured.');\n }\n return resolved;\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadGatewayException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Gateway', HttpStatus.BAD_GATEWAY);\n }\n}\n","import { HttpException, HttpStatus } from '@nestjs/common';\nimport type { FieldError } from '../types/error-response.types';\n\n// Re-export FieldError for backwards compatibility\nexport type { FieldError } from '../types/error-response.types';\n\nexport interface ProblemOptions {\n type?: string;\n label?: string;\n detail?: string;\n errors?: FieldError[];\n}\n\nexport abstract class HttpProblemException extends HttpException {\n constructor(detailOrOptions: string | ProblemOptions, httpStatus: HttpStatus) {\n const options = typeof detailOrOptions === 'string' ? { detail: detailOrOptions } : detailOrOptions;\n\n super(\n {\n type: options.type ?? 'about:blank',\n label: options.label,\n detail: options.detail,\n errors: options.errors ?? [],\n },\n httpStatus,\n );\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadRequestException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Request', HttpStatus.BAD_REQUEST);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ConflictException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Conflict', HttpStatus.CONFLICT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ForbiddenException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Forbidden', HttpStatus.FORBIDDEN);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class GoneException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Gone', HttpStatus.GONE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class InternalServerErrorException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class MethodNotAllowedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Method Not Allowed', HttpStatus.METHOD_NOT_ALLOWED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotAcceptableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Acceptable', HttpStatus.NOT_ACCEPTABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotFoundException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Found', HttpStatus.NOT_FOUND);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotImplementedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Implemented', HttpStatus.NOT_IMPLEMENTED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class PayloadTooLargeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Payload Too Large', HttpStatus.PAYLOAD_TOO_LARGE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class RequestTimeoutException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Request Timeout', HttpStatus.REQUEST_TIMEOUT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ServiceUnavailableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Service Unavailable', HttpStatus.SERVICE_UNAVAILABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class TooManyRequestsException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnauthorizedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unauthorized', HttpStatus.UNAUTHORIZED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnprocessableEntityException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unprocessable Entity', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnsupportedMediaTypeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unsupported Media Type', HttpStatus.UNSUPPORTED_MEDIA_TYPE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ValidationException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Validation Failed', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { Logger } from '@nestjs/common';\nimport type { R2ProvisionerConfig, ScopedTokenResult, StorageProvisioner } from '../types';\n\nconst CF_API = 'https://api.cloudflare.com/client/v4';\n\n// Bucket-level group: read/write/list objects in named buckets. Deliberately NOT 'Workers R2 Storage Write', which is\n// account-level and can create and delete buckets — exactly what a tenant credential must never do.\nconst BUCKET_ITEM_WRITE = 'Workers R2 Storage Bucket Item Write';\n\ninterface CloudflareEnvelope<T> {\n success: boolean;\n errors: { code: number; message: string }[];\n result: T;\n}\n\n// Generic R2 admin client: create/delete buckets, toggle a bucket's Cloudflare-managed public domain, and mint/revoke\n// bucket-scoped credentials. It has NO notion of organizations — bucket naming, which buckets a tenant gets, and how\n// the stored descriptor is assembled all live in the caller. Every operation is a Cloudflare REST call.\nexport class R2BucketProvisioner implements StorageProvisioner {\n private readonly logger = new Logger(R2BucketProvisioner.name);\n private readonly jurisdiction: string;\n private bucketItemWriteGroupId: string | null = null;\n\n constructor(private readonly config: R2ProvisionerConfig) {\n this.jurisdiction = config.jurisdiction ?? 'default';\n }\n\n // Creates one bucket, treating an existing bucket as success so provisioning can be re-run to reconcile\n async createBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.adminToken}`,\n 'Content-Type': 'application/json',\n 'cf-r2-jurisdiction': this.jurisdiction,\n },\n body: JSON.stringify({\n name,\n ...(this.config.locationHint && { locationHint: this.config.locationHint }),\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (body.success) {\n this.logger.log(`Created bucket ${name}`);\n return;\n }\n\n // 10004 is \"bucket already exists\" — two provisioning attempts racing, or a reconcile pass\n if (body.errors?.some((e) => e.code === 10004)) return;\n throw new Error(`Cloudflare bucket create failed for ${name}: ${this.describe(body)}`);\n }\n\n // Deletes one bucket. R2 refuses to delete a bucket that still holds objects, so the caller must have emptied it\n // first — a 'not empty' failure here means that step did not finish. A missing bucket counts as success.\n async deleteBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${name}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.adminToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n // 10006 is \"bucket not found\" — already gone, which is the state we wanted\n if (body.success || body.errors?.some((e) => e.code === 10006)) {\n this.logger.log(`Deleted bucket ${name}`);\n return;\n }\n throw new Error(`Cloudflare bucket delete failed for ${name}: ${this.describe(body)}`);\n }\n\n // Turns on the bucket's Cloudflare-managed domain and returns its https URL. NOTE: r2.dev is rate limited and\n // documented as non-production — sustained traffic gets 429s. A custom domain per bucket is the production answer,\n // and drops into this same field. Throws on failure; the caller decides whether that is fatal.\n async enablePublicAccess(bucket: string): Promise<string> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${bucket}/domains/managed`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${this.config.adminToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ enabled: true }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ domain?: string; enabled?: boolean }>;\n if (!body.success || !body.result?.domain) {\n throw new Error(`Cloudflare enable public access failed for ${bucket}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Enabled public access for ${bucket} → ${body.result.domain}`);\n return `https://${body.result.domain}`;\n }\n\n // Mints an ACCOUNT-owned token that can only touch the named buckets. Account-owned rather than user-owned so a\n // tenant's credential does not die with whichever Cloudflare user happened to create the parent token. Requires the\n // parent to hold `Account API Tokens Write`; a user-scoped `API Tokens: Edit` token is rejected here with 9109.\n // `name` is the token's display label; the value is returned by R2 exactly once.\n async createScopedToken(name: string, buckets: string[]): Promise<ScopedTokenResult> {\n const permissionGroupId = await this.resolveBucketItemWriteGroupId();\n\n // The jurisdiction is embedded in the resource key and must match the bucket's, or the token authenticates fine\n // and then 403s on every object because it is scoped to a bucket that does not exist\n const resources = Object.fromEntries(\n buckets.map((bucket) => [\n `com.cloudflare.edge.r2.bucket.${this.config.accountId}_${this.jurisdiction}_${bucket}`,\n '*',\n ]),\n );\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.tokensToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n name,\n policies: [{ effect: 'allow', permission_groups: [{ id: permissionGroupId }], resources }],\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; value: string }>;\n if (!body.success || !body.result?.value) {\n throw new Error(`Cloudflare token create failed for ${name}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Minted storage credential ${name} scoped to ${buckets.length} bucket(s)`);\n return body.result;\n }\n\n // Revokes a credential by its access key id, which on R2 is the Cloudflare token id\n async deleteCredential(accessKeyId: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/${accessKeyId}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (!body.success) {\n throw new Error(`Cloudflare token delete failed for ${accessKeyId}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Revoked storage credential ${accessKeyId}`);\n }\n\n // The create-token API takes permission group UUIDs, not names. Looked up once rather than hardcoded: a UUID copied\n // from docs fails much later with an error that says nothing useful. Same account-scoped endpoint as creation.\n private async resolveBucketItemWriteGroupId(): Promise<string> {\n if (this.bucketItemWriteGroupId) return this.bucketItemWriteGroupId;\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/permission_groups`, {\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; name: string }[]>;\n const group = body.result?.find((g) => g.name === BUCKET_ITEM_WRITE);\n if (!group) {\n throw new Error(`Cloudflare permission group '${BUCKET_ITEM_WRITE}' not found: ${this.describe(body)}`);\n }\n\n this.bucketItemWriteGroupId = group.id;\n return group.id;\n }\n\n private describe(body: CloudflareEnvelope<unknown>): string {\n return body.errors?.map((e) => `${e.code} ${e.message}`).join('; ') || 'unknown error';\n }\n}\n","import { readConfigSource, type StorageConfigSource } from './config-source';\nimport { R2StorageProvider } from './providers/r2-storage.provider';\nimport type { R2StorageConfig, StorageProvider } from './types';\n\nexport interface StorageFactoryOptions {\n r2?: StorageConfigSource<R2StorageConfig>;\n}\n\nexport class StorageFactory {\n private readonly cache = new Map<string, StorageProvider>();\n\n constructor(private readonly options: StorageFactoryOptions) {}\n\n // Resolves a provider by name, constructing it once on first use\n resolve(provider: string): StorageProvider {\n const cached = this.cache.get(provider);\n if (cached) return cached;\n\n const created = this.create(provider);\n this.cache.set(provider, created);\n return created;\n }\n\n private create(provider: string): StorageProvider {\n switch (provider) {\n case 'r2':\n return new R2StorageProvider(readConfigSource(this.options.r2, 'r2'));\n default:\n throw new Error(`Unsupported storage provider: ${provider}`);\n }\n }\n}\n","import { readConfigSource, type StorageConfigSource } from './config-source';\nimport { R2BucketProvisioner } from './provisioners/r2-bucket.provisioner';\nimport type { R2ProvisionerConfig, StorageProvisioner } from './types';\n\nexport interface StorageProvisionerFactoryOptions {\n r2?: StorageConfigSource<R2ProvisionerConfig>;\n}\n\n// Mirrors StorageFactory for the admin side: same provider names, same lazy config, different job. Resolves a generic\n// StorageProvisioner (bucket + credential admin) by provider name so the caller is not pinned to a single backend.\nexport class StorageProvisionerFactory {\n private readonly cache = new Map<string, StorageProvisioner>();\n\n constructor(private readonly options: StorageProvisionerFactoryOptions) {}\n\n // Resolves a provisioner by provider name, constructing it once on first use\n resolve(provider: string): StorageProvisioner {\n const cached = this.cache.get(provider);\n if (cached) return cached;\n\n const created = this.create(provider);\n this.cache.set(provider, created);\n return created;\n }\n\n private create(provider: string): StorageProvisioner {\n switch (provider) {\n case 'r2':\n return new R2BucketProvisioner(readConfigSource(this.options.r2, 'r2'));\n default:\n throw new Error(`Unsupported storage provisioner: ${provider}`);\n }\n }\n}\n"],"mappings":"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","names":["CF_GRAPHQL","QUERY","BucketUsageReader","logger","Logger","name","config","getBucketUsage","bucketName","windowHours","end","Date","start","getTime","response","fetch","method","headers","Authorization","analyticsToken","body","JSON","stringify","query","variables","accountTag","accountId","toISOString","json","errors","length","Error","map","e","message","join","sample","data","viewer","accounts","r2StorageAdaptiveGroups","max","debug","bytes","objectCount","payloadSize","metadataSize","readConfigSource","source","provider","undefined","Error","import_common","import_common","import_common","HttpProblemException","HttpException","detailOrOptions","httpStatus","options","detail","type","label","errors","import_common","import_common","import_common","import_common","import_common","import_common","import_common","import_common","NotFoundException","HttpProblemException","detailOrOptions","HttpStatus","NOT_FOUND","import_common","import_common","import_common","import_common","import_common","import_common","import_common","import_common","import_common","R2StorageProvider","logger","Logger","name","client","config","S3Client","region","endpoint","accountId","credentials","accessKeyId","secretAccessKey","upload","params","send","PutObjectCommand","Bucket","requireBucket","bucket","defaultBucket","Key","key","Body","body","ContentType","contentType","log","uploadPublic","publicBucket","url","getPublicUrl","publicUrl","Error","delete","DeleteObjectCommand","getSignedUrl","expiresInSeconds","command","GetObjectCommand","expiresIn","getStream","response","NotFoundException","listObjects","continuationToken","ListObjectsV2Command","ContinuationToken","objects","Contents","map","o","size","Size","lastModified","LastModified","Date","nextToken","NextContinuationToken","fallback","resolved","import_common","CF_API","BUCKET_ITEM_WRITE","R2BucketProvisioner","logger","Logger","name","jurisdiction","bucketItemWriteGroupId","config","createBucket","response","fetch","accountId","method","headers","Authorization","adminToken","body","JSON","stringify","locationHint","json","success","log","errors","some","e","code","Error","describe","deleteBucket","enablePublicAccess","bucket","enabled","result","domain","createScopedToken","buckets","permissionGroupId","resolveBucketItemWriteGroupId","resources","Object","fromEntries","map","tokensToken","policies","effect","permission_groups","id","value","length","deleteCredential","accessKeyId","group","find","g","message","join","StorageFactory","cache","Map","options","resolve","provider","cached","get","created","create","set","R2StorageProvider","readConfigSource","r2","Error","StorageProvisionerFactory","cache","Map","options","resolve","provider","cached","get","created","create","set","R2BucketProvisioner","readConfigSource","r2","Error"]}
@@ -42,6 +42,17 @@ interface StorageProvider {
42
42
  getStream(key: string, bucket?: string): Promise<Readable>;
43
43
  listObjects(bucket: string, continuationToken?: string): Promise<ListObjectsPage>;
44
44
  }
45
+ interface ScopedTokenResult {
46
+ id: string;
47
+ value: string;
48
+ }
49
+ interface StorageProvisioner {
50
+ createBucket(name: string): Promise<void>;
51
+ deleteBucket(name: string): Promise<void>;
52
+ enablePublicAccess(bucket: string): Promise<string>;
53
+ createScopedToken(name: string, buckets: string[]): Promise<ScopedTokenResult>;
54
+ deleteCredential(accessKeyId: string): Promise<void>;
55
+ }
45
56
  interface R2ProvisionerConfig {
46
57
  accountId: string;
47
58
  adminToken: string;
@@ -73,7 +84,7 @@ declare class R2StorageProvider implements StorageProvider {
73
84
  private requireBucket;
74
85
  }
75
86
 
76
- declare class R2BucketProvisioner {
87
+ declare class R2BucketProvisioner implements StorageProvisioner {
77
88
  private readonly config;
78
89
  private readonly logger;
79
90
  private readonly jurisdiction;
@@ -82,10 +93,7 @@ declare class R2BucketProvisioner {
82
93
  createBucket(name: string): Promise<void>;
83
94
  deleteBucket(name: string): Promise<void>;
84
95
  enablePublicAccess(bucket: string): Promise<string>;
85
- createScopedToken(name: string, buckets: string[]): Promise<{
86
- id: string;
87
- value: string;
88
- }>;
96
+ createScopedToken(name: string, buckets: string[]): Promise<ScopedTokenResult>;
89
97
  deleteCredential(accessKeyId: string): Promise<void>;
90
98
  private resolveBucketItemWriteGroupId;
91
99
  private describe;
@@ -102,4 +110,15 @@ declare class StorageFactory {
102
110
  private create;
103
111
  }
104
112
 
105
- export { type BucketUsage, BucketUsageReader, type BucketUsageReaderConfig, type ListObjectsPage, R2BucketProvisioner, type R2ProvisionerConfig, type R2StorageConfig, R2StorageProvider, type StorageConfigSource, StorageFactory, type StorageFactoryOptions, type StorageProvider, type StoredObject, type UploadParams, readConfigSource };
113
+ interface StorageProvisionerFactoryOptions {
114
+ r2?: StorageConfigSource<R2ProvisionerConfig>;
115
+ }
116
+ declare class StorageProvisionerFactory {
117
+ private readonly options;
118
+ private readonly cache;
119
+ constructor(options: StorageProvisionerFactoryOptions);
120
+ resolve(provider: string): StorageProvisioner;
121
+ private create;
122
+ }
123
+
124
+ export { type BucketUsage, BucketUsageReader, type BucketUsageReaderConfig, type ListObjectsPage, R2BucketProvisioner, type R2ProvisionerConfig, type R2StorageConfig, R2StorageProvider, type ScopedTokenResult, type StorageConfigSource, StorageFactory, type StorageFactoryOptions, type StorageProvider, type StorageProvisioner, StorageProvisionerFactory, type StorageProvisionerFactoryOptions, type StoredObject, type UploadParams, readConfigSource };
package/dist/storage.d.ts CHANGED
@@ -42,6 +42,17 @@ interface StorageProvider {
42
42
  getStream(key: string, bucket?: string): Promise<Readable>;
43
43
  listObjects(bucket: string, continuationToken?: string): Promise<ListObjectsPage>;
44
44
  }
45
+ interface ScopedTokenResult {
46
+ id: string;
47
+ value: string;
48
+ }
49
+ interface StorageProvisioner {
50
+ createBucket(name: string): Promise<void>;
51
+ deleteBucket(name: string): Promise<void>;
52
+ enablePublicAccess(bucket: string): Promise<string>;
53
+ createScopedToken(name: string, buckets: string[]): Promise<ScopedTokenResult>;
54
+ deleteCredential(accessKeyId: string): Promise<void>;
55
+ }
45
56
  interface R2ProvisionerConfig {
46
57
  accountId: string;
47
58
  adminToken: string;
@@ -73,7 +84,7 @@ declare class R2StorageProvider implements StorageProvider {
73
84
  private requireBucket;
74
85
  }
75
86
 
76
- declare class R2BucketProvisioner {
87
+ declare class R2BucketProvisioner implements StorageProvisioner {
77
88
  private readonly config;
78
89
  private readonly logger;
79
90
  private readonly jurisdiction;
@@ -82,10 +93,7 @@ declare class R2BucketProvisioner {
82
93
  createBucket(name: string): Promise<void>;
83
94
  deleteBucket(name: string): Promise<void>;
84
95
  enablePublicAccess(bucket: string): Promise<string>;
85
- createScopedToken(name: string, buckets: string[]): Promise<{
86
- id: string;
87
- value: string;
88
- }>;
96
+ createScopedToken(name: string, buckets: string[]): Promise<ScopedTokenResult>;
89
97
  deleteCredential(accessKeyId: string): Promise<void>;
90
98
  private resolveBucketItemWriteGroupId;
91
99
  private describe;
@@ -102,4 +110,15 @@ declare class StorageFactory {
102
110
  private create;
103
111
  }
104
112
 
105
- export { type BucketUsage, BucketUsageReader, type BucketUsageReaderConfig, type ListObjectsPage, R2BucketProvisioner, type R2ProvisionerConfig, type R2StorageConfig, R2StorageProvider, type StorageConfigSource, StorageFactory, type StorageFactoryOptions, type StorageProvider, type StoredObject, type UploadParams, readConfigSource };
113
+ interface StorageProvisionerFactoryOptions {
114
+ r2?: StorageConfigSource<R2ProvisionerConfig>;
115
+ }
116
+ declare class StorageProvisionerFactory {
117
+ private readonly options;
118
+ private readonly cache;
119
+ constructor(options: StorageProvisionerFactoryOptions);
120
+ resolve(provider: string): StorageProvisioner;
121
+ private create;
122
+ }
123
+
124
+ export { type BucketUsage, BucketUsageReader, type BucketUsageReaderConfig, type ListObjectsPage, R2BucketProvisioner, type R2ProvisionerConfig, type R2StorageConfig, R2StorageProvider, type ScopedTokenResult, type StorageConfigSource, StorageFactory, type StorageFactoryOptions, type StorageProvider, type StorageProvisioner, StorageProvisionerFactory, type StorageProvisionerFactoryOptions, type StoredObject, type UploadParams, readConfigSource };
package/dist/storage.js CHANGED
@@ -445,11 +445,40 @@ var StorageFactory = class {
445
445
  }
446
446
  }
447
447
  };
448
+
449
+ // src/storage/storage-provisioner.factory.ts
450
+ var StorageProvisionerFactory = class {
451
+ static {
452
+ __name(this, "StorageProvisionerFactory");
453
+ }
454
+ options;
455
+ cache = /* @__PURE__ */ new Map();
456
+ constructor(options) {
457
+ this.options = options;
458
+ }
459
+ // Resolves a provisioner by provider name, constructing it once on first use
460
+ resolve(provider) {
461
+ const cached = this.cache.get(provider);
462
+ if (cached) return cached;
463
+ const created = this.create(provider);
464
+ this.cache.set(provider, created);
465
+ return created;
466
+ }
467
+ create(provider) {
468
+ switch (provider) {
469
+ case "r2":
470
+ return new R2BucketProvisioner(readConfigSource(this.options.r2, "r2"));
471
+ default:
472
+ throw new Error(`Unsupported storage provisioner: ${provider}`);
473
+ }
474
+ }
475
+ };
448
476
  export {
449
477
  BucketUsageReader,
450
478
  R2BucketProvisioner,
451
479
  R2StorageProvider,
452
480
  StorageFactory,
481
+ StorageProvisionerFactory,
453
482
  readConfigSource
454
483
  };
455
484
  //# sourceMappingURL=storage.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storage/bucket-usage.reader.ts","../src/storage/config-source.ts","../src/storage/providers/r2-storage.provider.ts","../src/exceptions/bad-gateway.exception.ts","../src/exceptions/base-field.exception.ts","../src/exceptions/bad-request.exception.ts","../src/exceptions/conflict.exception.ts","../src/exceptions/forbidden.exception.ts","../src/exceptions/gone.exception.ts","../src/exceptions/internal-server-error.exception.ts","../src/exceptions/method-not-allowed.exception.ts","../src/exceptions/not-acceptable.exception.ts","../src/exceptions/not-found.exception.ts","../src/exceptions/not-implemented.exception.ts","../src/exceptions/payload-too-large.exception.ts","../src/exceptions/request-timeout.exception.ts","../src/exceptions/service-unavailable.exception.ts","../src/exceptions/too-many-requests.exception.ts","../src/exceptions/unauthorized.exception.ts","../src/exceptions/unprocessable-entity.exception.ts","../src/exceptions/unsupported-media-type.exception.ts","../src/exceptions/validation.exception.ts","../src/storage/provisioners/r2-bucket.provisioner.ts","../src/storage/storage.factory.ts"],"sourcesContent":["import { Logger } from '@nestjs/common';\n\nconst CF_GRAPHQL = 'https://api.cloudflare.com/client/v4/graphql';\n\nexport interface BucketUsage {\n bytes: number;\n objectCount: number;\n}\n\nexport interface BucketUsageReaderConfig {\n accountId: string;\n // Account-level token with Analytics Read. NOT the org's scoped S3 credential and NOT the R2 admin token: the\n // analytics dataset is filtered by accountTag and is not reachable with object-level credentials.\n analyticsToken: string;\n}\n\n// r2StorageAdaptiveGroups is a time series, so the newest bucketed sample is the closest thing to \"current\". It lags\n// real writes by minutes — fine for a periodic quota check, useless for gating an individual upload.\n//\n// `dimensions { datetime }` is required, not decorative: ordering by a field that is neither aggregated nor selected\n// as a dimension is rejected. Keep the query free of `#` comments too — Cloudflare's parser rejects them.\nconst QUERY = `query BucketUsage($accountTag: string!, $bucketName: string, $start: Time, $end: Time) {\n viewer {\n accounts(filter: { accountTag: $accountTag }) {\n r2StorageAdaptiveGroups(\n limit: 1\n filter: { datetime_geq: $start, datetime_leq: $end, bucketName: $bucketName }\n orderBy: [datetime_DESC]\n ) {\n max { objectCount payloadSize metadataSize }\n dimensions { datetime }\n }\n }\n }\n}`;\n\ninterface UsageResponse {\n errors?: { message: string }[];\n data?: {\n viewer?: {\n accounts?: {\n r2StorageAdaptiveGroups?: {\n max?: { objectCount?: number; payloadSize?: number; metadataSize?: number };\n }[];\n }[];\n };\n };\n}\n\n// Reads how much an org's bucket actually holds, straight from the provider — the authoritative figure a locally\n// maintained counter would only ever approximate.\nexport class BucketUsageReader {\n private readonly logger = new Logger(BucketUsageReader.name);\n\n constructor(private readonly config: BucketUsageReaderConfig) {}\n\n // Returns the newest reported sample, or zeroes for a bucket the dataset has not reported on yet (a new or\n // empty bucket produces no rows at all rather than a row of zeroes)\n async getBucketUsage(bucketName: string, windowHours = 24): Promise<BucketUsage> {\n const end = new Date();\n const start = new Date(end.getTime() - windowHours * 60 * 60 * 1000);\n\n const response = await fetch(CF_GRAPHQL, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.analyticsToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n query: QUERY,\n variables: {\n accountTag: this.config.accountId,\n bucketName,\n start: start.toISOString(),\n end: end.toISOString(),\n },\n }),\n });\n\n const body = (await response.json()) as UsageResponse;\n if (body.errors?.length) {\n throw new Error(\n `Cloudflare usage query failed for ${bucketName}: ${body.errors.map((e) => e.message).join('; ')}`,\n );\n }\n\n const sample = body.data?.viewer?.accounts?.[0]?.r2StorageAdaptiveGroups?.[0]?.max;\n if (!sample) {\n this.logger.debug(`No usage samples yet for bucket ${bucketName}`);\n return { bytes: 0, objectCount: 0 };\n }\n\n // Billed storage is payload plus per-object metadata, so both count against a quota\n return {\n bytes: (sample.payloadSize ?? 0) + (sample.metadataSize ?? 0),\n objectCount: sample.objectCount ?? 0,\n };\n }\n}\n","// Config may be a value or a thunk. Servers pass a thunk when the credentials come from required-only-if-selected\n// env keys, so reading them is deferred to the first resolve() instead of running at module construction.\nexport type StorageConfigSource<T> = T | (() => T);\n\n// A backend configured nowhere is a deployment asking for something it was never given credentials for\nexport function readConfigSource<T>(source: StorageConfigSource<T> | undefined, provider: string): T {\n if (source === undefined) {\n throw new Error(`Storage provider '${provider}' is not configured.`);\n }\n return typeof source === 'function' ? (source as () => T)() : source;\n}\n","import type { Readable } from 'node:stream';\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n S3Client,\n} from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Logger } from '@nestjs/common';\nimport { NotFoundException } from '../../exceptions';\nimport type { ListObjectsPage, R2StorageConfig, StorageProvider, UploadParams } from '../types';\n\nexport class R2StorageProvider implements StorageProvider {\n private readonly logger = new Logger(R2StorageProvider.name);\n private readonly client: S3Client;\n\n constructor(private readonly config: R2StorageConfig) {\n this.client = new S3Client({\n region: 'auto',\n endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey },\n });\n }\n\n // Uploads a file buffer or stream\n async upload(params: UploadParams): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(params.bucket, this.config.defaultBucket),\n Key: params.key,\n Body: params.body,\n ContentType: params.contentType,\n }),\n );\n\n this.logger.log(`Uploaded file to R2: ${params.key}`);\n return params.key;\n }\n\n // Uploads a file to a public bucket and returns its permanent URL\n async uploadPublic(key: string, body: Buffer, contentType: string, bucket?: string): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(bucket, this.config.publicBucket),\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n\n const url = this.getPublicUrl(key);\n this.logger.log(`Uploaded public file to R2: ${key} → ${url}`);\n return url;\n }\n\n // Public URLs come from the configured custom domain, which maps to one bucket — R2 has no per-bucket URL to derive,\n // so a multi-tenant caller with a bucket per org cannot build one at all\n getPublicUrl(key: string): string {\n if (!this.config.publicUrl) {\n throw new Error('R2 storage has no publicUrl configured; public URLs cannot be built.');\n }\n return `${this.config.publicUrl}/${key}`;\n }\n\n // Deletes a file\n async delete(key: string, bucket?: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({ Bucket: bucket ?? this.config.defaultBucket, Key: key }));\n\n this.logger.log(`Deleted file from R2: ${key}`);\n }\n\n // Generates a presigned download URL (default 1 hour)\n async getSignedUrl(key: string, expiresInSeconds = 3600, bucket?: string): Promise<string> {\n const command = new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key });\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n }\n\n // Returns a readable stream\n async getStream(key: string, bucket?: string): Promise<Readable> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key }),\n );\n\n if (!response.Body) {\n throw new NotFoundException('File not found in storage.');\n }\n\n return response.Body as Readable;\n }\n\n // One page of a bucket's contents. S3 has no \"list everything\" call and no bucket-size call — a full inventory is\n // this looped until nextToken is absent, and each page is a Class A request.\n async listObjects(bucket: string, continuationToken?: string): Promise<ListObjectsPage> {\n const response = await this.client.send(\n new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: continuationToken }),\n );\n\n return {\n objects: (response.Contents ?? []).map((o) => ({\n key: o.Key ?? '',\n size: o.Size ?? 0,\n lastModified: o.LastModified ?? new Date(0),\n })),\n nextToken: response.NextContinuationToken,\n };\n }\n\n // Failing loudly here beats letting a missing org bucket fall through to some other tenant's default\n private requireBucket(bucket: string | undefined, fallback: string | undefined): string {\n const resolved = bucket ?? fallback;\n if (!resolved) {\n throw new Error('No bucket supplied and R2 storage has no default bucket configured.');\n }\n return resolved;\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadGatewayException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Gateway', HttpStatus.BAD_GATEWAY);\n }\n}\n","import { HttpException, HttpStatus } from '@nestjs/common';\nimport type { FieldError } from '../types/error-response.types';\n\n// Re-export FieldError for backwards compatibility\nexport type { FieldError } from '../types/error-response.types';\n\nexport interface ProblemOptions {\n type?: string;\n label?: string;\n detail?: string;\n errors?: FieldError[];\n}\n\nexport abstract class HttpProblemException extends HttpException {\n constructor(detailOrOptions: string | ProblemOptions, httpStatus: HttpStatus) {\n const options = typeof detailOrOptions === 'string' ? { detail: detailOrOptions } : detailOrOptions;\n\n super(\n {\n type: options.type ?? 'about:blank',\n label: options.label,\n detail: options.detail,\n errors: options.errors ?? [],\n },\n httpStatus,\n );\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadRequestException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Request', HttpStatus.BAD_REQUEST);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ConflictException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Conflict', HttpStatus.CONFLICT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ForbiddenException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Forbidden', HttpStatus.FORBIDDEN);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class GoneException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Gone', HttpStatus.GONE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class InternalServerErrorException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class MethodNotAllowedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Method Not Allowed', HttpStatus.METHOD_NOT_ALLOWED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotAcceptableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Acceptable', HttpStatus.NOT_ACCEPTABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotFoundException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Found', HttpStatus.NOT_FOUND);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotImplementedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Implemented', HttpStatus.NOT_IMPLEMENTED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class PayloadTooLargeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Payload Too Large', HttpStatus.PAYLOAD_TOO_LARGE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class RequestTimeoutException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Request Timeout', HttpStatus.REQUEST_TIMEOUT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ServiceUnavailableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Service Unavailable', HttpStatus.SERVICE_UNAVAILABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class TooManyRequestsException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnauthorizedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unauthorized', HttpStatus.UNAUTHORIZED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnprocessableEntityException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unprocessable Entity', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnsupportedMediaTypeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unsupported Media Type', HttpStatus.UNSUPPORTED_MEDIA_TYPE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ValidationException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Validation Failed', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { Logger } from '@nestjs/common';\nimport type { R2ProvisionerConfig } from '../types';\n\nconst CF_API = 'https://api.cloudflare.com/client/v4';\n\n// Bucket-level group: read/write/list objects in named buckets. Deliberately NOT 'Workers R2 Storage Write', which is\n// account-level and can create and delete buckets — exactly what a tenant credential must never do.\nconst BUCKET_ITEM_WRITE = 'Workers R2 Storage Bucket Item Write';\n\ninterface CloudflareEnvelope<T> {\n success: boolean;\n errors: { code: number; message: string }[];\n result: T;\n}\n\n// Generic R2 admin client: create/delete buckets, toggle a bucket's Cloudflare-managed public domain, and mint/revoke\n// bucket-scoped credentials. It has NO notion of organizations — bucket naming, which buckets a tenant gets, and how\n// the stored descriptor is assembled all live in the caller. Every operation is a Cloudflare REST call.\nexport class R2BucketProvisioner {\n private readonly logger = new Logger(R2BucketProvisioner.name);\n private readonly jurisdiction: string;\n private bucketItemWriteGroupId: string | null = null;\n\n constructor(private readonly config: R2ProvisionerConfig) {\n this.jurisdiction = config.jurisdiction ?? 'default';\n }\n\n // Creates one bucket, treating an existing bucket as success so provisioning can be re-run to reconcile\n async createBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.adminToken}`,\n 'Content-Type': 'application/json',\n 'cf-r2-jurisdiction': this.jurisdiction,\n },\n body: JSON.stringify({\n name,\n ...(this.config.locationHint && { locationHint: this.config.locationHint }),\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (body.success) {\n this.logger.log(`Created bucket ${name}`);\n return;\n }\n\n // 10004 is \"bucket already exists\" — two provisioning attempts racing, or a reconcile pass\n if (body.errors?.some((e) => e.code === 10004)) return;\n throw new Error(`Cloudflare bucket create failed for ${name}: ${this.describe(body)}`);\n }\n\n // Deletes one bucket. R2 refuses to delete a bucket that still holds objects, so the caller must have emptied it\n // first — a 'not empty' failure here means that step did not finish. A missing bucket counts as success.\n async deleteBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${name}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.adminToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n // 10006 is \"bucket not found\" — already gone, which is the state we wanted\n if (body.success || body.errors?.some((e) => e.code === 10006)) {\n this.logger.log(`Deleted bucket ${name}`);\n return;\n }\n throw new Error(`Cloudflare bucket delete failed for ${name}: ${this.describe(body)}`);\n }\n\n // Turns on the bucket's Cloudflare-managed domain and returns its https URL. NOTE: r2.dev is rate limited and\n // documented as non-production — sustained traffic gets 429s. A custom domain per bucket is the production answer,\n // and drops into this same field. Throws on failure; the caller decides whether that is fatal.\n async enablePublicAccess(bucket: string): Promise<string> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${bucket}/domains/managed`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${this.config.adminToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ enabled: true }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ domain?: string; enabled?: boolean }>;\n if (!body.success || !body.result?.domain) {\n throw new Error(`Cloudflare enable public access failed for ${bucket}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Enabled public access for ${bucket} → ${body.result.domain}`);\n return `https://${body.result.domain}`;\n }\n\n // Mints an ACCOUNT-owned token that can only touch the named buckets. Account-owned rather than user-owned so a\n // tenant's credential does not die with whichever Cloudflare user happened to create the parent token. Requires the\n // parent to hold `Account API Tokens Write`; a user-scoped `API Tokens: Edit` token is rejected here with 9109.\n // `name` is the token's display label; the value is returned by R2 exactly once.\n async createScopedToken(name: string, buckets: string[]): Promise<{ id: string; value: string }> {\n const permissionGroupId = await this.resolveBucketItemWriteGroupId();\n\n // The jurisdiction is embedded in the resource key and must match the bucket's, or the token authenticates fine\n // and then 403s on every object because it is scoped to a bucket that does not exist\n const resources = Object.fromEntries(\n buckets.map((bucket) => [\n `com.cloudflare.edge.r2.bucket.${this.config.accountId}_${this.jurisdiction}_${bucket}`,\n '*',\n ]),\n );\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.tokensToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n name,\n policies: [{ effect: 'allow', permission_groups: [{ id: permissionGroupId }], resources }],\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; value: string }>;\n if (!body.success || !body.result?.value) {\n throw new Error(`Cloudflare token create failed for ${name}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Minted storage credential ${name} scoped to ${buckets.length} bucket(s)`);\n return body.result;\n }\n\n // Revokes a credential by its access key id, which on R2 is the Cloudflare token id\n async deleteCredential(accessKeyId: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/${accessKeyId}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (!body.success) {\n throw new Error(`Cloudflare token delete failed for ${accessKeyId}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Revoked storage credential ${accessKeyId}`);\n }\n\n // The create-token API takes permission group UUIDs, not names. Looked up once rather than hardcoded: a UUID copied\n // from docs fails much later with an error that says nothing useful. Same account-scoped endpoint as creation.\n private async resolveBucketItemWriteGroupId(): Promise<string> {\n if (this.bucketItemWriteGroupId) return this.bucketItemWriteGroupId;\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/permission_groups`, {\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; name: string }[]>;\n const group = body.result?.find((g) => g.name === BUCKET_ITEM_WRITE);\n if (!group) {\n throw new Error(`Cloudflare permission group '${BUCKET_ITEM_WRITE}' not found: ${this.describe(body)}`);\n }\n\n this.bucketItemWriteGroupId = group.id;\n return group.id;\n }\n\n private describe(body: CloudflareEnvelope<unknown>): string {\n return body.errors?.map((e) => `${e.code} ${e.message}`).join('; ') || 'unknown error';\n }\n}\n","import { readConfigSource, type StorageConfigSource } from './config-source';\nimport { R2StorageProvider } from './providers/r2-storage.provider';\nimport type { R2StorageConfig, StorageProvider } from './types';\n\nexport interface StorageFactoryOptions {\n r2?: StorageConfigSource<R2StorageConfig>;\n}\n\nexport class StorageFactory {\n private readonly cache = new Map<string, StorageProvider>();\n\n constructor(private readonly options: StorageFactoryOptions) {}\n\n // Resolves a provider by name, constructing it once on first use\n resolve(provider: string): StorageProvider {\n const cached = this.cache.get(provider);\n if (cached) return cached;\n\n const created = this.create(provider);\n this.cache.set(provider, created);\n return created;\n }\n\n private create(provider: string): StorageProvider {\n switch (provider) {\n case 'r2':\n return new R2StorageProvider(readConfigSource(this.options.r2, 'r2'));\n default:\n throw new Error(`Unsupported storage provider: ${provider}`);\n }\n }\n}\n"],"mappings":"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","names":["Logger","CF_GRAPHQL","QUERY","BucketUsageReader","logger","Logger","name","config","getBucketUsage","bucketName","windowHours","end","Date","start","getTime","response","fetch","method","headers","Authorization","analyticsToken","body","JSON","stringify","query","variables","accountTag","accountId","toISOString","json","errors","length","Error","map","e","message","join","sample","data","viewer","accounts","r2StorageAdaptiveGroups","max","debug","bytes","objectCount","payloadSize","metadataSize","readConfigSource","source","provider","undefined","Error","DeleteObjectCommand","GetObjectCommand","ListObjectsV2Command","PutObjectCommand","S3Client","getSignedUrl","Logger","HttpStatus","HttpException","HttpProblemException","HttpException","detailOrOptions","httpStatus","options","detail","type","label","errors","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","NotFoundException","HttpProblemException","detailOrOptions","HttpStatus","NOT_FOUND","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","R2StorageProvider","logger","Logger","name","client","config","S3Client","region","endpoint","accountId","credentials","accessKeyId","secretAccessKey","upload","params","send","PutObjectCommand","Bucket","requireBucket","bucket","defaultBucket","Key","key","Body","body","ContentType","contentType","log","uploadPublic","publicBucket","url","getPublicUrl","publicUrl","Error","delete","DeleteObjectCommand","getSignedUrl","expiresInSeconds","command","GetObjectCommand","expiresIn","getStream","response","NotFoundException","listObjects","continuationToken","ListObjectsV2Command","ContinuationToken","objects","Contents","map","o","size","Size","lastModified","LastModified","Date","nextToken","NextContinuationToken","fallback","resolved","Logger","CF_API","BUCKET_ITEM_WRITE","R2BucketProvisioner","logger","Logger","name","jurisdiction","bucketItemWriteGroupId","config","createBucket","response","fetch","accountId","method","headers","Authorization","adminToken","body","JSON","stringify","locationHint","json","success","log","errors","some","e","code","Error","describe","deleteBucket","enablePublicAccess","bucket","enabled","result","domain","createScopedToken","buckets","permissionGroupId","resolveBucketItemWriteGroupId","resources","Object","fromEntries","map","tokensToken","policies","effect","permission_groups","id","value","length","deleteCredential","accessKeyId","group","find","g","message","join","StorageFactory","cache","Map","options","resolve","provider","cached","get","created","create","set","R2StorageProvider","readConfigSource","r2","Error"]}
1
+ {"version":3,"sources":["../src/storage/bucket-usage.reader.ts","../src/storage/config-source.ts","../src/storage/providers/r2-storage.provider.ts","../src/exceptions/bad-gateway.exception.ts","../src/exceptions/base-field.exception.ts","../src/exceptions/bad-request.exception.ts","../src/exceptions/conflict.exception.ts","../src/exceptions/forbidden.exception.ts","../src/exceptions/gone.exception.ts","../src/exceptions/internal-server-error.exception.ts","../src/exceptions/method-not-allowed.exception.ts","../src/exceptions/not-acceptable.exception.ts","../src/exceptions/not-found.exception.ts","../src/exceptions/not-implemented.exception.ts","../src/exceptions/payload-too-large.exception.ts","../src/exceptions/request-timeout.exception.ts","../src/exceptions/service-unavailable.exception.ts","../src/exceptions/too-many-requests.exception.ts","../src/exceptions/unauthorized.exception.ts","../src/exceptions/unprocessable-entity.exception.ts","../src/exceptions/unsupported-media-type.exception.ts","../src/exceptions/validation.exception.ts","../src/storage/provisioners/r2-bucket.provisioner.ts","../src/storage/storage.factory.ts","../src/storage/storage-provisioner.factory.ts"],"sourcesContent":["import { Logger } from '@nestjs/common';\n\nconst CF_GRAPHQL = 'https://api.cloudflare.com/client/v4/graphql';\n\nexport interface BucketUsage {\n bytes: number;\n objectCount: number;\n}\n\nexport interface BucketUsageReaderConfig {\n accountId: string;\n // Account-level token with Analytics Read. NOT the org's scoped S3 credential and NOT the R2 admin token: the\n // analytics dataset is filtered by accountTag and is not reachable with object-level credentials.\n analyticsToken: string;\n}\n\n// r2StorageAdaptiveGroups is a time series, so the newest bucketed sample is the closest thing to \"current\". It lags\n// real writes by minutes — fine for a periodic quota check, useless for gating an individual upload.\n//\n// `dimensions { datetime }` is required, not decorative: ordering by a field that is neither aggregated nor selected\n// as a dimension is rejected. Keep the query free of `#` comments too — Cloudflare's parser rejects them.\nconst QUERY = `query BucketUsage($accountTag: string!, $bucketName: string, $start: Time, $end: Time) {\n viewer {\n accounts(filter: { accountTag: $accountTag }) {\n r2StorageAdaptiveGroups(\n limit: 1\n filter: { datetime_geq: $start, datetime_leq: $end, bucketName: $bucketName }\n orderBy: [datetime_DESC]\n ) {\n max { objectCount payloadSize metadataSize }\n dimensions { datetime }\n }\n }\n }\n}`;\n\ninterface UsageResponse {\n errors?: { message: string }[];\n data?: {\n viewer?: {\n accounts?: {\n r2StorageAdaptiveGroups?: {\n max?: { objectCount?: number; payloadSize?: number; metadataSize?: number };\n }[];\n }[];\n };\n };\n}\n\n// Reads how much an org's bucket actually holds, straight from the provider — the authoritative figure a locally\n// maintained counter would only ever approximate.\nexport class BucketUsageReader {\n private readonly logger = new Logger(BucketUsageReader.name);\n\n constructor(private readonly config: BucketUsageReaderConfig) {}\n\n // Returns the newest reported sample, or zeroes for a bucket the dataset has not reported on yet (a new or\n // empty bucket produces no rows at all rather than a row of zeroes)\n async getBucketUsage(bucketName: string, windowHours = 24): Promise<BucketUsage> {\n const end = new Date();\n const start = new Date(end.getTime() - windowHours * 60 * 60 * 1000);\n\n const response = await fetch(CF_GRAPHQL, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.analyticsToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n query: QUERY,\n variables: {\n accountTag: this.config.accountId,\n bucketName,\n start: start.toISOString(),\n end: end.toISOString(),\n },\n }),\n });\n\n const body = (await response.json()) as UsageResponse;\n if (body.errors?.length) {\n throw new Error(\n `Cloudflare usage query failed for ${bucketName}: ${body.errors.map((e) => e.message).join('; ')}`,\n );\n }\n\n const sample = body.data?.viewer?.accounts?.[0]?.r2StorageAdaptiveGroups?.[0]?.max;\n if (!sample) {\n this.logger.debug(`No usage samples yet for bucket ${bucketName}`);\n return { bytes: 0, objectCount: 0 };\n }\n\n // Billed storage is payload plus per-object metadata, so both count against a quota\n return {\n bytes: (sample.payloadSize ?? 0) + (sample.metadataSize ?? 0),\n objectCount: sample.objectCount ?? 0,\n };\n }\n}\n","// Config may be a value or a thunk. Servers pass a thunk when the credentials come from required-only-if-selected\n// env keys, so reading them is deferred to the first resolve() instead of running at module construction.\nexport type StorageConfigSource<T> = T | (() => T);\n\n// A backend configured nowhere is a deployment asking for something it was never given credentials for\nexport function readConfigSource<T>(source: StorageConfigSource<T> | undefined, provider: string): T {\n if (source === undefined) {\n throw new Error(`Storage provider '${provider}' is not configured.`);\n }\n return typeof source === 'function' ? (source as () => T)() : source;\n}\n","import type { Readable } from 'node:stream';\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n S3Client,\n} from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { Logger } from '@nestjs/common';\nimport { NotFoundException } from '../../exceptions';\nimport type { ListObjectsPage, R2StorageConfig, StorageProvider, UploadParams } from '../types';\n\nexport class R2StorageProvider implements StorageProvider {\n private readonly logger = new Logger(R2StorageProvider.name);\n private readonly client: S3Client;\n\n constructor(private readonly config: R2StorageConfig) {\n this.client = new S3Client({\n region: 'auto',\n endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey },\n });\n }\n\n // Uploads a file buffer or stream\n async upload(params: UploadParams): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(params.bucket, this.config.defaultBucket),\n Key: params.key,\n Body: params.body,\n ContentType: params.contentType,\n }),\n );\n\n this.logger.log(`Uploaded file to R2: ${params.key}`);\n return params.key;\n }\n\n // Uploads a file to a public bucket and returns its permanent URL\n async uploadPublic(key: string, body: Buffer, contentType: string, bucket?: string): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.requireBucket(bucket, this.config.publicBucket),\n Key: key,\n Body: body,\n ContentType: contentType,\n }),\n );\n\n const url = this.getPublicUrl(key);\n this.logger.log(`Uploaded public file to R2: ${key} → ${url}`);\n return url;\n }\n\n // Public URLs come from the configured custom domain, which maps to one bucket — R2 has no per-bucket URL to derive,\n // so a multi-tenant caller with a bucket per org cannot build one at all\n getPublicUrl(key: string): string {\n if (!this.config.publicUrl) {\n throw new Error('R2 storage has no publicUrl configured; public URLs cannot be built.');\n }\n return `${this.config.publicUrl}/${key}`;\n }\n\n // Deletes a file\n async delete(key: string, bucket?: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({ Bucket: bucket ?? this.config.defaultBucket, Key: key }));\n\n this.logger.log(`Deleted file from R2: ${key}`);\n }\n\n // Generates a presigned download URL (default 1 hour)\n async getSignedUrl(key: string, expiresInSeconds = 3600, bucket?: string): Promise<string> {\n const command = new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key });\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n }\n\n // Returns a readable stream\n async getStream(key: string, bucket?: string): Promise<Readable> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.requireBucket(bucket, this.config.defaultBucket), Key: key }),\n );\n\n if (!response.Body) {\n throw new NotFoundException('File not found in storage.');\n }\n\n return response.Body as Readable;\n }\n\n // One page of a bucket's contents. S3 has no \"list everything\" call and no bucket-size call — a full inventory is\n // this looped until nextToken is absent, and each page is a Class A request.\n async listObjects(bucket: string, continuationToken?: string): Promise<ListObjectsPage> {\n const response = await this.client.send(\n new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: continuationToken }),\n );\n\n return {\n objects: (response.Contents ?? []).map((o) => ({\n key: o.Key ?? '',\n size: o.Size ?? 0,\n lastModified: o.LastModified ?? new Date(0),\n })),\n nextToken: response.NextContinuationToken,\n };\n }\n\n // Failing loudly here beats letting a missing org bucket fall through to some other tenant's default\n private requireBucket(bucket: string | undefined, fallback: string | undefined): string {\n const resolved = bucket ?? fallback;\n if (!resolved) {\n throw new Error('No bucket supplied and R2 storage has no default bucket configured.');\n }\n return resolved;\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadGatewayException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Gateway', HttpStatus.BAD_GATEWAY);\n }\n}\n","import { HttpException, HttpStatus } from '@nestjs/common';\nimport type { FieldError } from '../types/error-response.types';\n\n// Re-export FieldError for backwards compatibility\nexport type { FieldError } from '../types/error-response.types';\n\nexport interface ProblemOptions {\n type?: string;\n label?: string;\n detail?: string;\n errors?: FieldError[];\n}\n\nexport abstract class HttpProblemException extends HttpException {\n constructor(detailOrOptions: string | ProblemOptions, httpStatus: HttpStatus) {\n const options = typeof detailOrOptions === 'string' ? { detail: detailOrOptions } : detailOrOptions;\n\n super(\n {\n type: options.type ?? 'about:blank',\n label: options.label,\n detail: options.detail,\n errors: options.errors ?? [],\n },\n httpStatus,\n );\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class BadRequestException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Bad Request', HttpStatus.BAD_REQUEST);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ConflictException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Conflict', HttpStatus.CONFLICT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ForbiddenException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Forbidden', HttpStatus.FORBIDDEN);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class GoneException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Gone', HttpStatus.GONE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class InternalServerErrorException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class MethodNotAllowedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Method Not Allowed', HttpStatus.METHOD_NOT_ALLOWED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotAcceptableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Acceptable', HttpStatus.NOT_ACCEPTABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotFoundException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Found', HttpStatus.NOT_FOUND);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class NotImplementedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Not Implemented', HttpStatus.NOT_IMPLEMENTED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class PayloadTooLargeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Payload Too Large', HttpStatus.PAYLOAD_TOO_LARGE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class RequestTimeoutException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Request Timeout', HttpStatus.REQUEST_TIMEOUT);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ServiceUnavailableException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Service Unavailable', HttpStatus.SERVICE_UNAVAILABLE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class TooManyRequestsException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnauthorizedException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unauthorized', HttpStatus.UNAUTHORIZED);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnprocessableEntityException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unprocessable Entity', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class UnsupportedMediaTypeException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Unsupported Media Type', HttpStatus.UNSUPPORTED_MEDIA_TYPE);\n }\n}\n","import { HttpStatus } from '@nestjs/common';\nimport { HttpProblemException, type ProblemOptions } from './base-field.exception';\n\nexport class ValidationException extends HttpProblemException {\n constructor(detailOrOptions?: string | ProblemOptions) {\n super(detailOrOptions ?? 'Validation Failed', HttpStatus.UNPROCESSABLE_ENTITY);\n }\n}\n","import { Logger } from '@nestjs/common';\nimport type { R2ProvisionerConfig, ScopedTokenResult, StorageProvisioner } from '../types';\n\nconst CF_API = 'https://api.cloudflare.com/client/v4';\n\n// Bucket-level group: read/write/list objects in named buckets. Deliberately NOT 'Workers R2 Storage Write', which is\n// account-level and can create and delete buckets — exactly what a tenant credential must never do.\nconst BUCKET_ITEM_WRITE = 'Workers R2 Storage Bucket Item Write';\n\ninterface CloudflareEnvelope<T> {\n success: boolean;\n errors: { code: number; message: string }[];\n result: T;\n}\n\n// Generic R2 admin client: create/delete buckets, toggle a bucket's Cloudflare-managed public domain, and mint/revoke\n// bucket-scoped credentials. It has NO notion of organizations — bucket naming, which buckets a tenant gets, and how\n// the stored descriptor is assembled all live in the caller. Every operation is a Cloudflare REST call.\nexport class R2BucketProvisioner implements StorageProvisioner {\n private readonly logger = new Logger(R2BucketProvisioner.name);\n private readonly jurisdiction: string;\n private bucketItemWriteGroupId: string | null = null;\n\n constructor(private readonly config: R2ProvisionerConfig) {\n this.jurisdiction = config.jurisdiction ?? 'default';\n }\n\n // Creates one bucket, treating an existing bucket as success so provisioning can be re-run to reconcile\n async createBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.adminToken}`,\n 'Content-Type': 'application/json',\n 'cf-r2-jurisdiction': this.jurisdiction,\n },\n body: JSON.stringify({\n name,\n ...(this.config.locationHint && { locationHint: this.config.locationHint }),\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (body.success) {\n this.logger.log(`Created bucket ${name}`);\n return;\n }\n\n // 10004 is \"bucket already exists\" — two provisioning attempts racing, or a reconcile pass\n if (body.errors?.some((e) => e.code === 10004)) return;\n throw new Error(`Cloudflare bucket create failed for ${name}: ${this.describe(body)}`);\n }\n\n // Deletes one bucket. R2 refuses to delete a bucket that still holds objects, so the caller must have emptied it\n // first — a 'not empty' failure here means that step did not finish. A missing bucket counts as success.\n async deleteBucket(name: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${name}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.adminToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n // 10006 is \"bucket not found\" — already gone, which is the state we wanted\n if (body.success || body.errors?.some((e) => e.code === 10006)) {\n this.logger.log(`Deleted bucket ${name}`);\n return;\n }\n throw new Error(`Cloudflare bucket delete failed for ${name}: ${this.describe(body)}`);\n }\n\n // Turns on the bucket's Cloudflare-managed domain and returns its https URL. NOTE: r2.dev is rate limited and\n // documented as non-production — sustained traffic gets 429s. A custom domain per bucket is the production answer,\n // and drops into this same field. Throws on failure; the caller decides whether that is fatal.\n async enablePublicAccess(bucket: string): Promise<string> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${bucket}/domains/managed`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${this.config.adminToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ enabled: true }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ domain?: string; enabled?: boolean }>;\n if (!body.success || !body.result?.domain) {\n throw new Error(`Cloudflare enable public access failed for ${bucket}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Enabled public access for ${bucket} → ${body.result.domain}`);\n return `https://${body.result.domain}`;\n }\n\n // Mints an ACCOUNT-owned token that can only touch the named buckets. Account-owned rather than user-owned so a\n // tenant's credential does not die with whichever Cloudflare user happened to create the parent token. Requires the\n // parent to hold `Account API Tokens Write`; a user-scoped `API Tokens: Edit` token is rejected here with 9109.\n // `name` is the token's display label; the value is returned by R2 exactly once.\n async createScopedToken(name: string, buckets: string[]): Promise<ScopedTokenResult> {\n const permissionGroupId = await this.resolveBucketItemWriteGroupId();\n\n // The jurisdiction is embedded in the resource key and must match the bucket's, or the token authenticates fine\n // and then 403s on every object because it is scoped to a bucket that does not exist\n const resources = Object.fromEntries(\n buckets.map((bucket) => [\n `com.cloudflare.edge.r2.bucket.${this.config.accountId}_${this.jurisdiction}_${bucket}`,\n '*',\n ]),\n );\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${this.config.tokensToken}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n name,\n policies: [{ effect: 'allow', permission_groups: [{ id: permissionGroupId }], resources }],\n }),\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; value: string }>;\n if (!body.success || !body.result?.value) {\n throw new Error(`Cloudflare token create failed for ${name}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Minted storage credential ${name} scoped to ${buckets.length} bucket(s)`);\n return body.result;\n }\n\n // Revokes a credential by its access key id, which on R2 is the Cloudflare token id\n async deleteCredential(accessKeyId: string): Promise<void> {\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/${accessKeyId}`, {\n method: 'DELETE',\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<unknown>;\n if (!body.success) {\n throw new Error(`Cloudflare token delete failed for ${accessKeyId}: ${this.describe(body)}`);\n }\n\n this.logger.log(`Revoked storage credential ${accessKeyId}`);\n }\n\n // The create-token API takes permission group UUIDs, not names. Looked up once rather than hardcoded: a UUID copied\n // from docs fails much later with an error that says nothing useful. Same account-scoped endpoint as creation.\n private async resolveBucketItemWriteGroupId(): Promise<string> {\n if (this.bucketItemWriteGroupId) return this.bucketItemWriteGroupId;\n\n const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/permission_groups`, {\n headers: { Authorization: `Bearer ${this.config.tokensToken}` },\n });\n\n const body = (await response.json()) as CloudflareEnvelope<{ id: string; name: string }[]>;\n const group = body.result?.find((g) => g.name === BUCKET_ITEM_WRITE);\n if (!group) {\n throw new Error(`Cloudflare permission group '${BUCKET_ITEM_WRITE}' not found: ${this.describe(body)}`);\n }\n\n this.bucketItemWriteGroupId = group.id;\n return group.id;\n }\n\n private describe(body: CloudflareEnvelope<unknown>): string {\n return body.errors?.map((e) => `${e.code} ${e.message}`).join('; ') || 'unknown error';\n }\n}\n","import { readConfigSource, type StorageConfigSource } from './config-source';\nimport { R2StorageProvider } from './providers/r2-storage.provider';\nimport type { R2StorageConfig, StorageProvider } from './types';\n\nexport interface StorageFactoryOptions {\n r2?: StorageConfigSource<R2StorageConfig>;\n}\n\nexport class StorageFactory {\n private readonly cache = new Map<string, StorageProvider>();\n\n constructor(private readonly options: StorageFactoryOptions) {}\n\n // Resolves a provider by name, constructing it once on first use\n resolve(provider: string): StorageProvider {\n const cached = this.cache.get(provider);\n if (cached) return cached;\n\n const created = this.create(provider);\n this.cache.set(provider, created);\n return created;\n }\n\n private create(provider: string): StorageProvider {\n switch (provider) {\n case 'r2':\n return new R2StorageProvider(readConfigSource(this.options.r2, 'r2'));\n default:\n throw new Error(`Unsupported storage provider: ${provider}`);\n }\n }\n}\n","import { readConfigSource, type StorageConfigSource } from './config-source';\nimport { R2BucketProvisioner } from './provisioners/r2-bucket.provisioner';\nimport type { R2ProvisionerConfig, StorageProvisioner } from './types';\n\nexport interface StorageProvisionerFactoryOptions {\n r2?: StorageConfigSource<R2ProvisionerConfig>;\n}\n\n// Mirrors StorageFactory for the admin side: same provider names, same lazy config, different job. Resolves a generic\n// StorageProvisioner (bucket + credential admin) by provider name so the caller is not pinned to a single backend.\nexport class StorageProvisionerFactory {\n private readonly cache = new Map<string, StorageProvisioner>();\n\n constructor(private readonly options: StorageProvisionerFactoryOptions) {}\n\n // Resolves a provisioner by provider name, constructing it once on first use\n resolve(provider: string): StorageProvisioner {\n const cached = this.cache.get(provider);\n if (cached) return cached;\n\n const created = this.create(provider);\n this.cache.set(provider, created);\n return created;\n }\n\n private create(provider: string): StorageProvisioner {\n switch (provider) {\n case 'r2':\n return new R2BucketProvisioner(readConfigSource(this.options.r2, 'r2'));\n default:\n throw new Error(`Unsupported storage provisioner: ${provider}`);\n }\n }\n}\n"],"mappings":"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","names":["Logger","CF_GRAPHQL","QUERY","BucketUsageReader","logger","Logger","name","config","getBucketUsage","bucketName","windowHours","end","Date","start","getTime","response","fetch","method","headers","Authorization","analyticsToken","body","JSON","stringify","query","variables","accountTag","accountId","toISOString","json","errors","length","Error","map","e","message","join","sample","data","viewer","accounts","r2StorageAdaptiveGroups","max","debug","bytes","objectCount","payloadSize","metadataSize","readConfigSource","source","provider","undefined","Error","DeleteObjectCommand","GetObjectCommand","ListObjectsV2Command","PutObjectCommand","S3Client","getSignedUrl","Logger","HttpStatus","HttpException","HttpProblemException","HttpException","detailOrOptions","httpStatus","options","detail","type","label","errors","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","NotFoundException","HttpProblemException","detailOrOptions","HttpStatus","NOT_FOUND","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","HttpStatus","R2StorageProvider","logger","Logger","name","client","config","S3Client","region","endpoint","accountId","credentials","accessKeyId","secretAccessKey","upload","params","send","PutObjectCommand","Bucket","requireBucket","bucket","defaultBucket","Key","key","Body","body","ContentType","contentType","log","uploadPublic","publicBucket","url","getPublicUrl","publicUrl","Error","delete","DeleteObjectCommand","getSignedUrl","expiresInSeconds","command","GetObjectCommand","expiresIn","getStream","response","NotFoundException","listObjects","continuationToken","ListObjectsV2Command","ContinuationToken","objects","Contents","map","o","size","Size","lastModified","LastModified","Date","nextToken","NextContinuationToken","fallback","resolved","Logger","CF_API","BUCKET_ITEM_WRITE","R2BucketProvisioner","logger","Logger","name","jurisdiction","bucketItemWriteGroupId","config","createBucket","response","fetch","accountId","method","headers","Authorization","adminToken","body","JSON","stringify","locationHint","json","success","log","errors","some","e","code","Error","describe","deleteBucket","enablePublicAccess","bucket","enabled","result","domain","createScopedToken","buckets","permissionGroupId","resolveBucketItemWriteGroupId","resources","Object","fromEntries","map","tokensToken","policies","effect","permission_groups","id","value","length","deleteCredential","accessKeyId","group","find","g","message","join","StorageFactory","cache","Map","options","resolve","provider","cached","get","created","create","set","R2StorageProvider","readConfigSource","r2","Error","StorageProvisionerFactory","cache","Map","options","resolve","provider","cached","get","created","create","set","R2BucketProvisioner","readConfigSource","r2","Error"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vritti/api-sdk",
3
3
  "type": "module",
4
- "version": "0.3.9",
4
+ "version": "0.3.10",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",