@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.
- package/client/index.d.ts +6 -0
- package/client/index.d.ts.map +1 -1
- package/client/index.js +7 -0
- package/client/index.js.map +1 -1
- package/lib/block-listener.d.ts +93 -0
- package/lib/block-listener.d.ts.map +1 -0
- package/lib/block-listener.js +259 -0
- package/lib/block-listener.js.map +1 -0
- package/lib/error.d.ts.map +1 -1
- package/lib/error.js +3 -1
- package/lib/error.js.map +1 -1
- package/lib/http-server/index.d.ts +14 -1
- package/lib/http-server/index.d.ts.map +1 -1
- package/lib/http-server/index.js +86 -7
- package/lib/http-server/index.js.map +1 -1
- package/lib/queue/index.d.ts +20 -5
- package/lib/queue/index.d.ts.map +1 -1
- package/lib/queue/index.js +52 -17
- package/lib/queue/index.js.map +1 -1
- package/lib/resolver.d.ts +57 -0
- package/lib/resolver.d.ts.map +1 -1
- package/lib/resolver.js +864 -250
- package/lib/resolver.js.map +1 -1
- package/npm-shrinkwrap.json +4 -4
- package/package.json +1 -1
- package/services/asset-movement/client.d.ts +9 -2
- package/services/asset-movement/client.d.ts.map +1 -1
- package/services/asset-movement/client.js +35 -2
- package/services/asset-movement/client.js.map +1 -1
- package/services/asset-movement/common.d.ts +1 -0
- package/services/asset-movement/common.d.ts.map +1 -1
- package/services/asset-movement/common.js +75 -0
- package/services/asset-movement/common.js.map +1 -1
- package/services/asset-movement/server.d.ts +0 -10
- package/services/asset-movement/server.d.ts.map +1 -1
- package/services/asset-movement/server.js +0 -2
- package/services/asset-movement/server.js.map +1 -1
- package/services/fx/common.d.ts +1 -1
- package/services/fx/common.js.map +1 -1
- package/services/fx/server.d.ts +37 -6
- package/services/fx/server.d.ts.map +1 -1
- package/services/fx/server.js +207 -66
- package/services/fx/server.js.map +1 -1
- package/services/storage/client.d.ts +332 -0
- package/services/storage/client.d.ts.map +1 -0
- package/services/storage/client.js +1078 -0
- package/services/storage/client.js.map +1 -0
- package/services/storage/clients/contacts.d.ts +94 -0
- package/services/storage/clients/contacts.d.ts.map +1 -0
- package/services/storage/clients/contacts.generated.d.ts +3 -0
- package/services/storage/clients/contacts.generated.d.ts.map +1 -0
- package/services/storage/clients/contacts.generated.js +1197 -0
- package/services/storage/clients/contacts.generated.js.map +1 -0
- package/services/storage/clients/contacts.js +141 -0
- package/services/storage/clients/contacts.js.map +1 -0
- package/services/storage/common.d.ts +667 -0
- package/services/storage/common.d.ts.map +1 -0
- package/services/storage/common.generated.d.ts +17 -0
- package/services/storage/common.generated.d.ts.map +1 -0
- package/services/storage/common.generated.js +863 -0
- package/services/storage/common.generated.js.map +1 -0
- package/services/storage/common.js +587 -0
- package/services/storage/common.js.map +1 -0
- package/services/storage/lib/validators.d.ts +64 -0
- package/services/storage/lib/validators.d.ts.map +1 -0
- package/services/storage/lib/validators.js +82 -0
- package/services/storage/lib/validators.js.map +1 -0
- package/services/storage/server.d.ts +127 -0
- package/services/storage/server.d.ts.map +1 -0
- package/services/storage/server.js +626 -0
- package/services/storage/server.js.map +1 -0
- package/services/storage/test-utils.d.ts +70 -0
- package/services/storage/test-utils.d.ts.map +1 -0
- package/services/storage/test-utils.js +347 -0
- package/services/storage/test-utils.js.map +1 -0
- package/services/storage/utils.d.ts +3 -0
- package/services/storage/utils.d.ts.map +1 -0
- package/services/storage/utils.js +10 -0
- package/services/storage/utils.js.map +1 -0
- package/services/username/client.d.ts +145 -0
- package/services/username/client.d.ts.map +1 -0
- package/services/username/client.js +681 -0
- package/services/username/client.js.map +1 -0
- package/services/username/common.d.ts +136 -0
- package/services/username/common.d.ts.map +1 -0
- package/services/username/common.generated.d.ts +13 -0
- package/services/username/common.generated.d.ts.map +1 -0
- package/services/username/common.generated.js +256 -0
- package/services/username/common.generated.js.map +1 -0
- package/services/username/common.js +226 -0
- package/services/username/common.js.map +1 -0
- package/services/username/server.d.ts +49 -0
- package/services/username/server.d.ts.map +1 -0
- package/services/username/server.js +262 -0
- 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
|