@tinycloud/sdk-services 2.2.0-beta.10 → 2.2.0-beta.13

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
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BaseService: () => BaseService,
24
+ DEFAULT_SIGNED_READ_URL_EXPIRY_MS: () => DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
24
25
  DataVaultService: () => DataVaultService,
25
26
  DatabaseHandle: () => DatabaseHandle,
26
27
  DuckDbAction: () => DuckDbAction,
@@ -904,6 +905,13 @@ var PrefixedKVService = class _PrefixedKVService {
904
905
  const fullKey = this.getFullKey(key);
905
906
  return this._kv.head(fullKey, { ...options, prefix: "" });
906
907
  }
908
+ /**
909
+ * Create a short-lived signed URL for reading a KV object.
910
+ */
911
+ async createSignedReadUrl(key, options) {
912
+ const fullKey = this.getFullKey(key);
913
+ return this._kv.createSignedReadUrl(fullKey, { ...options, prefix: "" });
914
+ }
907
915
  /**
908
916
  * Create a nested prefix-scoped view.
909
917
  */
@@ -915,6 +923,7 @@ var PrefixedKVService = class _PrefixedKVService {
915
923
  };
916
924
 
917
925
  // src/kv/types.ts
926
+ var DEFAULT_SIGNED_READ_URL_EXPIRY_MS = 5 * 60 * 1e3;
918
927
  var KVAction = {
919
928
  GET: "tinycloud.kv/get",
920
929
  PUT: "tinycloud.kv/put",
@@ -999,6 +1008,15 @@ var KVService = class extends BaseService {
999
1008
  get host() {
1000
1009
  return this.context.hosts[0];
1001
1010
  }
1011
+ withJsonContentType(headers) {
1012
+ if (Array.isArray(headers)) {
1013
+ return [...headers, ["content-type", "application/json"]];
1014
+ }
1015
+ return {
1016
+ ...headers,
1017
+ "content-type": "application/json"
1018
+ };
1019
+ }
1002
1020
  /**
1003
1021
  * Execute an invoke operation.
1004
1022
  *
@@ -1068,6 +1086,48 @@ var KVService = class extends BaseService {
1068
1086
  return text;
1069
1087
  }
1070
1088
  }
1089
+ async createSignedReadUrlError(response, key) {
1090
+ let errorText = response.statusText;
1091
+ try {
1092
+ const text = await response.text();
1093
+ if (text) {
1094
+ errorText = text;
1095
+ }
1096
+ } catch {
1097
+ }
1098
+ if (response.status === 401 || response.status === 403) {
1099
+ const { resource, action } = parseAuthError(errorText);
1100
+ return err(authUnauthorizedError("kv", errorText, {
1101
+ status: response.status,
1102
+ ...action && { requiredAction: action },
1103
+ ...resource && { resource }
1104
+ }));
1105
+ }
1106
+ const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
1107
+ return err(
1108
+ serviceError(
1109
+ code,
1110
+ `Failed to create signed read URL for key "${key}": ${response.status} - ${errorText}`,
1111
+ "kv",
1112
+ { meta: { status: response.status, statusText: response.statusText } }
1113
+ )
1114
+ );
1115
+ }
1116
+ normalizeSignedReadUrlResponse(data) {
1117
+ if (!data || typeof data !== "object") {
1118
+ return void 0;
1119
+ }
1120
+ const response = data;
1121
+ if (typeof response.url !== "string" || typeof response.ticketId !== "string" || typeof response.expiresAt !== "string") {
1122
+ return void 0;
1123
+ }
1124
+ return {
1125
+ url: new URL(response.url, this.host).toString(),
1126
+ relativeUrl: response.url,
1127
+ ticketId: response.ticketId,
1128
+ expiresAt: response.expiresAt
1129
+ };
1130
+ }
1071
1131
  /**
1072
1132
  * Get a value by key.
1073
1133
  */
@@ -1340,6 +1400,61 @@ var KVService = class extends BaseService {
1340
1400
  }
1341
1401
  });
