@tinycloud/operations 0.6.1-beta.0 → 0.6.1-beta.10

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.js CHANGED
@@ -151,6 +151,7 @@ import { resolveSecretPath, SECRET_NAME_RE } from "@tinycloud/sdk-services";
151
151
  var DEFAULT_MANIFEST_SPACE = "applications";
152
152
  var SERVICE_SHORT_TO_LONG = Object.freeze({
153
153
  kv: "tinycloud.kv",
154
+ space: "tinycloud.space",
154
155
  sql: "tinycloud.sql",
155
156
  duckdb: "tinycloud.duckdb",
156
157
  capabilities: "tinycloud.capabilities",
@@ -381,15 +382,16 @@ function canonicalizeCaveats(caveats) {
381
382
  // src/state.ts
382
383
  import { randomUUID } from "crypto";
383
384
  import { AsyncLocalStorage } from "async_hooks";
384
- import { readFileSync, readlinkSync } from "fs";
385
+ import { constants as fsConstants, fstatSync, readFileSync, readlinkSync } from "fs";
385
386
  import {
386
387
  chmod,
387
388
  link,
388
- lstat,
389
389
  mkdir,
390
+ lstat,
390
391
  open,
391
392
  readFile,
392
393
  readdir,
394
+ realpath,
393
395
  rename,
394
396
  rmdir,
395
397
  rm,
@@ -419,14 +421,18 @@ var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIE
419
421
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
420
422
  var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
421
423
  function processWideContext(key) {
422
- var _a3;
424
+ var _a4;
423
425
  const registry = globalThis;
424
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
426
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
425
427
  }
426
428
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
427
429
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
430
+ var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
431
+ var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
428
432
  var _a;
429
- var processInstanceId = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a] = randomUUID());
433
+ var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
434
+ var _a2;
435
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
430
436
  var cachedProcessOrigin;
431
437
  function processOrigin() {
432
438
  if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
@@ -604,9 +610,14 @@ function tinycloudHomePath() {
604
610
  function profilesPath() {
605
611
  return join(tinycloudHomePath(), "profiles");
606
612
  }
607
- function profilePath(profile) {
613
+ function namedProfilePath(profile) {
608
614
  return join(profilesPath(), validateProfileName(profile));
609
615
  }
616
+ function profilePath(profile) {
617
+ const named = namedProfilePath(profile);
618
+ const pinned = pinnedProfiles.getStore()?.get(named);
619
+ return pinned ? livePinnedPath(profile, pinned) : named;
620
+ }
610
621
  function profileConfigPath(profile) {
611
622
  return join(profilePath(profile), "profile.json");
612
623
  }
@@ -617,10 +628,12 @@ function profileStoreMetadataPath(profile, store) {
617
628
  return `${profileStorePath(profile, store)}.metadata.json`;
618
629
  }
619
630
  function profileLockPath(profile) {
620
- return join(profilePath(profile), ".lock");
631
+ return join(namedProfilePath(profile), ".lock");
621
632
  }
622
633
  function profileTurnLockPath(profile) {
623
- return join(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
634
+ const home = tinycloudHomePath();
635
+ const pinned = pinnedHomes.getStore()?.get(home);
636
+ return join(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
624
637
  }
625
638
  async function readJson(filePath) {
626
639
  try {
@@ -630,6 +643,44 @@ async function readJson(filePath) {
630
643
  throw error;
631
644
  }
632
645
  }
646
+ async function readProfileJson(profile, filePath) {
647
+ const before = process.platform !== "linux" ? await lstat(filePath, { bigint: true }).catch((error) => {
648
+ if (error.code === "ENOENT") return null;
649
+ throw error;
650
+ }) : null;
651
+ if (process.platform !== "linux" && !before) return null;
652
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
653
+ const dev = before && exactFileId(before.dev);
654
+ const ino = before && exactFileId(before.ino);
655
+ try {
656
+ const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
657
+ try {
658
+ if (before) {
659
+ const opened = await handle.stat({ bigint: true });
660
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
661
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
662
+ }
663
+ }
664
+ const contents = await handle.readFile("utf8");
665
+ if (before) {
666
+ const named = await lstat(filePath, { bigint: true }).catch((error) => {
667
+ if (error.code === "ENOENT") return null;
668
+ throw error;
669
+ });
670
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
671
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
672
+ }
673
+ }
674
+ return JSON.parse(contents);
675
+ } finally {
676
+ await handle.close();
677
+ }
678
+ } catch (error) {
679
+ if (isErrno(error, "ENOENT")) return null;
680
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
681
+ throw error;
682
+ }
683
+ }
633
684
  async function writeJsonAtomic(filePath, value) {
634
685
  await replaceJson(filePath, value, false);
635
686
  }
@@ -666,43 +717,194 @@ async function readAuthRequests(profile) {
666
717
  return (await readProfileStore(profile, "auth-requests")).records;
667
718
  }
668
719
  async function readStoreMetadata(profile, store) {
669
- const metadata = await readJson(profileStoreMetadataPath(profile, store));
670
- if (metadata === null) return { formatVersion: 1 };
671
- if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
672
- throw new TypeError(`Unsupported store format for "${store}".`);
673
- }
674
- return metadata;
720
+ return withPinnedProfileDirectory(profile, async (directory) => {
721
+ const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
722
+ if (metadata === null) return { formatVersion: 1 };
723
+ if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
724
+ throw new TypeError(`Unsupported store format for "${store}".`);
725
+ }
726
+ return metadata;
727
+ });
675
728
  }
676
729
  async function readProfileStore(profile, store) {
677
- const [metadata, raw] = await Promise.all([
678
- readStoreMetadata(profile, store),
679
- readJson(profileStorePath(profile, store))
680
- ]);
681
- return {
682
- formatVersion: metadata.formatVersion,
683
- records: Array.isArray(raw) ? raw : []
684
- };
730
+ return withPinnedProfileDirectory(profile, async (directory) => {
731
+ if (!directory) return { formatVersion: 1, records: [] };
732
+ const [metadata, raw] = await Promise.all([
733
+ readStoreMetadata(profile, store),
734
+ readProfileJson(profile, profileStorePath(profile, store))
735
+ ]);
736
+ return {
737
+ formatVersion: metadata.formatVersion,
738
+ records: Array.isArray(raw) ? raw : []
739
+ };
740
+ });
685
741
  }
686
- async function withProfileLock(profile, action, options = {}) {
687
- const normalizedProfile = validateProfileName(profile);
688
- const lockPath = profileLockPath(normalizedProfile);
689
- const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
690
- if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
691
- const acquired = await acquireProfileLock(normalizedProfile, options);
692
- const ownership = {
693
- lockPath,
694
- turnSlot: acquired.turnSlot,
695
- deletedWhileWaiting: acquired.deletedWhileWaiting,
696
- active: true
697
- };
742
+ var ProfileSymlinkError = class extends Error {
743
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
744
+ super(`Profile "${profile}" ${detail}.`);
745
+ this.code = "PROFILE_SYMLINK";
746
+ this.name = "ProfileSymlinkError";
747
+ }
748
+ };
749
+ function exactFileId(value) {
750
+ if (typeof value === "bigint") return value >= 0n ? value : null;
751
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
752
+ }
753
+ function isPinnedLive(pinned) {
754
+ if (!pinned.active) return false;
698
755
  try {
699
- return await heldProfileLocks.run([...held, ownership], action);
756
+ const stats = fstatSync(pinned.fd, { bigint: true });
757
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
758
+ } catch {
759
+ return false;
760
+ }
761
+ }
762
+ function livePinnedPath(profile, pinned) {
763
+ if (isPinnedLive(pinned)) return pinned.path;
764
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
765
+ }
766
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
767
+ const named = namedProfilePath(profile);
768
+ const existing = pinnedProfiles.getStore()?.get(named);
769
+ if (existing) return action(livePinnedPath(profile, existing));
770
+ if (process.platform !== "linux") {
771
+ if (options.create) await mkdir(named, { recursive: true, mode: PRIVATE_DIR_MODE });
772
+ const stats = await lstat(named).catch((error) => {
773
+ if (error.code === "ENOENT") return null;
774
+ throw error;
775
+ });
776
+ if (!stats) return action(null);
777
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
778
+ return action(named);
779
+ }
780
+ const home = tinycloudHomePath();
781
+ const handles = [];
782
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
783
+ const flags = fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW;
784
+ try {
785
+ if (options.create) await mkdir(home, { recursive: true, mode: PRIVATE_DIR_MODE });
786
+ const resolved = canonicalHomes.get(home) ?? await realpath(home).catch((error) => {
787
+ if (error.code === "ENOENT" && !options.create) return null;
788
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
789
+ throw error;
790
+ });
791
+ if (!resolved) return await action(null);
792
+ const root = canonicalHomes.get(home) ?? resolved;
793
+ canonicalHomes.set(home, root);
794
+ const segments = [root, "profiles", validateProfileName(profile)];
795
+ for (let index = 0; index < segments.length; index++) {
796
+ const path = handles.length ? join(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
797
+ if (options.create && handles.length) {
798
+ await mkdir(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
799
+ if (error.code !== "EEXIST") throw error;
800
+ });
801
+ }
802
+ const handle = await open(path, index === 1 ? flags & ~fsConstants.O_NOFOLLOW : flags).catch((error) => {
803
+ if (error.code === "ENOENT" && !options.create) return null;
804
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
805
+ throw error;
806
+ });
807
+ if (!handle) return await action(null);
808
+ handles.push(handle);
809
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
810
+ }
811
+ const opened = handles[2];
812
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), lstat(named, { bigint: true })]);
813
+ const dev = exactFileId(actual.dev);
814
+ const ino = exactFileId(actual.ino);
815
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
816
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
817
+ }
818
+ const directory = fdPath(opened.fd);
819
+ const pinned = {
820
+ path: directory,
821
+ fd: opened.fd,
822
+ dev,
823
+ ino,
824
+ active: true
825
+ };
826
+ const homeHandle = handles[0];
827
+ const homeStats = await homeHandle.stat({ bigint: true });
828
+ const homeDev = exactFileId(homeStats.dev);
829
+ const homeIno = exactFileId(homeStats.ino);
830
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
831
+ const homePinned = {
832
+ path: fdPath(homeHandle.fd),
833
+ fd: homeHandle.fd,
834
+ dev: homeDev,
835
+ ino: homeIno,
836
+ active: true
837
+ };
838
+ const context = new Map(pinnedProfiles.getStore());
839
+ context.set(named, pinned);
840
+ const homes = new Map(pinnedHomes.getStore());
841
+ homes.set(home, homePinned);
842
+ try {
843
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
844
+ } finally {
845
+ pinned.active = false;
846
+ homePinned.active = false;
847
+ }
700
848
  } finally {
701
- ownership.active = false;
702
- await acquired.release();
703
- if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
849
+ for (const handle of handles.reverse()) await handle.close();
704
850
  }
