@tinycloud/replica 0.1.0 → 0.1.1-beta.1
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 +8 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/replica.worker.js +582 -360
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +64 -4
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +17 -2
- package/dist/sqlite.d.ts +17 -2
- package/dist/sqlite.js +64 -4
- package/dist/sqlite.js.map +1 -1
- package/package.json +2 -2
package/dist/replica.worker.js
CHANGED
|
@@ -6852,7 +6852,12 @@ var TelemetryEvents = {
|
|
|
6852
6852
|
SERVICE_ERROR: "service.error",
|
|
6853
6853
|
SERVICE_RETRY: "service.retry",
|
|
6854
6854
|
SESSION_CHANGED: "session.changed",
|
|
6855
|
-
SESSION_EXPIRED: "session.expired"
|
|
6855
|
+
SESSION_EXPIRED: "session.expired",
|
|
6856
|
+
REPLICATION_READ: "replication.read",
|
|
6857
|
+
REPLICATION_WRITE: "replication.write",
|
|
6858
|
+
REPLICATION_SYNC: "replication.sync",
|
|
6859
|
+
REPLICATION_STATE: "replication.state",
|
|
6860
|
+
REPLICATION_DIVERGENCE: "replication.divergence"
|
|
6856
6861
|
};
|
|
6857
6862
|
function ok(data) {
|
|
6858
6863
|
return { ok: true, data };
|
|
@@ -6876,9 +6881,34 @@ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
|
|
|
6876
6881
|
"endedAt",
|
|
6877
6882
|
"startedAt",
|
|
6878
6883
|
"status",
|
|
6879
|
-
"timestamp"
|
|
6884
|
+
"timestamp",
|
|
6885
|
+
"latencyMs",
|
|
6886
|
+
"stalenessMs",
|
|
6887
|
+
"count",
|
|
6888
|
+
"changes",
|
|
6889
|
+
"deleted",
|
|
6890
|
+
"fetched",
|
|
6891
|
+
"pages",
|
|
6892
|
+
"lagMs",
|
|
6893
|
+
"pendingCleared"
|
|
6880
6894
|
]);
|
|
6881
6895
|
var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
|
|
6896
|
+
var SAFE_REPLICATION_FIELDS = {
|
|
6897
|
+
source: ["replica", "network", "none"],
|
|
6898
|
+
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"],
|
|
6899
|
+
op: ["get", "list", "put", "delete", "batchPut"],
|
|
6900
|
+
trigger: ["start", "interval", "stale_read", "manual"],
|
|
6901
|
+
outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
|
|
6902
|
+
class: ["offline", "authority", "storage", "node"],
|
|
6903
|
+
state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
|
|
6904
|
+
kind: ["value", "missing_local", "extra_local", "keys", "order"],
|
|
6905
|
+
coverage: ["empty", "bootstrapping", "complete"],
|
|
6906
|
+
authority: ["valid", "expired", "revoked", "not-yet-valid"],
|
|
6907
|
+
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"],
|
|
6908
|
+
pendingState: ["in_flight", "committed", "ambiguous"],
|
|
6909
|
+
strategy: ["session", "minted", "installed"],
|
|
6910
|
+
verify: ["match", "diverged", "error", "aborted"]
|
|
6911
|
+
};
|
|
6882
6912
|
function finiteNumber(value) {
|
|
6883
6913
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
6884
6914
|
}
|
|
@@ -6915,6 +6945,12 @@ function projectDiagnosticData(value) {
|
|
|
6915
6945
|
const boolean = read(value, key);
|
|
6916
6946
|
if (typeof boolean === "boolean") projected[key] = boolean;
|
|
6917
6947
|
}
|
|
6948
|
+
for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
|
|
6949
|
+
const candidate = read(value, key);
|
|
6950
|
+
if (typeof candidate === "string" && allowed.includes(candidate)) {
|
|
6951
|
+
projected[key] = candidate;
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6918
6954
|
const url = read(value, "url");
|
|
6919
6955
|
if (url !== void 0) projected.url = REDACTED;
|
|
6920
6956
|
const error = read(value, "error");
|
|
@@ -7412,6 +7448,12 @@ var ServiceContext = class {
|
|
|
7412
7448
|
get abortSignal() {
|
|
7413
7449
|
return this._abortController.signal;
|
|
7414
7450
|
}
|
|
7451
|
+
get operationAbortSignal() {
|
|
7452
|
+
return this._operationAbortSignal;
|
|
7453
|
+
}
|
|
7454
|
+
setOperationAbortSignal(signal) {
|
|
7455
|
+
this._operationAbortSignal = signal;
|
|
7456
|
+
}
|
|
7415
7457
|
/**
|
|
7416
7458
|
* Permanently retire this graph after its owner installs a replacement.
|
|
7417
7459
|
* Unlike `abort()`, retirement never creates a fresh controller, so captured
|
|
@@ -8086,6 +8128,7 @@ var BaseService = class {
|
|
|
8086
8128
|
const parents = [
|
|
8087
8129
|
this.abortController.signal,
|
|
8088
8130
|
this.context?.abortSignal,
|
|
8131
|
+
this.context?.operationAbortSignal,
|
|
8089
8132
|
signal
|
|
8090
8133
|
].filter((parent) => parent !== void 0);
|
|
8091
8134
|
for (const parent of parents) {
|
|
@@ -8325,14 +8368,28 @@ var KVService = class extends BaseService {
|
|
|
8325
8368
|
*/
|
|
8326
8369
|
constructor(config = {}) {
|
|
8327
8370
|
super();
|
|
8371
|
+
this.readThrough = null;
|
|
8372
|
+
this.readThroughReady = null;
|
|
8328
8373
|
this._config = config;
|
|
8329
8374
|
}
|
|
8375
|
+
/** Delay KV routing until an asynchronously loaded read-through is attached. */
|
|
8376
|
+
setReadThroughReady(ready) {
|
|
8377
|
+
const settled = ready.then(() => void 0, () => void 0);
|
|
8378
|
+
this.readThroughReady = settled;
|
|
8379
|
+
void settled.then(() => {
|
|
8380
|
+
if (this.readThroughReady === settled) this.readThroughReady = null;
|
|
8381
|
+
});
|
|
8382
|
+
}
|
|
8330
8383
|
/**
|
|
8331
8384
|
* Get the service configuration.
|
|
8332
8385
|
*/
|
|
8333
8386
|
get config() {
|
|
8334
8387
|
return this._config;
|
|
8335
8388
|
}
|
|
8389
|
+
/** Install or remove the optional replication read-through policy. */
|
|
8390
|
+
setReadThrough(readThrough) {
|
|
8391
|
+
this.readThrough = readThrough;
|
|
8392
|
+
}
|
|
8336
8393
|
handleQuotaErrorResponse(response, errorText, key) {
|
|
8337
8394
|
if (response.status !== 402 && response.status !== 413) {
|
|
8338
8395
|
return void 0;
|
|
@@ -8586,6 +8643,7 @@ var KVService = class extends BaseService {
|
|
|
8586
8643
|
return aligned;
|
|
8587
8644
|
}
|
|
8588
8645
|
async batchRead(keys, action, options) {
|
|
8646
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
8589
8647
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
8590
8648
|
if (keys.length === 0) return ok({ results: [], count: 0 });
|
|
8591
8649
|
if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
|
|
@@ -8836,6 +8894,7 @@ var KVService = class extends BaseService {
|
|
|
8836
8894
|
*/
|
|
8837
8895
|
async get(key, options) {
|
|
8838
8896
|
return this.withTelemetry("get", key, async () => {
|
|
8897
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
8839
8898
|
if (!this.requireAuth()) {
|
|
8840
8899
|
return err(authRequiredError("kv"));
|
|
8841
8900
|
}
|
|
@@ -8849,64 +8908,38 @@ var KVService = class extends BaseService {
|
|
|
8849
8908
|
const path = this.getFullPath(key, options?.prefix);
|
|
8850
8909
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
8851
8910
|
try {
|
|
8852
|
-
const
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
response,
|
|
8875
|
-
errorText2,
|
|
8876
|
-
[path],
|
|
8877
|
-
KVAction.GET,
|
|
8878
|
-
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
8879
|
-
);
|
|
8880
|
-
}
|
|
8881
|
-
if (response.status === 404) {
|
|
8882
|
-
return this.classifyNotFound(response, key);
|
|
8883
|
-
}
|
|
8884
|
-
const errorText = await response.text();
|
|
8885
|
-
if (response.status === 413) {
|
|
8886
|
-
return err(serviceError(
|
|
8887
|
-
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
8888
|
-
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
8889
|
-
"kv",
|
|
8890
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8891
|
-
));
|
|
8911
|
+
const network = () => this.getFromNetwork(key, path, options, request.signal);
|
|
8912
|
+
if (options?.source === "network") {
|
|
8913
|
+
const observer = this.readThrough;
|
|
8914
|
+
if (observer === null) return await network();
|
|
8915
|
+
let outcome = "error";
|
|
8916
|
+
const startedAt = Date.now();
|
|
8917
|
+
try {
|
|
8918
|
+
const result = await network();
|
|
8919
|
+
outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
|
|
8920
|
+
return result;
|
|
8921
|
+
} finally {
|
|
8922
|
+
try {
|
|
8923
|
+
observer.observeNetworkRequested({
|
|
8924
|
+
op: "get",
|
|
8925
|
+
space: this.context.session.spaceId,
|
|
8926
|
+
path,
|
|
8927
|
+
reason: "NETWORK_REQUESTED",
|
|
8928
|
+
outcome,
|
|
8929
|
+
latencyMs: Date.now() - startedAt
|
|
8930
|
+
});
|
|
8931
|
+
} catch {
|
|
8932
|
+
}
|
|
8892
8933
|
}
|
|
8893
|
-
return err(
|
|
8894
|
-
serviceError(
|
|
8895
|
-
ErrorCodes.NETWORK_ERROR,
|
|
8896
|
-
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
8897
|
-
"kv",
|
|
8898
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8899
|
-
)
|
|
8900
|
-
);
|
|
8901
8934
|
}
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8935
|
+
if (this.readThrough === null) return await network();
|
|
8936
|
+
return await this.readThrough.get({
|
|
8937
|
+
space: this.context.session.spaceId,
|
|
8938
|
+
key,
|
|
8939
|
+
path,
|
|
8940
|
+
options,
|
|
8941
|
+
signal: request.signal,
|
|
8942
|
+
network
|
|
8910
8943
|
});
|
|
8911
8944
|
} catch (error) {
|
|
8912
8945
|
return err(wrapError("kv", error));
|
|
@@ -8915,6 +8948,56 @@ var KVService = class extends BaseService {
|
|
|
8915
8948
|
}
|
|
8916
8949
|
});
|
|
8917
8950
|
}
|
|
8951
|
+
async getFromNetwork(key, path, options, signal) {
|
|
8952
|
+
const response = await this.invokeOperation(
|
|
8953
|
+
path,
|
|
8954
|
+
KVAction.GET,
|
|
8955
|
+
void 0,
|
|
8956
|
+
signal,
|
|
8957
|
+
options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
|
|
8958
|
+
);
|
|
8959
|
+
if (!response.ok) {
|
|
8960
|
+
if (response.status === 401 || response.status === 403) {
|
|
8961
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
8962
|
+
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
8963
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
8964
|
+
service: "kv",
|
|
8965
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
8966
|
+
meta: {
|
|
8967
|
+
status: response.status,
|
|
8968
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
8969
|
+
requiredAction: KVAction.GET
|
|
8970
|
+
}
|
|
8971
|
+
}) : void 0;
|
|
8972
|
+
return this.authorizationFailure(
|
|
8973
|
+
`Failed to get key ${JSON.stringify(key)}`,
|
|
8974
|
+
response,
|
|
8975
|
+
errorText2,
|
|
8976
|
+
[path],
|
|
8977
|
+
KVAction.GET,
|
|
8978
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
8979
|
+
);
|
|
8980
|
+
}
|
|
8981
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
8982
|
+
const errorText = await response.text();
|
|
8983
|
+
if (response.status === 413) {
|
|
8984
|
+
return err(serviceError(
|
|
8985
|
+
ErrorCodes.KV_RESPONSE_TOO_LARGE,
|
|
8986
|
+
`KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
|
|
8987
|
+
"kv",
|
|
8988
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8989
|
+
));
|
|
8990
|
+
}
|
|
8991
|
+
return err(serviceError(
|
|
8992
|
+
ErrorCodes.NETWORK_ERROR,
|
|
8993
|
+
`Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
8994
|
+
"kv",
|
|
8995
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8996
|
+
));
|
|
8997
|
+
}
|
|
8998
|
+
const data = await this.parseResponse(response, options?.raw, options?.binary);
|
|
8999
|
+
return ok({ data, headers: this.createResponseHeaders(response.headers) });
|
|
9000
|
+
}
|
|
8918
9001
|
async batchGet(keys, options) {
|
|
8919
9002
|
return this.withTelemetry(
|
|
8920
9003
|
"batchGet",
|
|
@@ -8927,6 +9010,7 @@ var KVService = class extends BaseService {
|
|
|
8927
9010
|
*/
|
|
8928
9011
|
async put(key, value, options) {
|
|
8929
9012
|
return this.withTelemetry("put", key, async () => {
|
|
9013
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
8930
9014
|
if (!this.requireAuth()) {
|
|
8931
9015
|
return err(authRequiredError("kv"));
|
|
8932
9016
|
}
|
|
@@ -8941,63 +9025,15 @@ var KVService = class extends BaseService {
|
|
|
8941
9025
|
const body = this.serializePutValue(value, options?.contentType);
|
|
8942
9026
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
8943
9027
|
try {
|
|
8944
|
-
const
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
{
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
);
|
|
8954
|
-
if (response.status === 401 || response.status === 403) {
|
|
8955
|
-
return this.authorizationFailure(
|
|
8956
|
-
`Failed to put key ${JSON.stringify(key)}`,
|
|
8957
|
-
response,
|
|
8958
|
-
await this.readAuthorizationText(response),
|
|
8959
|
-
[path],
|
|
8960
|
-
KVAction.PUT
|
|
8961
|
-
);
|
|
8962
|
-
}
|
|
8963
|
-
if (!response.ok) {
|
|
8964
|
-
const errorText = await response.text();
|
|
8965
|
-
if (response.status === 412) {
|
|
8966
|
-
return err(serviceError(
|
|
8967
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
8968
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
8969
|
-
"kv",
|
|
8970
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8971
|
-
));
|
|
8972
|
-
}
|
|
8973
|
-
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
8974
|
-
return err(serviceError(
|
|
8975
|
-
ErrorCodes.KV_CONFLICT,
|
|
8976
|
-
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
8977
|
-
"kv",
|
|
8978
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8979
|
-
));
|
|
8980
|
-
}
|
|
8981
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
8982
|
-
response,
|
|
8983
|
-
errorText,
|
|
8984
|
-
key
|
|
8985
|
-
);
|
|
8986
|
-
if (quotaError) {
|
|
8987
|
-
return quotaError;
|
|
8988
|
-
}
|
|
8989
|
-
return err(
|
|
8990
|
-
serviceError(
|
|
8991
|
-
ErrorCodes.KV_WRITE_FAILED,
|
|
8992
|
-
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
8993
|
-
"kv",
|
|
8994
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8995
|
-
)
|
|
8996
|
-
);
|
|
8997
|
-
}
|
|
8998
|
-
return ok({
|
|
8999
|
-
data: void 0,
|
|
9000
|
-
headers: this.createResponseHeaders(response.headers)
|
|
9028
|
+
const network = () => this.putToNetwork(key, path, body, options, request.signal);
|
|
9029
|
+
if (this.readThrough === null) return await network();
|
|
9030
|
+
return await this.readThrough.write({
|
|
9031
|
+
op: "put",
|
|
9032
|
+
space: this.context.session.spaceId,
|
|
9033
|
+
entries: [{ path, body }],
|
|
9034
|
+
signal: request.signal,
|
|
9035
|
+
conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
|
|
9036
|
+
network
|
|
9001
9037
|
});
|
|
9002
9038
|
} catch (error) {
|
|
9003
9039
|
return err(wrapError("kv", error));
|
|
@@ -9006,11 +9042,64 @@ var KVService = class extends BaseService {
|
|
|
9006
9042
|
}
|
|
9007
9043
|
});
|
|
9008
9044
|
}
|
|
9045
|
+
async putToNetwork(key, path, body, options, signal) {
|
|
9046
|
+
const response = await this.invokeOperation(
|
|
9047
|
+
path,
|
|
9048
|
+
KVAction.PUT,
|
|
9049
|
+
body,
|
|
9050
|
+
signal,
|
|
9051
|
+
{
|
|
9052
|
+
...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
|
|
9053
|
+
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
9054
|
+
}
|
|
9055
|
+
);
|
|
9056
|
+
if (response.status === 401 || response.status === 403) {
|
|
9057
|
+
return this.authorizationFailure(
|
|
9058
|
+
`Failed to put key ${JSON.stringify(key)}`,
|
|
9059
|
+
response,
|
|
9060
|
+
await this.readAuthorizationText(response),
|
|
9061
|
+
[path],
|
|
9062
|
+
KVAction.PUT
|
|
9063
|
+
);
|
|
9064
|
+
}
|
|
9065
|
+
if (!response.ok) {
|
|
9066
|
+
const errorText = await response.text();
|
|
9067
|
+
if (response.status === 412) {
|
|
9068
|
+
return err(serviceError(
|
|
9069
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
9070
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
9071
|
+
"kv",
|
|
9072
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9073
|
+
));
|
|
9074
|
+
}
|
|
9075
|
+
if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
|
|
9076
|
+
return err(serviceError(
|
|
9077
|
+
ErrorCodes.KV_CONFLICT,
|
|
9078
|
+
`Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
|
|
9079
|
+
"kv",
|
|
9080
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9081
|
+
));
|
|
9082
|
+
}
|
|
9083
|
+
const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
|
|
9084
|
+
if (quotaError) return quotaError;
|
|
9085
|
+
return err(serviceError(
|
|
9086
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
9087
|
+
`Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9088
|
+
"kv",
|
|
9089
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9090
|
+
));
|
|
9091
|
+
}
|
|
9092
|
+
return ok({
|
|
9093
|
+
data: void 0,
|
|
9094
|
+
headers: this.createResponseHeaders(response.headers)
|
|
9095
|
+
});
|
|
9096
|
+
}
|
|
9009
9097
|
/**
|
|
9010
9098
|
* Store multiple values in one TinyCloud KV invocation.
|
|
9011
9099
|
*/
|
|
9012
9100
|
async batchPut(items, options) {
|
|
9013
9101
|
return this.withTelemetry("batchPut", String(items.length), async () => {
|
|
9102
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9014
9103
|
if (!this.requireAuth()) {
|
|
9015
9104
|
return err(authRequiredError("kv"));
|
|
9016
9105
|
}
|
|
@@ -9041,155 +9130,177 @@ var KVService = class extends BaseService {
|
|
|
9041
9130
|
}
|
|
9042
9131
|
seen.add(path);
|
|
9043
9132
|
}
|
|
9044
|
-
let requestMayHaveDispatched = false;
|
|
9045
9133
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9046
9134
|
try {
|
|
9047
|
-
const
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
session2,
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9135
|
+
const network = () => this.batchPutToNetwork(items, paths, request.signal);
|
|
9136
|
+
if (this.readThrough === null) return await network();
|
|
9137
|
+
const entries = items.map((item, index) => ({
|
|
9138
|
+
path: paths[index],
|
|
9139
|
+
body: this.serializeBatchPutValue(item)
|
|
9140
|
+
}));
|
|
9141
|
+
return await this.readThrough.write({
|
|
9142
|
+
op: "batchPut",
|
|
9143
|
+
space: session2.spaceId,
|
|
9144
|
+
entries,
|
|
9145
|
+
signal: request.signal,
|
|
9146
|
+
network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
|
|
9147
|
+
});
|
|
9148
|
+
} catch (error) {
|
|
9149
|
+
const wrapped = wrapError("kv", error);
|
|
9150
|
+
return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
|
|
9151
|
+
} finally {
|
|
9152
|
+
request.dispose();
|
|
9153
|
+
}
|
|
9154
|
+
});
|
|
9155
|
+
}
|
|
9156
|
+
async batchPutToNetwork(items, paths, signal, entries) {
|
|
9157
|
+
const session2 = this.context.session;
|
|
9158
|
+
let requestMayHaveDispatched = false;
|
|
9159
|
+
try {
|
|
9160
|
+
const body = new FormData();
|
|
9161
|
+
for (let index = 0; index < items.length; index++) {
|
|
9162
|
+
body.append(
|
|
9163
|
+
encodeKvBatchPartName(paths[index]),
|
|
9164
|
+
entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
|
|
9062
9165
|
);
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
return this.authorizationFailure(
|
|
9090
|
-
`Failed to batch put ${items.length} key(s)`,
|
|
9091
|
-
response,
|
|
9092
|
-
errorText,
|
|
9093
|
-
paths,
|
|
9094
|
-
KVAction.PUT
|
|
9095
|
-
);
|
|
9096
|
-
}
|
|
9097
|
-
const quotaError = this.handleQuotaErrorResponse(
|
|
9098
|
-
response,
|
|
9099
|
-
errorText,
|
|
9100
|
-
"batch"
|
|
9101
|
-
);
|
|
9102
|
-
if (quotaError) {
|
|
9103
|
-
return quotaError;
|
|
9104
|
-
}
|
|
9166
|
+
}
|
|
9167
|
+
const headers = this.context.invokeAny(
|
|
9168
|
+
session2,
|
|
9169
|
+
paths.map((path) => ({
|
|
9170
|
+
spaceId: session2.spaceId,
|
|
9171
|
+
service: "kv",
|
|
9172
|
+
path,
|
|
9173
|
+
action: KVAction.PUT
|
|
9174
|
+
}))
|
|
9175
|
+
);
|
|
9176
|
+
const url = `${this.host}/invoke`;
|
|
9177
|
+
const init = {
|
|
9178
|
+
method: "POST",
|
|
9179
|
+
headers,
|
|
9180
|
+
body,
|
|
9181
|
+
signal
|
|
9182
|
+
};
|
|
9183
|
+
const fetchFn = this.context.fetch;
|
|
9184
|
+
requestMayHaveDispatched = true;
|
|
9185
|
+
const response = await fetchFn(url, init);
|
|
9186
|
+
if (!response.ok) {
|
|
9187
|
+
let errorText;
|
|
9188
|
+
try {
|
|
9189
|
+
errorText = await response.text();
|
|
9190
|
+
} catch (textError) {
|
|
9191
|
+
const cause = textError instanceof Error ? textError : new Error(String(textError));
|
|
9105
9192
|
return err(
|
|
9106
9193
|
serviceError(
|
|
9107
9194
|
ErrorCodes.KV_WRITE_FAILED,
|
|
9108
|
-
`Failed to batch put ${items.length} key(s): ${response.status} - ${
|
|
9195
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
|
|
9109
9196
|
"kv",
|
|
9110
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9197
|
+
{ cause, meta: { status: response.status, statusText: response.statusText } }
|
|
9111
9198
|
)
|
|
9112
9199
|
);
|
|
9113
9200
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
serviceError(
|
|
9122
|
-
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
9123
|
-
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
9124
|
-
"kv",
|
|
9125
|
-
{
|
|
9126
|
-
cause,
|
|
9127
|
-
meta: {
|
|
9128
|
-
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
9129
|
-
requestMayHaveDispatched: true,
|
|
9130
|
-
responseReceived: true,
|
|
9131
|
-
status: response.status,
|
|
9132
|
-
outcome: "batch-unconfirmed"
|
|
9133
|
-
}
|
|
9134
|
-
}
|
|
9135
|
-
)
|
|
9201
|
+
if (response.status === 401 || response.status === 403) {
|
|
9202
|
+
return this.authorizationFailure(
|
|
9203
|
+
`Failed to batch put ${items.length} key(s)`,
|
|
9204
|
+
response,
|
|
9205
|
+
errorText,
|
|
9206
|
+
paths,
|
|
9207
|
+
KVAction.PUT
|
|
9136
9208
|
);
|
|
9137
9209
|
}
|
|
9138
|
-
const
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
{
|
|
9146
|
-
meta: {
|
|
9147
|
-
requestMayHaveDispatched: true,
|
|
9148
|
-
responseReceived: true,
|
|
9149
|
-
status: response.status,
|
|
9150
|
-
outcome: "batch-unconfirmed"
|
|
9151
|
-
}
|
|
9152
|
-
}
|
|
9153
|
-
)
|
|
9154
|
-
);
|
|
9210
|
+
const quotaError = this.handleQuotaErrorResponse(
|
|
9211
|
+
response,
|
|
9212
|
+
errorText,
|
|
9213
|
+
"batch"
|
|
9214
|
+
);
|
|
9215
|
+
if (quotaError) {
|
|
9216
|
+
return quotaError;
|
|
9155
9217
|
}
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9218
|
+
return err(
|
|
9219
|
+
serviceError(
|
|
9220
|
+
ErrorCodes.KV_WRITE_FAILED,
|
|
9221
|
+
`Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
|
|
9222
|
+
"kv",
|
|
9223
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9224
|
+
)
|
|
9225
|
+
);
|
|
9226
|
+
}
|
|
9227
|
+
let rawBody;
|
|
9228
|
+
try {
|
|
9229
|
+
rawBody = await response.json();
|
|
9230
|
+
} catch (jsonError) {
|
|
9231
|
+
const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
|
|
9232
|
+
const timedOut = isRequestTimeoutError(cause);
|
|
9233
|
+
return err(
|
|
9234
|
+
serviceError(
|
|
9235
|
+
timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
|
|
9236
|
+
timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
|
|
9237
|
+
"kv",
|
|
9238
|
+
{
|
|
9239
|
+
cause,
|
|
9240
|
+
meta: {
|
|
9241
|
+
...timedOut ? { timeoutMs: cause.timeoutMs } : {},
|
|
9242
|
+
requestMayHaveDispatched: true,
|
|
9243
|
+
responseReceived: true,
|
|
9244
|
+
status: response.status,
|
|
9245
|
+
outcome: "batch-unconfirmed"
|
|
9172
9246
|
}
|
|
9173
|
-
|
|
9174
|
-
)
|
|
9175
|
-
|
|
9176
|
-
return ok(batchResponse);
|
|
9177
|
-
} catch (error) {
|
|
9178
|
-
const wrapped = wrapError("kv", error);
|
|
9179
|
-
return err({
|
|
9180
|
-
...wrapped,
|
|
9181
|
-
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
9182
|
-
});
|
|
9183
|
-
} finally {
|
|
9184
|
-
request.dispose();
|
|
9247
|
+
}
|
|
9248
|
+
)
|
|
9249
|
+
);
|
|
9185
9250
|
}
|
|
9186
|
-
|
|
9251
|
+
const batchResponse = this.normalizeBatchPutResponse(rawBody);
|
|
9252
|
+
if (!batchResponse) {
|
|
9253
|
+
return err(
|
|
9254
|
+
serviceError(
|
|
9255
|
+
ErrorCodes.NETWORK_ERROR,
|
|
9256
|
+
"KV batchPut response did not include matching written keys and count",
|
|
9257
|
+
"kv",
|
|
9258
|
+
{
|
|
9259
|
+
meta: {
|
|
9260
|
+
requestMayHaveDispatched: true,
|
|
9261
|
+
responseReceived: true,
|
|
9262
|
+
status: response.status,
|
|
9263
|
+
outcome: "batch-unconfirmed"
|
|
9264
|
+
}
|
|
9265
|
+
}
|
|
9266
|
+
)
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
const requestedPaths = new Set(paths);
|
|
9270
|
+
const writtenPaths = new Set(batchResponse.written);
|
|
9271
|
+
const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
|
|
9272
|
+
if (!matchesRequest) {
|
|
9273
|
+
return err(
|
|
9274
|
+
serviceError(
|
|
9275
|
+
ErrorCodes.NETWORK_ERROR,
|
|
9276
|
+
`KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
|
|
9277
|
+
"kv",
|
|
9278
|
+
{
|
|
9279
|
+
meta: {
|
|
9280
|
+
requestMayHaveDispatched: true,
|
|
9281
|
+
responseReceived: true,
|
|
9282
|
+
status: response.status,
|
|
9283
|
+
outcome: "batch-unconfirmed"
|
|
9284
|
+
}
|
|
9285
|
+
}
|
|
9286
|
+
)
|
|
9287
|
+
);
|
|
9288
|
+
}
|
|
9289
|
+
return ok(batchResponse);
|
|
9290
|
+
} catch (error) {
|
|
9291
|
+
const wrapped = wrapError("kv", error);
|
|
9292
|
+
return err({
|
|
9293
|
+
...wrapped,
|
|
9294
|
+
meta: { ...wrapped.meta, requestMayHaveDispatched }
|
|
9295
|
+
});
|
|
9296
|
+
}
|
|
9187
9297
|
}
|
|
9188
9298
|
/**
|
|
9189
9299
|
* List keys with optional prefix filtering.
|
|
9190
9300
|
*/
|
|
9191
9301
|
async list(options) {
|
|
9192
9302
|
return this.withTelemetry("list", options?.prefix, async () => {
|
|
9303
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9193
9304
|
if (!this.requireAuth()) {
|
|
9194
9305
|
return err(authRequiredError("kv"));
|
|
9195
9306
|
}
|
|
@@ -9206,49 +9317,44 @@ var KVService = class extends BaseService {
|
|
|
9206
9317
|
}
|
|
9207
9318
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9208
9319
|
try {
|
|
9209
|
-
const
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9320
|
+
const network = () => this.listFromNetwork(listPath, options, request.signal);
|
|
9321
|
+
let page;
|
|
9322
|
+
if (options?.source === "network") {
|
|
9323
|
+
const observer = this.readThrough;
|
|
9324
|
+
if (observer === null) {
|
|
9325
|
+
page = await network();
|
|
9326
|
+
} else {
|
|
9327
|
+
let outcome = "error";
|
|
9328
|
+
const startedAt = Date.now();
|
|
9329
|
+
try {
|
|
9330
|
+
page = await network();
|
|
9331
|
+
outcome = page.ok ? "found" : "error";
|
|
9332
|
+
} finally {
|
|
9333
|
+
try {
|
|
9334
|
+
observer.observeNetworkRequested({
|
|
9335
|
+
op: "list",
|
|
9336
|
+
space: this.context.session.spaceId,
|
|
9337
|
+
path: listPath,
|
|
9338
|
+
reason: "NETWORK_REQUESTED",
|
|
9339
|
+
outcome,
|
|
9340
|
+
latencyMs: Date.now() - startedAt
|
|
9341
|
+
});
|
|
9342
|
+
} catch {
|
|
9343
|
+
}
|
|
9344
|
+
}
|
|
9228
9345
|
}
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
let keys = await this.parseResponse(response, options?.raw);
|
|
9240
|
-
keys = keys ?? [];
|
|
9241
|
-
if (options?.removePrefix && listPath) {
|
|
9242
|
-
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
9243
|
-
keys = keys.map(
|
|
9244
|
-
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
9245
|
-
);
|
|
9346
|
+
} else if (this.readThrough === null) {
|
|
9347
|
+
page = await network();
|
|
9348
|
+
} else {
|
|
9349
|
+
page = await this.readThrough.list({
|
|
9350
|
+
space: this.context.session.spaceId,
|
|
9351
|
+
listPath,
|
|
9352
|
+
options,
|
|
9353
|
+
signal: request.signal,
|
|
9354
|
+
network
|
|
9355
|
+
});
|
|
9246
9356
|
}
|
|
9247
|
-
return ok(
|
|
9248
|
-
keys,
|
|
9249
|
-
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
9250
|
-
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
9251
|
-
});
|
|
9357
|
+
return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
|
|
9252
9358
|
} catch (error) {
|
|
9253
9359
|
return err(wrapError("kv", error));
|
|
9254
9360
|
} finally {
|
|
@@ -9256,6 +9362,56 @@ var KVService = class extends BaseService {
|
|
|
9256
9362
|
}
|
|
9257
9363
|
});
|
|
9258
9364
|
}
|
|
9365
|
+
async listFromNetwork(listPath, options, signal) {
|
|
9366
|
+
const response = await this.invokeOperation(
|
|
9367
|
+
listPath,
|
|
9368
|
+
KVAction.LIST,
|
|
9369
|
+
void 0,
|
|
9370
|
+
signal,
|
|
9371
|
+
options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
|
|
9372
|
+
...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
|
|
9373
|
+
...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
|
|
9374
|
+
}
|
|
9375
|
+
);
|
|
9376
|
+
if (!response.ok) {
|
|
9377
|
+
if (response.status === 401 || response.status === 403) {
|
|
9378
|
+
return this.authorizationFailure(
|
|
9379
|
+
"Failed to list keys",
|
|
9380
|
+
response,
|
|
9381
|
+
await this.readAuthorizationText(response),
|
|
9382
|
+
[listPath],
|
|
9383
|
+
KVAction.LIST
|
|
9384
|
+
);
|
|
9385
|
+
}
|
|
9386
|
+
const errorText = await response.text();
|
|
9387
|
+
return err(serviceError(
|
|
9388
|
+
ErrorCodes.NETWORK_ERROR,
|
|
9389
|
+
`Failed to list keys: ${response.status} - ${errorText}`,
|
|
9390
|
+
"kv",
|
|
9391
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9392
|
+
));
|
|
9393
|
+
}
|
|
9394
|
+
const keys = await this.parseResponse(response, options?.raw) ?? [];
|
|
9395
|
+
return ok({
|
|
9396
|
+
keys,
|
|
9397
|
+
...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
|
|
9398
|
+
...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
|
|
9399
|
+
});
|
|
9400
|
+
}
|
|
9401
|
+
finishList(page, listPath, removePrefix) {
|
|
9402
|
+
let keys = page.keys;
|
|
9403
|
+
if (removePrefix && listPath) {
|
|
9404
|
+
const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
|
|
9405
|
+
keys = keys.map(
|
|
9406
|
+
(key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
|
|
9407
|
+
);
|
|
9408
|
+
}
|
|
9409
|
+
return ok({
|
|
9410
|
+
keys,
|
|
9411
|
+
...page.truncated === void 0 ? {} : { truncated: page.truncated },
|
|
9412
|
+
...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
|
|
9413
|
+
});
|
|
9414
|
+
}
|
|
9259
9415
|
/**
|
|
9260
9416
|
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
9261
9417
|
*/
|
|
@@ -9424,61 +9580,22 @@ var KVService = class extends BaseService {
|
|
|
9424
9580
|
*/
|
|
9425
9581
|
async delete(key, options) {
|
|
9426
9582
|
return this.withTelemetry("delete", key, async () => {
|
|
9583
|
+
if (this.readThroughReady !== null) await this.readThroughReady;
|
|
9427
9584
|
if (!this.requireAuth()) {
|
|
9428
9585
|
return err(authRequiredError("kv"));
|
|
9429
9586
|
}
|
|
9430
9587
|
const path = this.getFullPath(key, options?.prefix);
|
|
9431
9588
|
const request = this.createRequestSignal(options?.signal, options?.timeout);
|
|
9432
9589
|
try {
|
|
9433
|
-
const
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
return this.authorizationFailure(
|
|
9443
|
-
`Failed to delete key ${JSON.stringify(key)}`,
|
|
9444
|
-
response,
|
|
9445
|
-
await this.readAuthorizationText(response),
|
|
9446
|
-
[path],
|
|
9447
|
-
KVAction.DELETE
|
|
9448
|
-
);
|
|
9449
|
-
}
|
|
9450
|
-
if (response.status === 404) {
|
|
9451
|
-
return this.classifyNotFound(response, key);
|
|
9452
|
-
}
|
|
9453
|
-
const errorText = await response.text();
|
|
9454
|
-
if (response.status === 412) {
|
|
9455
|
-
return err(serviceError(
|
|
9456
|
-
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
9457
|
-
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
9458
|
-
"kv",
|
|
9459
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9460
|
-
));
|
|
9461
|
-
}
|
|
9462
|
-
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
9463
|
-
return err(serviceError(
|
|
9464
|
-
ErrorCodes.KV_CONFLICT,
|
|
9465
|
-
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
9466
|
-
"kv",
|
|
9467
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9468
|
-
));
|
|
9469
|
-
}
|
|
9470
|
-
return err(
|
|
9471
|
-
serviceError(
|
|
9472
|
-
ErrorCodes.NETWORK_ERROR,
|
|
9473
|
-
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9474
|
-
"kv",
|
|
9475
|
-
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9476
|
-
)
|
|
9477
|
-
);
|
|
9478
|
-
}
|
|
9479
|
-
return ok({
|
|
9480
|
-
data: void 0,
|
|
9481
|
-
headers: this.createResponseHeaders(response.headers)
|
|
9590
|
+
const network = () => this.deleteFromNetwork(key, path, options, request.signal);
|
|
9591
|
+
if (this.readThrough === null) return await network();
|
|
9592
|
+
return await this.readThrough.write({
|
|
9593
|
+
op: "delete",
|
|
9594
|
+
space: this.context.session.spaceId,
|
|
9595
|
+
entries: [{ path }],
|
|
9596
|
+
signal: request.signal,
|
|
9597
|
+
conditional: options?.ifMatch !== void 0,
|
|
9598
|
+
network
|
|
9482
9599
|
});
|
|
9483
9600
|
} catch (error) {
|
|
9484
9601
|
return err(wrapError("kv", error));
|
|
@@ -9487,6 +9604,54 @@ var KVService = class extends BaseService {
|
|
|
9487
9604
|
}
|
|
9488
9605
|
});
|
|
9489
9606
|
}
|
|
9607
|
+
async deleteFromNetwork(key, path, options, signal) {
|
|
9608
|
+
const response = await this.invokeOperation(
|
|
9609
|
+
path,
|
|
9610
|
+
KVAction.DELETE,
|
|
9611
|
+
void 0,
|
|
9612
|
+
signal,
|
|
9613
|
+
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
9614
|
+
);
|
|
9615
|
+
if (!response.ok) {
|
|
9616
|
+
if (response.status === 401 || response.status === 403) {
|
|
9617
|
+
return this.authorizationFailure(
|
|
9618
|
+
`Failed to delete key ${JSON.stringify(key)}`,
|
|
9619
|
+
response,
|
|
9620
|
+
await this.readAuthorizationText(response),
|
|
9621
|
+
[path],
|
|
9622
|
+
KVAction.DELETE
|
|
9623
|
+
);
|
|
9624
|
+
}
|
|
9625
|
+
if (response.status === 404) return this.classifyNotFound(response, key);
|
|
9626
|
+
const errorText = await response.text();
|
|
9627
|
+
if (response.status === 412) {
|
|
9628
|
+
return err(serviceError(
|
|
9629
|
+
ErrorCodes.KV_PRECONDITION_FAILED,
|
|
9630
|
+
`KV precondition failed for key ${JSON.stringify(key)}`,
|
|
9631
|
+
"kv",
|
|
9632
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9633
|
+
));
|
|
9634
|
+
}
|
|
9635
|
+
if (response.status === 503 && options?.ifMatch !== void 0) {
|
|
9636
|
+
return err(serviceError(
|
|
9637
|
+
ErrorCodes.KV_CONFLICT,
|
|
9638
|
+
`Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
|
|
9639
|
+
"kv",
|
|
9640
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9641
|
+
));
|
|
9642
|
+
}
|
|
9643
|
+
return err(serviceError(
|
|
9644
|
+
ErrorCodes.NETWORK_ERROR,
|
|
9645
|
+
`Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
|
|
9646
|
+
"kv",
|
|
9647
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
9648
|
+
));
|
|
9649
|
+
}
|
|
9650
|
+
return ok({
|
|
9651
|
+
data: void 0,
|
|
9652
|
+
headers: this.createResponseHeaders(response.headers)
|
|
9653
|
+
});
|
|
9654
|
+
}
|
|
9490
9655
|
/**
|
|
9491
9656
|
* Get metadata for a key without retrieving the value.
|
|
9492
9657
|
*/
|
|
@@ -9515,7 +9680,7 @@ var KVService = class extends BaseService {
|
|
|
9515
9680
|
);
|
|
9516
9681
|
}
|
|
9517
9682
|
if (response.status === 404) {
|
|
9518
|
-
return this.classifyNotFound(response, key);
|
|
9683
|
+
return await this.classifyNotFound(response, key);
|
|
9519
9684
|
}
|
|
9520
9685
|
const errorText = await response.text();
|
|
9521
9686
|
return err(
|
|
@@ -9581,7 +9746,7 @@ var KVService = class extends BaseService {
|
|
|
9581
9746
|
signal: request.signal
|
|
9582
9747
|
});
|
|
9583
9748
|
if (!response.ok) {
|
|
9584
|
-
return this.createSignedReadUrlError(response, key, path);
|
|
9749
|
+
return await this.createSignedReadUrlError(response, key, path);
|
|
9585
9750
|
}
|
|
9586
9751
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
9587
9752
|
await response.json()
|
|
@@ -9816,7 +9981,7 @@ var SQLService = class extends BaseService {
|
|
|
9816
9981
|
request.signal
|
|
9817
9982
|
);
|
|
9818
9983
|
if (!response.ok) {
|
|
9819
|
-
return this.handleErrorResponse(response, "query", dbName, [action]);
|
|
9984
|
+
return await this.handleErrorResponse(response, "query", dbName, [action]);
|
|
9820
9985
|
}
|
|
9821
9986
|
const data = await response.json();
|
|
9822
9987
|
return ok(data);
|
|
@@ -9856,7 +10021,7 @@ var SQLService = class extends BaseService {
|
|
|
9856
10021
|
request.signal
|
|
9857
10022
|
);
|
|
9858
10023
|
if (!response.ok) {
|
|
9859
|
-
return this.handleErrorResponse(response, "execute", dbName, requestedActions);
|
|
10024
|
+
return await this.handleErrorResponse(response, "execute", dbName, requestedActions);
|
|
9860
10025
|
}
|
|
9861
10026
|
const data = await response.json();
|
|
9862
10027
|
return ok(data);
|
|
@@ -9882,7 +10047,7 @@ var SQLService = class extends BaseService {
|
|
|
9882
10047
|
request.signal
|
|
9883
10048
|
);
|
|
9884
10049
|
if (!response.ok) {
|
|
9885
|
-
return this.handleErrorResponse(response, "batch", dbName, requestedActions);
|
|
10050
|
+
return await this.handleErrorResponse(response, "batch", dbName, requestedActions);
|
|
9886
10051
|
}
|
|
9887
10052
|
const data = await response.json();
|
|
9888
10053
|
return ok(data);
|
|
@@ -9907,7 +10072,7 @@ var SQLService = class extends BaseService {
|
|
|
9907
10072
|
request.signal
|
|
9908
10073
|
);
|
|
9909
10074
|
if (!response.ok) {
|
|
9910
|
-
return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
10075
|
+
return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
|
|
9911
10076
|
}
|
|
9912
10077
|
const data = await response.json();
|
|
9913
10078
|
return ok(data);
|
|
@@ -9932,7 +10097,7 @@ var SQLService = class extends BaseService {
|
|
|
9932
10097
|
request.signal
|
|
9933
10098
|
);
|
|
9934
10099
|
if (!response.ok) {
|
|
9935
|
-
return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
10100
|
+
return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
|
|
9936
10101
|
}
|
|
9937
10102
|
const resp = response;
|
|
9938
10103
|
if (typeof resp.blob === "function") {
|
|
@@ -10301,7 +10466,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10301
10466
|
request.signal
|
|
10302
10467
|
);
|
|
10303
10468
|
if (!response.ok) {
|
|
10304
|
-
return this.handleErrorResponse(response, "query");
|
|
10469
|
+
return await this.handleErrorResponse(response, "query");
|
|
10305
10470
|
}
|
|
10306
10471
|
const data = await response.json();
|
|
10307
10472
|
return ok(data);
|
|
@@ -10327,7 +10492,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10327
10492
|
{ Accept: "application/vnd.apache.arrow.stream" }
|
|
10328
10493
|
);
|
|
10329
10494
|
if (!response.ok) {
|
|
10330
|
-
return this.handleErrorResponse(response, "queryArrow");
|
|
10495
|
+
return await this.handleErrorResponse(response, "queryArrow");
|
|
10331
10496
|
}
|
|
10332
10497
|
const buffer = await response.arrayBuffer();
|
|
10333
10498
|
return ok(buffer);
|
|
@@ -10360,7 +10525,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10360
10525
|
request.signal
|
|
10361
10526
|
);
|
|
10362
10527
|
if (!response.ok) {
|
|
10363
|
-
return this.handleErrorResponse(response, "execute");
|
|
10528
|
+
return await this.handleErrorResponse(response, "execute");
|
|
10364
10529
|
}
|
|
10365
10530
|
const data = await response.json();
|
|
10366
10531
|
return ok(data);
|
|
@@ -10392,7 +10557,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10392
10557
|
request.signal
|
|
10393
10558
|
);
|
|
10394
10559
|
if (!response.ok) {
|
|
10395
|
-
return this.handleErrorResponse(response, "batch");
|
|
10560
|
+
return await this.handleErrorResponse(response, "batch");
|
|
10396
10561
|
}
|
|
10397
10562
|
const data = await response.json();
|
|
10398
10563
|
return ok(data);
|
|
@@ -10417,7 +10582,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10417
10582
|
request.signal
|
|
10418
10583
|
);
|
|
10419
10584
|
if (!response.ok) {
|
|
10420
|
-
return this.handleErrorResponse(response, "executeStatement");
|
|
10585
|
+
return await this.handleErrorResponse(response, "executeStatement");
|
|
10421
10586
|
}
|
|
10422
10587
|
const data = await response.json();
|
|
10423
10588
|
return ok(data);
|
|
@@ -10442,7 +10607,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10442
10607
|
request.signal
|
|
10443
10608
|
);
|
|
10444
10609
|
if (!response.ok) {
|
|
10445
|
-
return this.handleErrorResponse(response, "describe");
|
|
10610
|
+
return await this.handleErrorResponse(response, "describe");
|
|
10446
10611
|
}
|
|
10447
10612
|
const data = await response.json();
|
|
10448
10613
|
return ok(data);
|
|
@@ -10467,7 +10632,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10467
10632
|
request.signal
|
|
10468
10633
|
);
|
|
10469
10634
|
if (!response.ok) {
|
|
10470
|
-
return this.handleErrorResponse(response, "export");
|
|
10635
|
+
return await this.handleErrorResponse(response, "export");
|
|
10471
10636
|
}
|
|
10472
10637
|
const blob = await response.blob();
|
|
10473
10638
|
return ok(blob);
|
|
@@ -10502,7 +10667,7 @@ var DuckDbService = class extends BaseService {
|
|
|
10502
10667
|
signal: request.signal
|
|
10503
10668
|
});
|
|
10504
10669
|
if (!response.ok) {
|
|
10505
|
-
return this.handleErrorResponse(response, "import");
|
|
10670
|
+
return await this.handleErrorResponse(response, "import");
|
|
10506
10671
|
}
|
|
10507
10672
|
return ok(void 0);
|
|
10508
10673
|
} catch (error) {
|
|
@@ -11804,6 +11969,12 @@ function defaultVaultMessage(input) {
|
|
|
11804
11969
|
return input.message ?? `Public key not found for ${input.did}`;
|
|
11805
11970
|
case "STORAGE_ERROR":
|
|
11806
11971
|
return input.message ?? input.cause.message;
|
|
11972
|
+
case "ALREADY_EXISTS":
|
|
11973
|
+
return input.message ?? `A value already exists at ${input.key}`;
|
|
11974
|
+
case "AUTH_UNAUTHORIZED":
|
|
11975
|
+
return input.message;
|
|
11976
|
+
case "PERMISSION_DENIED":
|
|
11977
|
+
return input.message;
|
|
11807
11978
|
case "STORAGE_QUOTA_EXCEEDED":
|
|
11808
11979
|
case "STORAGE_LIMIT_REACHED":
|
|
11809
11980
|
return input.message;
|
|
@@ -11817,13 +11988,33 @@ function vaultError(input) {
|
|
|
11817
11988
|
};
|
|
11818
11989
|
return { ok: false, error };
|
|
11819
11990
|
}
|
|
11991
|
+
function typedKvCause(error) {
|
|
11992
|
+
return Object.assign(new Error(error.message), {
|
|
11993
|
+
name: "ServiceError",
|
|
11994
|
+
code: error.code,
|
|
11995
|
+
service: error.service,
|
|
11996
|
+
...error.meta !== void 0 ? { meta: error.meta } : {},
|
|
11997
|
+
...error.cause !== void 0 ? { cause: error.cause } : {}
|
|
11998
|
+
});
|
|
11999
|
+
}
|
|
11820
12000
|
function kvWriteError(context, error) {
|
|
11821
12001
|
if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
|
|
11822
12002
|
return vaultError({ code: error.code, message: error.message, meta: error.meta });
|
|
11823
12003
|
}
|
|
12004
|
+
const message = `${context}: ${error.message}`;
|
|
12005
|
+
if (error.code === ErrorCodes.AUTH_UNAUTHORIZED) {
|
|
12006
|
+
const code = authorizationVerdictOf(error) === "unauthenticated" ? "AUTH_UNAUTHORIZED" : "PERMISSION_DENIED";
|
|
12007
|
+
return vaultError({
|
|
12008
|
+
code,
|
|
12009
|
+
message,
|
|
12010
|
+
...error.meta !== void 0 ? { meta: error.meta } : {},
|
|
12011
|
+
cause: typedKvCause(error)
|
|
12012
|
+
});
|
|
12013
|
+
}
|
|
11824
12014
|
return vaultError({
|
|
11825
12015
|
code: "STORAGE_ERROR",
|
|
11826
|
-
cause: new Error(
|
|
12016
|
+
cause: new Error(message, { cause: typedKvCause(error) }),
|
|
12017
|
+
...error.meta !== void 0 ? { meta: error.meta } : {}
|
|
11827
12018
|
});
|
|
11828
12019
|
}
|
|
11829
12020
|
var DataVaultService = class extends BaseService {
|
|
@@ -12103,11 +12294,18 @@ var DataVaultService = class extends BaseService {
|
|
|
12103
12294
|
cause: new Error(envelopeResult.error.message)
|
|
12104
12295
|
});
|
|
12105
12296
|
}
|
|
12106
|
-
const
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
);
|
|
12297
|
+
const valuePath = `vault/${key}`;
|
|
12298
|
+
const valueBody = JSON.stringify(envelopeResult.data);
|
|
12299
|
+
const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
|
|
12110
12300
|
if (!valuePutResult.ok) {
|
|
12301
|
+
if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
|
|
12302
|
+
return vaultError({
|
|
12303
|
+
code: "ALREADY_EXISTS",
|
|
12304
|
+
key,
|
|
12305
|
+
...valuePutResult.error.meta !== void 0 ? { meta: valuePutResult.error.meta } : {},
|
|
12306
|
+
cause: typedKvCause(valuePutResult.error)
|
|
12307
|
+
});
|
|
12308
|
+
}
|
|
12111
12309
|
return kvWriteError("Failed to store encrypted value", valuePutResult.error);
|
|
12112
12310
|
}
|
|
12113
12311
|
return { ok: true, data: void 0 };
|
|
@@ -12134,7 +12332,8 @@ var DataVaultService = class extends BaseService {
|
|
|
12134
12332
|
}
|
|
12135
12333
|
try {
|
|
12136
12334
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
12137
|
-
raw: true
|
|
12335
|
+
raw: true,
|
|
12336
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
12138
12337
|
});
|
|
12139
12338
|
if (!valueResult.ok) {
|
|
12140
12339
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12179,7 +12378,7 @@ var DataVaultService = class extends BaseService {
|
|
|
12179
12378
|
}
|
|
12180
12379
|
let valueResult;
|
|
12181
12380
|
try {
|
|
12182
|
-
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
|
|
12381
|
+
valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
|
|
12183
12382
|
} catch {
|
|
12184
12383
|
return { status: "node_unreachable" };
|
|
12185
12384
|
}
|
|
@@ -12248,7 +12447,8 @@ var DataVaultService = class extends BaseService {
|
|
|
12248
12447
|
}
|
|
12249
12448
|
try {
|
|
12250
12449
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
12251
|
-
raw: true
|
|
12450
|
+
raw: true,
|
|
12451
|
+
source: "network"
|
|
12252
12452
|
});
|
|
12253
12453
|
if (!valueResult.ok) {
|
|
12254
12454
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12275,6 +12475,14 @@ var DataVaultService = class extends BaseService {
|
|
|
12275
12475
|
if (this.usesNetworkEncryption) {
|
|
12276
12476
|
return this.putNetworkEncrypted(key, value, options);
|
|
12277
12477
|
}
|
|
12478
|
+
if (options?.ifNoneMatch !== void 0) {
|
|
12479
|
+
return vaultError({
|
|
12480
|
+
code: "STORAGE_ERROR",
|
|
12481
|
+
cause: new Error(
|
|
12482
|
+
"ifNoneMatch is only supported by network-encrypted vaults"
|
|
12483
|
+
)
|
|
12484
|
+
});
|
|
12485
|
+
}
|
|
12278
12486
|
if (!this._isUnlocked || !this.masterKey) {
|
|
12279
12487
|
return vaultError({
|
|
12280
12488
|
code: "VAULT_LOCKED",
|
|
@@ -12374,7 +12582,8 @@ var DataVaultService = class extends BaseService {
|
|
|
12374
12582
|
}
|
|
12375
12583
|
try {
|
|
12376
12584
|
const keyResult = await this.tc.kv.get(`keys/${key}`, {
|
|
12377
|
-
raw: true
|
|
12585
|
+
raw: true,
|
|
12586
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
12378
12587
|
});
|
|
12379
12588
|
if (!keyResult.ok) {
|
|
12380
12589
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12383,7 +12592,8 @@ var DataVaultService = class extends BaseService {
|
|
|
12383
12592
|
const keyBlobBytes = base64Decode(keyEnvelope.key);
|
|
12384
12593
|
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
12385
12594
|
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
12386
|
-
raw: true
|
|
12595
|
+
raw: true,
|
|
12596
|
+
...options?.source === void 0 ? {} : { source: options.source }
|
|
12387
12597
|
});
|
|
12388
12598
|
if (!valueResult.ok) {
|
|
12389
12599
|
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
@@ -12493,6 +12703,7 @@ var DataVaultService = class extends BaseService {
|
|
|
12493
12703
|
prefix: listPrefix,
|
|
12494
12704
|
removePrefix: true,
|
|
12495
12705
|
...options?.limit === void 0 ? {} : { limit: options.limit },
|
|
12706
|
+
...options?.source === void 0 ? {} : { source: options.source },
|
|
12496
12707
|
...options?.cursor === void 0 ? {} : { cursor: options.cursor }
|
|
12497
12708
|
});
|
|
12498
12709
|
if (!listResult.ok) {
|
|
@@ -13064,6 +13275,11 @@ var DataVaultService = class extends BaseService {
|
|
|
13064
13275
|
}
|
|
13065
13276
|
};
|
|
13066
13277
|
DataVaultService.serviceName = "vault";
|
|
13278
|
+
var TC832_RESERVED_SCOPES = Object.freeze([
|
|
13279
|
+
"default",
|
|
13280
|
+
"global",
|
|
13281
|
+
"scoped"
|
|
13282
|
+
]);
|
|
13067
13283
|
function canonicalize(value) {
|
|
13068
13284
|
if (value === void 0) {
|
|
13069
13285
|
return "";
|
|
@@ -14383,6 +14599,11 @@ function kvPrefixCovers(prefix, key) {
|
|
|
14383
14599
|
if (prefix.endsWith("/")) return key.startsWith(prefix);
|
|
14384
14600
|
return key === prefix || key.startsWith(`${prefix}/`);
|
|
14385
14601
|
}
|
|
14602
|
+
function authorityPathCovers(grantPath, requestedPath) {
|
|
14603
|
+
if (grantPath === "" || grantPath === "/") return true;
|
|
14604
|
+
if (grantPath.endsWith("/")) return requestedPath.startsWith(grantPath);
|
|
14605
|
+
return requestedPath === grantPath;
|
|
14606
|
+
}
|
|
14386
14607
|
function requiresSecretsOptIn(space, prefix) {
|
|
14387
14608
|
if (space === "secrets" || space.endsWith(":secrets")) return true;
|
|
14388
14609
|
const firstSegment = prefix.split("/", 1)[0];
|
|
@@ -16496,7 +16717,7 @@ function kvPathIn(resource, space) {
|
|
|
16496
16717
|
function grantCovers(grant, space, prefix, ability) {
|
|
16497
16718
|
for (const [resource, abilities] of Object.entries(grant.att)) {
|
|
16498
16719
|
const path = kvPathIn(resource, space);
|
|
16499
|
-
if (path === void 0 || !
|
|
16720
|
+
if (path === void 0 || !authorityPathCovers(path, prefix)) continue;
|
|
16500
16721
|
if (ability in abilities) return true;
|
|
16501
16722
|
if (ability !== "tinycloud.kv/sync" && ability !== "tinycloud.kv/retain" && "tinycloud.kv/*" in abilities) return true;
|
|
16502
16723
|
}
|
|
@@ -16509,8 +16730,9 @@ function assertGrantInstallable(grant, input) {
|
|
|
16509
16730
|
`The grant was issued to ${grant.audience}, not this device (${input.deviceDid}).`
|
|
16510
16731
|
);
|
|
16511
16732
|
}
|
|
16733
|
+
const requiredPaths = input.prefix.endsWith("/") ? [input.prefix] : [input.prefix, `${input.prefix}/`];
|
|
16512
16734
|
for (const ability of ["tinycloud.kv/sync", "tinycloud.kv/get"]) {
|
|
16513
|
-
if (!grantCovers(grant, input.space,
|
|
16735
|
+
if (!requiredPaths.every((path) => grantCovers(grant, input.space, path, ability))) {
|
|
16514
16736
|
throw new ReplicaError(
|
|
16515
16737
|
ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
16516
16738
|
`The grant does not carry ${ability} on ${input.space}/kv/${input.prefix}.`,
|