@tinycloud/operations 0.3.4-beta.6 → 0.4.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifacts.cjs +499 -6678
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +116 -59
- package/dist/artifacts.d.ts +116 -59
- package/dist/artifacts.js +482 -6660
- package/dist/artifacts.js.map +1 -1
- package/dist/authority-ofwbEqmj.d.cts +30 -0
- package/dist/authority-ofwbEqmj.d.ts +30 -0
- package/dist/cli-runtime.cjs +1151 -7133
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.d.cts +1 -1
- package/dist/cli-runtime.d.ts +1 -1
- package/dist/cli-runtime.js +1140 -7125
- package/dist/cli-runtime.js.map +1 -1
- package/dist/{contract-xMIIMtpF.d.cts → contract-eWtDSMeI.d.cts} +1 -1
- package/dist/{contract-xMIIMtpF.d.ts → contract-eWtDSMeI.d.ts} +1 -1
- package/dist/delegation-binding.cjs +1411 -0
- package/dist/delegation-binding.cjs.map +1 -0
- package/dist/delegation-binding.d.cts +130 -0
- package/dist/delegation-binding.d.ts +130 -0
- package/dist/delegation-binding.js +1403 -0
- package/dist/delegation-binding.js.map +1 -0
- package/dist/index.cjs +1148 -7130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1140 -7125
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +9 -2
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.d.cts +1 -1
- package/dist/profile.d.ts +1 -1
- package/dist/profile.js +11 -2
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +426 -50
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +38 -5
- package/dist/state.d.ts +38 -5
- package/dist/state.js +424 -50
- package/dist/state.js.map +1 -1
- package/generated/operations.json +107 -4
- package/package.json +10 -4
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// ../../node_modules/ms/index.js
|
|
28
|
+
var require_ms = __commonJS({
|
|
29
|
+
"../../node_modules/ms/index.js"(exports, module) {
|
|
30
|
+
"use strict";
|
|
31
|
+
var s = 1e3;
|
|
32
|
+
var m = s * 60;
|
|
33
|
+
var h = m * 60;
|
|
34
|
+
var d = h * 24;
|
|
35
|
+
var w = d * 7;
|
|
36
|
+
var y = d * 365.25;
|
|
37
|
+
module.exports = function(val, options) {
|
|
38
|
+
options = options || {};
|
|
39
|
+
var type = typeof val;
|
|
40
|
+
if (type === "string" && val.length > 0) {
|
|
41
|
+
return parse(val);
|
|
42
|
+
} else if (type === "number" && isFinite(val)) {
|
|
43
|
+
return options.long ? fmtLong(val) : fmtShort(val);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(
|
|
46
|
+
"val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
function parse(str) {
|
|
50
|
+
str = String(str);
|
|
51
|
+
if (str.length > 100) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
|
|
55
|
+
str
|
|
56
|
+
);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
var n = parseFloat(match[1]);
|
|
61
|
+
var type = (match[2] || "ms").toLowerCase();
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "years":
|
|
64
|
+
case "year":
|
|
65
|
+
case "yrs":
|
|
66
|
+
case "yr":
|
|
67
|
+
case "y":
|
|
68
|
+
return n * y;
|
|
69
|
+
case "weeks":
|
|
70
|
+
case "week":
|
|
71
|
+
case "w":
|
|
72
|
+
return n * w;
|
|
73
|
+
case "days":
|
|
74
|
+
case "day":
|
|
75
|
+
case "d":
|
|
76
|
+
return n * d;
|
|
77
|
+
case "hours":
|
|
78
|
+
case "hour":
|
|
79
|
+
case "hrs":
|
|
80
|
+
case "hr":
|
|
81
|
+
case "h":
|
|
82
|
+
return n * h;
|
|
83
|
+
case "minutes":
|
|
84
|
+
case "minute":
|
|
85
|
+
case "mins":
|
|
86
|
+
case "min":
|
|
87
|
+
case "m":
|
|
88
|
+
return n * m;
|
|
89
|
+
case "seconds":
|
|
90
|
+
case "second":
|
|
91
|
+
case "secs":
|
|
92
|
+
case "sec":
|
|
93
|
+
case "s":
|
|
94
|
+
return n * s;
|
|
95
|
+
case "milliseconds":
|
|
96
|
+
case "millisecond":
|
|
97
|
+
case "msecs":
|
|
98
|
+
case "msec":
|
|
99
|
+
case "ms":
|
|
100
|
+
return n;
|
|
101
|
+
default:
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function fmtShort(ms2) {
|
|
106
|
+
var msAbs = Math.abs(ms2);
|
|
107
|
+
if (msAbs >= d) {
|
|
108
|
+
return Math.round(ms2 / d) + "d";
|
|
109
|
+
}
|
|
110
|
+
if (msAbs >= h) {
|
|
111
|
+
return Math.round(ms2 / h) + "h";
|
|
112
|
+
}
|
|
113
|
+
if (msAbs >= m) {
|
|
114
|
+
return Math.round(ms2 / m) + "m";
|
|
115
|
+
}
|
|
116
|
+
if (msAbs >= s) {
|
|
117
|
+
return Math.round(ms2 / s) + "s";
|
|
118
|
+
}
|
|
119
|
+
return ms2 + "ms";
|
|
120
|
+
}
|
|
121
|
+
function fmtLong(ms2) {
|
|
122
|
+
var msAbs = Math.abs(ms2);
|
|
123
|
+
if (msAbs >= d) {
|
|
124
|
+
return plural(ms2, msAbs, d, "day");
|
|
125
|
+
}
|
|
126
|
+
if (msAbs >= h) {
|
|
127
|
+
return plural(ms2, msAbs, h, "hour");
|
|
128
|
+
}
|
|
129
|
+
if (msAbs >= m) {
|
|
130
|
+
return plural(ms2, msAbs, m, "minute");
|
|
131
|
+
}
|
|
132
|
+
if (msAbs >= s) {
|
|
133
|
+
return plural(ms2, msAbs, s, "second");
|
|
134
|
+
}
|
|
135
|
+
return ms2 + " ms";
|
|
136
|
+
}
|
|
137
|
+
function plural(ms2, msAbs, n, name) {
|
|
138
|
+
var isPlural = msAbs >= n * 1.5;
|
|
139
|
+
return Math.round(ms2 / n) + " " + name + (isPlural ? "s" : "");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// src/delegation-binding.ts
|
|
145
|
+
import { join as join2 } from "path";
|
|
146
|
+
import * as nodeSdk2 from "@tinycloud/node-sdk";
|
|
147
|
+
|
|
148
|
+
// src/artifacts.ts
|
|
149
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
150
|
+
import { z } from "zod";
|
|
151
|
+
|
|
152
|
+
// ../sdk-core/src/manifest.ts
|
|
153
|
+
var import_ms = __toESM(require_ms(), 1);
|
|
154
|
+
import { resolveSecretPath, SECRET_NAME_RE } from "@tinycloud/sdk-services";
|
|
155
|
+
var DEFAULT_MANIFEST_SPACE = "applications";
|
|
156
|
+
var SERVICE_SHORT_TO_LONG = Object.freeze({
|
|
157
|
+
kv: "tinycloud.kv",
|
|
158
|
+
sql: "tinycloud.sql",
|
|
159
|
+
duckdb: "tinycloud.duckdb",
|
|
160
|
+
capabilities: "tinycloud.capabilities",
|
|
161
|
+
hooks: "tinycloud.hooks",
|
|
162
|
+
encryption: "tinycloud.encryption",
|
|
163
|
+
delegation: "tinycloud.delegation"
|
|
164
|
+
});
|
|
165
|
+
var SERVICE_LONG_TO_SHORT = Object.freeze(
|
|
166
|
+
Object.fromEntries(
|
|
167
|
+
Object.entries(SERVICE_SHORT_TO_LONG).map(([s, l]) => [l, s])
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
function expandActionShortNames(service, actions) {
|
|
171
|
+
return actions.map((a) => {
|
|
172
|
+
if (a.includes("/")) {
|
|
173
|
+
return a;
|
|
174
|
+
}
|
|
175
|
+
return `${service}/${a}`;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ../sdk-core/src/capabilities.ts
|
|
180
|
+
function normalizeSpace(space) {
|
|
181
|
+
if (!space.startsWith("tinycloud:")) {
|
|
182
|
+
return space;
|
|
183
|
+
}
|
|
184
|
+
const lastColon = space.lastIndexOf(":");
|
|
185
|
+
if (lastColon === -1 || lastColon === space.length - 1) {
|
|
186
|
+
return space;
|
|
187
|
+
}
|
|
188
|
+
return space.slice(lastColon + 1);
|
|
189
|
+
}
|
|
190
|
+
function isCapabilitySubset(requested, granted) {
|
|
191
|
+
const missing = [];
|
|
192
|
+
for (const req of requested) {
|
|
193
|
+
const matchingScope = granted.filter(
|
|
194
|
+
(entry) => canonicalizeEntryScopeMatches(req, entry)
|
|
195
|
+
);
|
|
196
|
+
const requestedActions = expandActionShortNames(
|
|
197
|
+
req.service,
|
|
198
|
+
req.actions
|
|
199
|
+
);
|
|
200
|
+
const grantedActions = matchingScope.flatMap(
|
|
201
|
+
(entry) => expandActionShortNames(entry.service, entry.actions)
|
|
202
|
+
);
|
|
203
|
+
const covered = requestedActions.every(
|
|
204
|
+
(requestedAction) => grantedActions.some(
|
|
205
|
+
(grantedAction) => actionContains(grantedAction, requestedAction)
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
if (!covered) {
|
|
209
|
+
missing.push(cloneEntry(req));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { subset: missing.length === 0, missing };
|
|
213
|
+
}
|
|
214
|
+
function canonicalizeEntryScopeMatches(requested, granted) {
|
|
215
|
+
if (requested.service !== granted.service) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (normalizeSpace(requested.space ?? DEFAULT_MANIFEST_SPACE) !== normalizeSpace(granted.space ?? DEFAULT_MANIFEST_SPACE)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (!pathContains(granted.path, requested.path)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (!recapCaveatsEqual(requested.caveats, granted.caveats)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
function recapCaveatsEqual(left, right) {
|
|
230
|
+
return canonicalizeRecapCaveats(left) === canonicalizeRecapCaveats(right);
|
|
231
|
+
}
|
|
232
|
+
function canonicalizeRecapCaveats(caveats) {
|
|
233
|
+
const canonicalize = (value) => {
|
|
234
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
235
|
+
return JSON.stringify(value);
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === "number") {
|
|
238
|
+
if (Number.isFinite(value)) return JSON.stringify(value);
|
|
239
|
+
throw new Error("ReCap caveats must contain only JSON values.");
|
|
240
|
+
}
|
|
241
|
+
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
|
|
242
|
+
if (typeof value === "object") {
|
|
243
|
+
const object = value;
|
|
244
|
+
const prototype = Object.getPrototypeOf(object);
|
|
245
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
246
|
+
throw new Error("ReCap caveats must contain only JSON values.");
|
|
247
|
+
}
|
|
248
|
+
return `{${Object.keys(object).sort().map(
|
|
249
|
+
(key) => `${JSON.stringify(key)}:${canonicalize(object[key])}`
|
|
250
|
+
).join(",")}}`;
|
|
251
|
+
}
|
|
252
|
+
throw new Error("ReCap caveats must contain only JSON values.");
|
|
253
|
+
};
|
|
254
|
+
const branches = (caveats ?? []).map(canonicalize).sort();
|
|
255
|
+
if (branches.length === 0 || branches.length === 1 && branches[0] === "{}") {
|
|
256
|
+
return "[]";
|
|
257
|
+
}
|
|
258
|
+
return `[${branches.join(",")}]`;
|
|
259
|
+
}
|
|
260
|
+
function actionContains(grantedAction, requestedAction) {
|
|
261
|
+
if (grantedAction === "*" || grantedAction === requestedAction) return true;
|
|
262
|
+
if (!grantedAction.endsWith("/*")) return false;
|
|
263
|
+
const service = grantedAction.slice(0, -2);
|
|
264
|
+
return requestedAction === service || requestedAction.startsWith(`${service}/`);
|
|
265
|
+
}
|
|
266
|
+
function pathContains(grantedPath, requestedPath) {
|
|
267
|
+
if (grantedPath === "" || grantedPath === "/") {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (grantedPath.endsWith("/")) {
|
|
271
|
+
return requestedPath.startsWith(grantedPath);
|
|
272
|
+
}
|
|
273
|
+
return requestedPath === grantedPath;
|
|
274
|
+
}
|
|
275
|
+
function cloneEntry(entry) {
|
|
276
|
+
return {
|
|
277
|
+
service: entry.service,
|
|
278
|
+
...entry.space !== void 0 ? { space: entry.space } : {},
|
|
279
|
+
path: entry.path,
|
|
280
|
+
actions: [...entry.actions],
|
|
281
|
+
...entry.caveats !== void 0 ? { caveats: JSON.parse(JSON.stringify(entry.caveats)) } : {},
|
|
282
|
+
...entry.skipPrefix !== void 0 ? { skipPrefix: entry.skipPrefix } : {},
|
|
283
|
+
...entry.expiry !== void 0 ? { expiry: entry.expiry } : {}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/authority.ts
|
|
288
|
+
function evaluateOperationAuthority(granted, required, resolveSpace) {
|
|
289
|
+
const canonicalGranted = canonicalizeOperationCapabilities(granted, resolveSpace);
|
|
290
|
+
const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace);
|
|
291
|
+
const missing = [];
|
|
292
|
+
for (const requested of canonicalRequired) {
|
|
293
|
+
const covered = canonicalGranted.some(
|
|
294
|
+
(candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space) && isCapabilitySubset([requested], [candidate]).subset
|
|
295
|
+
);
|
|
296
|
+
if (!covered) missing.push(requested);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
satisfied: missing.length === 0,
|
|
300
|
+
missing: canonicalizeCapabilities(missing)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function delegationWithinRequest(requested, effective, resolveSpace) {
|
|
304
|
+
return requested.length > 0 && evaluateOperationAuthority(requested, canonicalizeCapabilities(effective), resolveSpace).satisfied;
|
|
305
|
+
}
|
|
306
|
+
function canonicalizeOperationCapabilities(permissions, resolveSpace) {
|
|
307
|
+
return canonicalizeCapabilities(permissions.map((permission) => {
|
|
308
|
+
const canonicalPath = canonicalizeOperationPath(permission);
|
|
309
|
+
if (permission.space === void 0 || resolveSpace === void 0) {
|
|
310
|
+
return canonicalPath;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
return { ...canonicalPath, space: resolveSpace(permission.space) };
|
|
314
|
+
} catch {
|
|
315
|
+
return canonicalPath;
|
|
316
|
+
}
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
function canonicalizeOperationPath(permission) {
|
|
320
|
+
if (permission.service !== "tinycloud.encryption") return permission;
|
|
321
|
+
const match = permission.path.match(
|
|
322
|
+
/^(urn:tinycloud:encryption:did:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})(:.*)$/
|
|
323
|
+
);
|
|
324
|
+
if (!match) return permission;
|
|
325
|
+
return { ...permission, path: `${match[1]}${match[2].toLowerCase()}${match[3]}` };
|
|
326
|
+
}
|
|
327
|
+
function operationSpacesEqual(granted, requested) {
|
|
328
|
+
return granted === requested;
|
|
329
|
+
}
|
|
330
|
+
function canonicalizeCapabilities(permissions) {
|
|
331
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
332
|
+
for (const permission of permissions) {
|
|
333
|
+
const canonical = canonicalizeCapability(permission);
|
|
334
|
+
byIdentity.set(permissionIdentityFromCanonical(canonical), canonical);
|
|
335
|
+
}
|
|
336
|
+
return [...byIdentity.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, permission]) => permission);
|
|
337
|
+
}
|
|
338
|
+
function canonicalizeCapability(permission) {
|
|
339
|
+
const actions = [...new Set(permission.actions)].sort((left, right) => left.localeCompare(right));
|
|
340
|
+
return {
|
|
341
|
+
service: permission.service,
|
|
342
|
+
...permission.space !== void 0 ? { space: permission.space } : {},
|
|
343
|
+
path: permission.path,
|
|
344
|
+
actions,
|
|
345
|
+
...permission.skipPrefix !== void 0 ? { skipPrefix: permission.skipPrefix } : {},
|
|
346
|
+
...permission.expiry !== void 0 ? { expiry: permission.expiry } : {},
|
|
347
|
+
...permission.description !== void 0 ? { description: permission.description } : {},
|
|
348
|
+
...permission.caveats === void 0 ? {} : { caveats: canonicalizeCaveats(permission.caveats) }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function permissionIdentityFromCanonical(permission) {
|
|
352
|
+
return JSON.stringify({
|
|
353
|
+
service: permission.service,
|
|
354
|
+
...permission.space !== void 0 ? { space: permission.space } : {},
|
|
355
|
+
path: permission.path,
|
|
356
|
+
actions: permission.actions,
|
|
357
|
+
...permission.skipPrefix !== void 0 ? { skipPrefix: permission.skipPrefix } : {},
|
|
358
|
+
...permission.expiry !== void 0 ? { expiry: permission.expiry } : {},
|
|
359
|
+
...permission.description !== void 0 ? { description: permission.description } : {},
|
|
360
|
+
...permission.caveats === void 0 ? {} : { caveats: permission.caveats }
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function canonicalizeCaveats(caveats) {
|
|
364
|
+
return [...caveats].map((caveat) => ({
|
|
365
|
+
identity: canonicalizeRecapCaveats([caveat]),
|
|
366
|
+
value: JSON.parse(JSON.stringify(caveat))
|
|
367
|
+
})).sort((left, right) => left.identity.localeCompare(right.identity)).map(({ value }) => value);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/state.ts
|
|
371
|
+
import { randomUUID } from "crypto";
|
|
372
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
373
|
+
import {
|
|
374
|
+
chmod,
|
|
375
|
+
link,
|
|
376
|
+
mkdir,
|
|
377
|
+
open,
|
|
378
|
+
readFile,
|
|
379
|
+
readdir,
|
|
380
|
+
rename,
|
|
381
|
+
rmdir,
|
|
382
|
+
rm,
|
|
383
|
+
stat,
|
|
384
|
+
unlink,
|
|
385
|
+
writeFile
|
|
386
|
+
} from "fs/promises";
|
|
387
|
+
import { homedir } from "os";
|
|
388
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
389
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
390
|
+
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
391
|
+
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
392
|
+
var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
|
|
393
|
+
var PRIVATE_FILE_MODE = 384;
|
|
394
|
+
var PRIVATE_DIR_MODE = 448;
|
|
395
|
+
var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
|
|
396
|
+
var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
|
|
397
|
+
var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
398
|
+
var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
399
|
+
var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
400
|
+
var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
401
|
+
var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
402
|
+
var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
|
|
403
|
+
var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
404
|
+
var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
405
|
+
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
406
|
+
function processWideContext(key) {
|
|
407
|
+
var _a2;
|
|
408
|
+
const registry = globalThis;
|
|
409
|
+
return registry[_a2 = Symbol.for(key)] ?? (registry[_a2] = new AsyncLocalStorage());
|
|
410
|
+
}
|
|
411
|
+
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
412
|
+
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
413
|
+
var ProfileLockTimeoutError = class extends Error {
|
|
414
|
+
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
415
|
+
constructor(profile, timeoutMs, detail) {
|
|
416
|
+
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.${detail ? ` ${detail}` : ""}`);
|
|
417
|
+
this.code = "PROFILE_LOCK_TIMEOUT";
|
|
418
|
+
this.name = "ProfileLockTimeoutError";
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
var ProfileDeletedError = class extends Error {
|
|
422
|
+
constructor(profile) {
|
|
423
|
+
super(`Profile "${profile}" was deleted while this change waited for its lock, so the change was not written.`);
|
|
424
|
+
this.code = "PROFILE_NOT_FOUND";
|
|
425
|
+
this.name = "ProfileDeletedError";
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
function tinycloudHomePath() {
|
|
429
|
+
const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
430
|
+
return join(home, ".tinycloud");
|
|
431
|
+
}
|
|
432
|
+
function profilesPath() {
|
|
433
|
+
return join(tinycloudHomePath(), "profiles");
|
|
434
|
+
}
|
|
435
|
+
function profilePath(profile) {
|
|
436
|
+
return join(profilesPath(), validateProfileName(profile));
|
|
437
|
+
}
|
|
438
|
+
function profileConfigPath(profile) {
|
|
439
|
+
return join(profilePath(profile), "profile.json");
|
|
440
|
+
}
|
|
441
|
+
function sessionPath(profile) {
|
|
442
|
+
return profileStorePath(profile, "session");
|
|
443
|
+
}
|
|
444
|
+
function profileStorePath(profile, store) {
|
|
445
|
+
return join(profilePath(profile), `${store}.json`);
|
|
446
|
+
}
|
|
447
|
+
function profileStoreMetadataPath(profile, store) {
|
|
448
|
+
return `${profileStorePath(profile, store)}.metadata.json`;
|
|
449
|
+
}
|
|
450
|
+
function profileLockPath(profile) {
|
|
451
|
+
return join(profilePath(profile), ".lock");
|
|
452
|
+
}
|
|
453
|
+
function profileTurnLockPath(profile) {
|
|
454
|
+
return join(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
|
|
455
|
+
}
|
|
456
|
+
async function readJson(filePath) {
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (isErrno(error, "ENOENT")) return null;
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function writeJsonAtomic(filePath, value) {
|
|
465
|
+
await replaceJson(filePath, value, false);
|
|
466
|
+
}
|
|
467
|
+
async function replaceJson(filePath, value, durable) {
|
|
468
|
+
const directory = dirname(filePath);
|
|
469
|
+
const temporaryPath = join(
|
|
470
|
+
directory,
|
|
471
|
+
`.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`
|
|
472
|
+
);
|
|
473
|
+
const contents = `${JSON.stringify(value, null, 2)}
|
|
474
|
+
`;
|
|
475
|
+
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
476
|
+
try {
|
|
477
|
+
if (durable) await writeFileDurably(temporaryPath, contents);
|
|
478
|
+
else await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
479
|
+
await rename(temporaryPath, filePath);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function writeFileDurably(path, contents) {
|
|
486
|
+
const handle = await open(path, "wx", PRIVATE_FILE_MODE);
|
|
487
|
+
try {
|
|
488
|
+
if (contents) {
|
|
489
|
+
await handle.writeFile(contents, "utf8");
|
|
490
|
+
await handle.datasync();
|
|
491
|
+
}
|
|
492
|
+
} finally {
|
|
493
|
+
await handle.close();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function readSession(profile) {
|
|
497
|
+
await readStoreMetadata(profile, "session");
|
|
498
|
+
return readJson(sessionPath(profile));
|
|
499
|
+
}
|
|
500
|
+
async function readAdditionalDelegations(profile) {
|
|
501
|
+
return (await readProfileStore(profile, "additional-delegations")).records;
|
|
502
|
+
}
|
|
503
|
+
async function readStoreMetadata(profile, store) {
|
|
504
|
+
const metadata = await readJson(profileStoreMetadataPath(profile, store));
|
|
505
|
+
if (metadata === null) return { formatVersion: 1 };
|
|
506
|
+
if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
|
|
507
|
+
throw new TypeError(`Unsupported store format for "${store}".`);
|
|
508
|
+
}
|
|
509
|
+
return metadata;
|
|
510
|
+
}
|
|
511
|
+
async function readProfileStore(profile, store) {
|
|
512
|
+
const [metadata, raw] = await Promise.all([
|
|
513
|
+
readStoreMetadata(profile, store),
|
|
514
|
+
readJson(profileStorePath(profile, store))
|
|
515
|
+
]);
|
|
516
|
+
return {
|
|
517
|
+
formatVersion: metadata.formatVersion,
|
|
518
|
+
records: Array.isArray(raw) ? raw : []
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
async function withProfileLock(profile, action, options = {}) {
|
|
522
|
+
const normalizedProfile = validateProfileName(profile);
|
|
523
|
+
const lockPath = profileLockPath(normalizedProfile);
|
|
524
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
525
|
+
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
526
|
+
const acquired = await acquireProfileLock(normalizedProfile, options);
|
|
527
|
+
const ownership = {
|
|
528
|
+
lockPath,
|
|
529
|
+
turnSlot: acquired.turnSlot,
|
|
530
|
+
deletedWhileWaiting: acquired.deletedWhileWaiting,
|
|
531
|
+
active: true
|
|
532
|
+
};
|
|
533
|
+
try {
|
|
534
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
535
|
+
} finally {
|
|
536
|
+
ownership.active = false;
|
|
537
|
+
await acquired.release();
|
|
538
|
+
if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function refuseWriteToDeletedProfile(profile) {
|
|
542
|
+
const lockPath = profileLockPath(profile);
|
|
543
|
+
const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
|
|
544
|
+
if (!ownership?.deletedWhileWaiting) return;
|
|
545
|
+
if (await exists(profileConfigPath(profile))) return;
|
|
546
|
+
throw new ProfileDeletedError(profile);
|
|
547
|
+
}
|
|
548
|
+
async function updateProfileStore(profile, store, update, options = {}) {
|
|
549
|
+
return withProfileLock(
|
|
550
|
+
profile,
|
|
551
|
+
() => updateProfileStoreWhileLocked(profile, store, update),
|
|
552
|
+
options
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
async function updateProfileStoreWhileLocked(profile, store, update) {
|
|
556
|
+
await refuseWriteToDeletedProfile(profile);
|
|
557
|
+
const current = await readProfileStore(profile, store);
|
|
558
|
+
const next = await update(current.records);
|
|
559
|
+
await writeJsonAtomic(profileStorePath(profile, store), next.records);
|
|
560
|
+
await writeFormatOneMetadata(profile, store);
|
|
561
|
+
return next.result;
|
|
562
|
+
}
|
|
563
|
+
async function writeFormatOneMetadata(profile, store) {
|
|
564
|
+
await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
|
|
565
|
+
}
|
|
566
|
+
async function acquireProfileLock(profile, options) {
|
|
567
|
+
const deadline = {
|
|
568
|
+
startedAt: Date.now(),
|
|
569
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS,
|
|
570
|
+
retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
|
|
571
|
+
};
|
|
572
|
+
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
573
|
+
const lockPath = profileLockPath(profile);
|
|
574
|
+
const turnDirectory = profileTurnLockPath(profile);
|
|
575
|
+
const directory = profilePath(profile);
|
|
576
|
+
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
577
|
+
await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
578
|
+
for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
|
|
579
|
+
await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
|
|
580
|
+
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
|
|
584
|
+
try {
|
|
585
|
+
const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
|
|
586
|
+
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
587
|
+
while (true) {
|
|
588
|
+
const token = randomUUID();
|
|
589
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
590
|
+
return {
|
|
591
|
+
deletedWhileWaiting,
|
|
592
|
+
turnSlot: turn.slot,
|
|
593
|
+
release: async () => {
|
|
594
|
+
try {
|
|
595
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
596
|
+
} finally {
|
|
597
|
+
await releaseTurn(profile, turn);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
await signalTestLockContention(profile);
|
|
603
|
+
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
604
|
+
await waitOrTimeOut(profile, deadline);
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
await releaseTurn(profile, turn);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function checkDeadline(profile, deadline, detail) {
|
|
612
|
+
if (Date.now() - deadline.startedAt >= deadline.timeoutMs) {
|
|
613
|
+
throw new ProfileLockTimeoutError(profile, deadline.timeoutMs, detail);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function waitOrTimeOut(profile, deadline, detail) {
|
|
617
|
+
checkDeadline(profile, deadline, detail);
|
|
618
|
+
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
619
|
+
}
|
|
620
|
+
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
621
|
+
var _a;
|
|
622
|
+
var relinquishedTurns = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Set());
|
|
623
|
+
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
624
|
+
var SETTLE_RETRY_MS = 1e3;
|
|
625
|
+
var SETTLE_RETRIES = 300;
|
|
626
|
+
async function acquireTurn(profile, directory, deadline) {
|
|
627
|
+
let firstWaitedTurn;
|
|
628
|
+
let problem;
|
|
629
|
+
const damaged = (what) => `The turn lock ${directory} ${what}, so no process can take it. If no tc or MCP process is using profile "${profile}", remove that directory.`;
|
|
630
|
+
for (let first = true; ; first = false) {
|
|
631
|
+
if (!first) checkDeadline(profile, deadline, problem);
|
|
632
|
+
problem = void 0;
|
|
633
|
+
const latest = await readLatestTurn(directory);
|
|
634
|
+
if (latest === "missing") {
|
|
635
|
+
await createTurnDirectory(directory);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (latest === "empty") {
|
|
639
|
+
problem = damaged("has no turns");
|
|
640
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (latest === null) continue;
|
|
644
|
+
if (typeof latest === "number") {
|
|
645
|
+
problem = damaged(`has an unreadable turn ${latest}`);
|
|
646
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
650
|
+
if (latest.held && latest.done) {
|
|
651
|
+
const turn = await takeTurn(profile, directory, latest);
|
|
652
|
+
if (turn !== null) return { turn, firstWaitedTurn };
|
|
653
|
+
await waitOrTimeOut(profile, deadline);
|
|
654
|
+
} else if (latest.done) {
|
|
655
|
+
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
656
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
657
|
+
} else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
|
|
658
|
+
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
659
|
+
await settleAbandonedTurn(directory, latest);
|
|
660
|
+
relinquishedTurns.delete(latest.token);
|
|
661
|
+
} else {
|
|
662
|
+
await signalTestLockContention(profile);
|
|
663
|
+
await waitOrTimeOut(profile, deadline);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function readLatestTurn(directory) {
|
|
668
|
+
let names;
|
|
669
|
+
try {
|
|
670
|
+
names = await readdir(directory);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (isErrno(error, "ENOENT")) return "missing";
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
const slots = names.filter((name) => TURN.test(name)).map(Number);
|
|
676
|
+
if (slots.length === 0) return "empty";
|
|
677
|
+
const slot = Math.max(...slots);
|
|
678
|
+
const turn = await readTurn(directory, slot);
|
|
679
|
+
return turn === "damaged" ? slot : turn;
|
|
680
|
+
}
|
|
681
|
+
async function readTurn(directory, slot) {
|
|
682
|
+
const path = join(directory, String(slot));
|
|
683
|
+
let text;
|
|
684
|
+
try {
|
|
685
|
+
text = await readFile(join(path, "owner.json"), "utf8");
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (!isErrno(error, "ENOENT") && !isErrno(error, "ENOTDIR")) throw error;
|
|
688
|
+
return await exists(path) ? "damaged" : null;
|
|
689
|
+
}
|
|
690
|
+
let owner;
|
|
691
|
+
try {
|
|
692
|
+
owner = JSON.parse(text);
|
|
693
|
+
} catch {
|
|
694
|
+
return "damaged";
|
|
695
|
+
}
|
|
696
|
+
if (owner === null || typeof owner !== "object" || typeof owner.token !== "string" || typeof owner.pid !== "number") {
|
|
697
|
+
return "damaged";
|
|
698
|
+
}
|
|
699
|
+
const done = await exists(join(path, `${owner.token}.done`));
|
|
700
|
+
const held = await exists(join(path, `${owner.token}.held`));
|
|
701
|
+
return {
|
|
702
|
+
slot,
|
|
703
|
+
token: owner.token,
|
|
704
|
+
pid: owner.pid,
|
|
705
|
+
after: typeof owner.after === "string" ? owner.after : null,
|
|
706
|
+
held,
|
|
707
|
+
done
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async function isFinishedTurn(directory, slot, token) {
|
|
711
|
+
const turn = await readTurn(directory, slot);
|
|
712
|
+
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
713
|
+
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
714
|
+
}
|
|
715
|
+
async function takeTurn(profile, directory, latest) {
|
|
716
|
+
const token = randomUUID();
|
|
717
|
+
const slot = latest.slot + 1;
|
|
718
|
+
const path = join(directory, String(slot));
|
|
719
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
720
|
+
let published = false;
|
|
721
|
+
try {
|
|
722
|
+
published = await publishDirectory(
|
|
723
|
+
join(directory, `.stage-${process.pid}-${token}`),
|
|
724
|
+
path,
|
|
725
|
+
{ "owner.json": `${JSON.stringify(owner, null, 2)}
|
|
726
|
+
` },
|
|
727
|
+
() => waitForTestBarrier(TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, profile)
|
|
728
|
+
);
|
|
729
|
+
if (published && await isFinishedTurn(directory, latest.slot, latest.token) && await markTurn(path, token, "held")) {
|
|
730
|
+
return { directory, slot, token };
|
|
731
|
+
}
|
|
732
|
+
if (published) await markTurn(path, token, "done");
|
|
733
|
+
return null;
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (published) settleOwnTurn(directory, slot, token);
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function settleAbandonedTurn(directory, abandoned) {
|
|
740
|
+
const path = join(directory, String(abandoned.slot));
|
|
741
|
+
if (!abandoned.held && abandoned.after !== null && await isFinishedTurn(directory, abandoned.slot - 1, abandoned.after)) {
|
|
742
|
+
await markTurn(path, abandoned.token, "held");
|
|
743
|
+
}
|
|
744
|
+
await markTurn(path, abandoned.token, "done");
|
|
745
|
+
}
|
|
746
|
+
function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
|
|
747
|
+
relinquishedTurns.add(token);
|
|
748
|
+
void (async () => {
|
|
749
|
+
try {
|
|
750
|
+
const turn = await readTurn(directory, slot);
|
|
751
|
+
if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
|
|
752
|
+
relinquishedTurns.delete(token);
|
|
753
|
+
} catch {
|
|
754
|
+
if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
}
|
|
758
|
+
async function markTurn(path, token, kind) {
|
|
759
|
+
for (let attempt = 0; ; attempt++) {
|
|
760
|
+
try {
|
|
761
|
+
await writeFile(join(path, `${token}.${kind}`), "", { mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
762
|
+
return true;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (isErrno(error, "EEXIST")) return true;
|
|
765
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
766
|
+
if (attempt >= MARKER_RETRY_DELAYS_MS.length) throw error;
|
|
767
|
+
await sleep(MARKER_RETRY_DELAYS_MS[attempt]);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function createTurnDirectory(directory) {
|
|
772
|
+
const token = randomUUID();
|
|
773
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
774
|
+
await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
|
|
775
|
+
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
776
|
+
`,
|
|
777
|
+
[`0/${token}.held`]: "",
|
|
778
|
+
[`0/${token}.done`]: ""
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
async function publishDirectory(stage, target, files, beforeRename) {
|
|
782
|
+
try {
|
|
783
|
+
for (const [name, contents] of Object.entries(files)) {
|
|
784
|
+
await mkdir(dirname(join(stage, name)), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
785
|
+
await writeFileDurably(join(stage, name), contents);
|
|
786
|
+
}
|
|
787
|
+
await beforeRename?.();
|
|
788
|
+
await rename(stage, target);
|
|
789
|
+
return true;
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (["ENOTEMPTY", "EEXIST", "ENOENT"].some((code) => isErrno(error, code)) || await exists(target)) return false;
|
|
792
|
+
throw error;
|
|
793
|
+
} finally {
|
|
794
|
+
await rm(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function releaseTurn(profile, turn) {
|
|
798
|
+
try {
|
|
799
|
+
await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
|
|
800
|
+
} catch {
|
|
801
|
+
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
await collectTurns(profile, turn).catch(() => void 0);
|
|
805
|
+
}
|
|
806
|
+
function isAbandonedTurnEntry(name) {
|
|
807
|
+
if (name.startsWith(".trash-")) return true;
|
|
808
|
+
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
809
|
+
return writer !== void 0 && !isProcessAlive(Number(writer));
|
|
810
|
+
}
|
|
811
|
+
function isAbandonedTurnDirectoryStage(name) {
|
|
812
|
+
const writer = /^\.stage-.+-([0-9]+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(name)?.[1];
|
|
813
|
+
return writer !== void 0 && !isProcessAlive(Number(writer));
|
|
814
|
+
}
|
|
815
|
+
async function collectTurns(profile, turn) {
|
|
816
|
+
for (const [directory, abandoned] of [
|
|
817
|
+
[turn.directory, isAbandonedTurnEntry],
|
|
818
|
+
[dirname(turn.directory), isAbandonedTurnDirectoryStage]
|
|
819
|
+
]) {
|
|
820
|
+
for (const name of (await readdir(directory)).filter(abandoned)) {
|
|
821
|
+
await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
|
|
825
|
+
for (const slot of older) {
|
|
826
|
+
const candidate = await readTurn(turn.directory, slot);
|
|
827
|
+
if (candidate === null || candidate === "damaged" || !candidate.done) continue;
|
|
828
|
+
if (candidate.held) {
|
|
829
|
+
if (await exists(join(turn.directory, String(slot - 1)))) continue;
|
|
830
|
+
const next = await readTurn(turn.directory, slot + 1);
|
|
831
|
+
if (next === null || next === "damaged" || !next.held || !next.done) continue;
|
|
832
|
+
}
|
|
833
|
+
await waitForTestBarrier(TEST_LOCK_TURN_COLLECT_BARRIER_DIR, profile);
|
|
834
|
+
const trash = join(turn.directory, `.trash-${randomUUID()}`);
|
|
835
|
+
if (await rename(join(turn.directory, String(slot)), trash).then(() => true, () => false)) {
|
|
836
|
+
await rm(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
841
|
+
try {
|
|
842
|
+
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
845
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
846
|
+
await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
850
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
851
|
+
const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
|
|
852
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
853
|
+
`;
|
|
854
|
+
try {
|
|
855
|
+
await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
856
|
+
try {
|
|
857
|
+
await link(staged, ownerPath);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (!lacksHardLinks(error)) throw error;
|
|
860
|
+
await writeFile(ownerPath, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
865
|
+
await rmdir(lockPath).catch(() => void 0);
|
|
866
|
+
throw error;
|
|
867
|
+
} finally {
|
|
868
|
+
await rm(staged, { force: true }).catch(() => void 0);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
var NO_HARD_LINK_ERRORS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EISDIR", "EMLINK"]);
|
|
872
|
+
function lacksHardLinks(error) {
|
|
873
|
+
return typeof error === "object" && error !== null && "code" in error && NO_HARD_LINK_ERRORS.has(String(error.code));
|
|
874
|
+
}
|
|
875
|
+
async function releaseProfileLock(profile, lockPath, token) {
|
|
876
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
877
|
+
const owner = await readJson(ownerPath).catch(() => null);
|
|
878
|
+
if (owner?.token !== token) return;
|
|
879
|
+
await rm(ownerPath, { force: true });
|
|
880
|
+
await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
|
|
881
|
+
await rmdir(lockPath).catch(() => void 0);
|
|
882
|
+
}
|
|
883
|
+
async function signalTestLockContention(profile) {
|
|
884
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
885
|
+
const configuredPath = process.env[TEST_LOCK_CONTENTION_SIGNAL_PATH];
|
|
886
|
+
const configuredHome = process.env.TC_HOME;
|
|
887
|
+
if (!configuredPath || !configuredHome) return;
|
|
888
|
+
const home = resolve(configuredHome);
|
|
889
|
+
const signalPath = resolve(configuredPath);
|
|
890
|
+
const pathFromHome = relative(home, signalPath);
|
|
891
|
+
if (pathFromHome === "" || pathFromHome === ".." || pathFromHome.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || pathFromHome.startsWith("../") || pathFromHome.startsWith("..\\")) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
await writeFile(signalPath, `${profile}
|
|
895
|
+
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
896
|
+
}
|
|
897
|
+
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
898
|
+
let aged;
|
|
899
|
+
try {
|
|
900
|
+
const { mtimeMs } = await stat(lockPath);
|
|
901
|
+
aged = Date.now() - mtimeMs >= staleAfterMs;
|
|
902
|
+
} catch {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
const checkedAt = performance.now();
|
|
906
|
+
await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
|
|
907
|
+
try {
|
|
908
|
+
const entries = await readdir(lockPath);
|
|
909
|
+
if (entries.includes("owner.json")) return false;
|
|
910
|
+
const onlyOrphanedClaims = entries.length > 0 && entries.every((entry) => RECOVERY_CLAIM.test(entry));
|
|
911
|
+
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
912
|
+
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
913
|
+
const claimPath = join(lockPath, name);
|
|
914
|
+
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
915
|
+
if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
await rm(claimPath, { force: true });
|
|
919
|
+
}
|
|
920
|
+
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
921
|
+
await rm(join(lockPath, name), { force: true });
|
|
922
|
+
}
|
|
923
|
+
await rmdir(lockPath);
|
|
924
|
+
} catch {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
async function hasLiveRecoveryOwner(claimPath) {
|
|
931
|
+
let contents;
|
|
932
|
+
try {
|
|
933
|
+
contents = await readFile(claimPath, "utf8");
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
let owner;
|
|
939
|
+
try {
|
|
940
|
+
owner = JSON.parse(contents);
|
|
941
|
+
} catch {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
|
|
945
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
|
|
946
|
+
}
|
|
947
|
+
function recoveryFenced(since, staleAfterMs) {
|
|
948
|
+
return performance.now() - since >= staleAfterMs / 2;
|
|
949
|
+
}
|
|
950
|
+
var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
951
|
+
var RECOVERY_CLAIM = /^\.recover-[0-9a-f-]+\.json$/;
|
|
952
|
+
var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
953
|
+
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
954
|
+
const directory = dirname(lockPath);
|
|
955
|
+
const names = await readdir(directory).catch(() => []);
|
|
956
|
+
for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
|
|
957
|
+
const path = join(directory, name);
|
|
958
|
+
const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
|
|
959
|
+
if (aged) await rm(path, { force: true }).catch(() => void 0);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
963
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
964
|
+
const observed = await readFile(ownerPath, "utf8").catch(() => null);
|
|
965
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
966
|
+
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
967
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
968
|
+
const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
|
|
969
|
+
let moved = false;
|
|
970
|
+
try {
|
|
971
|
+
await link(ownerPath, claimPath);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (!lacksHardLinks(error)) return false;
|
|
974
|
+
try {
|
|
975
|
+
await rename(ownerPath, claimPath);
|
|
976
|
+
moved = true;
|
|
977
|
+
} catch {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (moved) await waitForTestBarrier(TEST_LOCK_MOVED_BARRIER_DIR, profile);
|
|
982
|
+
const sameInstance = await readFile(claimPath, "utf8").then((claimed) => claimed === observed, () => false);
|
|
983
|
+
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
984
|
+
if (!sameInstance) {
|
|
985
|
+
if (moved) {
|
|
986
|
+
await rename(claimPath, ownerPath).catch(() => void 0);
|
|
987
|
+
} else {
|
|
988
|
+
await unlink(claimPath).catch(() => void 0);
|
|
989
|
+
}
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
if (!moved) await rm(ownerPath, { force: true });
|
|
993
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
994
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
995
|
+
for (const name of (await readdir(lockPath).catch(() => [])).filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
996
|
+
if (join(lockPath, name) !== claimPath) await rm(join(lockPath, name), { force: true });
|
|
997
|
+
}
|
|
998
|
+
if (await unlink(claimPath).then(() => true, () => false)) await rmdir(lockPath).catch(() => void 0);
|
|
999
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
1003
|
+
let owner;
|
|
1004
|
+
try {
|
|
1005
|
+
owner = JSON.parse(observed);
|
|
1006
|
+
} catch {
|
|
1007
|
+
owner = null;
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
1010
|
+
return isStaleOwner(owner, staleAfterMs);
|
|
1011
|
+
}
|
|
1012
|
+
const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
1013
|
+
return Date.now() - modifiedAt >= staleAfterMs;
|
|
1014
|
+
}
|
|
1015
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
1016
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
1017
|
+
const barrierDirectory = process.env[environmentName];
|
|
1018
|
+
if (!barrierDirectory) return;
|
|
1019
|
+
await mkdir(barrierDirectory, { recursive: true });
|
|
1020
|
+
const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
1021
|
+
await writeFile(readyPath, "ready\n", { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
1022
|
+
const releasePath = join(barrierDirectory, "release");
|
|
1023
|
+
while (true) {
|
|
1024
|
+
try {
|
|
1025
|
+
await readFile(releasePath, "utf8");
|
|
1026
|
+
return;
|
|
1027
|
+
} catch {
|
|
1028
|
+
await sleep(1);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function isStaleOwner(owner, staleAfterMs) {
|
|
1033
|
+
const now = Date.now();
|
|
1034
|
+
const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
|
|
1035
|
+
if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
|
|
1036
|
+
if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
return !isProcessAlive(owner.pid);
|
|
1040
|
+
}
|
|
1041
|
+
async function exists(path) {
|
|
1042
|
+
try {
|
|
1043
|
+
await stat(path);
|
|
1044
|
+
return true;
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR")) return false;
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function isProcessAlive(pid) {
|
|
1051
|
+
try {
|
|
1052
|
+
process.kill(pid, 0);
|
|
1053
|
+
return true;
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
return !isErrno(error, "ESRCH");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function validateProfileName(profile) {
|
|
1059
|
+
if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
|
|
1060
|
+
throw new TypeError("Profile names must be non-empty path segments.");
|
|
1061
|
+
}
|
|
1062
|
+
return profile;
|
|
1063
|
+
}
|
|
1064
|
+
function isErrno(error, code) {
|
|
1065
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1066
|
+
}
|
|
1067
|
+
function sleep(milliseconds) {
|
|
1068
|
+
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/artifacts.ts
|
|
1072
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1073
|
+
var PermissionEntrySchema = z.object({
|
|
1074
|
+
service: z.string().min(1),
|
|
1075
|
+
space: z.string().min(1).optional(),
|
|
1076
|
+
path: z.string(),
|
|
1077
|
+
actions: z.array(z.string().min(1)).min(1),
|
|
1078
|
+
caveats: z.array(z.record(z.unknown())).optional(),
|
|
1079
|
+
skipPrefix: z.boolean().optional(),
|
|
1080
|
+
expiry: z.string().min(1).optional(),
|
|
1081
|
+
description: z.string().optional()
|
|
1082
|
+
}).strict();
|
|
1083
|
+
var CommandSchema = z.object({
|
|
1084
|
+
argv: z.array(z.string()).min(1),
|
|
1085
|
+
cwd: z.string().min(1)
|
|
1086
|
+
}).strict();
|
|
1087
|
+
var LegacyPermissionRequestArtifactSchema = z.object({
|
|
1088
|
+
kind: z.literal("tinycloud.auth.request"),
|
|
1089
|
+
version: z.literal(1),
|
|
1090
|
+
requestId: z.string(),
|
|
1091
|
+
sessionDid: z.string().min(1),
|
|
1092
|
+
requested: z.array(PermissionEntrySchema),
|
|
1093
|
+
requestedExpiry: z.union([z.string(), z.number().finite()]).optional()
|
|
1094
|
+
}).strict();
|
|
1095
|
+
var PortableDelegationSchema = z.lazy(() => z.object({
|
|
1096
|
+
cid: z.string().min(1),
|
|
1097
|
+
spaceId: z.string().min(1),
|
|
1098
|
+
path: z.string(),
|
|
1099
|
+
actions: z.array(z.string().min(1)).min(1),
|
|
1100
|
+
delegateDID: z.string().min(1),
|
|
1101
|
+
ownerAddress: z.string().min(1),
|
|
1102
|
+
chainId: z.number().int().positive(),
|
|
1103
|
+
expiry: z.union([z.string().datetime({ offset: true }), z.date()]),
|
|
1104
|
+
delegationHeader: z.object({ Authorization: z.string().min(1) }).strict(),
|
|
1105
|
+
host: z.string().min(1).optional(),
|
|
1106
|
+
disableSubDelegation: z.boolean().optional(),
|
|
1107
|
+
publicDelegation: PortableDelegationSchema.optional(),
|
|
1108
|
+
resources: z.array(PermissionEntrySchema).optional()
|
|
1109
|
+
}).passthrough());
|
|
1110
|
+
var PermissionRequestArtifactSchema = z.object({
|
|
1111
|
+
kind: z.literal("tinycloud.auth.request"),
|
|
1112
|
+
version: z.literal(1),
|
|
1113
|
+
requestId: z.string().min(1),
|
|
1114
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
1115
|
+
profile: z.string().min(1),
|
|
1116
|
+
posture: z.enum(["owner-openkey", "delegate-session", "local-owner-key", "unauthenticated"]),
|
|
1117
|
+
operatorType: z.enum(["human", "agent"]),
|
|
1118
|
+
host: z.string().min(1),
|
|
1119
|
+
sessionDid: z.string().min(1),
|
|
1120
|
+
ownerDid: z.string().min(1).optional(),
|
|
1121
|
+
spaceId: z.string().min(1).optional(),
|
|
1122
|
+
requestedExpiry: z.union([z.string().min(1), z.number().finite()]).optional(),
|
|
1123
|
+
requested: z.array(PermissionEntrySchema),
|
|
1124
|
+
command: CommandSchema.optional()
|
|
1125
|
+
}).strict();
|
|
1126
|
+
var DelegationImportArtifactSchema = z.object({
|
|
1127
|
+
kind: z.literal("tinycloud.auth.delegation"),
|
|
1128
|
+
version: z.literal(1),
|
|
1129
|
+
requestId: z.string().min(1),
|
|
1130
|
+
delegationCid: z.string().min(1).optional(),
|
|
1131
|
+
delegation: PortableDelegationSchema,
|
|
1132
|
+
// This field is presentation metadata only. A later import operation obtains
|
|
1133
|
+
// effective permissions from validated, installed delegation authority.
|
|
1134
|
+
permissions: z.array(PermissionEntrySchema).optional(),
|
|
1135
|
+
expiry: z.string().datetime({ offset: true }).optional(),
|
|
1136
|
+
prompted: z.boolean().optional()
|
|
1137
|
+
}).strict();
|
|
1138
|
+
var DelegationRequestBindingSchema = PermissionRequestArtifactSchema.pick({ requestId: true, requested: true }).strict();
|
|
1139
|
+
|
|
1140
|
+
// src/secrets.ts
|
|
1141
|
+
import * as nodeSdk from "@tinycloud/node-sdk";
|
|
1142
|
+
|
|
1143
|
+
// src/errors.ts
|
|
1144
|
+
function operationError(code, message, options = {}) {
|
|
1145
|
+
return {
|
|
1146
|
+
code,
|
|
1147
|
+
message,
|
|
1148
|
+
retryable: options.retryable ?? false,
|
|
1149
|
+
...options.details === void 0 ? {} : { details: options.details }
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
var OperationInvocationError = class extends Error {
|
|
1153
|
+
constructor(operationError2) {
|
|
1154
|
+
super(operationError2.message);
|
|
1155
|
+
this.name = "OperationInvocationError";
|
|
1156
|
+
this.operationError = operationError2;
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// src/secrets.ts
|
|
1161
|
+
function operationSpaceResolver(node, ownerSpace) {
|
|
1162
|
+
return (space) => resolveOperationSpace(space, node, ownerSpace);
|
|
1163
|
+
}
|
|
1164
|
+
function resolveOperationSpace(space, node, ownerSpace) {
|
|
1165
|
+
if (space.startsWith("tinycloud:")) return canonicalizeSpaceUri(space);
|
|
1166
|
+
const ownerSpaceId = ownerSpace === void 0 ? void 0 : ownedSpaceForName(ownerSpace, space);
|
|
1167
|
+
if (ownerSpaceId !== void 0) return canonicalizeSpaceUri(ownerSpaceId);
|
|
1168
|
+
const candidate = node;
|
|
1169
|
+
try {
|
|
1170
|
+
const resolved = candidate.resolveOwnedSpace?.(space);
|
|
1171
|
+
if (typeof resolved === "string" && resolved.length > 0) {
|
|
1172
|
+
return canonicalizeSpaceUri(resolved);
|
|
1173
|
+
}
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof candidate.spaceId === "string") {
|
|
1177
|
+
const resolved = ownedSpaceForName(candidate.spaceId, space);
|
|
1178
|
+
if (resolved !== void 0) return canonicalizeSpaceUri(resolved);
|
|
1179
|
+
}
|
|
1180
|
+
const did = typeof candidate.did === "string" ? candidate.did : void 0;
|
|
1181
|
+
const pkh = did === void 0 ? null : nodeSdk.parsePkhDid(did);
|
|
1182
|
+
if (pkh === null) {
|
|
1183
|
+
throw invalidSecretInput();
|
|
1184
|
+
}
|
|
1185
|
+
return nodeSdk.makePkhSpaceId(pkh.address, pkh.chainId, space);
|
|
1186
|
+
}
|
|
1187
|
+
function ownedSpaceForName(ownerSpace, name) {
|
|
1188
|
+
const parsed = nodeSdk.parseSpaceUri(ownerSpace);
|
|
1189
|
+
if (parsed === null) return void 0;
|
|
1190
|
+
try {
|
|
1191
|
+
const pkh = nodeSdk.parsePkhDid(parsed.owner);
|
|
1192
|
+
return pkh === null ? void 0 : nodeSdk.makePkhSpaceId(pkh.address, pkh.chainId, name);
|
|
1193
|
+
} catch {
|
|
1194
|
+
return void 0;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function canonicalizeSpaceUri(space) {
|
|
1198
|
+
const parts = space.split(":");
|
|
1199
|
+
const name = parts.at(-1);
|
|
1200
|
+
if (parts.length < 3 || name === void 0 || name === "") {
|
|
1201
|
+
throw invalidSecretInput();
|
|
1202
|
+
}
|
|
1203
|
+
const pkh = space.match(
|
|
1204
|
+
/^tinycloud:pkh:eip155:(\d+):(0x[0-9a-fA-F]{40}):([^:]+)$/
|
|
1205
|
+
);
|
|
1206
|
+
if (pkh !== null) {
|
|
1207
|
+
return `tinycloud:pkh:eip155:${pkh[1]}:${nodeSdk.canonicalizeAddress(pkh[2]).toLowerCase()}:${pkh[3]}`;
|
|
1208
|
+
}
|
|
1209
|
+
return space;
|
|
1210
|
+
}
|
|
1211
|
+
function invalidSecretInput() {
|
|
1212
|
+
return new OperationInvocationError(
|
|
1213
|
+
operationError("INPUT_INVALID", "The secret reference is invalid.")
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/delegation-binding.ts
|
|
1218
|
+
var REQUEST_ID_PREFIX = {
|
|
1219
|
+
migration: "migrated",
|
|
1220
|
+
"unbound-import": "unbound-import"
|
|
1221
|
+
};
|
|
1222
|
+
var BINDING_NOTES = {
|
|
1223
|
+
migration: "Stored without a request binding before bindings were required. Bound at migration to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them.",
|
|
1224
|
+
"unbound-import": "Imported by `tc auth import` without a stored request. Bound to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them."
|
|
1225
|
+
};
|
|
1226
|
+
var MIGRATION_LOCK_TIMEOUT_MS = 250;
|
|
1227
|
+
function bindingMigrationPath(profile) {
|
|
1228
|
+
return join2(profilePath(profile), "delegation-binding-migration.json");
|
|
1229
|
+
}
|
|
1230
|
+
function storedDelegationKind(entry) {
|
|
1231
|
+
if (!isRecord(entry)) return "refused";
|
|
1232
|
+
const delegation = entry.delegation;
|
|
1233
|
+
if (!isRecord(delegation)) return "refused";
|
|
1234
|
+
const header = delegation.delegationHeader;
|
|
1235
|
+
if (!isRecord(header) || Object.keys(header).length !== 1 || !Object.prototype.hasOwnProperty.call(header, "Authorization") || typeof header.Authorization !== "string") {
|
|
1236
|
+
return "refused";
|
|
1237
|
+
}
|
|
1238
|
+
if ("siweProof" in delegation) return "signed-login";
|
|
1239
|
+
if (header.Authorization.includes(".")) return "compact";
|
|
1240
|
+
return "authorityRequest" in entry ? "refused" : "other";
|
|
1241
|
+
}
|
|
1242
|
+
function replayLimit(entry, migrated) {
|
|
1243
|
+
const kind = storedDelegationKind(entry);
|
|
1244
|
+
if (!isRecord(entry) || kind !== "compact" && kind !== "signed-login") return void 0;
|
|
1245
|
+
const binding = DelegationRequestBindingSchema.safeParse(entry.authorityRequest);
|
|
1246
|
+
if (binding.success) return binding.data.requested;
|
|
1247
|
+
return !migrated && kind === "compact" && !("authorityRequest" in entry) ? "signed" : void 0;
|
|
1248
|
+
}
|
|
1249
|
+
function bindToSignedCapabilities(record, signed, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1250
|
+
return {
|
|
1251
|
+
...record,
|
|
1252
|
+
authorityRequest: DelegationRequestBindingSchema.parse({
|
|
1253
|
+
requestId: `${REQUEST_ID_PREFIX[source]}:${record.delegation.cid}`,
|
|
1254
|
+
requested: structuredClone(signed)
|
|
1255
|
+
}),
|
|
1256
|
+
authorityRequestAudit: { source, recordedAt, note: BINDING_NOTES[source] }
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
async function activateUnboundCompactImport(node, delegation, host) {
|
|
1260
|
+
const activated = await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host });
|
|
1261
|
+
return bindToSignedCapabilities(
|
|
1262
|
+
// The record keeps the delegation as imported; replay validates it again.
|
|
1263
|
+
{ delegation, permissions: [...activated.effectivePermissions] },
|
|
1264
|
+
activated.effectivePermissions,
|
|
1265
|
+
"unbound-import"
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
function mergeDelegationsWithoutRequest(stored, incoming) {
|
|
1269
|
+
let records = [...stored];
|
|
1270
|
+
for (const record of incoming) {
|
|
1271
|
+
const cid = record.delegation.cid;
|
|
1272
|
+
if (records.some((entry) => storedCid(entry) === cid && isRecord(entry) && "authorityRequest" in entry)) continue;
|
|
1273
|
+
const position = records.findIndex((entry) => storedCid(entry) === cid);
|
|
1274
|
+
const others = records.filter((entry) => storedCid(entry) !== cid);
|
|
1275
|
+
records = position === -1 ? [...records, record] : [...others.slice(0, position), record, ...others.slice(position)];
|
|
1276
|
+
}
|
|
1277
|
+
return records;
|
|
1278
|
+
}
|
|
1279
|
+
async function prepareStoredDelegationReplay(profile, node, options) {
|
|
1280
|
+
if (await bindingMigrationRecorded(profile)) return true;
|
|
1281
|
+
if (!options.migrate || !await holdsPersistedSession(profile, node)) return false;
|
|
1282
|
+
try {
|
|
1283
|
+
return await withProfileLock(profile, async () => {
|
|
1284
|
+
if (await bindingMigrationRecorded(profile)) return true;
|
|
1285
|
+
if (!await holdsPersistedSession(profile, node)) return false;
|
|
1286
|
+
const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1287
|
+
const bound = /* @__PURE__ */ new Map();
|
|
1288
|
+
const unbound = [];
|
|
1289
|
+
for (const entry of await readAdditionalDelegations(profile)) {
|
|
1290
|
+
if (!isRecord(entry) || "authorityRequest" in entry || storedDelegationKind(entry) !== "compact") continue;
|
|
1291
|
+
const migrated = await migratedRecord(node, entry, options.host, recordedAt);
|
|
1292
|
+
if (migrated === void 0) unbound.push(String(storedCid(entry)));
|
|
1293
|
+
else bound.set(recordKey(entry), migrated);
|
|
1294
|
+
}
|
|
1295
|
+
if (bound.size > 0) {
|
|
1296
|
+
await updateProfileStore(profile, "additional-delegations", (records) => ({
|
|
1297
|
+
records: records.map((entry) => {
|
|
1298
|
+
const key = !isRecord(entry) || "authorityRequest" in entry ? void 0 : recordKey(entry);
|
|
1299
|
+
return (key === void 0 ? void 0 : bound.get(key)) ?? entry;
|
|
1300
|
+
}),
|
|
1301
|
+
result: void 0
|
|
1302
|
+
}));
|
|
1303
|
+
}
|
|
1304
|
+
await writeJsonAtomic(bindingMigrationPath(profile), {
|
|
1305
|
+
formatVersion: 1,
|
|
1306
|
+
migratedAt: recordedAt,
|
|
1307
|
+
bound: [...bound.values()].map(storedCid),
|
|
1308
|
+
unbound
|
|
1309
|
+
});
|
|
1310
|
+
return true;
|
|
1311
|
+
}, { timeoutMs: MIGRATION_LOCK_TIMEOUT_MS });
|
|
1312
|
+
} catch {
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async function holdsPersistedSession(profile, node) {
|
|
1317
|
+
try {
|
|
1318
|
+
const persisted = await readSession(profile);
|
|
1319
|
+
return typeof persisted?.verificationMethod === "string" && typeof node.sessionDid === "string" && persisted.verificationMethod.split("#", 1)[0] === node.sessionDid.split("#", 1)[0];
|
|
1320
|
+
} catch {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function migratedRecord(node, entry, host, recordedAt) {
|
|
1325
|
+
const delegation = normalizeStoredDelegation(entry);
|
|
1326
|
+
if (delegation === void 0) return void 0;
|
|
1327
|
+
const signed = await signedCapabilities(node, delegation, host);
|
|
1328
|
+
if (signed === void 0) return void 0;
|
|
1329
|
+
try {
|
|
1330
|
+
return bindToSignedCapabilities({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
|
|
1331
|
+
} catch {
|
|
1332
|
+
return void 0;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function replayStoredDelegation(node, entry, options) {
|
|
1336
|
+
const delegation = normalizeStoredDelegation(entry);
|
|
1337
|
+
if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) return void 0;
|
|
1338
|
+
const limit = replayLimit(entry, options.migrated);
|
|
1339
|
+
if (limit === void 0) return void 0;
|
|
1340
|
+
const authorize = limit === "signed" ? () => true : (effective) => delegationWithinRequest(limit, effective, options.resolveSpace);
|
|
1341
|
+
try {
|
|
1342
|
+
return await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize });
|
|
1343
|
+
} catch {
|
|
1344
|
+
return void 0;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async function bindingMigrationRecorded(profile) {
|
|
1348
|
+
try {
|
|
1349
|
+
return await readJson(bindingMigrationPath(profile)) !== null;
|
|
1350
|
+
} catch {
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function signedCapabilities(node, delegation, host) {
|
|
1355
|
+
let signed;
|
|
1356
|
+
try {
|
|
1357
|
+
await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, {
|
|
1358
|
+
// The transport host is not signed authority; check it at replay instead.
|
|
1359
|
+
host: delegation.host ?? host,
|
|
1360
|
+
authorize: (effective) => {
|
|
1361
|
+
signed = effective;
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
return signed;
|
|
1368
|
+
}
|
|
1369
|
+
function normalizeStoredDelegation(entry) {
|
|
1370
|
+
if (!isRecord(entry)) return void 0;
|
|
1371
|
+
const raw = entry.delegation;
|
|
1372
|
+
if (!isRecord(raw) || !isRecord(raw.delegationHeader)) return void 0;
|
|
1373
|
+
const expiry = raw.expiry instanceof Date ? raw.expiry : typeof raw.expiry === "string" ? new Date(raw.expiry) : void 0;
|
|
1374
|
+
if (expiry === void 0 || Number.isNaN(expiry.getTime()) || typeof raw.cid !== "string" || typeof raw.spaceId !== "string" || typeof raw.path !== "string" || !Array.isArray(raw.actions) || !raw.actions.every((action) => typeof action === "string") || typeof raw.delegateDID !== "string" || typeof raw.ownerAddress !== "string" || typeof raw.chainId !== "number" || typeof raw.delegationHeader.Authorization !== "string") {
|
|
1375
|
+
return void 0;
|
|
1376
|
+
}
|
|
1377
|
+
return { ...raw, expiry };
|
|
1378
|
+
}
|
|
1379
|
+
function storedCid(entry) {
|
|
1380
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
1381
|
+
return typeof cid === "string" ? cid : void 0;
|
|
1382
|
+
}
|
|
1383
|
+
function recordKey(entry) {
|
|
1384
|
+
const delegation = entry.delegation;
|
|
1385
|
+
if (!isRecord(delegation) || !isRecord(delegation.delegationHeader)) return void 0;
|
|
1386
|
+
return JSON.stringify([delegation.cid, delegation.delegationHeader.Authorization]);
|
|
1387
|
+
}
|
|
1388
|
+
function isRecord(value) {
|
|
1389
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1390
|
+
}
|
|
1391
|
+
export {
|
|
1392
|
+
BINDING_NOTES,
|
|
1393
|
+
activateUnboundCompactImport,
|
|
1394
|
+
bindingMigrationPath,
|
|
1395
|
+
bindingMigrationRecorded,
|
|
1396
|
+
mergeDelegationsWithoutRequest,
|
|
1397
|
+
operationSpaceResolver,
|
|
1398
|
+
prepareStoredDelegationReplay,
|
|
1399
|
+
replayLimit,
|
|
1400
|
+
replayStoredDelegation,
|
|
1401
|
+
storedDelegationKind
|
|
1402
|
+
};
|
|
1403
|
+
//# sourceMappingURL=delegation-binding.js.map
|