@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.cjs
CHANGED
|
@@ -99,6 +99,55 @@ function serviceError(code, message, service, options) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// src/errors.ts
|
|
103
|
+
var import_bootstrap = require("@tinycloud/bootstrap");
|
|
104
|
+
|
|
105
|
+
// src/encryption/networkId.ts
|
|
106
|
+
var URN_PREFIX = "urn:tinycloud:encryption:";
|
|
107
|
+
var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
108
|
+
var NetworkIdError = class extends Error {
|
|
109
|
+
constructor(message) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "NetworkIdError";
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function parseNetworkId(networkId) {
|
|
115
|
+
if (typeof networkId !== "string" || networkId.length === 0) {
|
|
116
|
+
throw new NetworkIdError("networkId must be a non-empty string");
|
|
117
|
+
}
|
|
118
|
+
if (!networkId.startsWith(URN_PREFIX)) {
|
|
119
|
+
throw new NetworkIdError(
|
|
120
|
+
`networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const body = networkId.slice(URN_PREFIX.length);
|
|
124
|
+
const lastColon = body.lastIndexOf(":");
|
|
125
|
+
if (lastColon <= 0 || lastColon === body.length - 1) {
|
|
126
|
+
throw new NetworkIdError(
|
|
127
|
+
`networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const ownerDid = body.slice(0, lastColon);
|
|
131
|
+
const name = body.slice(lastColon + 1);
|
|
132
|
+
if (!ownerDid.startsWith("did:")) {
|
|
133
|
+
throw new NetworkIdError(
|
|
134
|
+
`networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const didParts = ownerDid.split(":");
|
|
138
|
+
if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
|
|
139
|
+
throw new NetworkIdError(
|
|
140
|
+
`networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!NETWORK_NAME_RE.test(name)) {
|
|
144
|
+
throw new NetworkIdError(
|
|
145
|
+
`networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return { networkId, ownerDid, name };
|
|
149
|
+
}
|
|
150
|
+
|
|
102
151
|
// src/errors.ts
|
|
103
152
|
function parsePermissionHint(value) {
|
|
104
153
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -167,6 +216,101 @@ function parseAuthError(responseText) {
|
|
|
167
216
|
function authUnauthorizedError(service, message, meta) {
|
|
168
217
|
return serviceError(ErrorCodes.AUTH_UNAUTHORIZED, message, service, { meta });
|
|
169
218
|
}
|
|
219
|
+
var grantableActions = new Set(
|
|
220
|
+
import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
|
|
221
|
+
);
|
|
222
|
+
var capabilityServices = new Set(
|
|
223
|
+
import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
|
|
224
|
+
);
|
|
225
|
+
function parseCapabilityResource(resource, service) {
|
|
226
|
+
if (resource.length === 0 || resource.length > 1024 || !capabilityServices.has(service) || /[\s\p{C}*#]/u.test(resource) || resource.includes("://")) return void 0;
|
|
227
|
+
const segments = resource.split("/");
|
|
228
|
+
if (!/^tinycloud:[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(segments[0])) return void 0;
|
|
229
|
+
const serviceIndex = segments.findIndex(
|
|
230
|
+
(segment, index) => index > 0 && capabilityServices.has(segment)
|
|
231
|
+
);
|
|
232
|
+
if (serviceIndex < 1 || segments[serviceIndex] !== service) return void 0;
|
|
233
|
+
if (segments.slice(1, serviceIndex).some(
|
|
234
|
+
(segment) => segment.length === 0 || segment === "." || segment === ".."
|
|
235
|
+
)) return void 0;
|
|
236
|
+
const pathSegments = segments.slice(serviceIndex + 1);
|
|
237
|
+
if (pathSegments.some((segment) => segment === "." || segment === "..")) return void 0;
|
|
238
|
+
if (service !== "kv" && (pathSegments.length === 0 || pathSegments[pathSegments.length - 1] === "")) return void 0;
|
|
239
|
+
return {
|
|
240
|
+
space: segments.slice(0, serviceIndex).join("/"),
|
|
241
|
+
path: pathSegments.join("/")
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function safeCapabilityResource(value, service, action) {
|
|
245
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) return false;
|
|
246
|
+
if (service === "encryption" && value.startsWith("urn:tinycloud:encryption:")) {
|
|
247
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.:/-]*$/.test(value) || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
248
|
+
try {
|
|
249
|
+
parseNetworkId(value);
|
|
250
|
+
return true;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const parsed = parseCapabilityResource(value, service);
|
|
256
|
+
return parsed !== void 0 && (!parsed.path.endsWith("/") || service === "kv" && action === "tinycloud.kv/list") && (parsed.path !== "" || service === "kv" && action === "tinycloud.kv/list");
|
|
257
|
+
}
|
|
258
|
+
function validatedCapabilityOf(error) {
|
|
259
|
+
const verdict = authorizationVerdictOf(error);
|
|
260
|
+
if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
|
|
261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
262
|
+
let node = error;
|
|
263
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
264
|
+
seen.add(node);
|
|
265
|
+
const record = node;
|
|
266
|
+
const status = errorStatusOf(record);
|
|
267
|
+
const meta = record.meta;
|
|
268
|
+
if ((status === 401 || status === 403 || status === void 0 && record.code === ErrorCodes.AUTH_UNAUTHORIZED) && typeof meta === "object" && meta !== null && !Array.isArray(meta)) {
|
|
269
|
+
const metadata = meta;
|
|
270
|
+
const { resource, requiredAction } = metadata;
|
|
271
|
+
if (typeof requiredAction === "string" && grantableActions.has(requiredAction)) {
|
|
272
|
+
const actionService = requiredAction.slice("tinycloud.".length, requiredAction.indexOf("/"));
|
|
273
|
+
const explicitService = record.service === void 0 ? metadata.service : record.service;
|
|
274
|
+
if (safeCapabilityResource(resource, actionService, requiredAction) && (explicitService === void 0 || explicitService === actionService || explicitService === `tinycloud.${actionService}`)) {
|
|
275
|
+
return { resource, requiredAction };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
node = record.cause;
|
|
280
|
+
}
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
284
|
+
function errorStatusOf(node) {
|
|
285
|
+
const meta = node.meta;
|
|
286
|
+
const candidates = [
|
|
287
|
+
node.status,
|
|
288
|
+
node.statusCode,
|
|
289
|
+
typeof meta === "object" && meta !== null ? meta.status : void 0
|
|
290
|
+
];
|
|
291
|
+
for (const candidate of candidates) {
|
|
292
|
+
if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 400 && candidate <= 599) {
|
|
293
|
+
return candidate;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
function authorizationVerdictOf(error) {
|
|
299
|
+
const seen = /* @__PURE__ */ new Set();
|
|
300
|
+
let sawUnauthorizedCode = false;
|
|
301
|
+
let node = error;
|
|
302
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
|
|
303
|
+
seen.add(node);
|
|
304
|
+
const record = node;
|
|
305
|
+
const status = errorStatusOf(record);
|
|
306
|
+
if (status === 401) return "unauthenticated";
|
|
307
|
+
if (status === 403) return "forbidden";
|
|
308
|
+
if (status !== void 0) return "other";
|
|
309
|
+
if (record.code === ErrorCodes.AUTH_UNAUTHORIZED) sawUnauthorizedCode = true;
|
|
310
|
+
node = record.cause;
|
|
311
|
+
}
|
|
312
|
+
return sawUnauthorizedCode ? "forbidden" : void 0;
|
|
313
|
+
}
|
|
170
314
|
function storageQuotaExceededError(service, message, meta) {
|
|
171
315
|
return {
|
|
172
316
|
code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
|
|
@@ -877,12 +1021,14 @@ var KVService = class extends BaseService {
|
|
|
877
1021
|
signal: this.combineSignals(options?.signal)
|
|
878
1022
|
});
|
|
879
1023
|
if (!response.ok) {
|
|
880
|
-
const errorText = await response.text();
|
|
1024
|
+
const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
|
|
881
1025
|
if (response.status === 401 || response.status === 403) {
|
|
882
1026
|
return this.authorizationFailure(
|
|
883
1027
|
`Failed to batch read ${keys.length} key(s)`,
|
|
884
1028
|
response,
|
|
885
|
-
errorText
|
|
1029
|
+
errorText,
|
|
1030
|
+
paths,
|
|
1031
|
+
action
|
|
886
1032
|
);
|
|
887
1033
|
}
|
|
888
1034
|
if (response.status === 413) {
|
|
@@ -972,7 +1118,7 @@ var KVService = class extends BaseService {
|
|
|
972
1118
|
return text;
|
|
973
1119
|
}
|
|
974
1120
|
}
|
|
975
|
-
async createSignedReadUrlError(response, key) {
|
|
1121
|
+
async createSignedReadUrlError(response, key, path) {
|
|
976
1122
|
let errorText = response.statusText;
|
|
977
1123
|
try {
|
|
978
1124
|
const text = await response.text();
|
|
@@ -985,7 +1131,9 @@ var KVService = class extends BaseService {
|
|
|
985
1131
|
return this.authorizationFailure(
|
|
986
1132
|
`Failed to create signed read URL for key ${JSON.stringify(key)}`,
|
|
987
1133
|
response,
|
|
988
|
-
errorText
|
|
1134
|
+
errorText,
|
|
1135
|
+
[path],
|
|
1136
|
+
KVAction.GET
|
|
989
1137
|
);
|
|
990
1138
|
}
|
|
991
1139
|
const code = response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR;
|
|
@@ -998,21 +1146,42 @@ var KVService = class extends BaseService {
|
|
|
998
1146
|
)
|
|
999
1147
|
);
|
|
1000
1148
|
}
|
|
1149
|
+
/** A known 401/403 keeps its typed status even if reading its body fails. */
|
|
1150
|
+
async readAuthorizationText(response) {
|
|
1151
|
+
try {
|
|
1152
|
+
return await response.text();
|
|
1153
|
+
} catch {
|
|
1154
|
+
return "";
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1001
1157
|
/**
|
|
1002
1158
|
* AUTH_UNAUTHORIZED for a 401/403 response. The message keeps the HTTP
|
|
1003
1159
|
* status and the server text (`<context>: <status> - <text>`, the same
|
|
1004
1160
|
* shape as every other KV failure) so callers that rethrow only the message
|
|
1005
1161
|
* still see the status; `meta.status` carries it for typed callers.
|
|
1006
|
-
*
|
|
1162
|
+
* Server hints must match the requested path/action (whether the node sends
|
|
1163
|
+
* a relative path or full resource); metadata uses only the request-derived
|
|
1164
|
+
* canonical resource under the authenticated session's space.
|
|
1007
1165
|
*/
|
|
1008
|
-
authorizationFailure(context, response, errorText, extraMeta = {}) {
|
|
1166
|
+
authorizationFailure(context, response, errorText, paths, expectedAction, extraMeta = {}) {
|
|
1009
1167
|
const { resource, action } = parseAuthError(errorText);
|
|
1168
|
+
const requestedPath = paths.find(
|
|
1169
|
+
(path) => resource === path || resource === `${this.context.session.spaceId}/kv/${path}`
|
|
1170
|
+
);
|
|
1171
|
+
const capability = requestedPath !== void 0 && action === expectedAction ? validatedCapabilityOf({
|
|
1172
|
+
service: "kv",
|
|
1173
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1174
|
+
meta: {
|
|
1175
|
+
status: response.status,
|
|
1176
|
+
resource: `${this.context.session.spaceId}/kv/${requestedPath}`,
|
|
1177
|
+
requiredAction: action
|
|
1178
|
+
}
|
|
1179
|
+
}) : void 0;
|
|
1010
1180
|
const detail = errorText.trim().length > 0 ? errorText : response.statusText || "authorization failed";
|
|
1011
1181
|
return err(authUnauthorizedError("kv", `${context}: ${response.status} - ${detail}`, {
|
|
1012
1182
|
status: response.status,
|
|
1013
|
-
...
|
|
1014
|
-
...
|
|
1015
|
-
...extraMeta
|
|
1183
|
+
...extraMeta,
|
|
1184
|
+
...capability
|
|
1016
1185
|
}));
|
|
1017
1186
|
}
|
|
1018
1187
|
normalizeSignedReadUrlResponse(data) {
|
|
@@ -1056,13 +1225,24 @@ var KVService = class extends BaseService {
|
|
|
1056
1225
|
);
|
|
1057
1226
|
if (!response.ok) {
|
|
1058
1227
|
if (response.status === 401 || response.status === 403) {
|
|
1059
|
-
const errorText2 = await
|
|
1228
|
+
const errorText2 = await this.readAuthorizationText(response);
|
|
1060
1229
|
const permissionHint = parsePermissionHintFromErrorText(errorText2);
|
|
1230
|
+
const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
|
|
1231
|
+
service: "kv",
|
|
1232
|
+
code: ErrorCodes.AUTH_UNAUTHORIZED,
|
|
1233
|
+
meta: {
|
|
1234
|
+
status: response.status,
|
|
1235
|
+
resource: `${this.context.session.spaceId}/kv/${path}`,
|
|
1236
|
+
requiredAction: KVAction.GET
|
|
1237
|
+
}
|
|
1238
|
+
}) : void 0;
|
|
1061
1239
|
return this.authorizationFailure(
|
|
1062
1240
|
`Failed to get key ${JSON.stringify(key)}`,
|
|
1063
1241
|
response,
|
|
1064
1242
|
errorText2,
|
|
1065
|
-
|
|
1243
|
+
[path],
|
|
1244
|
+
KVAction.GET,
|
|
1245
|
+
structuredCapability ? { permissionHint, ...structuredCapability } : {}
|
|
1066
1246
|
);
|
|
1067
1247
|
}
|
|
1068
1248
|
if (response.status === 404) {
|
|
@@ -1139,7 +1319,9 @@ var KVService = class extends BaseService {
|
|
|
1139
1319
|
return this.authorizationFailure(
|
|
1140
1320
|
`Failed to put key ${JSON.stringify(key)}`,
|
|
1141
1321
|
response,
|
|
1142
|
-
await
|
|
1322
|
+
await this.readAuthorizationText(response),
|
|
1323
|
+
[path],
|
|
1324
|
+
KVAction.PUT
|
|
1143
1325
|
);
|
|
1144
1326
|
}
|
|
1145
1327
|
if (!response.ok) {
|
|
@@ -1268,7 +1450,9 @@ var KVService = class extends BaseService {
|
|
|
1268
1450
|
return this.authorizationFailure(
|
|
1269
1451
|
`Failed to batch put ${items.length} key(s)`,
|
|
1270
1452
|
response,
|
|
1271
|
-
errorText
|
|
1453
|
+
errorText,
|
|
1454
|
+
paths,
|
|
1455
|
+
KVAction.PUT
|
|
1272
1456
|
);
|
|
1273
1457
|
}
|
|
1274
1458
|
const quotaError = this.handleQuotaErrorResponse(
|
|
@@ -1389,11 +1573,13 @@ var KVService = class extends BaseService {
|
|
|
1389
1573
|
}
|
|
1390
1574
|
);
|
|
1391
1575
|
if (!response.ok) {
|
|
1392
|
-
if (response.status === 401) {
|
|
1576
|
+
if (response.status === 401 || response.status === 403) {
|
|
1393
1577
|
return this.authorizationFailure(
|
|
1394
1578
|
"Failed to list keys",
|
|
1395
1579
|
response,
|
|
1396
|
-
await
|
|
1580
|
+
await this.readAuthorizationText(response),
|
|
1581
|
+
[listPath],
|
|
1582
|
+
KVAction.LIST
|
|
1397
1583
|
);
|
|
1398
1584
|
}
|
|
1399
1585
|
const errorText = await response.text();
|
|
@@ -1442,11 +1628,13 @@ var KVService = class extends BaseService {
|
|
|
1442
1628
|
options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
|
|
1443
1629
|
);
|
|
1444
1630
|
if (!response.ok) {
|
|
1445
|
-
if (response.status === 401) {
|
|
1631
|
+
if (response.status === 401 || response.status === 403) {
|
|
1446
1632
|
return this.authorizationFailure(
|
|
1447
1633
|
`Failed to delete key ${JSON.stringify(key)}`,
|
|
1448
1634
|
response,
|
|
1449
|
-
await
|
|
1635
|
+
await this.readAuthorizationText(response),
|
|
1636
|
+
[path],
|
|
1637
|
+
KVAction.DELETE
|
|
1450
1638
|
);
|
|
1451
1639
|
}
|
|
1452
1640
|
if (response.status === 404) {
|
|
@@ -1504,11 +1692,13 @@ var KVService = class extends BaseService {
|
|
|
1504
1692
|
options?.signal
|
|
1505
1693
|
);
|
|
1506
1694
|
if (!response.ok) {
|
|
1507
|
-
if (response.status === 401) {
|
|
1695
|
+
if (response.status === 401 || response.status === 403) {
|
|
1508
1696
|
return this.authorizationFailure(
|
|
1509
1697
|
`Failed to get metadata for key ${JSON.stringify(key)}`,
|
|
1510
1698
|
response,
|
|
1511
|
-
await
|
|
1699
|
+
await this.readAuthorizationText(response),
|
|
1700
|
+
[path],
|
|
1701
|
+
KVAction.HEAD
|
|
1512
1702
|
);
|
|
1513
1703
|
}
|
|
1514
1704
|
if (response.status === 404) {
|
|
@@ -1575,7 +1765,7 @@ var KVService = class extends BaseService {
|
|
|
1575
1765
|
signal: this.combineSignals(options?.signal)
|
|
1576
1766
|
});
|
|
1577
1767
|
if (!response.ok) {
|
|
1578
|
-
return this.createSignedReadUrlError(response, key);
|
|
1768
|
+
return this.createSignedReadUrlError(response, key, path);
|
|
1579
1769
|
}
|
|
1580
1770
|
const signedUrl = this.normalizeSignedReadUrlResponse(
|
|
1581
1771
|
await response.json()
|