@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.
@@ -60,8 +60,12 @@ var init_constants = __esm({
60
60
  PERMISSION_DENIED: 5,
61
61
  NETWORK_ERROR: 6,
62
62
  NODE_ERROR: 7,
63
+ /** `tc secrets wait` only: the deadline passed while a secret was still missing. */
64
+ PENDING: 8,
63
65
  /** A write was refused because the owner's TinyCloud storage is full. */
64
- STORAGE_FULL: 10
66
+ STORAGE_FULL: 10,
67
+ /** Interrupted by SIGINT/SIGTERM (`CANCELLED`). */
68
+ CANCELLED: 130
65
69
  };
66
70
  }
67
71
  });
@@ -216,7 +220,7 @@ function warningLine(warnings) {
216
220
  return `${subject} not used (${which}). Approve access again when a command needs it; remove the old grant with tc auth grants remove <cid>.`;
217
221
  }
218
222
  function outputError(code3, message, hint, details = {}) {
219
- const { meta, warnings = [] } = details;
223
+ const { meta, warnings = [], setup, recovery, progress } = details;
220
224
  if (!shouldOutputJson()) {
221
225
  process.stderr.write(
222
226
  `${theme.error("\u2717")} ${theme.label(code3)}: ${message}
@@ -235,6 +239,9 @@ function outputError(code3, message, hint, details = {}) {
235
239
  error: { code: code3, message }
236
240
  };
237
241
  if (hint) payload.error.hint = hint;
242
+ if (recovery) payload.error.recovery = recovery;
243
+ if (setup) payload.error.setup = setup;
244
+ if (progress) payload.error.progress = progress;
238
245
  if (meta) payload.error.meta = meta;
239
246
  if (warnings.length > 0) payload.error.warnings = warnings;
240
247
  process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
@@ -422,6 +429,13 @@ var init_storage2 = __esm({
422
429
  import { readFileSync, readdirSync } from "fs";
423
430
  import { join as join2 } from "path";
424
431
  import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
432
+ import { isSafeRecovery } from "@tinycloud/operations/recovery";
433
+ import {
434
+ buildSecretRequestUrl,
435
+ isSafeRequestAccessRecovery,
436
+ isValidSecretName,
437
+ SECRET_REQUEST_MAX_NAMES
438
+ } from "@tinycloud/operations/secret-request";
425
439
  import { authorizationVerdictOf, parseCapabilityResource, SERVICE_LONG_TO_SHORT, validatedCapabilityOf } from "@tinycloud/sdk-core";
426
440
  function setActiveProfileName(name) {
427
441
  activeProfileName = name;
@@ -519,12 +533,64 @@ function handleError(error) {
519
533
  meta.resource = capability.resource;
520
534
  meta.requiredAction = capability.requiredAction;
521
535
  }
536
+ const deniedNames = cliError.metadata?.deniedNames;
537
+ const recovery = safeCliRecovery(cliError.metadata?.recovery, {
538
+ deniedNames: Array.isArray(deniedNames) && deniedNames.every((name) => typeof name === "string") ? deniedNames : []
539
+ });
540
+ const setup = cliError.code === "NOT_FOUND" || cliError.code === "PERMISSION_DENIED" ? safeCliSetup(cliError.metadata?.setup) : void 0;
541
+ const progress = cliError.code === "NETWORK_ERROR" || cliError.code === "CANCELLED" ? safeCliProgress(cliError.metadata?.progress) : void 0;
522
542
  outputError(cliError.code, cliError.message, hint, {
543
+ ...recovery ? { recovery } : {},
544
+ ...setup ? { setup } : {},
545
+ ...progress ? { progress } : {},
523
546
  ...Object.keys(meta).length ? { meta } : {},
524
547
  warnings: mergeWarnings(drainWarnings(), operationWarnings(cliError.metadata?.warnings))
525
548
  });
526
549
  process.exit(cliError.exitCode);
527
550
  }
551
+ function safeCliRecovery(value, context) {
552
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !("action" in value)) return void 0;
553
+ const action = value.action;
554
+ switch (action) {
555
+ case "sign_in":
556
+ case "complete_migration":
557
+ return isSafeRecovery(value, action) ? { ...value } : void 0;
558
+ case "request_access":
559
+ return isSafeRequestAccessRecovery(value, context.deniedNames) ? { ...value, names: [...value.names] } : void 0;
560
+ default:
561
+ return void 0;
562
+ }
563
+ }
564
+ function safeCliSetup(value) {
565
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
566
+ const setup = { ...value };
567
+ if (!Object.keys(setup).every((key) => SETUP_KEYS[key] === true)) return void 0;
568
+ const { url, names, scope, owner, action } = setup;
569
+ if (action !== "add_secret" || typeof url !== "string" || typeof owner !== "string") return void 0;
570
+ if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) return void 0;
571
+ if (scope !== void 0 && typeof scope !== "string") return void 0;
572
+ try {
573
+ const link3 = buildSecretRequestUrl({ owner, names, ...scope !== void 0 ? { scope } : {} });
574
+ if (link3.url !== url || link3.owner !== owner || link3.names.length !== names.length) return void 0;
575
+ } catch {
576
+ return void 0;
577
+ }
578
+ return { url, names: [...names], ...scope !== void 0 ? { scope } : {}, owner, action };
579
+ }
580
+ function safeNameList(value) {
581
+ if (!Array.isArray(value) || value.length > SECRET_REQUEST_MAX_NAMES) return void 0;
582
+ if (!value.every((name) => typeof name === "string" && isValidSecretName(name))) return void 0;
583
+ if (new Set(value).size !== value.length) return void 0;
584
+ return [...value];
585
+ }
586
+ function safeCliProgress(value) {
587
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
588
+ if (!("present" in value) || !("pending" in value) || Object.keys(value).length !== 2) return void 0;
589
+ const present = safeNameList(value.present);
590
+ const pending = safeNameList(value.pending);
591
+ if (!present || !pending || present.some((name) => pending.includes(name))) return void 0;
592
+ return { present, pending };
593
+ }
528
594
  function buildAuthHint(error) {
529
595
  const capability = validatedCapabilityOf({ meta: error.metadata });
530
596
  if (!capability) return void 0;
@@ -586,7 +652,7 @@ function buildNetworkHint() {
586
652
  lines.push(`Or override per-command with --host or TC_HOST.`);
587
653
  return lines.join("\n");
588
654
  }
589
- var activeProfileName, CLIError;
655
+ var activeProfileName, CLIError, SETUP_KEYS;
590
656
  var init_errors = __esm({
591
657
  "src/output/errors.ts"() {
592
658
  "use strict";
@@ -605,6 +671,7 @@ var init_errors = __esm({
605
671
  }
606
672
  status;
607
673
  };
674
+ SETUP_KEYS = { url: true, names: true, scope: true, owner: true, action: true };
608
675
  }
609
676
  });
610
677
 
@@ -7470,6 +7537,12 @@ function defaultVaultMessage(input) {
7470
7537
  return input.message ?? `Public key not found for ${input.did}`;
7471
7538
  case "STORAGE_ERROR":
7472
7539
  return input.message ?? input.cause.message;
7540
+ case "ALREADY_EXISTS":
7541
+ return input.message ?? `A value already exists at ${input.key}`;
7542
+ case "AUTH_UNAUTHORIZED":
7543
+ return input.message;
7544
+ case "PERMISSION_DENIED":
7545
+ return input.message;
7473
7546
  case "STORAGE_QUOTA_EXCEEDED":
7474
7547
  case "STORAGE_LIMIT_REACHED":
7475
7548
  return input.message;
@@ -7483,13 +7556,33 @@ function vaultError(input) {
7483
7556
  };
7484
7557
  return { ok: false, error };
7485
7558
  }
7559
+ function typedKvCause(error) {
7560
+ return Object.assign(new Error(error.message), {
7561
+ name: "ServiceError",
7562
+ code: error.code,
7563
+ service: error.service,
7564
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
7565
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
7566
+ });
7567
+ }
7486
7568
  function kvWriteError(context, error) {
7487
7569
  if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
7488
7570
  return vaultError({ code: error.code, message: error.message, meta: error.meta });
7489
7571
  }
7572
+ const message = `${context}: ${error.message}`;
7573
+ if (error.code === ErrorCodes.AUTH_UNAUTHORIZED) {
7574
+ const code3 = authorizationVerdictOf2(error) === "unauthenticated" ? "AUTH_UNAUTHORIZED" : "PERMISSION_DENIED";
7575
+ return vaultError({
7576
+ code: code3,
7577
+ message,
7578
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
7579
+ cause: typedKvCause(error)
7580
+ });
7581
+ }
7490
7582
  return vaultError({
7491
7583
  code: "STORAGE_ERROR",
7492
- cause: new Error(`${context}: ${error.message}`)
7584
+ cause: new Error(message, { cause: typedKvCause(error) }),
7585
+ ...error.meta !== void 0 ? { meta: error.meta } : {}
7493
7586
  });
7494
7587
  }
7495
7588
  function canonicalizeSecretScope(scope) {
@@ -8229,7 +8322,7 @@ function encOk(data) {
8229
8322
  function encErr(error) {
8230
8323
  return { ok: false, error };
8231
8324
  }
8232
- var import_decrypt_transport_response_error, ErrorCodes, defaultRetryPolicy, TelemetryEvents, REDACTED, SAFE_NUMBER_FIELDS, SAFE_BOOLEAN_FIELDS, DEBUG_FLAG, MAX_EVENTS, TinyCloudDebugLogger, tinyCloudDebugLogger, ServiceErrorSchema, GenericResultSchema, KVResponseHeadersSchema, GenericKVResponseSchema, KVListResponseSchema, KVListResultSchema, ServiceRequestEventSchema, ServiceResponseEventSchema, ServiceErrorEventSchema, ServiceRetryEventSchema, TelemetrySpanEventSchema, RetryPolicySchema, ServiceSessionSchema, URN_PREFIX, NETWORK_NAME_RE, PKH_EIP155_DID_RE, NetworkIdError, RequestTimeoutError, grantableActions, capabilityServices, MAX_CAUSE_DEPTH2, STORAGE_FULL_MESSAGE, STORAGE_WRITE_TOO_LARGE_MESSAGE, MAX_TIMER_DELAY_MS, BaseService, PrefixedKVService, DEFAULT_SIGNED_READ_URL_EXPIRY_MS, KVAction, MAX_KV_BATCH_READ_ITEMS, pathByteEncoder, REVOKED_BODY, KVService, SQLMigrations, DatabaseHandle, SQLAction, DDL_TOKENS, MIGRATIONS_TABLE, MIGRATIONS_SCHEMA, MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID, SQLService, DuckDbDatabaseHandle, DuckDbAction, DuckDbService, AsyncQueue, HOOK_STREAM_RETRY_BASE_DELAY_MS, HOOK_STREAM_RETRY_MAX_DELAY_MS, HOOK_STREAM_HEALTHY_MS, HooksService, SUPERSEDED, STREAM_ERROR_NAMES, STREAM_ERROR_CODES, VaultVersionConfig, CURRENT_VAULT_VERSION, VaultHeaders, DB_NAME, DB_VERSION, STORE_NAME, WRAP_KEY_ID, DataVaultService, SECRET_NAME_RE, SECRET_PREFIX, SCOPED_SECRET_PREFIX, RESERVED_SECRET_SCOPES, HEX, DEFAULT_ENCRYPTION_ALG, ENVELOPE_VERSION, DEFAULT_KEY_VERSION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, EncryptionService;
8325
+ var import_decrypt_transport_response_error, ErrorCodes, defaultRetryPolicy, TelemetryEvents, REDACTED, SAFE_NUMBER_FIELDS, SAFE_BOOLEAN_FIELDS, DEBUG_FLAG, MAX_EVENTS, TinyCloudDebugLogger, tinyCloudDebugLogger, ServiceErrorSchema, GenericResultSchema, KVResponseHeadersSchema, GenericKVResponseSchema, KVListResponseSchema, KVListResultSchema, ServiceRequestEventSchema, ServiceResponseEventSchema, ServiceErrorEventSchema, ServiceRetryEventSchema, TelemetrySpanEventSchema, RetryPolicySchema, ServiceSessionSchema, URN_PREFIX, NETWORK_NAME_RE, PKH_EIP155_DID_RE, NetworkIdError, RequestTimeoutError, grantableActions, capabilityServices, MAX_CAUSE_DEPTH2, STORAGE_FULL_MESSAGE, STORAGE_WRITE_TOO_LARGE_MESSAGE, MAX_TIMER_DELAY_MS, BaseService, PrefixedKVService, DEFAULT_SIGNED_READ_URL_EXPIRY_MS, KVAction, MAX_KV_BATCH_READ_ITEMS, pathByteEncoder, REVOKED_BODY, KVService, SQLMigrations, DatabaseHandle, SQLAction, DDL_TOKENS, MIGRATIONS_TABLE, MIGRATIONS_SCHEMA, MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID, SQLService, DuckDbDatabaseHandle, DuckDbAction, DuckDbService, AsyncQueue, HOOK_STREAM_RETRY_BASE_DELAY_MS, HOOK_STREAM_RETRY_MAX_DELAY_MS, HOOK_STREAM_HEALTHY_MS, HooksService, SUPERSEDED, STREAM_ERROR_NAMES, STREAM_ERROR_CODES, VaultVersionConfig, CURRENT_VAULT_VERSION, VaultHeaders, DB_NAME, DB_VERSION, STORE_NAME, WRAP_KEY_ID, DataVaultService, SECRET_NAME_RE, SECRET_PREFIX, SCOPED_SECRET_PREFIX, RESERVED_SECRET_SCOPES, TC832_RESERVED_SCOPES, HEX, DEFAULT_ENCRYPTION_ALG, ENVELOPE_VERSION, DEFAULT_KEY_VERSION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, EncryptionService;
8233
8326
  var init_dist2 = __esm({
8234
8327
  "../sdk-services/dist/index.js"() {
8235
8328
  "use strict";
@@ -9529,7 +9622,7 @@ var init_dist2 = __esm({
9529
9622
  );
9530
9623
  }
9531
9624
  if (response.status === 404) {
9532
- return this.classifyNotFound(response, key);
9625
+ return await this.classifyNotFound(response, key);
9533
9626
  }
9534
9627
  const errorText = await response.text();
9535
9628
  if (response.status === 413) {
@@ -10098,7 +10191,7 @@ var init_dist2 = __esm({
10098
10191
  );
10099
10192
  }
10100
10193
  if (response.status === 404) {
10101
- return this.classifyNotFound(response, key);
10194
+ return await this.classifyNotFound(response, key);
10102
10195
  }
10103
10196
  const errorText = await response.text();
10104
10197
  if (response.status === 412) {
@@ -10165,7 +10258,7 @@ var init_dist2 = __esm({
10165
10258
  );
10166
10259
  }
10167
10260
  if (response.status === 404) {
10168
- return this.classifyNotFound(response, key);
10261
+ return await this.classifyNotFound(response, key);
10169
10262
  }
10170
10263
  const errorText = await response.text();
10171
10264
  return err(
@@ -10231,7 +10324,7 @@ var init_dist2 = __esm({
10231
10324
  signal: request.signal
10232
10325
  });
10233
10326
  if (!response.ok) {
10234
- return this.createSignedReadUrlError(response, key, path);
10327
+ return await this.createSignedReadUrlError(response, key, path);
10235
10328
  }
10236
10329
  const signedUrl = this.normalizeSignedReadUrlResponse(
10237
10330
  await response.json()
@@ -12008,11 +12101,18 @@ var init_dist2 = __esm({
12008
12101
  cause: new Error(envelopeResult.error.message)
12009
12102
  });
12010
12103
  }
12011
- const valuePutResult = await this.tc.kv.put(
12012
- `vault/${key}`,
12013
- JSON.stringify(envelopeResult.data)
12014
- );
12104
+ const valuePath = `vault/${key}`;
12105
+ const valueBody = JSON.stringify(envelopeResult.data);
12106
+ const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
12015
12107
  if (!valuePutResult.ok) {
12108
+ if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
12109
+ return vaultError({
12110
+ code: "ALREADY_EXISTS",
12111
+ key,
12112
+ ...valuePutResult.error.meta !== void 0 ? { meta: valuePutResult.error.meta } : {},
12113
+ cause: typedKvCause(valuePutResult.error)
12114
+ });
12115
+ }
12016
12116
  return kvWriteError("Failed to store encrypted value", valuePutResult.error);
12017
12117
  }
12018
12118
  return { ok: true, data: void 0 };
@@ -12180,6 +12280,14 @@ var init_dist2 = __esm({
12180
12280
  if (this.usesNetworkEncryption) {
12181
12281
  return this.putNetworkEncrypted(key, value, options);
12182
12282
  }
12283
+ if (options?.ifNoneMatch !== void 0) {
12284
+ return vaultError({
12285
+ code: "STORAGE_ERROR",
12286
+ cause: new Error(
12287
+ "ifNoneMatch is only supported by network-encrypted vaults"
12288
+ )
12289
+ });
12290
+ }
12183
12291
  if (!this._isUnlocked || !this.masterKey) {
12184
12292
  return vaultError({
12185
12293
  code: "VAULT_LOCKED",
@@ -12973,6 +13081,11 @@ var init_dist2 = __esm({
12973
13081
  SECRET_PREFIX = "secrets/";
12974
13082
  SCOPED_SECRET_PREFIX = "secrets/scoped/";
12975
13083
  RESERVED_SECRET_SCOPES = /* @__PURE__ */ new Set(["default", "global"]);
13084
+ TC832_RESERVED_SCOPES = Object.freeze([
13085
+ "default",
13086
+ "global",
13087
+ "scoped"
13088
+ ]);
12976
13089
  HEX = "0123456789abcdef";
12977
13090
  DEFAULT_ENCRYPTION_ALG = "x25519-aes256gcm/v1";
12978
13091
  ENVELOPE_VERSION = 1;
@@ -13904,6 +14017,8 @@ import {
13904
14017
  buildPermissionRequestArtifact,
13905
14018
  isPermissionRequestArtifact
13906
14019
  } from "@tinycloud/operations/artifacts";
14020
+ import { isLocalOwnerProfile } from "@tinycloud/operations/profile";
14021
+ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
13907
14022
  import {
13908
14023
  additionalDelegationsPath as sharedAdditionalDelegationsPath,
13909
14024
  authRequestsPath as sharedAuthRequestsPath,
@@ -14190,17 +14305,26 @@ async function secretPermissionsFromAppManifest(manifest, profile, requestedOwne
14190
14305
  }
14191
14306
  async function defaultSecretsNetworkId(profileName2, requestedOwner) {
14192
14307
  const profile = await ProfileManager.getProfile(profileName2);
14193
- const ownDid = profile.did?.split("#")[0];
14194
- const recorded = profile.ownerDid ?? (ownDid?.startsWith("did:pkh:") ? ownDid : void 0);
14195
- const owner = (recorded ?? requestedOwner)?.split("#")[0];
14196
- if (!owner) {
14197
- throw new CLIError(
14308
+ const requested = requestedOwner === void 0 ? void 0 : canonicalOwnerDid(requestedOwner.split("#")[0]);
14309
+ const resolution = resolveSecretsOwner({
14310
+ local: isLocalOwnerProfile(profile),
14311
+ posture: resolveProfilePosture(profile),
14312
+ ...profile.ownerDid === void 0 ? {} : { ownerDid: profile.ownerDid.split("#")[0] },
14313
+ ...profile.did === void 0 ? {} : { principalDid: profile.did.split("#")[0] },
14314
+ ...profile.spaceId === void 0 ? {} : { space: profile.spaceId }
14315
+ }, requested);
14316
+ if (!resolution.ok) {
14317
+ throw resolution.code === "OWNER_MISMATCH" ? new CLIError(
14318
+ "OWNER_MISMATCH",
14319
+ `--owner does not match the secrets owner of profile "${profileName2}". ${resolution.message}`,
14320
+ ExitCode.USAGE_ERROR
14321
+ ) : new CLIError(
14198
14322
  "OWNER_DID_UNKNOWN",
14199
14323
  `Cannot determine the secrets owner for profile "${profileName2}". Pass --owner did:pkh:eip155:CHAIN:ADDRESS with --manifest.`,
14200
14324
  ExitCode.AUTH_REQUIRED
14201
14325
  );
14202
14326
  }
14203
- return `urn:tinycloud:encryption:${canonicalOwnerDid(owner, "Secrets owner")}:default`;
14327
+ return `urn:tinycloud:encryption:${resolution.owner}:default`;
14204
14328
  }
14205
14329
  function permissionsFromDelegation(delegation) {
14206
14330
  if (delegation.resources?.length) {
@@ -14335,14 +14459,17 @@ var init_permissions = __esm({
14335
14459
  });
14336
14460
 
14337
14461
  // src/auth/session-expired.ts
14462
+ import { signInRecovery } from "@tinycloud/operations/recovery";
14338
14463
  function signInAgainHint(profileName2, posture) {
14339
- if (posture === "local-owner-key") {
14340
- return `Sign in again with: tc --profile ${profileName2} auth login --method local`;
14341
- }
14342
- if (posture === "delegate-session") {
14343
- return `Have the owner approve a new scoped login: tc --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
14464
+ const delegate = posture === "delegate-session";
14465
+ const { command } = signInRecovery(
14466
+ profileName2,
14467
+ delegate ? "delegate-session" : posture === "local-owner-key" ? "local-owner-key" : "owner-openkey"
14468
+ );
14469
+ if (command === void 0) {
14470
+ return delegate ? `Have the owner approve a new scoped login for profile ${JSON.stringify(profileName2)} with tc auth login --method openkey --paste --manifest <manifest.json>.` : `Sign in again to profile ${JSON.stringify(profileName2)} with tc auth login.`;
14344
14471
  }
14345
- return `Sign in again with: tc --profile ${profileName2} auth login --method openkey`;
14472
+ 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}`;
14346
14473
  }
14347
14474
  function sessionExpiredError(profileName2, posture) {
14348
14475
  return new CLIError(
@@ -19439,8 +19566,8 @@ async function callbackFlow(did, options = {}) {
19439
19566
  }
19440
19567
  if (openBrowser) {
19441
19568
  try {
19442
- const open7 = (await import("open")).default;
19443
- await open7(authUrl);
19569
+ const open8 = (await import("open")).default;
19570
+ await open8(authUrl);
19444
19571
  } catch {
19445
19572
  settle({ error: new Error("Failed to open browser") });
19446
19573
  return;
@@ -19593,8 +19720,8 @@ async function localKeySignIn(options) {
19593
19720
  // src/auth/scoped-login.ts
19594
19721
  init_errors();
19595
19722
  init_constants();
19596
- init_types();
19597
19723
  import { NodeWasmBindings, signedCompactUcanExpiry as signedCompactUcanExpiry2 } from "@tinycloud/node-sdk";
19724
+ import { isLocalOwnerProfile as isLocalOwnerProfile2 } from "@tinycloud/operations/profile";
19598
19725
 
19599
19726
  // src/lib/duration.ts
19600
19727
  function parseDuration(input) {
@@ -19652,11 +19779,8 @@ function openKeyExpiryParam(expiry) {
19652
19779
  }
19653
19780
  return `${seconds}s`;
19654
19781
  }
19655
- function isLocalOwnerProfile(profile) {
19656
- return resolveProfilePosture(profile) === "local-owner-key" || profile.authMethod === "local" || typeof profile.privateKey === "string";
19657
- }
19658
19782
  function pinnedOwner(profile) {
19659
- return profile && !isLocalOwnerProfile(profile) ? profile.ownerDid : void 0;
19783
+ return profile && !isLocalOwnerProfile2(profile) ? profile.ownerDid : void 0;
19660
19784
  }
19661
19785
  var TRUST_FIELDS = ["ownerDid", "permissions", "permissionsSource", "expiresAt", "expiry", "expirationTime"];
19662
19786
  function withoutTrustFields(session) {
@@ -19907,9 +20031,10 @@ init_constants();
19907
20031
  init_profiles();
19908
20032
  init_errors();
19909
20033
  init_space();
20034
+ import { isLocalOwnerProfile as isLocalOwnerProfile3 } from "@tinycloud/operations/profile";
19910
20035
  import { ProfileLockTimeoutError as ProfileLockTimeoutError2 } from "@tinycloud/operations/state";
19911
20036
  function assertNotLocalOwner(profileName2, profile, flow) {
19912
- if (profile === null || !isLocalOwnerProfile(profile)) return;
20037
+ if (profile === null || !isLocalOwnerProfile3(profile)) return;
19913
20038
  throw new CLIError(
19914
20039
  "LOCAL_OWNER_PROFILE",
19915
20040
  `Profile "${profileName2}" holds a local owner key. ${flow} would turn it into an OpenKey profile while keeping that key. Use a separate profile: \`tc init --name publisher --key-only\`, then \`tc --profile publisher auth login --device --manifest ...\`.`,
@@ -24160,8 +24285,21 @@ init_warnings();
24160
24285
  init_constants();
24161
24286
  import { readFile as readFile9 } from "fs/promises";
24162
24287
  import { join as join9 } from "path";
24163
- import { homedir } from "os";
24288
+ import { homedir, hostname, userInfo } from "os";
24289
+ import { appSecretScope } from "@tinycloud/sdk-core";
24164
24290
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
24291
+ import { isLocalOwnerProfile as isLocalOwnerProfile4 } from "@tinycloud/operations/profile";
24292
+ import {
24293
+ buildSecretManagerUrl,
24294
+ buildSecretRequestUrl as buildSecretRequestUrl2,
24295
+ canonicalSecretRequestOwner,
24296
+ isValidSecretRequestScope,
24297
+ missingSecretRecovery,
24298
+ ownerOfSpace,
24299
+ resolveSecretsOwner as resolveSecretsOwner2,
24300
+ SecretRequestInputError,
24301
+ validSecretNames
24302
+ } from "@tinycloud/operations/secret-request";
24165
24303
  import {
24166
24304
  SECRET_DECRYPT_CAPABILITY,
24167
24305
  secretCapabilityAction
@@ -24170,6 +24308,402 @@ import { invokeSecretsGetWithLocalAuthorityRetry } from "@tinycloud/operations/c
24170
24308
  init_sdk();
24171
24309
  init_session_expired();
24172
24310
  init_space();
24311
+
24312
+ // src/lib/browser.ts
24313
+ init_formatter();
24314
+ init_theme();
24315
+ import open5 from "open";
24316
+ var LAUNCH_CHECK_MS = 1e3;
24317
+ async function tryOpenBrowser(url) {
24318
+ let launcher;
24319
+ try {
24320
+ launcher = await open5(url);
24321
+ } catch {
24322
+ return false;
24323
+ }
24324
+ return new Promise((resolve5) => {
24325
+ const timer = setTimeout(() => resolve5(true), LAUNCH_CHECK_MS);
24326
+ launcher.once("error", () => {
24327
+ clearTimeout(timer);
24328
+ resolve5(false);
24329
+ });
24330
+ launcher.once("exit", (code3) => {
24331
+ clearTimeout(timer);
24332
+ resolve5(code3 === 0);
24333
+ });
24334
+ });
24335
+ }
24336
+ function warnBrowserNotOpened() {
24337
+ process.stderr.write(shouldOutputJson() ? `${JSON.stringify({ warnings: [{ code: "BROWSER_OPEN_FAILED", reason: "open_failed" }] })}
24338
+ ` : `${theme.warn("!")} Could not open a browser. Open the URL above yourself.
24339
+ `);
24340
+ }
24341
+
24342
+ // src/lib/probe-child.ts
24343
+ import { spawn as nodeSpawn } from "child_process";
24344
+ var ATTEMPT_BUDGET_MS = 2e4;
24345
+ var KILL_GRACE_MS = 500;
24346
+ var MAX_PROBE_LINE_BYTES = 64 * 1024;
24347
+ var MAX_STDERR_BYTES = 64 * 1024;
24348
+ var STATUS_OPERATION_ID = "tinycloud.secrets.status";
24349
+ var ERROR_CODE_RE = /^[A-Z][A-Z0-9_]{0,63}$/;
24350
+ function probeCommandArgs(request) {
24351
+ return [
24352
+ "--json",
24353
+ "--profile",
24354
+ request.profile,
24355
+ "--host",
24356
+ request.host,
24357
+ "secrets",
24358
+ "probe",
24359
+ ...request.names,
24360
+ ...request.scope === void 0 ? [] : ["--scope", request.scope],
24361
+ ...request.owner === void 0 ? [] : ["--owner", request.owner]
24362
+ ];
24363
+ }
24364
+ function probeChildArgv(request) {
24365
+ const script = process.argv[1];
24366
+ if (script === void 0) throw new Error("Cannot locate the tc entry point for the probe child.");
24367
+ return [...process.execArgv, script, ...probeCommandArgs(request)];
24368
+ }
24369
+ function runProbeChild(request, options = {}) {
24370
+ const command = options.command ?? process.execPath;
24371
+ const args = options.argv ?? probeChildArgv(request);
24372
+ const budgetMs = options.budgetMs ?? ATTEMPT_BUDGET_MS;
24373
+ const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
24374
+ const spawn2 = options.spawn ?? nodeSpawn;
24375
+ const signal = options.signal;
24376
+ return new Promise((resolve5) => {
24377
+ if (signal?.aborted) {
24378
+ resolve5({ kind: "cancelled" });
24379
+ return;
24380
+ }
24381
+ const child = spawn2(command, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
24382
+ let outcome;
24383
+ let stdout = Buffer.alloc(0);
24384
+ let stderr = Buffer.alloc(0);
24385
+ let exited = false;
24386
+ let stopping = false;
24387
+ let graceTimer;
24388
+ const stop = () => {
24389
+ if (exited || stopping) return;
24390
+ stopping = true;
24391
+ child.kill("SIGTERM");
24392
+ graceTimer = setTimeout(() => {
24393
+ if (!exited) child.kill("SIGKILL");
24394
+ }, killGraceMs);
24395
+ };
24396
+ const settle = (value) => {
24397
+ if (outcome !== void 0) return;
24398
+ outcome = value;
24399
+ stop();
24400
+ };
24401
+ const onAbort = () => settle({ kind: "cancelled" });
24402
+ const budgetTimer = setTimeout(() => settle({ kind: "timeout" }), budgetMs);
24403
+ signal?.addEventListener("abort", onAbort, { once: true });
24404
+ const finish = () => {
24405
+ if (exited) return;
24406
+ exited = true;
24407
+ clearTimeout(budgetTimer);
24408
+ clearTimeout(graceTimer);
24409
+ signal?.removeEventListener("abort", onAbort);
24410
+ if (outcome === void 0) {
24411
+ const envelope = stdout.length > 0 && stdout.length <= MAX_PROBE_LINE_BYTES ? parseStatusEnvelope(stdout.toString("utf8")) : void 0;
24412
+ outcome = envelope !== void 0 ? { kind: "ok", envelope } : probeFailed(stderr.toString("utf8"));
24413
+ }
24414
+ resolve5(outcome);
24415
+ };
24416
+ if (child.pid !== void 0) options.onSpawn?.(child.pid);
24417
+ child.stdout?.on("data", (chunk) => {
24418
+ if (outcome !== void 0) return;
24419
+ stdout = Buffer.concat([stdout, chunk]);
24420
+ const newline = stdout.indexOf(10);
24421
+ if (newline >= 0 && newline <= MAX_PROBE_LINE_BYTES) {
24422
+ const envelope = parseStatusEnvelope(stdout.subarray(0, newline).toString("utf8"));
24423
+ settle(envelope !== void 0 ? { kind: "ok", envelope } : { kind: "probe_failed" });
24424
+ } else if (newline > MAX_PROBE_LINE_BYTES || newline < 0 && stdout.length > MAX_PROBE_LINE_BYTES) {
24425
+ settle({ kind: "probe_failed" });
24426
+ }
24427
+ });
24428
+ child.stderr?.on("data", (chunk) => {
24429
+ stderr = Buffer.concat([stderr, chunk]);
24430
+ if (stderr.length > MAX_STDERR_BYTES) stderr = stderr.subarray(stderr.length - MAX_STDERR_BYTES);
24431
+ });
24432
+ child.on("error", () => {
24433
+ settle({ kind: "probe_failed" });
24434
+ if (child.pid === void 0) finish();
24435
+ });
24436
+ child.on("close", finish);
24437
+ });
24438
+ }
24439
+ function probeFailed(stderr) {
24440
+ const code3 = childErrorCode(stderr);
24441
+ return code3 === void 0 ? { kind: "probe_failed" } : { kind: "probe_failed", childErrorCode: code3 };
24442
+ }
24443
+ function childErrorCode(stderr) {
24444
+ const lines = stderr.split("\n");
24445
+ for (let start = lines.length - 1; start >= 0; start -= 1) {
24446
+ if (!lines[start].startsWith("{")) continue;
24447
+ try {
24448
+ const parsed = JSON.parse(lines.slice(start).join("\n"));
24449
+ if (typeof parsed !== "object" || parsed === null || !("error" in parsed)) continue;
24450
+ const error = parsed.error;
24451
+ if (typeof error !== "object" || error === null || !("code" in error)) continue;
24452
+ return typeof error.code === "string" && ERROR_CODE_RE.test(error.code) ? error.code : void 0;
24453
+ } catch {
24454
+ }
24455
+ }
24456
+ return void 0;
24457
+ }
24458
+ function plainObject(value) {
24459
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24460
+ }
24461
+ function parseStatusEnvelope(line) {
24462
+ let parsed;
24463
+ try {
24464
+ parsed = JSON.parse(line);
24465
+ } catch {
24466
+ return void 0;
24467
+ }
24468
+ if (!plainObject(parsed)) return void 0;
24469
+ const value = parsed;
24470
+ if (!plainObject(value.operation) || value.operation?.operationId !== STATUS_OPERATION_ID) return void 0;
24471
+ const warnings = Array.isArray(value.warnings) ? { warnings: value.warnings } : {};
24472
+ const operation = { operationId: STATUS_OPERATION_ID };
24473
+ if (value.status === "ok" && plainObject(value.output)) {
24474
+ return { status: "ok", operation, output: value.output, ...warnings };
24475
+ }
24476
+ const error = value.error;
24477
+ if (value.status === "error" && plainObject(error) && typeof error?.code === "string" && typeof error.message === "string") {
24478
+ return {
24479
+ status: "error",
24480
+ operation,
24481
+ error: {
24482
+ code: error.code,
24483
+ message: error.message,
24484
+ ...typeof error.retryable === "boolean" ? { retryable: error.retryable } : {},
24485
+ ...plainObject(error.details) ? { details: error.details } : {}
24486
+ },
24487
+ ...warnings
24488
+ };
24489
+ }
24490
+ return void 0;
24491
+ }
24492
+
24493
+ // src/lib/secrets-wait.ts
24494
+ init_constants();
24495
+ init_errors();
24496
+ var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
24497
+ var WAIT_STEADY_INTERVAL_MS = 15e3;
24498
+ var DEFAULT_WAIT_TIMEOUT = "5m";
24499
+ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
24500
+ var MAX_CONSECUTIVE_FAILURES = 5;
24501
+ var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
24502
+ var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
24503
+ var EXPLICIT_KEY_HINT = "tc secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with tc --profile <profile> auth login --method local, without TC_PRIVATE_KEY.";
24504
+ function abortableSleep(ms2, signal) {
24505
+ return new Promise((resolve5) => {
24506
+ if (signal.aborted) {
24507
+ resolve5();
24508
+ return;
24509
+ }
24510
+ const done = () => {
24511
+ clearTimeout(timer);
24512
+ signal.removeEventListener("abort", done);
24513
+ resolve5();
24514
+ };
24515
+ const timer = setTimeout(done, ms2);
24516
+ signal.addEventListener("abort", done, { once: true });
24517
+ });
24518
+ }
24519
+ function parseWaitTimeout(value) {
24520
+ if (!TIMEOUT_RE.test(value)) {
24521
+ throw new CLIError("INVALID_TIMEOUT", `Invalid --timeout ${JSON.stringify(value)}: use 0, Ns or Nm (for example 90s or 5m).`, ExitCode.USAGE_ERROR);
24522
+ }
24523
+ const amount = value === "0" ? 0 : Number(value.slice(0, -1));
24524
+ const ms2 = value.endsWith("m") ? amount * 6e4 : amount * 1e3;
24525
+ if (ms2 > MAX_WAIT_TIMEOUT_MS) {
24526
+ throw new CLIError("INVALID_TIMEOUT", "--timeout may be at most 9m.", ExitCode.USAGE_ERROR);
24527
+ }
24528
+ return ms2;
24529
+ }
24530
+ async function runSecretsWait(request, deps) {
24531
+ const deadline = deps.now() + request.timeoutMs;
24532
+ let progress;
24533
+ let lastPending;
24534
+ let lastWarnings;
24535
+ let failures = 0;
24536
+ let step = 0;
24537
+ const cancelled = () => new CLIError(
24538
+ "CANCELLED",
24539
+ "Interrupted: tc secrets wait stopped before the secrets were ready.",
24540
+ ExitCode.CANCELLED,
24541
+ progress === void 0 ? {} : { progress }
24542
+ );
24543
+ const networkError = (message) => new CLIError(
24544
+ "NETWORK_ERROR",
24545
+ message,
24546
+ ExitCode.NETWORK_ERROR,
24547
+ {
24548
+ ...progress === void 0 ? {} : { progress },
24549
+ ...lastWarnings === void 0 ? {} : { warnings: lastWarnings }
24550
+ }
24551
+ );
24552
+ for (; ; ) {
24553
+ if (deps.signal.aborted) throw cancelled();
24554
+ const final = deps.now() >= deadline;
24555
+ const outcome = await deps.runProbe(deps.signal);
24556
+ if (deps.signal.aborted || outcome.kind === "cancelled") throw cancelled();
24557
+ let failed = false;
24558
+ switch (outcome.kind) {
24559
+ case "timeout":
24560
+ failed = true;
24561
+ break;
24562
+ case "probe_failed":
24563
+ throw new CLIError(
24564
+ "PROBE_FAILED",
24565
+ outcome.childErrorCode === void 0 ? "The status probe exited without a result." : `The status probe exited without a result (${outcome.childErrorCode}).`,
24566
+ ExitCode.ERROR
24567
+ );
24568
+ case "ok": {
24569
+ const envelope = outcome.envelope;
24570
+ lastWarnings = envelope.warnings;
24571
+ if (envelope.status === "error") {
24572
+ if (RETRYABLE_STATUS_ERRORS[envelope.error.code] === true && envelope.error.retryable !== false) {
24573
+ failed = true;
24574
+ break;
24575
+ }
24576
+ throw statusError(envelope.error, request.profile, envelope.warnings);
24577
+ }
24578
+ const output2 = statusOutput(envelope.output);
24579
+ if (output2 === void 0) throw new CLIError("PROBE_FAILED", "The status probe returned an unexpected result.", ExitCode.ERROR);
24580
+ if (output2.status === "ready") {
24581
+ return {
24582
+ exitCode: ExitCode.SUCCESS,
24583
+ stdout: { status: "ready", names: [...output2.names], ...output2.scope === void 0 ? {} : { scope: output2.scope } },
24584
+ ...envelope.warnings === void 0 ? {} : { warnings: envelope.warnings }
24585
+ };
24586
+ }
24587
+ if (output2.status === "consent_required") throw consentError(output2, request.consentHint, envelope.warnings);
24588
+ progress = { present: [...output2.present], pending: [...output2.pending] };
24589
+ lastPending = output2;
24590
+ break;
24591
+ }
24592
+ }
24593
+ if (failed) {
24594
+ failures += 1;
24595
+ if (failures >= MAX_CONSECUTIVE_FAILURES) {
24596
+ throw networkError(`The TinyCloud node could not be reached (${failures} attempts in a row).`);
24597
+ }
24598
+ } else {
24599
+ failures = 0;
24600
+ }
24601
+ const end = deps.now();
24602
+ if (final || end >= deadline) {
24603
+ if (failed || lastPending === void 0) {
24604
+ throw networkError("The TinyCloud node could not be reached before the wait's deadline.");
24605
+ }
24606
+ return {
24607
+ exitCode: ExitCode.PENDING,
24608
+ stdout: {
24609
+ status: "pending",
24610
+ pending: [...lastPending.pending],
24611
+ present: [...lastPending.present],
24612
+ ...lastPending.setup !== void 0 ? { url: lastPending.setup.url } : lastPending.hint !== void 0 ? { hint: lastPending.hint } : {}
24613
+ },
24614
+ ...lastWarnings === void 0 ? {} : { warnings: lastWarnings }
24615
+ };
24616
+ }
24617
+ const interval = WAIT_BACKOFF_MS[step] ?? WAIT_STEADY_INTERVAL_MS;
24618
+ step += 1;
24619
+ await deps.sleep(Math.min(interval, deadline - end), deps.signal);
24620
+ }
24621
+ }
24622
+ function stringList(value) {
24623
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
24624
+ }
24625
+ function statusOutput(output2) {
24626
+ const { status, owner, scope, setup, hint } = output2;
24627
+ const names = stringList(output2.names);
24628
+ const present = stringList(output2.present);
24629
+ const pending = stringList(output2.pending);
24630
+ const denied = stringList(output2.denied);
24631
+ if (status !== "ready" && status !== "pending" && status !== "consent_required") return void 0;
24632
+ if (typeof owner !== "string" || !names || !present || !pending || !denied) return void 0;
24633
+ if (scope !== void 0 && typeof scope !== "string") return void 0;
24634
+ if (hint !== void 0 && typeof hint !== "string") return void 0;
24635
+ if (setup !== void 0 && (typeof setup !== "object" || setup === null || !("url" in setup) || typeof setup.url !== "string")) return void 0;
24636
+ return {
24637
+ status,
24638
+ owner,
24639
+ names,
24640
+ present,
24641
+ pending,
24642
+ denied,
24643
+ ...scope === void 0 ? {} : { scope },
24644
+ ...setup === void 0 ? {} : { setup },
24645
+ ...hint === void 0 ? {} : { hint }
24646
+ };
24647
+ }
24648
+ function consentError(output2, hint, warnings) {
24649
+ const denied = [...output2.denied];
24650
+ return new CLIError(
24651
+ "PERMISSION_DENIED",
24652
+ `This profile may not read ${denied.join(", ")}. The owner must approve access first.`,
24653
+ ExitCode.PERMISSION_DENIED,
24654
+ {
24655
+ hint,
24656
+ recovery: {
24657
+ action: "request_access",
24658
+ names: denied,
24659
+ owner: output2.owner,
24660
+ ...output2.scope === void 0 ? {} : { scope: output2.scope }
24661
+ },
24662
+ deniedNames: denied,
24663
+ ...output2.setup === void 0 ? {} : { setup: output2.setup },
24664
+ ...warnings === void 0 ? {} : { warnings }
24665
+ }
24666
+ );
24667
+ }
24668
+ function recoveryOf(error, action) {
24669
+ const recovery = error.details?.recovery;
24670
+ if (typeof recovery !== "object" || recovery === null) return void 0;
24671
+ const candidate = recovery;
24672
+ if (candidate.action !== action) return void 0;
24673
+ return candidate.command === void 0 || typeof candidate.command === "string" ? recovery : void 0;
24674
+ }
24675
+ function statusError(error, profile, warnings) {
24676
+ const extra = warnings === void 0 ? {} : { warnings };
24677
+ if (error.code === "AUTH_REQUIRED" && error.details?.reason === "explicit_key") {
24678
+ return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint: EXPLICIT_KEY_HINT, ...extra });
24679
+ }
24680
+ if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
24681
+ const recovery = recoveryOf(error, "sign_in");
24682
+ if (recovery !== void 0) {
24683
+ const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with tc auth login, then retry.`;
24684
+ return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
24685
+ }
24686
+ }
24687
+ if (error.code === "PROFILE_MIGRATION_REQUIRED") {
24688
+ const recovery = recoveryOf(error, "complete_migration");
24689
+ const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run tc auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
24690
+ return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
24691
+ hint,
24692
+ ...recovery === void 0 ? {} : { recovery },
24693
+ ...extra
24694
+ });
24695
+ }
24696
+ if (error.code === "INPUT_INVALID") {
24697
+ const reason = error.details?.reason;
24698
+ if (reason === "OWNER_MISMATCH") return new CLIError("OWNER_MISMATCH", error.message, ExitCode.USAGE_ERROR, extra);
24699
+ if (reason === "OWNER_DID_UNKNOWN") return new CLIError("OWNER_DID_UNKNOWN", error.message, ExitCode.AUTH_REQUIRED, extra);
24700
+ return new CLIError("INPUT_INVALID", error.message, ExitCode.USAGE_ERROR, extra);
24701
+ }
24702
+ const mapped = cliErrorFromService(error);
24703
+ return new CLIError(mapped.code, mapped.message, mapped.exitCode, { ...mapped.metadata, ...extra });
24704
+ }
24705
+
24706
+ // src/commands/secrets.ts
24173
24707
  init_types();
24174
24708
  var SECRETS_SPACE3 = "secrets";
24175
24709
  var SECRET_KV_ABILITIES = {
@@ -24189,6 +24723,32 @@ function authOptions(options) {
24189
24723
  const privateKey = options.privateKey || process.env.TC_PRIVATE_KEY;
24190
24724
  return privateKey ? { privateKey } : void 0;
24191
24725
  }
24726
+ function effectiveLocal(profile, options) {
24727
+ return profile !== null && isLocalOwnerProfile4(profile) || authOptions(options) !== void 0;
24728
+ }
24729
+ function secretRequestUsageError(error) {
24730
+ return error instanceof SecretRequestInputError ? new CLIError(error.code, error.message, ExitCode.USAGE_ERROR) : error;
24731
+ }
24732
+ function requestedSecretScope(options) {
24733
+ if (options.scope !== void 0 && options.app !== void 0) {
24734
+ throw new CLIError("USAGE_ERROR", "Pass either --scope or --app, not both.", ExitCode.USAGE_ERROR);
24735
+ }
24736
+ if (options.app === void 0) return canonicalizeSecretScope2(options.scope);
24737
+ try {
24738
+ return appSecretScope(options.app);
24739
+ } catch (error) {
24740
+ throw new CLIError("INVALID_SECRET_SCOPE", error instanceof Error ? error.message : String(error), ExitCode.USAGE_ERROR);
24741
+ }
24742
+ }
24743
+ var CONTROL_OR_FORMAT = /[\p{Cc}\p{Cf}]/u;
24744
+ function defaultRequester() {
24745
+ try {
24746
+ const requester = `${userInfo().username}@${hostname().split(".")[0]}`;
24747
+ return requester.length <= 64 && !CONTROL_OR_FORMAT.test(requester) ? requester : void 0;
24748
+ } catch {
24749
+ return void 0;
24750
+ }
24751
+ }
24192
24752
  function resolveSecretScope(options) {
24193
24753
  return options.scope ? { scope: options.scope } : void 0;
24194
24754
  }
@@ -24447,14 +25007,20 @@ function throwCanonicalSecretGetError(result, name) {
24447
25007
  "Permission denied while reading secret",
24448
25008
  ExitCode.PERMISSION_DENIED
24449
25009
  );
24450
- case "setup_required":
25010
+ case "setup_required": {
25011
+ const { url, names, scope, owner, action } = result.setup;
25012
+ const message = typeof result.setup.message === "string" ? result.setup.message : `Secret "${name}" is not set.`;
24451
25013
  throw new CLIError(
24452
25014
  "NOT_FOUND",
24453
- result.setup.message,
25015
+ message,
24454
25016
  ExitCode.NOT_FOUND,
24455
- { hint: `${result.setup.url}
24456
- ${result.setup.message}`, setup: result.setup }
25017
+ {
25018
+ hint: `${String(url)}
25019
+ ${message}`,
25020
+ setup: { url, names, ...scope === void 0 ? {} : { scope }, owner, action }
25021
+ }
24457
25022
  );
25023
+ }
24458
25024
  case "error":
24459
25025
  if (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "PROFILE_POSTURE_NOT_ALLOWED" && result.context.posture === "unauthenticated") {
24460
25026
  throw new CLIError(
@@ -24467,6 +25033,9 @@ ${result.setup.message}`, setup: result.setup }
24467
25033
  if (result.error.code === "NODE_UNREACHABLE") {
24468
25034
  throw new CLIError("NETWORK_ERROR", result.error.message, ExitCode.NETWORK_ERROR);
24469
25035
  }
25036
+ if (result.error.code === "KV_NOT_FOUND") {
25037
+ throw new CLIError("NOT_FOUND", `Secret "${name}" not found.`, ExitCode.NOT_FOUND, { hint: result.error.message });
25038
+ }
24470
25039
  if (result.error.code === "PERMISSION_HINT_INVALID") {
24471
25040
  throw new CLIError(result.error.code, result.error.message, ExitCode.PERMISSION_DENIED);
24472
25041
  }
@@ -24738,17 +25307,26 @@ async function readDelegatedSecretValue(params) {
24738
25307
  });
24739
25308
  if (!envelopeResult.ok) {
24740
25309
  if (envelopeResult.error.code === "NOT_FOUND" || envelopeResult.error.code === "KEY_NOT_FOUND" || envelopeResult.error.code === "KV_NOT_FOUND") {
25310
+ const recovery = missingSecretRecovery({
25311
+ owner: ownerOfSpace(params.space?.startsWith("tinycloud:") ? params.space : params.delegation.spaceId),
25312
+ local: params.local,
25313
+ names: [params.name],
25314
+ ...params.scope === void 0 ? {} : { scope: params.scope }
25315
+ });
24741
25316
  throw new CLIError(
24742
25317
  "NOT_FOUND",
24743
25318
  `Secret "${params.name}" not found`,
24744
- ExitCode.NOT_FOUND
25319
+ ExitCode.NOT_FOUND,
25320
+ recovery.kind === "add_secret" ? { hint: `Ask the owner to add it: ${recovery.setup.url}`, setup: recovery.setup } : { hint: recovery.hint }
24745
25321
  );
24746
25322
  }
24747
- if (envelopeResult.error.code === "PERMISSION_DENIED") {
25323
+ const classified = cliErrorFromService(envelopeResult.error);
25324
+ if (envelopeResult.error.code === "AUTH_UNAUTHORIZED" || classified.code === "PERMISSION_DENIED" || classified.code === "AUTH_REQUIRED") {
24748
25325
  throw new CLIError(
24749
25326
  "PERMISSION_DENIED",
24750
25327
  `Delegation "${params.delegationCid}" does not cover secret "${params.secretPath}".`,
24751
- ExitCode.PERMISSION_DENIED
25328
+ ExitCode.PERMISSION_DENIED,
25329
+ classified.metadata
24752
25330
  );
24753
25331
  }
24754
25332
  throw new CLIError(
@@ -24885,7 +25463,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
24885
25463
  ...descriptor ? { descriptor } : {}
24886
25464
  });
24887
25465
  } catch (error) {
24888
- handleError(error);
25466
+ return handleError(error);
24889
25467
  }
24890
25468
  });
