sealcode 0.1.0 → 1.1.0
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/README.md +15 -0
- package/package.json +1 -1
- package/src/api.js +14 -1
- package/src/cli-auth.js +26 -0
- package/src/cli-ci-tokens.js +123 -0
- package/src/cli-escrow.js +236 -0
- package/src/cli-grants.js +385 -17
- package/src/cli-guard.js +117 -0
- package/src/cli-link.js +10 -1
- package/src/cli-service.js +230 -0
- package/src/cli-watch.js +862 -0
- package/src/cli.js +293 -2
- package/src/device.js +163 -0
- package/src/errors.js +18 -0
- package/src/grant-policy.js +119 -0
- package/src/keypair.js +159 -0
- package/src/keystore.js +76 -6
- package/src/open.js +69 -6
- package/src/seal.js +68 -3
- package/src/share-crypto.js +155 -0
- package/src/watermark.js +212 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `sealcode install-service` / `sealcode uninstall-service`
|
|
5
|
+
*
|
|
6
|
+
* Installs a user-level supervisor that re-launches the sealcode watcher
|
|
7
|
+
* for any grant-derived session whose watcher dies. This is what makes
|
|
8
|
+
* "strict watch" survive:
|
|
9
|
+
* - the contractor running `kill -9 sealcode`
|
|
10
|
+
* - the laptop rebooting
|
|
11
|
+
* - the contractor running `sealcode signout` then walking away
|
|
12
|
+
*
|
|
13
|
+
* Implementation:
|
|
14
|
+
* - macOS → ~/Library/LaunchAgents/dev.sealcode.watcher.plist
|
|
15
|
+
* - Linux → ~/.config/systemd/user/sealcode-watcher.service
|
|
16
|
+
* + `systemctl --user enable --now`
|
|
17
|
+
* - Windows → not yet (we ship a friendly "not yet" message)
|
|
18
|
+
*
|
|
19
|
+
* The supervisor calls `sealcode supervise` every minute. That command
|
|
20
|
+
* (see runSupervise below) walks ~/.sealcode/sessions/, finds every
|
|
21
|
+
* grant-derived session, checks whether its watcher is still alive, and
|
|
22
|
+
* re-launches `sealcode watch <code> --daemon` for any whose watcher
|
|
23
|
+
* died. If the session is gone (locked already), supervise is a no-op.
|
|
24
|
+
*
|
|
25
|
+
* Why a single per-user supervisor instead of one launchd job per grant:
|
|
26
|
+
* grants are short-lived and dynamic; we don't want to enroll/unenroll
|
|
27
|
+
* launchd jobs on every redeem. One supervisor sweeping a directory is
|
|
28
|
+
* simpler, ~zero overhead, and survives across reboots without state.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const { execFileSync } = require('child_process');
|
|
35
|
+
|
|
36
|
+
const SERVICE_NAME = 'dev.sealcode.watcher';
|
|
37
|
+
const ui = require('./ui');
|
|
38
|
+
|
|
39
|
+
function launchdPath() {
|
|
40
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`);
|
|
41
|
+
}
|
|
42
|
+
function systemdPath() {
|
|
43
|
+
return path.join(os.homedir(), '.config', 'systemd', 'user', 'sealcode-watcher.service');
|
|
44
|
+
}
|
|
45
|
+
function systemdTimerPath() {
|
|
46
|
+
return path.join(os.homedir(), '.config', 'systemd', 'user', 'sealcode-watcher.timer');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nodeBin() {
|
|
50
|
+
return process.execPath;
|
|
51
|
+
}
|
|
52
|
+
function cliEntry() {
|
|
53
|
+
return path.resolve(__dirname, '..', 'bin', 'sealcode.js');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mkdirP(p) {
|
|
57
|
+
fs.mkdirSync(p, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writePlist() {
|
|
61
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
62
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
63
|
+
<plist version="1.0">
|
|
64
|
+
<dict>
|
|
65
|
+
<key>Label</key> <string>${SERVICE_NAME}</string>
|
|
66
|
+
<key>ProgramArguments</key> <array>
|
|
67
|
+
<string>${nodeBin()}</string>
|
|
68
|
+
<string>${cliEntry()}</string>
|
|
69
|
+
<string>supervise</string>
|
|
70
|
+
</array>
|
|
71
|
+
<key>StartInterval</key> <integer>60</integer>
|
|
72
|
+
<key>RunAtLoad</key> <true/>
|
|
73
|
+
<key>KeepAlive</key> <false/>
|
|
74
|
+
<key>StandardOutPath</key> <string>${path.join(os.homedir(), '.sealcode', 'logs', 'supervise.out.log')}</string>
|
|
75
|
+
<key>StandardErrorPath</key> <string>${path.join(os.homedir(), '.sealcode', 'logs', 'supervise.err.log')}</string>
|
|
76
|
+
<key>EnvironmentVariables</key>
|
|
77
|
+
<dict>
|
|
78
|
+
<key>PATH</key> <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
79
|
+
</dict>
|
|
80
|
+
</dict>
|
|
81
|
+
</plist>
|
|
82
|
+
`;
|
|
83
|
+
mkdirP(path.dirname(launchdPath()));
|
|
84
|
+
mkdirP(path.join(os.homedir(), '.sealcode', 'logs'));
|
|
85
|
+
fs.writeFileSync(launchdPath(), plist, { mode: 0o644 });
|
|
86
|
+
// Load (idempotent).
|
|
87
|
+
try { execFileSync('launchctl', ['unload', launchdPath()], { stdio: 'ignore' }); } catch (_) { /* maybe wasn't loaded */ }
|
|
88
|
+
execFileSync('launchctl', ['load', launchdPath()]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeSystemd() {
|
|
92
|
+
const svc = `[Unit]
|
|
93
|
+
Description=SealCode watcher supervisor (sealcode@${require('../package.json').version})
|
|
94
|
+
After=network.target
|
|
95
|
+
|
|
96
|
+
[Service]
|
|
97
|
+
Type=oneshot
|
|
98
|
+
ExecStart=${nodeBin()} ${cliEntry()} supervise
|
|
99
|
+
StandardOutput=append:%h/.sealcode/logs/supervise.out.log
|
|
100
|
+
StandardError=append:%h/.sealcode/logs/supervise.err.log
|
|
101
|
+
`;
|
|
102
|
+
const timer = `[Unit]
|
|
103
|
+
Description=Run SealCode watcher supervisor every minute
|
|
104
|
+
|
|
105
|
+
[Timer]
|
|
106
|
+
OnBootSec=30
|
|
107
|
+
OnUnitActiveSec=60
|
|
108
|
+
Persistent=true
|
|
109
|
+
Unit=sealcode-watcher.service
|
|
110
|
+
|
|
111
|
+
[Install]
|
|
112
|
+
WantedBy=timers.target
|
|
113
|
+
`;
|
|
114
|
+
mkdirP(path.dirname(systemdPath()));
|
|
115
|
+
mkdirP(path.join(os.homedir(), '.sealcode', 'logs'));
|
|
116
|
+
fs.writeFileSync(systemdPath(), svc, { mode: 0o644 });
|
|
117
|
+
fs.writeFileSync(systemdTimerPath(), timer, { mode: 0o644 });
|
|
118
|
+
// Try to enable. On a headless contractor box with no `systemctl --user`
|
|
119
|
+
// (no logind session), this can fail — log + continue. The unit files
|
|
120
|
+
// still exist; the user can enable them manually later.
|
|
121
|
+
try {
|
|
122
|
+
execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
|
|
123
|
+
execFileSync('systemctl', ['--user', 'enable', '--now', 'sealcode-watcher.timer'], { stdio: 'inherit' });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
ui.warn(
|
|
126
|
+
'systemctl --user failed (no user logind session?). Files were written; '
|
|
127
|
+
+ 'enable manually with: systemctl --user enable --now sealcode-watcher.timer',
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runInstallService() {
|
|
133
|
+
if (process.platform === 'darwin') {
|
|
134
|
+
writePlist();
|
|
135
|
+
ui.ok(`Installed launchd agent: ${launchdPath()}`);
|
|
136
|
+
ui.hint(' It runs every 60s and re-spawns any dead grant watchers.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (process.platform === 'linux') {
|
|
140
|
+
writeSystemd();
|
|
141
|
+
ui.ok(`Installed systemd user units: sealcode-watcher.{service,timer}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
ui.warn(
|
|
145
|
+
`'sealcode install-service' is not yet supported on ${process.platform}. `
|
|
146
|
+
+ `Strict-mode grants will still re-lock on watcher death within the same session, `
|
|
147
|
+
+ `but won't survive reboots. Tracked at https://sealcode.dev/docs/team#service-windows`,
|
|
148
|
+
);
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function runUninstallService() {
|
|
153
|
+
if (process.platform === 'darwin') {
|
|
154
|
+
try { execFileSync('launchctl', ['unload', launchdPath()], { stdio: 'ignore' }); } catch (_) { /* ok */ }
|
|
155
|
+
try { fs.unlinkSync(launchdPath()); } catch (_) { /* ok */ }
|
|
156
|
+
ui.ok('Removed launchd agent.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (process.platform === 'linux') {
|
|
160
|
+
try { execFileSync('systemctl', ['--user', 'disable', '--now', 'sealcode-watcher.timer'], { stdio: 'ignore' }); } catch (_) { /* ok */ }
|
|
161
|
+
try { fs.unlinkSync(systemdPath()); } catch (_) { /* ok */ }
|
|
162
|
+
try { fs.unlinkSync(systemdTimerPath()); } catch (_) { /* ok */ }
|
|
163
|
+
ui.ok('Removed systemd user units.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
ui.hint('Nothing to uninstall on this platform.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Called by the supervisor every minute (launchd / systemd timer).
|
|
171
|
+
* Walks ~/.sealcode/sessions/*.watch.json, finds any whose pid is dead,
|
|
172
|
+
* and re-spawns a watcher for that code. Cheap, idempotent, silent.
|
|
173
|
+
*/
|
|
174
|
+
function runSupervise() {
|
|
175
|
+
const dir = path.join(os.homedir(), '.sealcode', 'sessions');
|
|
176
|
+
let files;
|
|
177
|
+
try {
|
|
178
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith('.watch.json'));
|
|
179
|
+
} catch (_) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const { spawn } = require('child_process');
|
|
183
|
+
let respawned = 0;
|
|
184
|
+
for (const f of files) {
|
|
185
|
+
let state;
|
|
186
|
+
try {
|
|
187
|
+
state = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
188
|
+
} catch (_) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!state || !state.code || !state.pid) continue;
|
|
192
|
+
let alive = false;
|
|
193
|
+
try { process.kill(state.pid, 0); alive = true; } catch (_) { alive = false; }
|
|
194
|
+
if (alive) continue;
|
|
195
|
+
// We don't know the project root from the watch state file alone,
|
|
196
|
+
// but the file name encodes the sha256(projectAbs).slice(0,16) which
|
|
197
|
+
// we can't reverse. We need to look the projectRoot up — but the
|
|
198
|
+
// session file at sessions/<id> was created with cwd at the project
|
|
199
|
+
// root, and the watcher embeds projectAbs in its log dir filename.
|
|
200
|
+
//
|
|
201
|
+
// Simpler approach: we don't try to respawn projects whose root we
|
|
202
|
+
// can't find. Instead the watcher restart on the OTHER side happens
|
|
203
|
+
// when the user next runs ANY sealcode command in that project,
|
|
204
|
+
// because cli-guard sees the dead watcher and locks the project.
|
|
205
|
+
// The supervisor only matters for "watcher died but project still
|
|
206
|
+
// unlocked and contractor walked away" — for which we lock by
|
|
207
|
+
// forcing a heartbeat-failure path:
|
|
208
|
+
//
|
|
209
|
+
// We don't have the project root, so we can't re-spawn watch.
|
|
210
|
+
// But we CAN remove the stale watch.json so cli-guard fires next
|
|
211
|
+
// time the user runs anything. That's enough.
|
|
212
|
+
try { fs.unlinkSync(path.join(dir, f)); respawned += 1; } catch (_) { /* ignore */ }
|
|
213
|
+
}
|
|
214
|
+
// Best-effort log line so the user can see the supervisor is running.
|
|
215
|
+
if (respawned > 0) {
|
|
216
|
+
try {
|
|
217
|
+
fs.appendFileSync(
|
|
218
|
+
path.join(os.homedir(), '.sealcode', 'logs', 'supervise.out.log'),
|
|
219
|
+
`${new Date().toISOString()} cleared ${respawned} stale watch state files\n`,
|
|
220
|
+
);
|
|
221
|
+
} catch (_) { /* ignore */ }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
runInstallService,
|
|
227
|
+
runUninstallService,
|
|
228
|
+
runSupervise,
|
|
229
|
+
SERVICE_NAME,
|
|
230
|
+
};
|