705
851
  }
852
+ async function assertRealProfileDirectory(profile) {
853
+ const named = namedProfilePath(profile);
854
+ const pinned = pinnedProfiles.getStore()?.get(named);
855
+ if (pinned) {
856
+ livePinnedPath(profile, pinned);
857
+ return;
858
+ }
859
+ const stats = await lstat(named).catch((error) => {
860
+ if (error.code === "ENOENT") return null;
861
+ throw error;
862
+ });
863
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
864
+ }
865
+ async function withProfileLock(profile, action, options = {}) {
866
+ const normalizedProfile = validateProfileName(profile);
867
+ const lockPath = profileLockPath(normalizedProfile);
868
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
869
+ if (held.some((ownership) => ownership.lockPath === lockPath)) {
870
+ await assertRealProfileDirectory(normalizedProfile);
871
+ return action();
872
+ }
873
+ const named = namedProfilePath(normalizedProfile);
874
+ const inherited = pinnedProfiles.getStore()?.get(named);
875
+ if (inherited && !isPinnedLive(inherited)) {
876
+ const profiles = new Map(pinnedProfiles.getStore());
877
+ profiles.delete(named);
878
+ const homes = new Map(pinnedHomes.getStore());
879
+ homes.delete(tinycloudHomePath());
880
+ return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
881
+ }
882
+ return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
883
+ if (!directory) throw new ProfileDeletedError(normalizedProfile);
884
+ const acquired = await acquireProfileLock(normalizedProfile, options);
885
+ const ownership = {
886
+ lockPath,
887
+ turnSlot: acquired.turnSlot,
888
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
889
+ active: true
890
+ };
891
+ try {
892
+ return await heldProfileLocks.run([...held, ownership], action);
893
+ } finally {
894
+ ownership.active = false;
895
+ await acquired.release();
896
+ if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
897
+ const current = await lstat(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
898
+ const original = await stat(directory, { bigint: true }).catch(() => null);
899
+ const currentDev = current && exactFileId(current.dev);
900
+ const currentIno = current && exactFileId(current.ino);
901
+ if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
902
+ await rmdir(namedProfilePath(normalizedProfile)).catch(() => void 0);
903
+ }
904
+ }
905
+ }
906
+ }, { create: !options.requireProfile });
907
+ }
706
908
  async function refuseWriteToDeletedProfile(profile) {
707
909
  const lockPath = profileLockPath(profile);
708
910
  const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
@@ -735,17 +937,15 @@ async function acquireProfileLock(profile, options) {
735
937
  retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
736
938
  };
737
939
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
738
- const lockPath = profileLockPath(profile);
940
+ const lockPath = join(profilePath(profile), ".lock");
739
941
  const turnDirectory = profileTurnLockPath(profile);
740
942
  const directory = profilePath(profile);
943
+ await assertRealProfileDirectory(profile);
741
944
  if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
742
945
  if (!options.requireProfile) await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
946
+ await assertRealProfileDirectory(profile);
743
947
  await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
744
- for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
745
- await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
746
- if (path !== directory || !isErrno(error, "ENOENT")) throw error;
747
- });
748
- }
948
+ await chmod(directory, PRIVATE_DIR_MODE);
749
949
  const liveness = { directory: turnDirectory, held: void 0 };
