Dev mode — no Cloudflare Access header present. Authenticated as dev@local.
EPL
dev@local

Lab 11 · Day 2, Session 4

ChatOps EmojiChef

Duration: 60 minutes

Not started
Environment:

Lab 11 — ChatOps EmojiChef

Duration: 60 minutes

Operators do not type commands in plaintext. They post what looks like a food appreciation thread in a GitHub issue, and the Worker decodes it into an authenticated command that gets dispatched to the correct device. This is EmojiChef: a steganographic encoding scheme that maps ASCII text to Cloudflare’s food emoji range (U+1F345 through U+1F37F) using a 6-bit-per-emoji quasi-base64 alphabet.

In this lab you configure a GitHub repository webhook that fires at your Worker, verify the HMAC-SHA256 signature, decode the emoji payload, and wire the decoded command into the KV job queue from Lab 10. When it works, posting @alpha 🥘🥫🥩🌯🥙🥘 as a comment on issue #1 silently enqueues a status job and the Worker replies in the same thread.


Learning objectives

  • Understand the EmojiChef encoding scheme: base codepoint, 6-bit windows, byte assembly.
  • Verify GitHub webhook signatures with HMAC-SHA256 and the X-Hub-Signature-256 header.
  • Parse the GitHub issue comment event envelope, extract the command payload, and guard against reply loops.
  • Wire the decoded command into the Lab 10 job queue (enqueueJob / RATE_LIMITS KV).
  • Configure a GitHub repository webhook and a fine-grained PAT scoped to a single repo.

Pre-state

Before starting, confirm:

# Lab 10 validation passes (KV + R2 working)
bash courses/engagement-platform-labs/labs/lab10-kv-r2-storage/validate.sh

# DOMAIN is exported
echo "${DOMAIN}"

# wrangler is authenticated
npx wrangler whoami

You also need a GitHub account and a repository you control (your fork of epl-student-starter works; a fresh empty repo is fine too).

Codespace users: wrangler is pre-installed in the operator console. Run npx wrangler whoami to confirm your session is active. If it is not, run npx wrangler login again.


Where each step runs

This is a pure cloud lab. No bench hardware is involved.

StepOperator console
1-9. All stepsyes
Validationyes

Walkthrough

Open labs/lab07-first-worker/worker/src/index.js and read handleGithubChatops(). Key behaviors:

  • Signature verification: GitHub signs every request with HMAC-SHA256 using your webhook secret. The Worker reads X-Hub-Signature-256 and recomputes the digest over the raw request body. Requests with missing or mismatched signatures are rejected with HTTP 401.
  • Loop protection: The Worker drops any comment whose body starts with [eplabs:result] (its own replies) or that was authored by a bot user (sender.type === "Bot"). Duplicate X-GitHub-Delivery IDs within 600 seconds are also rejected.
  • Prefix gate: The comment body must start with @${STUDENT_SLOT} (the student’s slot, e.g. @alpha). The Worker strips the prefix before passing the remainder to EmojiChef.
  • Decode and dispatch: EmojiChefQuick.decode() converts the emoji string to ASCII. The first whitespace-delimited token is the command name; remaining tokens are args.
  • Command vocabulary: status, reboot, capture, list, ping, exec, fetch, and HSC are valid commands. Unknown commands return 422 with a list of known commands.
  • Result post: Uses the GitHub Issues API to post a reply on the same issue. The reply body begins with [eplabs:result] @${STUDENT_SLOT}.

Also open labs/lab07-first-worker/worker/src/emojichef.js and read the EmojiChefQuick class. The encoder and decoder are symmetric: each ASCII byte becomes 8 bits, the bitstream is cut into 6-bit windows, and each window value n maps to codepoint 0x1F345 + n.

Use the live widget below to encode and decode strings. Enter status, HSC, or reboot and copy the emoji output.

EmojiChef

Verify the known test vectors with Node if preferred:

