@kody/notify
README.md
227 lines · 8.2 KB · Markdown@kody/notify
Intent
Give every Kody account a single fanout export that notifies the signed-in
user across the destinations they connected: Kody email (email_send to
their account inbox), Slack (via @kody/slack user-token helpers), Discord
(bot token + their channel), and Telegram (bot token + their chat). Channel
config lives in this package's packageStorage(). Success means a forked
copy can dry-run, then send, without anyone hard-coding Kent or official
Kody destinations.
Live @kody/notify storage is the platform package bucket, not the caller's.
Fork first (or packages.invoke the user's copy) before saving channels.
What this package does
One export, kody:@kody/notify, fans out a short message to every enabled
destination. Email is on by default (no secret). Slack, Discord, and Telegram
stay off until the fork owner enables them and stores their channel
or chat id.
| Field | Purpose |
|---|---|
text / subject / title / html | Message. html is email-only. |
dryRun | Preview without sending and without writing configure. |
channels | Optional subset: email, slack, discord, telegram. |
configure | Merge destination config into packageStorage. |
import notify from 'kody:@kody/notify'
const preview = await notify({
subject: 'Deploy finished',
text: 'web@sha shipped.',
dryRun: true,
})Auth model
| Destination | Kind | Default identity | Hosts |
|---|---|---|---|
Kody notify-self (email_send) | Account email | none | |
| Slack | OAuth user token via @kody/slack | Integration slack | slack.com |
| Discord | Bot token secret | Secret discordBotToken | discord.com |
| Telegram | Bot token secret | Secret telegramBotToken | api.telegram.org |
Multi-account: set configure.slack.integration, configure.discord.secretName,
or configure.telegram.secretName. Do not hard-code personal aliases.
Never paste tokens into chat.
Exact user steps
Do not ask the user to paste tokens into chat. Hand them the prefilled URL,
wait until they confirm it is saved, then store their destination ids with
configure + dryRun: true first. Never copy channel, guild, or chat ids
from Kent, @kody, or another account.
1. Fork so storage is theirs
Install/fork this listing into the user's account. Then invoke their
notify (kody:@user/notify or packages.invoke({ kodyId: 'notify' }) on
their copy). Configuring the live @kody/notify package writes the platform
bucket.
2. Email (no secret)
Kody email_send already delivers to the signed-in account email. Tell the
user: "Email is already connected — notifies go to your Kody inbox. Say if
you want it off." Persist that with:
await notify({ configure: { email: { enabled: true } }, dryRun: true })3. Slack (OAuth user token via @kody/slack)
This package depends on @kody/slack. Need a user-token connection
(not a bot token) with scope chat:write.
Prefer the built-in Slack app. Open this URL while signed in to Kody:
https://kody.codes/connect/oauth?provider=slackReview scopes on that page. Grant the user-token chat:write scope (use
Change scopes if needed). Continue to Slack and approve the workspace.
If built-in Slack is missing, grants a bot token, or lacks chat:write,
create a Slack app, register redirect URI exactly
https://kody.codes/connect/oauth, add User Token Scopes
chat:write (plus the read scopes @kody/slack uses), then open:
https://kody.codes/connect/oauth?provider=slack&authorizeUrl=https%3A%2F%2Fslack.com%2Foauth%2Fv2_user%2Fauthorize&tokenUrl=https%3A%2F%2Fslack.com%2Fapi%2Foauth.v2.user.access&apiBaseUrl=https%3A%2F%2Fslack.com%2Fapi&flow=confidential&tokenExchangeStyle=form&scopeSeparator=%2C&allowedHosts=slack.com&dashboardUrl=https%3A%2F%2Fapi.slack.com%2Fapps&scopes=chat%3Awrite%2Cchannels%3Ahistory%2Cchannels%3Aread%2Cgroups%3Ahistory%2Cgroups%3Aread%2Cim%3Ahistory%2Cim%3Aread%2Cmpim%3Ahistory%2Cmpim%3Aread%2Cusers%3AreadPaste Client ID / Client Secret into the Kody form, not into chat.
After connect, ask them which conversation to notify. Save that id:
await notify({
configure: { slack: { enabled: true, channel: 'C0123456789', integration: 'slack' } },
dryRun: true,
})Second workspace: change provider= to slack-work and pass
integration: 'slack-work'. Reconnect with
https://kody.codes/connect/oauth?provider=<integration>.
On missing_scope / HTTP 403 the error names chat:write and the
reconnect URL. Add the User Token Scope, reconnect, retry.
4. Discord (bot token)
User OAuth cannot post in servers. Posting requires a bot.
- discord.com/developers/applications → New Application → Bot tab → copy the bot token.
- Invite the bot into their server with View Channel, Send Messages,
Read Message History (permissions
68608):
https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&scope=bot&permissions=68608- Save the token (never paste it into chat). Approve host
discord.com:
https://kody.codes/account/secrets/new?name=discordBotToken&description=Discord%20bot%20token&allowedHosts=discord.com&scope=user- Store their destination channel snowflake:
await notify({
configure: {
discord: { enabled: true, channelId: '123456789012345678', secretName: 'discordBotToken' },
},
dryRun: true,
})Second bot: save another secret (name=discordBotTokenWork) with the same
allowedHosts=discord.com, then pass that secretName.
On 401: rotate/save the bot token at the secrets URL. On 403 Missing Access (code 50001) or Missing Permissions (code 50013): the bot is not in the server or lacks Send Messages — reinstall with the authorize URL above.
5. Telegram (bot token)
Telegram Login OAuth (oauth.telegram.org) is identity only and cannot send
messages. Sending uses the Bot API.
- @BotFather →
/newbot→ copy the token. - Message the bot (or add it to the target group).
- Save the token. Approve host
api.telegram.org:
https://kody.codes/account/secrets/new?name=telegramBotToken&description=Telegram%20bot%20token%20from%20BotFather&allowedHosts=api.telegram.org&scope=user- Store their numeric
chatId:
await notify({
configure: { telegram: { enabled: true, chatId: '123456789', secretName: 'telegramBotToken' } },
dryRun: true,
})On 401: the bot token is missing/invalid — save it at the URL above. On 403: the user has not started the bot (or the bot was kicked).
Smoke test
After publish, invoke in the package runtime so packageStorage and
secret mounts resolve. Call with dryRun: true (read-only send preview;
email is enabled by default):
import { packages } from 'kody:runtime'
export default async function main() {
return await packages.invoke({
kodyId: 'notify',
exportName: '.',
params: {
subject: 'Notify smoke',
text: 'dryRun fanout preview',
dryRun: true,
},
})
}Success: dryRun === true, saved === false, and results includes an
email preview (status preview). It must not call email_send or post to
Slack/Discord/Telegram. Discord/Telegram dry-run may call users/@me /
getMe when those secrets already exist.
A live send (no dryRun) requires explicit user confirmation of the text
and destinations.
Troubleshooting
- Slack
missing_scope/ 403: missingchat:write. Add the user scope, reconnect athttps://kody.codes/connect/oauth?provider=slack. - Slack
invalid_auth: reconnect the same integration URL. - Discord 401: save/rotate
discordBotTokenat the secrets URL above. - Discord 403: bot not in the server or missing Send Messages.
- Telegram 401/403: bot token or chat membership; see steps above.
- Static import vs invoke: config is package-owned storage. Use
packages.invoke({ kodyId: 'notify', ... })after a fork.
Out of scope
This package does not run a Discord gateway, manage the official Kody
Discord server, or pin a personal “send me a message” channel. Use
@kody/slack (and @kody/discord / @kody/telegram when listed) for
richer provider APIs.