getaiapi 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -262,7 +262,10 @@ Resolves a model by name. Accepts canonical names, aliases, and normalized varia
262
262
 
263
263
  ## R2 Storage (Asset Uploads)
264
264
 
265
- Some providers can't fetch from private or presigned URLs. getaiapi includes built-in Cloudflare R2 storage support that automatically uploads binary assets to a public bucket before sending them to providers.
265
+ getaiapi includes built-in Cloudflare R2 storage support that automatically uploads binary assets before sending them to providers. Two modes are supported:
266
+
267
+ - **`public`** (default) — requires a publicly readable bucket; returns public URLs (via `publicUrlBase` or the R2 endpoint)
268
+ - **`presigned`** — works with private buckets; returns time-limited presigned GET URLs signed with S3 Signature V4 (no public access needed, `publicUrlBase` is not required)
266
269
 
267
270
  ### Setup
268
271
 
@@ -275,10 +278,26 @@ export R2_BUCKET_NAME="your-bucket-name"
275
278
  export R2_ACCESS_KEY_ID="your-r2-access-key"
276
279
  export R2_SECRET_ACCESS_KEY="your-r2-secret-key"
277
280
 
278
- # Optional - custom public URL (e.g. CDN domain mapped to your bucket)
281
+ # Optional - custom public URL (only needed for mode: 'public')
279
282
  export R2_PUBLIC_URL="https://cdn.example.com"
283
+
284
+ # Optional - use presigned URLs for private buckets (default: 'public')
285
+ export R2_STORAGE_MODE="presigned"
286
+ export R2_PRESIGN_EXPIRES_IN="3600" # seconds, default: 3600, max: 604800 (7 days)
280
287
  ```
281
288
 
289
+ #### How to get your R2 Public URL (public mode only)
290
+
291
+ If using `mode: 'presigned'`, you can skip this — no public bucket access is needed.
292
+
293
+ 1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com)
294
+ 2. Go to **R2 Object Storage** in the left sidebar
295
+ 3. Click on your bucket
296
+ 4. Go to the **Settings** tab
297
+ 5. Under **Public access**, click **Allow Access**
298
+ 6. Cloudflare will provide a public URL like `https://<bucket>.<account-id>.r2.dev` — use this as your `R2_PUBLIC_URL`
299
+ 7. (Optional) You can also connect a **Custom Domain** under the same section for a cleaner URL like `https://cdn.yourdomain.com`
300
+
282
301
  Then call `configureStorage()` once at startup:
283
302
 
284
303
  ```typescript
@@ -295,6 +314,8 @@ configureStorage({
295
314
  secretAccessKey: 'your-secret',
296
315
  publicUrlBase: 'https://cdn.example.com', // optional
297
316
  autoUpload: false, // optional
317
+ mode: 'public', // 'public' | 'presigned' (default: 'public')
318
+ presignExpiresIn: 3600, // presigned URL TTL in seconds (default: 3600)
298
319
  })
299
320
  ```
300
321
 
@@ -352,6 +373,38 @@ console.log(url) // https://cdn.example.com/uploads/a1b2c3d4-...
352
373
  await deleteAsset(key)
