Skip to content

Add Blooio provider (send + receive) with example and tests - #1

Open
davext wants to merge 3 commits into
time-attack:mainfrom
davext:add-blooio-provider
Open

davext wants to merge 3 commits into
time-attack:mainfrom
davext:add-blooio-provider

Conversation

@davext

@davext davext commented Aug 15, 2026

Copy link
Copy Markdown

Summary

Adds a third provider adapter alongside Linq and Photon so an OpenPigeon bot can run over Blooio's iMessage API. The game codec is untouched — this is purely a transport adapter, and the same readreply logic works across all three providers.

  • src/providers/blooio.tsBlooio.send(url, { to }) posts a GamePigeon balloon (invite or move) to the Blooio v4 API (POST /v4/channels/{number}/messages with the imessage_app content type), plus sendText(). Blooio.fromWebhook(body) parses the message.received webhook.
  • src/index.ts — exports Blooio and a fromBlooioWebhook() helper (mirrors fromWebhook / fromPhotonMessage).
  • examples/blooio-express-bot.ts — ~25-line Express bot, mirroring linq-express-bot.ts.
  • test/inbound.test.ts — Blooio receive-path assertions (parse a real captured move wrapped in Blooio's webhook shape; ignore non-GamePigeon).
  • README — Blooio row in the providers table + a receive snippet.

Why it's a natural fit

Blooio delivers the inbound app balloon already decoded, so the webhook's data.imessage_app.url is the ready-to-read app-state URL — your bot never decodes anything Apple-specific. Balloons render natively on the recipient's GamePigeon, and onboarding is self-serve (provision a number from the API/dashboard, no sales call), which keeps the "install → first 8-Ball invite in one sitting" story true for the quickstart.

Test plan

  • npm test — all codecs OK; 16 inbound assertions pass (incl. new blooio: cases)
  • New provider typechecks (no new tsc errors; the one pre-existing require diagnostic in invite.ts is unrelated)
  • Verified end-to-end: a fresh invite("pool") URL sent via Blooio renders as a playable 8-Ball invite

Disclosure

I contribute to Blooio. Happy to adjust wording/placement — the goal is parity with the existing providers, not promotion.

Adds a third provider adapter alongside Linq and Photon:

- src/providers/blooio.ts — send GamePigeon balloons (invite or move) and
  plain text over the Blooio v4 REST API, and parse the `message.received`
  webhook. Blooio delivers the inbound balloon already decoded, so the
  webhook's `data.imessage_app.url` is the ready-to-read app-state URL.
- index.ts — export `Blooio` and a `fromBlooioWebhook()` helper.
- examples/blooio-express-bot.ts — ~25-line Express bot mirroring the Linq one.
- test/inbound.test.ts — Blooio receive-path assertions (parse + ignore).
- README — list Blooio in the providers table and receive section.

Disclosure: I contribute to Blooio. Game logic is identical across providers;
this only adds the transport adapter.

Co-authored-by: Cursor <cursoragent@cursor.com>
@davext
davext force-pushed the add-blooio-provider branch from 40eb8fc to 7c0e3de Compare August 15, 2026 00:57

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread examples/blooio-express-bot.ts Outdated
Comment thread examples/blooio-express-bot.ts
Comment thread examples/blooio-express-bot.ts Outdated
Comment thread src/providers/blooio.ts Outdated
Comment thread examples/blooio-express-bot.ts Outdated
Comment thread examples/blooio-express-bot.ts Outdated
Comment thread README.md Outdated
Comment thread README.md
@time-attack

Copy link
Copy Markdown
Owner

@davext fix thiese and ill merge

…trict imessage_app parsing; drop promo copy

- examples/blooio-express-bot.ts: verify X-Blooio-Signature (HMAC-SHA256 over
  `t.rawBody`, express.raw + replay guard) before trusting a webhook; ack 200
  before the outbound send so provider retries can't double-fire; admin-gate
  /start behind ADMIN_TOKEN and validate/URL-encode the E.164 `to`.
- src/providers/blooio.ts: only decode genuine `message_type: imessage_app`
  balloons via `data.imessage_app.url` (+ looksLikeGamePigeon), so a text that
  merely quotes a GamePigeon URL is ignored.
- README.md: make the receive snippet runnable (define `app`/`BOT`) and point
  to the signed example; drop pricing/sales copy for a neutral note.
@davext

davext commented Sep 15, 2026

Copy link
Copy Markdown
Author

@time-attack pushed a fix commit (24909c6) addressing all 8 cubic findings:

examples/blooio-express-bot.ts

  • P1 unsigned /webhook → now verifies X-Blooio-Signature (HMAC-SHA256 over ${t}.${rawBody}) using express.raw, with a 5-min replay guard and constant-time compare; rejects with 401 before doing anything.
  • P1 ack-after-send (dup replies) → acks 200 before the outbound call; the send is fire-and-forget with .catch logging.
  • P1 open /start → gated behind Authorization: Bearer $ADMIN_TOKEN (401 otherwise).
  • P2 /start missing to → validates E.164 and returns 400 instead of sending to "undefined".
  • P2 /start URL encoding → comment now shows ?to=%2B15551234567 and notes the destination must be URL-encoded.

src/providers/blooio.ts

  • P2 over-eager parsingfromWebhook now requires message_type === "imessage_app" and reads only data.imessage_app.url (+ looksLikeGamePigeon), so a text that merely quotes a GamePigeon URL is ignored.

README.md

  • P3 undefined app/BOT → snippet now defines both and links the signed example.
  • P3 promo copy → dropped the pricing/sales paragraph; kept a neutral one-liner.

npm test green (16/16, incl. the blooio: cases); tsc -p tsconfig.json shows only the pre-existing invite.ts diagnostic. Ready when you are.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread examples/blooio-express-bot.ts
Fail-closed is kept, but a missing signing secret now logs a loud startup
warning and a reason on each rejected /webhook, so a quickstart bot that
forgets the env var isn't left running-but-dead with no signal.
@davext

davext commented Sep 15, 2026

Copy link
Copy Markdown
Author

Pushed 4c0a440 for the follow-up P2: a missing BLOOIO_WEBHOOK_SECRET no longer fails silently. It still fails closed (401), but now logs a loud startup warning when the secret is unset, and logs a reason on each rejected /webhook (distinguishing "unset secret" from "bad/missing signature") — so a quickstart bot that forgot the env var gets a clear signal instead of running-but-dead. Tests still 16/16.

@davext

davext commented Sep 15, 2026

Copy link
Copy Markdown
Author

Should be good to go now @time-attack

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/blooio-express-bot.ts">

<violation number="1" location="examples/blooio-express-bot.ts:29">
P3: The new `console.warn` runs on every rejected POST, but `/webhook` is public and unauthenticated by design, so anyone can spam the endpoint and generate unbounded log output. The surrounding comment says the warning fires "on the first rejected request," but the code logs unconditionally, and in the unset-secret case it repeats a message the startup warning already printed. Throttle the per-request log (e.g., once per interval) or rely on the startup warning alone.</violation>

<violation number="2" location="examples/blooio-express-bot.ts:67">
P3: When a webhook is rejected because its `t=` timestamp is older than 300s, the new log reports 'invalid or missing X-Blooio-Signature' even though the signature is present and valid — only the timestamp expired. Rephrase the message to cover all rejection paths (e.g. 'missing, invalid, or expired signature') or have verifyBlooioSignature return a reason so the log distinguishes the expired-timestamp case.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// so a bot that forgets this env var would run but never reply. Warn at startup
// (and again on the first rejected request) instead of dying silently.
if (!WEBHOOK_SECRET) {
console.warn(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new console.warn runs on every rejected POST, but /webhook is public and unauthenticated by design, so anyone can spam the endpoint and generate unbounded log output. The surrounding comment says the warning fires "on the first rejected request," but the code logs unconditionally, and in the unset-secret case it repeats a message the startup warning already printed. Throttle the per-request log (e.g., once per interval) or rely on the startup warning alone.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/blooio-express-bot.ts, line 29:

<comment>The new `console.warn` runs on every rejected POST, but `/webhook` is public and unauthenticated by design, so anyone can spam the endpoint and generate unbounded log output. The surrounding comment says the warning fires "on the first rejected request," but the code logs unconditionally, and in the unset-secret case it repeats a message the startup warning already printed. Throttle the per-request log (e.g., once per interval) or rely on the startup warning alone.</comment>

<file context>
@@ -22,6 +22,17 @@ const bloo = new op.Blooio({
+// so a bot that forgets this env var would run but never reply. Warn at startup
+// (and again on the first rejected request) instead of dying silently.
+if (!WEBHOOK_SECRET) {
+  console.warn(
+    "[blooio] BLOOIO_WEBHOOK_SECRET is not set — every /webhook request will be " +
+      "rejected with 401 and the bot will never reply. Set it to your whsec_... " +
</file context>

if (!verifyBlooioSignature(req.body as Buffer, req.headers["x-blooio-signature"])) {
console.warn(
WEBHOOK_SECRET
? "[blooio] rejected /webhook: invalid or missing X-Blooio-Signature"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When a webhook is rejected because its t= timestamp is older than 300s, the new log reports 'invalid or missing X-Blooio-Signature' even though the signature is present and valid — only the timestamp expired. Rephrase the message to cover all rejection paths (e.g. 'missing, invalid, or expired signature') or have verifyBlooioSignature return a reason so the log distinguishes the expired-timestamp case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/blooio-express-bot.ts, line 67:

<comment>When a webhook is rejected because its `t=` timestamp is older than 300s, the new log reports 'invalid or missing X-Blooio-Signature' even though the signature is present and valid — only the timestamp expired. Rephrase the message to cover all rejection paths (e.g. 'missing, invalid, or expired signature') or have verifyBlooioSignature return a reason so the log distinguishes the expired-timestamp case.</comment>

<file context>
@@ -51,6 +62,11 @@ app.post(
     if (!verifyBlooioSignature(req.body as Buffer, req.headers["x-blooio-signature"])) {
+      console.warn(
+        WEBHOOK_SECRET
+          ? "[blooio] rejected /webhook: invalid or missing X-Blooio-Signature"
+          : "[blooio] rejected /webhook: BLOOIO_WEBHOOK_SECRET is unset (see startup warning)",
+      );
</file context>
Suggested change
? "[blooio] rejected /webhook: invalid or missing X-Blooio-Signature"
? "[blooio] rejected /webhook: X-Blooio-Signature missing, invalid, or expired (t > 300s old)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants