@spectrum-ts/slack 5.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License Copyright (c) 2025 Photon AI
2
+
3
+ Permission is hereby granted,
4
+ free of charge, to any person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use, copy, modify, merge,
7
+ publish, distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ (including the next paragraph) shall be included in all copies or substantial
13
+ portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # @spectrum-ts/slack
2
+
3
+ Slack provider for [spectrum-ts](https://github.com/photon-hq/spectrum-ts).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add spectrum-ts @spectrum-ts/slack
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```ts
14
+ import { Spectrum } from "spectrum-ts";
15
+ import { slack } from "@spectrum-ts/slack";
16
+
17
+ const spectrum = Spectrum({
18
+ providers: [slack.config({ tokens: { T012ABCDE: "xoxb-..." } })],
19
+ });
20
+ ```
21
+
22
+ See the [spectrum-ts documentation](https://photon.codes/spectrum) for the full guide.
@@ -0,0 +1,44 @@
1
+ import { SchemaMessage } from "@spectrum-ts/core";
2
+ import z from "zod";
3
+
4
+ //#region src/types.d.ts
5
+ declare const userSchema: z.ZodObject<{}, z.core.$strip>;
6
+ declare const spaceSchema: z.ZodObject<{
7
+ id: z.ZodString;
8
+ teamId: z.ZodString;
9
+ }, z.core.$strip>;
10
+ type SlackMessage = SchemaMessage<typeof userSchema, typeof spaceSchema> & {
11
+ isFromMe: boolean;
12
+ subtype?: string;
13
+ threadTs?: string;
14
+ ts?: string;
15
+ };
16
+ //#endregion
17
+ //#region src/index.d.ts
18
+ declare const slack: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"Slack", import("zod").ZodUnion<readonly [import("zod").ZodObject<{
19
+ endpoint: import("zod").ZodOptional<import("zod").ZodString>;
20
+ teams: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
21
+ appId: import("zod").ZodString;
22
+ botUserId: import("zod").ZodString;
23
+ grantedScopes: import("zod").ZodArray<import("zod").ZodString>;
24
+ teamName: import("zod").ZodString;
25
+ }, import("zod/v4/core").$strip>>>;
26
+ tokens: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>;
27
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{}, import("zod/v4/core").$strict>]>, import("zod").ZodObject<{}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
28
+ id: import("zod").ZodString;
29
+ teamId: import("zod").ZodString;
30
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
31
+ teamId: import("zod").ZodString;
32
+ }, import("zod/v4/core").$strip>, import("@photon-ai/slack").SlackClient, {
33
+ id: string;
34
+ }, {
35
+ id: string;
36
+ teamId: string;
37
+ }, import("zod").ZodObject<{
38
+ isFromMe: import("zod").ZodBoolean;
39
+ subtype: import("zod").ZodOptional<import("zod").ZodString>;
40
+ threadTs: import("zod").ZodOptional<import("zod").ZodString>;
41
+ ts: import("zod").ZodOptional<import("zod").ZodString>;
42
+ }, import("zod/v4/core").$strip>, SlackMessage, undefined, Record<never, never>, Record<never, never>, Record<never, never>>> & Readonly<Record<never, never>>;
43
+ //#endregion
44
+ export { slack };
package/dist/index.js ADDED
@@ -0,0 +1,466 @@
1
+ import { createClient, staticTokens } from "@photon-ai/slack";
2
+ import { UnsupportedError, cloud, definePlatform, mergeStreams, stream } from "@spectrum-ts/core";
3
+ import { asAttachment, asCustom, asReaction, asText, createLogger, errorAttrs } from "@spectrum-ts/core/authoring";
4
+ import z from "zod";
5
+ //#region src/auth.ts
6
+ const log = createLogger("spectrum.slack.auth");
7
+ const RENEWAL_RATIO = .8;
8
+ const EXPIRY_BUFFER_MS = 3e4;
9
+ const RETRY_DELAY_MS = 3e4;
10
+ const cloudAuthState = /* @__PURE__ */ new WeakMap();
11
+ const toTeamMetadata = (meta) => ({
12
+ appId: meta.appId,
13
+ botUserId: meta.botUserId,
14
+ grantedScopes: meta.grantedScopes,
15
+ teamName: meta.teamName
16
+ });
17
+ /**
18
+ * Build a {@link SlackClient} backed by a {@link TokenProvider} that lazily
19
+ * refreshes its tokens against `POST /projects/:id/slack/tokens` on TTL or on
20
+ * an UNAUTHENTICATED bounce (slack-ts middleware calls `invalidate(teamId)`).
21
+ *
22
+ * Token + team metadata are kept in a single snapshot, refreshed atomically.
23
+ * `listTeams()` returns the latest snapshot so `client.teams()` always
24
+ * reflects active installations — used by the slack provider's message stream
25
+ * to discover which workspaces to subscribe to.
26
+ */
27
+ async function createCloudClients(projectId, projectSecret, endpoint) {
28
+ let tokenData = await cloud.issueSlackTokens(projectId, projectSecret);
29
+ let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
30
+ let disposed = false;
31
+ let renewalTimer;
32
+ let refreshFailures = 0;
33
+ const clearRenewalTimer = () => {
34
+ if (renewalTimer !== void 0) {
35
+ clearTimeout(renewalTimer);
36
+ renewalTimer = void 0;
37
+ }
38
+ };
39
+ const refreshTokens = async () => {
40
+ tokenData = await cloud.issueSlackTokens(projectId, projectSecret);
41
+ tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
42
+ };
43
+ const onRefreshSuccess = () => {
44
+ if (refreshFailures > 0) {
45
+ log.info("slack token refresh recovered", { "spectrum.slack.auth.attempt": refreshFailures });
46
+ refreshFailures = 0;
47
+ }
48
+ };
49
+ const onRefreshFailure = (error) => {
50
+ refreshFailures += 1;
51
+ log.warn("slack token refresh failed; retrying", {
52
+ "spectrum.slack.auth.attempt": refreshFailures,
53
+ "spectrum.slack.auth.retry_in_ms": RETRY_DELAY_MS,
54
+ ...errorAttrs(error)
55
+ }, error);
56
+ };
57
+ const scheduleRetry = () => {
58
+ if (disposed) return;
59
+ clearRenewalTimer();
60
+ renewalTimer = setTimeout(async () => {
61
+ if (disposed) return;
62
+ try {
63
+ await refreshTokens();
64
+ onRefreshSuccess();
65
+ scheduleRenewal();
66
+ } catch (retryErr) {
67
+ onRefreshFailure(retryErr);
68
+ scheduleRetry();
69
+ }
70
+ }, RETRY_DELAY_MS);
71
+ renewalTimer?.unref?.();
72
+ };
73
+ const scheduleRenewal = () => {
74
+ if (disposed) return;
75
+ clearRenewalTimer();
76
+ const ttlMs = tokenData.expiresIn * 1e3;
77
+ const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
78
+ renewalTimer = setTimeout(async () => {
79
+ try {
80
+ await refreshTokens();
81
+ onRefreshSuccess();
82
+ scheduleRenewal();
83
+ } catch (err) {
84
+ onRefreshFailure(err);
85
+ scheduleRetry();
86
+ }
87
+ }, renewInMs);
88
+ renewalTimer?.unref?.();
89
+ };
90
+ const refreshIfNeeded = async () => {
91
+ if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) return;
92
+ await refreshTokens();
93
+ onRefreshSuccess();
94
+ scheduleRenewal();
95
+ };
96
+ scheduleRenewal();
97
+ const client = createClient({
98
+ spectrumSlackEndpoint: endpoint,
99
+ tokenProvider: {
100
+ async getAccessToken(teamId) {
101
+ await refreshIfNeeded();
102
+ const token = tokenData.auth[teamId];
103
+ if (!token) throw new Error(`Slack team ${teamId} has no active installation in this project`);
104
+ return token;
105
+ },
106
+ invalidate(_teamId) {
107
+ tokenExpiresAt = 0;
108
+ },
109
+ async listTeams() {
110
+ await refreshIfNeeded();
111
+ const entries = Object.entries(tokenData.teams).map(([teamId, meta]) => [teamId, toTeamMetadata(meta)]);
112
+ return new Map(entries);
113
+ }
114
+ }
115
+ });
116
+ cloudAuthState.set(client, { dispose: async () => {
117
+ disposed = true;
118
+ clearRenewalTimer();
119
+ } });
120
+ return client;
121
+ }
122
+ async function disposeCloudAuth(client) {
123
+ const auth = cloudAuthState.get(client);
124
+ if (!auth) return;
125
+ await auth.dispose();
126
+ cloudAuthState.delete(client);
127
+ }
128
+ //#endregion
129
+ //#region src/messages.ts
130
+ const toRecord = (result, space, content) => ({
131
+ id: result.ts,
132
+ content,
133
+ space: {
134
+ id: result.channel,
135
+ teamId: space.teamId
136
+ },
137
+ timestamp: tsToDate(result.ts),
138
+ ts: result.ts,
139
+ isFromMe: true
140
+ });
141
+ const toUploadRecord = (result, space, content) => {
142
+ const shareTs = result.shares.find((s) => s.channel === space.id)?.ts;
143
+ return {
144
+ id: shareTs ?? result.file.id,
145
+ content,
146
+ space: {
147
+ id: space.id,
148
+ teamId: space.teamId
149
+ },
150
+ timestamp: shareTs ? tsToDate(shareTs) : /* @__PURE__ */ new Date(),
151
+ ts: shareTs,
152
+ isFromMe: true
153
+ };
154
+ };
155
+ const tsToDate = (ts) => {
156
+ if (!ts) return /* @__PURE__ */ new Date();
157
+ const seconds = Number.parseFloat(ts);
158
+ if (!Number.isFinite(seconds)) return /* @__PURE__ */ new Date();
159
+ return /* @__PURE__ */ new Date(seconds * 1e3);
160
+ };
161
+ const lazySlackFile = (client, teamId, file) => asAttachment({
162
+ id: file.id,
163
+ name: file.name,
164
+ mimeType: file.mimeType,
165
+ size: file.size,
166
+ read: async () => {
167
+ const { bytes } = await client.team(teamId).files.getContentBuffer(file.id);
168
+ return Buffer.from(bytes);
169
+ },
170
+ stream: async () => {
171
+ const { content } = await client.team(teamId).files.getContent(file.id);
172
+ return new ReadableStream({ async start(controller) {
173
+ try {
174
+ for await (const chunk of content) controller.enqueue(chunk);
175
+ controller.close();
176
+ } catch (err) {
177
+ controller.error(err);
178
+ }
179
+ } });
180
+ }
181
+ });
182
+ const toMessages = (client, event) => {
183
+ if (event.type === "message") return messageToMessages(client, event.teamId, event.message);
184
+ if (event.type === "reaction") return [reactionToMessage(event.teamId, event.reaction)];
185
+ if (event.type === "mention") return [{
186
+ id: event.mention.ts,
187
+ content: asText(event.mention.text),
188
+ sender: { id: event.mention.user },
189
+ space: {
190
+ id: event.mention.channel,
191
+ teamId: event.teamId
192
+ },
193
+ timestamp: tsToDate(event.mention.ts),
194
+ ts: event.mention.ts,
195
+ isFromMe: event.mention.isFromMe
196
+ }];
197
+ return [];
198
+ };
199
+ const messageToMessages = (client, teamId, msg) => {
200
+ const base = {
201
+ sender: { id: msg.user },
202
+ space: {
203
+ id: msg.channel,
204
+ teamId
205
+ },
206
+ timestamp: tsToDate(msg.ts),
207
+ ts: msg.ts,
208
+ threadTs: msg.threadTs,
209
+ subtype: msg.subtype,
210
+ isFromMe: msg.isFromMe
211
+ };
212
+ const results = [];
213
+ if (msg.text) results.push({
214
+ ...base,
215
+ id: msg.files.length > 0 ? `${msg.ts}:text` : msg.ts,
216
+ content: asText(msg.text)
217
+ });
218
+ for (const [index, file] of msg.files.entries()) {
219
+ const singleFile = msg.files.length === 1 && !msg.text;
220
+ results.push({
221
+ ...base,
222
+ id: singleFile ? msg.ts : `${msg.ts}:file:${index}`,
223
+ content: lazySlackFile(client, teamId, file)
224
+ });
225
+ }
226
+ if (results.length === 0) results.push({
227
+ ...base,
228
+ id: msg.ts,
229
+ content: asCustom({ slack_type: "empty" })
230
+ });
231
+ return results;
232
+ };
233
+ const reactionToMessage = (teamId, reaction) => {
234
+ const stubTarget = {
235
+ id: reaction.itemTs,
236
+ content: asCustom({
237
+ slack_type: "reaction-target",
238
+ stub: true
239
+ }),
240
+ sender: { id: "" },
241
+ space: {
242
+ id: reaction.itemChannel,
243
+ teamId
244
+ }
245
+ };
246
+ return {
247
+ id: `${reaction.itemTs}:reaction:${reaction.user}:${reaction.name}`,
248
+ content: asReaction({
249
+ emoji: reaction.name,
250
+ target: stubTarget
251
+ }),
252
+ sender: { id: reaction.user },
253
+ space: {
254
+ id: reaction.itemChannel,
255
+ teamId
256
+ },
257
+ timestamp: /* @__PURE__ */ new Date(),
258
+ ts: reaction.itemTs,
259
+ subtype: reaction.removed ? "reaction_removed" : "reaction_added",
260
+ isFromMe: reaction.isFromMe
261
+ };
262
+ };
263
+ const teamStream = (client, teamId) => {
264
+ const eventStream = client.team(teamId).events.subscribe();
265
+ return stream((emit, end) => {
266
+ const pump = (async () => {
267
+ try {
268
+ for await (const event of eventStream) for (const m of toMessages(client, event)) await emit(m);
269
+ end();
270
+ } catch (e) {
271
+ end(e);
272
+ }
273
+ })();
274
+ return async () => {
275
+ await eventStream.close();
276
+ await pump;
277
+ };
278
+ });
279
+ };
280
+ const messages = (client, resolveTeamIds) => stream(async (emit, end) => {
281
+ let teamIds;
282
+ try {
283
+ teamIds = await resolveTeamIds();
284
+ } catch (err) {
285
+ end(err);
286
+ return;
287
+ }
288
+ const merged = mergeStreams(teamIds.map((id) => teamStream(client, id)));
289
+ const pump = (async () => {
290
+ try {
291
+ for await (const value of merged) await emit(value);
292
+ end();
293
+ } catch (e) {
294
+ end(e);
295
+ }
296
+ })();
297
+ return async () => {
298
+ await merged.close();
299
+ await pump;
300
+ };
301
+ });
302
+ const mimeToMediaName = (mimeType, fallback) => {
303
+ const slash = mimeType.indexOf("/");
304
+ if (slash < 0) return fallback;
305
+ return `${fallback}.${mimeType.slice(slash + 1)}`;
306
+ };
307
+ const send = async (client, space, content) => {
308
+ if (content.type === "reply") return await replyToMessage(client, space, content.target.ts ?? content.target.id, content.content);
309
+ if (content.type === "reaction") return await reactToMessage(client, space, content.target.ts ?? content.target.id, content);
310
+ if (content.type === "typing") return;
311
+ if (content.type === "read") return;
312
+ return await sendContent(client, space, content);
313
+ };
314
+ const sendContent = async (client, space, content, threadTs) => {
315
+ const team = client.team(space.teamId);
316
+ switch (content.type) {
317
+ case "text": return toRecord(await team.messages.send({
318
+ channel: space.id,
319
+ text: content.text,
320
+ threadTs
321
+ }), space, content);
322
+ case "attachment": return toUploadRecord(await team.files.upload({
323
+ channel: space.id,
324
+ content: await content.read(),
325
+ filename: content.name,
326
+ mimeType: content.mimeType,
327
+ threadTs
328
+ }), space, content);
329
+ case "voice": return toUploadRecord(await team.files.upload({
330
+ channel: space.id,
331
+ content: await content.read(),
332
+ filename: content.name ?? mimeToMediaName(content.mimeType, "voice"),
333
+ mimeType: content.mimeType,
334
+ threadTs
335
+ }), space, content);
336
+ case "app": return toRecord(await team.messages.send({
337
+ channel: space.id,
338
+ text: await content.url(),
339
+ threadTs
340
+ }), space, content);
341
+ default: throw UnsupportedError.content(content.type);
342
+ }
343
+ };
344
+ const reactToMessage = async (client, space, targetTs, content) => {
345
+ await client.team(space.teamId).messages.send({
346
+ channel: space.id,
347
+ reaction: {
348
+ emoji: content.emoji,
349
+ itemChannel: space.id,
350
+ itemTs: targetTs
351
+ }
352
+ });
353
+ return {
354
+ id: `${targetTs}:reaction:self:${content.emoji}`,
355
+ content,
356
+ space: {
357
+ id: space.id,
358
+ teamId: space.teamId
359
+ },
360
+ timestamp: /* @__PURE__ */ new Date(),
361
+ ts: targetTs,
362
+ isFromMe: true
363
+ };
364
+ };
365
+ const replyToMessage = async (client, space, targetTs, content) => await sendContent(client, space, content, targetTs);
366
+ //#endregion
367
+ //#region src/types.ts
368
+ const teamMetadataSchema = z.object({
369
+ appId: z.string(),
370
+ botUserId: z.string(),
371
+ grantedScopes: z.array(z.string()),
372
+ teamName: z.string()
373
+ });
374
+ const directConfig = z.object({
375
+ endpoint: z.string().optional(),
376
+ teams: z.record(z.string(), teamMetadataSchema).optional(),
377
+ tokens: z.record(z.string(), z.string().min(1)).refine((t) => Object.keys(t).length > 0, { message: "at least one token entry is required" })
378
+ });
379
+ const cloudConfig = z.object({}).strict();
380
+ const configSchema = z.union([directConfig, cloudConfig]);
381
+ const isCloudConfig = (config) => !("tokens" in config);
382
+ const userSchema = z.object({});
383
+ const spaceSchema = z.object({
384
+ id: z.string(),
385
+ teamId: z.string()
386
+ });
387
+ const spaceParamsSchema = z.object({ teamId: z.string() });
388
+ /**
389
+ * Slack-specific per-message metadata surfaced on `SlackMessage`.
390
+ * - `isFromMe`: server-stamped by spectrum-slack — `true` when `sender.id` is
391
+ * this installation's bot user id. Use this to filter self-echo without
392
+ * plumbing `bot_user_id` from `client.teams()` into the consumer.
393
+ * - `ts`: the canonical Slack message timestamp id (mirrors `id` for messages
394
+ * sourced from the events stream; useful when constructing replies that
395
+ * target the same thread).
396
+ * - `threadTs`: the parent message ts when the row is itself a threaded reply.
397
+ * - `subtype`: Slack's subtype, e.g. `bot_message`, `message_changed`, etc.
398
+ */
399
+ const messageSchema = z.object({
400
+ isFromMe: z.boolean(),
401
+ subtype: z.string().optional(),
402
+ threadTs: z.string().optional(),
403
+ ts: z.string().optional()
404
+ });
405
+ //#endregion
406
+ //#region src/index.ts
407
+ const slack = definePlatform("Slack", {
408
+ config: configSchema,
409
+ lifecycle: {
410
+ createClient: async ({ config, projectId, projectSecret }) => {
411
+ if (!isCloudConfig(config)) return createClient({
412
+ spectrumSlackEndpoint: config.endpoint,
413
+ tokenProvider: staticTokens({
414
+ tokens: config.tokens,
415
+ teams: config.teams
416
+ })
417
+ });
418
+ if (!(projectId && projectSecret)) throw new Error("Slack cloud mode requires projectId and projectSecret. Either pass credentials to Spectrum(), or provide direct credentials: slack.config({ tokens: { T012ABCDE: 'jwt...' } })");
419
+ return await createCloudClients(projectId, projectSecret, process.env.SPECTRUM_SLACK_ENDPOINT);
420
+ },
421
+ destroyClient: async ({ client }) => {
422
+ await disposeCloudAuth(client);
423
+ await client.close();
424
+ }
425
+ },
426
+ user: {
427
+ schema: userSchema,
428
+ resolve: async ({ input }) => ({ id: input.userID })
429
+ },
430
+ space: {
431
+ schema: spaceSchema,
432
+ params: spaceParamsSchema,
433
+ create: async ({ input }) => {
434
+ const teamId = input.params?.teamId;
435
+ if (!teamId) throw new Error("Slack space creation requires a teamId param. Pass it via space.create(user, { teamId }).");
436
+ if (input.users.length > 1) throw UnsupportedError.action("space.create", "Slack", "group DMs require an explicit channel id (Slack's conversations.open is not exposed); use space.get(channelId, { teamId })");
437
+ const user = input.users[0];
438
+ if (!user) throw new Error("Slack space creation requires a user");
439
+ return {
440
+ id: user.id,
441
+ teamId
442
+ };
443
+ },
444
+ get: async ({ input }) => {
445
+ const teamId = input.params?.teamId;
446
+ if (!teamId) throw new Error("Slack spaces require a teamId param. Pass it via space.get(channelId, { teamId }).");
447
+ return {
448
+ id: input.id,
449
+ teamId
450
+ };
451
+ }
452
+ },
453
+ message: { schema: messageSchema },
454
+ messages: ({ client, config }) => messages(client, async () => {
455
+ const teams = await client.teams();
456
+ if (teams.size > 0) return Array.from(teams.keys());
457
+ if (isCloudConfig(config)) return [];
458
+ return Object.keys(config.tokens);
459
+ }),
460
+ send: async ({ space, content, client }) => await send(client, {
461
+ id: space.id,
462
+ teamId: space.teamId
463
+ }, content)
464
+ });
465
+ //#endregion
466
+ export { slack };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@spectrum-ts/slack",
3
+ "version": "5.0.0",
4
+ "description": "Slack provider for spectrum-ts.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/photon-hq/spectrum-ts.git",
8
+ "directory": "packages/slack"
9
+ },
10
+ "homepage": "https://photon.codes/spectrum",
11
+ "bugs": {
12
+ "url": "https://github.com/photon-hq/spectrum-ts/issues"
13
+ },
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "spectrum": {
29
+ "key": "slack",
30
+ "import": "slack",
31
+ "label": "Slack"
32
+ },
33
+ "dependencies": {
34
+ "@photon-ai/slack": "^0.2.0",
35
+ "zod": "^4.2.1"
36
+ },
37
+ "peerDependencies": {
38
+ "@spectrum-ts/core": "^5.0.0",
39
+ "typescript": "^5 || ^6.0.0"
40
+ },
41
+ "license": "MIT"
42
+ }