skills/jitsi-jaas/SKILL.md
Embed branded video calls via Jitsi as a Service (JaaS / 8x8) in a web app — server-minted RS256 JWT auth, the JitsiMeetExternalAPI embed, live transcription piped to your app, and webhooks for room/recording/transcription events. Use when adding video meetings, a video-viewing/consult feature, or a transcript-driven meeting copilot. Triggers on "add video calls", "Jitsi", "JaaS", "8x8 video", "embed a meeting", "live transcription in a call", "meeting copilot". NOT the free meet.jit.si (no keys, no transcription, no SLA) — that's only for a throwaway demo.
npx skillsauth add RonanCodes/ronan-skills jitsi-jaasInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Branded, authenticated video in your own app. JaaS (by 8x8, console at jaas.8x8.vc) is the production Jitsi: JWT-gated rooms, your branding, webhooks, and transcription. Use it over the free meet.jit.si whenever you need auth, transcription, or reliability — meet.jit.si is fine ONLY for a zero-setup demo where you just need a call on screen.
Priced per Monthly Active User (MAU = a unique endpoint that attended a meeting with ≥1 other person that month), plus usage add-ons.
| Plan | MAU included | Price | |---|---|---| | Dev | 25 | Free (the demo plan) | | Basic | 300 | $99/mo | | Standard | 1500 | $499/mo | | Business | 3000 | $999/mo | | Enterprise | custom | Contact sales |
Overage: $0.99 / MAU beyond the included count. Add-ons (same on all plans): real-time transcription $0.03/min, recordings $0.01/min, RTMP streaming $0.01/min, SIP in/out $0.06/min, PSTN inbound 2 MAU, PSTN outbound 1 MAU + $0.03/min. Taxes/fees on top.
For Makely: on the free Dev plan (25 MAU) but with a card on file, so it's no longer free-only: the 25 MAU are still free, but usage add-ons and overage now bill for real. The two that bite: real-time transcription $0.03/min (the copilot feature) and $0.99 per MAU over 25. A handful of demo viewings is cents; just don't leave transcription running on a busy account unmonitored. Prices from the console (2026-06-27); confirm at jaas.8x8.vc → My plan.
vpaas-magic-cookie-<hex>. Public-ish (goes in the room name + as JWT sub).vpaas-magic-cookie-<hex>/<shortid>. Goes in the JWT header kid..pk, PKCS#8 PEM -----BEGIN PRIVATE KEY-----) + its public key (.pub, uploaded to JaaS). You sign JWTs with the private key. Secret.Base64 the PEM so it survives a single-line env var:
JAAS_APP_ID=vpaas-magic-cookie-...
JAAS_KID=vpaas-magic-cookie-.../<shortid>
JAAS_PRIVATE_KEY_B64=<base64 of the .pk PEM>
NEXT_PUBLIC_JAAS_APP_ID=vpaas-magic-cookie-... # the embed needs the App ID client-side
B64=$(base64 -i key.pk | tr -d '\n'). Verify the key first: openssl rsa -in key.pk -noout -check.
Use jose. Header carries kid; claims aud:"jitsi", iss:"chat", sub:<appId>, room:"*" (or a specific room), and a context with the user + feature flags. Transcription is a feature flag.
import { SignJWT, importPKCS8 } from "jose";
export async function mintJaasToken({ name, email, moderator }: {...}) {
const pem = Buffer.from(process.env.JAAS_PRIVATE_KEY_B64!, "base64").toString("utf8");
const key = await importPKCS8(pem, "RS256");
const now = Math.floor(Date.now()/1000);
return new SignJWT({
aud:"jitsi", iss:"chat", sub:process.env.JAAS_APP_ID!, room:"*",
context:{ user:{ name, email, moderator: moderator?"true":"false" },
features:{ transcription:"true", recording:"false" } },
}).setProtectedHeader({ alg:"RS256", kid:process.env.JAAS_KID!, typ:"JWT" })
.setIssuedAt(now).setExpirationTime(now+3*3600).setNotBefore(now-10).sign(key);
}
Common 401 causes: wrong kid (must be the full appId/shortid), sub not the App ID, public key not uploaded to JaaS, or signing with the wrong key. moderator/features must be strings ("true"), not booleans.
Domain is 8x8.vc; room name is <appId>/<room>; load the app-scoped script. Client component:
const s = document.createElement("script");
s.src = `https://8x8.vc/${appId}/external_api.js`; // app-scoped, not the generic one
s.onload = () => {
const api = new window.JitsiMeetExternalAPI("8x8.vc", {
roomName: `${appId}/${room}`, jwt, parentNode: el,
configOverwrite: { prejoinPageEnabled: false },
});
api.addListener("transcriptionChunkReceived", d => onTranscript(d.messageText ?? d.final ?? ""));
};
Always api.dispose() on unmount. A strict CSP must allow https://8x8.vc for script + frame.
With transcription:"true" in the JWT, the External API emits transcriptionChunkReceived as people speak. Pipe that text into your app (e.g. an LLM that ticks off covered questions / surfaces the next one). This is the reason to use JaaS over the free instance: the transcript is actually available to your code. (Recording + transcription storage are also deliverable via webhooks.)
Console → Webhooks → Add endpoint (https://<app>/api/jaas/webhook). Events include room created/destroyed, participant joined/left, recording uploaded, transcription ready. Verify the signature per the JaaS docs, then update your DB (e.g. mark a viewing attended, store a transcript URL). Set them up only when you need server-side reactions; the in-call External API events cover the live copilot without webhooks.
openssl ... -check passes.kid = full appId/shortid, sub = appId, string feature flags.8x8.vc, room <appId>/<room>, app-scoped external_api.js, dispose() on unmount.transcriptionChunkReceived wired if you need the transcript.meet.jit.si only as a keyless demo path.testing
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".