24891
25469
  network.command("init [name]").description("Create a secrets encryption network if needed").option("--private-key <hex>", "Ethereum private key override (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
@@ -24903,7 +25481,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
24903
25481
  descriptor
24904
25482
  });
24905
25483
  } catch (error) {
24906
- handleError(error);
25484
+ return handleError(error);
24907
25485
  }
24908
25486
  });
24909
25487
  secrets.command("doctor [name]").description("Check secrets setup and optional secret access").option("--scope <scope>", "Logical secret scope").option("--space <name|uri>", "Target a non-default secrets space (short name or full URI)").option("--network <name>", "Encryption network name", "default").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
@@ -24960,6 +25538,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
24960
25538
  });
24961
25539
  } else {
24962
25540
  const notFound = result.error.code === "NOT_FOUND" || result.error.code === "KEY_NOT_FOUND";
25541
+ const local = notFound && effectiveLocal(await ProfileManager.getProfile(ctx.profile).catch(() => null), options);
24963
25542
  secret = {
24964
25543
  name: resolved.name,
24965
25544
  path: resolved.permissionPaths.vault,
@@ -24971,7 +25550,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
24971
25550
  name: "Secret access",
24972
25551
  ok: false,
24973
25552
  detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
24974
- hint: notFound ? `tc secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} <value>` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
25553
+ hint: notFound ? local ? `tc secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `tc secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
24975
25554
  });
