mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

inbox-check.ts

Language TypeScript4.1 KB100 lines

scripts/inbox-check.ts

  1. /**
  2. * Does the Gmail service account actually work? Ask Google directly.
  3. *
  4. * Verifying the credential through the Worker is slow or impossible:
  5. * `wrangler dev` cannot see secrets set with `secret put`, so it always reports
  6. * "not set"; `wrangler dev --remote` can, but its preview-session API is
  7. * unreliable on the pinned wrangler; and a deployed Worker only reads Gmail
  8. * when its cron fires. So the one question that matters during setup (is the
  9. * delegation right?) needs its own fast answer.
  10. *
  11. * This asks Google the same question the Worker will, through the same client
  12. * code, with no Cloudflare in the middle.
  13. *
  14. * Read-only; writes nothing. It lists one message per mailbox to prove the
  15. * impersonation works, prints no subjects and no senders, and stores nothing.
  16. *
  17. * pnpm inbox:check ~/Downloads/meridian-dashboard-inbox-abc123.json
  18. *
  19. * Or, if you have already deleted the key file and put it in your environment:
  20. *
  21. * pnpm inbox:check
  22. */
  23. import { readFileSync } from 'node:fs'
  24. import { INBOX_MAILBOXES } from '../config/inbox.js'
  25. import { GMAIL_SCOPE, GMAIL_SCOPES, readMailbox, type ServiceAccount } from '@meridian/gmail-client'
  26. import { errorMessage } from '@meridian/shared'
  27. function credentials(): ServiceAccount {
  28. const file = process.argv[2]
  29. if (file) {
  30. let parsed: { client_email?: string; private_key?: string }
  31. try {
  32. parsed = JSON.parse(readFileSync(file, 'utf8')) as typeof parsed
  33. } catch (err) {
  34. console.error(`Could not read ${file}: ${errorMessage(err)}`)
  35. process.exit(1)
  36. }
  37. if (!parsed.client_email || !parsed.private_key) {
  38. console.error(`${file} has no client_email / private_key. Is it the service account key JSON?`)
  39. process.exit(1)
  40. }
  41. return { email: parsed.client_email, privateKey: parsed.private_key }
  42. }
  43. const email = process.env.GMAIL_SA_EMAIL
  44. const privateKey = process.env.GMAIL_SA_KEY
  45. if (!email || !privateKey) {
  46. console.error('Usage: pnpm inbox:check <path-to-service-account-key.json>')
  47. console.error(' or: set GMAIL_SA_EMAIL and GMAIL_SA_KEY and run with no argument.')
  48. process.exit(1)
  49. }
  50. return { email, privateKey }
  51. }
  52. const sa = credentials()
  53. console.log(`\nService account: ${sa.email}`)
  54. console.log(`Scopes: ${GMAIL_SCOPE}\n`)
  55. let ok = 0
  56. const failures: string[] = []
  57. for (const m of INBOX_MAILBOXES) {
  58. // One message is enough to prove impersonation. Nothing about it is printed.
  59. const r = await readMailbox(m.email, sa, { maxMessages: 1 })
  60. if (r.ok) {
  61. ok++
  62. console.log(` OK ${m.email.padEnd(28)} (${r.messages.length} message read)`)
  63. } else {
  64. failures.push(m.email)
  65. console.log(` FAILED ${m.email.padEnd(28)} ${r.error}`)
  66. }
  67. }
  68. console.log(`\n${ok} of ${INBOX_MAILBOXES.length} mailboxes readable.`)
  69. if (failures.length === INBOX_MAILBOXES.length) {
  70. // Every mailbox failing is a different diagnosis from one failing, and the
  71. // difference is worth stating rather than leaving to be inferred from a wall
  72. // of identical errors.
  73. console.log(
  74. `\nAll of them failed, which points at the delegation rather than at any\n` +
  75. `individual mailbox. Check, in this order:\n` +
  76. ` 1. Google Admin -> Security -> Access and data control -> API controls\n` +
  77. ` -> Domain-wide delegation has an entry for this service account's\n` +
  78. ` UNIQUE ID (the long number, not the email).\n` +
  79. ` 2. Its scopes are exactly (that field is COMMA-separated, unlike\n` +
  80. ` the OAuth parameter above): ${GMAIL_SCOPES.join(',')}\n` +
  81. ` 3. It has been a few minutes: Admin changes propagate slowly.\n` +
  82. ` 4. The Gmail API is enabled on the Cloud project.`,
  83. )
  84. } else if (failures.length) {
  85. console.log(`\nSome mailboxes read and some did not, so the delegation is working.`)
  86. console.log(`The failures are about those addresses: ${failures.join(', ')}.`)
  87. console.log(`A mailbox that does not exist, is suspended, or is a group rather`)
  88. console.log(`than a mailbox will fail this way.`)
  89. } else if (process.argv[2]) {
  90. console.log(`\nAll good. Delete the key file now: it opens every mailbox in the domain:`)
  91. console.log(` rm ${process.argv[2]}`)
  92. }