@mushi-mushi/plugin-msteams 0.1.1

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.
@@ -0,0 +1,51 @@
1
+ <!--
2
+ AUTO-SYNCED from repo root by scripts/sync-community-files.mjs.
3
+ Do not edit here — edit the canonical file at the repository root and
4
+ re-run `node scripts/sync-community-files.mjs` (pre-commit hook does this
5
+ automatically).
6
+ -->
7
+
8
+ # Contributor Covenant Code of Conduct
9
+
10
+ ## Our Pledge
11
+
12
+ We as members, contributors, and leaders pledge to make participation in our
13
+ community a harassment-free experience for everyone, regardless of age, body
14
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
15
+ identity and expression, level of experience, education, socio-economic status,
16
+ nationality, personal appearance, race, caste, color, religion, or sexual
17
+ identity and orientation.
18
+
19
+ We pledge to act and interact in ways that contribute to an open, welcoming,
20
+ diverse, inclusive, and healthy community.
21
+
22
+ ## Our Standards
23
+
24
+ Examples of behavior that contributes to a positive environment:
25
+
26
+ - Using welcoming and inclusive language
27
+ - Being respectful of differing viewpoints and experiences
28
+ - Gracefully accepting constructive criticism
29
+ - Focusing on what is best for the community
30
+ - Showing empathy towards other community members
31
+
32
+ Examples of unacceptable behavior:
33
+
34
+ - The use of sexualized language or imagery, and sexual attention or advances of any kind
35
+ - Trolling, insulting or derogatory comments, and personal or political attacks
36
+ - Public or private harassment
37
+ - Publishing others' private information without explicit permission
38
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
43
+ reported to the project team at **kensaurus@gmail.com**.
44
+
45
+ All complaints will be reviewed and investigated promptly and fairly.
46
+
47
+ ## Attribution
48
+
49
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
50
+ version 2.1, available at
51
+ https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
@@ -0,0 +1,126 @@
1
+ <!--
2
+ AUTO-SYNCED from repo root by scripts/sync-community-files.mjs.
3
+ Do not edit here — edit the canonical file at the repository root and
4
+ re-run `node scripts/sync-community-files.mjs` (pre-commit hook does this
5
+ automatically).
6
+ -->
7
+
8
+ # Contributing to Mushi Mushi
9
+
10
+ Thanks for wanting to help. Here's everything you need to get started.
11
+
12
+ ## Prerequisites
13
+
14
+ - **Node.js >= 22** (see `.node-version`)
15
+ - **pnpm >= 10** — install with `corepack enable`
16
+
17
+ ## Setup
18
+
19
+ ```bash
20
+ git clone https://github.com/kensaurus/mushi-mushi.git
21
+ cd mushi-mushi
22
+ pnpm install
23
+ pnpm build
24
+ ```
25
+
26
+ ## Development
27
+
28
+ ```bash
29
+ pnpm dev # Start all dev servers (admin on :6464)
30
+ pnpm test # Run Vitest across all packages
31
+ pnpm typecheck # TypeScript checks
32
+ pnpm lint # ESLint
33
+ pnpm format # Prettier
34
+ ```
35
+
36
+ Ad-hoc screenshots captured during UI reviews can live temporarily at the repo
37
+ root, but root-level `*.png` files are intentionally ignored. Canonical
38
+ screenshots that should be versioned belong under `docs/screenshots/`.
39
+
40
+ ### Working on a single package
41
+
42
+ ```bash
43
+ cd packages/core
44
+ pnpm dev # Watch mode
45
+ pnpm test # Tests for this package only
46
+ ```
47
+
48
+ ## Project Structure
49
+
50
+ ```
51
+ packages/
52
+ core/ Types, API client, offline queue (MIT)
53
+ web/ Browser SDK — widget, capture (MIT)
54
+ react/ React bindings (MIT)
55
+ vue/ Vue 3 plugin (MIT)
56
+ svelte/ Svelte SDK (MIT)
57
+ angular/ Angular SDK (MIT)
58
+ react-native/ React Native SDK (MIT)
59
+ cli/ CLI tool (MIT)
60
+ mcp/ MCP server for coding agents (MIT)
61
+ server/ Supabase Edge Functions (BSL)
62
+ agents/ Agentic fix pipeline (BSL)
63
+ verify/ Fix verification (BSL)
64
+ apps/
65
+ admin/ Admin dashboard (React + Tailwind)
66
+ docs/ Documentation site (planned)
67
+ tooling/
68
+ eslint-config/ Shared ESLint flat config
69
+ tsconfig/ Shared TypeScript configs
70
+ ```
71
+
72
+ ## Making Changes
73
+
74
+ 1. Create a feature branch from `master`
75
+ 2. Make your changes
76
+ 3. Add tests for new functionality
77
+ 4. Run `pnpm typecheck && pnpm lint && pnpm test` to verify
78
+ 5. Create a changeset if your change affects published packages:
79
+ ```bash
80
+ pnpm changeset
81
+ ```
82
+ 6. Open a pull request
83
+
84
+ ## Changesets
85
+
86
+ We use [Changesets](https://github.com/changesets/changesets) for versioning. If your PR modifies a published package (`core`, `web`, `react`, `vue`, `svelte`, `angular`, `react-native`, `cli`, `mcp`), add a changeset:
87
+
88
+ ```bash
89
+ pnpm changeset
90
+ ```
91
+
92
+ Select the affected packages, the semver bump type, and write a summary. The changeset file gets committed with your PR.
93
+
94
+ ## Code Style
95
+
96
+ - **TypeScript strict mode** — no `any` unless absolutely necessary
97
+ - **Prettier** formats everything — run `pnpm format` before committing
98
+ - **ESLint** catches bugs — `pnpm lint` must pass
99
+ - **No default exports** in library packages — use named exports
100
+ - **Dual ESM/CJS** builds via tsup for all SDK packages
101
+
102
+ ## Commit Messages
103
+
104
+ Use conventional commits:
105
+
106
+ ```
107
+ feat(core): add batch report submission
108
+ fix(web): prevent widget from opening during screenshot
109
+ docs(react): update provider usage example
110
+ chore: bump dependencies
111
+ ```
112
+
113
+ ## Tests
114
+
115
+ - **Framework:** Vitest
116
+ - **Location:** Co-located with source (`src/foo.test.ts`)
117
+ - **Coverage:** Required for `core`, `web`, `react` — encouraged for all packages
118
+
119
+ ## License
120
+
121
+ - SDK packages are MIT — your contributions will be MIT-licensed
122
+ - Server/agents/verify are BSL 1.1 — contributions to those packages fall under BSL
123
+
124
+ ## Questions?
125
+
126
+ Open an issue or start a discussion. We're happy to help.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kenji Sakuramoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # `@mushi-mushi/plugin-msteams`
2
+
3
+ Mushi Mushi plugin: post Adaptive Card 1.4 notifications to a Microsoft
4
+ Teams Incoming Webhook for the major report lifecycle events.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm i @mushi-mushi/plugin-msteams
10
+ ```
11
+
12
+ ## Run as a stand-alone server
13
+
14
+ ```bash
15
+ MUSHI_PLUGIN_SECRET=... # set when the plugin is installed in Mushi admin
16
+ TEAMS_WEBHOOK_URL=https://... # Microsoft Teams Incoming Webhook URL
17
+ ADMIN_BASE_URL=https://... # Mushi admin base URL (deep-links)
18
+ PORT=3000 # optional
19
+ npx mushi-plugin-msteams
20
+ ```
21
+
22
+ Install **Microsoft Teams** in the Mushi admin Marketplace and point its
23
+ `webhook_url` at `https://your-host/mushi/webhook`.
24
+
25
+ ## Programmatic usage
26
+
27
+ ```ts
28
+ import { createMsTeamsPlugin } from '@mushi-mushi/plugin-msteams'
29
+ import express from 'express'
30
+ import { expressMiddleware } from '@mushi-mushi/plugin-sdk'
31
+
32
+ const handler = createMsTeamsPlugin({
33
+ webhookUrl: process.env.TEAMS_WEBHOOK_URL!,
34
+ adminBaseUrl: process.env.ADMIN_BASE_URL!,
35
+ mushiSecret: process.env.MUSHI_PLUGIN_SECRET!,
36
+ })
37
+
38
+ express().post('/mushi/webhook', expressMiddleware(handler)).listen(3000)
39
+ ```
40
+
41
+ ## Subscribed events
42
+
43
+ - `report.classified` — Adaptive Card with severity-coloured accent.
44
+ - `fix.proposed` — Adaptive Card with PR link if available.
45
+ - `fix.applied` — green confirmation card.
46
+
47
+ ## Notes
48
+
49
+ - Adaptive Card schema version `1.4` so cards render correctly in modern
50
+ Teams clients (desktop ≥ 1.5.x, mobile ≥ April 2024 release).
51
+ - All webhook calls retry transient `5xx` via `withRetry` from
52
+ `@mushi-mushi/plugin-sdk`.
53
+
54
+ ## License
55
+
56
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,287 @@
1
+ <!--
2
+ AUTO-SYNCED from repo root by scripts/sync-community-files.mjs.
3
+ Do not edit here — edit the canonical file at the repository root and
4
+ re-run `node scripts/sync-community-files.mjs` (pre-commit hook does this
5
+ automatically).
6
+ -->
7
+
8
+ # Security Policy
9
+
10
+ ## Supported Versions
11
+
12
+ | Version | Supported |
13
+ |---------|-----------|
14
+ | 0.x | Yes |
15
+
16
+ ## Reporting a Vulnerability
17
+
18
+ If you discover a security vulnerability, please report it responsibly.
19
+
20
+ **Do NOT open a public GitHub issue.**
21
+
22
+ Use either channel below:
23
+
24
+ 1. **GitHub Private Vulnerability Reporting** — strongly preferred.
25
+ <https://github.com/kensaurus/mushi-mushi/security/advisories/new>
26
+ Routes the report into a private advisory with built-in CVE issuance,
27
+ patch coordination, and contributor-credit workflow.
28
+ 2. **Email** — `kensaurus@gmail.com`, subject prefix `[mushi-security]`.
29
+ PGP welcome but not required.
30
+
31
+ Include:
32
+ - Description of the vulnerability
33
+ - Steps to reproduce (smallest reproducer wins)
34
+ - Impact assessment (what an attacker gains)
35
+ - Suggested fix (if any)
36
+ - Whether you want public credit (and how to spell your name)
37
+
38
+ ### Coordinated-disclosure timeline
39
+
40
+ | Day | Action |
41
+ |-----|--------|
42
+ | 0 | Report received |
43
+ | ≤ 2 | Acknowledgment + assigned a tracking ID |
44
+ | ≤ 7 | Triage complete: severity assigned (CVSS 3.1) and target patch date communicated |
45
+ | ≤ 30 | Patch released for critical / high (CVSS ≥ 7.0); ≤ 60 days for medium; best-effort for low |
46
+ | Patch + 7 | Public advisory + CVE published; reporter credited unless they declined |
47
+ | Patch + 90 | Embargo expires regardless; if upstream is unresponsive, the reporter is free to publish |
48
+
49
+ ### Safe harbor
50
+
51
+ Good-faith security research on Mushi Mushi is welcome. If you stay
52
+ within the rules below, we will not pursue legal action, will not ask
53
+ your hosting provider to take you offline, and will publicly credit your
54
+ work:
55
+
56
+ - Test only against your own self-hosted instance, the public demo at
57
+ <https://kensaur.us/mushi-mushi/admin/>, or accounts you own.
58
+ - Do not access, exfiltrate, or modify data belonging to other users.
59
+ - Do not run automated scanning that affects availability for others
60
+ (rate-limit your tooling, exclude `/health`).
61
+ - Disclose privately first (channels above); do not publish before the
62
+ embargo above expires.
63
+ - Do not intentionally exploit a finding to escalate beyond proving it
64
+ exists.
65
+
66
+ If a finding requires touching production data to confirm, **stop and
67
+ ask first** — describe what you'd need to do and we'll spin up a sandbox.
68
+
69
+ ### Hall of fame
70
+
71
+ Researchers who report a confirmed vulnerability are credited in the
72
+ release notes for the patched version and added to
73
+ [`docs/SECURITY_HALL_OF_FAME.md`](./docs/SECURITY_HALL_OF_FAME.md) (with
74
+ permission).
75
+
76
+ ## Scope
77
+
78
+ - All `@mushi-mushi/*` npm packages
79
+ - Supabase Edge Functions (server-side)
80
+ - Admin console application
81
+ - CLI tool
82
+
83
+ ## Out of Scope
84
+
85
+ - Self-hosted deployments configured by the user
86
+ - Third-party integrations (Jira, Linear, PagerDuty)
87
+ - Vulnerabilities requiring physical access
88
+
89
+ ## Security Best Practices for Users
90
+
91
+ - **Never commit your API keys** — use environment variables
92
+ - **Rotate API keys** regularly via the admin console
93
+ - **Enable SSO** for team projects (Enterprise tier)
94
+ - **Review audit logs** periodically for suspicious activity
95
+ - **Verify SDK integrity** with `npm audit signatures` after install
96
+ - **Set `Content-Security-Policy`** on any page embedding the Mushi widget;
97
+ the widget itself ships with `script-src 'self'` and does not load
98
+ remote scripts.
99
+
100
+ ## Threat model
101
+
102
+ What we treat as in-scope attacker capabilities, and what we don't.
103
+
104
+ | Capability | In scope | Notes |
105
+ |-----------|----------|-------|
106
+ | Unauthenticated network attacker hitting public endpoints | ✅ | Rate-limit + HMAC + replay protection on every webhook endpoint (`packages/server/supabase/functions/_shared/webhook-middleware.ts`). |
107
+ | Authenticated user trying to read another tenant's data | ✅ | Postgres RLS on every `public.*` table; advisor lints reviewed monthly. |
108
+ | Authenticated user trying to escalate to super-admin | ✅ | Role lives in `auth.users.raw_app_meta_data.role`; cannot be self-edited via PostgREST. |
109
+ | Compromised dependency (npm supply-chain attack) | ✅ | 7-day cooldown + provenance + Harden-Runner + pinned SHAs (see "Supply-chain hardening" below). |
110
+ | Stolen API key | ✅ | Per-key scopes (`api_key_has_scope`), revocation via admin console, audit log of every use. |
111
+ | User pasting a Stripe / OpenAI / GitHub PAT into a bug report | ✅ | PII scrubber redacts ~15 vendor token formats client-side before the report leaves the device. Mirrors `packages/core/src/pii-scrubber.ts` across iOS, Android, Flutter, React Native. |
112
+ | Stolen end-user device with the SDK installed | ⚠️ partial | Offline queue is AsyncStorage / Keychain / SharedPreferences — no app-level encryption beyond the OS default. Reports waiting to flush are vulnerable to a forensic attacker. |
113
+ | Compromised Supabase service-role key | ❌ | Treated as a tier-0 incident; would require key rotation and audit-log forensics. Not defendable in software. |
114
+ | Compromise of `kensaurus@gmail.com` | ❌ | Treated as a project-fork event; downstream consumers should pin to the last known-good version and follow the new release channel. |
115
+ | Physical / OS-level attacker on an end-user device | ❌ | Out of scope. |
116
+ | Malicious fork using the Mushi name to ship malware | ❌ (technical) ✅ (legal) | The MIT/BSL grant lets the fork exist; the trademark policy (`TRADEMARK.md`) makes shipping it under the Mushi name an infringement we will pursue. |
117
+
118
+ ## Data handling and PII
119
+
120
+ ### What the SDK collects by default
121
+
122
+ | Field | Scope | PII risk |
123
+ |-------|-------|----------|
124
+ | URL / route the user was on | Always | Low — strip query strings if your routes encode user IDs. |
125
+ | Browser / OS / device | Always | None |
126
+ | Console errors (last 50) | Opt-in via `captureConsole: true` | Medium — can include user data your code logs. |
127
+ | Network failures (URL + status) | Opt-in via `captureNetwork: true` | Medium — query params logged as-is unless you redact in-app. |
128
+ | User id / email / role | Only if you call `setUser()` | High — only set what you need; we do not auto-discover. |
129
+ | Session replay frames | Off by default | High — handled by the masking layer; passwords / cards / opted-out elements never leave the page. |
130
+ | Free-text bug description | Always | Medium — passed through the PII scrubber (see below). |
131
+
132
+ ### What the PII scrubber redacts before send
133
+
134
+ Implemented identically across `@mushi-mushi/core`, the iOS, Android,
135
+ Flutter, and React Native SDKs. Defaults are below — every category can
136
+ be toggled off, but `secretTokens` is on by default and we recommend
137
+ keeping it that way.
138
+
139
+ | Category | Default | Patterns |
140
+ |----------|---------|----------|
141
+ | `ssns` | on | `123-45-6789` |
142
+ | `creditCards` | on | 12–19 digit Luhn-shaped sequences with optional separators |
143
+ | `secretTokens` | on | AWS access key (`AKIA…` / `ASIA…`), AWS secret (`aws_secret_access_key=…`), Stripe (`sk_live_…`, `sk_test_…`, `rk_…`, `pk_…`), Slack (`xox[abpor]-…`), GitHub PAT (`ghp_…`, `github_pat_…`), OpenAI (`sk-…`, `sk-proj-…`), Anthropic (`sk-ant-…`), Google API (`AIza…`), JWT (`eyJ…` 3-segment) |
144
+ | `emails` | on | RFC-5322 lite |
145
+ | `phones` | on | E.164 with optional country code |
146
+ | `ipAddresses` | off | IPv4 (off because internal IPs are usually not PII and noise hurts triage) |
147
+ | `ipv6` | off | Same |
148
+
149
+ The fields scrubbed are:
150
+
151
+ - `description` — primary free-text field of every bug report
152
+ - `summary` — short summary, in the same composer
153
+ - `breadcrumbs[].message` — auto-captured user-action trail (clicks, route changes, console messages)
154
+
155
+ Structured fields you set explicitly (`metadata.userEmail`,
156
+ `metadata.userId`, etc.) are intentionally **not** scrubbed — those are
157
+ opt-in attribution data, and silently rewriting them would break
158
+ support workflows.
159
+
160
+ ### Where data lives
161
+
162
+ - **Reports & telemetry** — Supabase Postgres in the `us-west-1` region.
163
+ - **Session replays** — Supabase Storage, same region. Lifecycle policy
164
+ trims replays older than 30 days unless explicitly retained from the
165
+ admin console.
166
+ - **Inbound webhook bodies** — only a SHA-256 hash + `delivery_id` of
167
+ the body is persisted (`webhook_audit_log`). The full body is
168
+ processed in memory and discarded.
169
+ - **Outbound integrations** (Slack, Jira, …) — Mushi is a sender only;
170
+ the receiving system's retention applies.
171
+
172
+ ### Encryption
173
+
174
+ | Surface | At rest | In transit |
175
+ |---------|---------|------------|
176
+ | Postgres (Supabase) | AES-256 (Supabase default) | TLS 1.2+ |
177
+ | Supabase Storage (replays) | AES-256 | TLS 1.2+ |
178
+ | Edge Function ↔ Postgres | — | TLS via the Supavisor pooler |
179
+ | SDK ↔ ingest endpoint | — | TLS 1.2+ enforced; HSTS preload on `kensaur.us` |
180
+ | Inbound webhooks | — | TLS terminated at CloudFront / Supabase edge |
181
+ | Audit log integrity | append-only by RLS; no in-row signing | — |
182
+
183
+ ### Cryptographic primitives
184
+
185
+ | Use | Algorithm | Implementation |
186
+ |-----|-----------|---------------|
187
+ | Webhook HMAC verification (Sentry, GitHub, Datadog, Honeycomb, Grafana, Bugsnag, Rollbar, Crashlytics) | HMAC-SHA256, constant-time compare | Web Crypto in Deno; `crypto.subtle.timingSafeEqual` analogue |
188
+ | AWS SNS subscription confirmation | RSA-SHA1 / RSA-SHA256 | Deno `crypto.subtle.verify` with the cert from `SigningCertURL` (URL allow-listed to `*.sns.*.amazonaws.com`) |
189
+ | Opsgenie JWT shared-token | HS256 with `aud` claim verification | `jose` (Deno-compatible) |
190
+ | API-key hashing (database) | SHA-256 prefix + bcrypt secret half | `pgcrypto` |
191
+ | Provenance attestations (npm) | Sigstore (Fulcio + Rekor) | `npm publish --provenance` |
192
+
193
+ We deliberately do not roll our own crypto. If you find an algorithm or
194
+ library above that has been deprecated, please file a security advisory.
195
+
196
+ ### Operator security checklist
197
+
198
+ When you provision a new self-hosted Mushi instance:
199
+
200
+ - [ ] Set `auth_leaked_password_protection = true` in Supabase Auth
201
+ (HaveIBeenPwned blocklist; flagged as `auth_leaked_password_protection`
202
+ in the security advisor).
203
+ - [ ] Enable at least two MFA factors in Supabase Auth (`auth_insufficient_mfa_options`).
204
+ - [ ] Rotate the service-role key on day 1, then quarterly.
205
+ - [ ] Restrict Postgres direct connections to your CI / migration runners
206
+ via Supabase network restrictions.
207
+ - [ ] Run `pnpm dlx supabase advisors --project-ref <ref>` after every
208
+ migration; aim for zero ERROR-level findings.
209
+ - [ ] Configure a Supabase log drain to your SIEM if you are subject to
210
+ SOC 2 / ISO 27001.
211
+ - [ ] Set CSP `frame-ancestors` on the host page if you embed the Mushi
212
+ widget (the widget is iframe-friendly but does not enforce
213
+ framing constraints itself).
214
+
215
+ ## Supply-chain hardening (how this package is protected)
216
+
217
+ Mushi Mushi is built and published with the controls below. Consumers can
218
+ verify each control independently — the goal is to make tampering both
219
+ difficult and detectable.
220
+
221
+ ### Publish-time controls
222
+
223
+ | Control | What it does | How to verify |
224
+ |---|---|---|
225
+ | **npm Trusted Publisher (OIDC)** | Every release is published from `.github/workflows/release.yml` on `master` using a short-lived OIDC token. Long-lived `NPM_TOKEN` is not used for publishing. | `npm view @mushi-mushi/<pkg> --json` shows `"trustedPublisher"` populated for recent versions. |
226
+ | **npm provenance attestations** | Every published tarball ships a [Sigstore provenance attestation](https://docs.npmjs.com/generating-provenance-statements) cryptographically linking the tarball to the exact GitHub Actions run that built it. | `npm audit signatures` (run inside any project that depends on `@mushi-mushi/*`) reports `verified registry signatures` and `verified attestations`. The npm web UI shows a "Built and signed on GitHub Actions" badge on each version. |
227
+ | **Pre-publish workspace-protocol guard** | Aborts the publish if `workspace:*` ranges leaked into the tarball (the bug class behind the v0.1.0 incident). | `scripts/check-workspace-protocol.mjs` runs before `changeset publish` in `pnpm release`. |
228
+ | **Post-publish tarball verification** | Re-downloads each just-published tarball and asserts it doesn't contain `workspace:*`. | See the "Verify published tarballs do not contain workspace:*" step in `release.yml`. |
229
+ | **Post-publish `npm audit signatures`** | Re-installs each published version and validates registry signatures + provenance against npm's transparency log. | See the "Audit signatures of installed dependencies" step in `release.yml`. |
230
+
231
+ ### Build-time controls
232
+
233
+ | Control | What it does |
234
+ |---|---|
235
+ | **All third-party GitHub Actions pinned to commit SHAs** | Every `uses:` in every workflow under `.github/workflows/` is pinned to a 40-character commit SHA with a version comment. Floating tags (`@v4`, `@main`) are mutable and were the entry point for the [tj-actions/changed-files compromise (CVE-2025-30066)](https://github.com/step-security/harden-runner#detected-attacks). |
236
+ | **Harden-Runner egress audit on every job** | [step-security/harden-runner](https://github.com/step-security/harden-runner) records every outbound network call, file write, and process spawn on every CI runner. Detects exfiltration attempts in real time — caught the tj-actions, NX, Shai-Hulud, and Axios attacks for other projects. |
237
+ | **OpenSSF Scorecard** | Weekly + on-push score of the repo's security posture (Pinned-Dependencies, Token-Permissions, Branch-Protection, Code-Review, Dangerous-Workflow, Maintained, SAST, Security-Policy, Signed-Releases, Vulnerabilities). Public results at [scorecard.dev](https://scorecard.dev/viewer/?uri=github.com/kensaurus/mushi-mushi). |
238
+ | **Server-side secret scan (Gitleaks)** | Every PR and every push to `master` runs Gitleaks across the diff / full tree. Belt-and-suspenders to the local pre-commit hook (`scripts/check-no-secrets.mjs`) which can be bypassed with `--no-verify`. |
239
+ | **Local pre-commit secret scanner** | `scripts/check-no-secrets.mjs` runs as a git hook installed by `pnpm install`, blocking commits that look like AWS / Stripe / GitHub / Anthropic / OpenAI / Slack / Supabase keys. |
240
+ | **CodeQL `security-extended`** | Semantic analysis of every TypeScript / JavaScript change finds injection sinks, taint flows, prototype pollution, etc. Runs on every PR, push, and weekly cron. |
241
+ | **Dependency review on PRs** | `actions/dependency-review-action` blocks the PR if it adds or upgrades a dep with a high-severity advisory. |
242
+ | **`pnpm audit --prod --audit-level=high`** | Weekly cron + every push to `master` fails on any high/critical advisory in production deps. |
243
+
244
+ ### Install-time controls (protect the project's own dependency graph)
245
+
246
+ | Control | What it does |
247
+ |---|---|
248
+ | **`min-release-age` (npm) / `minimumReleaseAge` (pnpm)** | Refuses to resolve any dep version published less than 7 days ago. The Axios 1.14.1 / 0.30.4 compromise (Mar 2026) was detected and removed within ~5 hours; Shai-Hulud (Sep 2025) within <12 hours — a 7-day cooldown blocks every publicly-disclosed 2025–2026 npm supply-chain attack outright. |
249
+ | **`strictDepBuilds: true`** | Fails the install if any transitive dep tries to run a `postinstall` hook the workspace hasn't pre-approved (`onlyBuiltDependencies` allow-list). |
250
+ | **`blockExoticSubdeps: true`** | Refuses to resolve transitive deps from git URLs, tarball URLs, or filesystem paths — anything that didn't go through the npm registry's signing pipeline. |
251
+ | **Dependabot with cooldown** | Routine dep upgrades wait 7 days; security advisories bypass the cooldown automatically. |
252
+ | **`pnpm audit signatures`-style verification** | The release pipeline re-runs `npm audit signatures` against each published version after the publish, with `--audit-level=high`. |
253
+
254
+ ### Verifying a Mushi Mushi tarball before installing
255
+
256
+ ```bash
257
+ # 1. Check provenance attestation matches the public GitHub Actions run
258
+ npm view @mushi-mushi/core --json | jq '.signatures, .dist'
259
+
260
+ # 2. Inside your own project after install
261
+ npm audit signatures
262
+
263
+ # Expected: every @mushi-mushi/* package reports
264
+ # "verified registry signature"
265
+ # "verified attestation"
266
+ ```
267
+
268
+ If `npm audit signatures` reports any `@mushi-mushi/*` package as unsigned
269
+ or with an invalid attestation, **stop the install and email
270
+ kensaurus@gmail.com immediately** — that's the symptom of either a
271
+ registry compromise or a tampered tarball, and we want to know within
272
+ hours, not days.
273
+
274
+ ### What this hardening does NOT cover
275
+
276
+ - **Self-hosted deployments.** Once the package is on your machine, the
277
+ security of your `node_modules`, build pipeline, and runtime is your
278
+ responsibility. The hardening above protects the path from source to
279
+ registry; it cannot protect a tarball after it has been downloaded.
280
+ - **Compromise of `kensaurus@gmail.com`.** A trusted-publisher rule still
281
+ lets the legitimate maintainer publish from any branch they push. If
282
+ you find yourself with admin access to this repo, treat
283
+ `.github/workflows/release.yml` as a tier-0 secret.
284
+ - **First-party bugs.** Provenance proves *who* built the tarball and
285
+ *when*; it does not prove the code is bug-free. CodeQL + tests cover
286
+ that surface, but no automation catches everything — please continue
287
+ to report issues to the address above.
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import {
5
+ createPluginHandler
6
+ } from "@mushi-mushi/plugin-sdk";
7
+ var SEVERITY_COLORS = {
8
+ critical: "attention",
9
+ high: "warning",
10
+ medium: "accent",
11
+ low: "good"
12
+ };
13
+ function createMsteamsPlugin(cfg) {
14
+ const f = cfg.fetchImpl ?? fetch;
15
+ const adminBase = cfg.adminBaseUrl.replace(/\/$/, "");
16
+ function reportUrl(reportId) {
17
+ return `${adminBase}/reports/${encodeURIComponent(reportId)}`;
18
+ }
19
+ function buildPayload(body, actions = []) {
20
+ return {
21
+ type: "message",
22
+ attachments: [
23
+ {
24
+ contentType: "application/vnd.microsoft.card.adaptive",
25
+ content: {
26
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
27
+ type: "AdaptiveCard",
28
+ version: "1.4",
29
+ body,
30
+ actions
31
+ }
32
+ }
33
+ ]
34
+ };
35
+ }
36
+ async function post(payload) {
37
+ const res = await f(cfg.webhookUrl, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify(payload)
41
+ });
42
+ if (!res.ok) {
43
+ const text = await res.text().catch(() => "");
44
+ throw new Error(`Teams webhook ${res.status}: ${text.slice(0, 300)}`);
45
+ }
46
+ }
47
+ return createPluginHandler({
48
+ secret: cfg.mushiSecret,
49
+ on: {
50
+ "report.classified": async (e) => {
51
+ const data = e.data;
52
+ const { report, classification } = data;
53
+ const color = SEVERITY_COLORS[classification.severity] ?? "accent";
54
+ const body = [
55
+ {
56
+ type: "TextBlock",
57
+ size: "Large",
58
+ weight: "Bolder",
59
+ text: `New ${classification.category} report classified`,
60
+ wrap: true
61
+ },
62
+ {
63
+ type: "TextBlock",
64
+ text: report.title ?? `Mushi report \`${report.id.slice(0, 8)}...\``,
65
+ wrap: true,
66
+ spacing: "None"
67
+ },
68
+ {
69
+ type: "FactSet",
70
+ facts: [
71
+ { title: "Severity", value: classification.severity },
72
+ { title: "Category", value: classification.category },
73
+ { title: "Confidence", value: `${Math.round(classification.confidence * 100)}%` },
74
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` }
75
+ ]
76
+ },
77
+ {
78
+ type: "TextBlock",
79
+ text: e.occurredAt,
80
+ size: "Small",
81
+ color: "default",
82
+ isSubtle: true,
83
+ spacing: "Medium"
84
+ }
85
+ ];
86
+ await post(
87
+ buildPayload(
88
+ [
89
+ {
90
+ type: "Container",
91
+ style: color,
92
+ items: body
93
+ }
94
+ ],
95
+ [
96
+ {
97
+ type: "Action.OpenUrl",
98
+ title: "View in Mushi",
99
+ url: reportUrl(report.id)
100
+ }
101
+ ]
102
+ )
103
+ );
104
+ },
105
+ "fix.proposed": async (e) => {
106
+ const data = e.data;
107
+ const { report, fix } = data;
108
+ const facts = [
109
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` },
110
+ { title: "Fix status", value: fix.status }
111
+ ];
112
+ if (fix.branch) facts.push({ title: "Branch", value: fix.branch });
113
+ const actions = [
114
+ { type: "Action.OpenUrl", title: "View report", url: reportUrl(report.id) }
115
+ ];
116
+ if (fix.pullRequestUrl) {
117
+ actions.push({ type: "Action.OpenUrl", title: "View Pull Request", url: fix.pullRequestUrl });
118
+ }
119
+ await post(
120
+ buildPayload([
121
+ {
122
+ type: "Container",
123
+ style: "accent",
124
+ items: [
125
+ { type: "TextBlock", size: "Large", weight: "Bolder", text: "Fix proposed by Mushi", wrap: true },
126
+ {
127
+ type: "TextBlock",
128
+ text: fix.summary ?? `A fix has been proposed for report \`${report.id.slice(0, 8)}...\``,
129
+ wrap: true,
130
+ spacing: "None"
131
+ },
132
+ { type: "FactSet", facts }
133
+ ]
134
+ }
135
+ ], actions)
136
+ );
137
+ },
138
+ "fix.applied": async (e) => {
139
+ const data = e.data;
140
+ const { report, fix } = data;
141
+ const actions = [
142
+ { type: "Action.OpenUrl", title: "View report", url: reportUrl(report.id) }
143
+ ];
144
+ if (fix.pullRequestUrl) {
145
+ actions.push({ type: "Action.OpenUrl", title: "View merged PR", url: fix.pullRequestUrl });
146
+ }
147
+ await post(
148
+ buildPayload([
149
+ {
150
+ type: "Container",
151
+ style: "good",
152
+ items: [
153
+ { type: "TextBlock", size: "Large", weight: "Bolder", text: "Fix applied \u2014 issue resolved", wrap: true },
154
+ {
155
+ type: "TextBlock",
156
+ text: fix.summary ?? `Fix merged for report \`${report.id.slice(0, 8)}...\``,
157
+ wrap: true,
158
+ spacing: "None"
159
+ },
160
+ {
161
+ type: "FactSet",
162
+ facts: [
163
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` },
164
+ { title: "Fix status", value: fix.status }
165
+ ]
166
+ }
167
+ ]
168
+ }
169
+ ], actions)
170
+ );
171
+ }
172
+ },
173
+ logger: {
174
+ info: (msg, meta) => console.log(`[mushi-plugin-msteams] ${msg}`, meta ?? ""),
175
+ warn: (msg, meta) => console.warn(`[mushi-plugin-msteams] ${msg}`, meta ?? ""),
176
+ error: (msg, meta) => console.error(`[mushi-plugin-msteams] ${msg}`, meta ?? "")
177
+ }
178
+ });
179
+ }
180
+
181
+ export {
182
+ createMsteamsPlugin
183
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ createMsteamsPlugin: () => createMsteamsPlugin
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+ var import_plugin_sdk = require("@mushi-mushi/plugin-sdk");
28
+ var SEVERITY_COLORS = {
29
+ critical: "attention",
30
+ high: "warning",
31
+ medium: "accent",
32
+ low: "good"
33
+ };
34
+ function createMsteamsPlugin(cfg) {
35
+ const f = cfg.fetchImpl ?? fetch;
36
+ const adminBase = cfg.adminBaseUrl.replace(/\/$/, "");
37
+ function reportUrl(reportId) {
38
+ return `${adminBase}/reports/${encodeURIComponent(reportId)}`;
39
+ }
40
+ function buildPayload(body, actions = []) {
41
+ return {
42
+ type: "message",
43
+ attachments: [
44
+ {
45
+ contentType: "application/vnd.microsoft.card.adaptive",
46
+ content: {
47
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
48
+ type: "AdaptiveCard",
49
+ version: "1.4",
50
+ body,
51
+ actions
52
+ }
53
+ }
54
+ ]
55
+ };
56
+ }
57
+ async function post(payload) {
58
+ const res = await f(cfg.webhookUrl, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify(payload)
62
+ });
63
+ if (!res.ok) {
64
+ const text = await res.text().catch(() => "");
65
+ throw new Error(`Teams webhook ${res.status}: ${text.slice(0, 300)}`);
66
+ }
67
+ }
68
+ return (0, import_plugin_sdk.createPluginHandler)({
69
+ secret: cfg.mushiSecret,
70
+ on: {
71
+ "report.classified": async (e) => {
72
+ const data = e.data;
73
+ const { report, classification } = data;
74
+ const color = SEVERITY_COLORS[classification.severity] ?? "accent";
75
+ const body = [
76
+ {
77
+ type: "TextBlock",
78
+ size: "Large",
79
+ weight: "Bolder",
80
+ text: `New ${classification.category} report classified`,
81
+ wrap: true
82
+ },
83
+ {
84
+ type: "TextBlock",
85
+ text: report.title ?? `Mushi report \`${report.id.slice(0, 8)}...\``,
86
+ wrap: true,
87
+ spacing: "None"
88
+ },
89
+ {
90
+ type: "FactSet",
91
+ facts: [
92
+ { title: "Severity", value: classification.severity },
93
+ { title: "Category", value: classification.category },
94
+ { title: "Confidence", value: `${Math.round(classification.confidence * 100)}%` },
95
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` }
96
+ ]
97
+ },
98
+ {
99
+ type: "TextBlock",
100
+ text: e.occurredAt,
101
+ size: "Small",
102
+ color: "default",
103
+ isSubtle: true,
104
+ spacing: "Medium"
105
+ }
106
+ ];
107
+ await post(
108
+ buildPayload(
109
+ [
110
+ {
111
+ type: "Container",
112
+ style: color,
113
+ items: body
114
+ }
115
+ ],
116
+ [
117
+ {
118
+ type: "Action.OpenUrl",
119
+ title: "View in Mushi",
120
+ url: reportUrl(report.id)
121
+ }
122
+ ]
123
+ )
124
+ );
125
+ },
126
+ "fix.proposed": async (e) => {
127
+ const data = e.data;
128
+ const { report, fix } = data;
129
+ const facts = [
130
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` },
131
+ { title: "Fix status", value: fix.status }
132
+ ];
133
+ if (fix.branch) facts.push({ title: "Branch", value: fix.branch });
134
+ const actions = [
135
+ { type: "Action.OpenUrl", title: "View report", url: reportUrl(report.id) }
136
+ ];
137
+ if (fix.pullRequestUrl) {
138
+ actions.push({ type: "Action.OpenUrl", title: "View Pull Request", url: fix.pullRequestUrl });
139
+ }
140
+ await post(
141
+ buildPayload([
142
+ {
143
+ type: "Container",
144
+ style: "accent",
145
+ items: [
146
+ { type: "TextBlock", size: "Large", weight: "Bolder", text: "Fix proposed by Mushi", wrap: true },
147
+ {
148
+ type: "TextBlock",
149
+ text: fix.summary ?? `A fix has been proposed for report \`${report.id.slice(0, 8)}...\``,
150
+ wrap: true,
151
+ spacing: "None"
152
+ },
153
+ { type: "FactSet", facts }
154
+ ]
155
+ }
156
+ ], actions)
157
+ );
158
+ },
159
+ "fix.applied": async (e) => {
160
+ const data = e.data;
161
+ const { report, fix } = data;
162
+ const actions = [
163
+ { type: "Action.OpenUrl", title: "View report", url: reportUrl(report.id) }
164
+ ];
165
+ if (fix.pullRequestUrl) {
166
+ actions.push({ type: "Action.OpenUrl", title: "View merged PR", url: fix.pullRequestUrl });
167
+ }
168
+ await post(
169
+ buildPayload([
170
+ {
171
+ type: "Container",
172
+ style: "good",
173
+ items: [
174
+ { type: "TextBlock", size: "Large", weight: "Bolder", text: "Fix applied \u2014 issue resolved", wrap: true },
175
+ {
176
+ type: "TextBlock",
177
+ text: fix.summary ?? `Fix merged for report \`${report.id.slice(0, 8)}...\``,
178
+ wrap: true,
179
+ spacing: "None"
180
+ },
181
+ {
182
+ type: "FactSet",
183
+ facts: [
184
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` },
185
+ { title: "Fix status", value: fix.status }
186
+ ]
187
+ }
188
+ ]
189
+ }
190
+ ], actions)
191
+ );
192
+ }
193
+ },
194
+ logger: {
195
+ info: (msg, meta) => console.log(`[mushi-plugin-msteams] ${msg}`, meta ?? ""),
196
+ warn: (msg, meta) => console.warn(`[mushi-plugin-msteams] ${msg}`, meta ?? ""),
197
+ error: (msg, meta) => console.error(`[mushi-plugin-msteams] ${msg}`, meta ?? "")
198
+ }
199
+ });
200
+ }
201
+ // Annotate the CommonJS export names for ESM import in node:
202
+ 0 && (module.exports = {
203
+ createMsteamsPlugin
204
+ });
@@ -0,0 +1,33 @@
1
+ import * as _mushi_mushi_plugin_sdk from '@mushi-mushi/plugin-sdk';
2
+
3
+ /**
4
+ * Microsoft Teams plugin for Mushi Mushi.
5
+ *
6
+ * Posts Adaptive Cards to a Teams channel via an Incoming Webhook connector.
7
+ * Uses the legacy Incoming Webhook format (still supported in 2026) which
8
+ * wraps an Adaptive Card inside a `message` attachment envelope.
9
+ *
10
+ * Adaptive Card version: 1.4 — compatible with Teams desktop, web, and
11
+ * mobile clients that shipped after 2021.
12
+ *
13
+ * Events handled:
14
+ * - `report.classified` — new classified bug with severity badge
15
+ * - `fix.proposed` — Mushi has opened a fix PR
16
+ * - `fix.applied` — fix has been merged
17
+ *
18
+ * Auth: Teams Incoming Webhook URL (no additional token required beyond
19
+ * the URL itself, which embeds the connector auth).
20
+ */
21
+ interface MsteamsPluginConfig {
22
+ /** Microsoft Teams Incoming Webhook URL. */
23
+ webhookUrl: string;
24
+ /** Mushi admin base URL used to build deep-link report URLs. */
25
+ adminBaseUrl: string;
26
+ /** Mushi plugin signing secret. */
27
+ mushiSecret: string;
28
+ /** Override `fetch` for tests. */
29
+ fetchImpl?: typeof fetch;
30
+ }
31
+ declare function createMsteamsPlugin(cfg: MsteamsPluginConfig): (input: _mushi_mushi_plugin_sdk.HandlePluginRequestInput) => Promise<_mushi_mushi_plugin_sdk.HandlePluginResult>;
32
+
33
+ export { type MsteamsPluginConfig, createMsteamsPlugin };
@@ -0,0 +1,33 @@
1
+ import * as _mushi_mushi_plugin_sdk from '@mushi-mushi/plugin-sdk';
2
+
3
+ /**
4
+ * Microsoft Teams plugin for Mushi Mushi.
5
+ *
6
+ * Posts Adaptive Cards to a Teams channel via an Incoming Webhook connector.
7
+ * Uses the legacy Incoming Webhook format (still supported in 2026) which
8
+ * wraps an Adaptive Card inside a `message` attachment envelope.
9
+ *
10
+ * Adaptive Card version: 1.4 — compatible with Teams desktop, web, and
11
+ * mobile clients that shipped after 2021.
12
+ *
13
+ * Events handled:
14
+ * - `report.classified` — new classified bug with severity badge
15
+ * - `fix.proposed` — Mushi has opened a fix PR
16
+ * - `fix.applied` — fix has been merged
17
+ *
18
+ * Auth: Teams Incoming Webhook URL (no additional token required beyond
19
+ * the URL itself, which embeds the connector auth).
20
+ */
21
+ interface MsteamsPluginConfig {
22
+ /** Microsoft Teams Incoming Webhook URL. */
23
+ webhookUrl: string;
24
+ /** Mushi admin base URL used to build deep-link report URLs. */
25
+ adminBaseUrl: string;
26
+ /** Mushi plugin signing secret. */
27
+ mushiSecret: string;
28
+ /** Override `fetch` for tests. */
29
+ fetchImpl?: typeof fetch;
30
+ }
31
+ declare function createMsteamsPlugin(cfg: MsteamsPluginConfig): (input: _mushi_mushi_plugin_sdk.HandlePluginRequestInput) => Promise<_mushi_mushi_plugin_sdk.HandlePluginResult>;
32
+
33
+ export { type MsteamsPluginConfig, createMsteamsPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createMsteamsPlugin
4
+ } from "./chunk-NTWIK2O3.js";
5
+ export {
6
+ createMsteamsPlugin
7
+ };
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/server.ts
5
+ var import_node_http = require("http");
6
+
7
+ // src/index.ts
8
+ var import_plugin_sdk = require("@mushi-mushi/plugin-sdk");
9
+ var SEVERITY_COLORS = {
10
+ critical: "attention",
11
+ high: "warning",
12
+ medium: "accent",
13
+ low: "good"
14
+ };
15
+ function createMsteamsPlugin(cfg) {
16
+ const f = cfg.fetchImpl ?? fetch;
17
+ const adminBase = cfg.adminBaseUrl.replace(/\/$/, "");
18
+ function reportUrl(reportId) {
19
+ return `${adminBase}/reports/${encodeURIComponent(reportId)}`;
20
+ }
21
+ function buildPayload(body, actions = []) {
22
+ return {
23
+ type: "message",
24
+ attachments: [
25
+ {
26
+ contentType: "application/vnd.microsoft.card.adaptive",
27
+ content: {
28
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
29
+ type: "AdaptiveCard",
30
+ version: "1.4",
31
+ body,
32
+ actions
33
+ }
34
+ }
35
+ ]
36
+ };
37
+ }
38
+ async function post(payload) {
39
+ const res = await f(cfg.webhookUrl, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(payload)
43
+ });
44
+ if (!res.ok) {
45
+ const text = await res.text().catch(() => "");
46
+ throw new Error(`Teams webhook ${res.status}: ${text.slice(0, 300)}`);
47
+ }
48
+ }
49
+ return (0, import_plugin_sdk.createPluginHandler)({
50
+ secret: cfg.mushiSecret,
51
+ on: {
52
+ "report.classified": async (e) => {
53
+ const data = e.data;
54
+ const { report, classification } = data;
55
+ const color = SEVERITY_COLORS[classification.severity] ?? "accent";
56
+ const body = [
57
+ {
58
+ type: "TextBlock",
59
+ size: "Large",
60
+ weight: "Bolder",
61
+ text: `New ${classification.category} report classified`,
62
+ wrap: true
63
+ },
64
+ {
65
+ type: "TextBlock",
66
+ text: report.title ?? `Mushi report \`${report.id.slice(0, 8)}...\``,
67
+ wrap: true,
68
+ spacing: "None"
69
+ },
70
+ {
71
+ type: "FactSet",
72
+ facts: [
73
+ { title: "Severity", value: classification.severity },
74
+ { title: "Category", value: classification.category },
75
+ { title: "Confidence", value: `${Math.round(classification.confidence * 100)}%` },
76
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` }
77
+ ]
78
+ },
79
+ {
80
+ type: "TextBlock",
81
+ text: e.occurredAt,
82
+ size: "Small",
83
+ color: "default",
84
+ isSubtle: true,
85
+ spacing: "Medium"
86
+ }
87
+ ];
88
+ await post(
89
+ buildPayload(
90
+ [
91
+ {
92
+ type: "Container",
93
+ style: color,
94
+ items: body
95
+ }
96
+ ],
97
+ [
98
+ {
99
+ type: "Action.OpenUrl",
100
+ title: "View in Mushi",
101
+ url: reportUrl(report.id)
102
+ }
103
+ ]
104
+ )
105
+ );
106
+ },
107
+ "fix.proposed": async (e) => {
108
+ const data = e.data;
109
+ const { report, fix } = data;
110
+ const facts = [
111
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` },
112
+ { title: "Fix status", value: fix.status }
113
+ ];
114
+ if (fix.branch) facts.push({ title: "Branch", value: fix.branch });
115
+ const actions = [
116
+ { type: "Action.OpenUrl", title: "View report", url: reportUrl(report.id) }
117
+ ];
118
+ if (fix.pullRequestUrl) {
119
+ actions.push({ type: "Action.OpenUrl", title: "View Pull Request", url: fix.pullRequestUrl });
120
+ }
121
+ await post(
122
+ buildPayload([
123
+ {
124
+ type: "Container",
125
+ style: "accent",
126
+ items: [
127
+ { type: "TextBlock", size: "Large", weight: "Bolder", text: "Fix proposed by Mushi", wrap: true },
128
+ {
129
+ type: "TextBlock",
130
+ text: fix.summary ?? `A fix has been proposed for report \`${report.id.slice(0, 8)}...\``,
131
+ wrap: true,
132
+ spacing: "None"
133
+ },
134
+ { type: "FactSet", facts }
135
+ ]
136
+ }
137
+ ], actions)
138
+ );
139
+ },
140
+ "fix.applied": async (e) => {
141
+ const data = e.data;
142
+ const { report, fix } = data;
143
+ const actions = [
144
+ { type: "Action.OpenUrl", title: "View report", url: reportUrl(report.id) }
145
+ ];
146
+ if (fix.pullRequestUrl) {
147
+ actions.push({ type: "Action.OpenUrl", title: "View merged PR", url: fix.pullRequestUrl });
148
+ }
149
+ await post(
150
+ buildPayload([
151
+ {
152
+ type: "Container",
153
+ style: "good",
154
+ items: [
155
+ { type: "TextBlock", size: "Large", weight: "Bolder", text: "Fix applied \u2014 issue resolved", wrap: true },
156
+ {
157
+ type: "TextBlock",
158
+ text: fix.summary ?? `Fix merged for report \`${report.id.slice(0, 8)}...\``,
159
+ wrap: true,
160
+ spacing: "None"
161
+ },
162
+ {
163
+ type: "FactSet",
164
+ facts: [
165
+ { title: "Report ID", value: `\`${report.id.slice(0, 8)}...\`` },
166
+ { title: "Fix status", value: fix.status }
167
+ ]
168
+ }
169
+ ]
170
+ }
171
+ ], actions)
172
+ );
173
+ }
174
+ },
175
+ logger: {
176
+ info: (msg, meta) => console.log(`[mushi-plugin-msteams] ${msg}`, meta ?? ""),
177
+ warn: (msg, meta) => console.warn(`[mushi-plugin-msteams] ${msg}`, meta ?? ""),
178
+ error: (msg, meta) => console.error(`[mushi-plugin-msteams] ${msg}`, meta ?? "")
179
+ }
180
+ });
181
+ }
182
+
183
+ // src/server.ts
184
+ var port = Number(process.env.PORT ?? 3e3);
185
+ var handler = createMsteamsPlugin({
186
+ mushiSecret: required("MUSHI_PLUGIN_SECRET"),
187
+ webhookUrl: required("TEAMS_WEBHOOK_URL"),
188
+ adminBaseUrl: process.env.ADMIN_BASE_URL ?? ""
189
+ });
190
+ (0, import_node_http.createServer)(async (req, res) => {
191
+ if (req.method !== "POST" || req.url !== "/mushi/webhook") {
192
+ res.statusCode = 404;
193
+ res.end();
194
+ return;
195
+ }
196
+ const chunks = [];
197
+ req.on("data", (c) => chunks.push(c));
198
+ req.on("end", async () => {
199
+ const rawBody = Buffer.concat(chunks).toString("utf8");
200
+ const headers = {};
201
+ for (const [k, v] of Object.entries(req.headers)) headers[k] = Array.isArray(v) ? v[0] : v;
202
+ const result = await handler({ rawBody, headers });
203
+ res.statusCode = result.status;
204
+ res.setHeader("Content-Type", "application/json");
205
+ res.end(JSON.stringify(result.body));
206
+ });
207
+ }).listen(port, () => {
208
+ console.warn(`mushi-plugin-msteams listening on :${port}`);
209
+ });
210
+ function required(name) {
211
+ const v = process.env[name];
212
+ if (!v) {
213
+ console.error(`Missing required env var: ${name}`);
214
+ process.exit(1);
215
+ }
216
+ return v;
217
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/server.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createMsteamsPlugin
4
+ } from "./chunk-NTWIK2O3.js";
5
+
6
+ // src/server.ts
7
+ import { createServer } from "http";
8
+ var port = Number(process.env.PORT ?? 3e3);
9
+ var handler = createMsteamsPlugin({
10
+ mushiSecret: required("MUSHI_PLUGIN_SECRET"),
11
+ webhookUrl: required("TEAMS_WEBHOOK_URL"),
12
+ adminBaseUrl: process.env.ADMIN_BASE_URL ?? ""
13
+ });
14
+ createServer(async (req, res) => {
15
+ if (req.method !== "POST" || req.url !== "/mushi/webhook") {
16
+ res.statusCode = 404;
17
+ res.end();
18
+ return;
19
+ }
20
+ const chunks = [];
21
+ req.on("data", (c) => chunks.push(c));
22
+ req.on("end", async () => {
23
+ const rawBody = Buffer.concat(chunks).toString("utf8");
24
+ const headers = {};
25
+ for (const [k, v] of Object.entries(req.headers)) headers[k] = Array.isArray(v) ? v[0] : v;
26
+ const result = await handler({ rawBody, headers });
27
+ res.statusCode = result.status;
28
+ res.setHeader("Content-Type", "application/json");
29
+ res.end(JSON.stringify(result.body));
30
+ });
31
+ }).listen(port, () => {
32
+ console.warn(`mushi-plugin-msteams listening on :${port}`);
33
+ });
34
+ function required(name) {
35
+ const v = process.env[name];
36
+ if (!v) {
37
+ console.error(`Missing required env var: ${name}`);
38
+ process.exit(1);
39
+ }
40
+ return v;
41
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@mushi-mushi/plugin-msteams",
3
+ "version": "0.1.1",
4
+ "description": "Mushi Mushi plugin for Microsoft Teams — posts Adaptive Cards to an Incoming Webhook for report and fix lifecycle events.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "bin": {
18
+ "mushi-plugin-msteams": "./dist/server.js"
19
+ },
20
+ "sideEffects": false,
21
+ "dependencies": {
22
+ "@mushi-mushi/plugin-sdk": "^0.4.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "tsup": "^8.4.0",
27
+ "typescript": "^6.0.2",
28
+ "vitest": "^4.1.4"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/kensaurus/mushi-mushi.git",
36
+ "directory": "packages/plugin-msteams"
37
+ },
38
+ "keywords": [
39
+ "mushi-mushi",
40
+ "microsoft-teams",
41
+ "msteams",
42
+ "plugin",
43
+ "notifications",
44
+ "adaptive-cards"
45
+ ],
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE",
50
+ "CONTRIBUTING.md",
51
+ "CODE_OF_CONDUCT.md",
52
+ "SECURITY.md"
53
+ ],
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": true
57
+ },
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "dev": "tsup --watch",
61
+ "test": "vitest run",
62
+ "typecheck": "tsc --noEmit"
63
+ }
64
+ }