24976
25555
  }
24977
25556
  } else {
@@ -24993,7 +25572,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
24993
25572
  checks
24994
25573
  });
24995
25574
  } catch (error) {
24996
- handleError(error);
25575
+ return handleError(error);
24997
25576
  }
24998
25577
  });
24999
25578
  secrets.command("list").description("List secrets").option("--scope <scope>", "Logical secret scope").option("--space <name|uri>", "Target a non-default secrets space (short name or full URI)").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (options, cmd) => {
@@ -25024,7 +25603,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25024
25603
  ...spaceUri ? { space: spaceUri } : {}
25025
25604
  });
25026
25605
  } catch (error) {
25027
- handleError(error);
25606
+ return handleError(error);
25028
25607
  }
25029
25608
  });
25030
25609
  secrets.command("get <name>").description("Get a secret value").option("--scope <scope>", "Logical secret scope").option("--space <name|uri>", "Target a non-default secrets space (short name or full URI)").option("--raw", "Output raw value (no JSON wrapping)").option("--value-only", "Output only the secret value (alias for --raw)").option("-o, --output <file>", "Write value to file").option("--delegation <source>", "Delegation file path or imported profile name").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
@@ -25043,6 +25622,8 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25043
25622
  );
25044
25623
  const effectiveHost = globalOpts.host ?? delegated.delegation.host ?? ctx.host;