750
950
  const closeAcquisitionLiveness = async () => {
751
951
  if (liveness.held) await closeLiveness(liveness.held);
@@ -765,6 +965,7 @@ async function acquireProfileLock(profile, options) {
765
965
  }
766
966
  const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
767
967
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
968
+ if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
768
969
  while (true) {
769
970
  const token = randomUUID();
770
971
  if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
@@ -801,8 +1002,8 @@ async function waitOrTimeOut(profile, deadline, detail) {
801
1002
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
802
1003
  }
803
1004
  var TURN = /^(?:0|[1-9][0-9]*)$/;
804
- var _a2;
805
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
1005
+ var _a3;
1006
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
806
1007
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
807
1008
  var SETTLE_RETRY_MS = 1e3;
808
1009
  var SETTLE_RETRIES = 300;
@@ -916,7 +1117,7 @@ async function takeTurn(profile, directory, latest, liveness) {
916
1117
  if (published) await markTurn(path, token, "done");
917
1118
  return null;
918
1119
  } catch (error) {
919
- if (published) settleOwnTurn(directory, slot, token);
1120
+ if (published) await settleOwnTurn(directory, slot, token);
920
1121
  throw error;
921
1122
  }
922
1123
  }
@@ -927,17 +1128,25 @@ async function settleAbandonedTurn(directory, abandoned) {
927
1128
  }
928
1129
  await markTurn(path, abandoned.token, "done");
929
1130
  }
