@tinycloud/operations 0.6.1-beta.3 → 0.6.1-beta.30

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/profile.js CHANGED
@@ -1,15 +1,16 @@
1
1
  // src/state.ts
2
2
  import { randomUUID } from "crypto";
3
3
  import { AsyncLocalStorage } from "async_hooks";
4
- import { readFileSync, readlinkSync } from "fs";
4
+ import { constants as fsConstants, fstatSync, readFileSync, readlinkSync } from "fs";
5
5
  import {
6
6
  chmod,
7
7
  link,
8
- lstat,
9
8
  mkdir,
9
+ lstat,
10
10
  open,
11
11
  readFile,
12
12
  readdir,
13
+ realpath,
13
14
  rename,
14
15
  rmdir,
15
16
  rm,
@@ -21,15 +22,20 @@ import { connect, createServer } from "net";
21
22
  import { homedir } from "os";
22
23
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
23
24
  var DEFAULT_PROFILE = "default";
25
+ var PRIVATE_DIR_MODE = 448;
24
26
  function processWideContext(key) {
25
- var _a3;
27
+ var _a4;
26
28
  const registry = globalThis;
27
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
29
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
28
30
  }
29
31
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
30
32
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
33
+ var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
34
+ var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
31
35
  var _a;
32
- var processInstanceId = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a] = randomUUID());
36
+ var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
37
+ var _a2;
38
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
33
39
  function tinycloudHomePath() {
34
40
  const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
35
41
  return join(home, ".tinycloud");
@@ -40,12 +46,9 @@ function tinycloudConfigPath() {
40
46
  function profilesPath() {
41
47
  return join(tinycloudHomePath(), "profiles");
42
48
  }
43
- function profilePath(profile) {
49
+ function namedProfilePath(profile) {
44
50
  return join(profilesPath(), validateProfileName(profile));
45
51
  }
46
- function profileConfigPath(profile) {
47
- return join(profilePath(profile), "profile.json");
48
- }
49
52
  async function readJson(filePath) {
50
53
  try {
51
54
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -54,11 +57,159 @@ async function readJson(filePath) {
54
57
  throw error;
55
58
  }
56
59
  }
60
+ async function readProfileJson(profile, filePath) {
61
+ const before = process.platform !== "linux" ? await lstat(filePath, { bigint: true }).catch((error) => {
62
+ if (error.code === "ENOENT") return null;
63
+ throw error;
64
+ }) : null;
65
+ if (process.platform !== "linux" && !before) return null;
66
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
67
+ const dev = before && exactFileId(before.dev);
68
+ const ino = before && exactFileId(before.ino);
69
+ try {
70
+ const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
71
+ try {
72
+ if (before) {
73
+ const opened = await handle.stat({ bigint: true });
74
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
75
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
76
+ }
77
+ }
78
+ const contents = await handle.readFile("utf8");
79
+ if (before) {
80
+ const named = await lstat(filePath, { bigint: true }).catch((error) => {
81
+ if (error.code === "ENOENT") return null;
82
+ throw error;
83
+ });
84
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
85
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
86
+ }
87
+ }
88
+ return JSON.parse(contents);
89
+ } finally {
90
+ await handle.close();
91
+ }
92
+ } catch (error) {
93
+ if (isErrno(error, "ENOENT")) return null;
94
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
95
+ throw error;
96
+ }
97
+ }
57
98
  async function readProfile(profile) {
58
- return readJson(profileConfigPath(profile));
99
+ return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, join(directory, "profile.json")) : Promise.resolve(null));
59
100
  }