25045
25624
  const delegatedCtx = { ...ctx, host: effectiveHost };
25625
+ const local = effectiveLocal(await ProfileManager.getProfile(ctx.profile).catch(() => null), options);
25626
+ const scope = resolveSecretPath2(name, scopeOptions).scope;
25046
25627
  const node = await ensureSecretsNode(delegatedCtx, options);
25047
25628
  const value2 = await withSpinner(
25048
25629
  `Getting secret ${name}...`,
@@ -25053,7 +25634,9 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25053
25634
  permissions: delegated.permissions,
25054
25635
  secretPath,
25055
25636
  space: legacySpaceUri ?? SECRETS_SPACE3,
25056
- name
25637
+ name,
25638
+ ...scope === void 0 ? {} : { scope },
25639
+ local
25057
25640
  })
25058
25641
  );
25059
25642
  if (options.output) {
@@ -25101,7 +25684,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25101
25684
  }
25102
25685
  outputWarnings(operationWarnings(result.warnings));
25103
25686
  } catch (error) {
25104
- handleError(error);
25687
+ return handleError(error);
25105
25688
  }
25106
25689
  });
25107
25690
  secrets.command("put <name> [value]").description("Store a secret").option("--scope <scope>", "Logical secret scope").option("--space <name|uri>", "Target a non-default secrets space (short name or full URI)").option("--file <path>", "Read value from file").option("--stdin", "Read value from stdin").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, value, options, cmd) => {
@@ -25142,7 +25725,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25142
25725
  }