930
- function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
1131
+ async function settleOwnTurn(directory, slot, token) {
931
1132
  relinquishedTurns.add(token);
932
- void (async () => {
933
- try {
934
- const turn = await readTurn(directory, slot);
935
- if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
936
- relinquishedTurns.delete(token);
937
- } catch {
938
- if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
939
- }
940
- })();
1133
+ const pinned = await pinDirectory(directory).catch(() => null);
1134
+ const path = pinned?.path(".") ?? directory;
1135
+ let retries = SETTLE_RETRIES;
1136
+ const attempt = () => {
1137
+ void (async () => {
1138
+ try {
1139
+ const turn = await readTurn(path, slot);
1140
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
1141
+ relinquishedTurns.delete(token);
1142
+ await pinned?.close();
1143
+ } catch {
1144
+ if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
1145
+ else await pinned?.close();
1146
+ }
1147
+ })();
1148
+ };
1149
+ attempt();
941
1150
  }
942
1151
  async function markTurn(path, token, kind) {
943
1152
  for (let attempt = 0; ; attempt++) {
@@ -982,7 +1191,7 @@ async function releaseTurn(profile, turn, liveness) {
982
1191
  try {
983
1192
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
984
1193
  } catch {
985
- settleOwnTurn(turn.directory, turn.slot, turn.token);
1194
+ await settleOwnTurn(turn.directory, turn.slot, turn.token);
986
1195
  return;
987
1196
  }
988
1197
  await collectTurns(profile, turn, liveness).catch(() => void 0);