@tinycloud/cli 1.3.0-beta.52 → 1.3.0-beta.54

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.
@@ -2734,11 +2734,11 @@ async function getLastPermissionRequestArtifact(profile) {
2734
2734
  return existing.at(-1) ?? null;
2735
2735
  }
2736
2736
  async function replayAdditionalDelegations(node, profile, options) {
2737
- const { operationSpaceResolver: operationSpaceResolver2, replayProfileDelegations } = await import("@tinycloud/operations/delegation-binding");
2737
+ const { operationSpaceResolver: operationSpaceResolver3, replayProfileDelegations } = await import("@tinycloud/operations/delegation-binding");
2738
2738
  const { warnings } = await replayProfileDelegations(profile, node, {
2739
2739
  host: options.host,
2740
2740
  migrate: options.migrate,
2741
- resolveSpace: operationSpaceResolver2(node, options.ownerSpace)
2741
+ resolveSpace: operationSpaceResolver3(node, options.ownerSpace)
2742
2742
  });
2743
2743
  return warnings;
2744
2744
  }
@@ -11083,15 +11083,15 @@ var makeIssue = (params) => {
11083
11083
  message: issueData.message
11084
11084
  };
11085
11085
  }
11086
- let errorMessage2 = "";
11086
+ let errorMessage3 = "";
11087
11087
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
11088
11088
  for (const map of maps) {
11089
- errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
11089
+ errorMessage3 = map(fullIssue, { data, defaultError: errorMessage3 }).message;
11090
11090
  }
11091
11091
  return {
11092
11092
  ...issueData,
11093
11093
  path: fullPath,
11094
- message: errorMessage2
11094
+ message: errorMessage3
11095
11095
  };
11096
11096
  };
11097
11097
  var EMPTY_PATH = [];
@@ -16612,15 +16612,15 @@ var makeIssue2 = (params) => {
16612
16612
  message: issueData.message
16613
16613
  };
16614
16614
  }
16615
- let errorMessage2 = "";
16615
+ let errorMessage3 = "";
16616
16616
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
16617
16617
  for (const map of maps) {
16618
- errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
16618
+ errorMessage3 = map(fullIssue, { data, defaultError: errorMessage3 }).message;
16619
16619
  }
16620
16620
  return {
16621
16621
  ...issueData,
16622
16622
  path: fullPath,
16623
- message: errorMessage2
16623
+ message: errorMessage3
16624
16624
  };
16625
16625
  };
16626
16626
  var EMPTY_PATH2 = [];
@@ -23658,15 +23658,15 @@ var makeIssue3 = (params) => {
23658
23658
  message: issueData.message
23659
23659
  };
23660
23660
  }
23661
- let errorMessage2 = "";
23661
+ let errorMessage3 = "";
23662
23662
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
23663
23663
  for (const map of maps) {
23664
- errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
23664
+ errorMessage3 = map(fullIssue, { data, defaultError: errorMessage3 }).message;
23665
23665
  }
23666
23666
  return {
23667
23667
  ...issueData,
23668
23668
  path: fullPath,
23669
- message: errorMessage2
23669
+ message: errorMessage3
23670
23670
  };
23671
23671
  };
23672
23672
  var EMPTY_PATH3 = [];
@@ -29616,6 +29616,9 @@ function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
29616
29616
  if (!scopeCovers(requested, approved, signed.ownerDid)) {
29617
29617
  throw new CLIError("OPENKEY_GRANT_BROADENED", `The signed grant contains authority beyond the requested ${expected.purpose === "grant" ? "grant" : "manifest"}. ${notStored}`, ExitCode.PERMISSION_DENIED);
29618
29618
  }
29619
+ if (expected.exactApproval && !scopeCovers(approved, requested, signed.ownerDid)) {
29620
+ throw new CLIError("OPENKEY_APPROVAL_NARROWER", `The approval was narrower than the request. Approve all of it, or ask the agent to request less. ${notStored}`, ExitCode.PERMISSION_DENIED);
29621
+ }
29619
29622
  const session = withVerifiedAuthority({ ...data, jwk: key }, { ...signed, permissions: approved });
29620
29623
  return { session, legacyNested };
29621
29624
  }
@@ -31156,7 +31159,7 @@ var APPS_AGENT_VARIANTS = {
31156
31159
  { service: "tinycloud.sql", actions: ["tinycloud.sql/read"], within: [""] }
31157
31160
  ],
31158
31161
  perAppOnly: false,
31159
- changesStructure: false
31162
+ schemaForCatalogApps: false
31160
31163
  },
31161
31164
  write: {
31162
31165
  grants: [
@@ -31165,9 +31168,13 @@ var APPS_AGENT_VARIANTS = {
31165
31168
  { service: "tinycloud.sql", actions: ["tinycloud.sql/read", "tinycloud.sql/write"], within: [""] }
31166
31169
  ],
31167
31170
  perAppOnly: true,
31168
- changesStructure: true
31171
+ schemaForCatalogApps: true
31169
31172
  }
31170
31173
  };
