@tinycloud/sdk-services 2.1.0 → 2.2.0-beta.12

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,
@@ -37,8 +38,10 @@ __export(index_exports, {
37
38
  KVService: () => KVService,
38
39
  PrefixedKVService: () => PrefixedKVService,
39
40
  RetryPolicySchema: () => RetryPolicySchema,
41
+ SECRET_NAME_RE: () => SECRET_NAME_RE,
40
42
  SQLAction: () => SQLAction,
41
43
  SQLService: () => SQLService,
44
+ SecretsService: () => SecretsService,
42
45
  ServiceContext: () => ServiceContext,
43
46
  ServiceErrorEventSchema: () => ServiceErrorEventSchema,
44
47
  ServiceErrorSchema: () => ServiceErrorSchema,
@@ -54,6 +57,7 @@ __export(index_exports, {
54
57
  authExpiredError: () => authExpiredError,
55
58
  authRequiredError: () => authRequiredError,
56
59
  authUnauthorizedError: () => authUnauthorizedError,
60
+ canonicalizeSecretScope: () => canonicalizeSecretScope,
57
61
  createKVResponseSchema: () => createKVResponseSchema,
58
62
  createResultSchema: () => createResultSchema,
59
63
  createVaultCrypto: () => createVaultCrypto,
@@ -65,6 +69,7 @@ __export(index_exports, {
65
69
  ok: () => ok,
66
70
  parseAuthError: () => parseAuthError,
67
71
  permissionDeniedError: () => permissionDeniedError,
72
+ resolveSecretPath: () => resolveSecretPath,
68
73
  serviceError: () => serviceError,
69
74
  storageLimitReachedError: () => storageLimitReachedError,
70
75
  storageQuotaExceededError: () => storageQuotaExceededError,
@@ -900,6 +905,13 @@ var PrefixedKVService = class _PrefixedKVService {
900
905
  const fullKey = this.getFullKey(key);
901
906
  return this._kv.head(fullKey, { ...options, prefix: "" });
902
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
+ }
903
915
  /**
904
916
  * Create a nested prefix-scoped view.
905
917
  */
@@ -911,6 +923,7 @@ var PrefixedKVService = class _PrefixedKVService {
911
923
  };
912
924
 
913
925
  // src/kv/types.ts
926
+ var DEFAULT_SIGNED_READ_URL_EXPIRY_MS = 5 * 60 * 1e3;
914
927
  var KVAction = {
915
928
  GET: "tinycloud.kv/get",
916
929
  PUT: "tinycloud.kv/put",
@@ -995,6 +1008,15 @@ var KVService = class extends BaseService {
995
1008
  get host() {
996
1009
  return this.context.hosts[0];
997
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
+ }
998
1020
  /**
999
1021
  * Execute an invoke operation.
1000
1022
  *
@@ -1064,6 +1086,48 @@ var KVService = class extends BaseService {
1064
1086
  return text;
1065
1087
  }
1066
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
+ }
1067
1131
  /**
1068
1132
  * Get a value by key.
1069
1133
  */
@@ -1336,6 +1400,61 @@ var KVService = class extends BaseService {
1336
1400
  }
1337
1401
  });
1338
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
+ }
1339
1458
  /**
1340
1459
  * Create a prefix-scoped view of this KV service.
1341
1460
  *
@@ -3844,9 +3963,131 @@ function createVaultCrypto(wasm) {
3844
3963
  sha256: (data) => wasm.vault_sha256(data)
3845
3964
  };
3846
3965
  }
3966
+
3967
+ // src/secrets/paths.ts
3968
+ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
3969
+ var SECRET_PREFIX = "secrets/";
3970
+ var SCOPED_SECRET_PREFIX = "secrets/scoped/";
3971
+ var RESERVED_SECRET_SCOPES = /* @__PURE__ */ new Set(["default", "global"]);
3972
+ function canonicalizeSecretScope(scope) {
3973
+ if (scope === void 0) {
3974
+ return void 0;
3975
+ }
3976
+ const trimmed = scope.trim();
3977
+ if (trimmed === "") {
3978
+ throw new Error("Secret scope must be non-empty; omit scope for global secrets.");
3979
+ }
3980
+ const canonical = trimmed.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3981
+ if (canonical === "") {
3982
+ throw new Error("Secret scope must contain at least one letter or number.");
3983
+ }
3984
+ if (RESERVED_SECRET_SCOPES.has(canonical)) {
3985
+ throw new Error(
3986
+ `Secret scope ${JSON.stringify(scope)} is reserved; omit scope for global secrets.`
3987
+ );
3988
+ }
3989
+ return canonical;
3990
+ }
3991
+ function resolveSecretPath(name, options = {}) {
3992
+ const normalizedName = name.trim();
3993
+ if (!SECRET_NAME_RE.test(normalizedName)) {
3994
+ throw new Error(
3995
+ `Invalid secret name ${JSON.stringify(name)}. Secret names must match ${SECRET_NAME_RE.source}.`
3996
+ );
3997
+ }
3998
+ const scope = canonicalizeSecretScope(options.scope);
3999
+ const vaultKey = scope === void 0 ? `${SECRET_PREFIX}${normalizedName}` : `${SCOPED_SECRET_PREFIX}${scope}/${normalizedName}`;
4000
+ return {
4001
+ name: normalizedName,
4002
+ ...scope !== void 0 ? { scope } : {},
4003
+ vaultKey,
4004
+ permissionPaths: {
4005
+ keys: `keys/${vaultKey}`,
4006
+ vault: `vault/${vaultKey}`
4007
+ }
4008
+ };
4009
+ }
4010
+
4011
+ // src/secrets/SecretsService.ts
4012
+ function invalidSecretInput(message) {
4013
+ return err({
4014
+ code: ErrorCodes.INVALID_INPUT,
4015
+ service: "secrets",
4016
+ message
4017
+ });
4018
+ }
4019
+ function resolveSecretPathResult(name, options) {
4020
+ try {
4021
+ return resolveSecretPath(name, options);
4022
+ } catch (error) {
4023
+ return invalidSecretInput(error instanceof Error ? error.message : String(error));
4024
+ }
4025
+ }
4026
+ var SecretsService = class {
4027
+ constructor(vault) {
4028
+ this.getVault = typeof vault === "function" ? vault : () => vault;
4029
+ }
4030
+ get vault() {
4031
+ return this.getVault();
4032
+ }
4033
+ get isUnlocked() {
4034
+ return this.vault.isUnlocked;
4035
+ }
4036
+ unlock(signer) {
4037
+ return this.vault.unlock(signer);
4038
+ }
4039
+ lock() {
4040
+ this.vault.lock();
4041
+ }
4042
+ async get(name, options) {
4043
+ const secretPath = resolveSecretPathResult(name, options);
4044
+ if ("ok" in secretPath) return secretPath;
4045
+ const result = await this.vault.get(secretPath.vaultKey);
4046
+ if (!result.ok) {
4047
+ return result;
4048
+ }
4049
+ return { ok: true, data: result.data.value.value };
4050
+ }
4051
+ async put(name, value, options) {
4052
+ const secretPath = resolveSecretPathResult(name, options);
4053
+ if ("ok" in secretPath) return secretPath;
4054
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4055
+ return this.vault.put(secretPath.vaultKey, {
4056
+ value,
4057
+ createdAt: now,
4058
+ updatedAt: now
4059
+ });
4060
+ }
4061
+ async delete(name, options) {
4062
+ const secretPath = resolveSecretPathResult(name, options);
4063
+ if ("ok" in secretPath) return secretPath;
4064
+ return this.vault.delete(secretPath.vaultKey);
4065
+ }
4066
+ async list(options) {
4067
+ let prefix;
4068
+ try {
4069
+ const scope = canonicalizeSecretScope(options?.scope);
4070
+ prefix = scope === void 0 ? "secrets/" : `secrets/scoped/${scope}/`;
4071
+ } catch (error) {
4072
+ return invalidSecretInput(error instanceof Error ? error.message : String(error));
4073
+ }
4074
+ const result = await this.vault.list({
4075
+ prefix,
4076
+ removePrefix: true
4077
+ });
4078
+ if (!result.ok) {
4079
+ return result;
4080
+ }
4081
+ return {
4082
+ ok: true,
4083
+ data: result.data.filter((name) => SECRET_NAME_RE.test(name))
4084
+ };
4085
+ }
4086
+ };
3847
4087
  // Annotate the CommonJS export names for ESM import in node:
3848
4088
  0 && (module.exports = {
3849
4089
  BaseService,
4090
+ DEFAULT_SIGNED_READ_URL_EXPIRY_MS,
3850
4091
  DataVaultService,
3851
4092
  DatabaseHandle,
3852
4093
  DuckDbAction,
@@ -3863,8 +4104,10 @@ function createVaultCrypto(wasm) {
3863
4104
  KVService,
3864
4105
  PrefixedKVService,
3865
4106
  RetryPolicySchema,
4107
+ SECRET_NAME_RE,
3866
4108
  SQLAction,
3867
4109
  SQLService,
4110
+ SecretsService,
3868
4111
  ServiceContext,
3869
4112
  ServiceErrorEventSchema,
3870
4113
  ServiceErrorSchema,
@@ -3880,6 +4123,7 @@ function createVaultCrypto(wasm) {
3880
4123
  authExpiredError,
3881
4124
  authRequiredError,
3882
4125
  authUnauthorizedError,
4126
+ canonicalizeSecretScope,
3883
4127
  createKVResponseSchema,
3884
4128
  createResultSchema,
3885
4129
  createVaultCrypto,
@@ -3891,6 +4135,7 @@ function createVaultCrypto(wasm) {
3891
4135
  ok,
3892
4136
  parseAuthError,
3893
4137
  permissionDeniedError,
4138
+ resolveSecretPath,
3894
4139
  serviceError,
3895
4140
  storageLimitReachedError,
3896
4141
  storageQuotaExceededError,