25143
25726
  outputJson({ name, written: true });
25144
25727
  } catch (error) {
25145
- handleError(error);
25728
+ return handleError(error);
25146
25729
  }
25147
25730
  });
25148
25731
  secrets.command("delete <name>").description("Delete a secret").option("--scope <scope>", "Logical secret scope").option("--space <name|uri>", "Target a non-default secrets space (short name or full URI)").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY)").action(async (name, options, cmd) => {
@@ -25168,7 +25751,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25168
25751
  }
25169
25752
  outputJson({ name, deleted: true });
25170
25753
  } catch (error) {
25171
- handleError(error);
25754
+ return handleError(error);
25172
25755
  }
25173
25756
  });
25174
25757
  network.command("grant <recipientDid> [name]").description("Grant decrypt permission for a secrets encryption network").option("--private-key <hex>", "Ethereum private key override (or set TC_PRIVATE_KEY)").action(async (recipientDid, name, options, cmd) => {
@@ -25199,16 +25782,135 @@ function registerSecretsCommand(program, openKeyAcquisition) {
25199
25782
  actions: result.delegation.actions
25200
25783
  });
25201
25784
  } catch (error) {
25202
- handleError(error);
25785
+ return handleError(error);
25203
25786
  }
25204
25787
  });
25205
- secrets.command("manage").description("Open the TinyCloud Secrets Manager in your browser").action(async () => {
25788
+ secrets.command("request <name...>").description("Print a Secret Manager link asking the owner to add secrets (offline)").option("--scope <scope>", "Logical secret scope").option("--app <id>", "Derive the scope from an app id (instead of --scope)").option("--owner <did>", "Owner did:pkh when the profile does not record one").option("--requester <label>", "Who is asking, shown to the owner (default: user@host)").option("--open", "Also open the link in a browser").option("--private-key <hex>", "Ethereum private key (or set TC_PRIVATE_KEY); a local key cannot request").action(async (names, options, cmd) => {
25206
25789
  try {
25207
- const open7 = (await import("open")).default;
25208
- await open7("https://secrets.tinycloud.xyz");
25209
- outputJson({ opened: "https://secrets.tinycloud.xyz" });
25790
+ const globalOpts = cmd.optsWithGlobals();
25791
+ const ctx = await ProfileManager.resolveContext(globalOpts);
25792
+ const scope = requestedSecretScope(options);
25793
+ const profile = await ProfileManager.getProfile(ctx.profile).catch(() => null);
25794
+ if (effectiveLocal(profile, options)) {
25795
+ throw new CLIError(
25796
+ "LOCAL_OWNER_PROFILE",
25797
+ `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with tc secrets put NAME --stdin instead of requesting them.`,
25798
+ ExitCode.USAGE_ERROR
25799
+ );
25800
+ }
25801
+ let link3;
25802
+ try {
25803
+ const requestedOwner = options.owner === void 0 ? void 0 : canonicalSecretRequestOwner(options.owner);
25804
+ const resolution = resolveSecretsOwner2({
25805
+ local: false,
25806
+ posture: resolveProfilePosture(profile ?? {}),
25807
+ ...profile?.ownerDid === void 0 ? {} : { ownerDid: profile.ownerDid.split("#")[0] },
25808
+ ...profile?.did === void 0 ? {} : { principalDid: profile.did.split("#")[0] },
25809
+ ...profile?.spaceId === void 0 ? {} : { space: profile.spaceId }
25810
+ }, requestedOwner);
25811
+ if (!resolution.ok) {
25812
+ throw new CLIError(
25813
+ resolution.code,
25814
+ resolution.message,
25815
+ resolution.code === "OWNER_MISMATCH" ? ExitCode.USAGE_ERROR : ExitCode.AUTH_REQUIRED
25816
+ );
25817
+ }
25818
+ const requester = options.requester ?? defaultRequester();
25819
+ link3 = buildSecretRequestUrl2({
25820
+ owner: resolution.owner,
25821
+ names,
25822
+ ...scope === void 0 ? {} : { scope },
25823
+ ...requester === void 0 ? {} : { requester }
25824
+ });
25825
+ } catch (error) {
25826
+ throw secretRequestUsageError(error);
25827
+ }
25828
+ outputJson({ url: link3.url, names: link3.names, ...link3.scope === void 0 ? {} : { scope: link3.scope }, owner: link3.owner });
25829
+ if (options.open && !await tryOpenBrowser(link3.url)) warnBrowserNotOpened();
25210
25830
  } catch (error) {
25211
- handleError(error);
25831
+ return handleError(error);
25832
+ }
25833
+ });
25834
+ secrets.command("manage [name...]").description("Open the TinyCloud Secrets Manager, optionally at the named secrets").option("--scope <scope>", "Logical secret scope").option("--app <id>", "Derive the scope from an app id (instead of --scope)").option("--no-open", "Print the URL without opening a browser").action(async (names, options) => {
25835
+ try {
25836
+ const scope = requestedSecretScope(options);
25837
+ let url;
25838
+ try {
25839
+ url = buildSecretManagerUrl({ names, ...scope === void 0 ? {} : { scope } });
25840
+ } catch (error) {
25841
+ throw secretRequestUsageError(error);
25842
+ }
25843
+ const opened = options.open === false ? false : await tryOpenBrowser(url);
25844
+ outputJson({ url, opened });
25845
+ if (options.open !== false && !opened) warnBrowserNotOpened();
25846
+ } catch (error) {
25847
+ return handleError(error);
25848
+ }
25849
+ });
25850
+ secrets.command("wait <name...>").description("Wait until named secrets exist. Never signs in: needs a stored session, not TC_PRIVATE_KEY").option("--scope <scope>", "Logical secret scope").option("--app <id>", "Derive the scope from an app id (instead of --scope)").option("--owner <did>", "Expected owner did:pkh, checked against the profile's live identity").option("--timeout <duration>", "0 (one attempt), Ns or Nm; at most 9m", DEFAULT_WAIT_TIMEOUT).action(async (names, options, cmd) => {
25851
+ const controller = new AbortController();
25852
+ const cancel = () => controller.abort();
25853
+ process.once("SIGINT", cancel);
25854
+ process.once("SIGTERM", cancel);
25855
+ try {
25856
+ const timeoutMs = parseWaitTimeout(options.timeout);
25857
+ const scope = requestedSecretScope(options);
25858
+ let validNames;
25859
+ let owner;
25860
+ try {
25861
+ validNames = validSecretNames(names);
25862
+ if (scope !== void 0 && !isValidSecretRequestScope(scope)) {
25863
+ throw new CLIError("INVALID_SECRET_SCOPE", `Invalid secret scope ${JSON.stringify(scope)}.`, ExitCode.USAGE_ERROR);
25864
+ }
25865
+ owner = options.owner === void 0 ? void 0 : canonicalSecretRequestOwner(options.owner);
25866
+ } catch (error) {
25867
+ throw secretRequestUsageError(error);
25868
+ }
25869
+ const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
25870
+ const probe = {
25871
+ profile: ctx.profile,
25872
+ host: ctx.host,
25873
+ names: validNames,
25874
+ ...scope === void 0 ? {} : { scope },
25875
+ ...owner === void 0 ? {} : { owner }
25876
+ };
25877
+ const result = await runSecretsWait(
25878
+ { profile: ctx.profile, timeoutMs, consentHint: scopedSecretLoginHint(ctx.profile) },
25879
+ {
25880
+ now: Date.now,
25881
+ sleep: abortableSleep,
25882
+ runProbe: (signal) => runProbeChild(probe, { signal }),
25883
+ signal: controller.signal
25884
+ }
25885
+ );
25886
+ outputJson(result.stdout);
25887
+ outputWarnings(operationWarnings(result.warnings));
25888
+ process.exitCode = result.exitCode;
25889
+ } catch (error) {
25890
+ return handleError(error);
25891
+ } finally {
25892
+ process.off("SIGINT", cancel);
25893
+ process.off("SIGTERM", cancel);
25894
+ }
25895
+ });
25896
+ secrets.command("probe <name...>", { hidden: true }).option("--scope <scope>", "Logical secret scope").option("--owner <did>", "Requested owner did:pkh").action(async (names, options, cmd) => {
25897
+ try {
25898
+ const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
25899
+ const privateKey = process.env.TC_PRIVATE_KEY;
25900
+ const result = await invokeOperation2("tinycloud.secrets.status", 1, {
25901
+ profile: ctx.profile,
25902
+ host: ctx.host,
25903
+ allowOwnerProfile: true,
25904
+ ...privateKey ? { privateKey } : {}
25905
+ }, {
25906
+ names,
25907
+ ...options.scope === void 0 ? {} : { scope: options.scope },
25908
+ ...options.owner === void 0 ? {} : { owner: options.owner }
25909
+ });
25910
+ process.stdout.write(`${JSON.stringify(result)}
25911
+ `, () => process.exit(0));
25912
+ } catch (error) {
25913
+ return handleError(error);
25212
25914
  }
25213
25915
  });
25214
25916
  }
@@ -30582,7 +31284,7 @@ async function importAesKey(key32, usage) {
30582
31284
  [usage]
30583
31285
  );
30584
31286
  }