31174
+ var APPS_AGENT_SCHEMA_DATABASES = {
31175
+ "xyz.tinycloud.sample.healthlog": "healthlog"
31176
+ };
31177
+ var SCHEMA_ACTION = "tinycloud.sql/schema";
31171
31178
  var ACTION_WORDS = {
31172
31179
  "tinycloud.kv/get": "read files",
31173
31180
  "tinycloud.kv/list": "list files",
@@ -31185,12 +31192,11 @@ var ACTION_WORDS = {
31185
31192
  "tinycloud.sql/*": "do anything in the database",
31186
31193
  "tinycloud.capabilities/read": "see which permissions this key holds"
31187
31194
  };
31188
- var STRUCTURE_ACTIONS = /* @__PURE__ */ new Set([
31189
- "tinycloud.sql/write",
31190
- "tinycloud.sql/schema",
31191
- "tinycloud.sql/admin",
31192
- "tinycloud.sql/*"
31193
- ]);
31195
+ var STRUCTURE_ACTIONS = {
31196
+ "tinycloud.sql/schema": true,
31197
+ "tinycloud.sql/admin": true,
31198
+ "tinycloud.sql/*": true
31199
+ };
31194
31200
  var APP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
31195
31201
  var APP_ID_RULE = "an app id looks like xyz.example.app: letters, digits, dots, hyphens and underscores, no slashes or spaces";
31196
31202
  function isAppId(value) {
@@ -31198,7 +31204,15 @@ function isAppId(value) {
31198
31204
  }
31199
31205
  function appsAgentPermissions(mode, appIds = []) {
31200
31206
  const bases = appIds.length === 0 ? [""] : appIds.map((appId) => `${appId}/`);
31201
- return APPS_AGENT_VARIANTS[mode].grants.flatMap(({ service, actions, within }) => within.flatMap((folder) => bases.map((base4) => ({ service, space: APPS_AGENT_SPACE, path: `${base4}${folder}`, actions: [...actions] }))));
31207
+ const grants = APPS_AGENT_VARIANTS[mode].grants.flatMap(({ service, actions, within }) => within.flatMap((folder) => bases.map((base4) => ({ service, space: APPS_AGENT_SPACE, path: `${base4}${folder}`, actions: [...actions] }))));
31208
+ const schema = APPS_AGENT_VARIANTS[mode].schemaForCatalogApps ? schemaPathsFor(appIds).map((path) => ({ service: "tinycloud.sql", space: APPS_AGENT_SPACE, path, actions: [SCHEMA_ACTION] })) : [];
31209
+ return [...grants, ...schema];
31210
+ }
31211
+ function schemaPathsFor(appIds) {
31212
+ return appIds.flatMap((appId) => {
31213
+ if (!Object.hasOwn(APPS_AGENT_SCHEMA_DATABASES, appId)) return [];
31214
+ return [`${appId}/${APPS_AGENT_SCHEMA_DATABASES[appId]}`];
31215
+ });
31202
31216
  }
31203
31217
  function quoteApps(appIds) {
31204
31218
  return appIds.map((appId) => `\`${appId}\``).join(", ");
@@ -31221,12 +31235,14 @@ function appsAgentScopeNotice(mode, appIds = []) {
31221
31235
  const reads = writes ? `read everything there, write knowledge and data files (${writableFolders}), and add, change and delete table rows` : "list and read its files and read its table rows";
31222
31236
  const scope = appIds.length === 0 ? `ALL your installed apps' data in the \`${APPS_AGENT_SPACE}\` space` : `the data of ${quoteApps(appIds)} (everything under ${appIds.map((appId) => `\`${appId}/\``).join(", ")}) in the \`${APPS_AGENT_SPACE}\` space`;
31223
31237
  const lines = [
31224
- variant.changesStructure ? `This gives the agent access to ${scope}: it can ${reads}.` : `This gives the agent READ-ONLY access to ${scope}: it can ${reads}.`
31238
+ writes ? `This gives the agent access to ${scope}: it can ${reads}.` : `This gives the agent READ-ONLY access to ${scope}: it can ${reads}.`
31225
31239
  ];
31226
- if (variant.changesStructure) {
31240
+ if (writes) {
31241
+ const schemaPaths = schemaPathsFor(appIds).map((path) => `\`${path}\``);
31227
31242
  lines.push(
31228
31243
  `It cannot change ${appIds.length === 1 ? "the app's" : "the apps'"} code, manifest or install record: files outside those folders are read-only for it.`,
31229
- `SQL write is separate from that: it can change ${appIds.length === 1 ? "this app's" : "these apps'"} table rows and, because of how TinyCloud permissions work today (TC-1581), also change or delete ${appIds.length === 1 ? "its" : "their"} tables (structure).`,
31244
+ `SQL write is separate from that: it can add, change and delete ${appIds.length === 1 ? "this app's" : "these apps'"} table rows.`,
31245
+ variant.schemaForCatalogApps && schemaPaths.length > 0 ? `It can also create, change and drop the tables in ${schemaPaths.join(", ")}, which the app needs to set itself up. That grant names no other database for table changes. Nodes before 1.21.6 do not enforce that database limit, so the write grant may still let it change tables elsewhere there.` : "This grant does not include permission to create, change or drop tables (tinycloud.sql/schema). Nodes before 1.21.6 do not enforce that split, so the write grant may still let it do so there.",
31230
31246
  "It cannot touch other apps, other spaces or secrets."
31231
31247
  );
31232
31248
  } else {
@@ -31379,12 +31395,13 @@ function describeEntry(entry, applicationsSpace) {
31379
31395
  function withinPresetSet(entry, applicationsSpace) {
31380
31396
  if (!inApplicationsSpace(entry, applicationsSpace) || entry.caveats !== void 0) return false;
31381
31397
  if (entry.path.split("/").some((segment) => segment === "." || segment === "..")) return false;
31398
+ if (entry.actions.includes(SCHEMA_ACTION)) return false;
31382
31399
  const slash = entry.path.indexOf("/");
31383
31400
  const folder = slash > 0 && isAppId(entry.path.slice(0, slash)) ? entry.path.slice(slash + 1) : void 0;
31384
31401
  const grants = Object.values(APPS_AGENT_VARIANTS).flatMap(({ grants: variantGrants }) => variantGrants.filter((grant) => grant.service === entry.service));
31385
31402
  return entry.actions.length > 0 && entry.actions.every((action) => grants.some((grant) => grant.actions.includes(action) && (!(action.endsWith("/put") || action.endsWith("/write")) || folder !== void 0 && grant.within.some((within) => folder.startsWith(within)))));
31386
31403
  }
31387
- function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
31404
+ function describeGrantScope(requested, requestedExpiry, applicationsSpace, maxLifetimeMs) {
31388
31405
  const preset = matchAppsAgentRequest(requested, requestedExpiry, applicationsSpace);
31389
31406
  if (preset !== void 0) {
31390
31407
  return { text: appsAgentScopeNotice(preset.mode, preset.appIds), entries: requested, extras: [] };
@@ -31397,11 +31414,11 @@ function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
31397
31414
  ...extras.map((entry) => `! ${describeEntry(entry, applicationsSpace)}`),
31398
31415
  ...others.map((entry) => `- ${describeEntry(entry, applicationsSpace)}`)
31399
31416
  ];
31400
- if (requested.some((entry) => entry.service === "tinycloud.sql" && entry.actions.some((action) => STRUCTURE_ACTIONS.has(action)))) {
31401
- lines.push("Because of how TinyCloud permissions work today (TC-1581), SQL write access can also change or delete tables (structure).");
31417
+ if (requested.some((entry) => entry.service === "tinycloud.sql" && entry.actions.some((action) => Object.hasOwn(STRUCTURE_ACTIONS, action)))) {
31418
+ lines.push("This request can create, change and drop tables (an SQL schema, admin or wildcard entry).");
31402
31419
  }
31403
31420
  const entryLifetimes = requested.filter((entry) => entry.expiry !== void 0).map((entry) => lifetimeMs(entry.expiry));
31404
- const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0);
31421
+ const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0).map((ms2) => Math.min(ms2, maxLifetimeMs ?? Number.POSITIVE_INFINITY));
31405
31422
  const longest = Math.max(...durations);
31406
31423
  lines.push(
31407
31424
  durations.length === 0 || !Number.isFinite(longest) ? "Its lifetime could not be read from the request." : `It lasts up to ${lifetimePhrase(longest)}${requestedExpiry === void 0 ? " (no lifetime was requested; this is the default)" : ""}; revoke it any time with \`${CLI_NAME16} delegation revoke <id>\`.`
@@ -32181,7 +32198,7 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
32181
32198
  `With --preset ${APPS_AGENT_PRESET}: limit the grant to this app's <app_id>/ prefix (recommended); repeat for up to ${APPS_AGENT_MAX_APPS} apps`,
32182
32199
  (value, previous) => [...previous, value],
32183
32200
  []
32184
- ).option("--write", `With --preset ${APPS_AGENT_PRESET}: also let the agent add and change the named --app apps' data: write their knowledge/ and prefs/ files (kv put) and change their tables (sql write). Their code, manifest and install record stay read-only. SQL write can also change or drop those apps' tables`).option("--paste", "With --grant on an OpenKey profile: wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (use when the browser cannot reach this CLI)").action(async (options2, cmd) => {
32201
+ ).option("--write", `With --preset ${APPS_AGENT_PRESET}: also let the agent add and change the named --app apps' data: write their knowledge/ and prefs/ files (kv put) and add, change or delete their table rows (sql write). Their code, manifest and install record stay read-only. Creating, changing or dropping tables is granted only for an app's catalog database (today Health Log's healthlog)`).option("--paste", "With --grant on an OpenKey profile: wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (use when the browser cannot reach this CLI)").action(async (options2, cmd) => {
32185
32202
  try {
32186
32203
  const globalOpts = cmd.optsWithGlobals();
32187
32204
  const ctx = await ProfileManager.resolveContext(globalOpts, options2.grant ? {} : { storedSession: "unused" });
@@ -32511,7 +32528,12 @@ ${appsAgentScopeNotice(preset.mode, preset.appIds)}
32511
32528
  [{ service: "tinycloud.kv", space: APPS_AGENT_SPACE, path: "", actions: ["tinycloud.kv/get"] }],
32512
32529
  ctx.profile
32513
32530
  );
32514
- const scope = describeGrantScope(requested, parsed.requestedExpiry, applications.space ?? APPS_AGENT_SPACE);
32531
+ const scope = describeGrantScope(
32532
+ requested,
32533
+ parsed.requestedExpiry,
32534
+ applications.space ?? APPS_AGENT_SPACE,
32535
+ profile.authMethod === "openkey" ? OPENKEY_AGENT_MAX_LIFETIME_MS : void 0
32536
+ );
32515
32537
  process.stderr.write(`
32516
32538
  ${scope.text}
32517
32539
 
@@ -32530,16 +32552,43 @@ ${scope.text}
32530
32552
  }
32531
32553
  const resolvedRequest = { ...parsed, requested };
32532
32554
  const node = await ensureAuthenticated(ctx);
32533
- await ensureDelegationAuthority({
32534
- ctx,
32535
- profile,
32536
- node,
32537
- requested,
32538
- expiryOption: parsed.requestedExpiry,
32539
- reason: "Grant permissions requested by a TinyCloud auth request artifact.",
32540
- yes: options2.yes === true || confirmedExtras
32541
- });
32542
- const grant = await grantAuthRequest(node, resolvedRequest);
32555
+ let grant;
32556
+ if (profile.authMethod === "openkey") {
32557
+ assertOpenKeyAgentRequest(requested);
32558
+ const approval = await acquireOpenKeyApproval({
32559
+ ctx,
32560
+ profile,
32561
+ requested,
32562
+ expiryOption: Math.min(
32563
+ parseRequestedExpiry(parsed.requestedExpiry ?? APPS_AGENT_EXPIRY).durationMs,
32564
+ OPENKEY_AGENT_MAX_LIFETIME_MS
32565
+ ),
32566
+ reason: "Grant permissions requested by a TinyCloud auth request artifact.",
32567
+ exactApproval: true,
32568
+ noPopup: !(process.stdin.isTTY || process.stderr.isTTY)
32569
+ });
32570
+ const approvalNode = await nodeFromOpenKeyApproval(ctx.host, approval.session);
32571
+ grant = {
32572
+ ...await grantAuthRequest(approvalNode, resolvedRequest, {
32573
+ expiry: approval.delegation.expiry.getTime() - Date.now()
32574
+ }),
32575
+ // The owner approved this grant on the OpenKey approval screen, so it was prompted.
32576
+ prompted: true
32577
+ };
32578
+ process.stderr.write(`Grant lasts until ${grant.expiry}, the lifetime of the OpenKey approval.
32579
+ `);
32580
+ } else {
32581
+ await ensureDelegationAuthority({
32582
+ ctx,
32583
+ profile,
32584
+ node,
32585
+ requested,
32586
+ expiryOption: parsed.requestedExpiry,
32587
+ reason: "Grant permissions requested by a TinyCloud auth request artifact.",
32588
+ yes: options2.yes === true || confirmedExtras
32589
+ });
32590
+ grant = await grantAuthRequest(node, resolvedRequest);
32591
+ }
32543
32592
  await appendLocalGrantArtifact(ctx.profile, grant.delegation);
32544
32593
  await appendGrantHistory(ctx.profile, {
32545
32594
  addedCaps: grant.permissions,
@@ -32919,12 +32968,72 @@ async function activateAndStoreOpenKeyGrants(profileName2, host, node, grants, s
32919
32968
  await appendAdditionalDelegations(profileName2, records);
32920
32969
  });
32921
32970
  }
32971
+ async function acquireOpenKeyApproval(params) {
32972
+ if (params.requested.some(isRawCidRevocationPermission)) {
32973
+ throw new CLIError(
32974
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
32975
+ "OpenKey cannot sign raw urn:cid revocation authority.",
32976
+ ExitCode.PERMISSION_DENIED
32977
+ );
32978
+ }
32979
+ const key = await ProfileManager.getKey(params.ctx.profile);
32980
+ if (!key) {
32981
+ throw new CLIError(
32982
+ "NO_KEY",
32983
+ `No key found for profile "${params.ctx.profile}". Run \`${CLI_NAME17} --profile ${params.ctx.profile} auth rotate\` to create a new key and sign in.`,
32984
+ ExitCode.AUTH_REQUIRED
32985
+ );
32986
+ }
32987
+ const expiryCap = params.expiryOption === void 0 ? void 0 : parseRequestedExpiry(params.expiryOption);
32988
+ const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
32989
+ const request = grantRequestPermissions(params.requested, anchorSpace);
32990
+ const acquireOpenKey = params.openKeyAcquisition ?? startAuthFlow;
32991
+ const delegationData = await acquireOpenKey(params.profile.did, {
32992
+ jwk: key,
32993
+ host: params.ctx.host,
32994
+ permissions: request,
32995
+ reason: permissionGrantReason(params.reason, request),
32996
+ openkeyHost: resolveOpenKeyHost(params.profile),
32997
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
32998
+ noPopup: params.noPopup
32999
+ });
33000
+ return verifyOpenKeyApproval(delegationData, request, params.ctx.host, {
33001
+ key,
33002
+ sessionDid: params.profile.sessionDid ?? params.profile.did,
33003
+ expectedOwner: pinnedOwner(params.profile),
33004
+ expiry: expiryCap,
33005
+ exactApproval: params.exactApproval === true
33006
+ });
33007
+ }
33008
+ async function nodeFromOpenKeyApproval(host, session) {
33009
+ const ownerParts = session.ownerDid.split(":");
33010
+ const node = new TinyCloudNode3({ host });
33011
+ await node.restoreSession({
33012
+ delegationHeader: session.delegationHeader,
33013
+ delegationCid: session.delegationCid,
33014
+ spaceId: session.spaceId,
33015
+ jwk: session.jwk,
33016
+ verificationMethod: session.verificationMethod,
33017
+ address: ownerParts[4],
33018
+ chainId: Number(ownerParts[3]),
33019
+ siwe: session.siwe,
33020
+ signature: session.signature,
33021
+ expiresAt: session.expiresAt,
33022
+ tinycloudHosts: [host]
33023
+ });
33024
+ return node;
33025
+ }
32922
33026
  async function ensureDelegationAuthority(params) {
32923
33027
  if (!params.force && params.node.hasRuntimePermissions(params.requested)) return void 0;
32924
33028
  if (!params.consentAlreadyGiven && (params.persist === false || params.profile.authMethod !== "openkey")) {
32925
33029
  await requirePermissionConsent(params.requested, params.yes);
32926
33030
  }
32927
33031
  if (params.profile.authMethod === "openkey") {
33032
+ if (params.persist === false) {
33033
+ const { delegation } = await acquireOpenKeyApproval(params);
33034
+ await params.node.useRuntimeDelegation(delegation);
33035
+ return delegation;
33036
+ }
32928
33037
  if (params.requested.some(isRawCidRevocationPermission)) {
32929
33038
  throw new CLIError(
32930
33039
  "RAW_RECAP_RESOURCE_UNSUPPORTED",
@@ -32947,28 +33056,10 @@ async function ensureDelegationAuthority(params) {
32947
33056
  key,
32948
33057
  sessionDid: params.profile.sessionDid ?? params.profile.did,
32949
33058
  expectedOwner: pinnedOwner(params.profile),
32950
- expiry: expiryCap
33059
+ expiry: expiryCap,
33060
+ exactApproval: params.exactApproval === true
32951
33061
  };
32952
33062
  const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
32953
- if (params.persist === false) {
32954
- const request = grantRequestPermissions(params.requested, anchorSpace);
32955
- const delegationData = await acquireOpenKey(params.profile.did, {
32956
- jwk: key,
32957
- host: params.ctx.host,
32958
- permissions: request,
32959
- reason: permissionGrantReason(params.reason, request),
32960
- openkeyHost,
32961
- expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
32962
- });
32963
- const delegation = portableFromOpenKeyDelegation(
32964
- delegationData,
32965
- request,
32966
- params.ctx.host,
32967
- proof
32968
- );
32969
- await params.node.useRuntimeDelegation(delegation);
32970
- return delegation;
32971
- }
32972
33063
  const grants = [];
32973
33064
  for (const group of groupPermissionsBySpace(params.requested)) {
32974
33065
  const request = grantRequestPermissions(group, anchorSpace);
@@ -32978,7 +33069,8 @@ async function ensureDelegationAuthority(params) {
32978
33069
  permissions: request,
32979
33070
  reason: permissionGrantReason(params.reason, group),
32980
33071
  openkeyHost,
32981
- expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
33072
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
33073
+ noPopup: params.noPopup
32982
33074
  });
32983
33075
  const delegation = portableFromOpenKeyDelegation(delegationData, request, params.ctx.host, proof);
32984
33076
  grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
@@ -33005,6 +33097,23 @@ async function ensureDelegationAuthority(params) {
33005
33097
  });
33006
33098
  }
33007
33099
  }
33100
+ var OPENKEY_AGENT_MAX_LIFETIME_MS = parseRequestedExpiry(APPS_AGENT_EXPIRY).durationMs;
33101
+ function assertOpenKeyAgentRequest(requested) {
33102
+ if (requested.some((permission) => isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission))) {
33103
+ throw new CLIError(
33104
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
33105
+ "OpenKey cannot approve raw urn:cid or raw encryption authority for an agent request.",
33106
+ ExitCode.PERMISSION_DENIED
33107
+ );
33108
+ }
33109
+ if (groupPermissionsBySpace(requested).length !== 1) {
33110
+ throw new CLIError(
33111
+ "AGENT_GRANT_MULTIPLE_SPACES",
33112
+ "An agent grant must cover exactly one space. Request one space per auth request.",
33113
+ ExitCode.PERMISSION_DENIED
33114
+ );
33115
+ }
33116
+ }
33008
33117
  function permissionGrantReason(context, permissions) {
33009
33118
  const first = permissions[0];
33010
33119
  const summary = first ? compactPermission(first) : "no permissions";
@@ -33127,10 +33236,14 @@ function groupPermissionsBySpace(permissions) {
33127
33236
  return grouped;
33128
33237
  }
33129
33238
  function portableFromOpenKeyDelegation(data, requested, host, proof) {
33239
+ return verifyOpenKeyApproval(data, requested, host, proof).delegation;
33240
+ }
33241
+ function verifyOpenKeyApproval(data, requested, host, proof) {
33130
33242
  const { session, legacyNested } = verifyScopedLogin(data, proof.key, proof.sessionDid, requested, {
33131
33243
  expectedOwner: proof.expectedOwner,
33132
33244
  expiry: proof.expiry,
33133
- purpose: "grant"
33245
+ purpose: "grant",
33246
+ exactApproval: proof.exactApproval
33134
33247
  });
33135
33248
  if (legacyNested.length > 0) {
33136
33249
  throw new CLIError(
@@ -33153,7 +33266,7 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
33153
33266
  ...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
33154
33267
  }));
33155
33268
  const ownerParts = session.ownerDid.split(":");
33156
- return {
33269
+ const delegation = {
33157
33270
  cid: data.delegationCid,
33158
33271
  delegationHeader: data.delegationHeader,
33159
33272
  spaceId: returnedSpace,
@@ -33168,6 +33281,7 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
33168
33281
  // Verified above; replay rebuilds the CACAO from it before trusting the grant.
33169
33282
  siweProof: { siwe: data.siwe, signature: data.signature }
33170
33283
  };
33284
+ return { delegation, session };
33171
33285
  }
33172
33286
  async function rotateAuthKey(profileName2, host, options = {}) {
33173
33287
  await recoverPendingLogout(profileName2);
@@ -34312,12 +34426,369 @@ function registerEnableCommand(program) {
34312
34426
  });
34313
34427
  }
34314
34428
 
34429
+ // src/commands/apps.ts
34430
+ init_profiles();
34431
+ init_constants();
34432
+ init_sdk();
34433
+ import { CLI_NAME as CLI_NAME23 } from "@tinycloud/operations/cli-name";
34434
+ import { operationSpaceResolver as operationSpaceResolver2 } from "@tinycloud/operations/delegation-binding";
34435
+
34436
+ // src/lib/knowledge-input.ts
34437
+ init_constants();
34438
+ init_errors();
34439
+ init_safe_text();
34440
+ function usage2(message) {
34441
+ return new CLIError("INVALID_ARGUMENT", message, ExitCode.USAGE_ERROR);
34442
+ }
34443
+ function assertKnowledgeAppId(value) {
34444
+ if (!isAppId(value)) throw usage2(`Invalid app id "${safeClipped(value)}": ${APP_ID_RULE}.`);
34445
+ return value;
34446
+ }
34447
+ function assertKnowledgeConcept(value) {
34448
+ const reason = value.length > 300 ? "it is longer than 300 characters" : value.startsWith("/") ? "it starts with a slash" : /[*\\]/.test(value) ? "it contains a wildcard or backslash" : /%(?:2e|2f|5c)/i.test(value) ? "it contains an encoded dot, slash or backslash" : value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") ? "it has an empty, `.` or `..` segment" : void 0;
34449
+ if (reason !== void 0) {
34450
+ throw usage2(
34451
+ `Invalid concept "${safeClipped(value)}": ${reason}. Use a path inside the app's knowledge folder, such as app or tables/meal.md.`
34452
+ );
34453
+ }
34454
+ return value;
34455
+ }
34456
+
34457
+ // src/lib/apps-install-pin.ts
34458
+ var SHARE_ORIGIN = "https://share.tinycloud.xyz";
34459
+ var HEALTHLOG_ID = "xyz.tinycloud.sample.healthlog";
34460
+ var PINNED = {
34461
+ [HEALTHLOG_ID]: {
34462
+ appId: HEALTHLOG_ID,
34463
+ database: `${HEALTHLOG_ID}/healthlog`,
34464
+ bundleDigest: "dc3ec19fffce8fee350944469948531659a258347538a98aacdd5544deea5014",
34465
+ files: [
34466
+ { path: "schema.sql", sha256: "bb46ab35ac7887f672beba2f50b9f982c31d41607b3fa77ac9c2e72003edf199", size: 4105 },
34467
+ { path: "knowledge/app.md", sha256: "74a65d0332ba06fe5f7fb47c67243ebc6ca80435f3388cb5201712287240f4c7", size: 3994 },
34468
+ { path: "knowledge/kv/app.md", sha256: "1583fdefdf6261a8ce110d8de22843855f8393f760de9fa79c3491868924bd42", size: 1283 },
34469
+ { path: "knowledge/kv/knowledge.md", sha256: "1f3fb9d50e0ccea10dbe4a223efdbbde2b277bbed8f4790251331c5a7dcebc0f", size: 1170 },
34470
+ { path: "knowledge/kv/prefs.md", sha256: "0de88eb2cd8c2ad7302b84c2f6b972364d9e9354f15b70657349339167dd52e4", size: 831 },
34471
+ { path: "knowledge/log.md", sha256: "7753435d03afe51c36023c596330c7f04c597319586b5b39d4cf8a675512b1d0", size: 263 },
34472
+ { path: "knowledge/tables/meal.md", sha256: "1bc09271c8c2cf987ef81dfb99c4427234fdfbc795f5229a1d631aef0fbff795", size: 1921 },
34473
+ { path: "knowledge/tables/weight.md", sha256: "fbae8b103931f976b838024d3ec7ea2596f1b1af3e9834e8aaba1d216d227bc0", size: 1401 },
34474
+ { path: "knowledge/views/day_card.md", sha256: "f1ea0738b6905a4952ab1f9c269310f2f888c6606b16304c12bdf69dfb3fafbe", size: 1707 },
34475
+ { path: "knowledge/views/week_rollup.md", sha256: "b9773f2ae10fadb9b0ce5f5a78885c87f90bd202a00f7f157428911239c2077e", size: 1579 },
34476
+ { path: "knowledge/index.md", sha256: "06e54fa6a4f14ae13bdbcf2ad5ab22b6d561da90e04c27fc616787762e45811e", size: 945 }
34477
+ ]
34478
+ }
34479
+ };
34480
+ function pinnedApp(appId) {
34481
+ return Object.hasOwn(PINNED, appId) ? PINNED[appId] : void 0;
34482
+ }
34483
+
34484
+ // src/lib/apps-install.ts
34485
+ import { createHash as createHash3 } from "crypto";
34486
+ function errorMessage2(error) {
34487
+ return error instanceof Error ? error.message : String(error);
34488
+ }
34489
+ var MAX_FILE_BYTES = 1e6;
34490
+ var AppsInstallError = class extends Error {
34491
+ constructor(code4, message) {
34492
+ super(message);
34493
+ this.code = code4;
34494
+ this.name = "AppsInstallError";
34495
+ }
34496
+ };
34497
+ function sha256Hex2(bytes) {
34498
+ return createHash3("sha256").update(bytes).digest("hex");
34499
+ }
34500
+ function bytesEqual(a, b) {
34501
+ return a.length === b.length && Buffer.from(a).equals(Buffer.from(b));
34502
+ }
34503
+ async function fetchVerified(app, io) {
34504
+ const verified = /* @__PURE__ */ new Map();
34505
+ for (const file of app.files) {
34506
+ const url = `${SHARE_ORIGIN}/apps/b/${app.bundleDigest}/${file.path}`;
34507
+ let bytes;
34508
+ try {
34509
+ bytes = await io.fetchBytes(url);
34510
+ } catch (error) {
34511
+ throw new AppsInstallError("FETCH_FAILED", `Could not fetch ${file.path}: ${errorMessage2(error)}`);
34512
+ }
34513
+ verifyBytes(file, bytes);
34514
+ verified.set(file.path, bytes);
34515
+ }
34516
+ return verified;
34517
+ }
34518
+ function verifyBytes(file, bytes) {
34519
+ if (bytes.length > MAX_FILE_BYTES) {
34520
+ throw new AppsInstallError("INTEGRITY_FAILED", `${file.path} is ${bytes.length} bytes, above the ${MAX_FILE_BYTES} byte limit`);
34521
+ }
34522
+ if (bytes.length !== file.size || sha256Hex2(bytes) !== file.sha256) {
34523
+ throw new AppsInstallError("INTEGRITY_FAILED", `${file.path} does not match the pinned sha256 and size`);
34524
+ }
34525
+ }
34526
+ function splitStatements(sql) {
34527
+ let stripped = "";
34528
+ let inQuote = false;
34529
+ for (let i = 0; i < sql.length; i++) {
34530
+ const ch = sql[i];
34531
+ if (ch === "'") inQuote = !inQuote;
34532
+ if (!inQuote && ch === "-" && sql[i + 1] === "-") {
34533
+ while (i < sql.length && sql[i] !== "\n") i++;
34534
+ stripped += "\n";
34535
+ continue;
34536
+ }
34537
+ stripped += ch;
34538
+ }
34539
+ return stripped.split(";").map((statement) => statement.replace(/\s+/g, " ").trim()).filter((statement) => statement.length > 0);
34540
+ }
34541
+ var TABLE_FORM = /^CREATE TABLE IF NOT EXISTS ([A-Za-z_][A-Za-z0-9_]*) \(.*\)$/i;
34542
+ var INDEX_FORM = /^CREATE INDEX IF NOT EXISTS ([A-Za-z_][A-Za-z0-9_]*) ON [A-Za-z_][A-Za-z0-9_]*\([A-Za-z_][A-Za-z0-9_, ]*\)$/i;
34543
+ var VIEW_FORM = /^CREATE VIEW IF NOT EXISTS ([A-Za-z_][A-Za-z0-9_]*) AS SELECT .+$/i;
34544
+ function planDdl(schemaSql) {
34545
+ const tables = [];
34546
+ const views = [];
34547
+ const statements = [];
34548
+ for (const statement of splitStatements(schemaSql)) {
34549
+ const table = TABLE_FORM.exec(statement);
34550
+ const index = INDEX_FORM.exec(statement);
34551
+ const view = VIEW_FORM.exec(statement);
34552
+ if (table) {
34553
+ tables.push(table[1]);
34554
+ statements.push(statement);
34555
+ } else if (index) {
34556
+ statements.push(statement);
34557
+ } else if (view) {
34558
+ views.push(view[1]);
34559
+ statements.push(`DROP VIEW IF EXISTS ${view[1]}`, statement);
34560
+ } else {
34561
+ throw new AppsInstallError("DDL_REFUSED", `Pinned schema statement is not an allowed form: ${statement.slice(0, 80)}`);
34562
+ }
34563
+ }
34564
+ return { tables, views, statements };
34565
+ }
34566
+ async function applySchema(plan, io) {
34567
+ let existing;
34568
+ try {
34569
+ existing = new Set(await io.sqlTableNames());
34570
+ } catch (error) {
34571
+ throw new AppsInstallError("SQL_FAILED", `Could not list tables: ${errorMessage2(error)}`);
34572
+ }
34573
+ for (const statement of plan.statements) {
34574
+ try {
34575
+ await io.sqlExecute(statement);
34576
+ } catch (error) {
34577
+ throw new AppsInstallError("SQL_FAILED", `Schema statement failed: ${errorMessage2(error)}`);
34578
+ }
34579
+ }
34580
+ return {
34581
+ tablesCreated: plan.tables.filter((name) => !existing.has(name)),
34582
+ viewsRecreated: [...plan.views]
34583
+ };
34584
+ }
34585
+ function knowledgePaths(app) {
34586
+ const paths = app.files.map((file) => file.path).filter((path) => path.startsWith("knowledge/"));
34587
+ return [...paths.filter((path) => path !== "knowledge/index.md"), ...paths.filter((path) => path === "knowledge/index.md")];
34588
+ }
34589
+ async function inspectKnowledge(appId, app, verified, io) {
34590
+ const missing = [];
34591
+ const unchanged = [];
34592
+ const conflicts = [];
34593
+ for (const path of knowledgePaths(app)) {
34594
+ const current = await readKv(`${appId}/${path}`, io);
34595
+ if (current === void 0) missing.push(path);
34596
+ else if (bytesEqual(current, verified.get(path))) unchanged.push(path);
34597
+ else conflicts.push(path);
34598
+ }
34599
+ return { missing, unchanged, conflicts };
34600
+ }
34601
+ async function createMissing(appId, missing, verified, io) {
34602
+ const written = [];
34603
+ const unchanged = [];
34604
+ const conflicts = [];
34605
+ for (const path of missing) {
34606
+ const key = `${appId}/${path}`;
34607
+ const bytes = verified.get(path);
34608
+ const outcome = await createKv(key, bytes, io);
34609
+ if (outcome === "created") {
34610
+ written.push(path);
34611
+ continue;
34612
+ }
34613
+ const raced = await readKv(key, io);
34614
+ if (raced !== void 0 && bytesEqual(raced, bytes)) unchanged.push(path);
34615
+ else conflicts.push(path);
34616
+ }
34617
+ return { written, unchanged, conflicts };
34618
+ }
34619
+ async function readKv(key, io) {
34620
+ try {
34621
+ return await io.kvGet(key);
34622
+ } catch (error) {
34623
+ throw new AppsInstallError("KV_FAILED", `Could not read ${key}: ${errorMessage2(error)}`);
34624
+ }
34625
+ }
34626
+ async function checkInstallRecord(appId, app, io) {
34627
+ const key = `${appId}/app/install.json`;
34628
+ const bytes = await readKv(key, io);
34629
+ if (bytes === void 0) return;
34630
+ const digest2 = installRecordDigest(bytes);
34631
+ if (digest2 === void 0) {
34632
+ throw new AppsInstallError("INSTALL_RECORD_INVALID", `${key} is not an install record with a digest`);
34633
+ }
34634
+ if (digest2 !== app.bundleDigest) {
34635
+ throw new AppsInstallError(
34636
+ "INSTALL_RECORD_MISMATCH",
34637
+ `${key} names bundle ${digest2}, but this CLI pins ${app.bundleDigest}; update the CLI before installing`
34638
+ );
34639
+ }
34640
+ }
34641
+ function installRecordDigest(bytes) {
34642
+ let parsed;
34643
+ try {
34644
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
34645
+ } catch {
34646
+ return void 0;
34647
+ }
34648
+ if (typeof parsed !== "object" || parsed === null || !("digest" in parsed)) return void 0;
34649
+ return typeof parsed.digest === "string" ? parsed.digest : void 0;
34650
+ }
34651
+ async function createKv(key, bytes, io) {
34652
+ try {
34653
+ return await io.kvCreate(key, bytes);
34654
+ } catch (error) {
34655
+ throw new AppsInstallError("KV_FAILED", `Could not write ${key}: ${errorMessage2(error)}`);
34656
+ }
34657
+ }
34658
+ async function installApp(appId, io, lookup = pinnedApp) {
34659
+ const app = lookup(appId);
34660
+ if (!app) throw new AppsInstallError("UNKNOWN_APP", `No pinned catalog entry for ${appId}`);
34661
+ const verified = await fetchVerified(app, io);
34662
+ await checkInstallRecord(appId, app, io);
34663
+ const schemaSql = new TextDecoder("utf-8", { fatal: true }).decode(verified.get("schema.sql"));
34664
+ const plan = planDdl(schemaSql);
34665
+ const inspected = await inspectKnowledge(appId, app, verified, io);
34666
+ const created = await createMissing(appId, inspected.missing, verified, io);
34667
+ let schema;
34668
+ try {
34669
+ schema = await applySchema(plan, io);
34670
+ } catch (error) {
34671
+ if (error instanceof AppsInstallError) {
34672
+ throw new AppsInstallError(error.code, `notes saved; tables not set up: run again. ${error.message}`);
34673
+ }
34674
+ throw error;
34675
+ }
34676
+ return {
34677
+ appId,
34678
+ status: created.written.length > 0 ? "installed" : "already",
34679
+ database: app.database,
34680
+ filesWritten: created.written,
34681
+ filesUnchanged: [...inspected.unchanged, ...created.unchanged],
34682
+ conflicts: [...inspected.conflicts, ...created.conflicts],
34683
+ tablesCreated: schema.tablesCreated,
34684
+ viewsRecreated: schema.viewsRecreated
34685
+ };
34686
+ }
34687
+
34688
+ // src/commands/apps.ts
34689
+ init_errors();
34690
+ init_formatter();
34691
+ init_theme();
34692
+ var APPLICATIONS_SPACE = "applications";
34693
+ function registerAppsCommand(program) {
34694
+ const apps = program.command("apps").description("Install catalog apps into your applications space");
34695
+ apps.command("install <app_id>").description("Install a pinned catalog app: verify Share bytes, apply its schema, write its knowledge bundle").addHelpText(
34696
+ "after",
34697
+ `
34698
+ Examples:
34699
+ $ ${CLI_NAME23} apps install xyz.tinycloud.sample.healthlog
34700
+ `
34701
+ ).action(async (appIdInput, _options, cmd) => {
34702
+ try {
34703
+ const appId = assertKnowledgeAppId(appIdInput);
34704
+ const entry = pinnedApp(appId);
34705
+ if (!entry) throw new CLIError("UNKNOWN_APP", `No pinned catalog entry for ${appId}`, ExitCode.USAGE_ERROR);
34706
+ const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
34707
+ const node = await ensureAuthenticated(ctx);
34708
+ const profile = await ProfileManager.getProfile(ctx.profile);
34709
+ const spaceUri = operationSpaceResolver2(node, profile.spaceId)(APPLICATIONS_SPACE);
34710
+ const io = appsInstallIo(node.kvForSpace(spaceUri), node.sqlForSpace(spaceUri).db(entry.database));
34711
+ const result = await withSpinner(`Installing ${appId}...`, () => installApp(appId, io)).catch((error) => {
34712
+ throw installFailure(error);
34713
+ });
34714
+ if (shouldOutputJson()) outputJson(result);
34715
+ else printResult(result);
34716
+ } catch (error) {
34717
+ return handleError(error);
34718
+ }
34719
+ });
34720
+ }
34721
+ function appsInstallIo(kv, db) {
34722
+ return {
34723
+ async fetchBytes(url) {
34724
+ const response = await fetch(url, { redirect: "error" });
34725
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
34726
+ return new Uint8Array(await response.arrayBuffer());
34727
+ },
34728
+ async kvGet(key) {
34729
+ const result = await kv.get(key, { binary: true });
34730
+ if (result.ok) {
34731
+ const bytes = result.data.data;
34732
+ if (bytes instanceof Uint8Array) return bytes;
34733
+ throw new Error("KV_GET_NOT_BYTES");
34734
+ }
34735
+ if (result.error.code === "KV_NOT_FOUND") return void 0;
34736
+ throw new Error(result.error.code ?? "KV_GET_FAILED");
34737
+ },
34738
+ async kvCreate(key, bytes) {
34739
+ const result = await kv.put(key, bytes, { ifNoneMatch: "*" });
34740
+ if (result.ok) return "created";
34741
+ if (result.error.code === "KV_PRECONDITION_FAILED") return "exists";
34742
+ throw new Error(result.error.code ?? "KV_PUT_FAILED");
34743
+ },
34744
+ async sqlExecute(statement) {
34745
+ const result = await db.execute(statement);
34746
+ if (!result.ok) throw new Error(result.error.code ?? "SQL_EXECUTE_FAILED");
34747
+ },
34748
+ async sqlTableNames() {
34749
+ const result = await db.query("SELECT name FROM sqlite_master WHERE type = 'table'");
34750
+ if (!result.ok) throw new Error(result.error.code ?? "SQL_QUERY_FAILED");
34751
+ return result.data.rows.map(tableName);
34752
+ }
34753
+ };
34754
+ }
34755
+ function tableName(row) {
34756
+ if (Array.isArray(row)) return String(row[0]);
34757
+ if (row !== null && typeof row === "object" && "name" in row) return String(row.name);
34758
+ throw new Error("SQL_ROW_UNEXPECTED");
34759
+ }
34760
+ function installFailure(error) {
34761
+ if (!(error instanceof AppsInstallError)) return error;
34762
+ return new CLIError(error.code, error.message, ExitCode.ERROR);
34763
+ }
34764
+ function printResult(result) {
34765
+ const rows = [
34766
+ ["App", result.appId],
34767
+ ["Status", result.status],
34768
+ ["Database", result.database],
34769
+ ["Tables created", result.tablesCreated.join(", ") || "none"],
34770
+ ["Views recreated", result.viewsRecreated.join(", ") || "none"],
34771
+ ["Knowledge written", String(result.filesWritten.length)],
34772
+ ["Knowledge unchanged", String(result.filesUnchanged.length)],
34773
+ ["Knowledge left as edited", String(result.conflicts.length)]
34774
+ ];
34775
+ process.stdout.write(formatTable(["Field", "Value"], rows) + "\n");
34776
+ if (result.conflicts.length > 0) {
34777
+ const count = result.conflicts.length;
34778
+ const sentence = count === 1 ? "1 note was left as you edited it" : `${count} notes were left as you edited them`;
34779
+ process.stdout.write(theme.muted(`${sentence}: ${result.conflicts.join(", ")}`) + "\n");
34780
+ }
34781
+ if (result.status === "already") {
34782
+ process.stdout.write(theme.muted("Already installed; views refreshed.") + "\n");
34783
+ }
34784
+ }
34785
+
34315
34786
  // src/commands/init.ts
34316
34787
  init_profiles();
34317
34788
  init_formatter();
34318
34789
  init_errors();
34319
34790
  init_constants();
34320
- import { CLI_NAME as CLI_NAME23 } from "@tinycloud/operations/cli-name";
34791
+ import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
34321
34792
  function registerInitCommand(program) {
34322
34793
  program.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--default-space <name>", "Default space used when --space is omitted (e.g. applications)").action(async (options, cmd) => {
34323
34794
  try {
@@ -34335,7 +34806,7 @@ function registerInitCommand(program) {
34335
34806
  if (await ProfileManager.profileExists(profileName2)) {
34336
34807
  throw new CLIError(
34337
34808
  "PROFILE_EXISTS",
34338
- `Profile "${profileName2}" already exists. Use \`${CLI_NAME23} profile delete ${profileName2}\` first or choose a different name.`,
34809
+ `Profile "${profileName2}" already exists. Use \`${CLI_NAME24} profile delete ${profileName2}\` first or choose a different name.`,
34339
34810
  ExitCode.ERROR
34340
34811
  );
34341
34812
  }
@@ -34391,30 +34862,7 @@ init_session_expired();
34391
34862
  init_constants();
34392
34863
  init_profiles();
34393
34864
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
34394
- import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
34395
-
34396
- // src/lib/knowledge-input.ts
34397
- init_constants();
34398
- init_errors();
34399
- init_safe_text();
34400
- function usage2(message) {
34401
- return new CLIError("INVALID_ARGUMENT", message, ExitCode.USAGE_ERROR);
34402
- }
34403
- function assertKnowledgeAppId(value) {
34404
- if (!isAppId(value)) throw usage2(`Invalid app id "${safeClipped(value)}": ${APP_ID_RULE}.`);
34405
- return value;
34406
- }
34407
- function assertKnowledgeConcept(value) {
34408
- const reason = value.length > 300 ? "it is longer than 300 characters" : value.startsWith("/") ? "it starts with a slash" : /[*\\]/.test(value) ? "it contains a wildcard or backslash" : /%(?:2e|2f|5c)/i.test(value) ? "it contains an encoded dot, slash or backslash" : value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") ? "it has an empty, `.` or `..` segment" : void 0;
34409
- if (reason !== void 0) {
34410
- throw usage2(
34411
- `Invalid concept "${safeClipped(value)}": ${reason}. Use a path inside the app's knowledge folder, such as app or tables/meal.md.`
34412
- );
34413
- }
34414
- return value;
34415
- }
34416
-
34417
- // src/commands/knowledge.ts
34865
+ import { CLI_NAME as CLI_NAME25 } from "@tinycloud/operations/cli-name";
34418
34866
  init_permissions();
34419
34867
  init_errors();
34420
34868
  init_formatter();
@@ -34517,7 +34965,7 @@ function knowledgeFailure(result, profile, appId) {
34517
34965
  case "error": {
34518
34966
  if (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "PROFILE_POSTURE_NOT_ALLOWED" && result.context.posture === "unauthenticated") {
34519
34967
  return new CLIError("AUTH_REQUIRED", "Not signed in to TinyCloud.", ExitCode.AUTH_REQUIRED, {
34520
- hint: `Sign in with: ${CLI_NAME24} --profile ${result.context.profile} auth login`
34968
+ hint: `Sign in with: ${CLI_NAME25} --profile ${result.context.profile} auth login`
34521
34969
  });
34522
34970
  }
34523
34971
  if (result.error.code === "NODE_UNREACHABLE") {
@@ -34547,7 +34995,7 @@ function knowledgeFailure(result, profile, appId) {
34547
34995
  }
34548
34996
  }
34549
34997
  function missingGrantHint(profile, appId) {
34550
- const request = `${CLI_NAME24} --profile ${profile} auth request`;
34998
+ const request = `${CLI_NAME25} --profile ${profile} auth request`;
34551
34999
  const lines = appId === void 0 ? [
34552
35000
  ` ${request} --preset ${APPS_AGENT_PRESET} --emit req.json (read-only, all installed apps)`,
34553
35001
  ` ${request} --cap tinycloud.kv:${APPS_AGENT_SPACE}::get,list --emit req.json (all files, no table access)`,
@@ -34560,7 +35008,7 @@ function missingGrantHint(profile, appId) {
34560
35008
  return [
34561
35009
  "Ask the owner for a grant, narrowest first. Create the request:",
34562
35010
  ...lines,
34563
- `The owner runs \`${CLI_NAME24} auth grant req.json > grant.json\`; then import the result with \`${CLI_NAME24} --profile ${profile} auth import grant.json\`.`
35011
+ `The owner runs \`${CLI_NAME25} auth grant req.json > grant.json\`; then import the result with \`${CLI_NAME25} --profile ${profile} auth import grant.json\`.`
34564
35012
  ].join("\n");
34565
35013
  }
34566
35014
 
@@ -34868,7 +35316,7 @@ init_errors();
34868
35316
  init_constants();
34869
35317
  init_theme();
34870
35318
  import { resolveManifestKnowledgeRoot as resolveManifestKnowledgeRoot2 } from "@tinycloud/sdk-core";
34871
- import { CLI_NAME as CLI_NAME25 } from "@tinycloud/operations/cli-name";
35319
+ import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
34872
35320
  import { readFile as readFile12 } from "fs/promises";
34873
35321
  var DEFAULT_APP_SPACE = "applications";
34874
35322
  function registerManifestCommand(program) {
@@ -34876,8 +35324,8 @@ function registerManifestCommand(program) {
34876
35324
  manifest.command("resolve <source>").description("Resolve a manifest file or URL to its effective space, paths, and DB basenames").addHelpText("after", `
34877
35325
 
34878
35326
  Examples:
34879
- $ ${CLI_NAME25} manifest resolve ./manifest.json
34880
- $ ${CLI_NAME25} manifest resolve https://app.example.com/manifest.json --json
35327
+ $ ${CLI_NAME26} manifest resolve ./manifest.json
35328
+ $ ${CLI_NAME26} manifest resolve https://app.example.com/manifest.json --json
34881
35329
 
34882
35330
  What it shows:
34883
35331
  - app_id, name, manifest_version
@@ -34887,7 +35335,7 @@ What it shows:
34887
35335
 
34888
35336
  This command is read-only and does NOT contact the node \u2014 it just resolves
34889
35337
  the manifest against the active profile's address/chain so you know which
34890
- \`--space\` and \`--db\` values to pass to other ${CLI_NAME25} commands.
35338
+ \`--space\` and \`--db\` values to pass to other ${CLI_NAME26} commands.
34891
35339
  `).action(async (source, _options, cmd) => {
34892
35340
  try {
34893
35341
  const globalOpts = cmd.optsWithGlobals();
@@ -34956,7 +35404,7 @@ ${theme.heading("SQL databases")}
34956
35404
  `);
34957
35405
  }
34958
35406
  process.stdout.write(theme.muted(`
34959
- Use with: ${CLI_NAME25} sql query --space ${spaceName} --db <db> "..."
35407
+ Use with: ${CLI_NAME26} sql query --space ${spaceName} --db <db> "..."
34960
35408
  `));
34961
35409
  }
34962
35410
  if (permissions.length > 0) {
@@ -35076,7 +35524,7 @@ init_profiles();
35076
35524
  init_formatter();
35077
35525
  init_errors();
35078
35526
  init_constants();
35079
- import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
35527
+ import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
35080
35528
  import { createInterface as createInterface5 } from "readline";
35081
35529
  init_theme();
35082
35530
  init_types();
@@ -35222,9 +35670,9 @@ profile at command time. Precedence for every kv/sql command:
35222
35670
  explicit --space flag > profile defaultSpace > primary space.
35223
35671
 
35224
35672
  Examples:
35225
- $ ${CLI_NAME26} profile set-default-space applications
35226
- $ ${CLI_NAME26} profile set-default-space applications --profile cli-test
35227
- $ ${CLI_NAME26} profile set-default-space --unset
35673
+ $ ${CLI_NAME27} profile set-default-space applications
35674
+ $ ${CLI_NAME27} profile set-default-space applications --profile cli-test
35675
+ $ ${CLI_NAME27} profile set-default-space --unset
35228
35676
  `).action(async (name, options, cmd) => {
35229
35677
  try {
35230
35678
  const globalOpts = cmd.optsWithGlobals();
@@ -35236,7 +35684,7 @@ Examples:
35236
35684
  if (!options.unset && (name === void 0 || name === "")) {
35237
35685
  throw new CLIError(
35238
35686
  "USAGE_ERROR",
35239
- `Provide a space name (e.g. \`${CLI_NAME26} profile set-default-space applications\`) or pass --unset.`,
35687
+ `Provide a space name (e.g. \`${CLI_NAME27} profile set-default-space applications\`) or pass --unset.`,
35240
35688
  ExitCode.USAGE_ERROR
35241
35689
  );
35242
35690
  }
@@ -35295,7 +35743,7 @@ function parseOperatorType(raw) {
35295
35743
 
35296
35744
  // src/commands/replica.ts
35297
35745
  init_profile_replicas();
35298
- import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
35746
+ import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
35299
35747
  import { randomBytes as randomBytes7 } from "crypto";
35300
35748
  import { constants as fsConstants5 } from "fs";
35301
35749
  import { mkdir as mkdir5, open as open11, readdir as readdir6, rename as rename4, unlink as unlink5, lstat as lstat9 } from "fs/promises";
@@ -35436,11 +35884,11 @@ var EXIT_BY_CODE = {
35436
35884
  var READ_FAILURE = {
35437
35885
  absent: { code: "KEY_ABSENT", exit: ExitCode.NOT_FOUND, what: "is not in the replica (the replica covers its whole prefix)" },
35438
35886
  deleted: { code: "KEY_DELETED", exit: ExitCode.NOT_FOUND, what: "was deleted at the source" },
35439
- content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME27} replica sync` },
35887
+ content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME28} replica sync` },
35440
35888
  coverage_incomplete: {
35441
35889
  code: "COVERAGE_INCOMPLETE",
35442
35890
  exit: ExitCode.NOT_FOUND,
35443
- what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME27} replica sync`
35891
+ what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME28} replica sync`
35444
35892
  },
35445
35893
  not_covered: { code: "NOT_COVERED", exit: ExitCode.USAGE_ERROR, what: "is outside the replica's prefix" }
35446
35894
  };
@@ -35558,7 +36006,7 @@ async function existingReplicaName(profile, option) {
35558
36006
  const names = await replicaNames(profile);
35559
36007
  if (names.length === 1) return names[0];
35560
36008
  if (names.length === 0) {
35561
- throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME27} replica sync --prefix <prefix>`);
36009
+ throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME28} replica sync --prefix <prefix>`);
35562
36010
  }
35563
36011
  throw new CLIError("USAGE_ERROR", `Profile "${profile}" has several replicas (${names.join(", ")}); pass --replica <name>.`, ExitCode.USAGE_ERROR);
35564
36012
  }
@@ -35666,9 +36114,9 @@ async function storedGrants(profile) {
35666
36114
  function grantHint(space, prefix) {
35667
36115
  return [
35668
36116
  "A replica needs a device grant with get and sync on its prefix:",
35669
- ` device: ${CLI_NAME27} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
35670
- ` owner: ${CLI_NAME27} auth grant req.json > grant.json`,
35671
- ` device: ${CLI_NAME27} auth import grant.json`
36117
+ ` device: ${CLI_NAME28} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
36118
+ ` owner: ${CLI_NAME28} auth grant req.json > grant.json`,
36119
+ ` device: ${CLI_NAME28} auth import grant.json`
35672
36120
  ].join("\n");
35673
36121
  }
35674
36122
  function resolveSpace(input, prefix, grants) {
@@ -35759,7 +36207,7 @@ function describeStatus(status) {
35759
36207
  formatField("Size", formatBytes(status.bytes)),
35760
36208
  formatField("Grant", status.device.delegationCid),
35761
36209
  formatField("Last error", status.lastError ? `${status.lastError.code}: ${status.lastError.message}` : null),
35762
- formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME27} replica command retries` : null)
36210
+ formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME28} replica command retries` : null)
35763
36211
  ].join("\n");
35764
36212
  }
35765
36213
  async function writeFileAtomic(path, bytes) {
@@ -35923,7 +36371,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
35923
36371
  (options, cmd) => run(async () => {
35924
36372
  const profile = await profileName(cmd);
35925
36373
  if (options.purge) {
35926
- throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME27} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
36374
+ throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME28} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
35927
36375
  }
35928
36376
  if (options.clearPending) {
35929
36377
  throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
@@ -36025,7 +36473,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
36025
36473
  if (differs !== void 0) {
36026
36474
  throw new ReplicaError(
36027
36475
  ReplicaErrorCode.CONFIG_MISMATCH,
36028
- `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME27} replica reset --replica ${name} --purge first.`
36476
+ `Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME28} replica reset --replica ${name} --purge first.`
36029
36477
  );
36030
36478
  }
36031
36479
  }
@@ -36190,7 +36638,7 @@ init_theme();
36190
36638
  init_errors();
36191
36639
  init_warnings();
36192
36640
  init_constants();
36193
- import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
36641
+ import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
36194
36642
  import { readFile as readFile13 } from "fs/promises";
36195
36643
  import { join as join17 } from "path";
36196
36644
  import { homedir, hostname, userInfo } from "os";
@@ -36401,7 +36849,7 @@ function parseStatusEnvelope(line) {
36401
36849
  // src/lib/secrets-wait.ts
36402
36850
  init_constants();
36403
36851
  init_errors();
36404
- import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
36852
+ import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
36405
36853
  var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
36406
36854
  var WAIT_STEADY_INTERVAL_MS = 15e3;
36407
36855
  var DEFAULT_WAIT_TIMEOUT = "5m";
@@ -36409,7 +36857,7 @@ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
36409
36857
  var MAX_CONSECUTIVE_FAILURES = 5;
36410
36858
  var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
36411
36859
  var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
36412
- var EXPLICIT_KEY_HINT = `${CLI_NAME28} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME28} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
36860
+ var EXPLICIT_KEY_HINT = `${CLI_NAME29} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME29} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
36413
36861
  function abortableSleep(ms2, signal) {
36414
36862
  return new Promise((resolve9) => {
36415
36863
  if (signal.aborted) {
@@ -36445,7 +36893,7 @@ async function runSecretsWait(request, deps) {
36445
36893
  let step = 0;
36446
36894
  const cancelled = () => new CLIError(
36447
36895
  "CANCELLED",
36448
- `Interrupted: ${CLI_NAME28} secrets wait stopped before the secrets were ready.`,
36896
+ `Interrupted: ${CLI_NAME29} secrets wait stopped before the secrets were ready.`,
36449
36897
  ExitCode.CANCELLED,
36450
36898
  progress === void 0 ? {} : { progress }
36451
36899
  );
@@ -36589,13 +37037,13 @@ function statusError(error, profile, warnings) {
36589
37037
  if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
36590
37038
  const recovery = recoveryOf(error, "sign_in");
36591
37039
  if (recovery !== void 0) {
36592
- const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME28} auth login, then retry.`;
37040
+ const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME29} auth login, then retry.`;
36593
37041
  return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
36594
37042
  }
36595
37043
  }
36596
37044
  if (error.code === "PROFILE_MIGRATION_REQUIRED") {
36597
37045
  const recovery = recoveryOf(error, "complete_migration");
36598
- const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME28} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
37046
+ const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME29} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
36599
37047
  return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
36600
37048
  hint,
36601
37049
  ...recovery === void 0 ? {} : { recovery },
@@ -36787,11 +37235,11 @@ async function runSecretOperation(params) {
36787
37235
  }
36788
37236
  function secretPermissionReason(action, name) {
36789
37237
  const target = name ? ` secret "${name}"` : " secrets";
36790
- return `Allow \`${CLI_NAME29} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
37238
+ return `Allow \`${CLI_NAME30} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
36791
37239
  }
36792
37240
  function assertOwnerApprovalPossible(profileName2, profile, action, name, missing = []) {
36793
37241
  if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
36794
- const command = `${CLI_NAME29} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
37242
+ const command = `${CLI_NAME30} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
36795
37243
  throw new CLIError(
36796
37244
  "PERMISSION_DENIED",
36797
37245
  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 "${profileName2}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
@@ -36802,7 +37250,7 @@ function assertOwnerApprovalPossible(profileName2, profile, action, name, missin
36802
37250
  );
36803
37251
  }
36804
37252
  function scopedSecretLoginHint(profileName2) {
36805
- return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME29} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
37253
+ return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME30} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
36806
37254
  }
36807
37255
  async function runSecretOperationAttempt(label, operation) {
36808
37256
  try {
@@ -36936,7 +37384,7 @@ ${message}`,
36936
37384
  "AUTH_REQUIRED",
36937
37385
  "Not signed in to TinyCloud.",
36938
37386
  ExitCode.AUTH_REQUIRED,
36939
- { hint: `Sign in with: ${CLI_NAME29} --profile ${result.context.profile} auth login` }
37387
+ { hint: `Sign in with: ${CLI_NAME30} --profile ${result.context.profile} auth login` }
36940
37388
  );
36941
37389
  }
36942
37390
  if (result.error.code === "NODE_UNREACHABLE") {
@@ -37413,7 +37861,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
37413
37861
  name: "Encryption network",
37414
37862
  ok: false,
37415
37863
  detail: `${networkName} not found`,
37416
- hint: `${CLI_NAME29} secrets network init ${networkName}`
37864
+ hint: `${CLI_NAME30} secrets network init ${networkName}`
37417
37865
  }
37418
37866
  ];
37419
37867
  let secret;
@@ -37459,7 +37907,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
37459
37907
  name: "Secret access",
37460
37908
  ok: false,
37461
37909
  detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
37462
- hint: notFound ? local ? `${CLI_NAME29} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME29} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
37910
+ hint: notFound ? local ? `${CLI_NAME30} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME30} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
37463
37911
  });
37464
37912
  }
37465
37913
  } else {
@@ -37703,7 +38151,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
37703
38151
  if (effectiveLocal(profile, options)) {
37704
38152
  throw new CLIError(
37705
38153
  "LOCAL_OWNER_PROFILE",
37706
- `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME29} secrets put NAME --stdin instead of requesting them.`,
38154
+ `Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME30} secrets put NAME --stdin instead of requesting them.`,
37707
38155
  ExitCode.USAGE_ERROR
37708
38156
  );
37709
38157
  }
@@ -37826,7 +38274,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
37826
38274
 
37827
38275
  // src/commands/share.ts
37828
38276
  import { Option } from "commander";
37829
- import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
38277
+ import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
37830
38278
  import { ProfileLockTimeoutError as ProfileLockTimeoutError3 } from "@tinycloud/operations/state";
37831
38279
  init_errors();
37832
38280
 
@@ -39220,7 +39668,7 @@ function htmlFolderNotice(bundle) {
39220
39668
  }
39221
39669
 
39222
39670
  // src/commands/share.ts
39223
- var SHARE_ORIGIN = "https://share.tinycloud.xyz";
39671
+ var SHARE_ORIGIN2 = "https://share.tinycloud.xyz";
39224
39672
  var NOTIFY_WINDOW_MESSAGE = "Share notification authorization expired (at share expiry or 5 minutes after publication); publish a new share and invite the recipient then.\n";
39225
39673
  var ONGOING_UNAVAILABLE = "Ongoing shares are not available yet; v1 shares pin a version";
39226
39674
  function artifactRequest(options, actions, inputCount) {
@@ -39285,7 +39733,7 @@ function shareCliError(error, operation = "publish") {
39285
39733
  "SHARE_HISTORY_UNREADABLE",
39286
39734
  "sender history cannot be read with the current session key",
39287
39735
  1,
39288
- { hint: `Run \`${CLI_NAME30} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
39736
+ { hint: `Run \`${CLI_NAME31} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
39289
39737
  );
39290
39738
  }
39291
39739
  if (error instanceof SharePublishAuthorityError) {
@@ -39293,12 +39741,12 @@ function shareCliError(error, operation = "publish") {
39293
39741
  const profileName2 = "profileName" in failure ? failure.profileName : void 0;
39294
39742
  const localKey = "localKey" in failure && failure.localKey === true;
39295
39743
  const profileHint = profileName2 === void 0 ? "" : `--profile ${profileName2} `;
39296
- const loginHint = localKey ? `\`${CLI_NAME30} ${profileHint}auth login --method local\`` : `\`${CLI_NAME30} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME30} ${profileHint}enable share\`)`;
39744
+ const loginHint = localKey ? `\`${CLI_NAME31} ${profileHint}auth login --method local\`` : `\`${CLI_NAME31} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME31} ${profileHint}enable share\`)`;
39297
39745
  if (failure.kind === "caveated-session") {
39298
39746
  const holder = profileName2 === void 0 ? "this session" : `profile ${profileName2}'s session`;
39299
39747
  return new CLIError(
39300
39748
  "PERMISSION_DENIED",
39301
- `${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME30} init --name publisher --key-only && ${CLI_NAME30} --profile publisher enable share\``,
39749
+ `${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME31} init --name publisher --key-only && ${CLI_NAME31} --profile publisher enable share\``,
39302
39750
  5
39303
39751
  );
39304
39752
  }
@@ -39384,7 +39832,7 @@ function shareCliError(error, operation = "publish") {
39384
39832
  if (nodeCode === "ENOENT") return new CLIError("INVALID_ARGUMENT", "share input was not found", 2);
39385
39833
  if (nodeCode === "EISDIR") return new CLIError("INVALID_ARGUMENT", "share input must be a Markdown file", 2);
39386
39834
  const mapped = typeof nodeCode === "string" && known[nodeCode] !== void 0 ? known[nodeCode] : known[message];
39387
- const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME30} auth login\``) : void 0;
39835
+ const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME31} auth login\``) : void 0;
39388
39836
  if (classified !== void 0) return Object.assign(new CLIError(classified.code, classified.reason, classified.exitCode), { cause: error });
39389
39837
  if (error instanceof TypeError) return new CLIError("INVALID_ARGUMENT", "share input is invalid", 2);
39390
39838
  return new CLIError(mapped?.code ?? "INVALID_ARGUMENT", mapped ? mapped.code : "share operation failed", mapped?.exit ?? 2);
@@ -39473,7 +39921,7 @@ function assertAggregateInputLimit(inputs, maxBytes) {
39473
39921
  }
39474
39922
  function registerShareCommand(program) {
39475
39923
  const share = program.command("share").description("Publish and consume TinyCloud Share links");
39476
- share.command("publish <files...>").description("Publish one or more bounded files as a Share, or a folder with an index.html as one self-contained page (anyone links only)").option("--name <filename>", "Filename for stdin input, or the published .html name of a folder").option("--to <target>", "Share target", "anyone").option("--notify", "Email an exact-email recipient, or a domain mailbox on node 1.17.3+").option("--notify-to <address>", "Mailbox to invite for --to domain:<name> --notify (node 1.17.3+)").option("--expires <duration>", "Share lifetime").option("--max-bytes <bytes>", "Bound input bytes").option("--media-type <type>", "Media type for a single input").option("--action <actions...>", "Addressed permission: read, list, or edit").option("--prefix", "Publish multiple inputs beneath one addressed prefix").option("--binary", "Allow non-UTF-8 bearer content").option("--artifact <id>", 'Publish as the next write-once version of an artifact ("new" starts one); the share is pinned to that version').addOption(new Option("--ongoing").hideHelp()).option("--note <text>", "Note stored with the artifact version").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Canonical HTTPS viewer origin", SHARE_ORIGIN).action(async (files, options, command) => {
39924
+ share.command("publish <files...>").description("Publish one or more bounded files as a Share, or a folder with an index.html as one self-contained page (anyone links only)").option("--name <filename>", "Filename for stdin input, or the published .html name of a folder").option("--to <target>", "Share target", "anyone").option("--notify", "Email an exact-email recipient, or a domain mailbox on node 1.17.3+").option("--notify-to <address>", "Mailbox to invite for --to domain:<name> --notify (node 1.17.3+)").option("--expires <duration>", "Share lifetime").option("--max-bytes <bytes>", "Bound input bytes").option("--media-type <type>", "Media type for a single input").option("--action <actions...>", "Addressed permission: read, list, or edit").option("--prefix", "Publish multiple inputs beneath one addressed prefix").option("--binary", "Allow non-UTF-8 bearer content").option("--artifact <id>", 'Publish as the next write-once version of an artifact ("new" starts one); the share is pinned to that version').addOption(new Option("--ongoing").hideHelp()).option("--note <text>", "Note stored with the artifact version").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Canonical HTTPS viewer origin", SHARE_ORIGIN2).action(async (files, options, command) => {
39477
39925
  try {
39478
39926
  const json = jsonOutput(options, command);
39479
39927
  if (options.ongoing === true) throw new CLIError("UNSUPPORTED_TARGET", ONGOING_UNAVAILABLE, 2);
@@ -39564,7 +40012,7 @@ function registerShareCommand(program) {
39564
40012
  return handleError(shareCliError(error));
39565
40013
  }
39566
40014
  });
39567
- share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
40015
+ share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN2).action(async (url, options, command) => {
39568
40016
  try {
39569
40017
  const json = jsonOutput(options, command);
39570
40018
  const result = await inspectShareInputOnce(url, options.stdin === true, options.viewerOrigin);
@@ -39609,7 +40057,7 @@ function registerShareCommand(program) {
39609
40057
  return handleError(shareCliError(error));
39610
40058
  }
39611
40059
  });
39612
- share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN).action(async (url, options, command) => {
40060
+ share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN2).action(async (url, options, command) => {
39613
40061
  try {
39614
40062
  const json = jsonOutput(options, command);
39615
40063
  if (options.stdout && json) throw new CLIError("INVALID_ARGUMENT", "--stdout and --json are mutually exclusive", 2);
@@ -39748,7 +40196,7 @@ init_constants();
39748
40196
  init_sdk();
39749
40197
  init_host();
39750
40198
  init_theme();
39751
- import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
40199
+ import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
39752
40200
  import { ACCOUNT_REGISTRY_SPACE } from "@tinycloud/node-sdk";
39753
40201
  import { randomBytes as randomBytes9 } from "crypto";
39754
40202
  import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
@@ -39795,7 +40243,7 @@ function registerSpaceCommand(program) {
39795
40243
  } else {
39796
40244
  const profile = await ProfileManager.getProfile(ctx.profile);
39797
40245
  const cap = "tinycloud.kv:account:spaces/:get,list";
39798
- const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME31} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME31} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
40246
+ const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME32} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME32} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
39799
40247
  throw new CLIError(
39800
40248
  "PERMISSION_DENIED",
39801
40249
  `Listing spaces needs tinycloud.kv/get and tinycloud.kv/list on the account space's spaces/ registry (${accountSpaceId ?? ACCOUNT_REGISTRY_SPACE}), or tinycloud.space/list. This session holds neither.`,
@@ -39832,10 +40280,10 @@ function registerSpaceCommand(program) {
39832
40280
 
39833
40281
  A delegate cannot host a space \u2014 only its owner (root authority) can. This
39834
40282
  emits a tinycloud.host.request artifact naming the space and its owner so you
39835
- can hand it to the owner; they run \`${CLI_NAME31} space host <name>\` and confirm.
40283
+ can hand it to the owner; they run \`${CLI_NAME32} space host <name>\` and confirm.
39836
40284
 
39837
40285
  If you ARE the owner of the resolved space, this refuses and tells you to host
39838
- it directly with \`${CLI_NAME31} space host <name>\` (no request needed).
40286
+ it directly with \`${CLI_NAME32} space host <name>\` (no request needed).
39839
40287
  `).action(async (name, options, cmd) => {
39840
40288
  try {
39841
40289
  const globalOpts = cmd.optsWithGlobals();
@@ -39846,7 +40294,7 @@ it directly with \`${CLI_NAME31} space host <name>\` (no request needed).
39846
40294
  if (await isRootAuthority(spaceId, ctx.profile)) {
39847
40295
  throw new CLIError(
39848
40296
  "ALREADY_ROOT_AUTHORITY",
39849
- `You are the owner of ${spaceId}. Host it directly: ${CLI_NAME31} space host ${spaceName}`,
40297
+ `You are the owner of ${spaceId}. Host it directly: ${CLI_NAME32} space host ${spaceName}`,
39850
40298
  ExitCode.USAGE_ERROR
39851
40299
  );
39852
40300
  }
@@ -39932,7 +40380,7 @@ init_sdk();
39932
40380
  init_space();
39933
40381
  init_host();
39934
40382
  init_theme();
39935
- import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
40383
+ import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
39936
40384
  import { writeFile as writeFile5 } from "fs/promises";
39937
40385
  import { resolve as resolve8 } from "path";
39938
40386
  async function dbHandle(node, dbName, spaceInput, profileName2) {
@@ -39956,12 +40404,12 @@ database for simple apps, or pass --db to target a named database. Pass
39956
40404
  --space to target a non-primary space (e.g. the manifest "applications" space).
39957
40405
 
39958
40406
  Common workflows:
39959
- $ ${CLI_NAME32} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
39960
- $ ${CLI_NAME32} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
39961
- $ ${CLI_NAME32} sql query "SELECT id, body FROM notes ORDER BY id"
39962
- $ ${CLI_NAME32} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
39963
- $ ${CLI_NAME32} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
39964
- $ ${CLI_NAME32} sql export --db analytics --output analytics.db
40407
+ $ ${CLI_NAME33} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
40408
+ $ ${CLI_NAME33} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
40409
+ $ ${CLI_NAME33} sql query "SELECT id, body FROM notes ORDER BY id"
40410
+ $ ${CLI_NAME33} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
40411
+ $ ${CLI_NAME33} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
40412
+ $ ${CLI_NAME33} sql export --db analytics --output analytics.db
39965
40413
 
39966
40414
  Commands:
39967
40415
  query Read rows with SELECT statements
@@ -39978,10 +40426,10 @@ Tips:
39978
40426
  sql.command("query <sql>").description("Run a read-only SELECT query").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
39979
40427
 
39980
40428
  Examples:
39981
- $ ${CLI_NAME32} sql query "SELECT * FROM notes ORDER BY id"
39982
- $ ${CLI_NAME32} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
39983
- $ ${CLI_NAME32} sql query "SELECT count(*) AS total FROM events" --db analytics --json
39984
- $ ${CLI_NAME32} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
40429
+ $ ${CLI_NAME33} sql query "SELECT * FROM notes ORDER BY id"
40430
+ $ ${CLI_NAME33} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
40431
+ $ ${CLI_NAME33} sql query "SELECT count(*) AS total FROM events" --db analytics --json
40432
+ $ ${CLI_NAME33} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
39985
40433
 
39986
40434
  Output:
39987
40435
  Human output is formatted as a table. Piped output or --json returns
@@ -40022,11 +40470,11 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
40022
40470
  sql.command("execute <sql>").description("Run a write or schema statement").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
40023
40471
 
40024
40472
  Examples:
40025
- $ ${CLI_NAME32} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
40026
- $ ${CLI_NAME32} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
40027
- $ ${CLI_NAME32} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
40028
- $ ${CLI_NAME32} sql execute "DROP TABLE old_notes" --db archive
40029
- $ ${CLI_NAME32} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
40473
+ $ ${CLI_NAME33} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
40474
+ $ ${CLI_NAME33} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
40475
+ $ ${CLI_NAME33} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
40476
+ $ ${CLI_NAME33} sql execute "DROP TABLE old_notes" --db archive
40477
+ $ ${CLI_NAME33} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
40030
40478
 
40031
40479
  Output:
40032
40480
  Returns JSON with the changed row count and last inserted row id when available.
@@ -40055,9 +40503,9 @@ Output:
40055
40503
  sql.command("export").description("Export a SQLite database as a binary .db file").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("-o, --output <file>", "Output file path", "export.db").addHelpText("after", `
40056
40504
 
40057
40505
  Examples:
40058
- $ ${CLI_NAME32} sql export
40059
- $ ${CLI_NAME32} sql export --db analytics --output analytics.db
40060
- $ ${CLI_NAME32} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
40506
+ $ ${CLI_NAME33} sql export
40507
+ $ ${CLI_NAME33} sql export --db analytics --output analytics.db
40508
+ $ ${CLI_NAME33} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
40061
40509
 
40062
40510
  Output:
40063
40511
  Writes the database file locally and returns JSON with the path and size.
@@ -40090,19 +40538,19 @@ Output:
40090
40538
  sql.command("copy").description("Copy rows between SQL databases (optionally across spaces)").requiredOption("--from-db <name>", "Source database name").requiredOption("--to-db <name>", "Destination database name").option("--from-space <name|uri>", "Source space (defaults to primary)").option("--to-space <name|uri>", "Destination space (defaults to primary)").option("--table <name...>", "Restrict copy to specific tables (repeat or comma-separated)").option("--dry-run", "Print the plan without writing", false).addHelpText("after", `
40091
40539
 
40092
40540
  Examples:
40093
- $ ${CLI_NAME32} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
40541
+ $ ${CLI_NAME33} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
40094
40542
  --to-db xyz.tinycloud.listen/conversations \\
40095
40543
  --space applications --dry-run
40096
- $ ${CLI_NAME32} sql copy --from-space applications --from-db com.foo/data \\
40544
+ $ ${CLI_NAME33} sql copy --from-space applications --from-db com.foo/data \\
40097
40545
  --to-space applications --to-db com.bar/data \\
40098
40546
  --table conversation --table participant
40099
40547
 
40100
40548
  Notes:
40101
40549
  - Refuses to run when (resolved space, db) is identical for source and destination.
40102
- - Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME32} sql execute\`)
40550
+ - Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME33} sql execute\`)
40103
40551
  to materialize the schema before copying.
40104
40552
  - One row at a time; suitable for small/medium datasets. Large copies should
40105
- use \`${CLI_NAME32} sql export\` + bulk import.
40553
+ use \`${CLI_NAME33} sql export\` + bulk import.
40106
40554
  - Authorization: the active session/delegation must cover sql/read on source
40107
40555
  AND sql/write on destination. Otherwise the relevant operation will fail.
40108
40556
  `).action(async (options, cmd) => {
@@ -40202,7 +40650,7 @@ init_errors();
40202
40650
  init_formatter();
40203
40651
  init_theme();
40204
40652
  init_permissions();
40205
- import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
40653
+ import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
40206
40654
  import {
40207
40655
  NodeWasmBindings as NodeWasmBindings2
40208
40656
  } from "@tinycloud/node-sdk";
@@ -40476,7 +40924,7 @@ function formatStatus(summary) {
40476
40924
  lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
40477
40925
  lines.push("");
40478
40926
  if (summary.profiles.length === 0) {
40479
- lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME33} init`));
40927
+ lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME34} init`));
40480
40928
  return `${lines.join("\n")}
40481
40929
  `;
40482
40930
  }
@@ -40975,6 +41423,7 @@ function registerTinyCloudCommands(program) {
40975
41423
  registerDuckdbCommand(program);
40976
41424
  registerManifestCommand(program);
40977
41425
  registerKnowledgeCommand(program);
41426
+ registerAppsCommand(program);
40978
41427
  registerUpgradeCommand(program);
40979
41428
  registerStatusCommand(program);
40980
41429
  registerAccountCommand(program);