60
- var _a2;
61
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
101
+ var ProfileSymlinkError = class extends Error {
102
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
103
+ super(`Profile "${profile}" ${detail}.`);
104
+ this.code = "PROFILE_SYMLINK";
105
+ this.name = "ProfileSymlinkError";
106
+ }
107
+ };
108
+ function exactFileId(value) {
109
+ if (typeof value === "bigint") return value >= 0n ? value : null;
110
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
111
+ }
112
+ function isPinnedLive(pinned) {
113
+ if (!pinned.active) return false;
114
+ try {
115
+ const stats = fstatSync(pinned.fd, { bigint: true });
116
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+ function livePinnedPath(profile, pinned) {
122
+ if (isPinnedLive(pinned)) return pinned.path;
123
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
124
+ }
125
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
126
+ const named = namedProfilePath(profile);
127
+ const existing = pinnedProfiles.getStore()?.get(named);
128
+ if (existing) return action(livePinnedPath(profile, existing));
129
+ if (process.platform !== "linux") {
130
+ if (options.create) await mkdir(named, { recursive: true, mode: PRIVATE_DIR_MODE });
131
+ const stats = await lstat(named).catch((error) => {
132
+ if (error.code === "ENOENT") return null;
133
+ throw error;
134
+ });
135
+ if (!stats) return action(null);
136
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
137
+ return action(named);
138
+ }
139
+ const home = tinycloudHomePath();
140
+ const handles = [];
141
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
142
+ const flags = fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW;
143
+ try {
144
+ if (options.create) await mkdir(home, { recursive: true, mode: PRIVATE_DIR_MODE });
145
+ const resolved = canonicalHomes.get(home) ?? await realpath(home).catch((error) => {
146
+ if (error.code === "ENOENT" && !options.create) return null;
147
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
148
+ throw error;
149
+ });
150
+ if (!resolved) return await action(null);
151
+ const root = canonicalHomes.get(home) ?? resolved;
152
+ canonicalHomes.set(home, root);
153
+ const segments = [root, "profiles", validateProfileName(profile)];
154
+ for (let index = 0; index < segments.length; index++) {
155
+ const path = handles.length ? join(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
156
+ if (options.create && handles.length) {
157
+ await mkdir(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
158
+ if (error.code !== "EEXIST") throw error;
159
+ });
160
+ }
161
+ const handle = await open(path, index === 1 ? flags & ~fsConstants.O_NOFOLLOW : flags).catch((error) => {
162
+ if (error.code === "ENOENT" && !options.create) return null;
163
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
164
+ throw error;
165
+ });
166
+ if (!handle) return await action(null);
167
+ handles.push(handle);
168
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
169
+ }
170
+ const opened = handles[2];
171
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), lstat(named, { bigint: true })]);
172
+ const dev = exactFileId(actual.dev);
173
+ const ino = exactFileId(actual.ino);
174
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
175
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
176
+ }
177
+ const directory = fdPath(opened.fd);
178
+ const pinned = {
179
+ path: directory,
180
+ fd: opened.fd,
181
+ dev,
182
+ ino,
183
+ active: true
184
+ };
185
+ const homeHandle = handles[0];
186
+ const homeStats = await homeHandle.stat({ bigint: true });
187
+ const homeDev = exactFileId(homeStats.dev);
188
+ const homeIno = exactFileId(homeStats.ino);
189
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
190
+ const homePinned = {
191
+ path: fdPath(homeHandle.fd),
192
+ fd: homeHandle.fd,
193
+ dev: homeDev,
194
+ ino: homeIno,
195
+ active: true
196
+ };
197
+ const context = new Map(pinnedProfiles.getStore());
198
+ context.set(named, pinned);
199
+ const homes = new Map(pinnedHomes.getStore());
200
+ homes.set(home, homePinned);
201
+ try {
202
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
203
+ } finally {
204
+ pinned.active = false;
205
+ homePinned.active = false;
206
+ }
207
+ } finally {
208
+ for (const handle of handles.reverse()) await handle.close();
209
+ }
210
+ }
211
+ var _a3;
212
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
62
213
  function validateProfileName(profile) {
63
214
  if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
64
215
  throw new TypeError("Profile names must be non-empty path segments.");