@tinycloud/cli 1.0.0-beta.22 → 1.0.0-beta.23

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.
@@ -12993,6 +12993,35 @@ init_errors();
12993
12993
  init_constants();
12994
12994
  init_space();
12995
12995
 
12996
+ // src/lib/raw-encryption.ts
12997
+ function isRawEncryptionPermission(permission) {
12998
+ return (permission.service === ENCRYPTION_PERMISSION_SERVICE2 || permission.service === "encryption") && typeof permission.path === "string" && permission.path.startsWith("urn:tinycloud:encryption:");
12999
+ }
13000
+ function isVerifiedRawEncryptionPermission(permission) {
13001
+ return isRawEncryptionPermission(permission) && (permission.space === void 0 || permission.space === "encryption");
13002
+ }
13003
+
13004
+ // src/lib/owner-did.ts
13005
+ init_constants();
13006
+ init_errors();
13007
+ import { ensureEip55 } from "@tinycloud/node-sdk-wasm";
13008
+ function canonicalOwnerDid(did, label = "--owner") {
13009
+ const match = /^did:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40})$/.exec(did);
13010
+ if (!match) {
13011
+ throw new CLIError("INVALID_ARGUMENT", `${label} "${did}" is not a did:pkh:eip155:CHAIN:ADDRESS identity.`, ExitCode.USAGE_ERROR);
13012
+ }
13013
+ return `did:pkh:eip155:${match[1]}:${ensureEip55(match[2])}`;
13014
+ }
13015
+ function rawEncryptionOwnerMatches(path, ownerDid) {
13016
+ const match = /^urn:tinycloud:encryption:(did:pkh:eip155:.+):([a-z0-9][a-z0-9-]*)$/.exec(path);
13017
+ if (!match) return false;
13018
+ try {
13019
+ return canonicalOwnerDid(match[1]) === canonicalOwnerDid(ownerDid);
13020
+ } catch {
13021
+ return false;
13022
+ }
13023
+ }
13024
+
12996
13025
  // src/share/publishing-manifest.ts
12997
13026
  var SHARE_PUBLISHING_MANIFEST_REF = "builtin:share-publishing";
