sealcode 1.1.0 → 1.2.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/package.json +1 -1
- package/src/cli-grants.js +129 -12
- package/src/cli-watch.js +8 -1
- package/src/cli.js +41 -1
package/package.json
CHANGED
package/src/cli-grants.js
CHANGED
|
@@ -74,17 +74,38 @@ function getActiveConfig(projectRoot) {
|
|
|
74
74
|
* a real cryptographic bug; we don't want to silently downgrade in that
|
|
75
75
|
* case).
|
|
76
76
|
*/
|
|
77
|
-
async function maybeWrapKeyForRecipient({
|
|
77
|
+
async function maybeWrapKeyForRecipient({
|
|
78
|
+
projectRoot,
|
|
79
|
+
recipientEmail,
|
|
80
|
+
verbose = false,
|
|
81
|
+
resolveKey = null,
|
|
82
|
+
}) {
|
|
78
83
|
if (!recipientEmail) return null;
|
|
79
84
|
|
|
80
|
-
// Need the master key K to wrap.
|
|
81
|
-
//
|
|
85
|
+
// Need the master key K to wrap. Prefer a cached session; if none, ask
|
|
86
|
+
// the caller (cli.js) to prompt for the passphrase. We deliberately do
|
|
87
|
+
// NOT silently downgrade to "passphrase fallback" here — the whole point
|
|
88
|
+
// of `--email` is to spare the recipient the passphrase exchange.
|
|
82
89
|
const config = getActiveConfig(projectRoot);
|
|
83
90
|
if (!isInitialized(projectRoot, config.lockedDir)) return null;
|
|
84
|
-
|
|
91
|
+
let K = loadSession(projectRoot);
|
|
92
|
+
if (!K && typeof resolveKey === 'function') {
|
|
93
|
+
if (verbose) {
|
|
94
|
+
ui.step('No cached master key — prompting for passphrase so we can wrap for the recipient.');
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
K = await resolveKey(projectRoot, config);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (verbose) {
|
|
100
|
+
ui.warn(`Could not derive master key (${err.message || err}). Will mint without wrapping.`);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
85
105
|
if (!K) {
|
|
86
106
|
if (verbose) {
|
|
87
|
-
ui.
|
|
107
|
+
ui.warn('No cached master key — minting code WITHOUT wrapped key. Recipient will need the passphrase.');
|
|
108
|
+
ui.hint('Run `sealcode unlock` first, or supply a passphrase prompt, to enable passphrase-free sharing.');
|
|
88
109
|
}
|
|
89
110
|
return null;
|
|
90
111
|
}
|
|
@@ -100,13 +121,14 @@ async function maybeWrapKeyForRecipient({ projectRoot, recipientEmail, verbose =
|
|
|
100
121
|
} catch (err) {
|
|
101
122
|
if (err instanceof ApiError) {
|
|
102
123
|
if (err.status === 404) {
|
|
103
|
-
// Either the user doesn't exist or hasn't published a pubkey
|
|
104
|
-
// yet. We still mint the code; the recipient just falls back to
|
|
105
|
-
// the legacy flow when they redeem.
|
|
106
124
|
if (verbose) {
|
|
125
|
+
ui.warn(
|
|
126
|
+
`Recipient ${recipientEmail} hasn't signed in to sealcode.dev yet, so no pubkey is on file.`,
|
|
127
|
+
);
|
|
107
128
|
ui.hint(
|
|
108
|
-
|
|
129
|
+
'Ask them to visit https://sealcode.dev/login, then re-run `sealcode share --email`.',
|
|
109
130
|
);
|
|
131
|
+
ui.hint('Continuing with passphrase-fallback for this code…');
|
|
110
132
|
}
|
|
111
133
|
return null;
|
|
112
134
|
}
|
|
@@ -146,6 +168,7 @@ async function runShare({
|
|
|
146
168
|
singleDeviceEnforce = false,
|
|
147
169
|
ndaText = null,
|
|
148
170
|
json = false,
|
|
171
|
+
resolveKey = null,
|
|
149
172
|
}) {
|
|
150
173
|
const link = requireLink(projectRoot);
|
|
151
174
|
|
|
@@ -159,6 +182,7 @@ async function runShare({
|
|
|
159
182
|
projectRoot,
|
|
160
183
|
recipientEmail: email,
|
|
161
184
|
verbose: !json,
|
|
185
|
+
resolveKey,
|
|
162
186
|
});
|
|
163
187
|
} catch (err) {
|
|
164
188
|
// Network errors during pubkey lookup shouldn't be silent — degrade
|
|
@@ -275,8 +299,14 @@ async function runGrants({ projectRoot, json = false }) {
|
|
|
275
299
|
process.stdout.write(`\n${link.projectName || link.projectId}: ${grants.length} grant(s)\n`);
|
|
276
300
|
for (const g of grants) {
|
|
277
301
|
const state = g.state.padEnd(8);
|
|
278
|
-
|
|
279
|
-
|
|
302
|
+
let remaining;
|
|
303
|
+
if (g.state === 'active') {
|
|
304
|
+
remaining = formatRemaining(g.remainingMs);
|
|
305
|
+
} else if (g.state === 'paused') {
|
|
306
|
+
remaining = 'paused';
|
|
307
|
+
} else {
|
|
308
|
+
remaining = g.state;
|
|
309
|
+
}
|
|
280
310
|
const who = g.developerEmail || g.developerLabel || '(no label)';
|
|
281
311
|
process.stdout.write(
|
|
282
312
|
` ${state} ${g.codePrefix.padEnd(12)} ${remaining.padEnd(10)} ${who} id=${g.id}\n`,
|
|
@@ -285,6 +315,85 @@ async function runGrants({ projectRoot, json = false }) {
|
|
|
285
315
|
process.stdout.write('\n');
|
|
286
316
|
}
|
|
287
317
|
|
|
318
|
+
/**
|
|
319
|
+
* sealcode@1.2.0 — Pause an active access code from the CLI.
|
|
320
|
+
*
|
|
321
|
+
* sealcode pause <grantId> [--reason "<text>"] [--extend-on-resume]
|
|
322
|
+
*
|
|
323
|
+
* Mirrors the dashboard pause modal. Owner must be `sealcode login`ed
|
|
324
|
+
* and be admin (or owner) of the project.
|
|
325
|
+
*/
|
|
326
|
+
async function runPause({ grantId, reason, extendOnResume = false }) {
|
|
327
|
+
if (!grantId) throw new Error('Usage: sealcode pause <grantId> [--reason "<text>"] [--extend-on-resume]');
|
|
328
|
+
const body = {};
|
|
329
|
+
if (reason) body.reason = String(reason).slice(0, 500);
|
|
330
|
+
if (extendOnResume) body.extendsExpiry = true;
|
|
331
|
+
try {
|
|
332
|
+
await request(
|
|
333
|
+
'POST',
|
|
334
|
+
`/api/v1/grants/${encodeURIComponent(grantId)}/pause`,
|
|
335
|
+
{ auth: true, body },
|
|
336
|
+
);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err instanceof ApiError && err.status === 409 && err.apiCode === 'wrong_state') {
|
|
339
|
+
ui.fail(err.message || 'Grant is not in a pausable state.');
|
|
340
|
+
process.exitCode = 1;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
process.stdout.write(`⏸ paused grant ${grantId}\n`);
|
|
346
|
+
if (reason) process.stdout.write(` reason: ${reason}\n`);
|
|
347
|
+
if (extendOnResume) {
|
|
348
|
+
process.stdout.write(` extend-on-resume: expiry will shift forward by the paused duration\n`);
|
|
349
|
+
}
|
|
350
|
+
process.stdout.write(' Any live watcher will re-lock the recipient\'s files within seconds.\n');
|
|
351
|
+
process.stdout.write(` Resume with: ${ui.c.cyan(`sealcode resume ${grantId}`)}\n`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* sealcode@1.2.0 — Resume a paused access code from the CLI.
|
|
356
|
+
*
|
|
357
|
+
* sealcode resume <grantId>
|
|
358
|
+
*
|
|
359
|
+
* If the grant was paused with --extend-on-resume, the expiresAt shifts
|
|
360
|
+
* forward by the just-elapsed pause duration. The recipient must run
|
|
361
|
+
* `sealcode redeem <code>` again to re-open their working copy.
|
|
362
|
+
*/
|
|
363
|
+
async function runResume({ grantId }) {
|
|
364
|
+
if (!grantId) throw new Error('Usage: sealcode resume <grantId>');
|
|
365
|
+
let res;
|
|
366
|
+
try {
|
|
367
|
+
res = await request(
|
|
368
|
+
'POST',
|
|
369
|
+
`/api/v1/grants/${encodeURIComponent(grantId)}/resume`,
|
|
370
|
+
{ auth: true, body: {} },
|
|
371
|
+
);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (err instanceof ApiError) {
|
|
374
|
+
if (err.status === 410) {
|
|
375
|
+
ui.fail(err.message || 'Grant expired while paused.');
|
|
376
|
+
ui.hint(' Issue a fresh access code instead: `sealcode share ...`');
|
|
377
|
+
process.exitCode = 1;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (err.status === 409 && err.apiCode === 'wrong_state') {
|
|
381
|
+
ui.fail(err.message || 'Grant is not paused.');
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write(`▶ resumed grant ${grantId}\n`);
|
|
389
|
+
if (res.newExpiresAt) process.stdout.write(` expires: ${res.newExpiresAt}\n`);
|
|
390
|
+
if (res.extendedExpiry) {
|
|
391
|
+
const mins = Math.round((res.pausedDurationMs || 0) / 60000);
|
|
392
|
+
process.stdout.write(` expiry shifted forward by ~${mins}m (paused duration)\n`);
|
|
393
|
+
}
|
|
394
|
+
process.stdout.write(' Recipient must re-redeem the code to re-open the vault.\n');
|
|
395
|
+
}
|
|
396
|
+
|
|
288
397
|
async function runRevoke({ grantId }) {
|
|
289
398
|
if (!grantId) throw new Error('Usage: sealcode revoke <grantId>');
|
|
290
399
|
const res = await request(
|
|
@@ -550,4 +659,12 @@ async function runLockdown({ projectRoot, confirm = true } = {}) {
|
|
|
550
659
|
}
|
|
551
660
|
}
|
|
552
661
|
|
|
553
|
-
module.exports = {
|
|
662
|
+
module.exports = {
|
|
663
|
+
runShare,
|
|
664
|
+
runGrants,
|
|
665
|
+
runRevoke,
|
|
666
|
+
runRedeem,
|
|
667
|
+
runLockdown,
|
|
668
|
+
runPause,
|
|
669
|
+
runResume,
|
|
670
|
+
};
|
package/src/cli-watch.js
CHANGED
|
@@ -780,7 +780,14 @@ async function finalLock(projectRoot, config, code, reason, json, daemon) {
|
|
|
780
780
|
process.stdout.write(JSON.stringify({ type: 'locked', count: res.count, reason: label }) + '\n');
|
|
781
781
|
} else {
|
|
782
782
|
ui.ok(`[${ts()}] re-locked ${ui.c.bold(res.count)} files into ${ui.c.cyan(config.lockedDir + '/')} — session cleared`);
|
|
783
|
-
|
|
783
|
+
// sealcode@1.2.0 — be specific about the "paused" case so the
|
|
784
|
+
// recipient knows it's reversible and what their next step is.
|
|
785
|
+
if (label === 'paused') {
|
|
786
|
+
ui.hint(' This access code was PAUSED by the owner — not revoked.');
|
|
787
|
+
ui.hint(` Wait for the owner to resume, then run: ${ui.c.cyan(`sealcode redeem ${code}`)}`);
|
|
788
|
+
} else {
|
|
789
|
+
ui.hint(' Your local plaintext has been wiped. Ask the owner for a fresh code if you still need access.');
|
|
790
|
+
}
|
|
784
791
|
}
|
|
785
792
|
process.exit(0);
|
|
786
793
|
}
|
package/src/cli.js
CHANGED
|
@@ -33,7 +33,14 @@ const { installHook, uninstallHook } = require('./hooks');
|
|
|
33
33
|
const { exportBundle, importBundle } = require('./bundle');
|
|
34
34
|
const { runLogin, runSignout, runWhoami } = require('./cli-auth');
|
|
35
35
|
const { runLink, runUnlink, runLinkInfo } = require('./cli-link');
|
|
36
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
runShare,
|
|
38
|
+
runGrants,
|
|
39
|
+
runRevoke,
|
|
40
|
+
runRedeem,
|
|
41
|
+
runPause,
|
|
42
|
+
runResume,
|
|
43
|
+
} = require('./cli-grants');
|
|
37
44
|
const {
|
|
38
45
|
runCiCreate,
|
|
39
46
|
runCiList,
|
|
@@ -714,6 +721,7 @@ function build() {
|
|
|
714
721
|
singleDeviceEnforce: !!opts.singleDevice,
|
|
715
722
|
ndaText: opts.nda || null,
|
|
716
723
|
json: !!opts.json,
|
|
724
|
+
resolveKey,
|
|
717
725
|
});
|
|
718
726
|
} catch (err) {
|
|
719
727
|
process.exitCode = reportError(err);
|
|
@@ -747,6 +755,38 @@ function build() {
|
|
|
747
755
|
}
|
|
748
756
|
});
|
|
749
757
|
|
|
758
|
+
// -------- pause (sealcode@1.2.0) --------
|
|
759
|
+
program
|
|
760
|
+
.command('pause')
|
|
761
|
+
.argument('<grantId>', 'grant ID to pause (see `sealcode grants`)')
|
|
762
|
+
.description('Pause an active access code. Reversible: any live watcher re-locks immediately, but you can resume later.')
|
|
763
|
+
.option('--reason <text>', 'optional note visible to project admins (≤500 chars)')
|
|
764
|
+
.option('--extend-on-resume', 'on resume, shift expiry forward by the paused duration (default: clock keeps ticking)', false)
|
|
765
|
+
.action(async (grantId, opts) => {
|
|
766
|
+
try {
|
|
767
|
+
await runPause({
|
|
768
|
+
grantId,
|
|
769
|
+
reason: opts.reason,
|
|
770
|
+
extendOnResume: !!opts.extendOnResume,
|
|
771
|
+
});
|
|
772
|
+
} catch (err) {
|
|
773
|
+
process.exitCode = reportError(err);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// -------- resume (sealcode@1.2.0) --------
|
|
778
|
+
program
|
|
779
|
+
.command('resume')
|
|
780
|
+
.argument('<grantId>', 'grant ID to resume (see `sealcode grants`)')
|
|
781
|
+
.description('Resume a paused access code. The recipient must `sealcode redeem <code>` again to re-open their working copy.')
|
|
782
|
+
.action(async (grantId) => {
|
|
783
|
+
try {
|
|
784
|
+
await runResume({ grantId });
|
|
785
|
+
} catch (err) {
|
|
786
|
+
process.exitCode = reportError(err);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
750
790
|
// -------- lockdown (sealcode@1.1.0) --------
|
|
751
791
|
program
|
|
752
792
|
.command('lockdown')
|