@tinycloud/cli 1.2.0-beta.1 → 1.2.0-beta.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/README.md CHANGED
@@ -62,9 +62,15 @@ tc kv list
62
62
 
63
63
  # Manage network-encrypted secrets on the default network
64
64
  tc secrets network init
65
- tc secrets put ANTHROPIC_API_KEY "sk-..."
65
+ printf '%s' "$ANTHROPIC_API_KEY" | tc secrets put ANTHROPIC_API_KEY --stdin
66
66
  tc secrets get ANTHROPIC_API_KEY
67
67
 
68
+ # Ask the owner for a secret an agent needs, then wait for it
69
+ # (call TinyCloud's CLI by absolute path: a bare `tc` can be iproute2's /usr/sbin/tc)
70
+ TC_BIN="$(npm prefix --global)/bin/tc"
71
+ "$TC_BIN" --profile agent secrets request OPENAI_API_KEY --json # offline; prints the owner's link
72
+ "$TC_BIN" --profile agent secrets wait OPENAI_API_KEY --timeout 9m --json # 0 ready, 8 pending
73
+
68
74
  # Manage spaces
69
75
  tc space list
70
76
  tc space create
@@ -105,6 +111,9 @@ tc delegation create --to did:pkh:eip155:1:0x...
105
111
  | `tc secrets network show` | Show a secrets decryption network |
106
112
  | `tc secrets network init` | Create or fetch the default secrets network |
107
113
  | `tc secrets network grant` | Grant `tinycloud.encryption/decrypt` on a secrets network |
114
+ | `tc secrets request` | Print a Secret Manager link asking the owner to add secrets (offline) |
115
+ | `tc secrets wait` | Wait until secrets exist and are readable (never signs in; needs a stored session; exit 8 pending, 130 interrupted) |
116
+ | `tc secrets manage` | Print or open the Secret Manager page, `{url, opened}` |
108
117
  | `tc vars` | Manage environment variables |
109
118
  | `tc doctor` | Run diagnostic checks |
110
119
  | `tc completion` | Generate shell completions |
package/dist/index.js CHANGED
@@ -33,8 +33,12 @@ var init_constants = __esm({
33
33
  PERMISSION_DENIED: 5,
34
34
  NETWORK_ERROR: 6,
35
35
  NODE_ERROR: 7,
36
+ /** `tc secrets wait` only: the deadline passed while a secret was still missing. */
37
+ PENDING: 8,
36
38
  /** A write was refused because the owner's TinyCloud storage is full. */
37
- STORAGE_FULL: 10
39
+ STORAGE_FULL: 10,
40
+ /** Interrupted by SIGINT/SIGTERM (`CANCELLED`). */
41
+ CANCELLED: 130
38
42
  };
39
43
  }
40
44
  });
@@ -119,7 +123,7 @@ function warningLine(warnings) {
119
123
  return `${subject} not used (${which}). Approve access again when a command needs it; remove the old grant with tc auth grants remove <cid>.`;
120
124
  }