30585
- async function open5(blob, key32) {
31287
+ async function open6(blob, key32) {
30586
31288
  if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
30587
31289
  throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
30588
31290
  }
@@ -30920,7 +31622,7 @@ async function resolvePolicyShare(link3, options) {
30920
31622
  options.signal?.throwIfAborted();
30921
31623
  let envelope;
30922
31624
  try {
30923
- const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open5(parsed.ciphertext, parsed.key32));
31625
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open6(parsed.ciphertext, parsed.key32));
30924
31626
  const value = JSON.parse(encoded);
30925
31627
  if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
30926
31628
  envelope = shareEnvelopeV3Schema.parse(value);
@@ -33509,7 +34211,7 @@ function receiveJson(result, path) {
33509
34211
 
33510
34212
  // src/share/io.ts
33511
34213
  import { constants } from "fs";
33512
- import { lstat as lstat4, mkdir as mkdir4, mkdtemp, open as open6, readFile as readFile10, realpath as realpath2, stat as stat4, link as link2, rename as rename4, rm as rm5, unlink as unlink3 } from "fs/promises";
34214
+ import { lstat as lstat4, mkdir as mkdir4, mkdtemp, open as open7, readFile as readFile10, realpath as realpath2, stat as stat4, link as link2, rename as rename4, rm as rm5, unlink as unlink3 } from "fs/promises";
33513
34215
  import { randomBytes as randomBytes6 } from "crypto";
33514
34216
  import { basename as basename4, join as join10, resolve as resolve3, sep as sep2 } from "path";
33515
34217
  init_errors();
@@ -33585,7 +34287,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
33585
34287
  const outputDirectory = resolve3(directory);
33586
34288
  await assertDirectory(outputDirectory);
33587
34289
  const safeName = safeFilename(filename);
33588
- const directoryHandle = await open6(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
34290
+ const directoryHandle = await open7(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
33589
34291
  const stableDirectory = await realpath2(outputDirectory);
33590
34292
  const outputPath = join10(stableDirectory, safeName);
33591
34293
  const directoryIdentity = await directoryHandle.stat();
@@ -33609,7 +34311,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
33609
34311
  if (error.code !== "ENOENT") throw error;
33610
34312
  }
33611
34313
  temporaryPath = stagingPath;
33612
- handle = await open6(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
34314
+ handle = await open7(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
33613
34315
  await handle.writeFile(bytes);
33614
34316
  await handle.close();
33615
34317
  handle = void 0;