cd courses/engagement-platform-labs/labs/lab07-first-worker/worker
node -e "
const BASE = 0x1F345;
const encode = t => [...t].reduce((b, c) => b + c.charCodeAt(0).toString(2).padStart(8,'0'), '')
    .match(/.{6}/g).map(s => String.fromCodePoint(BASE + parseInt(s,2))).join('');
const decode = e => [...e].map(c => (c.codePointAt(0) - BASE).toString(2).padStart(6,'0')).join('')
    .match(/.{8}/g).map(s => String.fromCharCode(parseInt(s,2))).join('');
['HSC','status','reboot'].forEach(t => console.log(t, '->', encode(t), '-> decoded:', decode(encode(t))));
"

Expected output:

HSC -> 🍗🍊🍒🍈 -> decoded: HSC
status -> 🥘🥫🥩🌯🥙🥘 -> decoded: status
reboot -> 🍱🥤🥩🥓🥨🥯🌯 -> decoded: reboot

The full set of test vectors is in test-vectors.txt in this lab directory.

The Worker reads GITHUB_OWNER, GITHUB_REPO, and GITHUB_ISSUE_NUMBER from its [vars] to know where to post replies. Issue #1 in your repo is the canonical command queue for this lab.

  1. Go to your workshop GitHub repo (fork of epl-student-starter or a fresh repo). If you do not have one, create a new empty public repo now.

  2. Open the Issues tab and click New issue.

  3. Title it EPL Command Queue (or anything recognizable). Click Submit new issue.

  4. Note the issue number in the URL. It should be #1 if this is the first issue in the repo.

Note your values for the next step:

GITHUB_OWNER=<your-github-username>
GITHUB_REPO=<your-repo-name>
GITHUB_ISSUE_NUMBER=1

The Worker needs a token to post replies. Generate a fine-grained token scoped to Issues on this one repo only. See github-issue-chatops-setup.md in this lab directory for the full click-by-click walkthrough; the abbreviated path is:

  1. GitHub Settings (avatar menu, top right).
  2. Developer settings (bottom of the left sidebar).
  3. Personal access tokens > Fine-grained tokens > Generate new token.
  4. Token name: eplabs-chatops-<your-slot>.
  5. Expiration: 30 days (or workshop duration).
  6. Repository access: “Only select repositories” > choose your workshop repo.
  7. Permissions > Repository permissions > Issues: set to Read and write.
  8. Leave all other permissions at their defaults (no access).
  9. Click Generate token and copy the token immediately (you will not see it again).

Keep the token value; you will need it in the next step.

Generate a random webhook secret:

openssl rand -hex 32
# or:
python3 -c 'import secrets; print(secrets.token_hex(32))'

Copy the output. This is your GITHUB_WEBHOOK_SECRET. Store it somewhere safe for this lab session. You will paste it into both the Worker and the GitHub webhook configuration.

Set the secrets:

cd courses/engagement-platform-labs/labs/lab07-first-worker/worker

npx wrangler secret put GITHUB_WEBHOOK_SECRET
# paste the 64-char hex string at the prompt

npx wrangler secret put GITHUB_TOKEN
# paste the fine-grained PAT from step 4

Open worker/wrangler.toml and add the [vars] entries for your repo:

[vars]
# ... existing vars ...
GITHUB_OWNER = "your-github-username"
GITHUB_REPO = "your-repo-name"
GITHUB_ISSUE_NUMBER = "1"
STUDENT_SLOT = "alpha"

Replace alpha with your assigned slot (e.g. bravo, charlie).

Redeploy the Worker so the new vars and secrets are active:

npx wrangler deploy

In your GitHub repository:

  1. Go to Settings > Webhooks > Add webhook.
  2. Payload URL: https://api.${DOMAIN}/v1/chatops/github (substitute your actual domain).
  3. Content type: application/json.
  4. Secret: paste the hex string you generated in step 5.
  5. Under Which events would you like to trigger this webhook? select “Let me select individual events”. Uncheck Pushes, then check Issue comments only.
  6. Make sure Active is checked.
  7. Click Add webhook.

