@toon-protocol/townhouse 0.1.0-rc5 → 0.1.1

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/index.js CHANGED
@@ -1,170 +1,61 @@
1
1
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
2
  import {
3
+ BootReconciler,
4
+ ComposeLoaderError,
3
5
  ConfigValidationError,
4
6
  ConnectorAdminClient,
5
7
  ConnectorConfigGenerator,
6
8
  DEFAULT_ATOR_PROXY,
7
9
  DockerOrchestrator,
10
+ ImageManifestSchema,
11
+ NodesYamlEntrySchema,
12
+ NodesYamlSchema,
13
+ OrchestratorError,
14
+ PeerTypeResolver,
15
+ SnapshotWriter,
8
16
  TransportProbe,
9
17
  WalletManager,
10
18
  createApiServer,
19
+ createDeltaComputer,
11
20
  createWizardApiServer,
12
21
  decryptWallet,
13
22
  encryptWallet,
14
23
  getDefaultConfig,
24
+ loadComposeTemplate,
15
25
  loadConfig,
16
26
  loadWallet,
27
+ materializeComposeTemplate,
28
+ readImageManifest,
29
+ readNodesYaml,
17
30
  saveConfig,
18
31
  saveWallet,
19
- validateConfig
20
- } from "./chunk-IB6TNCUQ.js";
21
- import "./chunk-UTFWPLTB.js";
22
-
23
- // src/compose-loader.ts
24
- import {
25
- readFileSync,
26
- writeFileSync,
27
- mkdirSync,
28
- chmodSync,
29
- statSync,
30
- lstatSync,
31
- existsSync
32
- } from "fs";
33
- import { dirname, join, resolve, isAbsolute } from "path";
34
- import { fileURLToPath } from "url";
35
- import { homedir } from "os";
36
- var VALID_PROFILES = ["dev", "hs"];
37
- var ComposeLoaderError = class extends Error {
38
- constructor(message) {
39
- super(message);
40
- this.name = "ComposeLoaderError";
41
- }
42
- };
43
- function defaultDistDir() {
44
- const here = dirname(fileURLToPath(import.meta.url));
45
- return resolve(here, "..", "dist");
46
- }
47
- function assertValidProfile(profile) {
48
- if (!VALID_PROFILES.includes(profile)) {
49
- throw new ComposeLoaderError(
50
- `invalid compose profile: '${profile}'. Must be one of: ${VALID_PROFILES.join(", ")}.`
51
- );
52
- }
53
- }
54
- var SYSTEM_PATH_PREFIXES = [
55
- "/etc",
56
- "/usr",
57
- "/bin",
58
- "/sbin",
59
- "/lib",
60
- "/lib64",
61
- "/proc",
62
- "/sys",
63
- "/dev",
64
- "/boot",
65
- "/root"
66
- ];
67
- function assertValidTownhouseHome(home) {
68
- if (!home) {
69
- throw new ComposeLoaderError(
70
- "townhouseHome resolved to an empty path. Set $HOME or pass options.townhouseHome explicitly."
71
- );
72
- }
73
- if (!isAbsolute(home)) {
74
- throw new ComposeLoaderError(
75
- `townhouseHome must be an absolute path; got '${home}'.`
76
- );
77
- }
78
- if (home === "/" || home === "\\") {
79
- throw new ComposeLoaderError(
80
- `townhouseHome must not be the filesystem root; got '${home}'. This usually means $HOME is unset and homedir() returned '/'.`
81
- );
82
- }
83
- for (const prefix of SYSTEM_PATH_PREFIXES) {
84
- if (home === prefix || home.startsWith(prefix + "/")) {
85
- throw new ComposeLoaderError(
86
- `townhouseHome must not target a system directory; got '${home}'. Allowed paths: under $HOME, under tmpdir(), or any user-writable location.`
87
- );
88
- }
89
- }
90
- }
91
- function assertNotSymlink(filePath) {
92
- try {
93
- const lst = lstatSync(filePath);
94
- if (lst.isSymbolicLink()) {
95
- throw new ComposeLoaderError(
96
- `${filePath} is a symlink; refusing to write through it. If this is intentional, remove the symlink and re-run.`
97
- );
98
- }
99
- } catch (err) {
100
- const code = err.code;
101
- if (code !== "ENOENT") throw err;
102
- }
103
- }
104
- function loadComposeTemplate(profile, options = {}) {
105
- assertValidProfile(profile);
106
- const distDir = options.distDir ?? defaultDistDir();
107
- const composePath = join(distDir, "compose", `townhouse-${profile}.yml`);
108
- if (!existsSync(composePath)) {
109
- throw new ComposeLoaderError(
110
- `compose template not found: ${composePath}. Did you run 'pnpm --filter @toon-protocol/townhouse build' first?`
111
- );
112
- }
113
- return readFileSync(composePath, "utf-8");
114
- }
115
- function materializeComposeTemplate(profile, options = {}) {
116
- assertValidProfile(profile);
117
- const home = options.townhouseHome || join(homedir(), ".townhouse");
118
- assertValidTownhouseHome(home);
119
- const distDir = options.distDir ?? defaultDistDir();
120
- const manifestSrc = join(distDir, "image-manifest.json");
121
- if (profile === "hs" && !existsSync(manifestSrc)) {
122
- throw new ComposeLoaderError(
123
- `image-manifest.json not found at ${manifestSrc}. HS mode requires a digest-pinned image manifest. Reinstall @toon-protocol/townhouse from npm to restore the manifest.`
124
- );
125
- }
126
- const yaml = loadComposeTemplate(profile, options);
127
- const composeDir = join(home, "compose");
128
- mkdirSync(composeDir, { recursive: true, mode: 448 });
129
- for (const dir of [home, composeDir]) {
130
- const lst = lstatSync(dir);
131
- if (lst.isSymbolicLink()) {
132
- const target = statSync(dir);
133
- if (!target.isDirectory()) {
134
- throw new ComposeLoaderError(
135
- `${dir} is a symlink to a non-directory; refusing to materialize.`
136
- );
137
- }
138
- continue;
139
- }
140
- const currentMode = lst.mode & 511;
141
- if ((currentMode & 63) !== 0) {
142
- chmodSync(dir, 448);
143
- }
144
- }
145
- const composePath = join(composeDir, `townhouse-${profile}.yml`);
146
- assertNotSymlink(composePath);
147
- writeFileSync(composePath, yaml, { mode: 384, encoding: "utf-8" });
148
- chmodSync(composePath, 384);
149
- const manifestPath = join(home, "image-manifest.json");
150
- if (existsSync(manifestSrc)) {
151
- assertNotSymlink(manifestPath);
152
- const manifest = readFileSync(manifestSrc, "utf-8");
153
- writeFileSync(manifestPath, manifest, { mode: 384, encoding: "utf-8" });
154
- chmodSync(manifestPath, 384);
155
- }
156
- return { composePath, manifestPath };
157
- }
32
+ utcDayBoundary,
33
+ utcMonthBoundary,
34
+ utcYearBoundary,
35
+ validateConfig,
36
+ writeNodesYaml
37
+ } from "./chunk-W33MEOPM.js";
38
+ import "./chunk-5O4SBV5O.js";
39
+ import "./chunk-GQNBZJ6F.js";
40
+ import "./chunk-I2R4CRUX.js";
158
41
  export {
42
+ BootReconciler,
159
43
  ComposeLoaderError,
160
44
  ConfigValidationError,
161
45
  ConnectorAdminClient,
162
46
  ConnectorConfigGenerator,
163
47
  DEFAULT_ATOR_PROXY,
164
48
  DockerOrchestrator,
49
+ ImageManifestSchema,
50
+ NodesYamlEntrySchema,
51
+ NodesYamlSchema,
52
+ OrchestratorError,
53
+ PeerTypeResolver,
54
+ SnapshotWriter,
165
55
  TransportProbe,
166
56
  WalletManager,
167
57
  createApiServer,
58
+ createDeltaComputer,
168
59
  createWizardApiServer,
169
60
  decryptWallet,
170
61
  encryptWallet,
@@ -173,8 +64,14 @@ export {
173
64
  loadConfig,
174
65
  loadWallet,
175
66
  materializeComposeTemplate,
67
+ readImageManifest,
68
+ readNodesYaml,
176
69
  saveConfig,
177
70
  saveWallet,
178
- validateConfig
71
+ utcDayBoundary,
72
+ utcMonthBoundary,
73
+ utcYearBoundary,
74
+ validateConfig,
75
+ writeNodesYaml
179
76
  };
180
77
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/compose-loader.ts"],"sourcesContent":["import {\n readFileSync,\n writeFileSync,\n mkdirSync,\n chmodSync,\n statSync,\n lstatSync,\n existsSync,\n} from 'node:fs';\nimport { dirname, join, resolve, isAbsolute } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { homedir } from 'node:os';\n\nexport type ComposeProfile = 'dev' | 'hs';\nconst VALID_PROFILES: readonly ComposeProfile[] = ['dev', 'hs'] as const;\n\nexport interface ComposeLoaderOptions {\n /** Override default `~/.townhouse/` write target. Used by tests. */\n townhouseHome?: string;\n /** Override the package-relative dist directory the loader reads from.\n * Defaults to the `dist/` adjacent to compose-loader.js at runtime.\n * Tests use this to point at fixture directories. */\n distDir?: string;\n}\n\nexport class ComposeLoaderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ComposeLoaderError';\n }\n}\n\nfunction defaultDistDir(): string {\n // Resolves to `dist/` adjacent to the bundled output at runtime.\n // When bundled by tsup, import.meta.url is the path of dist/index.js,\n // so dirname = <package>/dist. resolve(<package>/dist, '..', 'dist') = <package>/dist.\n // When running via tsx/ts-node from src/, dirname = <package>/src,\n // so resolve(<package>/src, '..', 'dist') = <package>/dist. Both work.\n const here = dirname(fileURLToPath(import.meta.url));\n return resolve(here, '..', 'dist');\n}\n\nfunction assertValidProfile(\n profile: string\n): asserts profile is ComposeProfile {\n if (!(VALID_PROFILES as readonly string[]).includes(profile)) {\n throw new ComposeLoaderError(\n `invalid compose profile: '${profile}'. Must be one of: ${VALID_PROFILES.join(', ')}.`\n );\n }\n}\n\n// Reject townhouseHome paths that target system directories. Internal callers\n// (CLI, API, Story 45.4) pass `~/.townhouse` or test tmpdirs; an attacker\n// reaching this code path with `townhouseHome: '/etc'` would otherwise write\n// `/etc/compose/townhouse-hs.yml` and `chmod /etc 0o700`. The list is a\n// belt-and-suspenders defense — it doesn't replace caller validation, but it\n// turns a silent privilege escalation into a loud error.\nconst SYSTEM_PATH_PREFIXES = [\n '/etc',\n '/usr',\n '/bin',\n '/sbin',\n '/lib',\n '/lib64',\n '/proc',\n '/sys',\n '/dev',\n '/boot',\n '/root',\n] as const;\n\nfunction assertValidTownhouseHome(home: string): void {\n if (!home) {\n throw new ComposeLoaderError(\n 'townhouseHome resolved to an empty path. Set $HOME or pass options.townhouseHome explicitly.'\n );\n }\n if (!isAbsolute(home)) {\n throw new ComposeLoaderError(\n `townhouseHome must be an absolute path; got '${home}'.`\n );\n }\n if (home === '/' || home === '\\\\') {\n throw new ComposeLoaderError(\n `townhouseHome must not be the filesystem root; got '${home}'. ` +\n `This usually means $HOME is unset and homedir() returned '/'.`\n );\n }\n for (const prefix of SYSTEM_PATH_PREFIXES) {\n if (home === prefix || home.startsWith(prefix + '/')) {\n throw new ComposeLoaderError(\n `townhouseHome must not target a system directory; got '${home}'. ` +\n `Allowed paths: under $HOME, under tmpdir(), or any user-writable location.`\n );\n }\n }\n}\n\n// Refuse to write through a symlink at composePath/manifestPath. The dir-level\n// guard above only protects the directory itself; this protects the file path.\nfunction assertNotSymlink(filePath: string): void {\n try {\n const lst = lstatSync(filePath);\n if (lst.isSymbolicLink()) {\n throw new ComposeLoaderError(\n `${filePath} is a symlink; refusing to write through it. ` +\n `If this is intentional, remove the symlink and re-run.`\n );\n }\n } catch (err) {\n // ENOENT is expected (file doesn't exist yet — fresh write); rethrow others.\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== 'ENOENT') throw err;\n }\n}\n\n/**\n * Returns the rendered compose YAML for the requested profile.\n * For 'hs', digest substitutions are already applied (resolved at build time).\n * For 'dev', the YAML is returned verbatim (uses local `toon:*` image tags).\n * Throws `ComposeLoaderError` if the requested profile's YAML is unreadable.\n */\nexport function loadComposeTemplate(\n profile: ComposeProfile,\n options: ComposeLoaderOptions = {}\n): string {\n assertValidProfile(profile);\n const distDir = options.distDir ?? defaultDistDir();\n const composePath = join(distDir, 'compose', `townhouse-${profile}.yml`);\n if (!existsSync(composePath)) {\n throw new ComposeLoaderError(\n `compose template not found: ${composePath}. ` +\n `Did you run 'pnpm --filter @toon-protocol/townhouse build' first?`\n );\n }\n return readFileSync(composePath, 'utf-8');\n}\n\n/**\n * Writes the resolved compose YAML to `<townhouseHome>/compose/<profile>.yml`\n * and copies `dist/image-manifest.json` to `<townhouseHome>/image-manifest.json`.\n * BOTH output files are written with mode 0o600 (NFR8 — operator-secret file mode).\n * Returns the absolute paths of the two files written.\n */\nexport function materializeComposeTemplate(\n profile: ComposeProfile,\n options: ComposeLoaderOptions = {}\n): { composePath: string; manifestPath: string } {\n assertValidProfile(profile);\n const home = options.townhouseHome || join(homedir(), '.townhouse');\n assertValidTownhouseHome(home);\n\n const distDir = options.distDir ?? defaultDistDir();\n const manifestSrc = join(distDir, 'image-manifest.json');\n\n // Validate inputs BEFORE any writes so a failure leaves disk untouched.\n // HS profile cannot succeed without a manifest — fail loudly up-front\n // rather than after writing a stale/torn compose file.\n if (profile === 'hs' && !existsSync(manifestSrc)) {\n throw new ComposeLoaderError(\n `image-manifest.json not found at ${manifestSrc}. ` +\n `HS mode requires a digest-pinned image manifest. ` +\n `Reinstall @toon-protocol/townhouse from npm to restore the manifest.`\n );\n }\n // loadComposeTemplate also throws ENOENT if the source is missing — surface\n // it now (read-only) so we don't mkdir or chmod for a doomed call.\n const yaml = loadComposeTemplate(profile, options);\n\n const composeDir = join(home, 'compose');\n // Pass mode: 0o700 so newly-created intermediates start tight (closes the\n // mkdir → chmod TOCTOU window that allowed brief world-readable state).\n mkdirSync(composeDir, { recursive: true, mode: 0o700 });\n\n // Refuse to chmod symlink targets — operator may have placed `~/.townhouse`\n // as a symlink to an encrypted volume, and we should not silently flip the\n // mode of a path we did not create. lstatSync inspects the link itself.\n for (const dir of [home, composeDir]) {\n const lst = lstatSync(dir);\n if (lst.isSymbolicLink()) {\n // Resolve the link target and confirm it's a directory; do not chmod.\n const target = statSync(dir);\n if (!target.isDirectory()) {\n throw new ComposeLoaderError(\n `${dir} is a symlink to a non-directory; refusing to materialize.`\n );\n }\n continue;\n }\n // Only narrow the mode if it is currently broader than 0o700. Operators\n // who deliberately set 0o700 OR tighter (e.g. 0o500) keep their setting.\n // Bug fix R2: previous `!== 0o700` widened 0o500 to 0o700 — now we only\n // chmod if the existing mode grants any permission outside the owner.\n const currentMode = lst.mode & 0o777;\n if ((currentMode & 0o077) !== 0) {\n chmodSync(dir, 0o700);\n }\n }\n\n const composePath = join(composeDir, `townhouse-${profile}.yml`);\n // R2 file-symlink guard — refuse to write through a planted symlink.\n assertNotSymlink(composePath);\n writeFileSync(composePath, yaml, { mode: 0o600, encoding: 'utf-8' });\n // Defensive re-chmod: writeFileSync's mode option is honored only on file\n // creation — if composePath already existed at e.g. 0o644 (stale state from\n // a prior interrupted run), the mode is unchanged by writeFileSync.\n // chmodSync corrects both that case AND the WSL2 umask-masking edge case.\n chmodSync(composePath, 0o600);\n\n const manifestPath = join(home, 'image-manifest.json');\n if (existsSync(manifestSrc)) {\n assertNotSymlink(manifestPath);\n const manifest = readFileSync(manifestSrc, 'utf-8');\n writeFileSync(manifestPath, manifest, { mode: 0o600, encoding: 'utf-8' });\n chmodSync(manifestPath, 0o600);\n }\n // (Manifest absence for 'dev' profile is silently tolerated — dev mode\n // doesn't need digest pinning. HS profile already failed at the entry guard.)\n\n return { composePath, manifestPath };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}