@tinycloud/sdk-services 3.2.6-beta.2 → 3.3.0-beta.11
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/{BaseService-4wwJq1Gn.d.cts → BaseService-Cc6Ueicm.d.cts} +1 -1
- package/dist/{BaseService-BGr2slUz.d.ts → BaseService-DOdXd7sr.d.ts} +1 -1
- package/dist/encryption/index.cjs +10 -0
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +10 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +360 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -13
- package/dist/index.d.ts +116 -13
- package/dist/index.js +359 -210
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +68 -21
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +7 -4
- package/dist/kv/index.d.ts +7 -4
- package/dist/kv/index.js +68 -21
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/contractConstants.cjs +115 -0
- package/dist/kv/replication/contractConstants.cjs.map +1 -0
- package/dist/kv/replication/contractConstants.d.cts +77 -0
- package/dist/kv/replication/contractConstants.d.ts +77 -0
- package/dist/kv/replication/contractConstants.js +75 -0
- package/dist/kv/replication/contractConstants.js.map +1 -0
- package/dist/kv/replication/grantPolicy.cjs +82 -0
- package/dist/kv/replication/grantPolicy.cjs.map +1 -0
- package/dist/kv/replication/grantPolicy.d.cts +19 -0
- package/dist/kv/replication/grantPolicy.d.ts +19 -0
- package/dist/kv/replication/grantPolicy.js +50 -0
- package/dist/kv/replication/grantPolicy.js.map +1 -0
- package/dist/kv/replication/grantPolicyConstants.cjs +42 -0
- package/dist/kv/replication/grantPolicyConstants.cjs.map +1 -0
- package/dist/kv/replication/grantPolicyConstants.d.cts +10 -0
- package/dist/kv/replication/grantPolicyConstants.d.ts +10 -0
- package/dist/kv/replication/grantPolicyConstants.js +14 -0
- package/dist/kv/replication/grantPolicyConstants.js.map +1 -0
- package/dist/kv/replication/grantPolicyTypes.cjs +19 -0
- package/dist/kv/replication/grantPolicyTypes.cjs.map +1 -0
- package/dist/kv/replication/grantPolicyTypes.d.cts +87 -0
- package/dist/kv/replication/grantPolicyTypes.d.ts +87 -0
- package/dist/kv/replication/grantPolicyTypes.js +1 -0
- package/dist/kv/replication/grantPolicyTypes.js.map +1 -0
- package/dist/kv/replication/index.cjs +575 -131
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +6 -3
- package/dist/kv/replication/index.d.ts +6 -3
- package/dist/kv/replication/index.js +575 -131
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/scope-B-m6cKH-.d.ts +1962 -0
- package/dist/scope-B7oeKdLM.d.cts +1962 -0
- package/dist/sql/index.cjs +72 -36
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +72 -36
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-D-9A3Sut.d.cts → types-BjH0ZtGy.d.cts} +8 -0
- package/dist/{types-D-9A3Sut.d.ts → types-BjH0ZtGy.d.ts} +8 -0
- package/package.json +55 -2
- package/dist/scope-CoKKmMiI.d.ts +0 -957
- package/dist/scope-D7wmjbMH.d.cts +0 -957
package/dist/index.js
CHANGED
|
@@ -14,6 +14,14 @@ var ErrorCodes = {
|
|
|
14
14
|
ABORTED: "ABORTED",
|
|
15
15
|
INVALID_INPUT: "INVALID_INPUT",
|
|
16
16
|
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
17
|
+
// Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
|
|
18
|
+
// retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
|
|
19
|
+
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
20
|
+
NODE_BUSY: "NODE_BUSY",
|
|
21
|
+
/** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
|
|
22
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
23
|
+
/** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
|
|
24
|
+
OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
|
|
17
25
|
// KV-specific errors
|
|
18
26
|
KV_NOT_FOUND: "KV_NOT_FOUND",
|
|
19
27
|
KV_WRITE_FAILED: "KV_WRITE_FAILED",
|
|
@@ -68,6 +76,8 @@ var TelemetryEvents = {
|
|
|
68
76
|
REPLICATION_SYNC: "replication.sync",
|
|
69
77
|
REPLICATION_STATE: "replication.state",
|
|
70
78
|
REPLICATION_DIVERGENCE: "replication.divergence",
|
|
79
|
+
REPLICATION_GRANT: "replication.grant",
|
|
80
|
+
REPLICATION_LIFECYCLE: "replication.lifecycle",
|
|
71
81
|
/** Emitted once, on the first write rejected because storage is full. */
|
|
72
82
|
STORAGE_FULL: "storage.full"
|
|
73
83
|
};
|
|
@@ -104,21 +114,27 @@ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
|
104
114
|
"fetched",
|
|
105
115
|
"pages",
|
|
106
116
|
"lagMs",
|
|
107
|
-
"pendingCleared"
|
|
117
|
+
"pendingCleared",
|
|
118
|
+
"partitions",
|
|
119
|
+
"remaining",
|
|
120
|
+
"expiryMs"
|
|
108
121
|
]);
|
|
109
|
-
var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
|
|
122
|
+
var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted", "purgePending"]);
|
|
110
123
|
var SAFE_REPLICATION_FIELDS = {
|
|
111
124
|
source: ["replica", "network", "none"],
|
|
112
|
-
reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED"],
|
|
125
|
+
reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "replica_gated", "sign_out", "account_switch", "consent_expired", "startup"],
|
|
113
126
|
op: ["get", "list", "put", "delete", "batchPut"],
|
|
114
|
-
trigger: ["start", "interval", "stale_read", "manual"],
|
|
127
|
+
trigger: ["start", "interval", "stale_read", "manual", "peer"],
|
|
128
|
+
origin: ["peer"],
|
|
115
129
|
outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
|
|
116
|
-
class: ["offline", "authority", "storage", "node"],
|
|
117
|
-
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
|
|
130
|
+
class: ["offline", "authority", "storage", "node", "lifecycle"],
|
|
131
|
+
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready", "gated"],
|
|
132
|
+
action: ["minted", "renewed", "renewal_refused", "renewal_deferred", "capped", "expired", "consent_deadline_reached", "denied", "deny_stale", "deny_failed", "database_deleted", "marker_set", "marker_cleared", "unguarded", "drained", "erased", "erase_failed", "activation_blocked", "activated", "recovered"],
|
|
133
|
+
gate: ["not_established", "stamp_stale", "principal_mismatch", "purge_pending", "logout_pending", "gate_unavailable"],
|
|
118
134
|
kind: ["value", "missing_local", "extra_local", "keys", "order"],
|
|
119
135
|
coverage: ["empty", "bootstrapping", "complete"],
|
|
120
136
|
authority: ["valid", "expired", "revoked", "not-yet-valid"],
|
|
121
|
-
code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL"],
|
|
137
|
+
code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL", "REPLICA_GATED", "RESET_REQUIRED", "STORAGE_BUDGET", "CONSENT_EXPIRED"],
|
|
122
138
|
pendingState: ["in_flight", "committed", "ambiguous"],
|
|
123
139
|
strategy: ["session", "minted", "installed"],
|
|
124
140
|
verify: ["match", "diverged", "error", "aborted"]
|
|
@@ -950,6 +966,42 @@ var ServiceContext = class {
|
|
|
950
966
|
}
|
|
951
967
|
};
|
|
952
968
|
|
|
969
|
+
// src/responseErrors.ts
|
|
970
|
+
function parseServiceErrorBody(errorText) {
|
|
971
|
+
try {
|
|
972
|
+
return JSON.parse(errorText);
|
|
973
|
+
} catch {
|
|
974
|
+
return {};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
978
|
+
if (parsed.message) {
|
|
979
|
+
return compactMessage(parsed.message);
|
|
980
|
+
}
|
|
981
|
+
if (looksLikeHtml(errorText)) {
|
|
982
|
+
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
983
|
+
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
984
|
+
}
|
|
985
|
+
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
986
|
+
}
|
|
987
|
+
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
988
|
+
}
|
|
989
|
+
function responseErrorMeta(status, statusText, errorText) {
|
|
990
|
+
const meta = { status, statusText };
|
|
991
|
+
const snippet = compactMessage(errorText);
|
|
992
|
+
if (snippet) {
|
|
993
|
+
meta.responseSnippet = snippet.slice(0, 300);
|
|
994
|
+
}
|
|
995
|
+
return meta;
|
|
996
|
+
}
|
|
997
|
+
function looksLikeHtml(text) {
|
|
998
|
+
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
999
|
+
}
|
|
1000
|
+
function compactMessage(message) {
|
|
1001
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
1002
|
+
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
953
1005
|
// src/errors.ts
|
|
954
1006
|
import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
|
|
955
1007
|
|
|
@@ -1109,6 +1161,30 @@ function networkError(service, message, cause) {
|
|
|
1109
1161
|
cause
|
|
1110
1162
|
};
|
|
1111
1163
|
}
|
|
1164
|
+
function nodeRetryError(service, message, response, errorText) {
|
|
1165
|
+
const body = errorText.trim();
|
|
1166
|
+
const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
|
|
1167
|
+
let code;
|
|
1168
|
+
if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable")) {
|
|
1169
|
+
code = ErrorCodes.NODE_BUSY;
|
|
1170
|
+
} else if (response.status === 503 && token === "request_timeout") {
|
|
1171
|
+
code = ErrorCodes.REQUEST_TIMEOUT;
|
|
1172
|
+
} else if (response.status === 500 && token === "outcome_unknown") {
|
|
1173
|
+
code = ErrorCodes.OUTCOME_UNKNOWN;
|
|
1174
|
+
} else {
|
|
1175
|
+
return void 0;
|
|
1176
|
+
}
|
|
1177
|
+
const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
|
|
1178
|
+
const retryAfter = response.headers.get("retry-after")?.trim();
|
|
1179
|
+
return serviceError(code, message, service, {
|
|
1180
|
+
meta: {
|
|
1181
|
+
...responseErrorMeta(response.status, response.statusText, errorText),
|
|
1182
|
+
retryable,
|
|
1183
|
+
...token === "sql_busy" && stage ? { stage } : {},
|
|
1184
|
+
...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1112
1188
|
var RequestTimeoutError = class extends Error {
|
|
1113
1189
|
constructor(timeoutMs) {
|
|
1114
1190
|
super(`Request timed out after ${timeoutMs}ms.`);
|
|
@@ -2447,9 +2523,12 @@ var KVService = class extends BaseService {
|
|
|
2447
2523
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2448
2524
|
));
|
|
2449
2525
|
}
|
|
2526
|
+
const message = `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
|
|
2527
|
+
const retryError = nodeRetryError("kv", message, response, errorText);
|
|
2528
|
+
if (retryError) return err(retryError);
|
|
2450
2529
|
return err(serviceError(
|
|
2451
2530
|
ErrorCodes.NETWORK_ERROR,
|
|
2452
|
-
|
|
2531
|
+
message,
|
|
2453
2532
|
"kv",
|
|
2454
2533
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2455
2534
|
));
|
|
@@ -2531,19 +2610,14 @@ var KVService = class extends BaseService {
|
|
|
2531
2610
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2532
2611
|
));
|
|
2533
2612
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
2538
|
-
"kv",
|
|
2539
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2540
|
-
));
|
|
2541
|
-
}
|
|
2613
|
+
const message = `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
|
|
2614
|
+
const retryError = nodeRetryError("kv", message, response, errorText);
|
|
2615
|
+
if (retryError) return err(retryError);
|
|
2542
2616
|
const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
|
|
2543
2617
|
if (quotaError) return quotaError;
|
|
2544
2618
|
return err(serviceError(
|
|
2545
2619
|
ErrorCodes.KV_WRITE_FAILED,
|
|
2546
|
-
|
|
2620
|
+
message,
|
|
2547
2621
|
"kv",
|
|
2548
2622
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2549
2623
|
));
|
|
@@ -2843,9 +2917,12 @@ var KVService = class extends BaseService {
|
|
|
2843
2917
|
);
|
|
2844
2918
|
}
|
|
2845
2919
|
const errorText = await response.text();
|
|
2920
|
+
const message = `Failed to list keys: ${response.status} - ${errorText}`;
|
|
2921
|
+
const retryError = nodeRetryError("kv", message, response, errorText);
|
|
2922
|
+
if (retryError) return err(retryError);
|
|
2846
2923
|
return err(serviceError(
|
|
2847
2924
|
ErrorCodes.NETWORK_ERROR,
|
|
2848
|
-
|
|
2925
|
+
message,
|
|
2849
2926
|
"kv",
|
|
2850
2927
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2851
2928
|
));
|
|
@@ -3096,17 +3173,12 @@ var KVService = class extends BaseService {
|
|
|
3096
3173
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3097
3174
|
));
|
|
3098
3175
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
3103
|
-
"kv",
|
|
3104
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3105
|
-
));
|
|
3106
|
-
}
|
|
3176
|
+
const message = `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
|
|
3177
|
+
const retryError = nodeRetryError("kv", message, response, errorText);
|
|
3178
|
+
if (retryError) return err(retryError);
|
|
3107
3179
|
return err(serviceError(
|
|
3108
3180
|
ErrorCodes.NETWORK_ERROR,
|
|
3109
|
-
|
|
3181
|
+
message,
|
|
3110
3182
|
"kv",
|
|
3111
3183
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3112
3184
|
));
|
|
@@ -3147,10 +3219,13 @@ var KVService = class extends BaseService {
|
|
|
3147
3219
|
return await this.classifyNotFound(response, key);
|
|
3148
3220
|
}
|
|
3149
3221
|
const errorText = await response.text();
|
|
3222
|
+
const message = `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
|
|
3223
|
+
const retryError = nodeRetryError("kv", message, response, errorText);
|
|
3224
|
+
if (retryError) return err(retryError);
|
|
3150
3225
|
return err(
|
|
3151
3226
|
serviceError(
|
|
3152
3227
|
ErrorCodes.NETWORK_ERROR,
|
|
3153
|
-
|
|
3228
|
+
message,
|
|
3154
3229
|
"kv",
|
|
3155
3230
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
3156
3231
|
)
|
|
@@ -3342,42 +3417,6 @@ function requiresSecretsOptIn(space, prefix) {
|
|
|
3342
3417
|
return prefix.split("/", 1)[0] === "vault";
|
|
3343
3418
|
}
|
|
3344
3419
|
|
|
3345
|
-
// src/responseErrors.ts
|
|
3346
|
-
function parseServiceErrorBody(errorText) {
|
|
3347
|
-
try {
|
|
3348
|
-
return JSON.parse(errorText);
|
|
3349
|
-
} catch {
|
|
3350
|
-
return {};
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
3354
|
-
if (parsed.message) {
|
|
3355
|
-
return compactMessage(parsed.message);
|
|
3356
|
-
}
|
|
3357
|
-
if (looksLikeHtml(errorText)) {
|
|
3358
|
-
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
3359
|
-
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
3360
|
-
}
|
|
3361
|
-
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
3362
|
-
}
|
|
3363
|
-
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
3364
|
-
}
|
|
3365
|
-
function responseErrorMeta(status, statusText, errorText) {
|
|
3366
|
-
const meta = { status, statusText };
|
|
3367
|
-
const snippet = compactMessage(errorText);
|
|
3368
|
-
if (snippet) {
|
|
3369
|
-
meta.responseSnippet = snippet.slice(0, 300);
|
|
3370
|
-
}
|
|
3371
|
-
return meta;
|
|
3372
|
-
}
|
|
3373
|
-
function looksLikeHtml(text) {
|
|
3374
|
-
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
3375
|
-
}
|
|
3376
|
-
function compactMessage(message) {
|
|
3377
|
-
const compact = message.replace(/\s+/g, " ").trim();
|
|
3378
|
-
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
3420
|
// src/sql/SQLMigrations.ts
|
|
3382
3421
|
var SQLMigrations = class {
|
|
3383
3422
|
constructor(service, dbName) {
|
|
@@ -3793,6 +3832,8 @@ var SQLService = class extends BaseService {
|
|
|
3793
3832
|
if (response.status === 413 && parseStorageRejection(errorText)) {
|
|
3794
3833
|
return err(storageRejectionError("sql", 413, meta, errorText));
|
|
3795
3834
|
}
|
|
3835
|
+
const retryError = nodeRetryError("sql", `SQL ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
|
|
3836
|
+
if (retryError) return err(retryError);
|
|
3796
3837
|
const errorBody = parseServiceErrorBody(errorText);
|
|
3797
3838
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
3798
3839
|
response.status,
|
|
@@ -4262,6 +4303,8 @@ var DuckDbService = class extends BaseService {
|
|
|
4262
4303
|
if (response.status === 413 && parseStorageRejection(errorText)) {
|
|
4263
4304
|
return err(storageRejectionError("duckdb", 413, meta, errorText));
|
|
4264
4305
|
}
|
|
4306
|
+
const retryError = nodeRetryError("duckdb", `DuckDB ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
|
|
4307
|
+
if (retryError) return err(retryError);
|
|
4265
4308
|
const errorBody = parseServiceErrorBody(errorText);
|
|
4266
4309
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
4267
4310
|
response.status,
|
|
@@ -5469,6 +5512,116 @@ var TinyCloudQuota = class {
|
|
|
5469
5512
|
}
|
|
5470
5513
|
};
|
|
5471
5514
|
|
|
5515
|
+
// src/encryption/canonical.ts
|
|
5516
|
+
function canonicalize(value) {
|
|
5517
|
+
if (value === void 0) {
|
|
5518
|
+
return "";
|
|
5519
|
+
}
|
|
5520
|
+
return stringify(value);
|
|
5521
|
+
}
|
|
5522
|
+
function stringify(value) {
|
|
5523
|
+
if (value === null) return "null";
|
|
5524
|
+
switch (typeof value) {
|
|
5525
|
+
case "boolean":
|
|
5526
|
+
case "number":
|
|
5527
|
+
return JSON.stringify(value);
|
|
5528
|
+
case "string":
|
|
5529
|
+
return JSON.stringify(value);
|
|
5530
|
+
case "object": {
|
|
5531
|
+
if (Array.isArray(value)) {
|
|
5532
|
+
return `[${value.map(stringify).join(",")}]`;
|
|
5533
|
+
}
|
|
5534
|
+
const keys = Object.keys(value).sort();
|
|
5535
|
+
const parts = [];
|
|
5536
|
+
for (const k of keys) {
|
|
5537
|
+
const v = value[k];
|
|
5538
|
+
if (v === void 0) continue;
|
|
5539
|
+
parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
|
|
5540
|
+
}
|
|
5541
|
+
return `{${parts.join(",")}}`;
|
|
5542
|
+
}
|
|
5543
|
+
default:
|
|
5544
|
+
throw new TypeError(
|
|
5545
|
+
`canonicalize: unsupported value type ${typeof value}`
|
|
5546
|
+
);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
var HEX = "0123456789abcdef";
|
|
5550
|
+
function hexEncode(bytes) {
|
|
5551
|
+
let out = "";
|
|
5552
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
5553
|
+
const b = bytes[i];
|
|
5554
|
+
out += HEX[b >> 4 & 15] + HEX[b & 15];
|
|
5555
|
+
}
|
|
5556
|
+
return out;
|
|
5557
|
+
}
|
|
5558
|
+
function hexDecode(hex) {
|
|
5559
|
+
if (hex.length % 2 !== 0) {
|
|
5560
|
+
throw new Error("hex string must have even length");
|
|
5561
|
+
}
|
|
5562
|
+
const out = new Uint8Array(hex.length / 2);
|
|
5563
|
+
for (let i = 0; i < out.length; i++) {
|
|
5564
|
+
const hi = parseInt(hex[i * 2], 16);
|
|
5565
|
+
const lo = parseInt(hex[i * 2 + 1], 16);
|
|
5566
|
+
if (Number.isNaN(hi) || Number.isNaN(lo)) {
|
|
5567
|
+
throw new Error("invalid hex character");
|
|
5568
|
+
}
|
|
5569
|
+
out[i] = hi << 4 | lo;
|
|
5570
|
+
}
|
|
5571
|
+
return out;
|
|
5572
|
+
}
|
|
5573
|
+
function base64Encode(bytes) {
|
|
5574
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5575
|
+
let out = "";
|
|
5576
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
5577
|
+
const b0 = bytes[i];
|
|
5578
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
5579
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
5580
|
+
out += chars[b0 >> 2 & 63];
|
|
5581
|
+
out += chars[(b0 << 4 | b1 >> 4) & 63];
|
|
5582
|
+
out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
|
|
5583
|
+
out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
|
|
5584
|
+
}
|
|
5585
|
+
return out;
|
|
5586
|
+
}
|
|
5587
|
+
function base64Decode(s) {
|
|
5588
|
+
const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
|
|
5589
|
+
const len = clean.length;
|
|
5590
|
+
if (len % 4 !== 0) {
|
|
5591
|
+
throw new Error("invalid base64 input");
|
|
5592
|
+
}
|
|
5593
|
+
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
|
5594
|
+
const outLen = len / 4 * 3 - padding;
|
|
5595
|
+
const out = new Uint8Array(outLen);
|
|
5596
|
+
const lookup = {};
|
|
5597
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5598
|
+
for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
|
|
5599
|
+
let outIdx = 0;
|
|
5600
|
+
for (let i = 0; i < len; i += 4) {
|
|
5601
|
+
const v0 = lookup[clean[i]] ?? 0;
|
|
5602
|
+
const v1 = lookup[clean[i + 1]] ?? 0;
|
|
5603
|
+
const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
|
|
5604
|
+
const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
|
|
5605
|
+
const b0 = v0 << 2 | v1 >> 4;
|
|
5606
|
+
const b1 = (v1 & 15) << 4 | v2 >> 2;
|
|
5607
|
+
const b2 = (v2 & 3) << 6 | v3;
|
|
5608
|
+
if (outIdx < outLen) out[outIdx++] = b0;
|
|
5609
|
+
if (outIdx < outLen) out[outIdx++] = b1;
|
|
5610
|
+
if (outIdx < outLen) out[outIdx++] = b2;
|
|
5611
|
+
}
|
|
5612
|
+
return out;
|
|
5613
|
+
}
|
|
5614
|
+
function utf8Encode(s) {
|
|
5615
|
+
return new TextEncoder().encode(s);
|
|
5616
|
+
}
|
|
5617
|
+
function utf8Decode(b) {
|
|
5618
|
+
return new TextDecoder().decode(b);
|
|
5619
|
+
}
|
|
5620
|
+
function canonicalHashHex(sha256, value) {
|
|
5621
|
+
const canonical = canonicalize(value);
|
|
5622
|
+
return hexEncode(sha256(utf8Encode(canonical)));
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5472
5625
|
// src/vault/types.ts
|
|
5473
5626
|
var VaultPublicSpaceKVActions = [
|
|
5474
5627
|
"tinycloud.kv/get",
|
|
@@ -5638,7 +5791,7 @@ function toBytes(str) {
|
|
|
5638
5791
|
function fromBytes(bytes) {
|
|
5639
5792
|
return new TextDecoder().decode(bytes);
|
|
5640
5793
|
}
|
|
5641
|
-
function
|
|
5794
|
+
function hexEncode2(bytes) {
|
|
5642
5795
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5643
5796
|
}
|
|
5644
5797
|
function concatBytes(...arrays) {
|
|
@@ -5651,14 +5804,14 @@ function concatBytes(...arrays) {
|
|
|
5651
5804
|
}
|
|
5652
5805
|
return result;
|
|
5653
5806
|
}
|
|
5654
|
-
function
|
|
5807
|
+
function base64Encode2(bytes) {
|
|
5655
5808
|
let binary = "";
|
|
5656
5809
|
for (let i = 0; i < bytes.length; i++) {
|
|
5657
5810
|
binary += String.fromCharCode(bytes[i]);
|
|
5658
5811
|
}
|
|
5659
5812
|
return btoa(binary);
|
|
5660
5813
|
}
|
|
5661
|
-
function
|
|
5814
|
+
function base64Decode2(str) {
|
|
5662
5815
|
const binary = atob(str);
|
|
5663
5816
|
const bytes = new Uint8Array(binary.length);
|
|
5664
5817
|
for (let i = 0; i < binary.length; i++) {
|
|
@@ -5700,6 +5853,11 @@ function defaultVaultMessage(input) {
|
|
|
5700
5853
|
return input.message;
|
|
5701
5854
|
case "PERMISSION_DENIED":
|
|
5702
5855
|
return input.message;
|
|
5856
|
+
case "AAD_MISMATCH":
|
|
5857
|
+
case "ETAG_MISMATCH":
|
|
5858
|
+
case "INVALID_ETAG":
|
|
5859
|
+
case "OUTCOME_UNKNOWN":
|
|
5860
|
+
case "NODE_BUSY":
|
|
5703
5861
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
5704
5862
|
case "STORAGE_LIMIT_REACHED":
|
|
5705
5863
|
return input.message;
|
|
@@ -5722,6 +5880,37 @@ function typedKvCause(error) {
|
|
|
5722
5880
|
...error.cause !== void 0 ? { cause: error.cause } : {}
|
|
5723
5881
|
});
|
|
5724
5882
|
}
|
|
5883
|
+
function canonicalAadSpaceId(spaceId) {
|
|
5884
|
+
return spaceId.replace(/0x[0-9a-fA-F]{40}/g, (address) => address.toLowerCase());
|
|
5885
|
+
}
|
|
5886
|
+
function checkEnvelopeAad(value, spaceId, key) {
|
|
5887
|
+
const e = value;
|
|
5888
|
+
if (typeof e !== "object" || e === null || Array.isArray(e) || !["networkId", "alg", "encryptedSymmetricKey", "encryptedSymmetricKeyHash", "ciphertext"].every(
|
|
5889
|
+
(field) => typeof e[field] === "string" && e[field] !== ""
|
|
5890
|
+
) || !Number.isInteger(e.keyVersion) || e.aad !== void 0 && typeof e.aad !== "string") {
|
|
5891
|
+
return "corrupt";
|
|
5892
|
+
}
|
|
5893
|
+
let aad = "";
|
|
5894
|
+
try {
|
|
5895
|
+
if (typeof e.aad === "string") aad = fromBytes(base64Decode(e.aad));
|
|
5896
|
+
} catch {
|
|
5897
|
+
}
|
|
5898
|
+
const suffix = `:${key}`;
|
|
5899
|
+
return aad.endsWith(suffix) && canonicalAadSpaceId(aad.slice(0, -suffix.length)) === canonicalAadSpaceId(`tinycloud.vault:${spaceId}`) ? "ok" : "mismatch";
|
|
5900
|
+
}
|
|
5901
|
+
var AMBIGUOUS_WRITE_STATUSES = [500, 502, 503, 504, 520, 522, 524];
|
|
5902
|
+
function ifMatchWriteError(key, error) {
|
|
5903
|
+
const status = error.meta?.status;
|
|
5904
|
+
const code = error.code === ErrorCodes.KV_PRECONDITION_FAILED ? "ETAG_MISMATCH" : error.code === ErrorCodes.KV_WRITE_FAILED && status === 400 ? "INVALID_ETAG" : error.code === ErrorCodes.OUTCOME_UNKNOWN || error.code === ErrorCodes.KV_WRITE_FAILED && typeof status === "number" && AMBIGUOUS_WRITE_STATUSES.includes(status) || (error.code === ErrorCodes.NETWORK_ERROR || error.code === ErrorCodes.TIMEOUT) && status === void 0 && error.meta?.requestMayHaveDispatched !== false ? "OUTCOME_UNKNOWN" : error.code === ErrorCodes.NODE_BUSY || error.code === ErrorCodes.REQUEST_TIMEOUT ? "NODE_BUSY" : void 0;
|
|
5905
|
+
if (code === void 0) return void 0;
|
|
5906
|
+
return vaultError({
|
|
5907
|
+
code,
|
|
5908
|
+
key,
|
|
5909
|
+
message: error.message,
|
|
5910
|
+
...error.meta !== void 0 ? { meta: error.meta } : {},
|
|
5911
|
+
cause: typedKvCause(error)
|
|
5912
|
+
});
|
|
5913
|
+
}
|
|
5725
5914
|
function kvWriteError(context, error) {
|
|
5726
5915
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
5727
5916
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
@@ -5908,7 +6097,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5908
6097
|
}
|
|
5909
6098
|
if (existingPubKey) {
|
|
5910
6099
|
this.encryptionIdentity = {
|
|
5911
|
-
publicKey:
|
|
6100
|
+
publicKey: base64Decode2(existingPubKey),
|
|
5912
6101
|
privateKey: new Uint8Array(0)
|
|
5913
6102
|
// private key not available without signing
|
|
5914
6103
|
};
|
|
@@ -5933,7 +6122,7 @@ var DataVaultService = class extends BaseService {
|
|
|
5933
6122
|
);
|
|
5934
6123
|
this.encryptionIdentity = this.crypto.x25519FromSeed(seed);
|
|
5935
6124
|
try {
|
|
5936
|
-
const pubKeyB64 =
|
|
6125
|
+
const pubKeyB64 = base64Encode2(this.encryptionIdentity.publicKey);
|
|
5937
6126
|
await this.tc.ensurePublicSpace();
|
|
5938
6127
|
await this.tc.publicKV.put(".well-known/vault-pubkey", pubKeyB64);
|
|
5939
6128
|
await this.tc.publicKV.put(".well-known/vault-version", CURRENT_VAULT_VERSION);
|
|
@@ -5999,6 +6188,21 @@ var DataVaultService = class extends BaseService {
|
|
|
5999
6188
|
message: "Authentication required"
|
|
6000
6189
|
});
|
|
6001
6190
|
}
|
|
6191
|
+
if (options?.ifMatch !== void 0) {
|
|
6192
|
+
if (options.ifNoneMatch !== void 0) {
|
|
6193
|
+
return vaultError({
|
|
6194
|
+
code: "STORAGE_ERROR",
|
|
6195
|
+
cause: new Error("ifMatch cannot be combined with ifNoneMatch")
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
6198
|
+
if (!/^"[^"\s]+"$/.test(options.ifMatch)) {
|
|
6199
|
+
return vaultError({
|
|
6200
|
+
code: "INVALID_ETAG",
|
|
6201
|
+
key,
|
|
6202
|
+
message: options.ifMatch.startsWith("W/") ? 'ifMatch is a weak etag (W/"..."): the node only issues strong etags, so the etag was weakened in transit (a proxy or CDN rewrote it). Read it from a connection that preserves strong ETags' : "ifMatch must be the strong, quoted ETag returned by a network read (VaultEntry.etag), passed exactly as returned"
|
|
6203
|
+
});
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6002
6206
|
try {
|
|
6003
6207
|
const { plaintext, contentType } = this.serializeValue(value, options);
|
|
6004
6208
|
const metadata = {
|
|
@@ -6021,7 +6225,8 @@ var DataVaultService = class extends BaseService {
|
|
|
6021
6225
|
}
|
|
6022
6226
|
const valuePath = `vault/${key}`;
|
|
6023
6227
|
const valueBody = JSON.stringify(envelopeResult.data);
|
|
6024
|
-
const
|
|
6228
|
+
const putOptions = options?.ifMatch !== void 0 ? { ifMatch: options.ifMatch } : options?.ifNoneMatch !== void 0 ? { ifNoneMatch: options.ifNoneMatch } : void 0;
|
|
6229
|
+
const valuePutResult = putOptions !== void 0 ? await this.tc.kv.put(valuePath, valueBody, putOptions) : await this.tc.kv.put(valuePath, valueBody);
|
|
6025
6230
|
if (!valuePutResult.ok) {
|
|
6026
6231
|
if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
|
|
6027
6232
|
return vaultError({
|
|
@@ -6031,6 +6236,10 @@ var DataVaultService = class extends BaseService {
|
|
|
6031
6236
|
cause: typedKvCause(valuePutResult.error)
|
|
6032
6237
|
});
|
|
6033
6238
|
}
|
|
6239
|
+
if (options?.ifMatch !== void 0) {
|
|
6240
|
+
const classified = ifMatchWriteError(key, valuePutResult.error);
|
|
6241
|
+
if (classified !== void 0) return classified;
|
|
6242
|
+
}
|
|
6034
6243
|
return kvWriteError("Failed to store encrypted value", valuePutResult.error);
|
|
6035
6244
|
}
|
|
6036
6245
|
return { ok: true, data: void 0 };
|
|
@@ -6065,6 +6274,19 @@ var DataVaultService = class extends BaseService {
|
|
|
6065
6274
|
}
|
|
6066
6275
|
const rawEnvelope = unwrapKVData(valueResult.data);
|
|
6067
6276
|
const envelope = typeof rawEnvelope === "string" ? JSON.parse(rawEnvelope) : rawEnvelope;
|
|
6277
|
+
if (options?.expectAad === true) {
|
|
6278
|
+
const verdict = checkEnvelopeAad(envelope, this.vaultConfig.spaceId, key);
|
|
6279
|
+
if (verdict === "corrupt") {
|
|
6280
|
+
return vaultError({ code: "DECRYPTION_FAILED", message: "envelope is malformed" });
|
|
6281
|
+
}
|
|
6282
|
+
if (verdict === "mismatch") {
|
|
6283
|
+
return vaultError({
|
|
6284
|
+
code: "AAD_MISMATCH",
|
|
6285
|
+
key,
|
|
6286
|
+
message: `Envelope at ${key} is not bound to this space and key`
|
|
6287
|
+
});
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6068
6290
|
const proof = await this.decryptCapabilityProof();
|
|
6069
6291
|
const plaintextResult = await config.service.decryptEnvelope(envelope, proof);
|
|
6070
6292
|
if (!plaintextResult.ok) {
|
|
@@ -6081,7 +6303,11 @@ var DataVaultService = class extends BaseService {
|
|
|
6081
6303
|
contentType,
|
|
6082
6304
|
options
|
|
6083
6305
|
);
|
|
6084
|
-
|
|
6306
|
+
const etag = options?.source === "network" ? valueResult.data.headers?.etag : void 0;
|
|
6307
|
+
return {
|
|
6308
|
+
ok: true,
|
|
6309
|
+
data: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6310
|
+
};
|
|
6085
6311
|
} catch (error) {
|
|
6086
6312
|
return vaultError({
|
|
6087
6313
|
code: "STORAGE_ERROR",
|
|
@@ -6127,6 +6353,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6127
6353
|
} catch {
|
|
6128
6354
|
return { status: "corrupt_envelope" };
|
|
6129
6355
|
}
|
|
6356
|
+
if (options?.expectAad === true) {
|
|
6357
|
+
const verdict = checkEnvelopeAad(envelope, this.vaultConfig.spaceId, key);
|
|
6358
|
+
if (verdict !== "ok") {
|
|
6359
|
+
return { status: verdict === "corrupt" ? "corrupt_envelope" : "aad_mismatch" };
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6130
6362
|
let plaintextResult;
|
|
6131
6363
|
try {
|
|
6132
6364
|
const proof = await this.decryptCapabilityProof();
|
|
@@ -6158,7 +6390,11 @@ var DataVaultService = class extends BaseService {
|
|
|
6158
6390
|
contentType,
|
|
6159
6391
|
options
|
|
6160
6392
|
);
|
|
6161
|
-
|
|
6393
|
+
const etag = valueResult.data.headers?.etag;
|
|
6394
|
+
return {
|
|
6395
|
+
status: "ok",
|
|
6396
|
+
entry: { value, metadata, keyId, ...etag === void 0 ? {} : { etag } }
|
|
6397
|
+
};
|
|
6162
6398
|
} catch {
|
|
6163
6399
|
return { status: "invalid_payload" };
|
|
6164
6400
|
}
|
|
@@ -6208,6 +6444,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6208
6444
|
)
|
|
6209
6445
|
});
|
|
6210
6446
|
}
|
|
6447
|
+
if (options?.ifMatch !== void 0) {
|
|
6448
|
+
return vaultError({
|
|
6449
|
+
code: "STORAGE_ERROR",
|
|
6450
|
+
cause: new Error(
|
|
6451
|
+
"ifMatch is only supported by network-encrypted vaults"
|
|
6452
|
+
)
|
|
6453
|
+
});
|
|
6454
|
+
}
|
|
6211
6455
|
if (!this._isUnlocked || !this.masterKey) {
|
|
6212
6456
|
return vaultError({
|
|
6213
6457
|
code: "VAULT_LOCKED",
|
|
@@ -6233,7 +6477,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6233
6477
|
}
|
|
6234
6478
|
const contentType = options?.contentType ?? (value instanceof Uint8Array ? "application/octet-stream" : "application/json");
|
|
6235
6479
|
const entryKey = this.crypto.randomBytes(32);
|
|
6236
|
-
const keyId =
|
|
6480
|
+
const keyId = hexEncode2(this.crypto.sha256(entryKey)).slice(0, 16);
|
|
6237
6481
|
const encrypted = this.crypto.encrypt(entryKey, plaintext);
|
|
6238
6482
|
const keyBlob = this.crypto.encrypt(this.masterKey, entryKey);
|
|
6239
6483
|
const metadata = {
|
|
@@ -6251,7 +6495,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6251
6495
|
...metadata
|
|
6252
6496
|
});
|
|
6253
6497
|
const keyPayload = JSON.stringify({
|
|
6254
|
-
key:
|
|
6498
|
+
key: base64Encode2(keyBlob),
|
|
6255
6499
|
metadata: keyMetadata
|
|
6256
6500
|
});
|
|
6257
6501
|
const keyPutResult = await this.tc.kv.put(
|
|
@@ -6262,7 +6506,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6262
6506
|
return kvWriteError("Failed to store key blob", keyPutResult.error);
|
|
6263
6507
|
}
|
|
6264
6508
|
const valuePayload = JSON.stringify({
|
|
6265
|
-
data:
|
|
6509
|
+
data: base64Encode2(encrypted),
|
|
6266
6510
|
metadata
|
|
6267
6511
|
});
|
|
6268
6512
|
const valuePutResult = await this.tc.kv.put(
|
|
@@ -6293,6 +6537,12 @@ var DataVaultService = class extends BaseService {
|
|
|
6293
6537
|
if (this.usesNetworkEncryption) {
|
|
6294
6538
|
return this.getNetworkEncrypted(key, options);
|
|
6295
6539
|
}
|
|
6540
|
+
if (options?.expectAad === true) {
|
|
6541
|
+
return vaultError({
|
|
6542
|
+
code: "STORAGE_ERROR",
|
|
6543
|
+
cause: new Error("expectAad is only supported by network-encrypted vaults")
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6296
6546
|
if (!this._isUnlocked || !this.masterKey) {
|
|
6297
6547
|
return vaultError({
|
|
6298
6548
|
code: "VAULT_LOCKED",
|
|
@@ -6314,7 +6564,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6314
6564
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6315
6565
|
}
|
|
6316
6566
|
const keyEnvelope = JSON.parse(keyResult.data.data);
|
|
6317
|
-
const keyBlobBytes =
|
|
6567
|
+
const keyBlobBytes = base64Decode2(keyEnvelope.key);
|
|
6318
6568
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
6319
6569
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
6320
6570
|
raw: true,
|
|
@@ -6324,7 +6574,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6324
6574
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6325
6575
|
}
|
|
6326
6576
|
const valueEnvelope = JSON.parse(valueResult.data.data);
|
|
6327
|
-
const encryptedBytes =
|
|
6577
|
+
const encryptedBytes = base64Decode2(valueEnvelope.data);
|
|
6328
6578
|
const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
|
|
6329
6579
|
const metadata = valueEnvelope.metadata ?? {};
|
|
6330
6580
|
const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
|
|
@@ -6572,7 +6822,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6572
6822
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
6573
6823
|
}
|
|
6574
6824
|
const keyEnvelope = JSON.parse(keyResult.data.data);
|
|
6575
|
-
const keyBlobBytes =
|
|
6825
|
+
const keyBlobBytes = base64Decode2(keyEnvelope.key);
|
|
6576
6826
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
6577
6827
|
const ephemeralSeed = this.crypto.randomBytes(32);
|
|
6578
6828
|
const ephemeralKeyPair = this.crypto.x25519FromSeed(ephemeralSeed);
|
|
@@ -6591,7 +6841,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6591
6841
|
encryptedGrant
|
|
6592
6842
|
);
|
|
6593
6843
|
const grantPayload = JSON.stringify({
|
|
6594
|
-
grant:
|
|
6844
|
+
grant: base64Encode2(grantBlob),
|
|
6595
6845
|
spaceId: this.vaultConfig.spaceId,
|
|
6596
6846
|
metadata: {
|
|
6597
6847
|
[VaultHeaders.GRANT_VERSION]: "1",
|
|
@@ -6631,6 +6881,14 @@ var DataVaultService = class extends BaseService {
|
|
|
6631
6881
|
*/
|
|
6632
6882
|
async getShared(grantorDID, key, options) {
|
|
6633
6883
|
return this.withTelemetry("getShared", key, async () => {
|
|
6884
|
+
if (options?.expectAad === true) {
|
|
6885
|
+
return vaultError({
|
|
6886
|
+
code: "STORAGE_ERROR",
|
|
6887
|
+
cause: new Error(
|
|
6888
|
+
"expectAad is not supported by getShared: the AAD binds the grantor's space, which this vault does not know"
|
|
6889
|
+
)
|
|
6890
|
+
});
|
|
6891
|
+
}
|
|
6634
6892
|
if (this.usesNetworkEncryption) {
|
|
6635
6893
|
const grantorKV = options?.kv;
|
|
6636
6894
|
if (!grantorKV) {
|
|
@@ -6725,7 +6983,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6725
6983
|
});
|
|
6726
6984
|
}
|
|
6727
6985
|
const grantEnvelope = typeof grantResult.data?.data === "string" ? JSON.parse(grantResult.data.data) : grantResult.data?.data;
|
|
6728
|
-
const grantBlobBytes =
|
|
6986
|
+
const grantBlobBytes = base64Decode2(grantEnvelope.grant);
|
|
6729
6987
|
const ephemeralPubKey = grantBlobBytes.slice(0, 32);
|
|
6730
6988
|
const encryptedGrant = grantBlobBytes.slice(32);
|
|
6731
6989
|
const sharedSecret = this.crypto.x25519Dh(
|
|
@@ -6748,7 +7006,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6748
7006
|
});
|
|
6749
7007
|
}
|
|
6750
7008
|
const valueEnvelope = typeof valueResult.data?.data === "string" ? JSON.parse(valueResult.data.data) : valueResult.data?.data;
|
|
6751
|
-
const encryptedBytes =
|
|
7009
|
+
const encryptedBytes = base64Decode2(valueEnvelope.data);
|
|
6752
7010
|
const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
|
|
6753
7011
|
const metadata = valueEnvelope.metadata ?? {};
|
|
6754
7012
|
const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
|
|
@@ -6802,7 +7060,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6802
7060
|
if (!result.ok) {
|
|
6803
7061
|
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
6804
7062
|
}
|
|
6805
|
-
const pubKeyBytes =
|
|
7063
|
+
const pubKeyBytes = base64Decode2(result.data);
|
|
6806
7064
|
return { ok: true, data: pubKeyBytes };
|
|
6807
7065
|
} catch (error) {
|
|
6808
7066
|
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
@@ -6921,7 +7179,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6921
7179
|
}
|
|
6922
7180
|
const currentEntry = getResult.data;
|
|
6923
7181
|
const newEntryKey = this.crypto.randomBytes(32);
|
|
6924
|
-
const newKeyId =
|
|
7182
|
+
const newKeyId = hexEncode2(this.crypto.sha256(newEntryKey)).slice(
|
|
6925
7183
|
0,
|
|
6926
7184
|
16
|
|
6927
7185
|
);
|
|
@@ -6938,7 +7196,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6938
7196
|
[VaultHeaders.KEY_ID]: newKeyId
|
|
6939
7197
|
};
|
|
6940
7198
|
const keyPayload = JSON.stringify({
|
|
6941
|
-
key:
|
|
7199
|
+
key: base64Encode2(newKeyBlob),
|
|
6942
7200
|
metadata: JSON.stringify({
|
|
6943
7201
|
keyId: newKeyId,
|
|
6944
7202
|
...metadata
|
|
@@ -6952,7 +7210,7 @@ var DataVaultService = class extends BaseService {
|
|
|
6952
7210
|
return kvWriteError("Failed to store rotated key blob", keyPutResult.error);
|
|
6953
7211
|
}
|
|
6954
7212
|
const valuePayload = JSON.stringify({
|
|
6955
|
-
data:
|
|
7213
|
+
data: base64Encode2(encrypted),
|
|
6956
7214
|
metadata
|
|
6957
7215
|
});
|
|
6958
7216
|
const valuePutResult = await this.tc.kv.put(
|
|
@@ -7244,116 +7502,6 @@ var SecretsService = class {
|
|
|
7244
7502
|
}
|
|
7245
7503
|
};
|
|
7246
7504
|
|
|
7247
|
-
// src/encryption/canonical.ts
|
|
7248
|
-
function canonicalize(value) {
|
|
7249
|
-
if (value === void 0) {
|
|
7250
|
-
return "";
|
|
7251
|
-
}
|
|
7252
|
-
return stringify(value);
|
|
7253
|
-
}
|
|
7254
|
-
function stringify(value) {
|
|
7255
|
-
if (value === null) return "null";
|
|
7256
|
-
switch (typeof value) {
|
|
7257
|
-
case "boolean":
|
|
7258
|
-
case "number":
|
|
7259
|
-
return JSON.stringify(value);
|
|
7260
|
-
case "string":
|
|
7261
|
-
return JSON.stringify(value);
|
|
7262
|
-
case "object": {
|
|
7263
|
-
if (Array.isArray(value)) {
|
|
7264
|
-
return `[${value.map(stringify).join(",")}]`;
|
|
7265
|
-
}
|
|
7266
|
-
const keys = Object.keys(value).sort();
|
|
7267
|
-
const parts = [];
|
|
7268
|
-
for (const k of keys) {
|
|
7269
|
-
const v = value[k];
|
|
7270
|
-
if (v === void 0) continue;
|
|
7271
|
-
parts.push(`${JSON.stringify(k)}:${stringify(v)}`);
|
|
7272
|
-
}
|
|
7273
|
-
return `{${parts.join(",")}}`;
|
|
7274
|
-
}
|
|
7275
|
-
default:
|
|
7276
|
-
throw new TypeError(
|
|
7277
|
-
`canonicalize: unsupported value type ${typeof value}`
|
|
7278
|
-
);
|
|
7279
|
-
}
|
|
7280
|
-
}
|
|
7281
|
-
var HEX = "0123456789abcdef";
|
|
7282
|
-
function hexEncode2(bytes) {
|
|
7283
|
-
let out = "";
|
|
7284
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
7285
|
-
const b = bytes[i];
|
|
7286
|
-
out += HEX[b >> 4 & 15] + HEX[b & 15];
|
|
7287
|
-
}
|
|
7288
|
-
return out;
|
|
7289
|
-
}
|
|
7290
|
-
function hexDecode(hex) {
|
|
7291
|
-
if (hex.length % 2 !== 0) {
|
|
7292
|
-
throw new Error("hex string must have even length");
|
|
7293
|
-
}
|
|
7294
|
-
const out = new Uint8Array(hex.length / 2);
|
|
7295
|
-
for (let i = 0; i < out.length; i++) {
|
|
7296
|
-
const hi = parseInt(hex[i * 2], 16);
|
|
7297
|
-
const lo = parseInt(hex[i * 2 + 1], 16);
|
|
7298
|
-
if (Number.isNaN(hi) || Number.isNaN(lo)) {
|
|
7299
|
-
throw new Error("invalid hex character");
|
|
7300
|
-
}
|
|
7301
|
-
out[i] = hi << 4 | lo;
|
|
7302
|
-
}
|
|
7303
|
-
return out;
|
|
7304
|
-
}
|
|
7305
|
-
function base64Encode2(bytes) {
|
|
7306
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7307
|
-
let out = "";
|
|
7308
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
7309
|
-
const b0 = bytes[i];
|
|
7310
|
-
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
7311
|
-
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
7312
|
-
out += chars[b0 >> 2 & 63];
|
|
7313
|
-
out += chars[(b0 << 4 | b1 >> 4) & 63];
|
|
7314
|
-
out += i + 1 < bytes.length ? chars[(b1 << 2 | b2 >> 6) & 63] : "=";
|
|
7315
|
-
out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
|
|
7316
|
-
}
|
|
7317
|
-
return out;
|
|
7318
|
-
}
|
|
7319
|
-
function base64Decode2(s) {
|
|
7320
|
-
const clean = s.replace(/[^A-Za-z0-9+/=]/g, "");
|
|
7321
|
-
const len = clean.length;
|
|
7322
|
-
if (len % 4 !== 0) {
|
|
7323
|
-
throw new Error("invalid base64 input");
|
|
7324
|
-
}
|
|
7325
|
-
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
|
7326
|
-
const outLen = len / 4 * 3 - padding;
|
|
7327
|
-
const out = new Uint8Array(outLen);
|
|
7328
|
-
const lookup = {};
|
|
7329
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7330
|
-
for (let i = 0; i < chars.length; i++) lookup[chars[i]] = i;
|
|
7331
|
-
let outIdx = 0;
|
|
7332
|
-
for (let i = 0; i < len; i += 4) {
|
|
7333
|
-
const v0 = lookup[clean[i]] ?? 0;
|
|
7334
|
-
const v1 = lookup[clean[i + 1]] ?? 0;
|
|
7335
|
-
const v2 = clean[i + 2] === "=" ? 0 : lookup[clean[i + 2]] ?? 0;
|
|
7336
|
-
const v3 = clean[i + 3] === "=" ? 0 : lookup[clean[i + 3]] ?? 0;
|
|
7337
|
-
const b0 = v0 << 2 | v1 >> 4;
|
|
7338
|
-
const b1 = (v1 & 15) << 4 | v2 >> 2;
|
|
7339
|
-
const b2 = (v2 & 3) << 6 | v3;
|
|
7340
|
-
if (outIdx < outLen) out[outIdx++] = b0;
|
|
7341
|
-
if (outIdx < outLen) out[outIdx++] = b1;
|
|
7342
|
-
if (outIdx < outLen) out[outIdx++] = b2;
|
|
7343
|
-
}
|
|
7344
|
-
return out;
|
|
7345
|
-
}
|
|
7346
|
-
function utf8Encode(s) {
|
|
7347
|
-
return new TextEncoder().encode(s);
|
|
7348
|
-
}
|
|
7349
|
-
function utf8Decode(b) {
|
|
7350
|
-
return new TextDecoder().decode(b);
|
|
7351
|
-
}
|
|
7352
|
-
function canonicalHashHex(sha256, value) {
|
|
7353
|
-
const canonical = canonicalize(value);
|
|
7354
|
-
return hexEncode2(sha256(utf8Encode(canonical)));
|
|
7355
|
-
}
|
|
7356
|
-
|
|
7357
7505
|
// src/encryption/types.ts
|
|
7358
7506
|
var DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
|
|
7359
7507
|
var ENVELOPE_VERSION = 1;
|
|
@@ -7563,7 +7711,7 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7563
7711
|
const symmetricKey = crypto2.randomBytes(32);
|
|
7564
7712
|
const ciphertext = crypto2.authEncrypt(symmetricKey, input.plaintext, input.aad);
|
|
7565
7713
|
const wrapped = crypto2.sealToNetworkKey(input.networkPublicKey, symmetricKey);
|
|
7566
|
-
const encryptedSymmetricKey =
|
|
7714
|
+
const encryptedSymmetricKey = base64Encode(wrapped);
|
|
7567
7715
|
const encryptedSymmetricKeyHash = canonicalHashHex(
|
|
7568
7716
|
crypto2.sha256,
|
|
7569
7717
|
encryptedSymmetricKey
|
|
@@ -7575,8 +7723,8 @@ function encryptToNetwork(crypto2, input) {
|
|
|
7575
7723
|
keyVersion,
|
|
7576
7724
|
encryptedSymmetricKey,
|
|
7577
7725
|
encryptedSymmetricKeyHash,
|
|
7578
|
-
ciphertext:
|
|
7579
|
-
...input.aad !== void 0 ? { aad:
|
|
7726
|
+
ciphertext: base64Encode(ciphertext),
|
|
7727
|
+
...input.aad !== void 0 ? { aad: base64Encode(input.aad) } : {},
|
|
7580
7728
|
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
7581
7729
|
};
|
|
7582
7730
|
return { envelope, symmetricKey };
|
|
@@ -7650,8 +7798,8 @@ function validateEnvelope(crypto2, envelope) {
|
|
|
7650
7798
|
return { ok: true, data: e };
|
|
7651
7799
|
}
|
|
7652
7800
|
function decryptEnvelopeWithKey(crypto2, envelope, symmetricKey) {
|
|
7653
|
-
const ciphertext =
|
|
7654
|
-
const aad = envelope.aad !== void 0 ?
|
|
7801
|
+
const ciphertext = base64Decode(envelope.ciphertext);
|
|
7802
|
+
const aad = envelope.aad !== void 0 ? base64Decode(envelope.aad) : void 0;
|
|
7655
7803
|
return crypto2.authDecrypt(symmetricKey, ciphertext, aad);
|
|
7656
7804
|
}
|
|
7657
7805
|
|
|
@@ -7669,7 +7817,7 @@ function buildCanonicalDecryptRequest(input) {
|
|
|
7669
7817
|
return { canonicalBody, bodyHash, receiverPublicKeyHash };
|
|
7670
7818
|
}
|
|
7671
7819
|
function buildDecryptFacts(input) {
|
|
7672
|
-
const bodyHash = input.canonicalBody !== void 0 ?
|
|
7820
|
+
const bodyHash = input.canonicalBody !== void 0 ? hexEncode(input.crypto.sha256(utf8Encode(input.canonicalBody))) : canonicalHashHex(
|
|
7673
7821
|
input.crypto.sha256,
|
|
7674
7822
|
input.body
|
|
7675
7823
|
);
|
|
@@ -7952,7 +8100,7 @@ function verifyDecryptResponse(input) {
|
|
|
7952
8100
|
})
|
|
7953
8101
|
};
|
|
7954
8102
|
}
|
|
7955
|
-
const expectedRequestHash =
|
|
8103
|
+
const expectedRequestHash = hexEncode(
|
|
7956
8104
|
crypto2.sha256(utf8Encode(`${invocationCid}${requestBodyHash}`))
|
|
7957
8105
|
);
|
|
7958
8106
|
if (response.requestHash !== expectedRequestHash) {
|
|
@@ -7976,7 +8124,7 @@ function verifyDecryptResponse(input) {
|
|
|
7976
8124
|
const signedBytes = new TextEncoder().encode(
|
|
7977
8125
|
canonicalSignedResponse(response)
|
|
7978
8126
|
);
|
|
7979
|
-
const signatureBytes =
|
|
8127
|
+
const signatureBytes = base64Decode(response.nodeSignature);
|
|
7980
8128
|
if (!crypto2.verifyNodeSignature(response.nodeId, signedBytes, signatureBytes)) {
|
|
7981
8129
|
return {
|
|
7982
8130
|
ok: false,
|
|
@@ -7988,7 +8136,7 @@ function verifyDecryptResponse(input) {
|
|
|
7988
8136
|
return { ok: true, data: response };
|
|
7989
8137
|
}
|
|
7990
8138
|
function openWrappedKey(crypto2, receiverPrivateKey, response) {
|
|
7991
|
-
const wrapped =
|
|
8139
|
+
const wrapped = base64Decode(response.wrappedKey);
|
|
7992
8140
|
return crypto2.openWithReceiverKey(receiverPrivateKey, wrapped);
|
|
7993
8141
|
}
|
|
7994
8142
|
|
|
@@ -8032,7 +8180,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8032
8180
|
const usable = ensureNetworkUsableForDecrypt(discovered.data);
|
|
8033
8181
|
if (!usable.ok) return usable;
|
|
8034
8182
|
const descriptor = usable.data;
|
|
8035
|
-
const networkPublicKey =
|
|
8183
|
+
const networkPublicKey = base64Decode(descriptor.publicEncryptionKey);
|
|
8036
8184
|
const result = encryptToNetwork(this.crypto, {
|
|
8037
8185
|
// Discovery returns the node's canonical network identifier. Persist
|
|
8038
8186
|
// that spelling in the envelope so a later response can be compared
|
|
@@ -8060,7 +8208,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8060
8208
|
this.assertActive();
|
|
8061
8209
|
const validated = validateEnvelope(this.crypto, envelope);
|
|
8062
8210
|
if (!validated.ok) return validated;
|
|
8063
|
-
if (options?.aad !== void 0 && validated.data.aad !==
|
|
8211
|
+
if (options?.aad !== void 0 && validated.data.aad !== base64Encode(options.aad)) {
|
|
8064
8212
|
return encErr(
|
|
8065
8213
|
encryptionError({
|
|
8066
8214
|
code: "INVALID_INPUT",
|
|
@@ -8088,7 +8236,7 @@ var EncryptionService = class extends BaseService {
|
|
|
8088
8236
|
);
|
|
8089
8237
|
}
|
|
8090
8238
|
const receiverKey = generateRandomReceiverKey({ crypto: this.crypto });
|
|
8091
|
-
const receiverPublicKey =
|
|
8239
|
+
const receiverPublicKey = base64Encode(receiverKey.publicKey);
|
|
8092
8240
|
const receiverPublicKeyHash = canonicalHashHex(
|
|
8093
8241
|
this.crypto.sha256,
|
|
8094
8242
|
receiverPublicKey
|
|
@@ -8264,8 +8412,8 @@ export {
|
|
|
8264
8412
|
authRequiredError,
|
|
8265
8413
|
authUnauthorizedError,
|
|
8266
8414
|
authorizationVerdictOf,
|
|
8267
|
-
|
|
8268
|
-
|
|
8415
|
+
base64Decode,
|
|
8416
|
+
base64Encode,
|
|
8269
8417
|
buildCanonicalDecryptRequest,
|
|
8270
8418
|
buildDecryptAttenuation,
|
|
8271
8419
|
buildDecryptFacts,
|
|
@@ -8295,13 +8443,14 @@ export {
|
|
|
8295
8443
|
generateRandomReceiverKey,
|
|
8296
8444
|
getTinyCloudDebugLogs,
|
|
8297
8445
|
hexDecode,
|
|
8298
|
-
|
|
8446
|
+
hexEncode,
|
|
8299
8447
|
installTinyCloudDebugGlobals,
|
|
8300
8448
|
isNetworkId,
|
|
8301
8449
|
isStorageFullError,
|
|
8302
8450
|
kvPrefixCovers,
|
|
8303
8451
|
networkDiscoveryKey,
|
|
8304
8452
|
networkError,
|
|
8453
|
+
nodeRetryError,
|
|
8305
8454
|
notFoundError,
|
|
8306
8455
|
ok,
|
|
8307
8456
|
openWrappedKey,
|