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 +55 -2
- package/dist/{chunk-RPORXMST.js → chunk-RC2KZLI2.js} +71 -3
- package/dist/{chunk-RPORXMST.js.map → chunk-RC2KZLI2.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/registry/catalog.json +53 -47
- package/registry/categories.json +10 -7
- package/registry/registry.json +121 -135
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
|
-
|
|
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 (
|
|
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
|
|
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-
|
|
1733
|
+
//# sourceMappingURL=chunk-RC2KZLI2.js.map
|