353
374
  ```
354
375
 
376
+ ### Presigned URLs (Private Buckets)
377
+
378
+ If your R2 bucket doesn't have public read access, use presigned mode. Instead of returning a public URL, `uploadAsset` will return a time-limited presigned GET URL signed with S3 Signature V4.
379
+
380
+ ```typescript
381
+ configureStorage({
382
+ accountId: 'your-account-id',
383
+ bucketName: 'private-bucket',
384
+ accessKeyId: 'your-key',
385
+ secretAccessKey: 'your-secret',
386
+ mode: 'presigned', // uploadAsset returns presigned URLs
387
+ presignExpiresIn: 1800, // URLs expire after 30 minutes
388
+ })
389
+
390
+ const { url } = await uploadAsset(Buffer.from('secret data'), {
391
+ contentType: 'application/octet-stream',
392
+ })
393
+ // url is a presigned GET URL, valid for 30 minutes
394
+ ```
395
+
396
+ You can also generate presigned URLs for existing objects:
397
+
398
+ ```typescript
399
+ import { presignAsset } from 'getaiapi'
400
+
401
+ const url = presignAsset('uploads/my-file.png')
402
+ // => https://<account>.r2.cloudflarestorage.com/<bucket>/uploads/my-file.png?X-Amz-Algorithm=...
403
+
404
+ // Custom expiry per-call (overrides config default)
405
+ const shortUrl = presignAsset('uploads/my-file.png', { expiresIn: 300 }) // 5 minutes
406
+ ```
407
+
355
408
  **UploadOptions**
356
409
 
357
410
  | Option | Type | Description |
@@ -1355,9 +1355,56 @@ function signS3Request(method, url, headers, body, credentials) {
1355
1355
  }
1356
1356
  };
1357
1357
  }
1358
+ function presignS3Url(url, credentials, options) {
1359
+ const region = credentials.region ?? "auto";
1360
+ const service = "s3";
1361
+ const parsedUrl = new URL(url);
1362
+ const now = /* @__PURE__ */ new Date();
1363
+ const { amzDate, dateStamp } = toAmzDate(now);
1364
+ const expiresIn = options?.expiresIn ?? 3600;
1365
+ const host = parsedUrl.host;
1366
+ const signedHeaders = "host";
1367
+ const scope = `${dateStamp}/${region}/${service}/aws4_request`;
1368
+ const credential = `${credentials.accessKeyId}/${scope}`;
1369
+ const queryParams = new URLSearchParams({
1370
+ "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
1371
+ "X-Amz-Credential": credential,
1372
+ "X-Amz-Date": amzDate,
1373
+ "X-Amz-Expires": String(expiresIn),
1374
+ "X-Amz-SignedHeaders": signedHeaders
1375
+ });
1376
+ const canonicalRequest = [
1377
+ "GET",
1378
+ parsedUrl.pathname,
1379
+ queryParams.toString(),
1380
+ `host:${host}
1381
+ `,
1382
+ signedHeaders,
1383
+ "UNSIGNED-PAYLOAD"
1384
+ ].join("\n");
1385
+ const stringToSign = [
1386
+ "AWS4-HMAC-SHA256",
1387
+ amzDate,
1388
+ scope,
1389
+ sha256(canonicalRequest)
1390
+ ].join("\n");
1391
+ const signingKey = getSigningKey(credentials.secretAccessKey, dateStamp, region, service);
1392
+ const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
1393
+ queryParams.set("X-Amz-Signature", signature);
1394
+ return `${parsedUrl.origin}${parsedUrl.pathname}?${queryParams.toString()}`;
1395
+ }
1358
1396
 
1359
1397
  // src/storage.ts
1398
+ var MAX_PRESIGN_EXPIRES = 604800;
1360
1399
  var storageConfig = null;
1400
+ function parseExpiresIn(value) {
1401
+ if (!value) return void 0;
1402
+ const parsed = parseInt(value, 10);
1403
+ if (Number.isNaN(parsed) || parsed <= 0) {
1404
+ throw new StorageError("config", `Invalid R2_PRESIGN_EXPIRES_IN: "${value}". Must be a positive integer.`);
1405
+ }
1406
+ return parsed;
1407
+ }
1361
1408
  function configureStorage(config) {
1362
1409
  if (config) {
1363
1410
  storageConfig = config;
@@ -1379,7 +1426,9 @@ function configureStorage(config) {
1379
1426
  accessKeyId,
1380
1427
  secretAccessKey,
1381
1428
  publicUrlBase: process.env.R2_PUBLIC_URL,
1382
- autoUpload: false
1429
+ autoUpload: false,
1430
+ mode: process.env.R2_STORAGE_MODE === "presigned" ? "presigned" : void 0,
1431
+ presignExpiresIn: parseExpiresIn(process.env.R2_PRESIGN_EXPIRES_IN)
1383
1432
  };
1384
1433
  }
1385
1434
  function getStorageConfig() {
@@ -1445,13 +1494,31 @@ async function uploadAsset(input, options) {
1445
1494
  const body = await response.text().catch(() => "");
1446
1495
  throw new StorageError("upload", `R2 returned ${response.status}: ${body}`, response.status);
1447
1496
  }
1497
+ const url = config.mode === "presigned" ? validatedPresign(config, key) : buildPublicUrl(config, key);
1448
1498
  return {
1449
- url: buildPublicUrl(config, key),
1499
+ url,
1450
1500
  key,
1451
1501
  size_bytes: buffer.length,
1452
1502
  content_type: contentType
1453
1503
  };
1454
1504
  }
1505
+ function validatedPresign(config, key, expiresIn) {
1506
+ const ttl = expiresIn ?? config.presignExpiresIn ?? 3600;
1507
+ if (ttl > MAX_PRESIGN_EXPIRES) {
1508
+ throw new StorageError(
1509
+ "config",
1510
+ `Presign expiry ${ttl}s exceeds maximum of ${MAX_PRESIGN_EXPIRES}s (7 days).`
1511
+ );
1512
+ }
1513
+ return presignS3Url(buildR2Url(config, key), {
1514
+ accessKeyId: config.accessKeyId,
1515
+ secretAccessKey: config.secretAccessKey
1516
+ }, { expiresIn: ttl });
1517
+ }
1518
+ function presignAsset(key, options) {
1519
+ const config = getConfig();
1520
+ return validatedPresign(config, key, options?.expiresIn);
1521
+ }
1455
1522
  async function deleteAsset(key) {
1456
1523
  const config = getConfig();
1457
1524
  const r2Url = buildR2Url(config, key);
@@ -1656,10 +1723,11 @@ export {
1656
1723
  configureAuth,
1657
1724
  configureStorage,
1658
1725
  uploadAsset,
1726
+ presignAsset,
1659
1727
  deleteAsset,
1660
1728
  generate,
1661
1729
  configure,
1662
1730
  listModels,
1663
1731
  getModel
1664
1732
  };
1665
- //# sourceMappingURL=chunk-RPORXMST.js.map
1733
+ //# sourceMappingURL=chunk-RC2KZLI2.js.map