121
125
  function outputError(code3, message, hint, details = {}) {
122
- const { meta, warnings = [] } = details;
126
+ const { meta, warnings = [], setup, recovery, progress } = details;
123
127
  if (!shouldOutputJson()) {
124
128
  process.stderr.write(
125
129
  `${theme.error("\u2717")} ${theme.label(code3)}: ${message}
@@ -138,6 +142,9 @@ function outputError(code3, message, hint, details = {}) {
138
142
  error: { code: code3, message }
139
143
  };
140
144
  if (hint) payload.error.hint = hint;
145
+ if (recovery) payload.error.recovery = recovery;
146
+ if (setup) payload.error.setup = setup;
147
+ if (progress) payload.error.progress = progress;
141
148
  if (meta) payload.error.meta = meta;
142
149
  if (warnings.length > 0) payload.error.warnings = warnings;
143
150
  process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
@@ -267,6 +274,13 @@ var init_storage = __esm({
267
274
  import { readFileSync, readdirSync } from "fs";
268
275
  import { join } from "path";
269
276
  import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
277
+ import { isSafeRecovery } from "@tinycloud/operations/recovery";
278
+ import {
279
+ buildSecretRequestUrl,
280
+ isSafeRequestAccessRecovery,
281
+ isValidSecretName,
282
+ SECRET_REQUEST_MAX_NAMES
283
+ } from "@tinycloud/operations/secret-request";
270
284
  import { authorizationVerdictOf, parseCapabilityResource, SERVICE_LONG_TO_SHORT, validatedCapabilityOf } from "@tinycloud/sdk-core";
271
285
  function setActiveProfileName(name) {
272
286
  activeProfileName = name;
@@ -342,12 +356,64 @@ function handleError(error) {
342
356
  meta.resource = capability.resource;
343
357
  meta.requiredAction = capability.requiredAction;
344
358
  }
359
+ const deniedNames = cliError.metadata?.deniedNames;
360
+ const recovery = safeCliRecovery(cliError.metadata?.recovery, {
361
+ deniedNames: Array.isArray(deniedNames) && deniedNames.every((name) => typeof name === "string") ? deniedNames : []
362
+ });
363
+ const setup = cliError.code === "NOT_FOUND" || cliError.code === "PERMISSION_DENIED" ? safeCliSetup(cliError.metadata?.setup) : void 0;
364
+ const progress = cliError.code === "NETWORK_ERROR" || cliError.code === "CANCELLED" ? safeCliProgress(cliError.metadata?.progress) : void 0;
345
365
  outputError(cliError.code, cliError.message, hint, {
366
+ ...recovery ? { recovery } : {},
367
+ ...setup ? { setup } : {},
368
+ ...progress ? { progress } : {},
346
369
  ...Object.keys(meta).length ? { meta } : {},
347
370
  warnings: mergeWarnings(drainWarnings(), operationWarnings(cliError.metadata?.warnings))
348
371
  });
349
372
  process.exit(cliError.exitCode);
350
373
  }
374
+ function safeCliRecovery(value, context) {
375
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !("action" in value)) return void 0;
376
+ const action = value.action;
377
+ switch (action) {
378
+ case "sign_in":
379
+ case "complete_migration":
380
+ return isSafeRecovery(value, action) ? { ...value } : void 0;
381
+ case "request_access":
382
+ return isSafeRequestAccessRecovery(value, context.deniedNames) ? { ...value, names: [...value.names] } : void 0;
383
+ default:
384
+ return void 0;
385
+ }
386
+ }
387
+ function safeCliSetup(value) {
388
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
389
+ const setup = { ...value };
390
+ if (!Object.keys(setup).every((key) => SETUP_KEYS[key] === true)) return void 0;
391
+ const { url, names, scope, owner, action } = setup;
392
+ if (action !== "add_secret" || typeof url !== "string" || typeof owner !== "string") return void 0;
393
+ if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) return void 0;
394
+ if (scope !== void 0 && typeof scope !== "string") return void 0;
395
+ try {
396
+ const link2 = buildSecretRequestUrl({ owner, names, ...scope !== void 0 ? { scope } : {} });
397
+ if (link2.url !== url || link2.owner !== owner || link2.names.length !== names.length) return void 0;
398
+ } catch {
399
+ return void 0;
400
+ }
401
+ return { url, names: [...names], ...scope !== void 0 ? { scope } : {}, owner, action };
402
+ }
403
+ function safeNameList(value) {
404
+ if (!Array.isArray(value) || value.length > SECRET_REQUEST_MAX_NAMES) return void 0;
405
+ if (!value.every((name) => typeof name === "string" && isValidSecretName(name))) return void 0;
406
+ if (new Set(value).size !== value.length) return void 0;
407
+ return [...value];
408
+ }
409
+ function safeCliProgress(value) {
410
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
411
+ if (!("present" in value) || !("pending" in value) || Object.keys(value).length !== 2) return void 0;
412
+ const present = safeNameList(value.present);
413
+ const pending = safeNameList(value.pending);
414
+ if (!present || !pending || present.some((name) => pending.includes(name))) return void 0;
415
+ return { present, pending };
416
+ }
351
417
  function buildAuthHint(error) {
352
418
  const capability = validatedCapabilityOf({ meta: error.metadata });
353
419
  if (!capability) return void 0;
@@ -409,7 +475,7 @@ function buildNetworkHint() {
409
475
  lines.push(`Or override per-command with --host or TC_HOST.`);
410
476
  return lines.join("\n");
411
477
  }
412
- var activeProfileName, CLIError;
478
+ var activeProfileName, CLIError, SETUP_KEYS;
413
479
  var init_errors = __esm({
414
480
  "src/output/errors.ts"() {
415
481
  "use strict";
@@ -428,6 +494,7 @@ var init_errors = __esm({
428
494
  }
429
495
  status;
430
496
  };
497
+ SETUP_KEYS = { url: true, names: true, scope: true, owner: true, action: true };
431
498
  }
432
499
  });
433
500
 
@@ -5464,6 +5531,8 @@ import {
5464
5531
  buildPermissionRequestArtifact,
5465
5532
  isPermissionRequestArtifact
5466
5533
  } from "@tinycloud/operations/artifacts";
5534
+ import { isLocalOwnerProfile } from "@tinycloud/operations/profile";
5535
+ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
5467
5536
  import {
5468
5537
  additionalDelegationsPath as sharedAdditionalDelegationsPath,
5469
5538
  authRequestsPath as sharedAuthRequestsPath,
@@ -5501,14 +5570,17 @@ var init_permissions = __esm({
5501
5570
  });
5502
5571
 
5503
5572
  // src/auth/session-expired.ts
5573
+ import { signInRecovery } from "@tinycloud/operations/recovery";
5504
5574
  function signInAgainHint(profileName, posture) {
5505
- if (posture === "local-owner-key") {
5506
- return `Sign in again with: tc --profile ${profileName} auth login --method local`;
5507
- }
5508
- if (posture === "delegate-session") {
5509
- return `Have the owner approve a new scoped login: tc --profile ${profileName} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
5575
+ const delegate = posture === "delegate-session";
5576
+ const { command } = signInRecovery(
5577
+ profileName,
5578
+ delegate ? "delegate-session" : posture === "local-owner-key" ? "local-owner-key" : "owner-openkey"
5579
+ );
5580
+ if (command === void 0) {
5581
+ return delegate ? `Have the owner approve a new scoped login for profile ${JSON.stringify(profileName)} with tc auth login --method openkey --paste --manifest <manifest.json>.` : `Sign in again to profile ${JSON.stringify(profileName)} with tc auth login.`;
5510
5582
  }
5511
- return `Sign in again with: tc --profile ${profileName} auth login --method openkey`;
5583
+ return delegate ? `Have the owner approve a new scoped login: ${command}. Pass the owner's code on stdin, newline-terminated.` : `Sign in again with: ${command}`;
5512
5584
  }
5513
5585
  function sessionExpiredError(profileName, posture) {
5514
5586
  return new CLIError(