@tinycloud/sdk-services 3.1.0-beta.2 → 3.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/encryption/index.cjs +106 -97
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.js +106 -97
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +269 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +249 -129
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +210 -20
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +5 -1
- package/dist/kv/index.d.ts +5 -1
- package/dist/kv/index.js +210 -20
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +33 -21
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.js +24 -12
- package/dist/sql/index.js.map +1 -1
- package/package.json +1 -1
package/dist/kv/index.d.cts
CHANGED
|
@@ -918,12 +918,16 @@ declare class KVService extends BaseService implements IKVService {
|
|
|
918
918
|
*/
|
|
919
919
|
private parseResponse;
|
|
920
920
|
private createSignedReadUrlError;
|
|
921
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
922
|
+
private readAuthorizationText;
|
|
921
923
|
/**
|
|
922
924
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
923
925
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
924
926
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
925
927
|
* still see the status; `meta.status` carries it for typed callers.
|
|
926
|
-
*
|
|
928
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
929
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
930
|
+
* canonical resource under the authenticated session's space.
|
|
927
931
|
*/
|
|
928
932
|
private authorizationFailure;
|
|
929
933
|
private normalizeSignedReadUrlResponse;
|
package/dist/kv/index.d.ts
CHANGED
|
@@ -918,12 +918,16 @@ declare class KVService extends BaseService implements IKVService {
|
|
|
918
918
|
*/
|
|
919
919
|
private parseResponse;
|
|
920
920
|
private createSignedReadUrlError;
|
|
921
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
922
|
+
private readAuthorizationText;
|
|
921
923
|
/**
|
|
922
924
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
923
925
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
924
926
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
925
927
|
* still see the status; `meta.status` carries it for typed callers.
|
|
926
|
-
*
|
|
928
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
929
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
930
|
+
* canonical resource under the authenticated session's space.
|
|
927
931
|
*/
|
|
928
932
|
private authorizationFailure;
|
|
929
933
|
private normalizeSignedReadUrlResponse;
|
package/dist/kv/index.js
CHANGED
|
@@ -70,6 +70,55 @@ function serviceError(code, message, service, options) {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// src/errors.ts
|
|
74
|
+
import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
|
|
75
|
+
|
|
76
|
+
// src/encryption/networkId.ts
|
|
77
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
78
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
79
|
+
var NetworkIdError = class extends Error {
|
|
80
|
+
constructor(message) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "NetworkIdError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function parseNetworkId(networkId) {
|
|
86
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
87
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
88
|
+
}
|
|
89
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
90
|
+
throw new NetworkIdError(
|
|
91
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
95
|
+
const lastColon = body.lastIndexOf(":");
|
|
96
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
97
|
+
throw new NetworkIdError(
|
|
98
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const ownerDid = body.slice(0, lastColon);
|
|
102
|
+
const name = body.slice(lastColon + 1);
|
|
103
|
+
if (!ownerDid.startsWith("did:")) {
|
|
104
|
+
throw new NetworkIdError(
|
|
105
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const didParts = ownerDid.split(":");
|
|
109
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
110
|
+
throw new NetworkIdError(
|
|
111
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
115
|
+
throw new NetworkIdError(
|
|
116
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return { networkId, ownerDid, name };
|
|
120
|
+
}
|
|
121
|
+
|
|
73
122
|
// src/errors.ts
|
|
74
123
|
function parsePermissionHint(value) {
|
|
75
124
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -138,6 +187,101 @@ function parseAuthError(responseText) {
|
|
|
138
187
|
function authUnauthorizedError(service, message, meta) {
|
|
139
188
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
140
189
|
}
|
|
190
|
+
var grantableActions = new Set(
|
|
191
|
+
CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
192
|
+
);
|
|
193
|
+
var capabilityServices = new Set(
|
|
194
|
+
CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
195
|
+
);
|
|
196
|
+
function parseCapabilityResource(resource, service) {
|
|
197
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
198
|
+
const segments = resource.split("/");
|
|
199
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
200
|
+
const serviceIndex = segments.findIndex(
|
|
201
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
202
|
+
);
|
|
203
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
204
|
+
if (segments.slice(1, serviceIndex).some(
|
|
205
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
206
|
+
)) return void 0;
|
|
207
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
208
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
209
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
210
|
+
return {
|
|
211
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
212
|
+
path: pathSegments.join("/")
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function safeCapabilityResource(value, service, action) {
|
|
216
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
217
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
218
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
219
|
+
try {
|
|
220
|
+
parseNetworkId(value);
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const parsed = parseCapabilityResource(value, service);
|
|
227
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
228
|
+
}
|
|
229
|
+
function validatedCapabilityOf(error) {
|
|
230
|
+
const verdict = authorizationVerdictOf(error);
|
|
231
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
233
|
+
let node = error;
|
|
234
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
235
|
+
seen.add(node);
|
|
236
|
+
const record = node;
|
|
237
|
+
const status = errorStatusOf(record);
|
|
238
|
+
const meta = record.meta;
|
|
239
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
240
|
+
const metadata = meta;
|
|
241
|
+
const { resource, requiredAction } = metadata;
|
|
242
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
243
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
244
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
245
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
246
|
+
return { resource, requiredAction };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
node = record.cause;
|
|
251
|
+
}
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
255
|
+
function errorStatusOf(node) {
|
|
256
|
+
const meta = node.meta;
|
|
257
|
+
const candidates = [
|
|
258
|
+
node.status,
|
|
259
|
+
node.statusCode,
|
|
260
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
261
|
+
];
|
|
262
|
+
for (const candidate of candidates) {
|
|
263
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
264
|
+
return candidate;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
function authorizationVerdictOf(error) {
|
|
270
|
+
const seen = /* @__PURE__ */ new Set();
|
|
271
|
+
let sawUnauthorizedCode = false;
|
|
272
|
+
let node = error;
|
|
273
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
274
|
+
seen.add(node);
|
|
275
|
+
const record = node;
|
|
276
|
+
const status = errorStatusOf(record);
|
|
277
|
+
if (status === 401) return "unauthenticated";
|
|
278
|
+
if (status === 403) return "forbidden";
|
|
279
|
+
if (status !== void 0) return "other";
|
|
280
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
281
|
+
node = record.cause;
|
|
282
|
+
}
|
|
283
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
284
|
+
}
|
|
141
285
|
function storageQuotaExceededError(service, message, meta) {
|
|
142
286
|
return {
|
|
143
287
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -848,12 +992,14 @@ var KVService = class extends BaseService {
|
|
|
848
992
|
signal: this.combineSignals(options?.signal)
|
|
849
993
|
});
|
|
850
994
|
if (!response.ok) {
|
|
851
|
-
const errorText = await response.text();
|
|
995
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
852
996
|
if (response.status === 401 || response.status === 403) {
|
|
853
997
|
return this.authorizationFailure(
|
|
854
998
|
`Failed to batch read ${keys.length} key(s)`,
|
|
855
999
|
response,
|
|
856
|
-
errorText
|
|
1000
|
+
errorText,
|
|
1001
|
+
paths,
|
|
1002
|
+
action
|
|
857
1003
|
);
|
|
858
1004
|
}
|
|
859
1005
|
if (response.status === 413) {
|
|
@@ -943,7 +1089,7 @@ var KVService = class extends BaseService {
|
|
|
943
1089
|
return text;
|
|
944
1090
|
}
|
|
945
1091
|
}
|
|
946
|
-
async createSignedReadUrlError(response, key) {
|
|
1092
|
+
async createSignedReadUrlError(response, key, path) {
|
|
947
1093
|
let errorText = response.statusText;
|
|
948
1094
|
try {
|
|
949
1095
|
const text = await response.text();
|
|
@@ -956,7 +1102,9 @@ var KVService = class extends BaseService {
|
|
|
956
1102
|
return this.authorizationFailure(
|
|
957
1103
|
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
958
1104
|
response,
|
|
959
|
-
errorText
|
|
1105
|
+
errorText,
|
|
1106
|
+
[path],
|
|
1107
|
+
KVAction.GET
|
|
960
1108
|
);
|
|
961
1109
|
}
|
|
962
1110
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
@@ -969,21 +1117,42 @@ var KVService = class extends BaseService {
|
|
|
969
1117
|
)
|
|
970
1118
|
);
|
|
971
1119
|
}
|
|
1120
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
1121
|
+
async readAuthorizationText(response) {
|
|
1122
|
+
try {
|
|
1123
|
+
return await response.text();
|
|
1124
|
+
} catch {
|
|
1125
|
+
return "";
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
972
1128
|
/**
|
|
973
1129
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
974
1130
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
975
1131
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
976
1132
|
* still see the status; `meta.status` carries it for typed callers.
|
|
977
|
-
*
|
|
1133
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
1134
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
1135
|
+
* canonical resource under the authenticated session's space.
|
|
978
1136
|
*/
|
|
979
|
-
authorizationFailure(context, response, errorText, extraMeta = {}) {
|
|
1137
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
980
1138
|
const { resource, action } = parseAuthError(errorText);
|
|
1139
|
+
const requestedPath = paths.find(
|
|
1140
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
1141
|
+
);
|
|
1142
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
1143
|
+
service: "kv",
|
|
1144
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1145
|
+
meta: {
|
|
1146
|
+
status: response.status,
|
|
1147
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
1148
|
+
requiredAction: action
|
|
1149
|
+
}
|
|
1150
|
+
}) : void 0;
|
|
981
1151
|
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
982
1152
|
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
983
1153
|
status: response.status,
|
|
984
|
-
...
|
|
985
|
-
...
|
|
986
|
-
...extraMeta
|
|
1154
|
+
...extraMeta,
|
|
1155
|
+
...capability
|
|
987
1156
|
}));
|
|
988
1157
|
}
|
|
989
1158
|
normalizeSignedReadUrlResponse(data) {
|
|
@@ -1027,13 +1196,24 @@ var KVService = class extends BaseService {
|
|
|
1027
1196
|
);
|
|
1028
1197
|
if (!response.ok) {
|
|
1029
1198
|
if (response.status === 401 || response.status === 403) {
|
|
1030
|
-
const errorText2 = await
|
|
1199
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1031
1200
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1201
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1202
|
+
service: "kv",
|
|
1203
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1204
|
+
meta: {
|
|
1205
|
+
status: response.status,
|
|
1206
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1207
|
+
requiredAction: KVAction.GET
|
|
1208
|
+
}
|
|
1209
|
+
}) : void 0;
|
|
1032
1210
|
return this.authorizationFailure(
|
|
1033
1211
|
`Failed to get key ${JSON.stringify(key)}`,
|
|
1034
1212
|
response,
|
|
1035
1213
|
errorText2,
|
|
1036
|
-
|
|
1214
|
+
[path],
|
|
1215
|
+
KVAction.GET,
|
|
1216
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1037
1217
|
);
|
|
1038
1218
|
}
|
|
1039
1219
|
if (response.status === 404) {
|
|
@@ -1110,7 +1290,9 @@ var KVService = class extends BaseService {
|
|
|
1110
1290
|
return this.authorizationFailure(
|
|
1111
1291
|
`Failed to put key ${JSON.stringify(key)}`,
|
|
1112
1292
|
response,
|
|
1113
|
-
await
|
|
1293
|
+
await this.readAuthorizationText(response),
|
|
1294
|
+
[path],
|
|
1295
|
+
KVAction.PUT
|
|
1114
1296
|
);
|
|
1115
1297
|
}
|
|
1116
1298
|
if (!response.ok) {
|
|
@@ -1239,7 +1421,9 @@ var KVService = class extends BaseService {
|
|
|
1239
1421
|
return this.authorizationFailure(
|
|
1240
1422
|
`Failed to batch put ${items.length} key(s)`,
|
|
1241
1423
|
response,
|
|
1242
|
-
errorText
|
|
1424
|
+
errorText,
|
|
1425
|
+
paths,
|
|
1426
|
+
KVAction.PUT
|
|
1243
1427
|
);
|
|
1244
1428
|
}
|
|
1245
1429
|
const quotaError = this.handleQuotaErrorResponse(
|
|
@@ -1360,11 +1544,13 @@ var KVService = class extends BaseService {
|
|
|
1360
1544
|
}
|
|
1361
1545
|
);
|
|
1362
1546
|
if (!response.ok) {
|
|
1363
|
-
if (response.status === 401) {
|
|
1547
|
+
if (response.status === 401 || response.status === 403) {
|
|
1364
1548
|
return this.authorizationFailure(
|
|
1365
1549
|
"Failed to list keys",
|
|
1366
1550
|
response,
|
|
1367
|
-
await
|
|
1551
|
+
await this.readAuthorizationText(response),
|
|
1552
|
+
[listPath],
|
|
1553
|
+
KVAction.LIST
|
|
1368
1554
|
);
|
|
1369
1555
|
}
|
|
1370
1556
|
const errorText = await response.text();
|
|
@@ -1413,11 +1599,13 @@ var KVService = class extends BaseService {
|
|
|
1413
1599
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1414
1600
|
);
|
|
1415
1601
|
if (!response.ok) {
|
|
1416
|
-
if (response.status === 401) {
|
|
1602
|
+
if (response.status === 401 || response.status === 403) {
|
|
1417
1603
|
return this.authorizationFailure(
|
|
1418
1604
|
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1419
1605
|
response,
|
|
1420
|
-
await
|
|
1606
|
+
await this.readAuthorizationText(response),
|
|
1607
|
+
[path],
|
|
1608
|
+
KVAction.DELETE
|
|
1421
1609
|
);
|
|
1422
1610
|
}
|
|
1423
1611
|
if (response.status === 404) {
|
|
@@ -1475,11 +1663,13 @@ var KVService = class extends BaseService {
|
|
|
1475
1663
|
options?.signal
|
|
1476
1664
|
);
|
|
1477
1665
|
if (!response.ok) {
|
|
1478
|
-
if (response.status === 401) {
|
|
1666
|
+
if (response.status === 401 || response.status === 403) {
|
|
1479
1667
|
return this.authorizationFailure(
|
|
1480
1668
|
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
1481
1669
|
response,
|
|
1482
|
-
await
|
|
1670
|
+
await this.readAuthorizationText(response),
|
|
1671
|
+
[path],
|
|
1672
|
+
KVAction.HEAD
|
|
1483
1673
|
);
|
|
1484
1674
|
}
|
|
1485
1675
|
if (response.status === 404) {
|
|
@@ -1546,7 +1736,7 @@ var KVService = class extends BaseService {
|
|
|
1546
1736
|
signal: this.combineSignals(options?.signal)
|
|
1547
1737
|
});
|
|
1548
1738
|
if (!response.ok) {
|
|
1549
|
-
return this.createSignedReadUrlError(response, key);
|
|
1739
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
1550
1740
|
}
|
|
1551
1741
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
1552
1742
|
await response.json()
|