@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,626 @@
1
+ import * as KeetaAnchorHTTPServer from '../../lib/http-server/index.js';
2
+ import { KeetaNet } from '../../client/index.js';
3
+ import { KeetaAnchorUserError } from '../../lib/error.js';
4
+ import { assertKeetaStorageAnchorDeleteResponse, assertKeetaStorageAnchorPutResponse, assertKeetaStorageAnchorGetRequest, assertKeetaStorageAnchorSearchRequest, assertKeetaStorageAnchorSearchResponse, assertKeetaStorageAnchorQuotaResponse } from './common.generated.js';
5
+ import { getKeetaStorageAnchorDeleteRequestSigningData, getKeetaStorageAnchorPutRequestSigningData, getKeetaStorageAnchorGetRequestSigningData, getKeetaStorageAnchorSearchRequestSigningData, getKeetaStorageAnchorQuotaRequestSigningData, parseContainerPayload, Errors, CONTENT_TYPE_OCTET_STREAM, DEFAULT_SIGNED_URL_TTL_SECONDS } from './common.js';
6
+ import { VerifySignedData } from '../../lib/utils/signing.js';
7
+ import { assertHTTPSignedField, parseSignatureFromURL } from '../../lib/http-server/common.js';
8
+ import { arrayBufferLikeToBuffer, Buffer } from '../../lib/utils/buffer.js';
9
+ import { requiresValidation, findMatchingValidators } from './lib/validators.js';
10
+ import { EncryptedContainer, EncryptedContainerError } from '../../lib/encrypted-container.js';
11
+ import { assertVisibility } from './utils.js';
12
+ /**
13
+ * Build a standardized search response from search results.
14
+ */
15
+ function buildSearchResponse(results) {
16
+ const response = {
17
+ ok: true,
18
+ results: results.results
19
+ };
20
+ if (results.nextCursor !== undefined) {
21
+ response.nextCursor = results.nextCursor;
22
+ }
23
+ return (response);
24
+ }
25
+ // #region Module-Level Helpers
26
+ /**
27
+ * Find a matching policy for a path, validate it, and check access.
28
+ *
29
+ * @param pathPolicies - Array of path policies to check against
30
+ * @param account - The account to check access for
31
+ * @param path - The path to check
32
+ * @param operation - The operation being performed
33
+ */
34
+ function assertPathAccess(pathPolicies, account, path, operation) {
35
+ for (const policy of pathPolicies) {
36
+ const parsed = policy.parse(path);
37
+ if (parsed !== null) {
38
+ policy.validate(path);
39
+ if (!policy.checkAccess(account, parsed, operation)) {
40
+ throw (new Errors.AccessDenied('Can only access your own namespace'));
41
+ }
42
+ return ({ policy, parsed });
43
+ }
44
+ }
45
+ throw (new Errors.InvalidPath('Path does not match any policy'));
46
+ }
47
+ /**
48
+ * Find a matching policy and parse a path.
49
+ * Used for public endpoints where auth is optional.
50
+ *
51
+ * @param pathPolicies - Array of path policies to check against
52
+ * @param path - The path to parse
53
+ */
54
+ function parsePath(pathPolicies, path) {
55
+ for (const policy of pathPolicies) {
56
+ const parsed = policy.parse(path);
57
+ if (parsed !== null) {
58
+ policy.validate(path);
59
+ return ({ policy, parsed });
60
+ }
61
+ }
62
+ throw (new Errors.InvalidPath('Path does not match any policy'));
63
+ }
64
+ /**
65
+ * Verify a signed request from POST body.
66
+ * Extracts account and signature from the request, verifies the signature,
67
+ * and returns the authenticated account.
68
+ *
69
+ * @typeParam T - Request type containing optional account and signed fields
70
+ *
71
+ * @param request - The request object containing account and signed fields
72
+ * @param getSigningData - Function to extract signable data from the request
73
+ *
74
+ * @returns The authenticated account
75
+ *
76
+ * @throws KeetaAnchorUserError if authentication is missing or invalid
77
+ */
78
+ async function verifyBodyAuth(request, getSigningData) {
79
+ if (!request.account || !request.signed) {
80
+ throw (new KeetaAnchorUserError('Authentication required'));
81
+ }
82
+ const account = KeetaNet.lib.Account.fromPublicKeyString(request.account).assertAccount();
83
+ const signable = getSigningData(request);
84
+ const signed = assertHTTPSignedField(request.signed);
85
+ const valid = await VerifySignedData(account, signable, signed);
86
+ if (!valid) {
87
+ throw (new KeetaAnchorUserError('Invalid signature'));
88
+ }
89
+ return (account);
90
+ }
91
+ /**
92
+ * Verify a signed request from URL query parameters.
93
+ * Parses signature from URL, builds a request object, verifies the signature,
94
+ * and returns the authenticated account.
95
+ *
96
+ * @typeParam T - Request type to build from the account public key
97
+ *
98
+ * @param url - The URL containing signature query parameters
99
+ * @param getSigningData - Function to extract signable data from the request
100
+ * @param buildRequest - Function to build a request object from the account public key
101
+ *
102
+ * @returns The authenticated account
103
+ *
104
+ * @throws KeetaAnchorUserError if authentication is missing or invalid
105
+ */
106
+ async function verifyURLAuth(url, getSigningData, buildRequest) {
107
+ let urlString;
108
+ if (typeof url === 'string') {
109
+ urlString = url;
110
+ }
111
+ else {
112
+ urlString = url.href;
113
+ }
114
+ const parsed = parseSignatureFromURL(urlString);
115
+ if (!parsed.account || !parsed.signedField) {
116
+ throw (new KeetaAnchorUserError('Authentication required'));
117
+ }
118
+ const request = buildRequest(parsed.account.publicKeyString.get());
119
+ const signable = getSigningData(request);
120
+ const valid = await VerifySignedData(parsed.account, signable, parsed.signedField);
121
+ if (!valid) {
122
+ throw (new KeetaAnchorUserError('Invalid signature'));
123
+ }
124
+ return (parsed.account);
125
+ }
126
+ /**
127
+ * Extract object path from wildcard route parameter.
128
+ * Prepends a leading slash to create a valid storage path.
129
+ *
130
+ * @param params - Route parameters containing the wildcard match
131
+ *
132
+ * @returns The object path with leading slash
133
+ *
134
+ * @throws InvalidPath if wildcard parameter is missing
135
+ */
136
+ function extractObjectPath(params) {
137
+ const wildcardPath = params.get('**');
138
+ if (!wildcardPath) {
139
+ throw (new Errors.InvalidPath());
140
+ }
141
+ return ('/' + wildcardPath);
142
+ }
143
+ /**
144
+ * Authorize access to an object path via URL-signed request.
145
+ * Combines path validation, signature verification, and access control.
146
+ *
147
+ * @typeParam T - Request type to build from path and account
148
+ *
149
+ * @param pathPolicies - Array of path policies to check against
150
+ * @param params - Route parameters containing the wildcard path
151
+ * @param url - The URL containing signature query parameters
152
+ * @param operation - The operation being authorized
153
+ * @param getSigningData - Function to extract signable data from the request
154
+ * @param buildRequest - Function to build a request object from path and account
155
+ *
156
+ * @returns The authenticated account and validated object path
157
+ *
158
+ * @throws InvalidPath if path is invalid or doesn't match any policy
159
+ * @throws AccessDenied if user doesn't have access to the path
160
+ * @throws KeetaAnchorUserError if signature is invalid
161
+ */
162
+ async function authorizeURLAccess(pathPolicies, params, url, operation, getSigningData, buildRequest) {
163
+ const objectPath = extractObjectPath(params);
164
+ parsePath(pathPolicies, objectPath);
165
+ const account = await verifyURLAuth(url, getSigningData, function (pubKey) {
166
+ return (buildRequest(objectPath, pubKey));
167
+ });
168
+ assertPathAccess(pathPolicies, account, objectPath, operation);
169
+ return ({ account, objectPath });
170
+ }
171
+ // Default quota configuration
172
+ const DEFAULT_QUOTAS = {
173
+ maxObjectSize: 10 * 1024 * 1024, // 10MB
174
+ maxObjectsPerUser: 1000,
175
+ maxStoragePerUser: 100 * 1024 * 1024, // 100MB
176
+ maxSearchLimit: 100,
177
+ maxSignedUrlTTL: 86400 // 24 hours
178
+ };
179
+ // Default tag validation configuration
180
+ const DEFAULT_TAG_VALIDATION = {
181
+ maxTags: 10,
182
+ maxTagLength: 50,
183
+ pattern: /^[a-zA-Z0-9_-]+$/
184
+ };
185
+ export class KeetaNetStorageAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAnchorHTTPServer {
186
+ homepage;
187
+ backend;
188
+ anchorAccount;
189
+ quotas;
190
+ validators;
191
+ signedUrlDefaultTTL;
192
+ publicCorsOrigin;
193
+ pathPolicies;
194
+ tagValidation;
195
+ constructor(config) {
196
+ super(config);
197
+ this.homepage = config.homepage ?? '';
198
+ this.backend = config.backend;
199
+ this.anchorAccount = config.anchorAccount;
200
+ this.quotas = { ...DEFAULT_QUOTAS, ...config.quotas };
201
+ this.validators = config.validators ?? [];
202
+ this.signedUrlDefaultTTL = config.signedUrlDefaultTTL ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
203
+ this.publicCorsOrigin = config.publicCorsOrigin ?? false;
204
+ this.pathPolicies = config.pathPolicies;
205
+ this.tagValidation = {
206
+ maxTags: config.tagValidation?.maxTags ?? DEFAULT_TAG_VALIDATION.maxTags,
207
+ maxTagLength: config.tagValidation?.maxTagLength ?? DEFAULT_TAG_VALIDATION.maxTagLength,
208
+ pattern: config.tagValidation?.pattern ?? DEFAULT_TAG_VALIDATION.pattern
209
+ };
210
+ // Validate anchorAccount has private key
211
+ if (!this.anchorAccount.hasPrivateKey) {
212
+ throw (new Error('anchorAccount must have a private key'));
213
+ }
214
+ // Validate at least one path policy is provided
215
+ if (this.pathPolicies.length === 0) {
216
+ throw (new Error('At least one path policy must be provided'));
217
+ }
218
+ // Validate quota configuration values are positive
219
+ if (this.quotas.maxObjectSize <= 0) {
220
+ throw (new Error('quotas.maxObjectSize must be positive'));
221
+ }
222
+ if (this.quotas.maxObjectsPerUser <= 0) {
223
+ throw (new Error('quotas.maxObjectsPerUser must be positive'));
224
+ }
225
+ if (this.quotas.maxStoragePerUser <= 0) {
226
+ throw (new Error('quotas.maxStoragePerUser must be positive'));
227
+ }
228
+ if (this.quotas.maxSearchLimit <= 0) {
229
+ throw (new Error('quotas.maxSearchLimit must be positive'));
230
+ }
231
+ if (this.quotas.maxSignedUrlTTL <= 0) {
232
+ throw (new Error('quotas.maxSignedUrlTTL must be positive'));
233
+ }
234
+ // Validate tag validation configuration
235
+ if (this.tagValidation.maxTags <= 0) {
236
+ throw (new Error('tagValidation.maxTags must be positive'));
237
+ }
238
+ if (this.tagValidation.maxTagLength <= 0) {
239
+ throw (new Error('tagValidation.maxTagLength must be positive'));
240
+ }
241
+ }
242
+ // Note: We use this.* properties instead of config.*.
243
+ // The config parameter is required by the abstract method signature but unused here.
244
+ async initRoutes(_ignoreConfig) {
245
+ const routes = {};
246
+ const backend = this.backend;
247
+ const anchorAccount = this.anchorAccount;
248
+ const quotas = this.quotas;
249
+ const validators = this.validators;
250
+ const publicCorsOrigin = this.publicCorsOrigin;
251
+ const pathPolicies = this.pathPolicies;
252
+ const tagValidation = this.tagValidation;
253
+ const logger = this.logger;
254
+ /**
255
+ * Build a JSON response with assertion.
256
+ */
257
+ function jsonResponse(response, assertionHandler) {
258
+ return ({ output: JSON.stringify(assertionHandler(response)) });
259
+ }
260
+ /**
261
+ * Get an object or throw DocumentNotFound.
262
+ */
263
+ async function requireObject(path) {
264
+ const result = await backend.get(path);
265
+ if (!result) {
266
+ throw (new Errors.DocumentNotFound());
267
+ }
268
+ return (result);
269
+ }
270
+ /**
271
+ * Enforce server-side search limit cap.
272
+ */
273
+ function enforceSearchLimit(pagination) {
274
+ const requestedLimit = pagination?.limit ?? quotas.maxSearchLimit;
275
+ return ({ ...pagination, limit: Math.min(requestedLimit, quotas.maxSearchLimit) });
276
+ }
277
+ /**
278
+ * Validate search results match expected constraints.
279
+ */
280
+ function assertSearchResults(results, constraint) {
281
+ for (const obj of results.results) {
282
+ if (constraint.visibility && obj.visibility !== constraint.visibility) {
283
+ throw (new Errors.InvariantViolation(`Backend returned ${obj.visibility} object in ${constraint.visibility} search`));
284
+ }
285
+ if (constraint.owner && obj.owner !== constraint.owner) {
286
+ throw (new Errors.InvariantViolation(`Backend returned object owned by ${obj.owner} in search for ${constraint.owner}`));
287
+ }
288
+ }
289
+ }
290
+ /**
291
+ * If a homepage is provided, setup the route for it
292
+ */
293
+ const homepage = this.homepage;
294
+ if (homepage) {
295
+ routes['GET /'] = async function () {
296
+ let homepageData;
297
+ if (typeof homepage === 'string') {
298
+ homepageData = homepage;
299
+ }
300
+ else {
301
+ homepageData = await homepage();
302
+ }
303
+ return ({
304
+ output: homepageData,
305
+ contentType: 'text/html'
306
+ });
307
+ };
308
+ }
309
+ // #region API Routes
310
+ // PUT /api/object/* - Create or update an object
311
+ routes['PUT /api/object/**'] = {
312
+ bodyType: 'raw',
313
+ maxBodySize: quotas.maxObjectSize,
314
+ handler: async function (params, postData, _headers, url) {
315
+ const objectPath = extractObjectPath(params);
316
+ // Get metadata from query params
317
+ const parsedUrl = new URL(url);
318
+ const visibilityParam = parsedUrl.searchParams.get('visibility');
319
+ const tagsParam = parsedUrl.searchParams.get('tags');
320
+ // Default to private when absent, assert valid value otherwise
321
+ let visibility = 'private';
322
+ if (visibilityParam !== null) {
323
+ visibility = assertVisibility(visibilityParam);
324
+ }
325
+ const rawTags = (tagsParam ?? '')
326
+ .split(',')
327
+ .map(function (t) {
328
+ return (t.trim());
329
+ })
330
+ .filter(function (t) {
331
+ return (t.length > 0);
332
+ });
333
+ // Verify signature
334
+ const account = await verifyURLAuth(url, getKeetaStorageAnchorPutRequestSigningData, function () {
335
+ return ({ path: objectPath, visibility, tags: rawTags });
336
+ });
337
+ // Validate tags
338
+ const { maxTags, maxTagLength, pattern: tagPattern } = tagValidation;
339
+ for (const tag of rawTags) {
340
+ if (tag.length > maxTagLength) {
341
+ throw (new Errors.InvalidTag(`Tag exceeds maximum length of ${maxTagLength}: "${tag}"`));
342
+ }
343
+ // Reset lastIndex in case the pattern has the global or sticky flag
344
+ tagPattern.lastIndex = 0;
345
+ if (!tagPattern.test(tag)) {
346
+ throw (new Errors.InvalidTag(`Tag contains invalid characters: "${tag}"`));
347
+ }
348
+ }
349
+ if (rawTags.length > maxTags) {
350
+ throw (new Errors.InvalidTag(`Too many tags: ${rawTags.length} exceeds maximum of ${maxTags}`));
351
+ }
352
+ const tags = rawTags;
353
+ // Validate path format, metadata, and ownership
354
+ const { policy, parsed } = assertPathAccess(pathPolicies, account, objectPath, 'put');
355
+ const owner = account.publicKeyString.get();
356
+ if (policy.validateMetadata) {
357
+ policy.validateMetadata(parsed, { owner, tags, visibility });
358
+ }
359
+ // Resolve per-user quota limits, falling back to global config
360
+ const userLimits = backend.getQuotaLimits
361
+ ? await backend.getQuotaLimits(owner)
362
+ : null;
363
+ const effectiveLimits = userLimits ?? quotas;
364
+ // Body is raw binary (EncryptedContainer)
365
+ const data = arrayBufferLikeToBuffer(postData);
366
+ const objectSize = data.byteLength;
367
+ if (objectSize > effectiveLimits.maxObjectSize) {
368
+ throw (new Errors.QuotaExceeded({
369
+ quotaType: 'maxObjectSize',
370
+ limit: effectiveLimits.maxObjectSize,
371
+ current: objectSize
372
+ }));
373
+ }
374
+ const needsValidation = requiresValidation(objectPath, validators);
375
+ const needsAnchorDecryption = needsValidation || visibility === 'public';
376
+ if (needsAnchorDecryption) {
377
+ try {
378
+ const container = EncryptedContainer.fromEncryptedBuffer(data, [anchorAccount]);
379
+ const plaintext = await container.getPlaintext();
380
+ if (needsValidation) {
381
+ // Extract content and mimeType from encrypted payload
382
+ const { content, mimeType } = parseContainerPayload(plaintext);
383
+ const matchingValidators = findMatchingValidators(objectPath, validators);
384
+ for (const validator of matchingValidators) {
385
+ const result = await validator.validate(objectPath, content, mimeType);
386
+ if (!result.valid) {
387
+ throw (new Errors.ValidationFailed(result.error));
388
+ }
389
+ }
390
+ }
391
+ }
392
+ catch (e) {
393
+ if (Errors.ValidationFailed.isInstance(e)) {
394
+ throw (e);
395
+ }
396
+ if (EncryptedContainerError.isInstance(e)) {
397
+ if (e.code.startsWith('MALFORMED_')) {
398
+ throw (new Errors.ValidationFailed(`Invalid encrypted container: ${e.message}`));
399
+ }
400
+ if (e.code === 'NO_MATCHING_KEY' || e.code === 'DECRYPTION_FAILED') {
401
+ throw (new Errors.AnchorPrincipalRequired());
402
+ }
403
+ }
404
+ throw (e);
405
+ }
406
+ }
407
+ // Reserve quota before upload
408
+ const reservation = await backend.reserveUpload(owner, objectPath, objectSize, {
409
+ quotaLimits: {
410
+ maxObjectsPerUser: effectiveLimits.maxObjectsPerUser,
411
+ maxStoragePerUser: effectiveLimits.maxStoragePerUser
412
+ }
413
+ });
414
+ let objectMetadata;
415
+ try {
416
+ objectMetadata = await backend.put(objectPath, data, {
417
+ owner,
418
+ tags,
419
+ visibility
420
+ });
421
+ await backend.commitUpload(reservation.id);
422
+ }
423
+ catch (e) {
424
+ try {
425
+ await backend.releaseUpload(reservation.id);
426
+ }
427
+ catch (releaseError) {
428
+ /**
429
+ * This provides a hint for cleanup
430
+ */
431
+ logger?.warn('Failed to release upload reservation', { reservationId: reservation.id, error: releaseError });
432
+ }
433
+ throw (e);
434
+ }
435
+ const response = {
436
+ ok: true,
437
+ object: objectMetadata
438
+ };
439
+ return (jsonResponse(response, assertKeetaStorageAnchorPutResponse));
440
+ }
441
+ };
442
+ // GET /api/object/* - Retrieve an object
443
+ routes['GET /api/object/**'] = async function (params, _postData, _headers, url) {
444
+ const { objectPath } = await authorizeURLAccess(pathPolicies, params, url, 'get', getKeetaStorageAnchorGetRequestSigningData, function (path, pubKey) {
445
+ return (assertKeetaStorageAnchorGetRequest({ path, account: pubKey }));
446
+ });
447
+ const result = await requireObject(objectPath);
448
+ return ({
449
+ output: result.data,
450
+ contentType: CONTENT_TYPE_OCTET_STREAM
451
+ });
452
+ };
453
+ // DELETE /api/object/* - Delete an object
454
+ routes['DELETE /api/object/**'] = async function (params, _postData, _headers, url) {
455
+ const { objectPath } = await authorizeURLAccess(pathPolicies, params, url, 'delete', getKeetaStorageAnchorDeleteRequestSigningData, function (path, pubKey) {
456
+ return ({ path, account: pubKey });
457
+ });
458
+ const deleted = await backend.delete(objectPath);
459
+ const response = {
460
+ ok: true,
461
+ deleted
462
+ };
463
+ return (jsonResponse(response, assertKeetaStorageAnchorDeleteResponse));
464
+ };
465
+ // GET /api/metadata/* - Get object metadata
466
+ routes['GET /api/metadata/**'] = async function (params, _postData, _headers, url) {
467
+ const { objectPath } = await authorizeURLAccess(pathPolicies, params, url, 'metadata', getKeetaStorageAnchorGetRequestSigningData, function (path, pubKey) {
468
+ return (assertKeetaStorageAnchorGetRequest({ path, account: pubKey }));
469
+ });
470
+ const result = await requireObject(objectPath);
471
+ return (jsonResponse({ ok: true, object: result.metadata }, assertKeetaStorageAnchorPutResponse));
472
+ };
473
+ // POST /api/search - Search for objects
474
+ routes['POST /api/search'] = async function (_params, postData) {
475
+ const request = assertKeetaStorageAnchorSearchRequest(postData);
476
+ const account = await verifyBodyAuth(request, getKeetaStorageAnchorSearchRequestSigningData);
477
+ const accountPubKey = account.publicKeyString.get();
478
+ // Check if searching for public objects outside namespace
479
+ const searchingPublic = request.criteria.visibility === 'public';
480
+ if (searchingPublic) {
481
+ // When searching for public objects, we allow searching outside the caller's namespace
482
+ // but only for objects with visibility: 'public'
483
+ const scopedCriteria = {
484
+ ...request.criteria,
485
+ visibility: 'public'
486
+ };
487
+ const results = await backend.search(scopedCriteria, enforceSearchLimit(request.pagination));
488
+ assertSearchResults(results, { visibility: 'public' });
489
+ return (jsonResponse(buildSearchResponse(results), assertKeetaStorageAnchorSearchResponse));
490
+ }
491
+ // Scope search to authenticated account's namespace
492
+ const scopedCriteria = {
493
+ ...request.criteria,
494
+ owner: accountPubKey
495
+ };
496
+ const results = await backend.search(scopedCriteria, enforceSearchLimit(request.pagination));
497
+ assertSearchResults(results, { owner: accountPubKey });
498
+ return (jsonResponse(buildSearchResponse(results), assertKeetaStorageAnchorSearchResponse));
499
+ };
500
+ // GET /api/quota - Get quota status
501
+ routes['GET /api/quota'] = async function (_params, _postData, _headers, url) {
502
+ const account = await verifyURLAuth(url, getKeetaStorageAnchorQuotaRequestSigningData, function () { return ({}); });
503
+ // Get current usage from backend and compute remaining using per-user or global limits
504
+ const owner = account.publicKeyString.get();
505
+ const userLimits = backend.getQuotaLimits
506
+ ? await backend.getQuotaLimits(owner)
507
+ : null;
508
+ const effectiveLimits = userLimits ?? quotas;
509
+ const backendStatus = await backend.getQuotaStatus(owner);
510
+ // Compute remaining from config limits
511
+ let remainingObjects = Math.max(0, effectiveLimits.maxObjectsPerUser - backendStatus.objectCount);
512
+ let remainingSize = Math.max(0, effectiveLimits.maxStoragePerUser - backendStatus.totalSize);
513
+ // If backend reports its own remaining values, use the tighter constraint
514
+ if (backendStatus.remainingObjects !== undefined && backendStatus.remainingObjects > 0) {
515
+ remainingObjects = Math.min(backendStatus.remainingObjects, remainingObjects);
516
+ }
517
+ if (backendStatus.remainingSize !== undefined && backendStatus.remainingSize > 0) {
518
+ remainingSize = Math.min(backendStatus.remainingSize, remainingSize);
519
+ }
520
+ const response = {
521
+ ok: true,
522
+ quota: {
523
+ objectCount: backendStatus.objectCount,
524
+ totalSize: backendStatus.totalSize,
525
+ remainingObjects,
526
+ remainingSize
527
+ }
528
+ };
529
+ return (jsonResponse(response, assertKeetaStorageAnchorQuotaResponse));
530
+ };
531
+ // GET /api/public/** - Public object access via pre-signed URL
532
+ routes['GET /api/public/**'] = async function (params, _postData, _headers, url) {
533
+ const objectPath = extractObjectPath(params);
534
+ const { policy, parsed } = parsePath(pathPolicies, objectPath);
535
+ // Parse signature using standard signed field convention
536
+ const urlParsed = parseSignatureFromURL(url);
537
+ if (!urlParsed.signedField) {
538
+ throw (new Errors.SignatureInvalid('Missing required signature parameters'));
539
+ }
540
+ // Resolve signer: policy-specified or from URL account param (any-signer for public objects)
541
+ const signerAccount = policy.getAuthorizedSigner(parsed) ?? urlParsed.account ?? null;
542
+ if (!signerAccount) {
543
+ throw (new Errors.SignatureInvalid('Missing signer'));
544
+ }
545
+ const signerPubKey = signerAccount.publicKeyString.get();
546
+ // Parse and validate expires param
547
+ const parsedUrl = typeof url === 'string' ? new URL(url) : url;
548
+ const expiresParam = parsedUrl.searchParams.get('expires');
549
+ if (!expiresParam) {
550
+ throw (new Errors.SignatureInvalid('Missing expires parameter'));
551
+ }
552
+ const expiresAt = parseInt(expiresParam, 10);
553
+ if (!Number.isFinite(expiresAt)) {
554
+ throw (new Errors.SignatureInvalid('Invalid expires parameter'));
555
+ }
556
+ if (Date.now() > expiresAt * 1000) {
557
+ throw (new Errors.SignatureExpired());
558
+ }
559
+ // Enforce maximum TTL
560
+ const maxExpiresAt = Math.floor(Date.now() / 1000) + quotas.maxSignedUrlTTL;
561
+ if (expiresAt > maxExpiresAt) {
562
+ throw (new Errors.SignatureExpired('Signed URL TTL exceeds maximum allowed'));
563
+ }
564
+ // Pre-validate signature is valid base64 with reasonable length
565
+ const signatureBuffer = Buffer.from(urlParsed.signedField.signature, 'base64');
566
+ if (signatureBuffer.length < 64 || signatureBuffer.length > 256) {
567
+ throw (new Errors.SignatureInvalid('Invalid signature format'));
568
+ }
569
+ try {
570
+ // Allow 5 minutes of clock skew for signature verification
571
+ const valid = await VerifySignedData(signerAccount, [objectPath, expiresAt, signerPubKey], urlParsed.signedField, {
572
+ maxSkewMs: 5 * 60 * 1000
573
+ });
574
+ if (!valid) {
575
+ throw (new Errors.SignatureInvalid());
576
+ }
577
+ }
578
+ catch (e) {
579
+ if (Errors.SignatureInvalid.isInstance(e)) {
580
+ throw (e);
581
+ }
582
+ throw (new Errors.SignatureInvalid('Signature verification failed'));
583
+ }
584
+ const result = await requireObject(objectPath);
585
+ if (result.metadata.visibility !== 'public') {
586
+ throw (new Errors.AccessDenied('Object is not public'));
587
+ }
588
+ // Decrypt using anchor account and extract mimeType from encrypted payload
589
+ const data = arrayBufferLikeToBuffer(result.data);
590
+ const container = EncryptedContainer.fromEncryptedBuffer(data, [anchorAccount]);
591
+ const plaintext = await container.getPlaintext();
592
+ const { content, mimeType } = parseContainerPayload(plaintext);
593
+ const headers = {};
594
+ if (publicCorsOrigin) {
595
+ headers['Access-Control-Allow-Origin'] = publicCorsOrigin;
596
+ }
597
+ return ({
598
+ output: content,
599
+ contentType: mimeType,
600
+ headers
601
+ });
602
+ };
603
+ // #endregion
604
+ return (routes);
605
+ }
606
+ async serviceMetadata() {
607
+ const authRequired = { options: { authentication: { type: 'required', method: 'keeta-account' } } };
608
+ const operations = {
609
+ put: { url: (new URL('/api/object', this.url)).toString(), ...authRequired },
610
+ get: { url: (new URL('/api/object', this.url)).toString(), ...authRequired },
611
+ delete: { url: (new URL('/api/object', this.url)).toString(), ...authRequired },
612
+ metadata: { url: (new URL('/api/metadata', this.url)).toString(), ...authRequired },
613
+ search: { url: (new URL('/api/search', this.url)).toString(), ...authRequired },
614
+ public: (new URL('/api/public', this.url)).toString(),
615
+ quota: { url: (new URL('/api/quota', this.url)).toString(), ...authRequired }
616
+ };
617
+ return ({
618
+ operations,
619
+ anchorAccount: this.anchorAccount.publicKeyString.get(),
620
+ quotas: this.quotas,
621
+ signedUrlDefaultTTL: this.signedUrlDefaultTTL,
622
+ searchableFields: ['owner', 'tags', 'visibility', 'pathPrefix']
623
+ });
624
+ }
625
+ }
626
+ //# sourceMappingURL=server.js.map