1342
1402
  }
1403
+ /**
1404
+ * Create a short-lived signed URL for reading a KV object.
1405
+ */
1406
+ async createSignedReadUrl(key, options) {
1407
+ return this.withTelemetry("createSignedReadUrl", key, async () => {
1408
+ if (!this.requireAuth()) {
1409
+ return err(authRequiredError("kv"));
1410
+ }
1411
+ const path = this.getFullPath(key, options?.prefix);
1412
+ const session = this.context.session;
1413
+ const headers = this.context.invoke(
1414
+ session,
1415
+ "kv",
1416
+ path,
1417
+ KVAction.GET
1418
+ );
1419
+ const body = {
1420
+ space: session.spaceId,
1421
+ path,
1422
+ ttl_seconds: options?.expiresInSeconds ?? Math.ceil(DEFAULT_SIGNED_READ_URL_EXPIRY_MS / 1e3)
1423
+ };
1424
+ if (options?.contentHash !== void 0) {
1425
+ body.content_hash = options.contentHash;
1426
+ }
1427
+ if (options?.etag !== void 0) {
1428
+ body.etag = options.etag;
1429
+ }
1430
+ try {
1431
+ const response = await this.context.fetch(`${this.host}/signed/kv`, {
1432
+ method: "POST",
1433
+ headers: this.withJsonContentType(headers),
1434
+ body: JSON.stringify(body),
1435
+ signal: this.combineSignals(options?.signal)
1436
+ });
1437
+ if (!response.ok) {
1438
+ return this.createSignedReadUrlError(response, key);
1439
+ }
1440
+ const signedUrl = this.normalizeSignedReadUrlResponse(
1441
+ await response.json()
1442
+ );
1443
+ if (!signedUrl) {
1444
+ return err(
1445
+ serviceError(
1446
+ ErrorCodes.NETWORK_ERROR,
1447
+ "Signed read URL response did not include url, ticketId, and expiresAt",
1448
+ "kv"
1449
+ )
1450
+ );
1451
+ }
1452
+ return ok(signedUrl);
1453
+ } catch (error) {
1454
+ return err(wrapError("kv", error));
1455
+ }
1456
+ });
1457
+ }
1343
1458
  /**
1344
1459
  * Create a prefix-scoped view of this KV service.
1345
1460
  *
@@ -2909,6 +3024,9 @@ function base64Decode(str) {
2909
3024
  }
2910
3025
  return bytes;
2911
3026
  }
3027
+ function isUnlockSigner(signer) {
3028
+ return typeof signer === "object" && signer !== null && typeof signer.signMessage === "function";
3029
+ }
2912
3030
  function defaultVaultMessage(input) {
2913
3031
  switch (input.code) {
2914
3032
  case "DECRYPTION_FAILED":
@@ -2946,6 +3064,7 @@ var DataVaultService = class extends BaseService {
2946
3064
  this.masterKey = null;
2947
3065
  this.encryptionIdentity = null;
2948
3066
  this._isUnlocked = false;
3067
+ this.unlockInFlight = null;
2949
3068
  this.vaultConfig = config;
2950
3069
  this._config = config;
2951
3070
  }
@@ -3005,30 +3124,40 @@ var DataVaultService = class extends BaseService {
3005
3124
  * signatures exist (browser only).
3006
3125
  */
