sealcode 1.3.1 → 1.3.2
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/package.json +1 -1
- package/src/cli-watch.js +76 -6
package/package.json
CHANGED
package/src/cli-watch.js
CHANGED
|
@@ -54,7 +54,7 @@ const {
|
|
|
54
54
|
clearSession,
|
|
55
55
|
projectId,
|
|
56
56
|
} = require('./keystore');
|
|
57
|
-
const { runLock } = require('./seal');
|
|
57
|
+
const { runLock, collectFiles } = require('./seal');
|
|
58
58
|
const { runUnlock } = require('./open');
|
|
59
59
|
const { getDeviceFingerprint } = require('./device');
|
|
60
60
|
const { SealcodeError } = require('./errors');
|
|
@@ -688,7 +688,35 @@ async function runWatch({
|
|
|
688
688
|
try {
|
|
689
689
|
r = await heartbeatOnce(trimmedCode, { waitMs });
|
|
690
690
|
} catch (err) {
|
|
691
|
-
|
|
691
|
+
// sealcode@1.3.2 — previously this rethrew, which propagated out
|
|
692
|
+
// of runWatch and turned the watcher into a silent zombie: the
|
|
693
|
+
// process kept running because exfilCtrl's inotify kept the event
|
|
694
|
+
// loop alive, but the polling loop was dead and the watcher
|
|
695
|
+
// stopped receiving pause/resume/unlock signals from the server.
|
|
696
|
+
// We now treat unexpected throws from heartbeatOnce as transient,
|
|
697
|
+
// back off, and try again — same path as `!r.ok`. If we exceed
|
|
698
|
+
// the offline grace we'll hard-lock cleanly via the regular path.
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
if (consecutiveTransient === 0) firstTransientAt = now;
|
|
701
|
+
consecutiveTransient += 1;
|
|
702
|
+
const offlineSec = Math.floor((now - firstTransientAt) / 1000);
|
|
703
|
+
const errMsg = String(err?.message || err);
|
|
704
|
+
const errDetail = err?.detail || err?.hint || null;
|
|
705
|
+
const errCode = err?.code || err?.apiCode || err?.status || null;
|
|
706
|
+
appendLog(projectRoot, {
|
|
707
|
+
type: 'heartbeat_throw',
|
|
708
|
+
error: errMsg,
|
|
709
|
+
detail: errDetail,
|
|
710
|
+
code: errCode,
|
|
711
|
+
stack: err?.stack ? String(err.stack).split('\n').slice(0, 4).join(' | ') : null,
|
|
712
|
+
offlineSec,
|
|
713
|
+
});
|
|
714
|
+
if (!daemon && !json) ui.warn(`[${ts()}] heartbeat raised: ${errMsg} (offline ${offlineSec}s / ${offlineGraceSec}s grace)`);
|
|
715
|
+
if (offlineSec >= offlineGraceSec) {
|
|
716
|
+
return finalLock(projectRoot, config, trimmedCode, 'offline_grace_exceeded', json, daemon);
|
|
717
|
+
}
|
|
718
|
+
await sleep(TRANSIENT_BACKOFF_SEC * 1000);
|
|
719
|
+
continue;
|
|
692
720
|
}
|
|
693
721
|
if (!r.ok) {
|
|
694
722
|
const now = Date.now();
|
|
@@ -840,14 +868,56 @@ async function softLock(projectRoot, config, code, reason, json, daemon) {
|
|
|
840
868
|
return;
|
|
841
869
|
}
|
|
842
870
|
|
|
871
|
+
// sealcode@1.3.2 — CRITICAL safety check. If the project has no
|
|
872
|
+
// plaintext files on disk (i.e. it's already in its locked state),
|
|
873
|
+
// calling runLock here would DESTROY the locked vendor/ directory:
|
|
874
|
+
// runLock does `rmIfExists(lockedRoot)` and then re-writes only files
|
|
875
|
+
// that are currently plaintext PLUS any blobs covered by
|
|
876
|
+
// preserveUnseen. preserveUnseen is only true for path-scoped grants
|
|
877
|
+
// (allowedPaths set), so for the common case of a full-project grant
|
|
878
|
+
// the wipe would leave just the keystore + 0 blobs and the manifest
|
|
879
|
+
// would shrink from 553 entries to 0. A subsequent `sealcode unlock`
|
|
880
|
+
// would then "restore" 0 files — irrecoverable data loss for the
|
|
881
|
+
// recipient if they don't have a separate copy.
|
|
882
|
+
//
|
|
883
|
+
// This is exactly Rejoan's "unlocked 1 files" bug: his watcher
|
|
884
|
+
// restarted (or auto-spawned by redeem) against an already-paused
|
|
885
|
+
// grant on Windows where files were already locked, softLock fired
|
|
886
|
+
// with no plaintext to encrypt, wiped vendor/, and his next unlock
|
|
887
|
+
// restored just the 1 stub file.
|
|
888
|
+
//
|
|
889
|
+
// The fix: if there's nothing to encrypt, softLock is a no-op. The
|
|
890
|
+
// files are already in the encrypted state we wanted them in.
|
|
891
|
+
let plaintextFiles = [];
|
|
892
|
+
try {
|
|
893
|
+
plaintextFiles = await collectFiles(projectRoot, config);
|
|
894
|
+
} catch (_) {
|
|
895
|
+
// If collectFiles itself fails, fall through to runLock which has
|
|
896
|
+
// its own error handling — at worst we hit the catch below and
|
|
897
|
+
// log soft_lock_error.
|
|
898
|
+
}
|
|
899
|
+
if (plaintextFiles.length === 0) {
|
|
900
|
+
K.fill(0);
|
|
901
|
+
if (daemon) {
|
|
902
|
+
appendLog(projectRoot, { type: 'soft_locked', count: 0, reason: label, note: 'already-locked' });
|
|
903
|
+
} else if (!json) {
|
|
904
|
+
ui.ok(`[${ts()}] already locked on disk — no re-encryption needed (waiting for resume)`);
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
843
909
|
const _sessionMeta = loadSessionMeta(projectRoot);
|
|
910
|
+
// sealcode@1.3.2 — always preserve unseen blobs for grant-derived
|
|
911
|
+
// sessions, not just path-scoped ones. Even without allowedPaths,
|
|
912
|
+
// the recipient may have a mix of plaintext and still-locked files
|
|
913
|
+
// (e.g. they manually `sealcode lock`ed a subset, or RO mode set
|
|
914
|
+
// 0444 prevented some files from materializing on disk). Without
|
|
915
|
+
// preserveUnseen, runLock would drop those unseen blobs and the
|
|
916
|
+
// resume + unlock flow would restore an incomplete project.
|
|
844
917
|
const _preserveUnseen =
|
|
845
918
|
!!_sessionMeta &&
|
|
846
919
|
_sessionMeta.meta &&
|
|
847
|
-
_sessionMeta.meta.source === 'grant'
|
|
848
|
-
_sessionMeta.meta.policy &&
|
|
849
|
-
Array.isArray(_sessionMeta.meta.policy.allowedPaths) &&
|
|
850
|
-
_sessionMeta.meta.policy.allowedPaths.length > 0;
|
|
920
|
+
_sessionMeta.meta.source === 'grant';
|
|
851
921
|
|
|
852
922
|
try {
|
|
853
923
|
const res = await runLock({ projectRoot, config, K, preserveUnseen: _preserveUnseen });
|