switchroom 0.14.34 → 0.14.36

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.
@@ -420,6 +420,7 @@ import {
420
420
  approvalRecord,
421
421
  } from '../../src/vault/approvals/client.js'
422
422
  import { resolveVaultApprovalPosture } from '../vault-approval-posture.js'
423
+ import { matchesAdminOnlyKey } from '../../src/vault/admin-only-keys.js'
423
424
  import {
424
425
  openTurnsDb,
425
426
  markOrphanedWithTimeoutClassification,
@@ -2690,6 +2691,15 @@ const VAULT_PASSPHRASE_TTL_MS = 30 * 60 * 1000
2690
2691
  */
2691
2692
  let VAULT_APPROVAL_AUTH_MODE: 'passphrase' | 'telegram-id' = 'passphrase'
2692
2693
 
2694
+ /**
2695
+ * Admin-only vault keys (`vault.broker.adminOnlyKeys`). A grant for one
2696
+ * of these may be approved ONLY by the admin operator
2697
+ * (`access.allowFrom[0]`) and is always minted via the operator
2698
+ * passphrase — never posture, even under telegram-id mode (the broker
2699
+ * enforces the same rule). Cached at boot alongside the posture mode.
2700
+ */
2701
+ let ADMIN_ONLY_KEYS: string[] = []
2702
+
2693
2703
  export function initVaultApprovalPosture(): void {
2694
2704
  let cfg: ReturnType<typeof loadSwitchroomConfig>
2695
2705
  try {
@@ -2722,6 +2732,13 @@ export function initVaultApprovalPosture(): void {
2722
2732
  `(single-factor — broker mediates attestation via attest_via_posture)\n`,
2723
2733
  )
2724
2734
  }
2735
+ ADMIN_ONLY_KEYS = cfg.vault?.broker?.adminOnlyKeys ?? []
2736
+ if (ADMIN_ONLY_KEYS.length > 0) {
2737
+ process.stderr.write(
2738
+ `telegram gateway: ${ADMIN_ONLY_KEYS.length} admin-only vault key pattern(s) ` +
2739
+ `— grants approved by allowFrom[0] + operator passphrase only\n`,
2740
+ )
2741
+ }
2725
2742
  }
2726
2743
  /**
2727
2744
  * Gateway-side guard on vault-key shape — UX gate, not a security
@@ -13851,11 +13868,31 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
13851
13868
  }
13852
13869
 
13853
13870
  if (action === 'approve') {
13871
+ // Admin-only credentials (`vault.broker.adminOnlyKeys`) are held to a
13872
+ // higher bar: ONLY the admin operator (allowFrom[0]) may approve, and
13873
+ // the grant must be minted with the operator passphrase — never
13874
+ // posture, even under telegram-id mode (the broker enforces the same
13875
+ // rule, so a posture mint would just be rejected). So for an
13876
+ // admin-only key we (a) reject taps from any non-admin allowFrom
13877
+ // member, and (b) skip the telegram-id posture branch below, falling
13878
+ // through to the passphrase-prompt path. The card + buttons stay
13879
+ // intact on a non-admin tap so the admin can still approve.
13880
+ const isAdminOnly = matchesAdminOnlyKey(pending.key, ADMIN_ONLY_KEYS)
13881
+ if (isAdminOnly && senderId !== access.allowFrom[0]) {
13882
+ await ctx
13883
+ .answerCallbackQuery({
13884
+ text: '🔒 Admin-only credential — only the owner can approve this.',
13885
+ })
13886
+ .catch(() => {})
13887
+ return
13888
+ }
13889
+
13854
13890
  // Posture: telegram-id (opt-in single-factor). The broker is
13855
13891
  // auto-unlocked and we silently hold the passphrase in memory; skip
13856
13892
  // the passphrase-cache lookup + prompt entirely and mint directly.
13857
13893
  // Allowlist check above already attested the operator's Telegram ID.
13858
- if (VAULT_APPROVAL_AUTH_MODE === 'telegram-id') {
13894
+ // Admin-only keys are excluded — they take the passphrase path below.
13895
+ if (!isAdminOnly && VAULT_APPROVAL_AUTH_MODE === 'telegram-id') {
13859
13896
  const username = ctx.from?.username ?? ctx.from?.first_name ?? `id=${senderId}`
13860
13897
  if (pending.card_message_id != null) {
13861
13898
  await ctx.api
@@ -13920,6 +13957,9 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
13920
13957
  pending.card_message_id,
13921
13958
  joiningBatch
13922
13959
  ? `🔐 <b>Queued behind an earlier card.</b> Type your passphrase as your next message — it covers <b>${items.length}</b> pending approvals in this chat (one entry mints all grants, no re-type per card).`
13960
+ : isAdminOnly
13961
+ ? `🔒 <b>Admin-only credential.</b> <code>${escapeHtmlForTg(pending.key)}</code> requires your vault passphrase to grant — reply with it as your next message and we'll mint the grant for <b>${escapeHtmlForTg(pending.agent)}</b>, then delete the passphrase message.\n\n` +
13962
+ `<i>The passphrase is what proves it's you: an agent can never mint this key on its own.</i>`
13923
13963
  : `🔐 <b>Vault is locked.</b> Reply with your passphrase as your next message — we'll unlock, mint the grant for <b>${escapeHtmlForTg(pending.agent)}</b>, and delete the passphrase message in one step.\n\n` +
13924
13964
  `<i>Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches.</i>`,
13925
13965
  { parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },