@ours.network/install 1.1.0 → 1.2.0-nightly.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.
Files changed (49) hide show
  1. package/README.md +196 -6
  2. package/assets/Dockerfile +41 -0
  3. package/assets/docker-compose.yaml +222 -0
  4. package/assets/scripts/README.md +20 -0
  5. package/assets/scripts/build/README.md +39 -0
  6. package/assets/scripts/build/build-common.mjs +37 -0
  7. package/assets/scripts/build/build-cowork.mjs +2 -0
  8. package/assets/scripts/build/build-fleet.mjs +2 -0
  9. package/assets/scripts/build/build-mcp.mjs +9 -0
  10. package/assets/scripts/build/build-messenger.mjs +2 -0
  11. package/assets/scripts/build/build-sdk.mjs +9 -0
  12. package/assets/scripts/build/build-telegram.mjs +2 -0
  13. package/assets/scripts/build/build.mjs +49 -0
  14. package/assets/scripts/build/record-build.mjs +31 -0
  15. package/assets/scripts/maintenance/README.md +35 -0
  16. package/assets/scripts/maintenance/build-context.mjs +162 -0
  17. package/assets/scripts/maintenance/docker-layout-conversion.mjs +238 -0
  18. package/assets/scripts/maintenance/provenance-compare.mjs +160 -0
  19. package/assets/scripts/maintenance/state-archive.mjs +238 -0
  20. package/assets/scripts/maintenance/state-native.mjs +56 -0
  21. package/assets/scripts/maintenance/state-operation.mjs +249 -0
  22. package/assets/scripts/runtime/README.md +24 -0
  23. package/assets/scripts/runtime/check-client.mjs +21 -0
  24. package/assets/scripts/runtime/check-start.mjs +15 -0
  25. package/assets/scripts/runtime/client-setup.mjs +197 -0
  26. package/assets/scripts/runtime/entrypoint.sh +13 -0
  27. package/assets/scripts/runtime/health-cowork.sh +11 -0
  28. package/assets/scripts/runtime/health-messenger.mjs +6 -0
  29. package/assets/scripts/runtime/health-telegram.sh +8 -0
  30. package/assets/scripts/runtime/healthcheck.mjs +17 -0
  31. package/assets/scripts/runtime/runtime-common.mjs +47 -0
  32. package/assets/scripts/runtime/start-cowork.sh +6 -0
  33. package/assets/scripts/runtime/start-messenger.sh +6 -0
  34. package/assets/scripts/runtime/start-telegram.sh +6 -0
  35. package/assets/sources.json +21 -0
  36. package/install.sh +2 -1
  37. package/lib/build-transition.mjs +56 -0
  38. package/lib/docker-conversion-runtime.mjs +96 -0
  39. package/lib/docker-layout-installation.mjs +62 -0
  40. package/lib/effects.mjs +945 -11
  41. package/lib/extras.mjs +23 -68
  42. package/lib/layout-conversion.mjs +297 -0
  43. package/lib/orchestrate-uninstall.mjs +30 -1
  44. package/lib/orchestrate.mjs +265 -18
  45. package/lib/plan.mjs +194 -1
  46. package/lib/target.mjs +100 -0
  47. package/lib/usage.mjs +27 -2
  48. package/package.json +9 -2
  49. package/uninstall.sh +2 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Semantic dependency-record comparison for cross-build admission.