3007
3126
  async unlock(signer) {
3008
- return this.withTelemetry("unlock", void 0, async () => {
3127
+ const unlockSigner = isUnlockSigner(signer) ? signer : void 0;
3128
+ if (this._isUnlocked && this.masterKey && (this.encryptionIdentity || !unlockSigner)) {
3129
+ return { ok: true, data: void 0 };
3130
+ }
3131
+ if (this.unlockInFlight) {
3132
+ return this.unlockInFlight;
3133
+ }
3134
+ this.unlockInFlight = this.withTelemetry("unlock", void 0, async () => {
3009
3135
  const spaceId = this.vaultConfig.spaceId;
3010
3136
  const versionConfig = VaultVersionConfig[CURRENT_VAULT_VERSION];
3011
3137
  const masterCacheKey = `vault-master:${spaceId}`;
3012
3138
  const identityCacheKey = `vault-identity:${this.tc.address}`;
3013
3139
  try {
3014
- let masterSigBytes = await loadCachedSignature(masterCacheKey);
3015
- if (!masterSigBytes) {
3016
- if (!signer) {
3017
- return vaultError({
3018
- code: "VAULT_LOCKED",
3019
- message: "Signer is required when no cached master signature exists"
3020
- });
3140
+ if (!this.masterKey) {
3141
+ let masterSigBytes = await loadCachedSignature(masterCacheKey);
3142
+ if (!masterSigBytes) {
3143
+ if (!unlockSigner) {
3144
+ return vaultError({
3145
+ code: "VAULT_LOCKED",
3146
+ message: "Signer is required when no cached master signature exists"
3147
+ });
3148
+ }
3149
+ const sig = await unlockSigner.signMessage(
3150
+ versionConfig.masterMessage(spaceId)
3151
+ );
3152
+ masterSigBytes = toBytes(sig);
3153
+ await cacheSignature(masterCacheKey, masterSigBytes);
3021
3154
  }
3022
- const s = signer;
3023
- const sig = await s.signMessage(versionConfig.masterMessage(spaceId));
3024
- masterSigBytes = toBytes(sig);
3025
- await cacheSignature(masterCacheKey, masterSigBytes);
3026
- }
3027
- this.masterKey = this.crypto.deriveKey(
3028
- masterSigBytes,
3029
- this.crypto.sha256(toBytes(spaceId)),
3030
- toBytes("vault-master")
3031
- );
3155
+ this.masterKey = this.crypto.deriveKey(
3156
+ masterSigBytes,
3157
+ this.crypto.sha256(toBytes(spaceId)),
3158
+ toBytes("vault-master")
3159
+ );
3160
+ }
3032
3161
  const publicSpaceId = this.tc.makePublicSpaceId(this.tc.address, this.tc.chainId);
3033
3162
  let existingPubKey = null;
3034
3163
  try {
@@ -3051,13 +3180,14 @@ var DataVaultService = class extends BaseService {
3051
3180
  } else {
3052
3181
  let identitySigBytes = await loadCachedSignature(identityCacheKey);
3053
3182
  if (!identitySigBytes) {
3054
- if (!signer) {
3183
+ if (!unlockSigner) {
3055
3184
  this.encryptionIdentity = null;
3056
3185
  this._isUnlocked = true;
3057
3186
  return ok(void 0);
3058
3187
  }
3059
- const s = signer;
3060
- const sig = await s.signMessage(versionConfig.identityMessage);
3188
+ const sig = await unlockSigner.signMessage(
3189
+ versionConfig.identityMessage
3190
+ );
3061
3191
  identitySigBytes = toBytes(sig);
3062
3192
  await cacheSignature(identityCacheKey, identitySigBytes);
3063
3193
  }
@@ -3087,6 +3217,11 @@ var DataVaultService = class extends BaseService {
3087
3217
  });
3088
3218
  }
3089
3219
  });
3220
+ try {
3221
+ return await this.unlockInFlight;
3222
+ } finally {
3223
+ this.unlockInFlight = null;
3224
+ }
3090
3225
  }
3091
3226
  /**
3092
3227
  * Clear the cached vault signatures.
@@ -3972,6 +4107,7 @@ var SecretsService = class {
3972
4107
  // Annotate the CommonJS export names for ESM import in node:
3973
4108
  0 && (module.exports = {
3974
4109
  BaseService,
4110
+ DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
3975
4111
  DataVaultService,
3976
4112
  DatabaseHandle,
3977
4113
  DuckDbAction,