deeplake 0.3.32 → 0.3.33

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.
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  /**
3
- * Node.js S3 storage client factory for the WASM deeplake engine.
3
+ * Node.js storage client factory for the WASM deeplake engine.
4
4
  *
5
5
  * The C++ WASM code calls js_storage::client_factory()(type, config, mode?)
6
6
  * to create a JS client object with methods like download, exists, list, etc.
7
- * This module provides the factory that creates those client objects using
8
- * the AWS SDK v3.
7
+ * This module provides the factory that creates those client objects for
8
+ * S3 (AWS SDK v3), GCS (REST API), and Azure Blob Storage (REST API).
9
9
  *
10
- * Credential rotation: STS credentials are refreshed automatically before
10
+ * Credential rotation: credentials are refreshed automatically before
11
11
  * they expire (5 minutes before expiry, matching the native C++ behavior).
12
12
  * The C++ side passes refresh metadata (orgId, dsName, token) through the
13
13
  * config object so the JS side can call the backend API for fresh creds.
@@ -204,86 +204,42 @@ function releaseStorageClients() {
204
204
  }
205
205
  function createStorageFactory(wasmModule) {
206
206
  return function clientFactory(type, config, mode) {
207
- if (type !== 'aws') {
208
- // GCS and Azure storage types are supported in the browser visualizer
209
- // (via GCSGetter/AzureGetter) but not yet in the Node.js SDK.
210
- // The C++ side may request these types when datasets are backed by
211
- // non-S3 storage. For now, throw a clear error.
212
- throw new Error(`Unsupported storage type: ${type}. The Node.js SDK currently supports "aws" only. ` +
213
- `GCS and Azure require additional dependencies (@azure/storage-blob, etc.).`);
214
- }
215
- const credentials = config?.credentials;
216
- const region = config?.region ||
217
- process.env.AWS_REGION ||
218
- process.env.AWS_DEFAULT_REGION ||
219
- 'us-east-1';
220
- const endpoint = config?.endpoint ?? undefined;
221
- // Resolve credentials: prefer config from C++, fall back to process.env.
222
- // In WASM, std::getenv() doesn't read Node.js process.env, so the C++
223
- // credential chain can't pick up env vars — we must do it here.
224
- let resolvedCreds;
225
- if (credentials?.accessKeyId && credentials?.secretAccessKey) {
226
- resolvedCreds = {
227
- accessKeyId: credentials.accessKeyId,
228
- secretAccessKey: credentials.secretAccessKey,
229
- sessionToken: credentials.sessionToken,
230
- };
231
- }
232
- else if (process.env.AWS_ACCESS_KEY_ID &&
233
- process.env.AWS_SECRET_ACCESS_KEY) {
234
- resolvedCreds = {
235
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
236
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
237
- sessionToken: process.env.AWS_SESSION_TOKEN,
238
- };
239
- }
240
- // Build refresh metadata from C++ params (org_id, ds_name, token)
241
- const refresh = config?.refresh;
242
- let refreshMeta = null;
243
- if (refresh?.orgId && refresh?.token && (refresh?.dsName || refresh?.credsKey)) {
244
- refreshMeta = {
245
- orgId: refresh.orgId,
246
- dsName: refresh.dsName,
247
- credsKey: refresh.credsKey,
248
- token: refresh.token,
249
- apiEndpoint: resolveApiEndpoint(wasmModule),
250
- };
251
- }
252
- const s3Config = {
253
- region,
254
- credentials: resolvedCreds,
255
- // Match C++ native client: 8s request/connect timeouts (s3_utils.hpp:313-314)
256
- requestHandler: new (require('@smithy/node-http-handler').NodeHttpHandler)({
257
- requestTimeout: 8_000,
258
- connectionTimeout: 8_000,
259
- }),
260
- // Match C++ native client: custom retry (s3_retry_strategy.hpp)
261
- // 200ms interval, ~6s max → roughly 30 attempts
262
- maxAttempts: 30,
263
- // Disable IMDS lookups to avoid 5-10s hangs when no instance role
264
- // is available (matches C++ shouldDisableIMDS=true)
265
- disableHostPrefix: false,
266
- followRegionRedirects: true,
267
- };
268
- if (endpoint) {
269
- s3Config.endpoint = endpoint;
270
- // Match C++ native client: use path-style addressing for custom
271
- // endpoints (s3_utils.hpp:329 — use_virtual_addressing=false)
272
- s3Config.forcePathStyle = true;
273
- }
274
207
  const dbg = !!process.env.DEEPLAKE_STORAGE_DEBUG;
275
- if (dbg) {
276
- console.log(`[storage] clientFactory type=${type} mode=${mode ?? 'r'} region=${region} creds=${resolvedCreds ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
277
- }
278
- const rotatingClient = new RotatingS3Client(s3Config, region, endpoint, refreshMeta, dbg);
279
208
  let result;
280
- if (mode === 'w') {
281
- result = createWriter(rotatingClient, wasmModule);
209
+ let destroyable = null;
210
+ if (type === 'aws') {
211
+ const built = buildS3Client(config, wasmModule, dbg);
212
+ destroyable = built.rotatingClient;
213
+ if (mode === 'w') {
214
+ result = createS3Writer(built.rotatingClient, wasmModule);
215
+ }
216
+ else {
217
+ result = createS3Getter(built.rotatingClient, wasmModule);
218
+ }
219
+ }
220
+ else if (type === 'gcs') {
221
+ const rotatingCreds = buildGcsCredentials(config, wasmModule, dbg);
222
+ if (mode === 'w') {
223
+ result = createGcsWriter(rotatingCreds, wasmModule);
224
+ }
225
+ else {
226
+ result = createGcsGetter(rotatingCreds, wasmModule);
227
+ }
228
+ }
229
+ else if (type === 'azure') {
230
+ const rotatingCreds = buildAzureCredentials(config, wasmModule, dbg);
231
+ if (mode === 'w') {
232
+ result = createAzureWriter(rotatingCreds, wasmModule);
233
+ }
234
+ else {
235
+ result = createAzureGetter(rotatingCreds, wasmModule);
236
+ }
282
237
  }
283
238
  else {
284
- result = createGetter(rotatingClient, wasmModule);
239
+ throw new Error(`Unsupported storage type: ${type}. Supported types: "aws", "gcs", "azure".`);
285
240
  }
286
241
  if (dbg) {
242
+ console.log(`[storage] clientFactory type=${type} mode=${mode ?? 'r'}`);
287
243
  // Wrap in Proxy to trace ALL method accesses and calls
288
244
  const label = mode === 'w' ? 'writer' : 'reader';
289
245
  result = new Proxy(result, {
@@ -291,13 +247,13 @@ function createStorageFactory(wasmModule) {
291
247
  const val = target[prop];
292
248
  if (typeof val === 'function') {
293
249
  return function (...args) {
294
- console.log(`[storage:${label}:${prop}] called with ${args.length} args`);
250
+ console.log(`[storage:${type}:${label}:${prop}] called with ${args.length} args`);
295
251
  try {
296
252
  const r = val.apply(target, args);
297
253
  return r;
298
254
  }
299
255
  catch (e) {
300
- console.log(`[storage:${label}:${prop}] threw: ${e.message}`);
256
+ console.log(`[storage:${type}:${label}:${prop}] threw: ${e.message}`);
301
257
  throw e;
302
258
  }
303
259
  };
@@ -313,14 +269,76 @@ function createStorageFactory(wasmModule) {
313
269
  // when the C++ dataset handle is freed.
314
270
  result._destroy = () => {
315
271
  _liveObjects.delete(result);
316
- if (rotatingClient && typeof rotatingClient.destroy === 'function') {
317
- rotatingClient.destroy();
272
+ if (destroyable && typeof destroyable.destroy === 'function') {
273
+ destroyable.destroy();
318
274
  }
319
275
  };
320
276
  return result;
321
277
  };
322
278
  }
323
- function removeScheme(key) {
279
+ // ---------------------------------------------------------------------------
280
+ // S3 client builder
281
+ // ---------------------------------------------------------------------------
282
+ function buildS3Client(config, wasmModule, dbg) {
283
+ const credentials = config?.credentials;
284
+ const region = config?.region ||
285
+ process.env.AWS_REGION ||
286
+ process.env.AWS_DEFAULT_REGION ||
287
+ 'us-east-1';
288
+ const endpoint = config?.endpoint ?? undefined;
289
+ // Resolve credentials: prefer config from C++, fall back to process.env.
290
+ let resolvedCreds;
291
+ if (credentials?.accessKeyId && credentials?.secretAccessKey) {
292
+ resolvedCreds = {
293
+ accessKeyId: credentials.accessKeyId,
294
+ secretAccessKey: credentials.secretAccessKey,
295
+ sessionToken: credentials.sessionToken,
296
+ };
297
+ }
298
+ else if (process.env.AWS_ACCESS_KEY_ID &&
299
+ process.env.AWS_SECRET_ACCESS_KEY) {
300
+ resolvedCreds = {
301
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
302
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
303
+ sessionToken: process.env.AWS_SESSION_TOKEN,
304
+ };
305
+ }
306
+ const refresh = config?.refresh;
307
+ let refreshMeta = null;
308
+ if (refresh?.orgId && refresh?.token && (refresh?.dsName || refresh?.credsKey)) {
309
+ refreshMeta = {
310
+ orgId: refresh.orgId,
311
+ dsName: refresh.dsName,
312
+ credsKey: refresh.credsKey,
313
+ token: refresh.token,
314
+ apiEndpoint: resolveApiEndpoint(wasmModule),
315
+ };
316
+ }
317
+ const s3Config = {
318
+ region,
319
+ credentials: resolvedCreds,
320
+ requestHandler: new (require('@smithy/node-http-handler').NodeHttpHandler)({
321
+ requestTimeout: 8_000,
322
+ connectionTimeout: 8_000,
323
+ }),
324
+ maxAttempts: 30,
325
+ disableHostPrefix: false,
326
+ followRegionRedirects: true,
327
+ };
328
+ if (endpoint) {
329
+ s3Config.endpoint = endpoint;
330
+ s3Config.forcePathStyle = true;
331
+ }
332
+ if (dbg) {
333
+ console.log(`[storage:s3] region=${region} creds=${resolvedCreds ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
334
+ }
335
+ const rotatingClient = new RotatingS3Client(s3Config, region, endpoint, refreshMeta, dbg);
336
+ return { rotatingClient };
337
+ }
338
+ // ---------------------------------------------------------------------------
339
+ // S3 helpers
340
+ // ---------------------------------------------------------------------------
341
+ function removeS3Scheme(key) {
324
342
  if (key.startsWith('s3://')) {
325
343
  return key.substring(5);
326
344
  }
@@ -335,11 +353,11 @@ function splitBucketPrefix(root) {
335
353
  const epos = root.lastIndexOf('/');
336
354
  return [root.substring(0, pos), root.substring(pos + 1, epos)];
337
355
  }
338
- function createGetter(rotatingClient, wasmModule) {
356
+ function createS3Getter(rotatingClient, wasmModule) {
339
357
  const debug = !!process.env.DEEPLAKE_STORAGE_DEBUG;
340
358
  const getter = {
341
359
  generateGetPresignedUrl(key, completion) {
342
- key = removeScheme(key);
360
+ key = removeS3Scheme(key);
343
361
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
344
362
  const [bucket, objKey] = splitBucketKey(key);
345
363
  rotatingClient.getClient().then((client) => (0, s3_request_presigner_1.getSignedUrl)(client, new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: objKey }), {
@@ -355,7 +373,7 @@ function createGetter(rotatingClient, wasmModule) {
355
373
  }));
356
374
  },
357
375
  exists(key, completion) {
358
- key = removeScheme(key);
376
+ key = removeS3Scheme(key);
359
377
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
360
378
  const [bucket, objKey] = splitBucketKey(key);
361
379
  if (debug) {
@@ -373,7 +391,7 @@ function createGetter(rotatingClient, wasmModule) {
373
391
  }));
374
392
  },
375
393
  download(key, range, index, completion) {
376
- key = removeScheme(key);
394
+ key = removeS3Scheme(key);
377
395
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
378
396
  const [bucket, objKey] = splitBucketKey(key);
379
397
  if (debug) {
@@ -529,11 +547,11 @@ function createGetter(rotatingClient, wasmModule) {
529
547
  };
530
548
  return getter;
531
549
  }
532
- function createWriter(rotatingClient, wasmModule) {
550
+ function createS3Writer(rotatingClient, wasmModule) {
533
551
  const debug = !!process.env.DEEPLAKE_STORAGE_DEBUG;
534
552
  const writer = {
535
553
  saveWithCompletion(key, content, completion) {
536
- key = removeScheme(key);
554
+ key = removeS3Scheme(key);
537
555
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
538
556
  const [bucket, objKey] = splitBucketKey(key);
539
557
  if (debug) {
@@ -571,7 +589,7 @@ function createWriter(rotatingClient, wasmModule) {
571
589
  * Returns 412 (Precondition Failed) if ETag doesn't match.
572
590
  */