3
+ * Compares structured content ignoring JSON key ordering and proven
4
+ * staging-location path differences. Unknown or unmatched shapes
5
+ * fail closed — they return false with no silent acceptance.
6
+ *
7
+ * Archive self-consistency and embedded-marker comparisons remain
8
+ * byte-exact (handled by state-archive.mjs, not this module).
9
+ */
10
+
11
+ function isObject(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ function sortedKeys(obj) {
16
+ return Object.keys(obj).sort();
17
+ }
18
+
19
+ function normalizeResolved(value) {
20
+ if (typeof value !== 'string') return value;
21
+ return value;
22
+ }
23
+
24
+ function sameOptionalString(a, b) {
25
+ if (a === undefined && b === undefined) return true;
26
+ return a === b;
27
+ }
28
+
29
+ function deepEqual(a, b) {
30
+ if (a === b) return true;
31
+ if (typeof a !== typeof b) return false;
32
+ if (typeof a !== 'object' || a === null || b === null) return false;
33
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
34
+ if (Array.isArray(a)) return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
35
+ const ak = sortedKeys(a), bk = sortedKeys(b);
36
+ if (ak.length !== bk.length) return false;
37
+ return ak.every((k, i) => k === bk[i] && deepEqual(a[k], b[bk[i]]));
38
+ }
39
+
40
+ function sameOptionalObject(a, b) {
41
+ if (a === undefined && b === undefined) return true;
42
+ if (!isObject(a) || !isObject(b)) return false;
43
+ return deepEqual(a, b);
44
+ }
45
+
46
+ function sameOptionalArray(a, b) {
47
+ if (a === undefined && b === undefined) return true;
48
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
49
+ if (a.length !== b.length) return false;
50
+ for (let i = 0; i < a.length; i++) {
51
+ if (a[i] !== b[i]) return false;
52
+ }
53
+ return true;
54
+ }
55
+
56
+ const LOCKFILE_ENTRY_KNOWN = new Set([
57
+ 'version', 'integrity', 'resolved', 'name', 'license', 'link', 'dev', 'optional', 'peer',
58
+ 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies',
59
+ 'engines', 'os', 'cpu', 'bin', 'funding', 'hasInstallScript', 'inBundle',
60
+ 'devOptional', 'peerDependenciesMeta', 'bundleDependencies', 'workspaces',
61
+ ]);
62
+
63
+ function packageEntryEqual(a, b) {
64
+ if (!isObject(a) || !isObject(b)) return false;
65
+ if (a.version !== b.version) return false;
66
+ if (a.integrity !== b.integrity) return false;
67
+ if (normalizeResolved(a.resolved) !== normalizeResolved(b.resolved)) return false;
68
+ for (const key of ['name', 'license', 'dev', 'optional', 'peer', 'link', 'hasInstallScript', 'inBundle', 'devOptional']) {
69
+ if (a[key] !== b[key]) return false;
70
+ }
71
+ for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'peerDependenciesMeta']) {
72
+ if (!sameOptionalObject(a[key], b[key])) return false;
73
+ }
74
+ for (const key of ['os', 'cpu', 'bundleDependencies']) {
75
+ if (!sameOptionalArray(a[key], b[key])) return false;
76
+ }
77
+ if (!sameOptionalObject(a.engines, b.engines)) return false;
78
+ if (!sameOptionalObject(a.bin, b.bin)) return false;
79
+ if (!sameOptionalArray(a.workspaces, b.workspaces)) return false;
80
+ if (!deepEqual(a.funding, b.funding)) return false;
81
+ const aExtra = Object.keys(a).filter(k => !LOCKFILE_ENTRY_KNOWN.has(k));
82
+ const bExtra = Object.keys(b).filter(k => !LOCKFILE_ENTRY_KNOWN.has(k));
83
+ if (aExtra.length > 0 || bExtra.length > 0) return false;
84
+ return true;
85
+ }
86
+
87
+ function validateLockfileShape(obj) {
88
+ if (!isObject(obj)) return false;
89
+ if (typeof obj.lockfileVersion !== 'number') return false;
90
+ if (!isObject(obj.packages)) return false;
91
+ return true;
92
+ }
93
+
94
+ const LOCKFILE_TOP_KNOWN = new Set(['name', 'version', 'lockfileVersion', 'requires', 'packages']);
95
+
96
+ function lockfileEqual(a, b) {
97
+ if (!validateLockfileShape(a) || !validateLockfileShape(b)) return false;
98
+ if (a.lockfileVersion !== b.lockfileVersion) return false;
99
+ if (!sameOptionalString(a.name, b.name)) return false;
100
+ if (!sameOptionalString(a.version, b.version)) return false;
101
+ if (a.requires !== b.requires) return false;
102
+ const aExtra = Object.keys(a).filter(k => !LOCKFILE_TOP_KNOWN.has(k));
103
+ const bExtra = Object.keys(b).filter(k => !LOCKFILE_TOP_KNOWN.has(k));
104
+ if (aExtra.length > 0 || bExtra.length > 0) return false;
105
+ const aPkgs = a.packages, bPkgs = b.packages;
106
+ const aKeys = sortedKeys(aPkgs), bKeys = sortedKeys(bPkgs);
107
+ if (aKeys.length !== bKeys.length) return false;
108
+ for (let i = 0; i < aKeys.length; i++) {
109
+ if (aKeys[i] !== bKeys[i]) return false;
110
+ if (!packageEntryEqual(aPkgs[aKeys[i]], bPkgs[bKeys[i]])) return false;
111
+ }
112
+ return true;
113
+ }
114
+
115
+ function validateTreeShape(obj) {
116
+ if (!isObject(obj)) return false;
117
+ if (typeof obj.version !== 'string') return false;
118
+ return true;
119
+ }
120
+
121
+ const TREE_NODE_KNOWN = new Set(['version', 'resolved', 'dependencies', 'from', 'overridden', 'name']);
122
+
123
+ function treeNodeEqual(a, b) {
124
+ if (!isObject(a) || !isObject(b)) return false;
125
+ if (Object.keys(a).length === 0 && Object.keys(b).length === 0) return true;
126
+ if (typeof a.version !== 'string' || typeof b.version !== 'string') return false;
127
+ if (a.version !== b.version) return false;
128
+ if (normalizeResolved(a.resolved) !== normalizeResolved(b.resolved)) return false;
129
+ if (!sameOptionalString(a.name, b.name)) return false;
130
+ if (!sameOptionalString(a.from, b.from)) return false;
131
+ if (a.overridden !== b.overridden) return false;
132
+ const aExtra = Object.keys(a).filter(k => !TREE_NODE_KNOWN.has(k) && k !== 'dependencies');
133
+ const bExtra = Object.keys(b).filter(k => !TREE_NODE_KNOWN.has(k) && k !== 'dependencies');
134
+ if (aExtra.length > 0 || bExtra.length > 0) return false;
135
+ const aDeps = a.dependencies, bDeps = b.dependencies;
136
+ if (aDeps === undefined && bDeps === undefined) return true;
137
+ if (!isObject(aDeps) || !isObject(bDeps)) return false;
138
+ const aKeys = sortedKeys(aDeps), bKeys = sortedKeys(bDeps);
139
+ if (aKeys.length !== bKeys.length) return false;
140
+ for (let i = 0; i < aKeys.length; i++) {
141
+ if (aKeys[i] !== bKeys[i]) return false;
142
+ if (!treeNodeEqual(aDeps[aKeys[i]], bDeps[bKeys[i]])) return false;
143
+ }
144
+ return true;
145
+ }
146
+
147
+ function dependencyTreeEqual(a, b) {
148
+ if (!validateTreeShape(a) || !validateTreeShape(b)) return false;
149
+ return treeNodeEqual(a, b);
150
+ }
151
+
152
+ export function semanticRecordEqual(name, aBytes, bBytes) {
153
+ if (aBytes.equals(bBytes)) return true;
154
+ let a, b;
155
+ try { a = JSON.parse(aBytes); b = JSON.parse(bBytes); }
156
+ catch { return false; }
157
+ if (name === 'package-lock.json') return lockfileEqual(a, b);
158
+ if (name === 'dependency-tree.json') return dependencyTreeEqual(a, b);
159
+ return false;
160
+ }
@@ -0,0 +1,238 @@
1
+ /** Opaque format-1/2 archive codec. The caller excludes writers for the entire operation. */
2
+ import * as fs from 'node:fs';
3
+ import { dirname, basename, join, relative, posix } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import tar from 'tar-stream';
7
+ import { parseTree } from 'jsonc-parser';
8
+ import { publishNoReplace, setMtimeNs } from './state-native.mjs';
9
+
10
+ import { recordNames, validateBuildRecordSet, CONTEXT } from './build-context.mjs';
11
+ const archiveRecords = options => recordNames(options.provenance);
12
+ const archivePayloads = options => [...archiveRecords(options), 'state.tar'];
13
+ const archiveFiles = options => ['metadata.json', ...archivePayloads(options)];
14
+ const archiveFormat = options => Object.hasOwn(options.provenance, CONTEXT) ? 2 : 1;
15
+
16
+ /** Copy stopped state with private owner modes and exact timestamps; leave the source unchanged. */
17
+ export function copyPrivateTree(source, destination, ownership) {
18
+ const entries = scanSource(source, ownership);
19
+ let destinationExists = false;
20
+ try {
21
+ fs.lstatSync(destination);
22
+ destinationExists = true;
23
+ } catch (error) {
24
+ if (error.code !== 'ENOENT') throw error;
25
+ }
26
+ if (destinationExists) throw new Error('Copy destination already exists');
27
+ fs.cpSync(source, destination, { recursive: true });
28
+ for (const entry of entries.reverse()) {
29
+ const path = entry.name === 'state'
30
+ ? destination
31
+ : join(destination, entry.name.slice(6));
32
+ fs.chmodSync(path, entry.mode);
33
+ setMtimeNs(path, entry.st.mtimeNs);
34
+ }
35
+ }
36
+ const sameKeys = (value, keys) => value && !Array.isArray(value) && typeof value === 'object' && Object.keys(value).sort().join('\0') === [...keys].sort().join('\0');
37
+ const reject = message => { throw new Error(message); };
38
+ const exists = path => { try { fs.lstatSync(path); return true; } catch (e) { if (e.code === 'ENOENT') return false; throw e; } };
39
+ const absent = path => { if (exists(path)) throw Object.assign(new Error(`Destination exists: ${path}`), { code: 'EEXIST' }); };
40
+
41
+ function inputs({ domain, provenance, uid, gid }) {
42
+ if (typeof domain !== 'string' || !domain) reject('domain must be a non-empty string');
43
+ if (![uid, gid].every(n => Number.isSafeInteger(n) && n >= 0)) reject('uid/gid must be non-negative integers');
44
+ validateBuildRecordSet(provenance);
45
+ }
46
+ function owner(st, { uid, gid }, label, allowedMode = 0o700) {
47
+ if (Number(st.uid) !== uid || Number(st.gid) !== gid) reject(`${label} has foreign ownership`);
48
+ const mode = Number(st.mode) & 0o7777;
49
+ if (mode & ~allowedMode) reject(`${label} has unsafe permission bits`);
50
+ return mode;
51
+ }
52
+ function parent(path, options) {
53
+ const st = fs.lstatSync(dirname(path));
54
+ if (!st.isDirectory()) reject('destination parent is not a directory');
55
+ owner(st, options, 'destination parent');
56
+ }
57
+ function memberName(name) {
58
+ if (!name || name.includes('\0') || name.startsWith('/') || name.endsWith('/') || posix.normalize(name) !== name || name.split('/').some(p => p === '.' || p === '..') || name.split('/')[0] !== 'state') reject(`Noncanonical archive member: ${name}`);
59
+ }
60
+ export function scanSource(source, options) {
61
+ const entries = [];
62
+ function visit(path, name) {
63
+ const st = fs.lstatSync(path, { bigint: true });
64
+ if (!st.isDirectory() && !st.isFile()) reject(`${name} is not a directory or regular file`);
65
+ if (st.isFile() && st.nlink !== 1n) reject(`${name} is linked`);
66
+ // Native packages may create readable descendants inside the private root.
67
+ // Never accept shared writes or special bits; emitted state remains private.
68
+ const mode = owner(st, options, name, name === 'state' ? 0o700 : 0o755) & 0o700;
69
+ memberName(name);
70
+ entries.push({ path, name, st, mode });
71
+ if (st.isDirectory()) for (const child of fs.readdirSync(path).sort()) visit(join(path, child), name + '/' + child);
72
+ }
73
+ if (!fs.lstatSync(source).isDirectory()) reject('source must be a directory');
74
+ visit(source, 'state');
75
+ return entries;
76
+ }
77
+ function mtimeText(ns) {
78
+ const sign = ns < 0n ? '-' : ''; const n = ns < 0n ? -ns : ns;
79
+ return sign + n / 1000000000n + '.' + String(n % 1000000000n).padStart(9, '0');
80
+ }
81
+ function mtimeNs(header) {
82
+ const text = header.pax?.mtime;
83
+ if (typeof text !== 'string') reject('archive member lacks an exact mtime');
84
+ const m = /^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text);
85
+ if (!m) reject('invalid archive mtime');
86
+ const fraction = m[3] || '';
87
+ const scale = Number(m[4] || 0) + 9 - fraction.length;
88
+ if (!Number.isSafeInteger(scale) || Math.abs(scale) > 1000) reject('archive mtime is outside supported range');
89
+ let value = BigInt(m[2] + fraction);
90
+ if (scale >= 0) value *= 10n ** BigInt(scale);
91
+ else { const divisor = 10n ** BigInt(-scale); if (value % divisor) reject('archive mtime has sub-nanosecond precision'); value /= divisor; }
92
+ return m[1] === '-' ? -value : value;
93
+ }
94
+ async function hash(path) {
95
+ const digest = createHash('sha256');
96
+ for await (const chunk of fs.createReadStream(path)) digest.update(chunk);
97
+ return digest.digest('hex');
98
+ }
99
+ function strictJson(bytes) {
100
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
101
+ const errors = []; const tree = parseTree(text, errors, { disallowComments: true, allowTrailingComma: false });
102
+ if (!tree || errors.length) reject('invalid metadata.json');
103
+ function check(node) {
104
+ if (node.type === 'object') {
105
+ const keys = node.children.map(p => p.children[0].value);
106
+ if (new Set(keys).size !== keys.length) reject('metadata contains duplicate key');
107
+ }
108
+ for (const child of node.children || []) check(child);
109
+ }
110
+ check(tree); return JSON.parse(text);
111
+ }
112
+ async function writeTar(path, entries) {
113
+ const pack = tar.pack();
114
+ const completion = pipeline(pack, fs.createWriteStream(path, { flags: 'wx', mode: 0o600 }));
115
+ completion.catch(() => {});
116
+ try {
117
+ for (const { path: source, name, st, mode } of entries) {
118
+ const header = { name, type: st.isDirectory() ? 'directory' : 'file', uid: Number(st.uid), gid: Number(st.gid), mode,
119
+ uname: '', gname: '', mtime: new Date(0), pax: { mtime: mtimeText(st.mtimeNs), uid: String(st.uid), gid: String(st.gid) }, size: st.isFile() ? Number(st.size) : 0 };
120
+ if (st.isDirectory()) { await new Promise((yes, no) => pack.entry(header, error => error ? no(error) : yes())); continue; }
121
+ const fd = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
122
+ let handedOff = false;
123
+ try {
124
+ const current = fs.fstatSync(fd, { bigint: true });
125
+ owner(current, header, name, 0o755);
126
+ if (!current.isFile() || current.dev !== st.dev || current.ino !== st.ino || current.size !== st.size) reject('source changed during archive creation');
127
+ const input = fs.createReadStream(source, { fd, autoClose: true }); handedOff = true;
128
+ await pipeline(input, pack.entry(header));
129
+ } finally { if (!handedOff) fs.closeSync(fd); }
130
+ }
131
+ pack.finalize(); await completion;
132
+ } catch (error) { pack.destroy(error); await completion.catch(() => {}); throw error; }
133
+ }
134
+ async function readTar(path, consume) {
135
+ const extract = tar.extract();
136
+ const completion = pipeline(fs.createReadStream(path), extract); completion.catch(() => {});
137
+ try {
138
+ for await (const entry of extract) {
139
+ const header = { ...entry.header };
140
+ // tar-stream applies PAX paths/size, but leaves uid/gid in raw headers.
141
+ // PAX ownership is authoritative, including when it contradicts the base header.
142
+ for (const key of ['uid', 'gid', 'size']) if (header.pax?.[key] !== undefined) {
143
+ const encoded = header.pax[key];
144
+ if (!/^[+-]?\d+$/.test(encoded) || !Number.isSafeInteger(Number(encoded)) || Number(encoded) < 0) reject(`invalid PAX ${key}`);
145
+ header[key] = Number(encoded);
146
+ }
147
+ // Python TarInfo normalizes a directory's trailing slash in the same way.
148
+ if (header.type === 'directory' && header.name.endsWith('/')) header.name = header.name.slice(0, -1);
149
+ await consume(header, entry);
150
+ }
151
+ await completion;
152
+ } catch (error) { extract.destroy(error); await completion.catch(() => {}); throw error; }
153
+ }
154
+ async function members(path, options) {
155
+ const result = new Map();
156
+ await readTar(path, async (header, entry) => {
157
+ memberName(header.name);
158
+ if (result.has(header.name)) reject('duplicate archive member');
159
+ if (!['directory', 'file', 'contiguous-file'].includes(header.type)) reject('archive member is a link or special entry');
160
+ owner(header, options, header.name); mtimeNs(header);
161
+ result.set(header.name, header);
162
+ for await (const chunk of entry) { /* Validate the whole stream without buffering file content. */ }
163
+ });
164
+ if (result.get('state')?.type !== 'directory') reject('state.tar lacks its root directory');
165
+ for (const name of result.keys()) if (name !== 'state' && result.get(posix.dirname(name))?.type !== 'directory') reject('archive member has a missing or non-directory parent');
166
+ return result;
167
+ }
168
+ export async function validateArchive(archive, options) {
169
+ inputs(options);
170
+ const RECORDS = archiveRecords(options), PAYLOADS = archivePayloads(options), FILES = archiveFiles(options);
171
+ const st = fs.lstatSync(archive);
172
+ if (!st.isDirectory() || owner(st, options, 'archive') !== 0o700) reject('archive must be a private directory');
173
+ if (fs.readdirSync(archive).sort().join('\0') !== [...FILES].sort().join('\0')) reject('archive must contain exactly the expected format files');
174
+ for (const name of FILES) {
175
+ const st = fs.lstatSync(join(archive, name));
176
+ if (!st.isFile() || owner(st, options, name) !== 0o600) reject('archive payload must be a private regular file');
177
+ }
178
+ const metadata = strictJson(fs.readFileSync(join(archive, 'metadata.json')));
179
+ if (!sameKeys(metadata, ['format', 'domain', 'created_at', 'uid', 'gid', 'sha256']) || metadata.format !== archiveFormat(options) || metadata.domain !== options.domain || metadata.uid !== options.uid || metadata.gid !== options.gid) reject('archive metadata does not match');
180
+ if (typeof metadata.created_at !== 'string' || !metadata.created_at.endsWith('Z') || !Number.isFinite(Date.parse(metadata.created_at))) reject('archive creation time is invalid');
181
+ if (!sameKeys(metadata.sha256, PAYLOADS)) reject('archive digest map is invalid');
182
+ for (const name of PAYLOADS) if (metadata.sha256[name] !== await hash(join(archive, name))) reject(`archive payload ${name} has a mismatched digest`);
183
+ for (const name of RECORDS) if (!fs.readFileSync(join(archive, name)).equals(options.provenance[name])) reject(`archive provenance ${name} is not admitted`);
184
+ await members(join(archive, 'state.tar'), options);
185
+ return metadata;
186
+ }
187
+ function inside(child, parentPath) { const rel = relative(parentPath, child); return !rel || (rel !== '..' && !rel.startsWith('../') && !rel.startsWith('/')); }
188
+ const destinationPath = path => join(fs.realpathSync(dirname(path)), basename(path));
189
+ export async function createArchive(source, destination, options) {
190
+ inputs(options);
191
+ const RECORDS = archiveRecords(options), PAYLOADS = archivePayloads(options); absent(destination); parent(destination, options);
192
+ if (inside(destinationPath(destination), fs.realpathSync(source))) reject('archive destination must be outside the source');
193
+ const entries = scanSource(source, options);
194
+ const stage = fs.mkdtempSync(join(dirname(destination), '.' + basename(destination) + '.tmp-'));
195
+ fs.chmodSync(stage, 0o700);
196
+ try {
197
+ await writeTar(join(stage, 'state.tar'), entries);
198
+ fs.chmodSync(join(stage, 'state.tar'), 0o600);
199
+ for (const name of RECORDS) {
200
+ fs.writeFileSync(join(stage, name), options.provenance[name], { flag: 'wx', mode: 0o600 });
201
+ fs.chmodSync(join(stage, name), 0o600);
202
+ }
203
+ const sha256 = {}; for (const name of PAYLOADS) sha256[name] = await hash(join(stage, name));
204
+ const metadata = { format: archiveFormat(options), domain: options.domain, created_at: new Date().toISOString(), uid: options.uid, gid: options.gid, sha256 };
205
+ fs.writeFileSync(join(stage, 'metadata.json'), JSON.stringify(metadata) + '\n', { flag: 'wx', mode: 0o600 });
206
+ fs.chmodSync(join(stage, 'metadata.json'), 0o600);
207
+ await validateArchive(stage, options); publishNoReplace(stage, destination);
208
+ return metadata;
209
+ } finally { fs.rmSync(stage, { recursive: true, force: true }); }
210
+ }
211
+ export async function extractArchive(archive, staging, options) {
212
+ inputs(options); absent(staging); parent(staging, options);
213
+ const sourcePath = fs.realpathSync(archive), targetPath = destinationPath(staging);
214
+ if (inside(sourcePath, targetPath) || inside(targetPath, sourcePath)) reject('extraction staging must be outside the archive');
215
+ const metadata = await validateArchive(archive, options);
216
+ const snapshotRoot = fs.mkdtempSync(join(dirname(staging), '.archive-snapshot-'));
217
+ let created = false, completed = false;
218
+ try {
219
+ const snapshot = join(snapshotRoot, 'state.tar'); fs.copyFileSync(join(archive, 'state.tar'), snapshot); fs.chmodSync(snapshot, 0o600);
220
+ if (await hash(snapshot) !== metadata.sha256['state.tar']) reject('state.tar changed after validation');
221
+ const inventory = await members(snapshot, options);
222
+ fs.mkdirSync(staging, { mode: 0o700 }); created = true;
223
+ const directories = [...inventory.values()].filter(h => h.type === 'directory').sort((a, b) => a.name.split('/').length - b.name.split('/').length);
224
+ const target = name => name === 'state' ? staging : join(staging, name.slice(6));
225
+ for (const h of directories) if (h.name !== 'state') fs.mkdirSync(target(h.name), { mode: 0o700 });
226
+ await readTar(snapshot, async (header, entry) => {
227
+ if (header.type === 'directory') { for await (const chunk of entry) {} return; }
228
+ const path = target(header.name);
229
+ await pipeline(entry, fs.createWriteStream(path, { flags: fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, mode: 0o600 }));
230
+ fs.chownSync(path, header.uid, header.gid); fs.chmodSync(path, header.mode); setMtimeNs(path, mtimeNs(header));
231
+ });
232
+ for (const header of directories.reverse()) { const path = target(header.name); fs.chownSync(path, header.uid, header.gid); fs.chmodSync(path, header.mode); setMtimeNs(path, mtimeNs(header)); }
233
+ completed = true; return metadata;
234
+ } finally {
235
+ fs.rmSync(snapshotRoot, { recursive: true, force: true });
236
+ if (created && !completed) fs.rmSync(staging, { recursive: true, force: true });
237
+ }
238
+ }
@@ -0,0 +1,56 @@
1
+ /** Private bindings for the existing stopped-state primitives, not a general FFI API. */
2
+ import koffi from 'koffi';
3
+ import { constants } from 'node:os';
4
+ import { getSystemErrorName } from 'node:util';
5
+
6
+ if (!['linux', 'darwin'].includes(process.platform)) {
7
+ throw new Error('State maintenance requires supported Linux or macOS native primitives');
8
+ }
9
+ const libc = koffi.load(null);
10
+ const flock = libc.func('int flock(int fd, int operation)');
11
+ const rename = process.platform === 'darwin'
12
+ ? libc.func('int renamex_np(const char *from, const char *to, unsigned int flags)')
13
+ : libc.func('int renameat2(int fromfd, const char *from, int tofd, const char *to, unsigned int flags)');
14
+ const timespec = koffi.struct({ tv_sec: 'long', tv_nsec: 'long' });
15
+ const utimensat = libc.func('utimensat', 'int', ['int', 'str', koffi.pointer(timespec), 'int']);
16
+
17
+ function failure(operation, number) {
18
+ const code = getSystemErrorName(-number);
19
+ return Object.assign(new Error(`${operation}: ${code}`), { code, errno: number });
20
+ }
21
+
22
+ function move(from, to, flags, operation) {
23
+ const result = process.platform === 'darwin'
24
+ ? rename(from, to, flags)
25
+ : rename(-100, from, -100, to, flags);
26
+ if (result !== 0) throw failure(operation, koffi.errno());
27
+ }
28
+
29
+ export function exchange(from, to) {
30
+ move(from, to, 2, 'atomic directory exchange');
31
+ }
32
+
33
+ export function publishNoReplace(from, to) {
34
+ move(from, to, process.platform === 'darwin' ? 4 : 1, 'atomic no-replace publication');
35
+ }
36
+
37
+ // The caller owns the descriptor and its lifetime. Closing its final reference
38
+ // releases the lock; never unlink the lock inode or use a stale-timeout takeover.
39
+ export function tryLock(fd) {
40
+ if (flock(fd, 2 | 4) === 0) return true;
41
+ const number = koffi.errno();
42
+ if ([constants.errno.EAGAIN, constants.errno.EWOULDBLOCK].includes(number)) return false;
43
+ throw failure('state lock', number);
44
+ }
45
+
46
+ // Node's Date-based APIs lose the nanoseconds retained by the archive format.
47
+ export function setMtimeNs(path, nanoseconds) {
48
+ let seconds = nanoseconds / 1000000000n;
49
+ let remainder = nanoseconds % 1000000000n;
50
+ if (remainder < 0n) { seconds -= 1n; remainder += 1000000000n; }
51
+ const value = { tv_sec: seconds, tv_nsec: remainder };
52
+ const darwin = process.platform === 'darwin';
53
+ if (utimensat(darwin ? -2 : -100, path, [value, value], darwin ? 0x20 : 0x100) !== 0) {
54
+ throw failure('restore state timestamps', koffi.errno());
55
+ }
56
+ }