@ai-digital-factory/asset-manager 0.1.0

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/index.cjs ADDED
@@ -0,0 +1,399 @@
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 __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AssetManagerError: () => AssetManagerError,
24
+ createAssetManager: () => createAssetManager
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // ../r2/dist/index.js
29
+ var import_node_fs = require("fs");
30
+ var import_promises = require("fs/promises");
31
+ var import_node_stream = require("stream");
32
+ var import_client_s3 = require("@aws-sdk/client-s3");
33
+ var import_lib_storage = require("@aws-sdk/lib-storage");
34
+ var import_mime_types = require("mime-types");
35
+ var R2AssetManagerError = class extends Error {
36
+ code;
37
+ retryable;
38
+ constructor(code, message, retryable, options) {
39
+ super(message, options);
40
+ this.code = code;
41
+ this.retryable = retryable;
42
+ this.name = "R2AssetManagerError";
43
+ }
44
+ };
45
+ var PUBLIC_BASE_URL = "https://pub-b3f9537bf89843aebcb0730a735af9b2.r2.dev";
46
+ var DEFAULT_PART_SIZE = 5 * 1024 * 1024;
47
+ var DEFAULT_QUEUE_SIZE = 4;
48
+ var CREDENTIAL_PREFIX = "am_v1.";
49
+ var MEDIA_TYPE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+(?:\s*;\s*[!#$%&'*+.^_`|~0-9A-Za-z-]+=(?:[!#$%&'*+.^_`|~0-9A-Za-z-]+|"(?:[^"\\]|\\.)*"))*$/u;
50
+ function invalidArgument(message) {
51
+ throw new R2AssetManagerError("INVALID_ARGUMENT", message, false);
52
+ }
53
+ function hasControlCharacter(value) {
54
+ return [...value].some((character) => {
55
+ const codePoint = character.codePointAt(0);
56
+ return codePoint <= 31 || codePoint === 127;
57
+ });
58
+ }
59
+ function invalidCredential() {
60
+ throw new Error("Invalid Asset Manager Credential");
61
+ }
62
+ function isR2CredentialConfig(value) {
63
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
64
+ return false;
65
+ }
66
+ const record = value;
67
+ const expectedKeys = [
68
+ "accessKeyId",
69
+ "accountId",
70
+ "bucket",
71
+ "secretAccessKey"
72
+ ];
73
+ if (Object.keys(record).sort().join(",") !== expectedKeys.join(",")) {
74
+ return false;
75
+ }
76
+ return expectedKeys.every((key) => typeof record[key] === "string" && record[key].length > 0);
77
+ }
78
+ function decodeR2Credential(credential) {
79
+ try {
80
+ if (!credential.startsWith(CREDENTIAL_PREFIX)) {
81
+ invalidCredential();
82
+ }
83
+ const payload = credential.slice(CREDENTIAL_PREFIX.length);
84
+ if (payload.length === 0 || !/^[A-Za-z0-9_-]+$/u.test(payload) || Buffer.from(payload, "base64url").toString("base64url") !== payload) {
85
+ invalidCredential();
86
+ }
87
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
88
+ if (!isR2CredentialConfig(decoded)) {
89
+ invalidCredential();
90
+ }
91
+ return decoded;
92
+ } catch {
93
+ invalidCredential();
94
+ }
95
+ }
96
+ function validateNamespace(namespace) {
97
+ if (namespace.length === 0 || namespace === "." || namespace === ".." || Buffer.byteLength(namespace, "utf8") > 128 || namespace.includes("/") || namespace.includes("\\") || hasControlCharacter(namespace)) {
98
+ invalidArgument("Namespace must be one valid name of at most 128 bytes");
99
+ }
100
+ }
101
+ function validateLogicalPath(value, label) {
102
+ const segments = value.split("/");
103
+ if (value.length === 0 || Buffer.byteLength(value, "utf8") > 1024 || value.includes("\\") || hasControlCharacter(value) || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
104
+ invalidArgument(`${label} must be a valid relative logical path`);
105
+ }
106
+ }
107
+ function validateContentType(contentType) {
108
+ if (!MEDIA_TYPE.test(contentType)) {
109
+ invalidArgument("Content type must be a valid media type");
110
+ }
111
+ }
112
+ function validateContentLength(contentLength) {
113
+ if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
114
+ invalidArgument("Content length must be a non-negative safe integer");
115
+ }
116
+ }
117
+ function createR2Client(config, client) {
118
+ return client ?? new import_client_s3.S3Client({
119
+ region: "auto",
120
+ endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
121
+ credentials: {
122
+ accessKeyId: config.accessKeyId,
123
+ secretAccessKey: config.secretAccessKey
124
+ }
125
+ });
126
+ }
127
+ function contentTypeFor(assetPath) {
128
+ return (0, import_mime_types.lookup)(assetPath) || "application/octet-stream";
129
+ }
130
+ function normalizeBody(body) {
131
+ if (body instanceof Uint8Array || body instanceof import_node_stream.Readable) {
132
+ return body;
133
+ }
134
+ return import_node_stream.Readable.fromWeb(body);
135
+ }
136
+ function lengthMismatch(expected, actual) {
137
+ return new R2AssetManagerError("LENGTH_MISMATCH", `Body length ${actual} does not match declared content length ${expected}`, false);
138
+ }
139
+ function aborted(cause) {
140
+ return new R2AssetManagerError("ABORTED", "Asset Manager operation was aborted", false, cause === void 0 ? void 0 : { cause });
141
+ }
142
+ function createMeteredBody(body, expectedLength) {
143
+ if (body instanceof Uint8Array) {
144
+ if (body.byteLength !== expectedLength) {
145
+ throw lengthMismatch(expectedLength, body.byteLength);
146
+ }
147
+ return { body };
148
+ }
149
+ const source = normalizeBody(body);
150
+ let sourceError;
151
+ let actualLength = 0;
152
+ const meter = new import_node_stream.Transform({
153
+ writableObjectMode: true,
154
+ transform(chunk, _encoding, callback) {
155
+ if (!(chunk instanceof Uint8Array)) {
156
+ callback(new R2AssetManagerError("INVALID_ARGUMENT", "Body streams must emit only Uint8Array chunks", false));
157
+ return;
158
+ }
159
+ actualLength += chunk.byteLength;
160
+ if (actualLength > expectedLength) {
161
+ callback(lengthMismatch(expectedLength, actualLength));
162
+ return;
163
+ }
164
+ callback(null, chunk);
165
+ },
166
+ flush(callback) {
167
+ if (actualLength !== expectedLength) {
168
+ callback(lengthMismatch(expectedLength, actualLength));
169
+ return;
170
+ }
171
+ callback();
172
+ }
173
+ });
174
+ source.once("error", (error) => {
175
+ sourceError = error;
176
+ meter.destroy(error);
177
+ });
178
+ return {
179
+ body: source.pipe(meter),
180
+ source,
181
+ sourceFailure: () => sourceError
182
+ };
183
+ }
184
+ function isAuthorizationError(error) {
185
+ return error instanceof Error && [
186
+ "AccessDenied",
187
+ "CredentialsProviderError",
188
+ "ExpiredToken",
189
+ "InvalidAccessKeyId",
190
+ "SignatureDoesNotMatch"
191
+ ].includes(error.name);
192
+ }
193
+ function encodePath(path) {
194
+ return path.split("/").map(encodeURIComponent).join("/");
195
+ }
196
+ var R2AssetManager = class {
197
+ bucket;
198
+ client;
199
+ namespace;
200
+ constructor(config, deps = {}) {
201
+ validateNamespace(config.namespace);
202
+ this.bucket = config.bucket;
203
+ this.client = createR2Client(config, deps.client);
204
+ this.namespace = config.namespace;
205
+ }
206
+ async publish(input) {
207
+ validateLogicalPath(input.assetPath, "Asset Path");
208
+ if (input.signal?.aborted) {
209
+ throw aborted(input.signal.reason);
210
+ }
211
+ let fileStat;
212
+ try {
213
+ fileStat = await (0, import_promises.stat)(input.localPath);
214
+ } catch (error) {
215
+ throw new R2AssetManagerError("SOURCE_UNAVAILABLE", "Local Asset is unavailable", false, { cause: error });
216
+ }
217
+ if (!fileStat.isFile()) {
218
+ throw new R2AssetManagerError("SOURCE_UNAVAILABLE", "Local Asset must be a regular file", false);
219
+ }
220
+ const providerKey = `assets/${this.namespace}/${input.assetPath}`;
221
+ await this.upload({
222
+ key: providerKey,
223
+ body: (0, import_node_fs.createReadStream)(input.localPath),
224
+ contentType: contentTypeFor(input.assetPath),
225
+ contentLength: fileStat.size,
226
+ signal: input.signal
227
+ });
228
+ return `${PUBLIC_BASE_URL}/${encodePath(providerKey)}`;
229
+ }
230
+ async put(input) {
231
+ validateLogicalPath(input.key, "Object Key");
232
+ validateContentType(input.contentType);
233
+ validateContentLength(input.contentLength);
234
+ await this.upload({
235
+ key: `objects/${this.namespace}/${input.key}`,
236
+ body: input.body,
237
+ contentType: input.contentType,
238
+ contentLength: input.contentLength,
239
+ signal: input.signal
240
+ });
241
+ }
242
+ async upload(input) {
243
+ if (input.signal?.aborted) {
244
+ throw aborted(input.signal.reason);
245
+ }
246
+ const metered = createMeteredBody(input.body, input.contentLength);
247
+ const upload = new import_lib_storage.Upload({
248
+ client: this.client,
249
+ params: {
250
+ Bucket: this.bucket,
251
+ Key: input.key,
252
+ Body: metered.body,
253
+ ContentType: input.contentType,
254
+ ContentLength: input.contentLength
255
+ },
256
+ queueSize: DEFAULT_QUEUE_SIZE,
257
+ partSize: DEFAULT_PART_SIZE,
258
+ leavePartsOnError: false
259
+ });
260
+ let wasAborted = false;
261
+ const abortUpload = () => {
262
+ wasAborted = true;
263
+ metered.source?.destroy();
264
+ void upload.abort();
265
+ };
266
+ input.signal?.addEventListener("abort", abortUpload, { once: true });
267
+ try {
268
+ await upload.done();
269
+ } catch (error) {
270
+ metered.source?.destroy();
271
+ if (wasAborted || input.signal?.aborted) {
272
+ throw aborted(error);
273
+ }
274
+ if (error instanceof R2AssetManagerError) {
275
+ throw error;
276
+ }
277
+ if (metered.sourceFailure?.() !== void 0) {
278
+ throw new R2AssetManagerError("SOURCE_UNAVAILABLE", "Object body became unavailable", false, { cause: metered.sourceFailure?.() });
279
+ }
280
+ if (isAuthorizationError(error)) {
281
+ throw new R2AssetManagerError("UNAUTHORIZED", "Asset Manager Credential is not authorized for storage", false, { cause: error });
282
+ }
283
+ throw new R2AssetManagerError("WRITE_FAILED", "Provider could not complete the write", true, { cause: error });
284
+ } finally {
285
+ input.signal?.removeEventListener("abort", abortUpload);
286
+ }
287
+ }
288
+ };
289
+
290
+ // src/index.ts
291
+ var AssetManagerError = class extends Error {
292
+ constructor(code, message, retryable, options) {
293
+ super(message, options);
294
+ this.code = code;
295
+ this.retryable = retryable;
296
+ this.name = "AssetManagerError";
297
+ }
298
+ code;
299
+ retryable;
300
+ };
301
+ function sanitizedCause(error, secrets) {
302
+ if (!(error instanceof Error)) {
303
+ return void 0;
304
+ }
305
+ let message = error.message;
306
+ for (const secret of secrets) {
307
+ if (secret.length > 0) {
308
+ message = message.replaceAll(secret, "[REDACTED]");
309
+ }
310
+ }
311
+ const cause = new Error(message);
312
+ cause.name = error.name;
313
+ return cause;
314
+ }
315
+ function normalizeError(error, secrets) {
316
+ if (error instanceof AssetManagerError) {
317
+ return error;
318
+ }
319
+ if (error instanceof R2AssetManagerError) {
320
+ return new AssetManagerError(error.code, error.message, error.retryable, {
321
+ cause: sanitizedCause(error.cause ?? error, secrets)
322
+ });
323
+ }
324
+ return new AssetManagerError(
325
+ "WRITE_FAILED",
326
+ "Asset Manager could not complete the write",
327
+ true,
328
+ { cause: sanitizedCause(error, secrets) }
329
+ );
330
+ }
331
+ var FacadeAssetManager = class {
332
+ constructor(provider, secrets) {
333
+ this.provider = provider;
334
+ this.secrets = secrets;
335
+ }
336
+ provider;
337
+ secrets;
338
+ async publish(input) {
339
+ try {
340
+ return await this.provider.publish(input);
341
+ } catch (error) {
342
+ throw normalizeError(error, this.secrets);
343
+ }
344
+ }
345
+ async put(input) {
346
+ try {
347
+ await this.provider.put(input);
348
+ } catch (error) {
349
+ throw normalizeError(error, this.secrets);
350
+ }
351
+ }
352
+ };
353
+ function createAssetManager(input) {
354
+ if (typeof input?.credential !== "string") {
355
+ throw new AssetManagerError(
356
+ "INVALID_CREDENTIAL",
357
+ "Invalid Asset Manager Credential",
358
+ false
359
+ );
360
+ }
361
+ if (typeof input.namespace !== "string") {
362
+ throw new AssetManagerError(
363
+ "INVALID_ARGUMENT",
364
+ "Namespace must be one valid name of at most 128 bytes",
365
+ false
366
+ );
367
+ }
368
+ let config;
369
+ try {
370
+ config = decodeR2Credential(input.credential);
371
+ } catch {
372
+ throw new AssetManagerError(
373
+ "INVALID_CREDENTIAL",
374
+ "Invalid Asset Manager Credential",
375
+ false
376
+ );
377
+ }
378
+ const secrets = [
379
+ input.credential,
380
+ config.accountId,
381
+ config.accessKeyId,
382
+ config.secretAccessKey,
383
+ config.bucket
384
+ ];
385
+ try {
386
+ const provider = new R2AssetManager({
387
+ ...config,
388
+ namespace: input.namespace
389
+ });
390
+ return new FacadeAssetManager(provider, secrets);
391
+ } catch (error) {
392
+ throw normalizeError(error, secrets);
393
+ }
394
+ }
395
+ // Annotate the CommonJS export names for ESM import in node:
396
+ 0 && (module.exports = {
397
+ AssetManagerError,
398
+ createAssetManager
399
+ });
@@ -0,0 +1,29 @@
1
+ import { Readable } from 'node:stream';
2
+
3
+ interface AssetManager {
4
+ publish(input: {
5
+ localPath: string;
6
+ assetPath: string;
7
+ signal?: AbortSignal;
8
+ }): Promise<string>;
9
+ put(input: {
10
+ key: string;
11
+ body: Uint8Array | Readable | ReadableStream<Uint8Array>;
12
+ contentType: string;
13
+ contentLength: number;
14
+ signal?: AbortSignal;
15
+ }): Promise<void>;
16
+ }
17
+
18
+ type AssetManagerErrorCode = "INVALID_ARGUMENT" | "INVALID_CREDENTIAL" | "SOURCE_UNAVAILABLE" | "LENGTH_MISMATCH" | "UNAUTHORIZED" | "ABORTED" | "WRITE_FAILED";
19
+ declare class AssetManagerError extends Error {
20
+ readonly code: AssetManagerErrorCode;
21
+ readonly retryable: boolean;
22
+ constructor(code: AssetManagerErrorCode, message: string, retryable: boolean, options?: ErrorOptions);
23
+ }
24
+ declare function createAssetManager(input: {
25
+ credential: string;
26
+ namespace: string;
27
+ }): AssetManager;
28
+
29
+ export { type AssetManager, AssetManagerError, createAssetManager };
@@ -0,0 +1,29 @@
1
+ import { Readable } from 'node:stream';
2
+
3
+ interface AssetManager {
4
+ publish(input: {
5
+ localPath: string;
6
+ assetPath: string;
7
+ signal?: AbortSignal;
8
+ }): Promise<string>;
9
+ put(input: {
10
+ key: string;
11
+ body: Uint8Array | Readable | ReadableStream<Uint8Array>;
12
+ contentType: string;
13
+ contentLength: number;
14
+ signal?: AbortSignal;
15
+ }): Promise<void>;
16
+ }
17
+
18
+ type AssetManagerErrorCode = "INVALID_ARGUMENT" | "INVALID_CREDENTIAL" | "SOURCE_UNAVAILABLE" | "LENGTH_MISMATCH" | "UNAUTHORIZED" | "ABORTED" | "WRITE_FAILED";
19
+ declare class AssetManagerError extends Error {
20
+ readonly code: AssetManagerErrorCode;
21
+ readonly retryable: boolean;
22
+ constructor(code: AssetManagerErrorCode, message: string, retryable: boolean, options?: ErrorOptions);
23
+ }
24
+ declare function createAssetManager(input: {
25
+ credential: string;
26
+ namespace: string;
27
+ }): AssetManager;
28
+
29
+ export { type AssetManager, AssetManagerError, createAssetManager };
package/dist/index.js ADDED
@@ -0,0 +1,371 @@
1
+ // ../r2/dist/index.js
2
+ import { createReadStream } from "fs";
3
+ import { stat } from "fs/promises";
4
+ import { Readable, Transform } from "stream";
5
+ import { S3Client } from "@aws-sdk/client-s3";
6
+ import { Upload } from "@aws-sdk/lib-storage";
7
+ import { lookup } from "mime-types";
8
+ var R2AssetManagerError = class extends Error {
9
+ code;
10
+ retryable;
11
+ constructor(code, message, retryable, options) {
12
+ super(message, options);
13
+ this.code = code;
14
+ this.retryable = retryable;
15
+ this.name = "R2AssetManagerError";
16
+ }
17
+ };
18
+ var PUBLIC_BASE_URL = "https://pub-b3f9537bf89843aebcb0730a735af9b2.r2.dev";
19
+ var DEFAULT_PART_SIZE = 5 * 1024 * 1024;
20
+ var DEFAULT_QUEUE_SIZE = 4;
21
+ var CREDENTIAL_PREFIX = "am_v1.";
22
+ var MEDIA_TYPE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+(?:\s*;\s*[!#$%&'*+.^_`|~0-9A-Za-z-]+=(?:[!#$%&'*+.^_`|~0-9A-Za-z-]+|"(?:[^"\\]|\\.)*"))*$/u;
23
+ function invalidArgument(message) {
24
+ throw new R2AssetManagerError("INVALID_ARGUMENT", message, false);
25
+ }
26
+ function hasControlCharacter(value) {
27
+ return [...value].some((character) => {
28
+ const codePoint = character.codePointAt(0);
29
+ return codePoint <= 31 || codePoint === 127;
30
+ });
31
+ }
32
+ function invalidCredential() {
33
+ throw new Error("Invalid Asset Manager Credential");
34
+ }
35
+ function isR2CredentialConfig(value) {
36
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
37
+ return false;
38
+ }
39
+ const record = value;
40
+ const expectedKeys = [
41
+ "accessKeyId",
42
+ "accountId",
43
+ "bucket",
44
+ "secretAccessKey"
45
+ ];
46
+ if (Object.keys(record).sort().join(",") !== expectedKeys.join(",")) {
47
+ return false;
48
+ }
49
+ return expectedKeys.every((key) => typeof record[key] === "string" && record[key].length > 0);
50
+ }
51
+ function decodeR2Credential(credential) {
52
+ try {
53
+ if (!credential.startsWith(CREDENTIAL_PREFIX)) {
54
+ invalidCredential();
55
+ }
56
+ const payload = credential.slice(CREDENTIAL_PREFIX.length);
57
+ if (payload.length === 0 || !/^[A-Za-z0-9_-]+$/u.test(payload) || Buffer.from(payload, "base64url").toString("base64url") !== payload) {
58
+ invalidCredential();
59
+ }
60
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
61
+ if (!isR2CredentialConfig(decoded)) {
62
+ invalidCredential();
63
+ }
64
+ return decoded;
65
+ } catch {
66
+ invalidCredential();
67
+ }
68
+ }
69
+ function validateNamespace(namespace) {
70
+ if (namespace.length === 0 || namespace === "." || namespace === ".." || Buffer.byteLength(namespace, "utf8") > 128 || namespace.includes("/") || namespace.includes("\\") || hasControlCharacter(namespace)) {
71
+ invalidArgument("Namespace must be one valid name of at most 128 bytes");
72
+ }
73
+ }
74
+ function validateLogicalPath(value, label) {
75
+ const segments = value.split("/");
76
+ if (value.length === 0 || Buffer.byteLength(value, "utf8") > 1024 || value.includes("\\") || hasControlCharacter(value) || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
77
+ invalidArgument(`${label} must be a valid relative logical path`);
78
+ }
79
+ }
80
+ function validateContentType(contentType) {
81
+ if (!MEDIA_TYPE.test(contentType)) {
82
+ invalidArgument("Content type must be a valid media type");
83
+ }
84
+ }
85
+ function validateContentLength(contentLength) {
86
+ if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
87
+ invalidArgument("Content length must be a non-negative safe integer");
88
+ }
89
+ }
90
+ function createR2Client(config, client) {
91
+ return client ?? new S3Client({
92
+ region: "auto",
93
+ endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
94
+ credentials: {
95
+ accessKeyId: config.accessKeyId,
96
+ secretAccessKey: config.secretAccessKey
97
+ }
98
+ });
99
+ }
100
+ function contentTypeFor(assetPath) {
101
+ return lookup(assetPath) || "application/octet-stream";
102
+ }
103
+ function normalizeBody(body) {
104
+ if (body instanceof Uint8Array || body instanceof Readable) {
105
+ return body;
106
+ }
107
+ return Readable.fromWeb(body);
108
+ }
109
+ function lengthMismatch(expected, actual) {
110
+ return new R2AssetManagerError("LENGTH_MISMATCH", `Body length ${actual} does not match declared content length ${expected}`, false);
111
+ }
112
+ function aborted(cause) {
113
+ return new R2AssetManagerError("ABORTED", "Asset Manager operation was aborted", false, cause === void 0 ? void 0 : { cause });
114
+ }
115
+ function createMeteredBody(body, expectedLength) {
116
+ if (body instanceof Uint8Array) {
117
+ if (body.byteLength !== expectedLength) {
118
+ throw lengthMismatch(expectedLength, body.byteLength);
119
+ }
120
+ return { body };
121
+ }
122
+ const source = normalizeBody(body);
123
+ let sourceError;
124
+ let actualLength = 0;
125
+ const meter = new Transform({
126
+ writableObjectMode: true,
127
+ transform(chunk, _encoding, callback) {
128
+ if (!(chunk instanceof Uint8Array)) {
129
+ callback(new R2AssetManagerError("INVALID_ARGUMENT", "Body streams must emit only Uint8Array chunks", false));
130
+ return;
131
+ }
132
+ actualLength += chunk.byteLength;
133
+ if (actualLength > expectedLength) {
134
+ callback(lengthMismatch(expectedLength, actualLength));
135
+ return;
136
+ }
137
+ callback(null, chunk);
138
+ },
139
+ flush(callback) {
140
+ if (actualLength !== expectedLength) {
141
+ callback(lengthMismatch(expectedLength, actualLength));
142
+ return;
143
+ }
144
+ callback();
145
+ }
146
+ });
147
+ source.once("error", (error) => {
148
+ sourceError = error;
149
+ meter.destroy(error);
150
+ });
151
+ return {
152
+ body: source.pipe(meter),
153
+ source,
154
+ sourceFailure: () => sourceError
155
+ };
156
+ }
157
+ function isAuthorizationError(error) {
158
+ return error instanceof Error && [
159
+ "AccessDenied",
160
+ "CredentialsProviderError",
161
+ "ExpiredToken",
162
+ "InvalidAccessKeyId",
163
+ "SignatureDoesNotMatch"
164
+ ].includes(error.name);
165
+ }
166
+ function encodePath(path) {
167
+ return path.split("/").map(encodeURIComponent).join("/");
168
+ }
169
+ var R2AssetManager = class {
170
+ bucket;
171
+ client;
172
+ namespace;
173
+ constructor(config, deps = {}) {
174
+ validateNamespace(config.namespace);
175
+ this.bucket = config.bucket;
176
+ this.client = createR2Client(config, deps.client);
177
+ this.namespace = config.namespace;
178
+ }
179
+ async publish(input) {
180
+ validateLogicalPath(input.assetPath, "Asset Path");
181
+ if (input.signal?.aborted) {
182
+ throw aborted(input.signal.reason);
183
+ }
184
+ let fileStat;
185
+ try {
186
+ fileStat = await stat(input.localPath);
187
+ } catch (error) {
188
+ throw new R2AssetManagerError("SOURCE_UNAVAILABLE", "Local Asset is unavailable", false, { cause: error });
189
+ }
190
+ if (!fileStat.isFile()) {
191
+ throw new R2AssetManagerError("SOURCE_UNAVAILABLE", "Local Asset must be a regular file", false);
192
+ }
193
+ const providerKey = `assets/${this.namespace}/${input.assetPath}`;
194
+ await this.upload({
195
+ key: providerKey,
196
+ body: createReadStream(input.localPath),
197
+ contentType: contentTypeFor(input.assetPath),
198
+ contentLength: fileStat.size,
199
+ signal: input.signal
200
+ });
201
+ return `${PUBLIC_BASE_URL}/${encodePath(providerKey)}`;
202
+ }
203
+ async put(input) {
204
+ validateLogicalPath(input.key, "Object Key");
205
+ validateContentType(input.contentType);
206
+ validateContentLength(input.contentLength);
207
+ await this.upload({
208
+ key: `objects/${this.namespace}/${input.key}`,
209
+ body: input.body,
210
+ contentType: input.contentType,
211
+ contentLength: input.contentLength,
212
+ signal: input.signal
213
+ });
214
+ }
215
+ async upload(input) {
216
+ if (input.signal?.aborted) {
217
+ throw aborted(input.signal.reason);
218
+ }
219
+ const metered = createMeteredBody(input.body, input.contentLength);
220
+ const upload = new Upload({
221
+ client: this.client,
222
+ params: {
223
+ Bucket: this.bucket,
224
+ Key: input.key,
225
+ Body: metered.body,
226
+ ContentType: input.contentType,
227
+ ContentLength: input.contentLength
228
+ },
229
+ queueSize: DEFAULT_QUEUE_SIZE,
230
+ partSize: DEFAULT_PART_SIZE,
231
+ leavePartsOnError: false
232
+ });
233
+ let wasAborted = false;
234
+ const abortUpload = () => {
235
+ wasAborted = true;
236
+ metered.source?.destroy();
237
+ void upload.abort();
238
+ };
239
+ input.signal?.addEventListener("abort", abortUpload, { once: true });
240
+ try {
241
+ await upload.done();
242
+ } catch (error) {
243
+ metered.source?.destroy();
244
+ if (wasAborted || input.signal?.aborted) {
245
+ throw aborted(error);
246
+ }
247
+ if (error instanceof R2AssetManagerError) {
248
+ throw error;
249
+ }
250
+ if (metered.sourceFailure?.() !== void 0) {
251
+ throw new R2AssetManagerError("SOURCE_UNAVAILABLE", "Object body became unavailable", false, { cause: metered.sourceFailure?.() });
252
+ }
253
+ if (isAuthorizationError(error)) {
254
+ throw new R2AssetManagerError("UNAUTHORIZED", "Asset Manager Credential is not authorized for storage", false, { cause: error });
255
+ }
256
+ throw new R2AssetManagerError("WRITE_FAILED", "Provider could not complete the write", true, { cause: error });
257
+ } finally {
258
+ input.signal?.removeEventListener("abort", abortUpload);
259
+ }
260
+ }
261
+ };
262
+
263
+ // src/index.ts
264
+ var AssetManagerError = class extends Error {
265
+ constructor(code, message, retryable, options) {
266
+ super(message, options);
267
+ this.code = code;
268
+ this.retryable = retryable;
269
+ this.name = "AssetManagerError";
270
+ }
271
+ code;
272
+ retryable;
273
+ };
274
+ function sanitizedCause(error, secrets) {
275
+ if (!(error instanceof Error)) {
276
+ return void 0;
277
+ }
278
+ let message = error.message;
279
+ for (const secret of secrets) {
280
+ if (secret.length > 0) {
281
+ message = message.replaceAll(secret, "[REDACTED]");
282
+ }
283
+ }
284
+ const cause = new Error(message);
285
+ cause.name = error.name;
286
+ return cause;
287
+ }
288
+ function normalizeError(error, secrets) {
289
+ if (error instanceof AssetManagerError) {
290
+ return error;
291
+ }
292
+ if (error instanceof R2AssetManagerError) {
293
+ return new AssetManagerError(error.code, error.message, error.retryable, {
294
+ cause: sanitizedCause(error.cause ?? error, secrets)
295
+ });
296
+ }
297
+ return new AssetManagerError(
298
+ "WRITE_FAILED",
299
+ "Asset Manager could not complete the write",
300
+ true,
301
+ { cause: sanitizedCause(error, secrets) }
302
+ );
303
+ }
304
+ var FacadeAssetManager = class {
305
+ constructor(provider, secrets) {
306
+ this.provider = provider;
307
+ this.secrets = secrets;
308
+ }
309
+ provider;
310
+ secrets;
311
+ async publish(input) {
312
+ try {
313
+ return await this.provider.publish(input);
314
+ } catch (error) {
315
+ throw normalizeError(error, this.secrets);
316
+ }
317
+ }
318
+ async put(input) {
319
+ try {
320
+ await this.provider.put(input);
321
+ } catch (error) {
322
+ throw normalizeError(error, this.secrets);
323
+ }
324
+ }
325
+ };
326
+ function createAssetManager(input) {
327
+ if (typeof input?.credential !== "string") {
328
+ throw new AssetManagerError(
329
+ "INVALID_CREDENTIAL",
330
+ "Invalid Asset Manager Credential",
331
+ false
332
+ );
333
+ }
334
+ if (typeof input.namespace !== "string") {
335
+ throw new AssetManagerError(
336
+ "INVALID_ARGUMENT",
337
+ "Namespace must be one valid name of at most 128 bytes",
338
+ false
339
+ );
340
+ }
341
+ let config;
342
+ try {
343
+ config = decodeR2Credential(input.credential);
344
+ } catch {
345
+ throw new AssetManagerError(
346
+ "INVALID_CREDENTIAL",
347
+ "Invalid Asset Manager Credential",
348
+ false
349
+ );
350
+ }
351
+ const secrets = [
352
+ input.credential,
353
+ config.accountId,
354
+ config.accessKeyId,
355
+ config.secretAccessKey,
356
+ config.bucket
357
+ ];
358
+ try {
359
+ const provider = new R2AssetManager({
360
+ ...config,
361
+ namespace: input.namespace
362
+ });
363
+ return new FacadeAssetManager(provider, secrets);
364
+ } catch (error) {
365
+ throw normalizeError(error, secrets);
366
+ }
367
+ }
368
+ export {
369
+ AssetManagerError,
370
+ createAssetManager
371
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@ai-digital-factory/asset-manager",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ai-digital-factory/asset-manager"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@aws-sdk/client-s3": "3.1098.0",
27
+ "@aws-sdk/lib-storage": "3.1098.0",
28
+ "mime-types": "^3.0.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.17.2",
32
+ "tsup": "^8.5.0",
33
+ "@ai-digital-factory/asset-manager-core": "0.0.2",
34
+ "@ai-digital-factory/asset-manager-r2": "0.0.2"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup --config tsup.config.ts",
38
+ "test": "vitest run",
39
+ "typecheck": "tsc --project tsconfig.json",
40
+ "lint": "eslint . --ext ts"
41
+ }
42
+ }