GitHub immediately sends a ping event. Open Recent Deliveries on the webhook settings page. You should see the ping with HTTP 204 (the Worker ignores pings) or HTTP 401 (bad secret: recheck the hex you pasted).

See github-issue-chatops-setup.md in this lab directory for the full walkthrough including screenshots.

Start tailing Worker logs in a terminal:

cd courses/engagement-platform-labs/labs/lab07-first-worker/worker
npx wrangler tail --format pretty

In a second terminal (or a browser), post a comment on your issue #1:

@alpha 🥘🥫🥩🌯🥙🥘

Replace @alpha with your actual STUDENT_SLOT value.

In the wrangler tail output you should see a log entry showing:

decoded: "status"   job_id: <uuid>   slot: alpha

The comment body 🥘🥫🥩🌯🥙🥘 decodes to the ASCII string status.

You can also post the comment via the GitHub API:

curl -s -X POST \
  -H "Authorization: Bearer ${GITHUB_TOKEN}" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues/${GITHUB_ISSUE_NUMBER}/comments" \
  -d '{"body":"@alpha 🥘🥫🥩🌯🥙🥘"}'

(Replace the emoji unicode escapes with the actual emoji string 🥘🥫🥩🌯🥙🥘 when typing in a shell that supports it.)

Reload your GitHub issue page. The Worker should have posted a reply comment whose body starts with:

[eplabs:result] @alpha status: queued, job_id: <uuid>

Confirm the job is also in KV:

JOB_ID="paste-job-id-here"
curl -s "https://api.${DOMAIN}/v1/jobs/${JOB_ID}" | jq .

Expected:

{
  "job_id": "...",
  "device_id": "broadcast",
  "command": "status",
  "params": { "args": [], "raw": "status" },
  "status": "queued",
  "created_at": "...",
  "timeout": 60,
  "source": "github_chatops",
  "author": "<your-github-username>"
}

Try all three known test vectors from issue #1:

Comment bodyDecoded command
@alpha 🍗🍊🍒🍈HSC
@alpha 🥘🥫🥩🌯🥙🥘status
@alpha 🍱🥤🥩🥓🥨🥯🌯reboot

Each should produce a separate [eplabs:result] reply and a KV job entry.

chmod +x courses/engagement-platform-labs/labs/lab11-chatops-emojichef/validate.sh
export DOMAIN="<your-domain>"
# GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO, GITHUB_ISSUE_NUMBER, STUDENT_SLOT
# are read from the environment or from wrangler.toml [vars]
courses/engagement-platform-labs/labs/lab11-chatops-emojichef/validate.sh

The script posts a fresh @<slot> 🥘🥫🥩🌯🥙🥘 comment using your GITHUB_TOKEN, then polls the issue for the bot’s reply for up to 30 seconds. It asserts that the reply body starts with [eplabs:result] @<slot> and that the decoded command is status.


Post-state

When this lab is complete:

  • GITHUB_WEBHOOK_SECRET and GITHUB_TOKEN are set as Worker secrets.
  • wrangler.toml [vars] contains GITHUB_OWNER, GITHUB_REPO, GITHUB_ISSUE_NUMBER, and STUDENT_SLOT.
  • The GitHub webhook fires on issue comment events and shows HTTP 204 or 200 in GitHub’s Recent Deliveries panel.
  • Posting @<slot> 🥘🥫🥩🌯🥙🥘 on issue #1 produces a [eplabs:result] reply and a KV job with command: "status".
  • validate.sh exits 0.

Validation

Three paths. All call the same validate.sh; only the delivery channel differs.

Path 1: epl CLI (recommended from the operator console).

epl validate lab11-chatops-emojichef

Path 2: direct script.

bash courses/engagement-platform-labs/labs/lab11-chatops-emojichef/validate.sh

Path 3: paste output into the widget.

