@keetanetwork/anchor 0.0.39 → 0.0.41

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.
Files changed (95) hide show
  1. package/client/index.d.ts +6 -0
  2. package/client/index.d.ts.map +1 -1
  3. package/client/index.js +7 -0
  4. package/client/index.js.map +1 -1
  5. package/lib/block-listener.d.ts +93 -0
  6. package/lib/block-listener.d.ts.map +1 -0
  7. package/lib/block-listener.js +259 -0
  8. package/lib/block-listener.js.map +1 -0
  9. package/lib/error.d.ts.map +1 -1
  10. package/lib/error.js +3 -1
  11. package/lib/error.js.map +1 -1
  12. package/lib/http-server/index.d.ts +14 -1
  13. package/lib/http-server/index.d.ts.map +1 -1
  14. package/lib/http-server/index.js +86 -7
  15. package/lib/http-server/index.js.map +1 -1
  16. package/lib/queue/index.d.ts +20 -5
  17. package/lib/queue/index.d.ts.map +1 -1
  18. package/lib/queue/index.js +52 -17
  19. package/lib/queue/index.js.map +1 -1
  20. package/lib/resolver.d.ts +57 -0
  21. package/lib/resolver.d.ts.map +1 -1
  22. package/lib/resolver.js +864 -250
  23. package/lib/resolver.js.map +1 -1
  24. package/npm-shrinkwrap.json +4 -4
  25. package/package.json +1 -1
  26. package/services/asset-movement/client.d.ts +9 -2
  27. package/services/asset-movement/client.d.ts.map +1 -1
  28. package/services/asset-movement/client.js +35 -2
  29. package/services/asset-movement/client.js.map +1 -1
  30. package/services/asset-movement/common.d.ts +1 -0
  31. package/services/asset-movement/common.d.ts.map +1 -1
  32. package/services/asset-movement/common.js +75 -0
  33. package/services/asset-movement/common.js.map +1 -1
  34. package/services/asset-movement/server.d.ts +0 -10
  35. package/services/asset-movement/server.d.ts.map +1 -1
  36. package/services/asset-movement/server.js +0 -2
  37. package/services/asset-movement/server.js.map +1 -1
  38. package/services/fx/common.d.ts +1 -1
  39. package/services/fx/common.js.map +1 -1
  40. package/services/fx/server.d.ts +37 -6
  41. package/services/fx/server.d.ts.map +1 -1
  42. package/services/fx/server.js +207 -66
  43. package/services/fx/server.js.map +1 -1
  44. package/services/storage/client.d.ts +332 -0
  45. package/services/storage/client.d.ts.map +1 -0
  46. package/services/storage/client.js +1078 -0
  47. package/services/storage/client.js.map +1 -0
  48. package/services/storage/clients/contacts.d.ts +94 -0
  49. package/services/storage/clients/contacts.d.ts.map +1 -0
  50. package/services/storage/clients/contacts.generated.d.ts +3 -0
  51. package/services/storage/clients/contacts.generated.d.ts.map +1 -0
  52. package/services/storage/clients/contacts.generated.js +1197 -0
  53. package/services/storage/clients/contacts.generated.js.map +1 -0
  54. package/services/storage/clients/contacts.js +141 -0
  55. package/services/storage/clients/contacts.js.map +1 -0
  56. package/services/storage/common.d.ts +667 -0
  57. package/services/storage/common.d.ts.map +1 -0
  58. package/services/storage/common.generated.d.ts +17 -0
  59. package/services/storage/common.generated.d.ts.map +1 -0
  60. package/services/storage/common.generated.js +863 -0
  61. package/services/storage/common.generated.js.map +1 -0
  62. package/services/storage/common.js +587 -0
  63. package/services/storage/common.js.map +1 -0
  64. package/services/storage/lib/validators.d.ts +64 -0
  65. package/services/storage/lib/validators.d.ts.map +1 -0
  66. package/services/storage/lib/validators.js +82 -0
  67. package/services/storage/lib/validators.js.map +1 -0
  68. package/services/storage/server.d.ts +127 -0
  69. package/services/storage/server.d.ts.map +1 -0
  70. package/services/storage/server.js +626 -0
  71. package/services/storage/server.js.map +1 -0
  72. package/services/storage/test-utils.d.ts +70 -0
  73. package/services/storage/test-utils.d.ts.map +1 -0
  74. package/services/storage/test-utils.js +347 -0
  75. package/services/storage/test-utils.js.map +1 -0
  76. package/services/storage/utils.d.ts +3 -0
  77. package/services/storage/utils.d.ts.map +1 -0
  78. package/services/storage/utils.js +10 -0
  79. package/services/storage/utils.js.map +1 -0
  80. package/services/username/client.d.ts +145 -0
  81. package/services/username/client.d.ts.map +1 -0
  82. package/services/username/client.js +681 -0
  83. package/services/username/client.js.map +1 -0
  84. package/services/username/common.d.ts +136 -0
  85. package/services/username/common.d.ts.map +1 -0
  86. package/services/username/common.generated.d.ts +13 -0
  87. package/services/username/common.generated.d.ts.map +1 -0
  88. package/services/username/common.generated.js +256 -0
  89. package/services/username/common.generated.js.map +1 -0
  90. package/services/username/common.js +226 -0
  91. package/services/username/common.js.map +1 -0
  92. package/services/username/server.d.ts +49 -0
  93. package/services/username/server.d.ts.map +1 -0
  94. package/services/username/server.js +262 -0
  95. package/services/username/server.js.map +1 -0