12998
13027
  var SHARE_PUBLISHING_MANIFEST = {
@@ -13072,6 +13101,9 @@ function didWithoutFragment(did) {
13072
13101
  async function loadAdditionalDelegations(profile) {
13073
13102
  return readAdditionalDelegations(profile);
13074
13103
  }
13104
+ async function saveAdditionalDelegations(profile, entries) {
13105
+ await replaceSharedRecords(profile, "additional-delegations", entries);
13106
+ }
13075
13107
  async function appendAdditionalDelegation(profile, entry) {
13076
13108
  await upsertProfileRecord(
13077
13109
  profile,
@@ -13093,6 +13125,9 @@ async function appendPermissionRequestArtifact(profile, artifact) {
13093
13125
  await writeSharedRecords(profile, "auth-requests", next);
13094
13126
  });
13095
13127
  }
13128
+ async function replaceSharedRecords(profile, store, entries) {
13129
+ await withProfileLock2(profile, () => writeSharedRecords(profile, store, entries));
13130
+ }
13096
13131
  async function writeSharedRecords(profile, store, entries) {
13097
13132
  const path = store === "additional-delegations" ? additionalDelegationsPath(profile) : permissionRequestsPath(profile);
13098
13133
  await writeJsonAtomic(path, entries);
@@ -13180,27 +13215,36 @@ async function loadPermissionRequest(source, profile) {
13180
13215
  async function loadManifestPermissions(source, profile, options = {}) {
13181
13216
  const raw = await loadManifestText(source);
13182
13217
  const manifest = JSON.parse(raw);
13218
+ if (options.device && typeof manifest.app_id === "string" && (manifest.secrets !== void 0 || manifest.space === "secrets" || typeof manifest.space === "string" && manifest.space.endsWith(":secrets") || Array.isArray(manifest.permissions) && manifest.permissions.some((entry) => entry !== null && typeof entry === "object" && normalizeService(String(entry.service ?? "")) === "tinycloud.encryption"))) {
13219
+ throw new CLIError("DEVICE_AUTH_UNSUPPORTED_SCOPE", "--device cannot authorize tinycloud.encryption or the secrets space. Use browser or --paste login.", ExitCode.USAGE_ERROR);
13220
+ }
13183
13221
  if (typeof manifest.id === "string") {
13184
13222
  const resolved = resolveManifest(manifest);
13223
+ if (options.device && resolved.resources.some((entry) => entry.service === "tinycloud.encryption" || entry.space === "secrets" || entry.space?.endsWith(":secrets"))) {
13224
+ throw new CLIError("DEVICE_AUTH_UNSUPPORTED_SCOPE", "--device cannot authorize tinycloud.encryption or the secrets space. Use browser or --paste login.", ExitCode.USAGE_ERROR);
13225
+ }
13185
13226
  return resolvePermissionSpaces(resolved.resources, profile, options);
13186
13227
  }
13187
13228
  if (typeof manifest.app_id === "string") {
13188
13229
  const permissions = (manifest.permissions ?? []).filter((entry) => entry !== null && typeof entry === "object").map((entry) => {
13189
13230
  const service = normalizeService(String(entry.service ?? ""));
13190
13231
  const path = String(entry.path ?? "");
13191
- const skipPrefix = entry.skipPrefix === true;
13192
- const resolvedPath = skipPrefix ? path : prefixAppManifestPath(path, manifest.app_id);
13232
+ const actions = expandActionShortNames(
13233
+ service,
13234
+ Array.isArray(entry.actions) ? entry.actions.map(String) : []
13235
+ );
13236
+ if (isRawEncryptionPermission({ service, path })) {
13237
+ return { service, space: ENCRYPTION_MANIFEST_SPACE2, path, actions };
13238
+ }
13239
+ const resolvedPath = entry.skipPrefix === true ? path : prefixAppManifestPath(path, manifest.app_id);
13193
13240
  return {
13194
13241
  service,
13195
13242
  space: String(manifest.space ?? "applications"),
13196
13243
  path: resolvedPath,
13197
- actions: expandActionShortNames(
13198
- service,
13199
- Array.isArray(entry.actions) ? entry.actions.map(String) : []
13200
- )
13244
+ actions
13201
13245
  };
13202
13246
  });
13203
- permissions.push(...await secretPermissionsFromAppManifest(manifest, profile));
13247
+ permissions.push(...await secretPermissionsFromAppManifest(manifest, profile, options.ownerDid));
13204
13248
  return resolvePermissionSpaces(permissions, profile, options);
13205
13249
  }
13206
13250
  throw new CLIError(
@@ -13209,7 +13253,7 @@ async function loadManifestPermissions(source, profile, options = {}) {
13209
13253
  ExitCode.USAGE_ERROR
13210
13254
  );
13211
13255
  }
13212
- async function secretPermissionsFromAppManifest(manifest, profile) {
13256
+ async function secretPermissionsFromAppManifest(manifest, profile, requestedOwner) {
13213
13257
  if (manifest.secrets === void 0) {
13214
13258
  return [];
13215
13259
  }
@@ -13230,24 +13274,25 @@ async function secretPermissionsFromAppManifest(manifest, profile) {
13230
13274
  permissions.push({
13231
13275
  service: ENCRYPTION_PERMISSION_SERVICE2,
13232
13276
  space: ENCRYPTION_MANIFEST_SPACE2,
13233
- path: await defaultSecretsNetworkId(profile),
13234
- actions: ["tinycloud.encryption/decrypt"],
13235
- skipPrefix: true
13277
+ path: await defaultSecretsNetworkId(profile, requestedOwner),
13278
+ actions: ["tinycloud.encryption/decrypt"]
13236
13279
  });
13237
13280
  }
13238
13281
  return permissions;
13239
13282
  }
13240
- async function defaultSecretsNetworkId(profileName) {
13283
+ async function defaultSecretsNetworkId(profileName, requestedOwner) {
13241
13284
  const profile = await ProfileManager.getProfile(profileName);
13242
- const ownerDid = (profile.ownerDid ?? profile.did)?.split("#")[0];
13243
- if (!ownerDid) {
13285
+ const ownDid = profile.did?.split("#")[0];
13286
+ const recorded = profile.ownerDid ?? (ownDid?.startsWith("did:pkh:") ? ownDid : void 0);
13287
+ const owner = (recorded ?? requestedOwner)?.split("#")[0];
13288
+ if (!owner) {
13244
13289
  throw new CLIError(
13245
13290
  "OWNER_DID_UNKNOWN",
13246
- `Cannot determine owner DID for profile "${profileName}". Run \`tc auth login\` first.`,
13291
+ `Cannot determine the secrets owner for profile "${profileName}". Pass --owner did:pkh:eip155:CHAIN:ADDRESS with --manifest.`,
13247
13292
  ExitCode.AUTH_REQUIRED
13248
13293
  );
13249
13294
  }
13250
- return `urn:tinycloud:encryption:${ownerDid}:default`;
13295
+ return `urn:tinycloud:encryption:${canonicalOwnerDid(owner, "Secrets owner")}:default`;
13251
13296
  }
13252
13297
  function permissionsFromDelegation(delegation) {
13253
13298
  if (delegation.resources?.length) {
@@ -13278,6 +13323,12 @@ async function resolvePermissionSpaces(entries, profile, options = {}) {
13278
13323
  const resolved = [];
13279
13324
  for (const entry of entries) {
13280
13325
  const service = normalizeService(entry.service);
13326
+ const actions = expandActionShortNames(service, entry.actions);
13327
+ if (isRawEncryptionPermission({ service, path: entry.path })) {
13328
+ const { skipPrefix: _skipPrefix, ...raw } = entry;
13329
+ resolved.push({ ...raw, service, space: ENCRYPTION_MANIFEST_SPACE2, actions });
13330
+ continue;
13331
+ }
13281
13332
  let space;
13282
13333
  try {
13283
13334
  space = await resolveSpaceUri(entry.space, profile) ?? entry.space;
@@ -13291,7 +13342,7 @@ async function resolvePermissionSpaces(entries, profile, options = {}) {
13291
13342
  ...entry,
13292
13343
  service,
13293
13344
  space,
13294
- actions: expandActionShortNames(service, entry.actions)
13345
+ actions
13295
13346
  });
13296
13347
  }
13297
13348
  return resolved;
@@ -13826,8 +13877,8 @@ import { grantAuthRequest, principalDidEquals } from "@tinycloud/node-sdk";
13826
13877
  import { invokeOperation } from "@tinycloud/operations";
13827
13878
 
13828
13879
  // src/auth/browser-auth.ts
13829
- init_formatter();
13830
13880
  init_constants();
13881
+ init_errors();
13831
13882
  import { createServer } from "http";
13832
13883
  import { createInterface } from "readline";
13833
13884
  var PRIVATE_JWK_FIELDS = /* @__PURE__ */ new Set([
@@ -13856,7 +13907,7 @@ async function startAuthFlow(did, options = {}) {
13856
13907
  try {
13857
13908
  return await callbackFlow(did, options);
13858
13909
  } catch {
13859
- if (isInteractive()) {
13910
+ if (process.stdin.isTTY) {
13860
13911
  console.error("Could not open browser. Falling back to manual paste mode.");
13861
13912
  return pasteFlow(did, options);
13862
13913
  }
@@ -14029,22 +14080,23 @@ async function callbackFlow(did, options = {}) {
14029
14080
  const callbackUrl = `http://127.0.0.1:${port}/callback`;
14030
14081
  const authUrl = buildAuthUrl(did, { ...options, callback: callbackUrl });
14031
14082
  const openBrowser = shouldOpenBrowser(options);
14032
- if (openBrowser && isInteractive()) {
14083
+ const hasTerminal = Boolean(process.stdin.isTTY || process.stderr.isTTY);
14084
+ if (openBrowser && hasTerminal) {
14033
14085
  console.error(`Opening browser for authentication...`);
14034
14086
  console.error(`If the browser doesn't open, visit: ${authUrl}`);
14035
- } else if (!openBrowser || isInteractive()) {
14087
+ } else if (!openBrowser || hasTerminal) {
14036
14088
  console.error(`Open this URL in a browser to authenticate: ${authUrl}`);
14037
14089
  }
14038
14090
  if (openBrowser) {
14039
14091
  try {
14040
- const open4 = (await import("open")).default;
14041
- await open4(authUrl);
14092
+ const open5 = (await import("open")).default;
14093
+ await open5(authUrl);
14042
14094
  } catch {
14043
- server.close();
14044
- throw new Error("Failed to open browser");
14095
+ settle({ error: new Error("Failed to open browser") });
14096
+ return;
14045
14097
  }
14046
14098
  }
14047
- if (isInteractive()) {
14099
+ if (process.stdin.isTTY) {
14048
14100
  console.error(`
14049
14101
  If the browser can't connect back, paste the delegation code here:`);
14050
14102
  rl = createInterface({
@@ -14079,7 +14131,10 @@ Open this URL in a browser to authenticate:
14079
14131
  output: process.stderr
14080
14132
  });
14081
14133
  return new Promise((resolve4, reject) => {
14082
- rl.question("Paste delegation code: ", (input) => {
14134
+ let answered = false;
14135
+ rl.on("line", (input) => {
14136
+ if (answered || input.trim() === "") return;
14137
+ answered = true;
14083
14138
  rl.close();
14084
14139
  let parsed;
14085
14140
  try {
@@ -14100,6 +14155,21 @@ Open this URL in a browser to authenticate:
14100
14155
  }
14101
14156
  resolve4(parsed);
14102
14157
  });
14158
+ rl.on("close", () => {
14159
+ if (answered) return;
14160
+ answered = true;
14161
+ reject(new CLIError(
14162
+ "PASTE_CODE_MISSING",
14163
+ `Stdin ended before a delegation code was pasted; no session was saved. Approve at ${authUrl} and pass the returned code on stdin.`,
14164
+ ExitCode.AUTH_REQUIRED,
14165
+ {
14166
+ approvalUrl: authUrl,
14167
+ hint: `Open ${authUrl}, approve, then pass the code on stdin followed by a newline.`
14168
+ }
14169
+ ));
14170
+ });
14171
+ rl.setPrompt("Paste delegation code: ");
14172
+ rl.prompt();
14103
14173
  });
14104
14174
  }
14105
14175
 
@@ -14220,6 +14290,7 @@ function parseExpiry2(input) {
14220
14290
 
14221
14291
  // src/auth/scoped-login.ts
14222
14292
  init_space();
14293
+ var CAPABILITIES_READ = "tinycloud.capabilities/read";
14223
14294
  var CLOCK_SKEW_MS = 3e4;
14224
14295
  var SIGNED_RECAP = "signed-recap";
14225
14296
  function parseRequestedExpiry(value) {
@@ -14289,8 +14360,13 @@ function permissionTuples(permissions, ownerDid) {
14289
14360
  }
14290
14361
  function actionTuples(permission, ownerDid) {
14291
14362
  const service = permission.service.startsWith("tinycloud.") ? permission.service : `tinycloud.${permission.service}`;
14292
- const space = normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
14293
- return permission.actions.map((action) => JSON.stringify([service, space, permission.path, action.includes("/") ? action : `${service}/${action}`]));
14363
+ const raw = isVerifiedRawEncryptionPermission(permission);
14364
+ const space = raw ? ENCRYPTION_MANIFEST_SPACE2 : normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
14365
+ const path = raw ? normalizePkhIdentifier(permission.path) : permission.path;
14366
+ return permission.actions.map((action) => JSON.stringify([service, space, path, action.includes("/") ? action : `${service}/${action}`]));
14367
+ }
14368
+ function isLegacyNestedDecrypt(permission, requested, ownerDid, spaceId) {
14369
+ return isRawEncryptionPermission(permission) && !isVerifiedRawEncryptionPermission(permission) && normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid)) === normalizePkhIdentifier(spaceId) && rawEncryptionOwnerMatches(permission.path, ownerDid) && permission.actions.every((action) => action === "tinycloud.encryption/decrypt") && requested.some((entry) => isRawEncryptionPermission(entry) && normalizePkhIdentifier(entry.path) === normalizePkhIdentifier(permission.path) && permission.actions.every((action) => entry.actions.includes(action)));
14294
14370
  }
14295
14371
  function canonicalJson(value) {
14296
14372
  const canonical = (entry) => Array.isArray(entry) ? entry.map(canonical) : entry && typeof entry === "object" ? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)).map(([name, inner]) => [name, canonical(inner)])) : entry;
@@ -14352,14 +14428,36 @@ function permissionsFromTuples(tuples) {
14352
14428
  }
14353
14429
  return [...grouped.values()];
14354
14430
  }
14431
+ function declinedPermissions(requested, signed, ownerDid) {
14432
+ const granted = permissionTuples(signed, ownerDid);
14433
+ const missing = [...permissionTuples(requested, ownerDid)].filter((tuple) => !granted.has(tuple));
14434
+ const requestedScopes = /* @__PURE__ */ new Map();
14435
+ for (const entry of requested) {
14436
+ const space = isVerifiedRawEncryptionPermission(entry) ? entry.space ?? ENCRYPTION_MANIFEST_SPACE2 : ownerSpaceId(entry.space ?? "", ownerDid);
14437
+ for (const tuple of actionTuples(entry, ownerDid)) {
14438
+ requestedScopes.set(tuple, { space, path: entry.path });
14439
+ }
14440
+ }
14441
+ return permissionsFromTuples(missing).map((entry) => {
14442
+ const original = requestedScopes.get(actionTuples(entry, ownerDid)[0]);
14443
+ return original === void 0 ? entry : { ...entry, ...original };
14444
+ });
14445
+ }
14355
14446
  function validateLoginPermissions(permissions) {
14356
- if (!permissions.length || permissions.some(
14357
- (p) => !p.service?.startsWith("tinycloud.") || !p.space || !p.space.startsWith("tinycloud:") && !/^[A-Za-z0-9_-]+$/.test(p.space) || typeof p.path !== "string" || !p.actions?.length || p.actions.some((action) => !action.startsWith(`${p.service}/`))
14358
- ) || new Set(permissions.map((p) => normalizePkhIdentifier(p.space ?? ""))).size !== 1) {
14359
- throw new CLIError("INVALID_LOGIN_SCOPE", "First login requires non-empty permissions in one TinyCloud space. Request additional spaces after login.", ExitCode.USAGE_ERROR);
14447
+ const spaced = permissions.filter((p) => !isRawEncryptionPermission(p));
14448
+ if (!spaced.length || permissions.some(
14449
+ (p) => !p.service?.startsWith("tinycloud.") || !p.space || (isRawEncryptionPermission(p) ? p.space !== ENCRYPTION_MANIFEST_SPACE2 : !p.space.startsWith("tinycloud:") && !/^[A-Za-z0-9_-]+$/.test(p.space)) || typeof p.path !== "string" || !p.actions?.length || p.actions.some((action) => !action.startsWith(`${p.service}/`))
14450
+ ) || new Set(spaced.map((p) => normalizePkhIdentifier(p.space ?? ""))).size !== 1) {
14451
+ throw new CLIError("INVALID_LOGIN_SCOPE", "First login requires non-empty permissions in one TinyCloud space (raw tinycloud.encryption network entries may accompany them). Request additional spaces after login.", ExitCode.USAGE_ERROR);
14360
14452
  }
14361
14453
  }
14454
+ function scopedLoginPermissions(permissions) {
14455
+ const space = permissions.find((p) => !isRawEncryptionPermission(p)).space ?? "";
14456
+ const hasRead = permissions.some((p) => p.service === "tinycloud.capabilities" && p.path === "" && p.actions.includes(CAPABILITIES_READ) && normalizePkhIdentifier(p.space ?? "") === normalizePkhIdentifier(space));
14457
+ return hasRead ? permissions : [{ service: "tinycloud.capabilities", space, path: "", actions: [CAPABILITIES_READ] }, ...permissions];
14458
+ }
14362
14459
  function verifySignedSession(data, key, sessionDid, expected = {}) {
14460
+ const notStored = expected.purpose === "grant" ? "No grant was stored." : "No session was saved.";
14363
14461
  let permissions;
14364
14462
  let expiresAt;
14365
14463
  try {
@@ -14384,35 +14482,48 @@ function verifySignedSession(data, key, sessionDid, expected = {}) {
14384
14482
  expiresAt = proof.expiresAt;
14385
14483
  } catch (error) {
14386
14484
  if (/expir/i.test(error instanceof Error ? error.message : String(error))) {
14387
- throw new CLIError("AUTH_EXPIRED", "The approved session has expired. No session was saved.", ExitCode.AUTH_REQUIRED);
14485
+ throw new CLIError("AUTH_EXPIRED", `The approved session has expired. ${notStored}`, ExitCode.AUTH_REQUIRED);
14388
14486
  }
14389
- throw new CLIError("OPENKEY_PROOF_INVALID", "OpenKey did not return a complete, verifiable session proof. No session was saved.", ExitCode.AUTH_REQUIRED);
14487
+ throw new CLIError("OPENKEY_PROOF_INVALID", `OpenKey did not return a complete, verifiable session proof. ${notStored}`, ExitCode.AUTH_REQUIRED);
14390
14488
  }
14391
14489
  const signedExpiry = Date.parse(expiresAt);
14392
14490
  if (signedExpiry <= Date.now()) {
14393
- throw new CLIError("AUTH_EXPIRED", "The approved session has expired. No session was saved.", ExitCode.AUTH_REQUIRED);
14491
+ throw new CLIError("AUTH_EXPIRED", `The approved session has expired. ${notStored}`, ExitCode.AUTH_REQUIRED);
14394
14492
  }
14395
14493
  if (expected.expiry !== void 0 && signedExpiry > expiryLimit(expected.expiry)) {
14396
- throw new CLIError("OPENKEY_EXPIRY_EXCEEDED", "The signed session outlives the requested --expiry. No session was saved.", ExitCode.PERMISSION_DENIED);
14494
+ throw new CLIError("OPENKEY_EXPIRY_EXCEEDED", `The signed session outlives the requested --expiry. ${notStored}`, ExitCode.PERMISSION_DENIED);
14397
14495
  }
14398
14496
  const ownerDid = `did:pkh:eip155:${data.chainId}:${data.address}`;
14399
14497
  if (expected.expectedOwner && normalizePkhIdentifier(expected.expectedOwner) !== normalizePkhIdentifier(ownerDid)) {
14400
- throw new CLIError("OPENKEY_OWNER_MISMATCH", "The approved signing identity differs from this profile's owner. No session was saved. Use a new profile for another account.", ExitCode.PERMISSION_DENIED);
14498
+ throw new CLIError("OPENKEY_OWNER_MISMATCH", `The approved signing identity differs from this profile's owner. ${notStored} Use a new profile for another account.`, ExitCode.PERMISSION_DENIED);
14401
14499
  }
14402
14500
  return { ownerDid, permissions, expiresAt };
14403
14501
  }
14404
14502
  function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
14503
+ const notStored = expected.purpose === "grant" ? "No grant was stored." : "No scoped session was saved.";
14405
14504
  const signed = verifySignedSession(data, key, sessionDid, expected);
14406
14505
  const spaceId = data.spaceId;
14407
14506
  for (const permission of requested) {
14507
+ if (isRawEncryptionPermission(permission)) continue;
14408
14508
  if (normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", signed.ownerDid)) !== normalizePkhIdentifier(spaceId)) {
14409
- throw new CLIError("OPENKEY_SCOPE_MISMATCH", "The approved space differs from the requested space. No scoped session was saved.", ExitCode.PERMISSION_DENIED);
14509
+ throw new CLIError("OPENKEY_SCOPE_MISMATCH", `The approved space differs from the requested space. ${notStored}`, ExitCode.PERMISSION_DENIED);
14410
14510
  }
14411
14511
  }
14412
- if (!scopeCovers(requested, signed.permissions, signed.ownerDid)) {
14413
- throw new CLIError("OPENKEY_GRANT_BROADENED", "The signed grant contains authority beyond the requested manifest. No scoped session was saved.", ExitCode.PERMISSION_DENIED);
14512
+ const legacyNested = [];
14513
+ const approved = [];
14514
+ for (const permission of signed.permissions) {
14515
+ (isLegacyNestedDecrypt(permission, requested, signed.ownerDid, spaceId) ? legacyNested : approved).push(permission);
14516
+ }
14517
+ for (const permission of signed.permissions) {
14518
+ if (isVerifiedRawEncryptionPermission(permission) && !rawEncryptionOwnerMatches(permission.path, signed.ownerDid)) {
14519
+ throw new CLIError("OPENKEY_SCOPE_MISMATCH", `OpenKey signed decrypt for a network not owned by the approving identity. ${notStored}`, ExitCode.PERMISSION_DENIED);
14520
+ }
14414
14521
  }
14415
- return withVerifiedAuthority({ ...data, jwk: key }, signed);
14522
+ if (!scopeCovers(requested, approved, signed.ownerDid)) {
14523
+ throw new CLIError("OPENKEY_GRANT_BROADENED", `The signed grant contains authority beyond the requested ${expected.purpose === "grant" ? "grant" : "manifest"}. ${notStored}`, ExitCode.PERMISSION_DENIED);
14524
+ }
14525
+ const session = withVerifiedAuthority({ ...data, jwk: key }, { ...signed, permissions: approved });
14526
+ return { session, legacyNested };
14416
14527
  }
14417
14528
 
14418
14529
  // src/auth/login-commit.ts
@@ -14468,7 +14579,8 @@ function assertSessionReplaceable(profileName, snapshot, ownerDid, scope, newExp
14468
14579
  if (session === null) return;
14469
14580
  const expiresAt = sessionExpiresAt(session);
14470
14581
  if (expiresAt !== null && Date.parse(expiresAt) <= Date.now()) return;
14471
- const keepsScope = ownerDid !== void 0 && session.permissionsSource === SIGNED_RECAP && Array.isArray(session.permissions) && scopeCovers(scope, session.permissions, ownerDid);
14582
+ const held = Array.isArray(session.permissions) ? session.permissions.filter((permission) => ownerDid === void 0 || typeof session.spaceId !== "string" || !isLegacyNestedDecrypt(permission, scope, ownerDid, session.spaceId)) : void 0;
14583
+ const keepsScope = ownerDid !== void 0 && session.permissionsSource === SIGNED_RECAP && held !== void 0 && scopeCovers(scope, held, ownerDid);
14472
14584
  const shortens = newExpiresAt !== void 0 && expiresAt !== null && Date.parse(newExpiresAt) < Date.parse(expiresAt) - CLOCK_SKEW_MS;
14473
14585
  if (keepsScope && !shortens) return;
14474
14586
  const space = typeof session.spaceId === "string" ? session.spaceId : "an unknown space";
@@ -14721,7 +14833,7 @@ async function verifyApproval(input) {
14721
14833
  }
14722
14834
  const invalid = validateDelegationCallbackPayload(delegation);
14723
14835
  if (invalid) throw invalidResponse(`OpenKey returned an invalid delegation: ${invalid}`);
14724
- const session = verifyScopedLogin(delegation, input.key, input.sessionDid, input.requested, {
14836
+ const { session } = verifyScopedLogin(delegation, input.key, input.sessionDid, input.requested, {
14725
14837
  expectedOwner: input.expectedOwner,
14726
14838
  expiry: input.expiry
14727
14839
  });
@@ -14733,14 +14845,13 @@ async function verifyApproval(input) {
14733
14845
  if (!sameTuples(signed, permissionTuples(permissionList(binding.permissions, "approved"), ownerDid)) || !sameTuples(signed, permissionTuples(permissionList(delegation.permissions, "delegated"), ownerDid))) {
14734
14846
  throw new CLIError("DEVICE_AUTH_BINDING_MISMATCH", "OpenKey's approved permissions differ from the signed grant. No session was saved.", ExitCode.PERMISSION_DENIED);
14735
14847
  }
14736
- const requested = permissionTuples(input.requested, ownerDid);
14737
14848
  return {
14738
14849
  session,
14739
14850
  ownerDid,
14740
14851
  spaceId: delegation.spaceId,
14741
14852
  expiresAt: session.expiresAt,
14742
14853
  approved: permissionsFromTuples(signed),
14743
- declined: permissionsFromTuples([...requested].filter((tuple) => !signed.has(tuple)))
14854
+ declined: declinedPermissions(input.requested, session.permissions, ownerDid)
14744
14855
  };
14745
14856
  }
14746
14857
  function writeApprovalPrompt(prompt) {
@@ -14754,6 +14865,13 @@ function writeApprovalPrompt(prompt) {
14754
14865
  }
14755
14866
  async function acquireDeviceDelegation(input) {
14756
14867
  validateLoginPermissions(input.permissions);
14868
+ if (input.permissions.some((permission) => permission.service === "tinycloud.encryption" || permission.space === "secrets" || permission.space?.startsWith("tinycloud:") && permission.space.endsWith(":secrets"))) {
14869
+ throw new CLIError(
14870
+ "DEVICE_AUTH_UNSUPPORTED_SCOPE",
14871
+ "--device cannot authorize tinycloud.encryption or the secrets space. Use another OpenKey approval method for this manifest.",
14872
+ ExitCode.USAGE_ERROR
14873
+ );
14874
+ }
14757
14875
  const expiry = input.expiry ?? { durationMs: DEVICE_DELEGATION_MAX_SECONDS * 1e3 };
14758
14876
  const ttlSeconds = Math.floor(expiry.durationMs / 1e3);
14759
14877
  if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > DEVICE_DELEGATION_MAX_SECONDS) {
@@ -14894,14 +15012,17 @@ async function loginWithDeviceAuthorization(input) {
14894
15012
  const existing = snapshot.profile;
14895
15013
  assertNotLocalOwner(input.profileName, existing, "Device login");
14896
15014
  const expectedOwner = expectedOwnerFor(input.profileName, existing, input.expectedOwner);
15015
+ validateLoginPermissions(input.permissions);
15016
+ const permissions = scopedLoginPermissions(input.permissions);
14897
15017
  if (input.replaceSession !== true) {
14898
15018
  const estimatedExpiry = new Date(Date.now() + (input.expiry?.durationMs ?? DEVICE_DELEGATION_MAX_SECONDS * 1e3)).toISOString();
14899
- assertSessionReplaceable(input.profileName, snapshot, expectedOwner, input.permissions, estimatedExpiry);
15019
+ assertSessionReplaceable(input.profileName, snapshot, expectedOwner, permissions, estimatedExpiry);
14900
15020
  }
14901
15021
  const key = snapshot.key ?? generateKey().jwk;
14902
15022
  const sessionDid = keyToDID(key);
14903
15023
  const result = await acquireDeviceDelegation({
14904
15024
  ...input,
15025
+ permissions,
14905
15026
  sessionDid,
14906
15027
  jwk: key,
14907
15028
  openkeyHost: input.openkeyHost ?? resolveDeviceApiHost(existing),
@@ -14966,7 +15087,7 @@ async function promptAuthMethod() {
14966
15087
  }
14967
15088
  function registerAuthCommand(program) {
14968
15089
  const auth = program.command("auth").description("Authentication management");
14969
- auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest that also requests tinycloud.capabilities/read on the space root (SQL needs browser login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Require this existing primary DID (did:pkh:eip155:CHAIN:ADDRESS) for scoped login").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options, cmd) => {
15090
+ auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Require this existing primary DID (did:pkh:eip155:CHAIN:ADDRESS) for scoped login; names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options, cmd) => {
14970
15091
  try {
14971
15092
  if (options.device && options.paste) {
14972
15093
  throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
@@ -14987,7 +15108,8 @@ function registerAuthCommand(program) {
14987
15108
  }
14988
15109
  const globalOpts = cmd.optsWithGlobals();
14989
15110
  const ctx = await ProfileManager.resolveContext(globalOpts);
14990
- const permissions = options.manifest ? await loadManifestPermissions(options.manifest, ctx.profile, { allowLogicalSpaces: true }) : void 0;
15111
+ const owner = options.owner === void 0 ? void 0 : canonicalOwnerDid(options.owner);
15112
+ const permissions = options.manifest ? await loadManifestPermissions(options.manifest, ctx.profile, { allowLogicalSpaces: true, ownerDid: owner, device: options.device === true }) : void 0;
14991
15113
  const persistHost = globalOpts.host !== void 0;
14992
15114
  if (options.device) {
14993
15115
  const { profile, result } = await loginWithDeviceAuthorization({
@@ -14997,7 +15119,7 @@ function registerAuthCommand(program) {
14997
15119
  permissions,
14998
15120
  ...options.expiry === void 0 ? {} : { expiry: parseRequestedExpiry(parseExpiryOption(options.expiry)) },
14999
15121
  reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest.",
15000
- expectedOwner: options.owner,
15122
+ expectedOwner: owner,
15001
15123
  replaceSession: options.replaceSession === true,
15002
15124
  persistHost
15003
15125
  });
@@ -15031,10 +15153,10 @@ function registerAuthCommand(program) {
15031
15153
  } else {
15032
15154
  method = scoped ? "openkey" : await promptAuthMethod();
15033
15155
  }
15034
- if (method === "openkey" && !options.paste && options.popup !== false && !isInteractive()) {
15156
+ if (method === "openkey" && !options.paste && options.popup !== false && !process.stdin.isTTY && !process.stderr.isTTY) {
15035
15157
  throw new CLIError(
15036
15158
  "INTERACTIVE_LOGIN_REQUIRED",
15037
- `Browser login needs a browser on this machine and would wait silently. Use \`tc auth login --device --manifest ${SHARE_PUBLISHING_MANIFEST_REF}\` (or your app's manifest) to approve on a phone, or \`--paste\` to paste a return code.`,
15159
+ "Browser login needs a visible approval URL and would wait silently here. Use `--paste` to print the URL and provide the owner's code on stdin.",
15038
15160
  ExitCode.USAGE_ERROR
15039
15161
  );
15040
15162
  }
@@ -15046,7 +15168,7 @@ function registerAuthCommand(program) {
15046
15168
  noPopup: options.popup === false,
15047
15169
  permissions,
15048
15170
  expiry: parseExpiryOption(options.expiry),
15049
- expectedOwner: options.owner,
15171
+ expectedOwner: owner,
15050
15172
  replaceSession: options.replaceSession === true,
15051
15173
  persistHost
15052
15174
  });
@@ -15173,10 +15295,15 @@ function registerAuthCommand(program) {
15173
15295
  if (!key) {
15174
15296
  throw new CLIError("NO_KEY", `No key found for profile "${ctx.profile}". Run \`tc init\` first.`, ExitCode.AUTH_REQUIRED);
15175
15297
  }
15176
- const delegationCids2 = [];
15177
- let expiry2;
15178
15298
  const openkeyHost = resolveOpenKeyHost(profile);
15179
- const openkeyEffective = [];
15299
+ const grants = [];
15300
+ const expiryCap = expiryOption === void 0 ? void 0 : parseRequestedExpiry(expiryOption);
15301
+ const proof = {
15302
+ key,
15303
+ sessionDid: profile.sessionDid ?? profile.did,
15304
+ expectedOwner: pinnedOwner(profile),
15305
+ expiry: expiryCap
15306
+ };
15180
15307
  const declined = [];
15181
15308
  for (const group of groupPermissionsBySpace(requested)) {
15182
15309
  const reason = permissionGrantReason(
@@ -15205,29 +15332,20 @@ function registerAuthCommand(program) {
15205
15332
  permissions: group,
15206
15333
  reason,
15207
15334
  openkeyHost,
15208
- expiry: expiryOption === void 0 ? void 0 : openKeyExpiryParam(parseRequestedExpiry(expiryOption)),
15335
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
15209
15336
  noPopup: options.popup === false
15210
15337
  });
15211
15338
  }
15212
- const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
15213
- const effective = permissionsFromDelegation(delegation);
15214
- openkeyEffective.push(...effective);
15215
- const stored = storedAdditionalDelegation(delegation, effective);
15216
- await appendAdditionalDelegation(ctx.profile, stored);
15217
- await node.useRuntimeDelegation(delegation);
15218
- delegationCids2.push(delegation.cid);
15219
- expiry2 = delegation.expiry.toISOString();
15220
- await appendGrantHistory(ctx.profile, {
15221
- addedCaps: effective,
15222
- source: options.manifest ? "manifest" : "cli",
15223
- delegationCid: delegation.cid,
15224
- expiry: expiry2
15225
- });
15339
+ const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host, proof);
15340
+ grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
15226
15341
  }
15342
+ await activateAndStoreOpenKeyGrants(ctx.profile, node, grants, options.manifest ? "manifest" : "cli");
15343
+ const delegationCids2 = grants.map(({ delegation }) => delegation.cid);
15344
+ const expiry2 = grants.at(-1)?.delegation.expiry.toISOString();
15227
15345
  reportDeclined(declined);
15228
15346
  outputJson({
15229
15347
  changed: delegationCids2.length > 0,
15230
- added: openkeyEffective,
15348
+ added: grants.flatMap(({ effective }) => effective),
15231
15349
  delegationCid: delegationCids2[0],
15232
15350
  delegationCids: delegationCids2,
15233
15351
  expiry: expiry2,
@@ -15712,6 +15830,26 @@ function normalizePortableDelegation(delegation) {
15712
15830
  }
15713
15831
  return { ...delegation, expiry };
15714
15832
  }
15833
+ async function activateAndStoreOpenKeyGrants(profileName, node, grants, source) {
15834
+ for (const { delegation } of grants) await node.useRuntimeDelegation(delegation);
15835
+ if (grants.length === 0) return;
15836
+ await ProfileManager.withLock(profileName, async () => {
15837
+ const existing = await loadAdditionalDelegations(profileName);
15838
+ const replacing = new Set(grants.map(({ delegation }) => delegation.cid));
15839
+ for (const { delegation, effective } of grants) {
15840
+ await appendGrantHistory(profileName, {
15841
+ addedCaps: effective,
15842
+ source,
15843
+ delegationCid: delegation.cid,
15844
+ expiry: delegation.expiry.toISOString()
15845
+ });
15846
+ }
15847
+ await saveAdditionalDelegations(profileName, [
15848
+ ...existing.filter(({ delegation }) => !replacing.has(delegation.cid)),
15849
+ ...grants.map(({ delegation, effective }) => storedAdditionalDelegation(delegation, effective))
15850
+ ]);
15851
+ });
15852
+ }
15715
15853
  async function ensureDelegationAuthority(params) {
15716
15854
  if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
15717
15855
  if (params.profile.authMethod === "openkey") {
@@ -15725,6 +15863,14 @@ async function ensureDelegationAuthority(params) {
15725
15863
  }
15726
15864
  const openkeyHost = resolveOpenKeyHost(params.profile);
15727
15865
  const acquireOpenKey = params.openKeyAcquisition ?? startAuthFlow;
15866
+ const expiryCap = params.expiryOption === void 0 ? void 0 : parseRequestedExpiry(params.expiryOption);
15867
+ const proof = {
15868
+ key,
15869
+ sessionDid: params.profile.sessionDid ?? params.profile.did,
15870
+ expectedOwner: pinnedOwner(params.profile),
15871
+ expiry: expiryCap
15872
+ };
15873
+ const grants = [];
15728
15874
  for (const group of groupPermissionsBySpace(params.requested)) {
15729
15875
  const delegationData = await acquireOpenKey(params.profile.did, {
15730
15876
  jwk: key,
@@ -15732,22 +15878,12 @@ async function ensureDelegationAuthority(params) {
15732
15878
  permissions: group,
15733
15879
  reason: permissionGrantReason(params.reason, group),
15734
15880
  openkeyHost,
15735
- expiry: params.expiryOption === void 0 ? void 0 : openKeyExpiryParam(parseRequestedExpiry(params.expiryOption))
15736
- });
15737
- const delegation = portableFromOpenKeyDelegation(delegationData, group, params.ctx.host);
15738
- const effective = permissionsFromDelegation(delegation);
15739
- await appendAdditionalDelegation(
15740
- params.ctx.profile,
15741
- storedAdditionalDelegation(delegation, effective)
15742
- );
15743
- await params.node.useRuntimeDelegation(delegation);
15744
- await appendGrantHistory(params.ctx.profile, {
15745
- addedCaps: effective,
15746
- source: "cli",
15747
- delegationCid: delegation.cid,
15748
- expiry: delegation.expiry.toISOString()
15881
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
15749
15882
  });
15883
+ const delegation = portableFromOpenKeyDelegation(delegationData, group, params.ctx.host, proof);
15884
+ grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
15750
15885
  }
15886
+ await activateAndStoreOpenKeyGrants(params.ctx.profile, params.node, grants, "cli");
15751
15887
  return;
15752
15888
  }
15753
15889
  if (isInteractive()) {
@@ -15818,7 +15954,7 @@ async function collectRequestedPermissions(options, profile) {
15818
15954
  permissions.push(...await loadPermissionRequest(options.permission, profile));
15819
15955
  }
15820
15956
  if (options.manifest) {
15821
- permissions.push(...await loadManifestPermissions(options.manifest, profile));
15957
+ permissions.push(...await loadManifestPermissions(options.manifest, profile, { device: options.device === true }));
15822
15958
  }
15823
15959
  return permissions;
15824
15960
  }
@@ -15870,7 +16006,7 @@ function groupPermissionsBySpace(permissions) {
15870
16006
  const groups = /* @__PURE__ */ new Map();
15871
16007
  const rawEntries = [];
15872
16008
  for (const permission of permissions) {
15873
- if (isRawPermission(permission)) {
16009
+ if (isRawEncryptionPermission(permission)) {
15874
16010
  rawEntries.push(permission);
15875
16011
  continue;
15876
16012
  }
@@ -15886,119 +16022,44 @@ function groupPermissionsBySpace(permissions) {
15886
16022
  grouped[0].push(...rawEntries);
15887
16023
  return grouped;
15888
16024
  }
15889
- function isRawPermission(permission) {
15890
- return permission.service === "tinycloud.encryption" && permission.path.startsWith("urn:tinycloud:encryption:");
15891
- }
15892
- function returnedSpaceMatchesExpected(returnedSpace, expectedSpace) {
15893
- if (normalizePkhIdentifier(returnedSpace) === normalizePkhIdentifier(expectedSpace)) {
15894
- return true;
15895
- }
15896
- if (!returnedSpace.startsWith("tinycloud:")) return false;
15897
- const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
15898
- return returnedName === expectedSpace;
15899
- }
15900
- function portableFromOpenKeyDelegation(data, permissions, host) {
15901
- const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
15902
- const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
15903
- const expectedSpaces = new Set(
15904
- permissions.filter((permission) => !isRawPermission(permission)).map((permission) => normalizePkhIdentifier(permission.space ?? ""))
15905
- );
15906
- const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
15907
- if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
16025
+ function portableFromOpenKeyDelegation(data, requested, host, proof) {
16026
+ const { session, legacyNested } = verifyScopedLogin(data, proof.key, proof.sessionDid, requested, {
16027
+ expectedOwner: proof.expectedOwner,
16028
+ expiry: proof.expiry,
16029
+ purpose: "grant"
16030
+ });
16031
+ if (legacyNested.length > 0) {
15908
16032
  throw new CLIError(
15909
- "OPENKEY_SCOPE_MISMATCH",
15910
- `OpenKey returned delegation for ${returnedSpace}, expected ${Array.from(expectedSpaces).join(", ")}.`,
16033
+ "OPENKEY_GRANT_BROADENED",
16034
+ "OpenKey signed decrypt inside the space instead of on the raw network. The OpenKey deployment is too old for agent secret reads.",
15911
16035
  ExitCode.PERMISSION_DENIED
15912
16036
  );
15913
16037
  }
15914
- const expiry = inferDelegationExpiry(data);
15915
- const requestedPairs = new Set(
15916
- permissions.flatMap(
15917
- (p) => isRawPermission(p) ? p.actions.map((a) => `${p.service}|${p.space ?? ""}|${p.path}|${a}`) : p.actions.map((a) => `${p.service}|${normalizePkhIdentifier(p.space ?? "")}|${p.path}|${a}`)
15918
- )
15919
- );
15920
- const returnedPermissions = Array.isArray(data.permissions) ? data.permissions : null;
15921
- const resources = (returnedPermissions ?? permissions).map((permission) => {
15922
- const service = permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service;
15923
- const rawService = permission.service.startsWith("tinycloud.") ? permission.service : `tinycloud.${service}`;
15924
- const permSpace = permission.space ?? "";
15925
- if (returnedPermissions) {
15926
- const rawSpace = isRawPermission({
15927
- service: rawService,
15928
- space: permSpace,
15929
- path: permission.path,
15930
- actions: []
15931
- }) ? permSpace : normalizePkhIdentifier(permSpace);
15932
- for (const action of permission.actions) {
15933
- const key = `${rawService}|${rawSpace}|${permission.path}|${action}`;
15934
- if (!requestedPairs.has(key)) {
15935
- throw new CLIError(
15936
- "OPENKEY_GRANT_BROADENED",
15937
- `OpenKey returned grant ${rawService}/${action} on ${permSpace}/${permission.path} that was not requested.`,
15938
- ExitCode.PERMISSION_DENIED
15939
- );
15940
- }
15941
- }
15942
- }
15943
- const resolvedSpace = isRawPermission({
15944
- service: rawService,
15945
- space: permSpace,
15946
- path: permission.path,
15947
- actions: []
15948
- }) ? permSpace : returnedSpace;
15949
- const caveats = Array.isArray(permission.caveats) && permission.caveats.length > 0 ? permission.caveats.map((caveat) => structuredClone(caveat)) : void 0;
15950
- return {
15951
- service,
15952
- space: resolvedSpace,
15953
- path: permission.path,
15954
- actions: [...permission.actions],
15955
- ...caveats === void 0 ? {} : { caveats }
15956
- };
15957
- });
16038
+ const returnedSpace = data.spaceId;
16039
+ const effective = session.permissions;
16040
+ const primary = effective.find((permission) => !isRawEncryptionPermission(permission)) ?? effective[0];
16041
+ const resources = effective.map((permission) => ({
16042
+ service: permission.service.slice("tinycloud.".length),
16043
+ space: isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
16044
+ path: permission.path,
16045
+ actions: [...permission.actions],
16046
+ ...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
16047
+ }));
16048
+ const ownerParts = session.ownerDid.split(":");
15958
16049
  return {
15959
- cid: String(data.delegationCid),
16050
+ cid: data.delegationCid,
15960
16051
  delegationHeader: data.delegationHeader,
15961
16052
  spaceId: returnedSpace,
15962
16053
  path: primary.path,
15963
- actions: primary.actions,
16054
+ actions: [...primary.actions],
15964
16055
  resources,
15965
- expiry,
15966
- delegateDID: String(data.verificationMethod),
15967
- ownerAddress: String(data.address ?? ""),
15968
- chainId: typeof data.chainId === "number" ? data.chainId : DEFAULT_CHAIN_ID,
16056
+ expiry: new Date(session.expiresAt),
16057
+ delegateDID: data.verificationMethod,
16058
+ ownerAddress: ownerParts[4],
16059
+ chainId: Number(ownerParts[3]),
15969
16060
  host
15970
16061
  };
15971
16062
  }
15972
- function inferDelegationExpiry(data) {
15973
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
15974
- const parsed = parseDelegationExpiryField(data[key]);
15975
- if (parsed) return parsed;
15976
- }
15977
- if (typeof data.siwe === "string") {
15978
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
15979
- const parsed = match ? parseDelegationExpiryField(match[1]?.trim()) : null;
15980
- if (parsed) return parsed;
15981
- }
15982
- throw new CLIError(
15983
- "OPENKEY_EXPIRY_MISSING",
15984
- "OpenKey delegation response did not include expiry, expiresAt, expirationTime, or a SIWE Expiration Time.",
15985
- ExitCode.ERROR
15986
- );
15987
- }
15988
- function parseDelegationExpiryField(value) {
15989
- if (value instanceof Date) {
15990
- return Number.isNaN(value.getTime()) ? null : value;
15991
- }
15992
- if (typeof value === "number") {
15993
- const parsed = new Date(value < 1e10 ? value * 1e3 : value);
15994
- if (!Number.isNaN(parsed.getTime())) return parsed;
15995
- }
15996
- if (typeof value === "string") {
15997
- const parsed = new Date(value);
15998
- if (!Number.isNaN(parsed.getTime())) return parsed;
15999
- }
16000
- return null;
16001
- }
16002
16063
  async function rotateAuthKey(profileName, host, options = {}) {
16003
16064
  const profile = await ProfileManager.getProfile(profileName);
16004
16065
  const posture = resolveProfilePosture(profile);
@@ -16173,7 +16234,8 @@ async function handleLocalAuth(profileName, host, options = {}) {
16173
16234
  return { profile: updatedProfile, sessionResult };
16174
16235
  }
16175
16236
  async function handleOpenKeyAuth(profileName, host, options = {}) {
16176
- const { profile, delegationData } = await refreshOpenKeySession(profileName, host, options);
16237
+ const { profile, delegationData, declined, legacyNested } = await refreshOpenKeySession(profileName, host, options);
16238
+ reportDeclined(declined, legacyNested);
16177
16239
  outputJson({
16178
16240
  authenticated: true,
16179
16241
  profile: profileName,
@@ -16185,18 +16247,23 @@ async function handleOpenKeyAuth(profileName, host, options = {}) {
16185
16247
  ownerDid: profile.ownerDid,
16186
16248
  host,
16187
16249
  permissions: delegationData.permissions,
16250
+ declined,
16188
16251
  expiresAt: delegationData.expiresAt,
16189
16252
  activation: delegationData.hostActivated === true ? "confirmed-by-openkey" : "unverified"
16190
16253
  } : {}
16191
16254
  });
16192
16255
  }
16193
- function reportDeclined(declined) {
16256
+ function reportDeclined(declined, signed) {
16194
16257
  if (declined.length === 0) return;
16195
16258
  process.stderr.write(
16196
- `${theme.warn("The owner did not approve:")}
16259
+ `${theme.warn("Not granted in this session:")}
16197
16260
  ${declined.map((permission) => ` ${compactPermission(permission)}`).join("\n")}
16198
16261
  `
16199
16262
  );
16263
+ if (declined.some((permission) => isRawEncryptionPermission(permission) && signed?.some((entry) => isRawEncryptionPermission(entry) && !isVerifiedRawEncryptionPermission(entry) && normalizePkhIdentifier(entry.path) === normalizePkhIdentifier(permission.path)))) {
16264
+ process.stderr.write(`${theme.warn("OpenKey signed decrypt inside the space; the TinyCloud node refuses that. The OpenKey deployment is too old for agent secret reads.")}
16265
+ `);
16266
+ }
16200
16267
  }
16201
16268
  async function refreshOpenKeySession(profileName, host, options = {}) {
16202
16269
  const snapshot = await readProfileSnapshot(profileName);
@@ -16209,16 +16276,17 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
16209
16276
  );
16210
16277
  }
16211
16278
  const profile = snapshot.profile ?? await ProfileManager.getProfile(profileName);
16212
- if (options.permissions !== void 0) {
16213
- validateLoginPermissions(options.permissions);
16279
+ if (options.permissions !== void 0) validateLoginPermissions(options.permissions);
16280
+ const permissions = options.permissions === void 0 ? void 0 : scopedLoginPermissions(options.permissions);
16281
+ if (permissions !== void 0) {
16214
16282
  assertNotLocalOwner(profileName, profile, "Scoped browser login");
16215
16283
  }
16216
16284
  const expiry = options.expiry === void 0 ? void 0 : parseRequestedExpiry(options.expiry);
16217
16285
  const openKeyExpiry = expiry === void 0 ? void 0 : openKeyExpiryParam(expiry);
16218
16286
  const expectedOwner = expectedOwnerFor(profileName, profile, options.expectedOwner);
16219
- if (options.permissions !== void 0 && options.replaceSession !== true) {
16287
+ if (permissions !== void 0 && options.replaceSession !== true) {
16220
16288
  const estimatedExpiry = expiry === void 0 ? void 0 : new Date(Date.now() + expiry.durationMs).toISOString();
16221
- assertSessionReplaceable(profileName, snapshot, expectedOwner, options.permissions, estimatedExpiry);
16289
+ assertSessionReplaceable(profileName, snapshot, expectedOwner, permissions, estimatedExpiry);
16222
16290
  }
16223
16291
  const acquireOpenKey = options.openKeyAcquisition ?? startAuthFlow;
16224
16292
  const delegationData = await acquireOpenKey(profile.did, {
@@ -16227,16 +16295,21 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
16227
16295
  jwk: key,
16228
16296
  host,
16229
16297
  openkeyHost: resolveOpenKeyHost(profile),
16230
- permissions: options.permissions,
16298
+ permissions,
16231
16299
  expiry: openKeyExpiry,
16232
- ...options.permissions ? { reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest." } : {}
16300
+ ...permissions ? { reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest." } : {}
16233
16301
  });
16234
16302
  const sessionDid = profile.sessionDid ?? profile.did;
16235
16303
  let sanitizedSession;
16236
16304
  let verifiedOwner;
16237
- if (options.permissions) {
16238
- sanitizedSession = verifyScopedLogin(delegationData, key, sessionDid, options.permissions, { expectedOwner, expiry });
16305
+ let declined = [];
16306
+ let legacyNested = [];
16307
+ if (permissions) {
16308
+ const verified = verifyScopedLogin(delegationData, key, sessionDid, permissions, { expectedOwner, expiry });
16309
+ sanitizedSession = verified.session;
16310
+ legacyNested = verified.legacyNested;
16239
16311
  verifiedOwner = sanitizedSession.ownerDid;
16312
+ declined = declinedPermissions(permissions, sanitizedSession.permissions, verifiedOwner);
16240
16313
  } else {
16241
16314
  const carriesProof = typeof delegationData.siwe === "string" && typeof delegationData.signature === "string";
16242
16315
  const merged = mergePrivateJwkIntoSession(delegationData, key);
@@ -16262,9 +16335,9 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
16262
16335
  key,
16263
16336
  session: sanitizedSession,
16264
16337
  profile: updatedProfile,
16265
- ...options.permissions && verifiedOwner ? { approved: { scope: sanitizedSession.permissions, ownerDid: verifiedOwner, replaceSession: options.replaceSession === true } } : {}
16338
+ ...permissions && verifiedOwner ? { approved: { scope: sanitizedSession.permissions, ownerDid: verifiedOwner, replaceSession: options.replaceSession === true } } : {}
16266
16339
  });
16267
- return { profile: updatedProfile, delegationData: sanitizedSession };
16340
+ return { profile: updatedProfile, delegationData: sanitizedSession, declined, legacyNested };
16268
16341
  }
16269
16342
 
16270
16343
  // src/commands/completion.ts
@@ -17568,9 +17641,10 @@ init_formatter();
17568
17641
  init_theme();
17569
17642
  init_errors();
17570
17643
  init_constants();
17571
- import { readFile as readFile8 } from "fs/promises";
17572
- import { writeFile as writeFile5 } from "fs/promises";
17573
- import { join as join5 } from "path";
17644
+ init_storage();
17645
+ import { randomUUID as randomUUID2 } from "crypto";
17646
+ import { lstat, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
17647
+ import { basename as basename2, dirname as dirname3, join as join5 } from "path";
17574
17648
  import { homedir } from "os";
17575
17649
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
17576
17650
  import {
@@ -17673,6 +17747,14 @@ async function ensureSecretsNode(ctx, options, openKeyAcquisition, selectedProfi
17673
17747
  if (profile?.authMethod === "openkey" && canRequestOwnerPermissions(profile)) {
17674
17748
  const session = await ProfileManager.getSession(ctx.profile);
17675
17749
  if (!session || isStoredSessionExpired(session)) {
17750
+ if (!process.stdin.isTTY && !process.stderr.isTTY) {
17751
+ throw new CLIError(
17752
+ "AUTH_REQUIRED",
17753
+ `Profile "${ctx.profile}" has ${session ? "an expired" : "no"} OpenKey session; headless secret access cannot open a browser login.`,
17754
+ ExitCode.AUTH_REQUIRED,
17755
+ { hint: scopedSecretLoginHint(ctx.profile) }
17756
+ );
17757
+ }
17676
17758
  await withSpinner(
17677
17759
  session ? "Refreshing TinyCloud session..." : "Creating TinyCloud session...",
17678
17760
  () => refreshOpenKeySession(ctx.profile, ctx.host, { openKeyAcquisition })
@@ -17690,6 +17772,7 @@ async function runSecretOperation(params) {
17690
17772
  if (!canRequestOwnerPermissions(profile)) {
17691
17773
  return first;
17692
17774
  }
17775
+ assertOwnerApprovalPossible(params.ctx.profile, profile, params.action, params.name);
17693
17776
  const requested = secretPermissionEntries({
17694
17777
  action: params.action,
17695
17778
  name: params.name,
@@ -17717,6 +17800,72 @@ function secretPermissionReason(action, name) {
17717
17800
  const target = name ? ` secret "${name}"` : " secrets";
17718
17801
  return `Allow \`tc secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
17719
17802
  }
17803
+ function assertOwnerApprovalPossible(profileName, profile, action, name, missing = []) {
17804
+ if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
17805
+ const command = `tc secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
17806
+ throw new CLIError(
17807
+ "PERMISSION_DENIED",
17808
+ action === "get" ? missing.length > 0 && missing.every((permission) => permission.service === "tinycloud.encryption" && Array.isArray(permission.actions) && permission.actions.length > 0 && permission.actions.every((capability) => capability === SECRET_DECRYPT_CAPABILITY)) ? `Profile "${profileName}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
17809
+ ExitCode.PERMISSION_DENIED,
17810
+ {
17811
+ hint: scopedSecretLoginHint(profileName)
17812
+ }
17813
+ );
17814
+ }
17815
+ function scopedSecretLoginHint(profileName) {
17816
+ return `Have the owner approve a scoped login whose manifest names the secret: tc --profile ${profileName} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
17817
+ }
17818
+ async function validateSecretOutput(path) {
17819
+ try {
17820
+ const destination = await lstat(path);
17821
+ if (!destination.isFile()) {
17822
+ throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
17823
+ }
17824
+ } catch (error) {
17825
+ if (!isMissingFileError(error)) throw error;
17826
+ }
17827
+ try {
17828
+ const parent = await lstat(dirname3(path));
17829
+ if (!parent.isDirectory()) {
17830
+ throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
17831
+ }
17832
+ } catch (error) {
17833
+ if (!isMissingFileError(error)) throw error;
17834
+ throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
17835
+ }
17836
+ }
17837
+ async function writeSecretFile(path, value) {
17838
+ await validateSecretOutput(path);
17839
+ const parentPath = dirname3(path);
17840
+ const temp = join5(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
17841
+ let created = false;
17842
+ try {
17843
+ const handle = await open2(temp, "wx", PRIVATE_FILE_MODE);
17844
+ created = true;
17845
+ try {
17846
+ await handle.writeFile(value);
17847
+ await handle.sync();
17848
+ } finally {
17849
+ await handle.close();
17850
+ }
17851
+ await rename2(temp, path);
17852
+ } catch (error) {
17853
+ if (created) {
17854
+ await rm3(temp, { force: true }).catch(() => void 0);
17855
+ }
17856
+ const code2 = error instanceof Error && "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
17857
+ throw new CLIError("ERROR", `Could not write secret output "${path}"${code2}.`, ExitCode.ERROR);
17858
+ }
17859
+ try {
17860
+ const parent = await open2(parentPath, "r");
17861
+ try {
17862
+ await parent.sync();
17863
+ } finally {
17864
+ await parent.close();
17865
+ }
17866
+ } catch {
17867
+ }
17868
+ }
17720
17869
  async function runSecretOperationAttempt(label, operation) {
17721
17870
  try {
17722
17871
  return await withSpinner(label, operation);
@@ -17729,6 +17878,15 @@ async function runSecretOperationAttempt(label, operation) {
17729
17878
  async function invokeCanonicalSecretGet(params) {
17730
17879
  const auth = authOptions(params.options);
17731
17880
  let ownerNode;
17881
+ if (!auth?.privateKey) {
17882
+ const profile2 = await ProfileManager.getProfile(params.ctx.profile).catch(() => null);
17883
+ if (profile2?.authMethod === "openkey" && canRequestOwnerPermissions(profile2)) {
17884
+ const session = await ProfileManager.getSession(params.ctx.profile);
17885
+ if (!session || isStoredSessionExpired(session)) {
17886
+ ownerNode = await ensureSecretsNode(params.ctx, params.options, params.openKeyAcquisition, profile2);
17887
+ }
17888
+ }
17889
+ }
17732
17890
  const target = {
17733
17891
  profile: params.ctx.profile,
17734
17892
  host: params.ctx.host,
@@ -17764,6 +17922,7 @@ async function invokeCanonicalSecretGet(params) {
17764
17922
  }
17765
17923
  const profile = await ProfileManager.getProfile(params.ctx.profile);
17766
17924
  if (!canRequestOwnerPermissions(profile)) return first;
17925
+ assertOwnerApprovalPossible(params.ctx.profile, profile, "get", params.name, first.missing);
17767
17926
  const node = params.node ?? ownerNode ?? await ensureSecretsNode(
17768
17927
  params.ctx,
17769
17928
  params.options,
@@ -18387,6 +18546,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18387
18546
  const scopeOptions = resolveSecretScope(options);
18388
18547
  const legacySpaceUri = await resolveSecretSpace(options.space, ctx.profile);
18389
18548
  const secretPath = resolveSecretPath2(name, scopeOptions).permissionPaths.vault;
18549
+ if (options.output) await validateSecretOutput(options.output);
18390
18550
  if (options.delegation) {
18391
18551
  const delegated = await resolveDelegatedSecretSource(
18392
18552
  options.delegation,
@@ -18409,7 +18569,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18409
18569
  })
18410
18570
  );
18411
18571
  if (options.output) {
18412
- await writeFile5(options.output, value2);
18572
+ await writeSecretFile(options.output, value2);
18413
18573
  outputJson({ name, written: options.output });
18414
18574
  return;
18415
18575
  }
@@ -18436,7 +18596,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18436
18596
  }
18437
18597
  const value = result.output.value;
18438
18598
  if (options.output) {
18439
- await writeFile5(options.output, value);
18599
+ await writeSecretFile(options.output, value);
18440
18600
  outputJson({ name, written: options.output });
18441
18601
  return;
18442
18602
  }
@@ -18549,8 +18709,8 @@ function registerSecretsCommand(program, openKeyAcquisition) {
18549
18709
  });
18550
18710
  secrets.command("manage").description("Open the TinyCloud Secrets Manager in your browser").action(async () => {
18551
18711
  try {
18552
- const open4 = (await import("open")).default;
18553
- await open4("https://secrets.tinycloud.xyz");
18712
+ const open5 = (await import("open")).default;
18713
+ await open5("https://secrets.tinycloud.xyz");
18554
18714
  outputJson({ opened: "https://secrets.tinycloud.xyz" });
18555
18715
  } catch (error) {
18556
18716
  handleError(error);
@@ -25744,7 +25904,7 @@ async function importAesKey(key32, usage) {
25744
25904
  [usage]
25745
25905
  );
25746
25906
  }
25747
- async function open2(blob, key32) {
25907
+ async function open3(blob, key32) {
25748
25908
  if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
25749
25909
  throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
25750
25910
  }
@@ -26082,7 +26242,7 @@ async function resolvePolicyShare(link2, options) {
26082
26242
  options.signal?.throwIfAborted();
26083
26243
  let envelope;
26084
26244
  try {
26085
- const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open2(parsed.ciphertext, parsed.key32));
26245
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open3(parsed.ciphertext, parsed.key32));
26086
26246
  const value = JSON.parse(encoded);
26087
26247
  if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
26088
26248
  envelope = shareEnvelopeV3Schema.parse(value);
@@ -27316,9 +27476,9 @@ function receiveJson(result, path) {
27316
27476
 
27317
27477
  // src/share/io.ts
27318
27478
  import { constants } from "fs";
27319
- import { lstat, mkdir as mkdir3, mkdtemp, open as open3, readFile as readFile9, realpath, stat as stat2, link, rename as rename2, rm as rm3, unlink } from "fs/promises";
27479
+ import { lstat as lstat2, mkdir as mkdir3, mkdtemp, open as open4, readFile as readFile9, realpath, stat as stat2, link, rename as rename3, rm as rm4, unlink } from "fs/promises";
27320
27480
  import { randomBytes as randomBytes4 } from "crypto";
27321
- import { basename as basename2, join as join6, resolve as resolve2, sep } from "path";
27481
+ import { basename as basename3, join as join6, resolve as resolve2, sep } from "path";
27322
27482
  init_errors();
27323
27483
  var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
27324
27484
  var MAX_SHARE_URL_BYTES = 64 * 1024;
@@ -27355,7 +27515,7 @@ function shareFilename(value) {
27355
27515
  }
27356
27516
  }
27357
27517
  function shareInputFilename(input, name) {
27358
- return shareFilename(name ?? (input === "-" ? "stdin.md" : basename2(resolve2(input))));
27518
+ return shareFilename(name ?? (input === "-" ? "stdin.md" : basename3(resolve2(input))));
27359
27519
  }
27360
27520
  async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
27361
27521
  const filename = shareInputFilename(input, name);
@@ -27374,7 +27534,7 @@ async function assertDirectory(path) {
27374
27534
  for (const segment of segments) {
27375
27535
  current = current === sep ? join6(current, segment) : join6(current, segment);
27376
27536
  try {
27377
- const info = await lstat(current);
27537
+ const info = await lstat2(current);
27378
27538
  if (info.isSymbolicLink()) {
27379
27539
  const canonical = await realpath(current);
27380
27540
  if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
@@ -27383,7 +27543,7 @@ async function assertDirectory(path) {
27383
27543
  } catch (error) {
27384
27544
  if (error.code !== "ENOENT") throw error;
27385
27545
  await mkdir3(current, { mode: 448 });
27386
- const created = await lstat(current);
27546
+ const created = await lstat2(current);
27387
27547
  if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
27388
27548
  }
27389
27549
  }
@@ -27392,7 +27552,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
27392
27552
  const outputDirectory = resolve2(directory);
27393
27553
  await assertDirectory(outputDirectory);
27394
27554
  const safeName = safeFilename(filename);
27395
- const directoryHandle = await open3(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
27555
+ const directoryHandle = await open4(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
27396
27556
  const stableDirectory = await realpath(outputDirectory);
27397
27557
  const outputPath = join6(stableDirectory, safeName);
27398
27558
  const directoryIdentity = await directoryHandle.stat();
@@ -27402,7 +27562,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
27402
27562
  };
27403
27563
  await assertStableDirectory();
27404
27564
  const stagingDirectory = await mkdtemp(join6(stableDirectory, ".tinycloud-share-stage-"));
27405
- const stagingInfo = await lstat(stagingDirectory);
27565
+ const stagingInfo = await lstat2(stagingDirectory);
27406
27566
  if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
27407
27567
  const stagingPath = join6(stagingDirectory, `.tinycloud-share-${randomBytes4(16).toString("hex")}.tmp`);
27408
27568
  let temporaryPath;
@@ -27410,19 +27570,19 @@ async function writeShareOutput(directory, filename, bytes, force) {
27410
27570
  try {
27411
27571
  await assertStableDirectory();
27412
27572
  try {
27413
- const existing = await lstat(outputPath);
27573
+ const existing = await lstat2(outputPath);
27414
27574
  if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
27415
27575
  } catch (error) {
27416
27576
  if (error.code !== "ENOENT") throw error;
27417
27577
  }
27418
27578
  temporaryPath = stagingPath;
27419
- handle = await open3(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
27579
+ handle = await open4(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
27420
27580
  await handle.writeFile(bytes);
27421
27581
  await handle.close();
27422
27582
  handle = void 0;
27423
27583
  await assertStableDirectory();
27424
27584
  if (force) {
27425
- await rename2(temporaryPath, outputPath);
27585
+ await rename3(temporaryPath, outputPath);
27426
27586
  } else {
27427
27587
  await link(temporaryPath, outputPath);
27428
27588
  await unlink(temporaryPath);
@@ -27438,7 +27598,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
27438
27598
  if (temporaryPath !== void 0) await unlink(temporaryPath);
27439
27599
  } catch {
27440
27600
  }
27441
- await rm3(stagingDirectory, { recursive: true, force: true });
27601
+ await rm4(stagingDirectory, { recursive: true, force: true });
27442
27602
  await directoryHandle.close();
27443
27603
  }
27444
27604
  return join6(outputDirectory, safeName);
@@ -27786,8 +27946,8 @@ init_formatter();
27786
27946
  init_errors();
27787
27947
  init_constants();
27788
27948
  import { randomBytes as randomBytes5 } from "crypto";
27789
- import { mkdir as mkdir4, writeFile as writeFile6 } from "fs/promises";
27790
- import { dirname as dirname3 } from "path";
27949
+ import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
27950
+ import { dirname as dirname4 } from "path";
27791
27951
  init_host();
27792
27952
  init_theme();
27793
27953
  function didWithoutFragment2(did) {
@@ -27914,8 +28074,8 @@ it directly with \`tc space host <name>\` (no request needed).
27914
28074
  }
27915
28075
  async function emitHostRequestArtifact(artifact, emitOption) {
27916
28076
  if (typeof emitOption === "string" && emitOption.length > 0) {
27917
- await mkdir4(dirname3(emitOption), { recursive: true });
27918
- await writeFile6(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
28077
+ await mkdir4(dirname4(emitOption), { recursive: true });
28078
+ await writeFile5(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
27919
28079
  outputJson({
27920
28080
  emitted: true,
27921
28081
  path: emitOption,
@@ -27934,7 +28094,7 @@ init_profiles();
27934
28094
  init_formatter();
27935
28095
  init_errors();
27936
28096
  init_constants();
27937
- import { writeFile as writeFile7 } from "fs/promises";
28097
+ import { writeFile as writeFile6 } from "fs/promises";
27938
28098
  import { resolve as resolve3 } from "path";
27939
28099
  init_space();
27940
28100
  init_host();
@@ -28079,7 +28239,7 @@ Output:
28079
28239
  const blob = result.data;
28080
28240
  const buffer = Buffer.from(await blob.arrayBuffer());
28081
28241
  const outputPath = resolve3(options.output);
28082
- await writeFile7(outputPath, buffer);
28242
+ await writeFile6(outputPath, buffer);
28083
28243
  outputJson({
28084
28244
  file: outputPath,
28085
28245
  size: blob.size,
@@ -28630,7 +28790,7 @@ init_formatter();
28630
28790
  init_errors();
28631
28791
  init_constants();
28632
28792
  import { readFile as readFile10 } from "fs/promises";
28633
- import { writeFile as writeFile8 } from "fs/promises";
28793
+ import { writeFile as writeFile7 } from "fs/promises";
28634
28794
  import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
28635
28795
  async function readStdin4() {
28636
28796
  const chunks = [];
@@ -28719,7 +28879,7 @@ function registerVaultCommand(program) {
28719
28879
  const data = result.data.data ?? result.data;
28720
28880
  if (options.output) {
28721
28881
  const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
28722
- await writeFile8(options.output, content);
28882
+ await writeFile7(options.output, content);
28723
28883
  outputJson({ key, written: options.output });
28724
28884
  return;
28725
28885
  }
@@ -28807,7 +28967,7 @@ init_formatter();
28807
28967
  init_errors();
28808
28968
  init_constants();
28809
28969
  import { readFile as readFile11 } from "fs/promises";
28810
- import { writeFile as writeFile9 } from "fs/promises";
28970
+ import { writeFile as writeFile8 } from "fs/promises";
28811
28971
  var VARIABLES_PREFIX = "variables/";
28812
28972
  async function readStdin5() {
28813
28973
  const chunks = [];
@@ -28879,7 +29039,7 @@ function registerVarsCommand(program) {
28879
29039
  value = typeof data === "string" ? data : JSON.stringify(data);
28880
29040
  }
28881
29041
  if (options.output) {
28882
- await writeFile9(options.output, value);
29042
+ await writeFile8(options.output, value);
28883
29043
  outputJson({ name, written: options.output });
28884
29044
  return;
28885
29045
  }