Run validate.sh anywhere, copy the full terminal output, and paste it below.

Validate Output Paste your validate.sh output below

Troubleshooting

GitHub webhook shows HTTP 401 “Unauthorized” in Recent Deliveries

The HMAC-SHA256 signature did not match. The most common cause is a mismatch between the secret you typed into the GitHub webhook configuration and the GITHUB_WEBHOOK_SECRET value in the Worker.

Steps to fix:

  1. Generate a fresh secret with openssl rand -hex 32.
  2. Run npx wrangler secret put GITHUB_WEBHOOK_SECRET and paste the new value.
  3. Run npx wrangler deploy to push the updated secret.
  4. Edit the GitHub webhook (Settings > Webhooks > your webhook > Edit) and paste the same new value into the Secret field.
  5. Click Update webhook. GitHub sends a new ping; check Recent Deliveries.
GitHub webhook shows HTTP 204 for the ping but no response for issue comments

The Worker returns 204 for ping events (the action field is absent on ping payloads). If issue comment events are also returning 204, the event filter may not be set correctly.

  • Re-open the webhook settings and confirm Issue comments is the only checked event.
  • Check that the comment body starts with @<STUDENT_SLOT> (with a trailing space). The prefix gate rejects comments that do not start with the correct slot marker.
Worker posts a reply but then immediately processes it again (loop)

The loop-protection sentinel [eplabs:result] must appear at the very start of the Worker’s reply body. Check your Worker source for the handleGithubChatops reply format. The reply body must begin with [eplabs:result]; any whitespace before it breaks the guard.

validate.sh times out waiting for the bot reply

The Worker may be slow on the first invocation after a cold start. Run the script again; subsequent runs should be faster.

If the timeout repeats:

  • Confirm GITHUB_TOKEN in the Worker secrets has Issues write permission on the correct repo.
  • Check wrangler tail for errors in the reply POST.
  • Confirm GITHUB_OWNER, GITHUB_REPO, and GITHUB_ISSUE_NUMBER in wrangler.toml [vars] match the repo where you are posting comments.
POST /v1/chatops/github returns 422 “Unknown command”

The emoji string decoded to a command name not in the vocabulary. Use the EmojiChef widget above to verify your encoding before posting. The vocabulary is: status, reboot, capture, list, ping, exec, fetch, HSC.


Primitives and drop-in substitutes

The observable behavior of this lab is an asynchronous out-of-band command queue with built-in audit trail: an operator posts a message to an external service; the Worker receives it, authenticates the source, decodes the payload, enqueues a job, and posts a result back to the same thread.

The transport layer is the only thing that changes between systems. The EmojiChef encoder, KV job queue, Tailnet dispatch, and R2 signed URL mechanisms are identical regardless of which transport you use.

TransportAuthenticationProtocolInvocation form
GitHub Issues (this lab)HMAC-SHA256 webhook secretHTTPS POSTIssue comment starting with @<slot>
Discord (take-home)Ed25519 application public keyHTTPS POSTSlash command or message interaction
SlackSigning secret + timestampHTTPS POSTSlash command or app mention
MattermostToken in header or signingHTTPS POSTSlash command
MatrixAccess tokenHTTPS PUTRoom message event
TelegramBot API token in URL pathHTTPS POSTWebhook update
MQTT brokerTLS client cert or username/passwordMQTT 3.1.1 / 5.0Topic publish

Switching transports requires changing only the webhook handler and the secrets. No Worker route logic, no job-queue shape, no audit-log schema changes.


Further reading


Take-home extension

The Discord variant of this lab uses the same EmojiChef encoder and KV job queue. Only the transport changes: Ed25519 application key verification instead of HMAC-SHA256, and Discord slash command interaction bodies instead of GitHub issue comment events.

See take-home/discord/README.md for the full framing and setup guide.


Validate output paster — available in Wave 2D (ValidateOutputPaster lab="lab11")
Downloadable artifacts for lab11 — served from R2 after Wave 3B deployment