@@ -0,0 +1,1078 @@
1
+ import * as __typia_transform__assertGuard from "typia/lib/internal/_assertGuard.js";
2
+ import * as __typia_transform__accessExpressionAsString from "typia/lib/internal/_accessExpressionAsString.js";
3
+ import { isKeetaStorageAnchorDeleteResponse, isKeetaStorageAnchorPutResponse, isKeetaStorageAnchorSearchResponse, isKeetaStorageAnchorQuotaResponse } from './common.generated.js';
4
+ import { getKeetaStorageAnchorDeleteRequestSigningData, getKeetaStorageAnchorPutRequestSigningData, getKeetaStorageAnchorGetRequestSigningData, getKeetaStorageAnchorSearchRequestSigningData, getKeetaStorageAnchorQuotaRequestSigningData, parseContainerPayload, Errors, CONTENT_TYPE_JSON, CONTENT_TYPE_OCTET_STREAM, DEFAULT_SIGNED_URL_TTL_SECONDS } from './common.js';
5
+ import { KeetaNet } from '../../client/index.js';
6
+ import { getDefaultResolver } from '../../config.js';
7
+ import { EncryptedContainer } from '../../lib/encrypted-container.js';
8
+ import { createAssertEquals } from 'typia';
9
+ import { addSignatureToURL } from '../../lib/http-server/common.js';
10
+ import { SignData } from '../../lib/utils/signing.js';
11
+ import { KeetaAnchorError } from '../../lib/error.js';
12
+ import { arrayBufferLikeToBuffer } from '../../lib/utils/buffer.js';
13
+ import { StorageContactsClient } from './clients/contacts.js';
14
+ import Resolver from '../../lib/resolver.js';
15
+ import crypto from '../../lib/utils/crypto.js';
16
+ const KeetaStorageAnchorClientAccessToken = Symbol('KeetaStorageAnchorClientAccessToken');
17
+ const assertServiceMetadataEndpoint = (() => { const _io0 = (input, _exceptionable = true) => "string" === typeof input.url && (undefined === input.options || "object" === typeof input.options && null !== input.options && false === Array.isArray(input.options) && _io1(input.options, true && _exceptionable)) && (1 === Object.keys(input).length || Object.keys(input).every(key => {
18
+ if (["url", "options"].some(prop => key === prop))
19
+ return true;
20
+ const value = input[key];
21
+ if (undefined === value)
22
+ return true;
23
+ return false;
24
+ })); const _io1 = (input, _exceptionable = true) => (undefined === input.authentication || "object" === typeof input.authentication && null !== input.authentication && _io2(input.authentication, true && _exceptionable)) && (0 === Object.keys(input).length || Object.keys(input).every(key => {
25
+ if (["authentication"].some(prop => key === prop))
26
+ return true;
27
+ const value = input[key];
28
+ if (undefined === value)
29
+ return true;
30
+ return false;
31
+ })); const _io2 = (input, _exceptionable = true) => "keeta-account" === input.method && ("optional" === input.type || "required" === input.type || "none" === input.type) && (2 === Object.keys(input).length || Object.keys(input).every(key => {
32
+ if (["method", "type"].some(prop => key === prop))
33
+ return true;
34
+ const value = input[key];
35
+ if (undefined === value)
36
+ return true;
37
+ return false;
38
+ })); const _ao0 = (input, _path, _exceptionable = true) => ("string" === typeof input.url || __typia_transform__assertGuard._assertGuard(_exceptionable, {
39
+ method: "createAssertEquals",
40
+ path: _path + ".url",
41
+ expected: "string",
42
+ value: input.url
43
+ }, _errorFactory)) && (undefined === input.options || ("object" === typeof input.options && null !== input.options && false === Array.isArray(input.options) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
44
+ method: "createAssertEquals",
45
+ path: _path + ".options",
46
+ expected: "(__type.o1 | undefined)",
47
+ value: input.options
48
+ }, _errorFactory)) && _ao1(input.options, _path + ".options", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
49
+ method: "createAssertEquals",
50
+ path: _path + ".options",
51
+ expected: "(__type.o1 | undefined)",
52
+ value: input.options
53
+ }, _errorFactory)) && (1 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
54
+ if (["url", "options"].some(prop => key === prop))
55
+ return true;
56
+ const value = input[key];
57
+ if (undefined === value)
58
+ return true;
59
+ return __typia_transform__assertGuard._assertGuard(_exceptionable, {
60
+ method: "createAssertEquals",
61
+ path: _path + __typia_transform__accessExpressionAsString._accessExpressionAsString(key),
62
+ expected: "undefined",
63
+ value: value
64
+ }, _errorFactory);
65
+ }))); const _ao1 = (input, _path, _exceptionable = true) => (undefined === input.authentication || ("object" === typeof input.authentication && null !== input.authentication || __typia_transform__assertGuard._assertGuard(_exceptionable, {
66
+ method: "createAssertEquals",
67
+ path: _path + ".authentication",
68
+ expected: "(ServiceMetadataAuthenticationType | undefined)",
69
+ value: input.authentication
70
+ }, _errorFactory)) && _ao2(input.authentication, _path + ".authentication", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
71
+ method: "createAssertEquals",
72
+ path: _path + ".authentication",
73
+ expected: "(ServiceMetadataAuthenticationType | undefined)",
74
+ value: input.authentication
75
+ }, _errorFactory)) && (0 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
76
+ if (["authentication"].some(prop => key === prop))
77
+ return true;
78
+ const value = input[key];
79
+ if (undefined === value)
80
+ return true;
81
+ return __typia_transform__assertGuard._assertGuard(_exceptionable, {
82
+ method: "createAssertEquals",
83
+ path: _path + __typia_transform__accessExpressionAsString._accessExpressionAsString(key),
84
+ expected: "undefined",
85
+ value: value
86
+ }, _errorFactory);
87
+ }))); const _ao2 = (input, _path, _exceptionable = true) => ("keeta-account" === input.method || __typia_transform__assertGuard._assertGuard(_exceptionable, {
88
+ method: "createAssertEquals",
89
+ path: _path + ".method",
90
+ expected: "\"keeta-account\"",
91
+ value: input.method
92
+ }, _errorFactory)) && ("optional" === input.type || "required" === input.type || "none" === input.type || __typia_transform__assertGuard._assertGuard(_exceptionable, {
93
+ method: "createAssertEquals",
94
+ path: _path + ".type",
95
+ expected: "(\"none\" | \"optional\" | \"required\")",
96
+ value: input.type
97
+ }, _errorFactory)) && (2 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
98
+ if (["method", "type"].some(prop => key === prop))
99
+ return true;
100
+ const value = input[key];
101
+ if (undefined === value)
102
+ return true;
103
+ return __typia_transform__assertGuard._assertGuard(_exceptionable, {
104
+ method: "createAssertEquals",
105
+ path: _path + __typia_transform__accessExpressionAsString._accessExpressionAsString(key),
106
+ expected: "undefined",
107
+ value: value
108
+ }, _errorFactory);
109
+ }))); const __is = (input, _exceptionable = true) => null !== input && undefined !== input && ("string" === typeof input || "object" === typeof input && null !== input && _io0(input, true)); let _errorFactory; return (input, errorFactory) => {
110
+ if (false === __is(input)) {
111
+ _errorFactory = errorFactory;
112
+ ((input, _path, _exceptionable = true) => (null !== input || __typia_transform__assertGuard._assertGuard(true, {
113
+ method: "createAssertEquals",
114
+ path: _path + "",
115
+ expected: "(__type | string)",
116
+ value: input
117
+ }, _errorFactory)) && (undefined !== input || __typia_transform__assertGuard._assertGuard(true, {
118
+ method: "createAssertEquals",
119
+ path: _path + "",
120
+ expected: "(__type | string)",
121
+ value: input
122
+ }, _errorFactory)) && ("string" === typeof input || ("object" === typeof input && null !== input || __typia_transform__assertGuard._assertGuard(true, {
123
+ method: "createAssertEquals",
124
+ path: _path + "",
125
+ expected: "(__type | string)",
126
+ value: input
127
+ }, _errorFactory)) && _ao0(input, _path + "", true) || __typia_transform__assertGuard._assertGuard(true, {
128
+ method: "createAssertEquals",
129
+ path: _path + "",
130
+ expected: "(__type | string)",
131
+ value: input
132
+ }, _errorFactory)))(input, "$input", true);
133
+ }
134
+ return input;
135
+ }; })();
136
+ function validateURL(url) {
137
+ if (url === undefined || url === null) {
138
+ throw (new Errors.InvalidPath('Invalid URL: null or undefined'));
139
+ }
140
+ try {
141
+ return (new URL(url));
142
+ }
143
+ catch {
144
+ throw (new Errors.InvalidResponse(`Invalid URL in service metadata: ${url}`));
145
+ }
146
+ }
147
+ async function getEndpoints(resolver, logger) {
148
+ const response = await resolver.lookup('storage', {});
149
+ if (response === undefined) {
150
+ return (null);
151
+ }
152
+ const serviceInfoPromises = Object.entries(response).map(async function ([id, serviceInfo]) {
153
+ const operations = await serviceInfo.operations('object');
154
+ const operationsFunctions = {};
155
+ for (const [key, operation] of Object.entries(operations)) {
156
+ if (operation === undefined) {
157
+ continue;
158
+ }
159
+ Object.defineProperty(operationsFunctions, key, {
160
+ get: async function () {
161
+ const endpointInfo = assertServiceMetadataEndpoint(await Resolver.Metadata.fullyResolveValuizable(operation));
162
+ let url;
163
+ let authentication = {
164
+ type: 'none',
165
+ method: 'keeta-account'
166
+ };
167
+ if (typeof endpointInfo === 'string') {
168
+ url = endpointInfo;
169
+ }
170
+ else {
171
+ url = endpointInfo.url;
172
+ if (endpointInfo.options?.authentication) {
173
+ authentication = endpointInfo.options.authentication;
174
+ }
175
+ }
176
+ return ({
177
+ url: function (params) {
178
+ let substitutedURL;
179
+ try {
180
+ substitutedURL = decodeURI(url);
181
+ }
182
+ catch (error) {
183
+ logger?.debug('getEndpoints', 'Failed to decode URI, using original URL for substitution', error, url);
184
+ substitutedURL = url;
185
+ }
186
+ for (const [paramKey, paramValue] of Object.entries(params ?? {})) {
187
+ substitutedURL = substitutedURL.replace(`{${paramKey}}`, encodeURIComponent(paramValue));
188
+ }
189
+ return (validateURL(substitutedURL));
190
+ },
191
+ options: { authentication }
192
+ });
193
+ },
194
+ enumerable: true,
195
+ configurable: true
196
+ });
197
+ }
198
+ // Extract anchor account public key from service metadata
199
+ const result = { operations: operationsFunctions };
200
+ if ('anchorAccount' in serviceInfo && typeof serviceInfo.anchorAccount === 'function') {
201
+ const anchorAccountValue = await serviceInfo.anchorAccount('primitive');
202
+ if (typeof anchorAccountValue === 'string') {
203
+ result.anchorAccountPublicKey = anchorAccountValue;
204
+ }
205
+ }
206
+ return ([
207
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
208
+ id,
209
+ result
210
+ ]);
211
+ });
212
+ const retval = Object.fromEntries(await Promise.all(serviceInfoPromises));
213
+ return (retval);
214
+ }
215
+ class KeetaStorageAnchorBase {
216
+ logger;
217
+ client;
218
+ constructor(config) {
219
+ this.client = config.client;
220
+ this.logger = config.logger;
221
+ }
222
+ }
223
+ /**
224
+ * A session provides a simplified API for storage operations with default account,
225
+ * working directory, and visibility settings.
226
+ */
227
+ export class KeetaStorageAnchorSession {
228
+ provider;
229
+ account;
230
+ workingDirectory;
231
+ #defaultVisibility;
232
+ constructor(provider, config) {
233
+ this.provider = provider;
234
+ this.account = config.account;
235
+ this.workingDirectory = config.workingDirectory ?? '/';
236
+ this.#defaultVisibility = config.defaultVisibility ?? 'private';
237
+ }
238
+ /**
239
+ * Resolve a relative path to a full storage path.
240
+ *
241
+ * @param relativePath - Path to resolve (absolute paths returned as-is)
242
+ *
243
+ * @returns The full storage path with working directory prepended if relative
244
+ */
245
+ #resolvePath(relativePath) {
246
+ // If path is already absolute (starts with /), use it as-is
247
+ if (relativePath.startsWith('/')) {
248
+ return (relativePath);
249
+ }
250
+ // Otherwise, prepend working directory
251
+ return (this.workingDirectory + relativePath);
252
+ }
253
+ /**
254
+ * Store data at a relative path.
255
+ * For public visibility, the anchor account is automatically fetched from the provider.
256
+ *
257
+ * @param relativePath - The relative path
258
+ * @param data - The binary data to store
259
+ * @param options.mimeType - MIME type of the data
260
+ * @param options.tags - Optional plaintext tags
261
+ * @param options.visibility - Object visibility
262
+ *
263
+ * @returns The created/updated object metadata
264
+ */
265
+ async put(relativePath, data, options) {
266
+ const fullPath = this.#resolvePath(relativePath);
267
+ const visibility = options.visibility ?? this.#defaultVisibility;
268
+ const putOpts = {
269
+ path: fullPath,
270
+ data,
271
+ mimeType: options.mimeType,
272
+ visibility,
273
+ account: this.account
274
+ };
275
+ if (options.tags) {
276
+ putOpts.tags = options.tags;
277
+ }
278
+ if (visibility === 'public' && this.provider.anchorAccount) {
279
+ putOpts.anchorAccount = this.provider.anchorAccount;
280
+ }
281
+ return (await this.provider.put(putOpts));
282
+ }
283
+ /**
284
+ * Get data from a relative path.
285
+ *
286
+ * @param relativePath - The relative path
287
+ *
288
+ * @returns The decrypted data and mime-type, or null if not found
289
+ */
290
+ async get(relativePath) {
291
+ const fullPath = this.#resolvePath(relativePath);
292
+ return (await this.provider.get({ path: fullPath, account: this.account }));
293
+ }
294
+ /**
295
+ * Delete data at a relative path.
296
+ *
297
+ * @param relativePath - The relative path
298
+ *
299
+ * @returns True if the object was deleted, false if it didn't exist
300
+ */
301
+ async delete(relativePath) {
302
+ const fullPath = this.#resolvePath(relativePath);
303
+ return (await this.provider.delete({ path: fullPath, account: this.account }));
304
+ }
305
+ /**
306
+ * Search for objects. Owner is automatically set to the session account.
307
+ *
308
+ * @param criteria - Optional search criteria (owner is set automatically)
309
+ * @param pagination - Optional pagination settings
310
+ *
311
+ * @returns Search results with optional nextCursor for pagination
312
+ */
313
+ async search(criteria, pagination) {
314
+ const fullCriteria = {
315
+ ...criteria,
316
+ owner: this.account.publicKeyString.get()
317
+ };
318
+ const searchOpts = {
319
+ criteria: fullCriteria,
320
+ account: this.account
321
+ };
322
+ if (pagination) {
323
+ searchOpts.pagination = pagination;
324
+ }
325
+ return (await this.provider.search(searchOpts));
326
+ }
327
+ /**
328
+ * Get a pre-signed public URL for a relative path.
329
+ *
330
+ * @param relativePath - The relative path
331
+ * @param options.ttl - Optional TTL in seconds
332
+ *
333
+ * @returns The pre-signed URL for public access
334
+ */
335
+ async getPublicUrl(relativePath, options) {
336
+ const fullPath = this.#resolvePath(relativePath);
337
+ const urlOpts = {
338
+ path: fullPath,
339
+ account: this.account
340
+ };
341
+ if (options?.ttl) {
342
+ urlOpts.ttl = options.ttl;
343
+ }
344
+ return (await this.provider.getPublicUrl(urlOpts));
345
+ }
346
+ }
347
+ /**
348
+ * Represents a Storage Anchor provider for performing storage operations.
349
+ */
350
+ export class KeetaStorageAnchorProvider extends KeetaStorageAnchorBase {
351
+ /**
352
+ * Service information including available operations and endpoints.
353
+ */
354
+ serviceInfo;
355
+ /**
356
+ * Unique identifier for this provider.
357
+ */
358
+ providerID;
359
+ /**
360
+ * The anchor account for this provider.
361
+ */
362
+ anchorAccount;
363
+ constructor(serviceInfo, providerID, parent) {
364
+ const parentPrivate = parent._internals(KeetaStorageAnchorClientAccessToken);
365
+ super(parentPrivate);
366
+ this.serviceInfo = serviceInfo;
367
+ this.providerID = providerID;
368
+ // Convert anchor account public key string to Account
369
+ if (serviceInfo.anchorAccountPublicKey) {
370
+ try {
371
+ this.anchorAccount = KeetaNet.lib.Account.fromPublicKeyString(serviceInfo.anchorAccountPublicKey).assertAccount();
372
+ }
373
+ catch {
374
+ throw (new Errors.InvalidAnchorAccount(serviceInfo.anchorAccountPublicKey));
375
+ }
376
+ }
377
+ else {
378
+ this.anchorAccount = null;
379
+ }
380
+ }
381
+ /**
382
+ * Get operation endpoint data for a given operation.
383
+ *
384
+ * @param operationName - The operation to get endpoint data for
385
+ * @param params - Optional URL template parameters to substitute
386
+ *
387
+ * @returns The endpoint URL and authentication configuration
388
+ *
389
+ * @throws OperationNotSupported if the operation is not available
390
+ * @throws UnsupportedAuthMethod if the authentication method is not supported
391
+ */
392
+ async #getOperationData(operationName, params) {
393
+ const endpoint = await this.serviceInfo.operations[operationName];
394
+ if (endpoint === undefined) {
395
+ throw (new Errors.OperationNotSupported(operationName));
396
+ }
397
+ if (endpoint.options.authentication.method !== 'keeta-account') {
398
+ throw (new Errors.UnsupportedAuthMethod(endpoint.options.authentication.method));
399
+ }
400
+ return ({
401
+ url: endpoint.url(params),
402
+ auth: endpoint.options.authentication
403
+ });
404
+ }
405
+ /**
406
+ * Parse an error response from the server.
407
+ * Attempts to restore structured error types from JSON.
408
+ *
409
+ * @param data - The error response data
410
+ *
411
+ * @returns A KeetaAnchorError or generic Error with the error message
412
+ *
413
+ * @throws InvariantViolation if the response is not a valid error object
414
+ */
415
+ async #parseResponseError(data) {
416
+ if (typeof data !== 'object' || data === null) {
417
+ throw (new Errors.InvariantViolation('expected error response object'));
418
+ }
419
+ if (!('ok' in data) || data.ok !== false) {
420
+ throw (new Errors.InvariantViolation('expected error response with ok=false'));
421
+ }
422
+ let parsedError = null;
423
+ try {
424
+ parsedError = await KeetaAnchorError.fromJSON(data);
425
+ }
426
+ catch (error) {
427
+ this.logger?.debug('Failed to parse error response as KeetaAnchorError', error, data);
428
+ }
429
+ if (parsedError) {
430
+ return (parsedError);
431
+ }
432
+ else {
433
+ let errorStr;
434
+ if ('error' in data && typeof data.error === 'string') {
435
+ errorStr = data.error;
436
+ }
437
+ else {
438
+ errorStr = 'Unknown error';
439
+ }
440
+ return (new Error(`storage request failed: ${errorStr}`));
441
+ }
442
+ }
443
+ /**
444
+ * Resolve account to use for signing, with private key validation.
445
+ *
446
+ * @param account - Optional account override (falls back to client account)
447
+ * @param requirePrivateKey - Whether private key is required
448
+ *
449
+ * @returns The resolved account with optional private key validation
450
+ *
451
+ * @throws PrivateKeyRequired if private key is needed but not available
452
+ */
453
+ #resolveSignerAccount(account, requirePrivateKey = true) {
454
+ const resolved = account ?? this.client.account;
455
+ if (requirePrivateKey && !resolved?.hasPrivateKey) {
456
+ throw (new Errors.PrivateKeyRequired());
457
+ }
458
+ if (!resolved) {
459
+ throw (new Errors.AccountRequired());
460
+ }
461
+ return (resolved);
462
+ }
463
+ /**
464
+ * Handle error responses from binary PUT/GET requests.
465
+ * Attempts to parse JSON error response, otherwise throws generic error.
466
+ *
467
+ * @param response - The failed HTTP response to handle
468
+ *
469
+ * @throws The parsed error from the response body, or a generic ServiceUnavailable error
470
+ */
471
+ async #handleBinaryResponseError(response) {
472
+ let errorJSON;
473
+ try {
474
+ errorJSON = await response.json();
475
+ }
476
+ catch {
477
+ // JSON parsing failed - throw generic error
478
+ throw (new Errors.InvalidResponse(`HTTP ${response.status}: ${response.statusText}`));
479
+ }
480
+ // JSON parsed successfully - attempt to extract error
481
+ if (typeof errorJSON === 'object' && errorJSON !== null && 'ok' in errorJSON && errorJSON.ok === false) {
482
+ throw (await this.#parseResponseError(errorJSON));
483
+ }
484
+ // Unexpected response shape
485
+ throw (new Errors.InvalidResponse(`HTTP ${response.status}: ${response.statusText}`));
486
+ }
487
+ /**
488
+ * Normalize a path suffix and append it to a URL pathname.
489
+ * Handles leading slashes and trailing slashes consistently.
490
+ * Validates against path traversal attacks.
491
+ *
492
+ * @param url - The URL object to modify (mutated in place)
493
+ * @param path - The path suffix to append
494
+ *
495
+ * @throws InvalidPath if path contains traversal patterns
496
+ */
497
+ #appendPathToUrl(url, path) {
498
+ // Validate path is not empty
499
+ if (!path || path.trim().length === 0) {
500
+ throw (new Errors.InvalidPath('Path cannot be empty'));
501
+ }
502
+ // Validate against path traversal attacks
503
+ if (path.includes('..') || path.includes('//')) {
504
+ throw (new Errors.InvalidPath('Path contains invalid segments'));
505
+ }
506
+ const pathSuffix = path.startsWith('/') ? path.slice(1) : path;
507
+ url.pathname = url.pathname.replace(/\/$/, '') + '/' + pathSuffix;
508
+ }
509
+ /**
510
+ * Make a JSON API request to the storage service.
511
+ * Handles authentication, serialization, and response parsing.
512
+ *
513
+ * @typeParam Response - The expected response type
514
+ * @typeParam Request - The request body type
515
+ * @typeParam SerializedRequest - The serialized request type
516
+ *
517
+ * @param input.method - HTTP method
518
+ * @param input.endpoint - Operation endpoint name
519
+ * @param input.account - Account for signing the request
520
+ * @param input.params - URL template parameters
521
+ * @param input.queryParams - Query string parameters
522
+ * @param input.pathSuffix - Path to append to the endpoint URL
523
+ * @param input.body - Request body
524
+ * @param input.serializeRequest - Function to serialize the request body
525
+ * @param input.getSignedData - Function to get signing data from request
526
+ * @param input.isResponse - Type guard for the response
527
+ *
528
+ * @returns The successful response
529
+ *
530
+ * @throws AccountRequired if account is required but not provided
531
+ * @throws InvariantViolation if getSignedData is required but not provided
532
+ * @throws InvalidResponse if the response is invalid
533
+ */
534
+ async #makeRequest(input) {
535
+ const { url, auth } = await this.#getOperationData(input.endpoint, input.params);
536
+ // Append path suffix to URL pathname if provided
537
+ if (input.pathSuffix) {
538
+ this.#appendPathToUrl(url, input.pathSuffix);
539
+ }
540
+ // Add query parameters to URL if provided
541
+ if (input.queryParams) {
542
+ for (const [key, value] of Object.entries(input.queryParams)) {
543
+ url.searchParams.set(key, value);
544
+ }
545
+ }
546
+ // Serialize the request body if provided.
547
+ let serializedBody;
548
+ if (input.body !== undefined && input.serializeRequest) {
549
+ serializedBody = await input.serializeRequest(input.body);
550
+ }
551
+ let signed;
552
+ if (auth.type === 'required' || (auth.type === 'optional' && input.account)) {
553
+ if (!input.account) {
554
+ throw (new Errors.AccountRequired());
555
+ }
556
+ if (!input.getSignedData) {
557
+ throw (new Errors.InvariantViolation('getSignedData required for signed requests'));
558
+ }
559
+ const signable = input.getSignedData(serializedBody);
560
+ signed = await SignData(input.account.assertAccount(), signable);
561
+ }
562
+ const headers = { 'Accept': CONTENT_TYPE_JSON };
563
+ let usingUrl = url;
564
+ let body = null;
565
+ if (input.method === 'POST' || input.method === 'PUT') {
566
+ headers['Content-Type'] = CONTENT_TYPE_JSON;
567
+ body = JSON.stringify({ ...serializedBody, signed });
568
+ }
569
+ else {
570
+ if (signed) {
571
+ if (!input.account) {
572
+ throw (new Errors.InvariantViolation('Account information is required for this operation'));
573
+ }
574
+ usingUrl = addSignatureToURL(usingUrl, { signedField: signed, account: input.account.assertAccount() });
575
+ }
576
+ if (input.body) {
577
+ throw (new Errors.InvariantViolation('body cannot be sent with GET/DELETE requests'));
578
+ }
579
+ }
580
+ let httpResponse;
581
+ try {
582
+ httpResponse = await fetch(usingUrl, {
583
+ method: input.method, headers, body
584
+ });
585
+ }
586
+ catch (e) {
587
+ throw (new Errors.ServiceUnavailable(`Network error: ${e instanceof Error ? e.message : String(e)}`));
588
+ }
589
+ const requestInformationJSON = await httpResponse.json();
590
+ if (!input.isResponse(requestInformationJSON)) {
591
+ throw (new Errors.InvalidResponse(JSON.stringify(requestInformationJSON)));
592
+ }
593
+ if (!requestInformationJSON.ok) {
594
+ throw (await this.#parseResponseError(requestInformationJSON));
595
+ }
596
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
597
+ return requestInformationJSON;
598
+ }
599
+ /**
600
+ * Make a PUT request with raw binary body.
601
+ * Used for uploading encrypted container data to storage.
602
+ *
603
+ * @param input.path - The storage path for the object
604
+ * @param input.data - The binary data (encrypted container) to upload
605
+ * @param input.visibility - Object visibility (public or private)
606
+ * @param input.tags - Optional tags to associate with the object
607
+ * @param input.account - Account for signing the request
608
+ *
609
+ * @returns The created/updated object metadata
610
+ *
611
+ * @throws AccountRequired if authentication is required but no account provided
612
+ * @throws ServiceUnavailable if the request fails
613
+ */
614
+ async #makeBinaryPutRequest(input) {
615
+ const { url, auth } = await this.#getOperationData('put');
616
+ const visibility = input.visibility ?? 'private';
617
+ this.#appendPathToUrl(url, input.path);
618
+ if (auth.type === 'required' && !input.account) {
619
+ throw (new Errors.AccountRequired());
620
+ }
621
+ const tags = input.tags ?? [];
622
+ for (const tag of tags) {
623
+ // Validate tags are not empty
624
+ if (!tag || tag.trim().length === 0) {
625
+ throw (new Errors.InvalidTag('Tags cannot be empty'));
626
+ }
627
+ // Validate tags don't contain commas (used as delimiter in query params)
628
+ if (tag.includes(',')) {
629
+ throw (new Errors.InvalidTag('Tags cannot contain commas'));
630
+ }
631
+ }
632
+ const signable = getKeetaStorageAnchorPutRequestSigningData({ path: input.path, visibility, tags });
633
+ const signed = await SignData(input.account.assertAccount(), signable);
634
+ // Add auth to query params using helper
635
+ const signedUrl = addSignatureToURL(url, { signedField: signed, account: input.account.assertAccount() });
636
+ signedUrl.searchParams.set('visibility', visibility);
637
+ if (tags.length > 0) {
638
+ signedUrl.searchParams.set('tags', tags.join(','));
639
+ }
640
+ let response;
641
+ try {
642
+ response = await fetch(signedUrl, {
643
+ method: 'PUT',
644
+ headers: { 'Content-Type': CONTENT_TYPE_OCTET_STREAM, 'Accept': CONTENT_TYPE_JSON },
645
+ body: input.data
646
+ });
647
+ }
648
+ catch (e) {
649
+ throw (new Errors.ServiceUnavailable(`Network error: ${e instanceof Error ? e.message : String(e)}`));
650
+ }
651
+ if (!response.ok) {
652
+ await this.#handleBinaryResponseError(response);
653
+ }
654
+ const responseJSON = await response.json();
655
+ if (!isKeetaStorageAnchorPutResponse(responseJSON)) {
656
+ throw (new Errors.InvalidResponse(JSON.stringify(responseJSON)));
657
+ }
658
+ if (!responseJSON.ok) {
659
+ throw (await this.#parseResponseError(responseJSON));
660
+ }
661
+ return (responseJSON);
662
+ }
663
+ /**
664
+ * Make a GET request that returns raw binary data.
665
+ * Used for downloading encrypted container data from storage.
666
+ *
667
+ * @param input.path - The storage path to retrieve
668
+ * @param input.account - Account for signing the request
669
+ *
670
+ * @returns The raw binary data (encrypted container)
671
+ *
672
+ * @throws AccountRequired if authentication is required but no account provided
673
+ * @throws ServiceUnavailable if the request fails
674
+ */
675
+ async #makeBinaryGetRequest(input) {
676
+ const { url, auth } = await this.#getOperationData('get');
677
+ this.#appendPathToUrl(url, input.path);
678
+ if (auth.type === 'required' && !input.account) {
679
+ throw (new Errors.AccountRequired());
680
+ }
681
+ // Sign the request
682
+ const signable = getKeetaStorageAnchorGetRequestSigningData({ path: input.path, account: input.account.publicKeyString.get() });
683
+ const signed = await SignData(input.account.assertAccount(), signable);
684
+ // Add auth to query params
685
+ const signedUrl = addSignatureToURL(url, { signedField: signed, account: input.account.assertAccount() });
686
+ let response;
687
+ try {
688
+ response = await fetch(signedUrl, {
689
+ method: 'GET',
690
+ headers: { 'Accept': CONTENT_TYPE_OCTET_STREAM }
691
+ });
692
+ }
693
+ catch (e) {
694
+ throw (new Errors.ServiceUnavailable(`Network error: ${e instanceof Error ? e.message : String(e)}`));
695
+ }
696
+ if (!response.ok) {
697
+ await this.#handleBinaryResponseError(response);
698
+ }
699
+ const arrayBuffer = await response.arrayBuffer();
700
+ return (arrayBufferLikeToBuffer(arrayBuffer));
701
+ }
702
+ /**
703
+ * Delete an object by path.
704
+ *
705
+ * @param request - The delete request parameters
706
+ * @param request.path - The storage path to delete
707
+ * @param request.account - Optional account for signing (falls back to client account)
708
+ *
709
+ * @returns True if the object was deleted, false if it didn't exist
710
+ */
711
+ async delete(request) {
712
+ this.logger?.debug(`Deleting object at ${request.path} for provider ID: ${String(this.providerID)}`);
713
+ const response = await this.#makeRequest({
714
+ method: 'DELETE',
715
+ endpoint: 'delete',
716
+ account: request.account,
717
+ pathSuffix: request.path,
718
+ getSignedData: function () {
719
+ return (getKeetaStorageAnchorDeleteRequestSigningData({ path: request.path }));
720
+ },
721
+ isResponse: isKeetaStorageAnchorDeleteResponse
722
+ });
723
+ this.logger?.debug(`Delete request successful for path: ${request.path}`);
724
+ return (response.deleted);
725
+ }
726
+ /**
727
+ * Get (retrieve) an object by path.
728
+ * Data is automatically decrypted from the EncryptedContainer.
729
+ *
730
+ * @param options.path - The storage path (e.g., "/user/<publicKey>/myfile.txt")
731
+ * @param options.account - Optional account for signing and decryption (falls back to client account)
732
+ *
733
+ * @returns The decrypted data and mime-type, or null if not found
734
+ *
735
+ * @throws PrivateKeyRequired if no account with private key is available
736
+ * @throws InvalidResponse if the stored data cannot be decrypted
737
+ */
738
+ async get(options) {
739
+ const { path } = options;
740
+ this.logger?.debug(`Getting object at path: ${path}`);
741
+ const signerAccount = this.#resolveSignerAccount(options.account);
742
+ try {
743
+ const encodedData = await this.#makeBinaryGetRequest({
744
+ path,
745
+ account: signerAccount
746
+ });
747
+ // Decrypt the container and extract mimeType from encrypted payload
748
+ const container = EncryptedContainer.fromEncryptedBuffer(encodedData, [signerAccount]);
749
+ const plaintext = await container.getPlaintext();
750
+ const { mimeType, content: data } = parseContainerPayload(plaintext);
751
+ this.logger?.debug(`Get request successful for path: ${path}`);
752
+ return ({
753
+ data,
754
+ mimeType
755
+ });
756
+ }
757
+ catch (e) {
758
+ if (Errors.DocumentNotFound.isInstance(e)) {
759
+ return (null);
760
+ }
761
+ throw (e);
762
+ }
763
+ }
764
+ /**
765
+ * Get metadata for an object by path.
766
+ *
767
+ * @param options.path - The storage path (e.g., "/user/<publicKey>/myfile.txt")
768
+ * @param options.account - Optional account for signing (falls back to client account)
769
+ *
770
+ * @returns The object metadata, or null if not found
771
+ *
772
+ * @throws PrivateKeyRequired if no account with private key is available
773
+ */
774
+ async getMetadata(options) {
775
+ const { path } = options;
776
+ this.logger?.debug(`Getting metadata at path: ${path}`);
777
+ const signerAccount = this.#resolveSignerAccount(options.account);
778
+ try {
779
+ const response = await this.#makeRequest({
780
+ method: 'GET',
781
+ endpoint: 'metadata',
782
+ account: signerAccount,
783
+ pathSuffix: path,
784
+ getSignedData: function () {
785
+ return (getKeetaStorageAnchorGetRequestSigningData({
786
+ path,
787
+ account: signerAccount.publicKeyString.get()
788
+ }));
789
+ },
790
+ isResponse: function (data) {
791
+ if (typeof data !== 'object' || data === null || !('ok' in data)) {
792
+ return (false);
793
+ }
794
+ if ('object' in data && typeof data.object === 'object' && data.object !== null) {
795
+ return (data.ok === true);
796
+ }
797
+ if ('error' in data && typeof data.error === 'string') {
798
+ return (data.ok === false);
799
+ }
800
+ return (false);
801
+ }
802
+ });
803
+ this.logger?.debug(`Get metadata successful for path: ${path}`);
804
+ return (response.object);
805
+ }
806
+ catch (e) {
807
+ // Check if it's a "not found" error
808
+ if (Errors.DocumentNotFound.isInstance(e)) {
809
+ return (null);
810
+ }
811
+ throw (e);
812
+ }
813
+ }
814
+ /**
815
+ * Put (create/update) an object.
816
+ * Data is automatically wrapped in an EncryptedContainer.
817
+ *
818
+ * @param options.path - The storage path (e.g., "/user/<publicKey>/myfile.txt")
819
+ * @param options.data - The binary data to store
820
+ * @param options.mimeType - MIME type of the data (stored encrypted in the container)
821
+ * @param options.tags - Optional plaintext tags for the object
822
+ * @param options.visibility - Object visibility
823
+ * @param options.account - Optional account for signing (falls back to client account)
824
+ * @param options.anchorAccount - Anchor account for public object encryption (falls back to provider's anchor)
825
+ *
826
+ * @returns The created/updated object metadata
827
+ *
828
+ * @throws PrivateKeyRequired if no account with private key is available
829
+ * @throws InvalidAnchorAccount if public visibility requires an anchor account that's not available
830
+ */
831
+ async put(options) {
832
+ const { path, data, mimeType, tags, visibility, anchorAccount } = options;
833
+ this.logger?.debug(`Putting object at path: ${path}`);
834
+ const signerAccount = this.#resolveSignerAccount(options.account);
835
+ const principals = [signerAccount];
836
+ if (visibility === 'public') {
837
+ const effectiveAnchor = anchorAccount ?? this.anchorAccount;
838
+ if (!effectiveAnchor) {
839
+ throw (new Errors.AccountRequired('anchorAccount is required for public visibility and no provider anchor account is available'));
840
+ }
841
+ principals.push(effectiveAnchor);
842
+ }
843
+ // Create payload with mimeType inside (encrypted)
844
+ const payload = {
845
+ mimeType,
846
+ data: data.toString('base64')
847
+ };
848
+ const container = EncryptedContainer.fromPlaintext(JSON.stringify(payload), principals, { signer: signerAccount });
849
+ const encodedBuffer = await container.getEncodedBuffer();
850
+ const binaryData = arrayBufferLikeToBuffer(encodedBuffer);
851
+ const response = await this.#makeBinaryPutRequest({
852
+ path,
853
+ data: binaryData,
854
+ account: signerAccount,
855
+ ...(visibility !== undefined && { visibility }),
856
+ ...(tags !== undefined && { tags })
857
+ });
858
+ this.logger?.debug(`Put request successful for path: ${path}`);
859
+ return (response.object);
860
+ }
861
+ /**
862
+ * Search for objects matching criteria.
863
+ *
864
+ * @param options.criteria - Search criteria
865
+ * @param options.pagination - Optional pagination settings
866
+ * @param options.account - Optional account for signing (falls back to client account)
867
+ *
868
+ * @returns Search results with optional nextCursor for pagination
869
+ */
870
+ async search(options) {
871
+ const { criteria, pagination } = options;
872
+ this.logger?.debug('Searching for objects');
873
+ const signerAccount = this.#resolveSignerAccount(options.account);
874
+ const bodyToSend = {
875
+ criteria,
876
+ account: signerAccount
877
+ };
878
+ if (pagination !== undefined) {
879
+ bodyToSend.pagination = pagination;
880
+ }
881
+ const response = await this.#makeRequest({
882
+ method: 'POST',
883
+ endpoint: 'search',
884
+ account: signerAccount,
885
+ serializeRequest(body) {
886
+ const serialized = {
887
+ criteria: body.criteria
888
+ };
889
+ if (body.pagination !== undefined) {
890
+ serialized.pagination = body.pagination;
891
+ }
892
+ if (body.account !== undefined) {
893
+ serialized.account = body.account.assertAccount().publicKeyString.get();
894
+ }
895
+ return (serialized);
896
+ },
897
+ body: bodyToSend,
898
+ getSignedData: function () {
899
+ return (getKeetaStorageAnchorSearchRequestSigningData({
900
+ criteria,
901
+ ...(pagination !== undefined ? { pagination } : {})
902
+ }));
903
+ },
904
+ isResponse: isKeetaStorageAnchorSearchResponse
905
+ });
906
+ this.logger?.debug(`Search returned ${response.results.length} results`);
907
+ return ({
908
+ results: response.results,
909
+ ...(response.nextCursor !== undefined ? { nextCursor: response.nextCursor } : {})
910
+ });
911
+ }
912
+ /**
913
+ * Get quota status for the authenticated user.
914
+ *
915
+ * @param options.account - Optional account for signing (falls back to client account)
916
+ *
917
+ * @returns Current quota status
918
+ */
919
+ async getQuotaStatus(options) {
920
+ this.logger?.debug('Getting quota status');
921
+ const signerAccount = this.#resolveSignerAccount(options?.account);
922
+ const response = await this.#makeRequest({
923
+ method: 'GET',
924
+ endpoint: 'quota',
925
+ account: signerAccount,
926
+ getSignedData: function () {
927
+ return (getKeetaStorageAnchorQuotaRequestSigningData({}));
928
+ },
929
+ isResponse: isKeetaStorageAnchorQuotaResponse
930
+ });
931
+ this.logger?.debug('Quota status retrieved successfully');
932
+ return (response.quota);
933
+ }
934
+ /**
935
+ * Generate a pre-signed URL for public access to an object.
936
+ * The URL is signed by the owner and has a limited lifetime.
937
+ *
938
+ * @param options.path - The storage path to the public object
939
+ * @param options.ttl - TTL in seconds
940
+ * @param options.account - Owner account for signing (falls back to client account, must have private key)
941
+ *
942
+ * @returns The pre-signed URL for unauthenticated access
943
+ *
944
+ * @throws PrivateKeyRequired if no account with private key is available
945
+ * @throws OperationNotSupported if the public endpoint is not available
946
+ */
947
+ async getPublicUrl(options) {
948
+ const { path } = options;
949
+ const signerAccount = this.#resolveSignerAccount(options.account);
950
+ const ttl = options.ttl ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
951
+ if (ttl <= 0) {
952
+ throw (new Errors.InvariantViolation('TTL must be positive'));
953
+ }
954
+ const expiresAt = Math.floor(Date.now() / 1000) + ttl;
955
+ // Sign the message with signer pubkey cryptographically bound
956
+ const signerPubKey = signerAccount.publicKeyString.get();
957
+ const signed = await SignData(signerAccount.assertAccount(), [path, expiresAt, signerPubKey]);
958
+ // Get base URL from service info
959
+ const operationInfo = await this.serviceInfo.operations.public;
960
+ if (!operationInfo) {
961
+ throw (new Errors.OperationNotSupported('public'));
962
+ }
963
+ // Construct the public URL using standard signed field convention
964
+ const publicUrl = new URL(operationInfo.url().href);
965
+ this.#appendPathToUrl(publicUrl, path);
966
+ publicUrl.searchParams.set('expires', String(expiresAt));
967
+ const finalUrl = addSignatureToURL(publicUrl, { signedField: signed, account: signerAccount.assertAccount() });
968
+ return (finalUrl.toString());
969
+ }
970
+ /**
971
+ * Create a session.
972
+ */
973
+ beginSession(config) {
974
+ return (new KeetaStorageAnchorSession(this, config));
975
+ }
976
+ /**
977
+ * Execute a function within a session scope.
978
+ */
979
+ async withSession(config, fn) {
980
+ const session = this.beginSession(config);
981
+ return (await fn(session));
982
+ }
983
+ /**
984
+ * Get a contacts client bound to the given account.
985
+ */
986
+ getContactsClient(config) {
987
+ const session = this.beginSession({ account: config.account, workingDirectory: config.basePath });
988
+ return (new StorageContactsClient(session));
989
+ }
990
+ }
991
+ class KeetaStorageAnchorClient extends KeetaStorageAnchorBase {
992
+ resolver;
993
+ id;
994
+ #signer;
995
+ #account;
996
+ constructor(client, config = {}) {
997
+ super({ client, logger: config.logger });
998
+ this.resolver = config.resolver ?? getDefaultResolver(client, config);
999
+ this.id = config.id ?? crypto.randomUUID();
1000
+ if (config.signer) {
1001
+ this.#signer = config.signer;
1002
+ }
1003
+ else if ('signer' in client && client.signer !== null) {
1004
+ this.#signer = client.signer;
1005
+ }
1006
+ else if ('account' in client && client.account.hasPrivateKey) {
1007
+ this.#signer = client.account;
1008
+ }
1009
+ else {
1010
+ throw (new Errors.SignerRequired());
1011
+ }
1012
+ if (config.account) {
1013
+ this.#account = config.account;
1014
+ }
1015
+ else if ('account' in client) {
1016
+ this.#account = client.account;
1017
+ }
1018
+ else {
1019
+ throw (new Errors.AccountRequired());
1020
+ }
1021
+ }
1022
+ async #lookup() {
1023
+ const endpoints = await getEndpoints(this.resolver, this.logger);
1024
+ if (endpoints === null) {
1025
+ return (null);
1026
+ }
1027
+ const providers = Object.entries(endpoints).map(([id, serviceInfo]) => {
1028
+ return (new KeetaStorageAnchorProvider(serviceInfo, id, this));
1029
+ });
1030
+ return (providers);
1031
+ }
1032
+ /**
1033
+ * Get all available storage providers
1034
+ */
1035
+ async getProviders() {
1036
+ return (await this.#lookup());
1037
+ }
1038
+ /**
1039
+ * Get a specific provider by ID
1040
+ */
1041
+ async getProviderByID(providerID) {
1042
+ const providers = await this.#lookup();
1043
+ if (!providers) {
1044
+ return (null);
1045
+ }
1046
+ const provider = providers.find(function (p) {
1047
+ return (p.providerID === providerID);
1048
+ });
1049
+ return (provider ?? null);
1050
+ }
1051
+ /**
1052
+ * Get a contacts client bound to the given account.
1053
+ * Resolves the first available provider and constructs a StorageContactsClient.
1054
+ */
1055
+ async getContactsClient(config) {
1056
+ const providers = await this.getProviders();
1057
+ const provider = providers?.[0];
1058
+ if (!provider) {
1059
+ return (null);
1060
+ }
1061
+ return (provider.getContactsClient(config));
1062
+ }
1063
+ /** @internal */
1064
+ _internals(accessToken) {
1065
+ if (accessToken !== KeetaStorageAnchorClientAccessToken) {
1066
+ throw (new Errors.InvariantViolation('invalid internal access token'));
1067
+ }
1068
+ return ({
1069
+ resolver: this.resolver,
1070
+ logger: this.logger,
1071
+ client: this.client,
1072
+ signer: this.#signer,
1073
+ account: this.#account
1074
+ });
1075
+ }
1076
+ }
1077
+ export default KeetaStorageAnchorClient;
1078
+ //# sourceMappingURL=client.js.map