@vritti/api-sdk 0.3.6 → 0.3.8
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/catalog-resolver.cjs.map +1 -1
- package/dist/catalog-resolver.js.map +1 -1
- package/dist/decimal.d.cts +1 -0
- package/dist/decimal.d.ts +1 -0
- package/dist/pluralize.cjs +41 -0
- package/dist/pluralize.cjs.map +1 -0
- package/dist/pluralize.d.cts +1 -0
- package/dist/pluralize.d.ts +1 -0
- package/dist/pluralize.js +6 -0
- package/dist/pluralize.js.map +1 -0
- package/dist/storage.cjs +571 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +145 -0
- package/dist/storage.d.ts +145 -0
- package/dist/storage.js +540 -0
- package/dist/storage.js.map +1 -0
- package/package.json +36 -5
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/pluralize.ts
|
|
31
|
+
var pluralize_exports = {};
|
|
32
|
+
__export(pluralize_exports, {
|
|
33
|
+
pluralize: () => import_pluralize_esm.default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(pluralize_exports);
|
|
36
|
+
var import_pluralize_esm = __toESM(require("pluralize-esm"), 1);
|
|
37
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
38
|
+
0 && (module.exports = {
|
|
39
|
+
pluralize
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=pluralize.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pluralize.ts"],"sourcesContent":["// Re-export pluralize-esm's default export as the subpath entry point\nexport { default as pluralize } from 'pluralize-esm';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;AACA,2BAAqC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as pluralize } from 'pluralize-esm';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as pluralize } from 'pluralize-esm';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pluralize.ts"],"sourcesContent":["// Re-export pluralize-esm's default export as the subpath entry point\nexport { default as pluralize } from 'pluralize-esm';\n"],"mappings":";AACA,SAAoBA,WAAXC,gBAA4B;","names":["pluralize","default"]}
|
package/dist/storage.cjs
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/storage/index.ts
|
|
22
|
+
var storage_exports = {};
|
|
23
|
+
__export(storage_exports, {
|
|
24
|
+
BucketUsageReader: () => BucketUsageReader,
|
|
25
|
+
OrgStorageProvisionerFactory: () => OrgStorageProvisionerFactory,
|
|
26
|
+
R2BucketProvisioner: () => R2BucketProvisioner,
|
|
27
|
+
R2StorageProvider: () => R2StorageProvider,
|
|
28
|
+
StorageFactory: () => StorageFactory,
|
|
29
|
+
orgBucketNames: () => orgBucketNames,
|
|
30
|
+
readConfigSource: () => readConfigSource
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(storage_exports);
|
|
33
|
+
|
|
34
|
+
// src/storage/bucket-usage.reader.ts
|
|
35
|
+
var import_common = require("@nestjs/common");
|
|
36
|
+
var CF_GRAPHQL = "https://api.cloudflare.com/client/v4/graphql";
|
|
37
|
+
var QUERY = `query BucketUsage($accountTag: string!, $bucketName: string, $start: Time, $end: Time) {
|
|
38
|
+
viewer {
|
|
39
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
40
|
+
r2StorageAdaptiveGroups(
|
|
41
|
+
limit: 1
|
|
42
|
+
filter: { datetime_geq: $start, datetime_leq: $end, bucketName: $bucketName }
|
|
43
|
+
orderBy: [datetime_DESC]
|
|
44
|
+
) {
|
|
45
|
+
max { objectCount payloadSize metadataSize }
|
|
46
|
+
dimensions { datetime }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}`;
|
|
51
|
+
var BucketUsageReader = class _BucketUsageReader {
|
|
52
|
+
static {
|
|
53
|
+
__name(this, "BucketUsageReader");
|
|
54
|
+
}
|
|
55
|
+
config;
|
|
56
|
+
logger = new import_common.Logger(_BucketUsageReader.name);
|
|
57
|
+
constructor(config) {
|
|
58
|
+
this.config = config;
|
|
59
|
+
}
|
|
60
|
+
// Returns the newest reported sample, or zeroes for a bucket the dataset has not reported on yet (a new or
|
|
61
|
+
// empty bucket produces no rows at all rather than a row of zeroes)
|
|
62
|
+
async getBucketUsage(bucketName, windowHours = 24) {
|
|
63
|
+
const end = /* @__PURE__ */ new Date();
|
|
64
|
+
const start = new Date(end.getTime() - windowHours * 60 * 60 * 1e3);
|
|
65
|
+
const response = await fetch(CF_GRAPHQL, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${this.config.analyticsToken}`,
|
|
69
|
+
"Content-Type": "application/json"
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
query: QUERY,
|
|
73
|
+
variables: {
|
|
74
|
+
accountTag: this.config.accountId,
|
|
75
|
+
bucketName,
|
|
76
|
+
start: start.toISOString(),
|
|
77
|
+
end: end.toISOString()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
const body = await response.json();
|
|
82
|
+
if (body.errors?.length) {
|
|
83
|
+
throw new Error(`Cloudflare usage query failed for ${bucketName}: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
84
|
+
}
|
|
85
|
+
const sample = body.data?.viewer?.accounts?.[0]?.r2StorageAdaptiveGroups?.[0]?.max;
|
|
86
|
+
if (!sample) {
|
|
87
|
+
this.logger.debug(`No usage samples yet for bucket ${bucketName}`);
|
|
88
|
+
return {
|
|
89
|
+
bytes: 0,
|
|
90
|
+
objectCount: 0
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
bytes: (sample.payloadSize ?? 0) + (sample.metadataSize ?? 0),
|
|
95
|
+
objectCount: sample.objectCount ?? 0
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/storage/config-source.ts
|
|
101
|
+
function readConfigSource(source, provider) {
|
|
102
|
+
if (source === void 0) {
|
|
103
|
+
throw new Error(`Storage provider '${provider}' is not configured.`);
|
|
104
|
+
}
|
|
105
|
+
return typeof source === "function" ? source() : source;
|
|
106
|
+
}
|
|
107
|
+
__name(readConfigSource, "readConfigSource");
|
|
108
|
+
|
|
109
|
+
// src/storage/providers/r2-storage.provider.ts
|
|
110
|
+
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
111
|
+
var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
112
|
+
var import_common21 = require("@nestjs/common");
|
|
113
|
+
|
|
114
|
+
// src/exceptions/bad-gateway.exception.ts
|
|
115
|
+
var import_common3 = require("@nestjs/common");
|
|
116
|
+
|
|
117
|
+
// src/exceptions/base-field.exception.ts
|
|
118
|
+
var import_common2 = require("@nestjs/common");
|
|
119
|
+
var HttpProblemException = class extends import_common2.HttpException {
|
|
120
|
+
static {
|
|
121
|
+
__name(this, "HttpProblemException");
|
|
122
|
+
}
|
|
123
|
+
constructor(detailOrOptions, httpStatus) {
|
|
124
|
+
const options = typeof detailOrOptions === "string" ? {
|
|
125
|
+
detail: detailOrOptions
|
|
126
|
+
} : detailOrOptions;
|
|
127
|
+
super({
|
|
128
|
+
type: options.type ?? "about:blank",
|
|
129
|
+
label: options.label,
|
|
130
|
+
detail: options.detail,
|
|
131
|
+
errors: options.errors ?? []
|
|
132
|
+
}, httpStatus);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/exceptions/bad-request.exception.ts
|
|
137
|
+
var import_common4 = require("@nestjs/common");
|
|
138
|
+
|
|
139
|
+
// src/exceptions/conflict.exception.ts
|
|
140
|
+
var import_common5 = require("@nestjs/common");
|
|
141
|
+
|
|
142
|
+
// src/exceptions/forbidden.exception.ts
|
|
143
|
+
var import_common6 = require("@nestjs/common");
|
|
144
|
+
|
|
145
|
+
// src/exceptions/gone.exception.ts
|
|
146
|
+
var import_common7 = require("@nestjs/common");
|
|
147
|
+
|
|
148
|
+
// src/exceptions/internal-server-error.exception.ts
|
|
149
|
+
var import_common8 = require("@nestjs/common");
|
|
150
|
+
|
|
151
|
+
// src/exceptions/method-not-allowed.exception.ts
|
|
152
|
+
var import_common9 = require("@nestjs/common");
|
|
153
|
+
|
|
154
|
+
// src/exceptions/not-acceptable.exception.ts
|
|
155
|
+
var import_common10 = require("@nestjs/common");
|
|
156
|
+
|
|
157
|
+
// src/exceptions/not-found.exception.ts
|
|
158
|
+
var import_common11 = require("@nestjs/common");
|
|
159
|
+
var NotFoundException = class extends HttpProblemException {
|
|
160
|
+
static {
|
|
161
|
+
__name(this, "NotFoundException");
|
|
162
|
+
}
|
|
163
|
+
constructor(detailOrOptions) {
|
|
164
|
+
super(detailOrOptions ?? "Not Found", import_common11.HttpStatus.NOT_FOUND);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/exceptions/not-implemented.exception.ts
|
|
169
|
+
var import_common12 = require("@nestjs/common");
|
|
170
|
+
|
|
171
|
+
// src/exceptions/payload-too-large.exception.ts
|
|
172
|
+
var import_common13 = require("@nestjs/common");
|
|
173
|
+
|
|
174
|
+
// src/exceptions/request-timeout.exception.ts
|
|
175
|
+
var import_common14 = require("@nestjs/common");
|
|
176
|
+
|
|
177
|
+
// src/exceptions/service-unavailable.exception.ts
|
|
178
|
+
var import_common15 = require("@nestjs/common");
|
|
179
|
+
|
|
180
|
+
// src/exceptions/too-many-requests.exception.ts
|
|
181
|
+
var import_common16 = require("@nestjs/common");
|
|
182
|
+
|
|
183
|
+
// src/exceptions/unauthorized.exception.ts
|
|
184
|
+
var import_common17 = require("@nestjs/common");
|
|
185
|
+
|
|
186
|
+
// src/exceptions/unprocessable-entity.exception.ts
|
|
187
|
+
var import_common18 = require("@nestjs/common");
|
|
188
|
+
|
|
189
|
+
// src/exceptions/unsupported-media-type.exception.ts
|
|
190
|
+
var import_common19 = require("@nestjs/common");
|
|
191
|
+
|
|
192
|
+
// src/exceptions/validation.exception.ts
|
|
193
|
+
var import_common20 = require("@nestjs/common");
|
|
194
|
+
|
|
195
|
+
// src/storage/providers/r2-storage.provider.ts
|
|
196
|
+
var R2StorageProvider = class _R2StorageProvider {
|
|
197
|
+
static {
|
|
198
|
+
__name(this, "R2StorageProvider");
|
|
199
|
+
}
|
|
200
|
+
config;
|
|
201
|
+
logger = new import_common21.Logger(_R2StorageProvider.name);
|
|
202
|
+
client;
|
|
203
|
+
constructor(config) {
|
|
204
|
+
this.config = config;
|
|
205
|
+
this.client = new import_client_s3.S3Client({
|
|
206
|
+
region: "auto",
|
|
207
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
208
|
+
credentials: {
|
|
209
|
+
accessKeyId: config.accessKeyId,
|
|
210
|
+
secretAccessKey: config.secretAccessKey
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Uploads a file buffer or stream
|
|
215
|
+
async upload(params) {
|
|
216
|
+
await this.client.send(new import_client_s3.PutObjectCommand({
|
|
217
|
+
Bucket: this.requireBucket(params.bucket, this.config.defaultBucket),
|
|
218
|
+
Key: params.key,
|
|
219
|
+
Body: params.body,
|
|
220
|
+
ContentType: params.contentType
|
|
221
|
+
}));
|
|
222
|
+
this.logger.log(`Uploaded file to R2: ${params.key}`);
|
|
223
|
+
return params.key;
|
|
224
|
+
}
|
|
225
|
+
// Uploads a file to a public bucket and returns its permanent URL
|
|
226
|
+
async uploadPublic(key, body, contentType, bucket) {
|
|
227
|
+
await this.client.send(new import_client_s3.PutObjectCommand({
|
|
228
|
+
Bucket: this.requireBucket(bucket, this.config.publicBucket),
|
|
229
|
+
Key: key,
|
|
230
|
+
Body: body,
|
|
231
|
+
ContentType: contentType
|
|
232
|
+
}));
|
|
233
|
+
const url = this.getPublicUrl(key);
|
|
234
|
+
this.logger.log(`Uploaded public file to R2: ${key} \u2192 ${url}`);
|
|
235
|
+
return url;
|
|
236
|
+
}
|
|
237
|
+
// Public URLs come from the configured custom domain, which maps to one bucket — R2 has no per-bucket URL to derive,
|
|
238
|
+
// so a multi-tenant caller with a bucket per org cannot build one at all
|
|
239
|
+
getPublicUrl(key) {
|
|
240
|
+
if (!this.config.publicUrl) {
|
|
241
|
+
throw new Error("R2 storage has no publicUrl configured; public URLs cannot be built.");
|
|
242
|
+
}
|
|
243
|
+
return `${this.config.publicUrl}/${key}`;
|
|
244
|
+
}
|
|
245
|
+
// Deletes a file
|
|
246
|
+
async delete(key, bucket) {
|
|
247
|
+
await this.client.send(new import_client_s3.DeleteObjectCommand({
|
|
248
|
+
Bucket: bucket ?? this.config.defaultBucket,
|
|
249
|
+
Key: key
|
|
250
|
+
}));
|
|
251
|
+
this.logger.log(`Deleted file from R2: ${key}`);
|
|
252
|
+
}
|
|
253
|
+
// Generates a presigned download URL (default 1 hour)
|
|
254
|
+
async getSignedUrl(key, expiresInSeconds = 3600, bucket) {
|
|
255
|
+
const command = new import_client_s3.GetObjectCommand({
|
|
256
|
+
Bucket: this.requireBucket(bucket, this.config.defaultBucket),
|
|
257
|
+
Key: key
|
|
258
|
+
});
|
|
259
|
+
return (0, import_s3_request_presigner.getSignedUrl)(this.client, command, {
|
|
260
|
+
expiresIn: expiresInSeconds
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// Returns a readable stream
|
|
264
|
+
async getStream(key, bucket) {
|
|
265
|
+
const response = await this.client.send(new import_client_s3.GetObjectCommand({
|
|
266
|
+
Bucket: this.requireBucket(bucket, this.config.defaultBucket),
|
|
267
|
+
Key: key
|
|
268
|
+
}));
|
|
269
|
+
if (!response.Body) {
|
|
270
|
+
throw new NotFoundException("File not found in storage.");
|
|
271
|
+
}
|
|
272
|
+
return response.Body;
|
|
273
|
+
}
|
|
274
|
+
// One page of a bucket's contents. S3 has no "list everything" call and no bucket-size call — a full inventory is
|
|
275
|
+
// this looped until nextToken is absent, and each page is a Class A request.
|
|
276
|
+
async listObjects(bucket, continuationToken) {
|
|
277
|
+
const response = await this.client.send(new import_client_s3.ListObjectsV2Command({
|
|
278
|
+
Bucket: bucket,
|
|
279
|
+
ContinuationToken: continuationToken
|
|
280
|
+
}));
|
|
281
|
+
return {
|
|
282
|
+
objects: (response.Contents ?? []).map((o) => ({
|
|
283
|
+
key: o.Key ?? "",
|
|
284
|
+
size: o.Size ?? 0,
|
|
285
|
+
lastModified: o.LastModified ?? /* @__PURE__ */ new Date(0)
|
|
286
|
+
})),
|
|
287
|
+
nextToken: response.NextContinuationToken
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// Failing loudly here beats letting a missing org bucket fall through to some other tenant's default
|
|
291
|
+
requireBucket(bucket, fallback) {
|
|
292
|
+
const resolved = bucket ?? fallback;
|
|
293
|
+
if (!resolved) {
|
|
294
|
+
throw new Error("No bucket supplied and R2 storage has no default bucket configured.");
|
|
295
|
+
}
|
|
296
|
+
return resolved;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/storage/provisioners/r2-bucket.provisioner.ts
|
|
301
|
+
var import_node_crypto = require("crypto");
|
|
302
|
+
var import_common22 = require("@nestjs/common");
|
|
303
|
+
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
304
|
+
var BUCKET_ITEM_WRITE = "Workers R2 Storage Bucket Item Write";
|
|
305
|
+
function orgBucketNames(subdomain) {
|
|
306
|
+
return {
|
|
307
|
+
storageBucket: `org-${subdomain}`,
|
|
308
|
+
storagePublicBucket: `org-${subdomain}-public`
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
__name(orgBucketNames, "orgBucketNames");
|
|
312
|
+
var R2BucketProvisioner = class _R2BucketProvisioner {
|
|
313
|
+
static {
|
|
314
|
+
__name(this, "R2BucketProvisioner");
|
|
315
|
+
}
|
|
316
|
+
config;
|
|
317
|
+
logger = new import_common22.Logger(_R2BucketProvisioner.name);
|
|
318
|
+
jurisdiction;
|
|
319
|
+
bucketItemWriteGroupId = null;
|
|
320
|
+
constructor(config) {
|
|
321
|
+
this.config = config;
|
|
322
|
+
this.jurisdiction = config.jurisdiction ?? "default";
|
|
323
|
+
}
|
|
324
|
+
// Creates the org's two buckets and one credential scoped to just those buckets
|
|
325
|
+
async provisionOrg(subdomain) {
|
|
326
|
+
const names = orgBucketNames(subdomain);
|
|
327
|
+
await this.createBucket(names.storageBucket);
|
|
328
|
+
await this.createBucket(names.storagePublicBucket);
|
|
329
|
+
let publicUrl = null;
|
|
330
|
+
try {
|
|
331
|
+
publicUrl = await this.enablePublicAccess(names.storagePublicBucket);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
this.logger.warn(`Public access not enabled for ${names.storagePublicBucket}: ${error}`);
|
|
334
|
+
}
|
|
335
|
+
const token = await this.createScopedToken(subdomain, [
|
|
336
|
+
names.storageBucket,
|
|
337
|
+
names.storagePublicBucket
|
|
338
|
+
]);
|
|
339
|
+
return {
|
|
340
|
+
provider: "r2",
|
|
341
|
+
accountId: this.config.accountId,
|
|
342
|
+
bucket: names.storageBucket,
|
|
343
|
+
publicBucket: names.storagePublicBucket,
|
|
344
|
+
publicUrl,
|
|
345
|
+
accessKeyId: token.id,
|
|
346
|
+
// R2 derives the S3 pair from the token: key id = token id, secret = SHA-256 of the token value. The value is
|
|
347
|
+
// returned exactly once, so it is hashed here rather than handed back to a caller that might drop it.
|
|
348
|
+
secretAccessKey: (0, import_node_crypto.createHash)("sha256").update(token.value).digest("hex"),
|
|
349
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
// Mints a replacement credential scoped to the same buckets. Returns only the credential pair, not a whole
|
|
353
|
+
// descriptor: the control plane does not hold the org's publicUrl, so it is in no position to rebuild one.
|
|
354
|
+
//
|
|
355
|
+
// The old token is NOT revoked here. Revoking before the caller has persisted the new credential would leave the org
|
|
356
|
+
// with no working key in the gap; deleteCredential is a separate call, made once the new one is stored.
|
|
357
|
+
async rotateCredential(subdomain, buckets) {
|
|
358
|
+
const token = await this.createScopedToken(subdomain, [
|
|
359
|
+
buckets.storageBucket,
|
|
360
|
+
buckets.storagePublicBucket
|
|
361
|
+
]);
|
|
362
|
+
return {
|
|
363
|
+
accessKeyId: token.id,
|
|
364
|
+
secretAccessKey: (0, import_node_crypto.createHash)("sha256").update(token.value).digest("hex")
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// Removes an org's buckets. R2 refuses to delete a bucket that still holds objects, so the uploading server must
|
|
368
|
+
// have emptied them first — a 'not empty' failure here means that step did not finish.
|
|
369
|
+
async deleteOrgBuckets(buckets) {
|
|
370
|
+
for (const bucket of [
|
|
371
|
+
buckets.storageBucket,
|
|
372
|
+
buckets.storagePublicBucket
|
|
373
|
+
]) {
|
|
374
|
+
const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${bucket}`, {
|
|
375
|
+
method: "DELETE",
|
|
376
|
+
headers: {
|
|
377
|
+
Authorization: `Bearer ${this.config.adminToken}`
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
const body = await response.json();
|
|
381
|
+
if (body.success || body.errors?.some((e) => e.code === 10006)) {
|
|
382
|
+
this.logger.log(`Deleted bucket ${bucket}`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
throw new Error(`Cloudflare bucket delete failed for ${bucket}: ${this.describe(body)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Revokes a credential by its access key id, which on R2 is the Cloudflare token id
|
|
389
|
+
async deleteCredential(accessKeyId) {
|
|
390
|
+
const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/${accessKeyId}`, {
|
|
391
|
+
method: "DELETE",
|
|
392
|
+
headers: {
|
|
393
|
+
Authorization: `Bearer ${this.config.tokensToken}`
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
const body = await response.json();
|
|
397
|
+
if (!body.success) {
|
|
398
|
+
throw new Error(`Cloudflare token delete failed for ${accessKeyId}: ${this.describe(body)}`);
|
|
399
|
+
}
|
|
400
|
+
this.logger.log(`Revoked storage credential ${accessKeyId}`);
|
|
401
|
+
}
|
|
402
|
+
// Creates one bucket, treating an existing bucket as success so provisioning can be re-run to reconcile
|
|
403
|
+
async createBucket(name) {
|
|
404
|
+
const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets`, {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
Authorization: `Bearer ${this.config.adminToken}`,
|
|
408
|
+
"Content-Type": "application/json",
|
|
409
|
+
"cf-r2-jurisdiction": this.jurisdiction
|
|
410
|
+
},
|
|
411
|
+
body: JSON.stringify({
|
|
412
|
+
name,
|
|
413
|
+
...this.config.locationHint && {
|
|
414
|
+
locationHint: this.config.locationHint
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
});
|
|
418
|
+
const body = await response.json();
|
|
419
|
+
if (body.success) {
|
|
420
|
+
this.logger.log(`Created bucket ${name}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (body.errors?.some((e) => e.code === 10004)) return;
|
|
424
|
+
throw new Error(`Cloudflare bucket create failed for ${name}: ${this.describe(body)}`);
|
|
425
|
+
}
|
|
426
|
+
// Turns on the bucket's Cloudflare-managed domain and returns it. NOTE: r2.dev is rate limited and documented as
|
|
427
|
+
// non-production — sustained traffic gets 429s. A custom domain per bucket is the production answer, and drops into
|
|
428
|
+
// this same field.
|
|
429
|
+
async enablePublicAccess(bucket) {
|
|
430
|
+
const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/r2/buckets/${bucket}/domains/managed`, {
|
|
431
|
+
method: "PUT",
|
|
432
|
+
headers: {
|
|
433
|
+
Authorization: `Bearer ${this.config.adminToken}`,
|
|
434
|
+
"Content-Type": "application/json"
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
enabled: true
|
|
438
|
+
})
|
|
439
|
+
});
|
|
440
|
+
const body = await response.json();
|
|
441
|
+
if (!body.success || !body.result?.domain) {
|
|
442
|
+
throw new Error(`Cloudflare enable public access failed for ${bucket}: ${this.describe(body)}`);
|
|
443
|
+
}
|
|
444
|
+
this.logger.log(`Enabled public access for ${bucket} \u2192 ${body.result.domain}`);
|
|
445
|
+
return `https://${body.result.domain}`;
|
|
446
|
+
}
|
|
447
|
+
// Mints an ACCOUNT-owned token that can only touch this org's buckets. Account-owned rather than user-owned so a
|
|
448
|
+
// tenant's credential does not die with whichever Cloudflare user happened to create the parent token. Requires the
|
|
449
|
+
// parent to hold `Account API Tokens Write`; a user-scoped `API Tokens: Edit` token is rejected here with 9109.
|
|
450
|
+
async createScopedToken(subdomain, buckets) {
|
|
451
|
+
const permissionGroupId = await this.resolveBucketItemWriteGroupId();
|
|
452
|
+
const resources = Object.fromEntries(buckets.map((bucket) => [
|
|
453
|
+
`com.cloudflare.edge.r2.bucket.${this.config.accountId}_${this.jurisdiction}_${bucket}`,
|
|
454
|
+
"*"
|
|
455
|
+
]));
|
|
456
|
+
const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens`, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: {
|
|
459
|
+
Authorization: `Bearer ${this.config.tokensToken}`,
|
|
460
|
+
"Content-Type": "application/json"
|
|
461
|
+
},
|
|
462
|
+
body: JSON.stringify({
|
|
463
|
+
name: `org-${subdomain}`,
|
|
464
|
+
policies: [
|
|
465
|
+
{
|
|
466
|
+
effect: "allow",
|
|
467
|
+
permission_groups: [
|
|
468
|
+
{
|
|
469
|
+
id: permissionGroupId
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
resources
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
})
|
|
476
|
+
});
|
|
477
|
+
const body = await response.json();
|
|
478
|
+
if (!body.success || !body.result?.value) {
|
|
479
|
+
throw new Error(`Cloudflare token create failed for org-${subdomain}: ${this.describe(body)}`);
|
|
480
|
+
}
|
|
481
|
+
this.logger.log(`Minted storage credential for org-${subdomain} scoped to ${buckets.length} bucket(s)`);
|
|
482
|
+
return body.result;
|
|
483
|
+
}
|
|
484
|
+
// The create-token API takes permission group UUIDs, not names. Looked up once rather than hardcoded: a UUID copied
|
|
485
|
+
// from docs fails much later with an error that says nothing useful. Same account-scoped endpoint as creation.
|
|
486
|
+
async resolveBucketItemWriteGroupId() {
|
|
487
|
+
if (this.bucketItemWriteGroupId) return this.bucketItemWriteGroupId;
|
|
488
|
+
const response = await fetch(`${CF_API}/accounts/${this.config.accountId}/tokens/permission_groups`, {
|
|
489
|
+
headers: {
|
|
490
|
+
Authorization: `Bearer ${this.config.tokensToken}`
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
const body = await response.json();
|
|
494
|
+
const group = body.result?.find((g) => g.name === BUCKET_ITEM_WRITE);
|
|
495
|
+
if (!group) {
|
|
496
|
+
throw new Error(`Cloudflare permission group '${BUCKET_ITEM_WRITE}' not found: ${this.describe(body)}`);
|
|
497
|
+
}
|
|
498
|
+
this.bucketItemWriteGroupId = group.id;
|
|
499
|
+
return group.id;
|
|
500
|
+
}
|
|
501
|
+
describe(body) {
|
|
502
|
+
return body.errors?.map((e) => `${e.code} ${e.message}`).join("; ") || "unknown error";
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/storage/provisioner.factory.ts
|
|
507
|
+
var OrgStorageProvisionerFactory = class {
|
|
508
|
+
static {
|
|
509
|
+
__name(this, "OrgStorageProvisionerFactory");
|
|
510
|
+
}
|
|
511
|
+
options;
|
|
512
|
+
cache = /* @__PURE__ */ new Map();
|
|
513
|
+
constructor(options) {
|
|
514
|
+
this.options = options;
|
|
515
|
+
}
|
|
516
|
+
// Resolves a provisioner by provider name, constructing it once on first use
|
|
517
|
+
resolve(provider) {
|
|
518
|
+
const cached = this.cache.get(provider);
|
|
519
|
+
if (cached) return cached;
|
|
520
|
+
const created = this.create(provider);
|
|
521
|
+
this.cache.set(provider, created);
|
|
522
|
+
return created;
|
|
523
|
+
}
|
|
524
|
+
create(provider) {
|
|
525
|
+
switch (provider) {
|
|
526
|
+
case "r2":
|
|
527
|
+
return new R2BucketProvisioner(readConfigSource(this.options.r2, "r2"));
|
|
528
|
+
default:
|
|
529
|
+
throw new Error(`Unsupported storage provider: ${provider}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/storage/storage.factory.ts
|
|
535
|
+
var StorageFactory = class {
|
|
536
|
+
static {
|
|
537
|
+
__name(this, "StorageFactory");
|
|
538
|
+
}
|
|
539
|
+
options;
|
|
540
|
+
cache = /* @__PURE__ */ new Map();
|
|
541
|
+
constructor(options) {
|
|
542
|
+
this.options = options;
|
|
543
|
+
}
|
|
544
|
+
// Resolves a provider by name, constructing it once on first use
|
|
545
|
+
resolve(provider) {
|
|
546
|
+
const cached = this.cache.get(provider);
|
|
547
|
+
if (cached) return cached;
|
|
548
|
+
const created = this.create(provider);
|
|
549
|
+
this.cache.set(provider, created);
|
|
550
|
+
return created;
|
|
551
|
+
}
|
|
552
|
+
create(provider) {
|
|
553
|
+
switch (provider) {
|
|
554
|
+
case "r2":
|
|
555
|
+
return new R2StorageProvider(readConfigSource(this.options.r2, "r2"));
|
|
556
|
+
default:
|
|
557
|
+
throw new Error(`Unsupported storage provider: ${provider}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
562
|
+
0 && (module.exports = {
|
|
563
|
+
BucketUsageReader,
|
|
564
|
+
OrgStorageProvisionerFactory,
|
|
565
|
+
R2BucketProvisioner,
|
|
566
|
+
R2StorageProvider,
|
|
567
|
+
StorageFactory,
|
|
568
|
+
orgBucketNames,
|
|
569
|
+
readConfigSource
|
|
570
|
+
});
|
|
571
|
+
//# sourceMappingURL=storage.cjs.map
|