@tinycloud/sdk-services 2.3.0-beta.2 → 2.3.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -921,6 +921,18 @@ var PrefixedKVService = class _PrefixedKVService {
921
921
  const fullKey = this.getFullKey(key);
922
922
  return this._kv.put(fullKey, value, { ...options, prefix: "" });
923
923
  }
924
+ /**
925
+ * Store multiple values within this prefix in one TinyCloud KV invocation.
926
+ */
927
+ async batchPut(items, options) {
928
+ return this._kv.batchPut(
929
+ items.map((item) => ({
930
+ ...item,
931
+ key: this.getFullKey(item.key)
932
+ })),
933
+ { ...options, prefix: "" }
934
+ );
935
+ }
924
936
  /**
925
937
  * List keys within this prefix.
926
938
  */
@@ -974,6 +986,12 @@ var KVAction = {
974
986
  };
975
987
 
976
988
  // src/kv/KVService.ts
989
+ function encodeKvBatchPartName(path) {
990
+ return encodeURIComponent(path).replace(
991
+ /[!'()*]/g,
992
+ (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
993
+ );
994
+ }
977
995
  var KVService = class extends BaseService {
978
996
  /**
979
997
  * Create a new KVService instance.
@@ -1082,6 +1100,53 @@ var KVService = class extends BaseService {
1082
1100
  signal: this.combineSignals(signal)
1083
1101
  });
1084
1102
  }
1103
+ serializeBatchPutValue(item) {
1104
+ const contentType = item.contentType;
1105
+ if (item.value instanceof Blob) {
1106
+ if (!contentType || item.value.type === contentType) {
1107
+ return item.value;
1108
+ }
1109
+ return new Blob([item.value], { type: contentType });
1110
+ }
1111
+ if (item.value instanceof ArrayBuffer) {
1112
+ return new Blob([item.value], {
1113
+ type: contentType ?? "application/octet-stream"
1114
+ });
1115
+ }
1116
+ if (ArrayBuffer.isView(item.value)) {
1117
+ const value = item.value;
1118
+ const bytes = new Uint8Array(value.byteLength);
1119
+ bytes.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
1120
+ return new Blob([bytes], {
1121
+ type: contentType ?? "application/octet-stream"
1122
+ });
1123
+ }
1124
+ if (typeof item.value === "string") {
1125
+ return new Blob([item.value], {
1126
+ type: contentType ?? "text/plain;charset=UTF-8"
1127
+ });
1128
+ }
1129
+ const json = JSON.stringify(item.value);
1130
+ if (json === void 0) {
1131
+ throw new Error(`Cannot JSON serialize KV batch value for key "${item.key}"`);
1132
+ }
1133
+ return new Blob([json], {
1134
+ type: contentType ?? "application/json"
1135
+ });
1136
+ }
1137
+ normalizeBatchPutResponse(data) {
1138
+ if (!data || typeof data !== "object") {
1139
+ return void 0;
1140
+ }
1141
+ const response = data;
1142
+ if (!Array.isArray(response.written) || !response.written.every((key) => typeof key === "string") || typeof response.count !== "number") {
1143
+ return void 0;
1144
+ }
1145
+ return {
1146
+ written: response.written,
1147
+ count: response.count
1148
+ };
1149
+ }
1085
1150
  /**
1086
1151
  * Create KVResponseHeaders from fetch response headers.
1087
1152
  *
@@ -1283,6 +1348,107 @@ var KVService = class extends BaseService {
1283
1348
  }
1284
1349
  });
1285
1350
  }
1351
+ /**
1352
+ * Store multiple values in one TinyCloud KV invocation.
1353
+ */
1354
+ async batchPut(items, options) {
1355
+ return this.withTelemetry("batchPut", String(items.length), async () => {
1356
+ if (!this.requireAuth()) {
1357
+ return err(authRequiredError("kv"));
1358
+ }
1359
+ if (items.length === 0) {
1360
+ return ok({ written: [], count: 0 });
1361
+ }
1362
+ if (!this.context.invokeAny) {
1363
+ return err(
1364
+ serviceError(
1365
+ ErrorCodes.INVALID_INPUT,
1366
+ "KV batchPut requires SDK runtime support for multi-resource invocations",
1367
+ "kv"
1368
+ )
1369
+ );
1370
+ }
1371
+ const session = this.context.session;
1372
+ const paths = items.map((item) => this.getFullPath(item.key, options?.prefix));
1373
+ const seen = /* @__PURE__ */ new Set();
1374
+ for (const path of paths) {
1375
+ if (seen.has(path)) {
1376
+ return err(
1377
+ serviceError(
1378
+ ErrorCodes.INVALID_INPUT,
1379
+ `KV batchPut received duplicate key after prefix resolution: ${path}`,
1380
+ "kv"
1381
+ )
1382
+ );
1383
+ }
1384
+ seen.add(path);
1385
+ }
1386
+ try {
1387
+ const body = new FormData();
1388
+ for (let index = 0; index < items.length; index++) {
1389
+ body.append(
1390
+ encodeKvBatchPartName(paths[index]),
1391
+ this.serializeBatchPutValue(items[index])
1392
+ );
1393
+ }
1394
+ const headers = this.context.invokeAny(
1395
+ session,
1396
+ paths.map((path) => ({
1397
+ spaceId: session.spaceId,
1398
+ service: "kv",
1399
+ path,
1400
+ action: KVAction.PUT
1401
+ }))
1402
+ );
1403
+ const response = await this.context.fetch(`${this.host}/invoke`, {
1404
+ method: "POST",
1405
+ headers,
1406
+ body,
1407
+ signal: this.combineSignals(options?.signal)
1408
+ });
1409
+ if (!response.ok) {
1410
+ const errorText = await response.text();
1411
+ if (response.status === 401 || response.status === 403) {
1412
+ const { resource, action } = parseAuthError(errorText);
1413
+ return err(authUnauthorizedError("kv", errorText, {
1414
+ status: response.status,
1415
+ ...action && { requiredAction: action },
1416
+ ...resource && { resource }
1417
+ }));
1418
+ }
1419
+ const quotaError = this.handleQuotaErrorResponse(
1420
+ response,
1421
+ errorText,
1422
+ "batch"
1423
+ );
1424
+ if (quotaError) {
1425
+ return quotaError;
1426
+ }
1427
+ return err(
1428
+ serviceError(
1429
+ ErrorCodes.KV_WRITE_FAILED,
1430
+ `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
1431
+ "kv",
1432
+ { meta: { status: response.status, statusText: response.statusText } }
1433
+ )
1434
+ );
1435
+ }
1436
+ const batchResponse = this.normalizeBatchPutResponse(await response.json());
1437
+ if (!batchResponse || batchResponse.count !== batchResponse.written.length) {
1438
+ return err(
1439
+ serviceError(
1440
+ ErrorCodes.NETWORK_ERROR,
1441
+ "KV batchPut response did not include matching written keys and count",
1442
+ "kv"
1443
+ )
1444
+ );
1445
+ }
1446
+ return ok(batchResponse);
1447
+ } catch (error) {
1448
+ return err(wrapError("kv", error));
1449
+ }
1450
+ });
1451
+ }
1286
1452
  /**
1287
1453
  * List keys with optional prefix filtering.
1288
1454
  */
@@ -4538,6 +4704,7 @@ function canonicalHashHex(sha256, value) {
4538
4704
  // src/encryption/networkId.ts
4539
4705
  var URN_PREFIX = "urn:tinycloud:encryption:";
4540
4706
  var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
4707
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
4541
4708
  var NetworkIdError = class extends Error {
4542
4709
  constructor(message) {
4543
4710
  super(message);
@@ -4604,6 +4771,22 @@ function isNetworkId(networkId) {
4604
4771
  return false;
4605
4772
  }
4606
4773
  }
4774
+ function parsePkhOwnerDid(ownerDid) {
4775
+ const match = ownerDid.match(PKH_EIP155_DID_RE);
4776
+ if (!match) return null;
4777
+ return {
4778
+ chainId: match[1],
4779
+ address: match[2].toLowerCase()
4780
+ };
4781
+ }
4782
+ function ownerDidMatches(a, b) {
4783
+ const aPkh = parsePkhOwnerDid(a);
4784
+ const bPkh = parsePkhOwnerDid(b);
4785
+ if (aPkh && bPkh) {
4786
+ return aPkh.chainId === bPkh.chainId && aPkh.address === bPkh.address;
4787
+ }
4788
+ return a === b;
4789
+ }
4607
4790
  function networkDiscoveryKey(name) {
4608
4791
  if (!NETWORK_NAME_RE.test(name)) {
4609
4792
  throw new NetworkIdError(
@@ -4739,7 +4922,19 @@ async function discoverNetwork(input) {
4739
4922
  };
4740
4923
  }
4741
4924
  function validateDescriptor(descriptor, networkId, ownerDid, name) {
4742
- if (descriptor.networkId !== networkId) {
4925
+ let descriptorNetwork;
4926
+ try {
4927
+ descriptorNetwork = parseNetworkId(descriptor.networkId);
4928
+ } catch (err3) {
4929
+ return {
4930
+ ok: false,
4931
+ error: encryptionError({
4932
+ code: "INVALID_NETWORK_ID",
4933
+ message: `descriptor networkId is malformed: ${err3 instanceof Error ? err3.message : String(err3)}`
4934
+ })
4935
+ };
4936
+ }
4937
+ if (descriptorNetwork.name !== name || !ownerDidMatches(descriptorNetwork.ownerDid, ownerDid)) {
4743
4938
  return {
4744
4939
  ok: false,
4745
4940
  error: encryptionError({
@@ -4748,7 +4943,8 @@ function validateDescriptor(descriptor, networkId, ownerDid, name) {
4748
4943
  })
4749
4944
  };
4750
4945
  }
4751
- if (descriptor.ownerDid !== ownerDid) {
4946
+ const descriptorOwnerDid = descriptorOwner(descriptor);
4947
+ if (descriptorOwnerDid === void 0 || !ownerDidMatches(descriptorOwnerDid, ownerDid) || !ownerDidMatches(descriptorOwnerDid, descriptorNetwork.ownerDid)) {
4752
4948
  return {
4753
4949
  ok: false,
4754
4950
  error: encryptionError({
@@ -4775,7 +4971,20 @@ function validateDescriptor(descriptor, networkId, ownerDid, name) {
4775
4971
  })
4776
4972
  };
4777
4973
  }
4778
- return { ok: true, data: descriptor };
4974
+ return {
4975
+ ok: true,
4976
+ data: {
4977
+ ...descriptor,
4978
+ ownerDid: descriptorOwnerDid
4979
+ }
4980
+ };
4981
+ }
4982
+ function descriptorOwner(descriptor) {
4983
+ if (typeof descriptor.ownerDid === "string" && descriptor.ownerDid.length > 0) {
4984
+ return descriptor.ownerDid;
4985
+ }
4986
+ const legacyDescriptor = descriptor;
4987
+ return typeof legacyDescriptor.principal === "string" && legacyDescriptor.principal.length > 0 ? legacyDescriptor.principal : void 0;
4779
4988
  }
4780
4989
  function ensureNetworkUsableForDecrypt(descriptor) {
4781
4990
  if (descriptor.state === "active" || descriptor.state === "rotating") {