
Product
Socket Threat Intel Now Shows Which Malware Campaigns Were Blocked or Detected
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.
@photon-ai/slack
Advanced tools
TypeScript SDK for Slack, via Spectrum.
This is a thin client over the spectrum-slack gRPC runtime. You provide
the JWTs (one per workspace); the SDK handles transport, retries, and
event streaming.
bun add @photon-ai/slack
import {
createClient,
staticTokens,
text,
blocks,
section,
divider,
} from "@photon-ai/slack";
const client = createClient({
tokenProvider: staticTokens({
tokens: { T012ABCDE: process.env.SLACK_JWT! },
teams: {
T012ABCDE: {
teamName: "Acme",
botUserId: "U-BOT",
appId: "A-APP",
grantedScopes: ["chat:write"],
},
},
}),
});
// Pick a team and act on it.
const team = client.team("T012ABCDE");
// Send text.
await team.messages.send({ channel: "C012XYZ", ...text("hello") });
// Send Block Kit blocks.
await team.messages.send({
channel: "C012XYZ",
...blocks(
[section("Hello *world*"), divider()],
"Hello world (fallback)"
),
});
// Reply in a thread.
await team.messages.send({
channel: "C012XYZ",
threadTs: "1700000000.000100",
...text("threaded reply"),
});
// React to a message.
await team.messages.send({
channel: "C012XYZ", // ignored for reactions
reaction: {
emoji: "thumbsup",
itemTs: "1700000000.000100",
itemChannel: "C012XYZ",
},
});
const stream = client.team("T012ABCDE").events.subscribe();
for await (const event of stream) {
switch (event.type) {
case "message":
console.log(`${event.message.user}: ${event.message.text}`);
break;
case "mention":
console.log(`@bot mentioned by ${event.mention.user}`);
break;
case "reaction":
console.log(`reaction ${event.reaction.name} (removed=${event.reaction.removed})`);
break;
case "interactive":
console.log("interactive:", event.interactive.rawPayload);
break;
case "command":
console.log(`/${event.command.command} ${event.command.text}`);
break;
}
}
The stream reconnects automatically on UNAVAILABLE, draining missed events
via fetchMissedEvents before resuming live. It exits cleanly when the
platform is disabled (PermissionError(kind: "platform_disabled")).
To survive process restarts, plug in a persistent cursor store:
const client = createClient({
tokenProvider: staticTokens({ tokens, teams }),
cursorStore: {
async get(teamId) {
return await redis.get(`slack:cursor:${teamId}`) ?? undefined;
},
async set(teamId, cursor) {
await redis.set(`slack:cursor:${teamId}`, cursor);
},
},
});
const { file, shares } = await client.team("T012ABCDE").files.upload({
channel: "C012XYZ",
filename: "report.pdf",
mimeType: "application/pdf",
content: await fs.readFile("./report.pdf"),
initialComment: "Here's the report",
});
// `shares` has one entry per channel id passed in `channel` — use
// (channel, ts) to react/reply/edit/delete the share message Slack
// created when the file was posted.
const share = shares[0];
if (share) {
await client.team("T012ABCDE").messages.send({
channel: share.channel,
reaction: {
emoji: "thumbsup",
itemTs: share.ts,
itemChannel: share.channel,
},
});
}
All errors are subclasses of SlackError. Use instanceof to handle them:
import {
AuthenticationError,
ConnectionError,
NotFoundError,
PermissionError,
RateLimitError,
SlackError,
ValidationError,
} from "@photon-ai/slack";
try {
await team.messages.send({ channel: "C1", ...text("hi") });
} catch (err) {
if (err instanceof PermissionError && err.permission.kind === "feature_not_enabled") {
console.error(`feature disabled: ${err.permission.feature}`);
} else if (err instanceof RateLimitError) {
console.error(`rate limited; retry after ${err.retryAfterMs}ms`);
} else if (err instanceof SlackError) {
console.error(err);
}
}
PermissionError.permission is a typed discriminated union covering
feature_not_enabled, platform_disabled, team_not_owned, and other.
Feature-gated RPCs today: files.upload / files.getUrl (feature files),
reactions via messages.send({ reaction }) (feature reactions), and
messages.markRead (feature read-tracking).
createClient requires a tokenProvider — the SDK consumes JWTs through
the TokenProvider contract and stays agnostic about where they come from.
staticTokens ships in the box: a fixed team_id → JWT map plus team
metadata. Mint or refresh tokens however you like out-of-band, then plug
the result into staticTokens (or implement TokenProvider directly).
import { createClient, staticTokens } from "@photon-ai/slack";
const client = createClient({
tokenProvider: staticTokens({
tokens: { T012ABCDE: "eyJ..." },
teams: {
T012ABCDE: {
teamName: "Acme",
botUserId: "U-BOT",
appId: "A-APP",
grantedScopes: ["chat:write"],
},
},
}),
});
| Env var | Default |
|---|---|
SPECTRUM_SLACK_ENDPOINT | slack-grpc.spectrum.photon.codes:443 (overridable via the spectrumSlackEndpoint option on createClient) |
For local dev, set it to localhost:50051 — the SDK uses insecure gRPC
for localhost: addresses.
The .proto files live in
photon-hq/spectrum-slack-buf
and are mounted as a git submodule at ./proto/, tracking its main branch.
git clone --recurse-submodules <slack-ts-url>
# or, if already cloned:
git submodule update --init --merge proto
bun install
bun run generate # buf generate proto → src/generated
bun run check # tsc --noEmit
bun test
bun run build # tsdown → dist/
# Pull the latest commit from the proto repo's main into the working tree:
bun run proto:update
CI always runs git submodule update --remote --merge proto before
generating, so every build uses the live tip of the proto repo's main.
The committed submodule SHA is a developer convenience, not a pin —
a breaking proto change shows up on the next CI run.
FAQs
TypeScript SDK for Slack via Spectrum
The npm package @photon-ai/slack receives a total of 61,144 weekly downloads. As such, @photon-ai/slack popularity was classified as popular.
We found that @photon-ai/slack demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.

Product
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.

Security News
Fixes for eMudhra and Brotli are out. Getting them everywhere they’re needed means tracking down bundled libraries, runtimes, and certificate stores.

Threat Research
/Security News
Socket uncovered secret-stealing workflows that ran in LiteLLM’s GitHub repository 11 days before the supply chain attack on PyPI and Docker.