Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@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.

FieldPurpose
text / subject / title / htmlMessage. html is email-only.
dryRunPreview without sending and without writing configure.
channelsOptional subset: email, slack, discord, telegram.
configureMerge 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

DestinationKindDefault identityHosts
EmailKody notify-self (email_send)Account emailnone
SlackOAuth user token via @kody/slackIntegration slackslack.com
DiscordBot token secretSecret discordBotTokendiscord.com
TelegramBot token secretSecret telegramBotTokenapi.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=slack

Review 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%3Aread

Paste 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.

  1. discord.com/developers/applications → New Application → Bot tab → copy the bot token.
  2. 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
  1. 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
  1. 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.

  1. @BotFather/newbot → copy the token.
  2. Message the bot (or add it to the target group).
  3. 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
  1. 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: missing chat:write. Add the user scope, reconnect at https://kody.codes/connect/oauth?provider=slack.
  • Slack invalid_auth: reconnect the same integration URL.
  • Discord 401: save/rotate discordBotToken at 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.