573
591
  saveConditional(key, content, ifMatchEtag, completion) {
574
- key = removeScheme(key);
592
+ key = removeS3Scheme(key);
575
593
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
576
594
  const [bucket, objKey] = splitBucketKey(key);
577
595
  const body = typeof content === 'string' ? Buffer.from(content) : content;
@@ -610,7 +628,7 @@ function createWriter(rotatingClient, wasmModule) {
610
628
  * Returns 412 (Precondition Failed) if the object already exists.
611
629
  */
612
630
  saveExclusive(key, content, completion) {
613
- key = removeScheme(key);
631
+ key = removeS3Scheme(key);
614
632
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
615
633
  const [bucket, objKey] = splitBucketKey(key);
616
634
  const body = typeof content === 'string' ? Buffer.from(content) : content;
@@ -643,7 +661,7 @@ function createWriter(rotatingClient, wasmModule) {
643
661
  }));
644
662
  },
645
663
  remove(key, completion) {
646
- key = removeScheme(key);
664
+ key = removeS3Scheme(key);
647
665
  const c1 = (0, inflight_tracker_1.trackClone)(completion);
648
666
  const [bucket, objKey] = splitBucketKey(key);
649
667
  rotatingClient.getClient().then((client) => client
@@ -668,4 +686,829 @@ function createWriter(rotatingClient, wasmModule) {
668
686
  };
669
687
  return writer;
670
688
  }
689
+ // ---------------------------------------------------------------------------
690
+ // Rotating credentials for GCS and Azure
691
+ // ---------------------------------------------------------------------------
692
+ /**
693
+ * Generic rotating credential holder. Works like RotatingS3Client but
694
+ * stores plain credential objects instead of SDK client instances.
695
+ */
696
+ class RotatingCredentials {
697
+ creds;
698
+ refreshMeta;
699
+ applyCreds;
700
+ createdAt;
701
+ lastFailureAt = 0;
702
+ refreshInProgress = null;
703
+ debug;
704
+ constructor(initial, refreshMeta, applyCreds, debug) {
705
+ this.creds = initial;
706
+ this.refreshMeta = refreshMeta;
707
+ this.applyCreds = applyCreds;
708
+ this.createdAt = Date.now();
709
+ this.debug = debug;
710
+ }
711
+ async get() {
712
+ if (!this.refreshMeta)
713
+ return this.creds;
714
+ const now = Date.now();
715
+ const age = now - this.createdAt;
716
+ if (age <= DEFAULT_CRED_LIFETIME_MS - REFRESH_BUFFER_MS)
717
+ return this.creds;
718
+ if (this.lastFailureAt && now - this.lastFailureAt < REFRESH_FAILURE_COOLDOWN_MS) {
719
+ return this.creds;
720
+ }
721
+ if (this.refreshInProgress) {
722
+ await this.refreshInProgress;
723
+ return this.creds;
724
+ }
725
+ this.refreshInProgress = this.doRefresh();
726
+ try {
727
+ await this.refreshInProgress;
728
+ }
729
+ finally {
730
+ this.refreshInProgress = null;
731
+ }
732
+ return this.creds;
733
+ }
734
+ async doRefresh() {
735
+ const meta = this.refreshMeta;
736
+ if (this.debug) {
737
+ console.log(`[storage] refreshing credentials for ds=${meta.dsName ?? ''} credsKey=${meta.credsKey ?? ''}`);
738
+ }
739
+ try {
740
+ const resp = await fetchCredentials(meta);
741
+ this.creds = this.applyCreds(resp.creds);
742
+ this.createdAt = Date.now();
743
+ this.lastFailureAt = 0;
744
+ if (this.debug) {
745
+ console.log(`[storage] credential refresh succeeded (repo_type=${resp.repoType})`);
746
+ }
747
+ }
748
+ catch (e) {
749
+ this.lastFailureAt = Date.now();
750
+ console.warn(`[storage] credential refresh failed: ${e.message ?? e}`);
751
+ }
752
+ }
753
+ }
754
+ function buildRefreshMeta(config, wasmModule) {
755
+ const refresh = config?.refresh;
756
+ if (refresh?.orgId && refresh?.token && (refresh?.dsName || refresh?.credsKey)) {
757
+ return {
758
+ orgId: refresh.orgId,
759
+ dsName: refresh.dsName,
760
+ credsKey: refresh.credsKey,
761
+ token: refresh.token,
762
+ apiEndpoint: resolveApiEndpoint(wasmModule),
763
+ };
764
+ }
765
+ return null;
766
+ }
767
+ // ---------------------------------------------------------------------------
768
+ // GCS (Google Cloud Storage) — fetch-based, no extra dependencies
769
+ // ---------------------------------------------------------------------------
770
+ /** Convert string or Uint8Array to a value accepted by Node.js fetch as body. */
771
+ function toFetchBody(content) {
772
+ if (typeof content === 'string')
773
+ return Buffer.from(content);
774
+ return Buffer.isBuffer(content) ? content : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
775
+ }
776
+ const GCS_API_BASE = 'https://storage.googleapis.com';
777
+ function buildGcsCredentials(config, wasmModule, dbg) {
778
+ const initial = { accessToken: config?.accessToken ?? '' };
779
+ const refreshMeta = buildRefreshMeta(config, wasmModule);
780
+ if (dbg) {
781
+ console.log(`[storage:gcs] token=${initial.accessToken ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
782
+ }
783
+ return new RotatingCredentials(initial, refreshMeta, (raw) => ({
784
+ accessToken: raw.gcs_oauth_token ?? raw.oauth_token ?? raw.access_token ?? '',
785
+ }), dbg);
786
+ }
787
+ function gcsHeaders(creds) {
788
+ const h = {};
789
+ if (creds.accessToken) {
790
+ h['Authorization'] = `Bearer ${creds.accessToken}`;
791
+ }
792
+ return h;
793
+ }
794
+ function removeGcsScheme(key) {
795
+ if (key.startsWith('gs://'))
796
+ return key.substring(5);
797
+ if (key.startsWith('gcs://'))
798
+ return key.substring(6);
799
+ return key;
800
+ }
801
+ function createGcsGetter(rotatingCreds, wasmModule) {
802
+ const getter = {
803
+ generateGetPresignedUrl(_key, completion) {
804
+ // GCS presigned URLs require service account private keys, which
805
+ // aren't available here. Return empty like the browser implementation.
806
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
807
+ c1.call('');
808
+ c1.delete();
809
+ },
810
+ exists(key, completion) {
811
+ key = removeGcsScheme(key);
812
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
813
+ rotatingCreds.get().then((creds) => {
814
+ fetch(`${GCS_API_BASE}/${key}`, {
815
+ method: 'HEAD',
816
+ headers: gcsHeaders(creds),
817
+ })
818
+ .then((resp) => {
819
+ if (resp.ok) {
820
+ c1.call(Number(resp.headers.get('content-length') ?? 0));
821
+ }
822
+ else {
823
+ c1.call(-1);
824
+ }
825
+ c1.delete();
826
+ })
827
+ .catch(() => {
828
+ c1.call(-1);
829
+ c1.delete();
830
+ });
831
+ });
832
+ },
833
+ download(key, range, index, completion) {
834
+ key = removeGcsScheme(key);
835
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
836
+ rotatingCreds.get().then((creds) => {
837
+ const headers = gcsHeaders(creds);
838
+ if (range)
839
+ headers['Range'] = range;
840
+ const controller = new AbortController();
841
+ wasmModule.storeJsObject(index, { abort() { controller.abort(); } });
842
+ fetch(`${GCS_API_BASE}/${key}`, { headers, signal: controller.signal })
843
+ .then(async (resp) => {
844
+ if (!resp.ok) {
845
+ c1.call({ responseCode: resp.status, message: resp.statusText || 'download_failed' });
846
+ c1.delete();
847
+ return;
848
+ }
849
+ const buf = Buffer.from(await resp.arrayBuffer());
850
+ const view = wasmModule.provideResponseBuffer(index, buf.length);
851
+ view.set(buf);
852
+ c1.call({ responseCode: 200 });
853
+ c1.delete();
854
+ })
855
+ .catch((err) => {
856
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
857
+ c1.delete();
858
+ });
859
+ });
860
+ },
861
+ list(root, key, startAfter, completion) {
862
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
863
+ const [bucket, prefix] = splitBucketKey(root);
864
+ const finalPrefix = (prefix.endsWith('/') ? prefix : `${prefix}/`) + key;
865
+ const results = [];
866
+ rotatingCreds.get().then((creds) => {
867
+ const impl = (pageToken) => {
868
+ let url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o?prefix=${encodeURIComponent(finalPrefix)}`;
869
+ if (startAfter)
870
+ url += `&startOffset=${encodeURIComponent(startAfter)}`;
871
+ if (pageToken)
872
+ url += `&pageToken=${encodeURIComponent(pageToken)}`;
873
+ fetch(url, { headers: gcsHeaders(creds) })
874
+ .then((resp) => {
875
+ if (!resp.ok) {
876
+ c1.call({ responseCode: resp.status, message: 'list_request_failed' });
877
+ c1.delete();
878
+ return;
879
+ }
880
+ return resp.json();
881
+ })
882
+ .then((json) => {
883
+ if (!json)
884
+ return; // error already handled
885
+ for (const item of json.items ?? []) {
886
+ results.push({
887
+ path: item.name.substring(prefix.length + 1),
888
+ size: Number(item.size),
889
+ lastModified: new Date(item.updated),
890
+ etag: item.etag ?? '',
891
+ });
892
+ }
893
+ if (json.nextPageToken) {
894
+ impl(json.nextPageToken);
895
+ }
896
+ else {
897
+ c1.call({ responseCode: 200, data: results });
898
+ c1.delete();
899
+ }
900
+ })
901
+ .catch((err) => {
902
+ c1.call({ responseCode: 1000, message: err.message ?? 'list_request_failed' });
903
+ c1.delete();
904
+ });
905
+ };
906
+ impl();
907
+ });
908
+ },
909
+ listDirs(root, prefix, completion) {
910
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
911
+ const [bucket, rootPrefix] = splitBucketPrefix(root);
912
+ const finalPrefix = (rootPrefix.endsWith('/') ? rootPrefix : `${rootPrefix}/`) + prefix;
913
+ rotatingCreds.get().then((creds) => {
914
+ const url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o?prefix=${encodeURIComponent(finalPrefix)}&delimiter=/`;
915
+ fetch(url, { headers: gcsHeaders(creds) })
916
+ .then((resp) => {
917
+ if (!resp.ok) {
918
+ c1.call({ responseCode: resp.status, message: 'list_dirs_failed' });
919
+ c1.delete();
920
+ return;
921
+ }
922
+ return resp.json();
923
+ })
924
+ .then((json) => {
925
+ if (!json)
926
+ return;
927
+ const dirs = (json.prefixes ?? []);
928
+ c1.call({ responseCode: 200, data: dirs });
929
+ c1.delete();
930
+ })
931
+ .catch((err) => {
932
+ c1.call({ responseCode: 1000, message: err.message ?? 'list_dirs_failed' });
933
+ c1.delete();
934
+ });
935
+ });
936
+ },
937
+ metadata(root, key, completion) {
938
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
939
+ const [bucket, prefix] = splitBucketPrefix(root);
940
+ const objName = (prefix.endsWith('/') ? prefix : `${prefix}/`) + key;
941
+ rotatingCreds.get().then((creds) => {
942
+ const url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o/${encodeURIComponent(objName)}`;
943
+ fetch(url, { headers: gcsHeaders(creds) })
944
+ .then((resp) => {
945
+ if (!resp.ok) {
946
+ c1.call({ responseCode: resp.status, message: 'metadata_request_failed' });
947
+ c1.delete();
948
+ return;
949
+ }
950
+ return resp.json();
951
+ })
952
+ .then((json) => {
953
+ if (!json)
954
+ return;
955
+ c1.call({
956
+ responseCode: 200,
957
+ data: {
958
+ path: json.name.substring(prefix.length + 1),
959
+ size: Number(json.size),
960
+ lastModified: new Date(json.updated),
961
+ etag: json.etag ?? '',
962
+ },
963
+ });
964
+ c1.delete();
965
+ })
966
+ .catch((err) => {
967
+ c1.call({ responseCode: 1000, message: err.message ?? 'metadata_request_failed' });
968
+ c1.delete();
969
+ });
970
+ });
971
+ },
972
+ copyGetter() {
973
+ return getter;
974
+ },
975
+ };
976
+ return getter;
977
+ }
978
+ function createGcsWriter(rotatingCreds, _wasmModule) {
979
+ const writer = {
980
+ saveWithCompletion(key, content, completion) {
981
+ key = removeGcsScheme(key);
982
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
983
+ const body = toFetchBody(content);
984
+ rotatingCreds.get().then((creds) => {
985
+ // Upload via GCS JSON API resumable or simple upload
986
+ const [bucket, objKey] = splitBucketKey(key);
987
+ const url = `${GCS_API_BASE}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(objKey)}`;
988
+ fetch(url, {
989
+ method: 'POST',
990
+ headers: { ...gcsHeaders(creds), 'Content-Type': 'application/octet-stream' },
991
+ body,
992
+ })
993
+ .then(async (resp) => {
994
+ if (!resp.ok) {
995
+ const text = await resp.text().catch(() => '');
996
+ c1.call({ responseCode: resp.status, message: text || 'upload_failed' });
997
+ c1.delete();
998
+ return;
999
+ }
1000
+ const json = await resp.json().catch(() => ({}));
1001
+ c1.call({ responseCode: 200, etag: json.etag ?? '' });
1002
+ c1.delete();
1003
+ })
1004
+ .catch((err) => {
1005
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1006
+ c1.delete();
1007
+ });
1008
+ });
1009
+ },
1010
+ saveConditional(key, content, ifMatchEtag, completion) {
1011
+ key = removeGcsScheme(key);
1012
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1013
+ const body = toFetchBody(content);
1014
+ const [bucket, objKey] = splitBucketKey(key);
1015
+ const normalizedEtag = ifMatchEtag.replace(/"/g, '');
1016
+ rotatingCreds.get().then((creds) => {
1017
+ // First, fetch the object's metadata to get its generation number.
1018
+ // GCS CAS requires ifGenerationMatch=<generation>, not HTTP If-Match.
1019
+ const metaUrl = `${GCS_API_BASE}/storage/v1/b/${bucket}/o/${encodeURIComponent(objKey)}`;
1020
+ fetch(metaUrl, { headers: gcsHeaders(creds) })
1021
+ .then((resp) => {
1022
+ if (!resp.ok) {
1023
+ // Object doesn't exist or other error — CAS fails
1024
+ c1.call({ responseCode: resp.status === 404 ? 412 : resp.status, message: 'conditional_write_failed' });
1025
+ c1.delete();
1026
+ return;
1027
+ }
1028
+ return resp.json();
1029
+ })
1030
+ .then((meta) => {
1031
+ if (!meta)
1032
+ return; // error already handled
1033
+ const currentEtag = (meta.etag ?? '').replace(/"/g, '');
1034
+ if (currentEtag !== normalizedEtag) {
1035
+ // ETag mismatch — another writer changed the object
1036
+ c1.call({ responseCode: 412, message: 'conditional_write_failed' });
1037
+ c1.delete();
1038
+ return;
1039
+ }
1040
+ // ETag matches — upload with ifGenerationMatch to guarantee atomicity
1041
+ const uploadUrl = `${GCS_API_BASE}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(objKey)}&ifGenerationMatch=${meta.generation}`;
1042
+ fetch(uploadUrl, {
1043
+ method: 'POST',
1044
+ headers: {
1045
+ ...gcsHeaders(creds),
1046
+ 'Content-Type': 'application/octet-stream',
1047
+ },
1048
+ body,
1049
+ })
1050
+ .then(async (resp) => {
1051
+ if (!resp.ok) {
1052
+ c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'conditional_write_failed' });
1053
+ c1.delete();
1054
+ return;
1055
+ }
1056
+ const json = await resp.json().catch(() => ({}));
1057
+ c1.call({ responseCode: 200, etag: json.etag ?? '' });
1058
+ c1.delete();
1059
+ })
1060
+ .catch((err) => {
1061
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1062
+ c1.delete();
1063
+ });
1064
+ })
1065
+ .catch((err) => {
1066
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1067
+ c1.delete();
1068
+ });
1069
+ });
1070
+ },
1071
+ saveExclusive(key, content, completion) {
1072
+ key = removeGcsScheme(key);
1073
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1074
+ const body = toFetchBody(content);
1075
+ const [bucket, objKey] = splitBucketKey(key);
1076
+ rotatingCreds.get().then((creds) => {
1077
+ // ifGenerationMatch=0 means "only if the object does not exist"
1078
+ const url = `${GCS_API_BASE}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(objKey)}&ifGenerationMatch=0`;
1079
+ fetch(url, {
1080
+ method: 'POST',
1081
+ headers: { ...gcsHeaders(creds), 'Content-Type': 'application/octet-stream' },
1082
+ body,
1083
+ })
1084
+ .then(async (resp) => {
1085
+ if (!resp.ok) {
1086
+ c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'exclusive_write_failed' });
1087
+ c1.delete();
1088
+ return;
1089
+ }
1090
+ const json = await resp.json().catch(() => ({}));
1091
+ c1.call({ responseCode: 200, etag: json.etag ?? '' });
1092
+ c1.delete();
1093
+ })
1094
+ .catch((err) => {
1095
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1096
+ c1.delete();
1097
+ });
1098
+ });
1099
+ },
1100
+ remove(key, completion) {
1101
+ key = removeGcsScheme(key);
1102
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1103
+ const [bucket, objKey] = splitBucketKey(key);
1104
+ rotatingCreds.get().then((creds) => {
1105
+ const url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o/${encodeURIComponent(objKey)}`;
1106
+ fetch(url, {
1107
+ method: 'DELETE',
1108
+ headers: gcsHeaders(creds),
1109
+ })
1110
+ .then((resp) => {
1111
+ c1.call({ responseCode: resp.ok ? 200 : resp.status });
1112
+ c1.delete();
1113
+ })
1114
+ .catch((err) => {
1115
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1116
+ c1.delete();
1117
+ });
1118
+ });
1119
+ },
1120
+ copyWriter() {
1121
+ return writer;
1122
+ },
1123
+ };
1124
+ return writer;
1125
+ }
1126
+ function buildAzureCredentials(config, wasmModule, dbg) {
1127
+ const initial = {
1128
+ accountName: config?.accountName ?? '',
1129
+ sasToken: config?.blobSasToken ?? '',
1130
+ containerName: config?.containerName ?? '',
1131
+ };
1132
+ const refreshMeta = buildRefreshMeta(config, wasmModule);
1133
+ if (dbg) {
1134
+ console.log(`[storage:azure] account=${initial.accountName} container=${initial.containerName} sas=${initial.sasToken ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
1135
+ }
1136
+ return new RotatingCredentials(initial, refreshMeta, (raw) => ({
1137
+ accountName: raw.account_name ?? initial.accountName,
1138
+ sasToken: raw.sas_token ?? '',
1139
+ containerName: raw.container_name ?? initial.containerName,
1140
+ }), dbg);
1141
+ }
1142
+ function azureBlobUrl(creds, blobPath) {
1143
+ const base = `https://${creds.accountName}.blob.core.windows.net/${creds.containerName}/${blobPath}`;
1144
+ return creds.sasToken ? `${base}?${creds.sasToken}` : base;
1145
+ }
1146
+ function azureListUrl(creds, params) {
1147
+ const base = `https://${creds.accountName}.blob.core.windows.net/${creds.containerName}`;
1148
+ const sep = creds.sasToken ? `?${creds.sasToken}&` : '?';
1149
+ return `${base}${sep}${params}`;
1150
+ }
1151
+ function removeAzureScheme(key) {
1152
+ if (key.startsWith('azure://'))
1153
+ return key.substring(8);
1154
+ if (key.startsWith('az://'))
1155
+ return key.substring(5);
1156
+ return key;
1157
+ }
1158
+ /**
1159
+ * Parse an Azure blob path. The C++ side sends paths as:
1160
+ * accountName/containerName/blob/path
1161
+ * We need to extract just the blob path portion since accountName
1162
+ * and containerName come from the credentials.
1163
+ */
1164
+ function azureBlobPath(key, creds) {
1165
+ const prefix = `${creds.accountName}/${creds.containerName}/`;
1166
+ if (key.startsWith(prefix)) {
1167
+ return key.substring(prefix.length);
1168
+ }
1169
+ // May already be just the blob path
1170
+ return key;
1171
+ }
1172
+ /** Parse a minimal XML list-blobs response */
1173
+ function parseAzureListBlobsXml(xml) {
1174
+ const blobs = [];
1175
+ const blobRegex = /<Blob>[\s\S]*?<\/Blob>/g;
1176
+ let match;
1177
+ while ((match = blobRegex.exec(xml)) !== null) {
1178
+ const blob = match[0];
1179
+ const name = blob.match(/<Name>([\s\S]*?)<\/Name>/)?.[1] ?? '';
1180
+ const size = Number(blob.match(/<Content-Length>([\s\S]*?)<\/Content-Length>/)?.[1] ?? 0);
1181
+ const lastMod = blob.match(/<Last-Modified>([\s\S]*?)<\/Last-Modified>/)?.[1] ?? '';
1182
+ const etag = (blob.match(/<Etag>([\s\S]*?)<\/Etag>/)?.[1] ?? '').replace(/"/g, '');
1183
+ blobs.push({ name, size, lastModified: new Date(lastMod), etag });
1184
+ }
1185
+ const nextMarker = xml.match(/<NextMarker>([\s\S]*?)<\/NextMarker>/)?.[1] || undefined;
1186
+ return { blobs, nextMarker };
1187
+ }
1188
+ /** Parse directory prefixes from Azure list-blobs XML with delimiter */
1189
+ function parseAzureBlobPrefixes(xml) {
1190
+ const prefixes = [];
1191
+ const prefixRegex = /<BlobPrefix><Name>([\s\S]*?)<\/Name><\/BlobPrefix>/g;
1192
+ let match;
1193
+ while ((match = prefixRegex.exec(xml)) !== null) {
1194
+ prefixes.push(match[1]);
1195
+ }
1196
+ return prefixes;
1197
+ }
1198
+ function createAzureGetter(rotatingCreds, wasmModule) {
1199
+ const getter = {
1200
+ generateGetPresignedUrl(key, completion) {
1201
+ // With SAS token, the URL is already "presigned"
1202
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1203
+ rotatingCreds.get().then((creds) => {
1204
+ key = removeAzureScheme(key);
1205
+ const blobP = azureBlobPath(key, creds);
1206
+ c1.call(azureBlobUrl(creds, blobP));
1207
+ c1.delete();
1208
+ });
1209
+ },
1210
+ exists(key, completion) {
1211
+ key = removeAzureScheme(key);
1212
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1213
+ rotatingCreds.get().then((creds) => {
1214
+ const blobP = azureBlobPath(key, creds);
1215
+ fetch(azureBlobUrl(creds, blobP), { method: 'HEAD' })
1216
+ .then((resp) => {
1217
+ if (resp.ok) {
1218
+ c1.call(Number(resp.headers.get('content-length') ?? 0));
1219
+ }
1220
+ else {
1221
+ c1.call(-1);
1222
+ }
1223
+ c1.delete();
1224
+ })
1225
+ .catch(() => {
1226
+ c1.call(-1);
1227
+ c1.delete();
1228
+ });
1229
+ });
1230
+ },
1231
+ download(key, range, index, completion) {
1232
+ key = removeAzureScheme(key);
1233
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1234
+ rotatingCreds.get().then((creds) => {
1235
+ const blobP = azureBlobPath(key, creds);
1236
+ const headers = {};
1237
+ if (range)
1238
+ headers['Range'] = range;
1239
+ // Azure requires this header for range requests
1240
+ if (range)
1241
+ headers['x-ms-range'] = range;
1242
+ const controller = new AbortController();
1243
+ wasmModule.storeJsObject(index, { abort() { controller.abort(); } });
1244
+ fetch(azureBlobUrl(creds, blobP), { headers, signal: controller.signal })
1245
+ .then(async (resp) => {
1246
+ if (!resp.ok) {
1247
+ c1.call({ responseCode: resp.status, message: resp.statusText || 'download_failed' });
1248
+ c1.delete();
1249
+ return;
1250
+ }
1251
+ const buf = Buffer.from(await resp.arrayBuffer());
1252
+ const view = wasmModule.provideResponseBuffer(index, buf.length);
1253
+ view.set(buf);
1254
+ c1.call({ responseCode: 200 });
1255
+ c1.delete();
1256
+ })
1257
+ .catch((err) => {
1258
+ if (err.name === 'AbortError') {
1259
+ c1.call({ responseCode: 1000, message: 'aborted' });
1260
+ }
1261
+ else {
1262
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1263
+ }
1264
+ c1.delete();
1265
+ });
1266
+ });
1267
+ },
1268
+ list(root, key, _startAfter, completion) {
1269
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1270
+ const results = [];
1271
+ rotatingCreds.get().then((creds) => {
1272
+ // root comes as accountName/containerName/prefix/
1273
+ // We need to extract the prefix portion
1274
+ const rootClean = removeAzureScheme(root);
1275
+ const prefixPart = azureBlobPath(rootClean, creds);
1276
+ const finalPrefix = (prefixPart.endsWith('/') ? prefixPart : `${prefixPart}/`) + key;
1277
+ const impl = (marker) => {
1278
+ let params = `restype=container&comp=list&prefix=${encodeURIComponent(finalPrefix)}`;
1279
+ if (marker)
1280
+ params += `&marker=${encodeURIComponent(marker)}`;
1281
+ fetch(azureListUrl(creds, params))
1282
+ .then((resp) => {
1283
+ if (!resp.ok) {
1284
+ c1.call({ responseCode: resp.status, message: 'list_request_failed' });
1285
+ c1.delete();
1286
+ return;
1287
+ }
1288
+ return resp.text();
1289
+ })
1290
+ .then((xml) => {
1291
+ if (!xml)
1292
+ return;
1293
+ const parsed = parseAzureListBlobsXml(xml);
1294
+ for (const blob of parsed.blobs) {
1295
+ results.push({
1296
+ path: blob.name.substring(prefixPart.length + 1),
1297
+ size: blob.size,
1298
+ lastModified: blob.lastModified,
1299
+ etag: blob.etag,
1300
+ });
1301
+ }
1302
+ if (parsed.nextMarker) {
1303
+ impl(parsed.nextMarker);
1304
+ }
1305
+ else {
1306
+ c1.call({ responseCode: 200, data: results });
1307
+ c1.delete();
1308
+ }
1309
+ })
1310
+ .catch((err) => {
1311
+ c1.call({ responseCode: 1000, message: err.message ?? 'list_request_failed' });
1312
+ c1.delete();
1313
+ });
1314
+ };
1315
+ impl();
1316
+ });
1317
+ },
1318
+ listDirs(root, prefix, completion) {
1319
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1320
+ rotatingCreds.get().then((creds) => {
1321
+ const rootClean = removeAzureScheme(root);
1322
+ const rootPrefix = azureBlobPath(rootClean, creds);
1323
+ const finalPrefix = (rootPrefix.endsWith('/') ? rootPrefix : `${rootPrefix}/`) + prefix;
1324
+ let params = `restype=container&comp=list&prefix=${encodeURIComponent(finalPrefix)}&delimiter=/`;
1325
+ fetch(azureListUrl(creds, params))
1326
+ .then((resp) => {
1327
+ if (!resp.ok) {
1328
+ c1.call({ responseCode: resp.status, message: 'list_dirs_failed' });
1329
+ c1.delete();
1330
+ return;
1331
+ }
1332
+ return resp.text();
1333
+ })
1334
+ .then((xml) => {
1335
+ if (!xml)
1336
+ return;
1337
+ const dirs = parseAzureBlobPrefixes(xml);
1338
+ c1.call({ responseCode: 200, data: dirs });
1339
+ c1.delete();
1340
+ })
1341
+ .catch((err) => {
1342
+ c1.call({ responseCode: 1000, message: err.message ?? 'list_dirs_failed' });
1343
+ c1.delete();
1344
+ });
1345
+ });
1346
+ },
1347
+ metadata(root, key, completion) {
1348
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1349
+ rotatingCreds.get().then((creds) => {
1350
+ const rootClean = removeAzureScheme(root);
1351
+ const rootPrefix = azureBlobPath(rootClean, creds);
1352
+ const blobName = (rootPrefix.endsWith('/') ? rootPrefix : `${rootPrefix}/`) + key;
1353
+ // Use a list call with exact prefix to get metadata
1354
+ let params = `restype=container&comp=list&prefix=${encodeURIComponent(blobName)}`;
1355
+ fetch(azureListUrl(creds, params))
1356
+ .then((resp) => {
1357
+ if (!resp.ok) {
1358
+ c1.call({ responseCode: resp.status, message: 'metadata_request_failed' });
1359
+ c1.delete();
1360
+ return;
1361
+ }
1362
+ return resp.text();
1363
+ })
1364
+ .then((xml) => {
1365
+ if (!xml)
1366
+ return;
1367
+ const parsed = parseAzureListBlobsXml(xml);
1368
+ if (parsed.blobs.length === 0) {
1369
+ c1.call({ responseCode: 404, message: 'resource_not_found' });
1370
+ c1.delete();
1371
+ return;
1372
+ }
1373
+ const blob = parsed.blobs[0];
1374
+ c1.call({
1375
+ responseCode: 200,
1376
+ data: {
1377
+ path: blob.name.substring(rootPrefix.length + 1),
1378
+ size: blob.size,
1379
+ lastModified: blob.lastModified,
1380
+ etag: blob.etag,
1381
+ },
1382
+ });
1383
+ c1.delete();
1384
+ })
1385
+ .catch((err) => {
1386
+ c1.call({ responseCode: 1000, message: err.message ?? 'metadata_request_failed' });
1387
+ c1.delete();
1388
+ });
1389
+ });
1390
+ },
1391
+ copyGetter() {
1392
+ return getter;
1393
+ },
1394
+ };
1395
+ return getter;
1396
+ }
1397
+ function createAzureWriter(rotatingCreds, _wasmModule) {
1398
+ const writer = {
1399
+ saveWithCompletion(key, content, completion) {
1400
+ key = removeAzureScheme(key);
1401
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1402
+ const body = toFetchBody(content);
1403
+ rotatingCreds.get().then((creds) => {
1404
+ const blobP = azureBlobPath(key, creds);
1405
+ fetch(azureBlobUrl(creds, blobP), {
1406
+ method: 'PUT',
1407
+ headers: {
1408
+ 'x-ms-blob-type': 'BlockBlob',
1409
+ 'Content-Type': 'application/octet-stream',
1410
+ },
1411
+ body,
1412
+ })
1413
+ .then((resp) => {
1414
+ const etag = (resp.headers.get('etag') ?? '').replace(/"/g, '');
1415
+ if (resp.ok) {
1416
+ c1.call({ responseCode: resp.status, etag });
1417
+ }
1418
+ else {
1419
+ c1.call({ responseCode: resp.status, message: 'upload_failed' });
1420
+ }
1421
+ c1.delete();
1422
+ })
1423
+ .catch((err) => {
1424
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1425
+ c1.delete();
1426
+ });
1427
+ });
1428
+ },
1429
+ saveConditional(key, content, ifMatchEtag, completion) {
1430
+ key = removeAzureScheme(key);
1431
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1432
+ const body = toFetchBody(content);
1433
+ const quotedEtag = ifMatchEtag.startsWith('"') ? ifMatchEtag : `"${ifMatchEtag}"`;
1434
+ rotatingCreds.get().then((creds) => {
1435
+ const blobP = azureBlobPath(key, creds);
1436
+ fetch(azureBlobUrl(creds, blobP), {
1437
+ method: 'PUT',
1438
+ headers: {
1439
+ 'x-ms-blob-type': 'BlockBlob',
1440
+ 'Content-Type': 'application/octet-stream',
1441
+ 'If-Match': quotedEtag,
1442
+ },
1443
+ body,
1444
+ })
1445
+ .then((resp) => {
1446
+ const etag = (resp.headers.get('etag') ?? '').replace(/"/g, '');
1447
+ if (resp.ok) {
1448
+ c1.call({ responseCode: resp.status, etag });
1449
+ }
1450
+ else {
1451
+ c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'conditional_write_failed' });
1452
+ }
1453
+ c1.delete();
1454
+ })
1455
+ .catch((err) => {
1456
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1457
+ c1.delete();
1458
+ });
1459
+ });
1460
+ },
1461
+ saveExclusive(key, content, completion) {
1462
+ key = removeAzureScheme(key);
1463
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1464
+ const body = toFetchBody(content);
1465
+ rotatingCreds.get().then((creds) => {
1466
+ const blobP = azureBlobPath(key, creds);
1467
+ fetch(azureBlobUrl(creds, blobP), {
1468
+ method: 'PUT',
1469
+ headers: {
1470
+ 'x-ms-blob-type': 'BlockBlob',
1471
+ 'Content-Type': 'application/octet-stream',
1472
+ 'If-None-Match': '*',
1473
+ },
1474
+ body,
1475
+ })
1476
+ .then((resp) => {
1477
+ const etag = (resp.headers.get('etag') ?? '').replace(/"/g, '');
1478
+ if (resp.ok) {
1479
+ c1.call({ responseCode: resp.status, etag });
1480
+ }
1481
+ else {
1482
+ c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'exclusive_write_failed' });
1483
+ }
1484
+ c1.delete();
1485
+ })
1486
+ .catch((err) => {
1487
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1488
+ c1.delete();
1489
+ });
1490
+ });
1491
+ },
1492
+ remove(key, completion) {
1493
+ key = removeAzureScheme(key);
1494
+ const c1 = (0, inflight_tracker_1.trackClone)(completion);
1495
+ rotatingCreds.get().then((creds) => {
1496
+ const blobP = azureBlobPath(key, creds);
1497
+ fetch(azureBlobUrl(creds, blobP), { method: 'DELETE' })
1498
+ .then((resp) => {
1499
+ c1.call({ responseCode: resp.ok ? 200 : resp.status });
1500
+ c1.delete();
1501
+ })
1502
+ .catch((err) => {
1503
+ c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
1504
+ c1.delete();
1505
+ });
1506
+ });
1507
+ },
1508
+ copyWriter() {
1509
+ return writer;
1510
+ },
1511
+ };
1512
+ return writer;
1513
+ }
671
1514
  //# sourceMappingURL=storage.js.map