@cuylabs/channel-slack 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -25,22 +25,26 @@ use:
25
25
  npm install @slack/bolt @slack/web-api @slack/types express
26
26
  ```
27
27
 
28
+ Postgres-backed helpers lazy-load `pg` when you pass a connection string. Install
29
+ `pg` in applications that use those helpers without injecting their own client
30
+ or pool.
31
+
28
32
  ## Import Map
29
33
 
30
34
  Prefer feature-specific imports so applications only couple to the Slack
31
35
  surface they need.
32
36
 
33
- | Import | Use for |
34
- | --- | --- |
35
- | `@cuylabs/channel-slack/core` | Activity parsing, formatting, sessions, turn context, shared types |
36
- | `@cuylabs/channel-slack/policy` | Message admission, duplicate suppression, mentioned-thread state |
37
- | `@cuylabs/channel-slack/history` | Slack history reading, prompt shaping, supplemental-history visibility |
38
- | `@cuylabs/channel-slack/bolt` | Bolt app factories, auth options, Socket Mode runtime helpers, installation stores |
39
- | `@cuylabs/channel-slack/setup` | Required scopes/events/settings, generated manifests, setup inspection |
40
- | `@cuylabs/channel-slack/diagnostics` | Slack token and scope checks |
41
- | `@cuylabs/channel-slack/users` | User profile lookup and mention enrichment |
42
- | `@cuylabs/channel-slack/targets` | Human-friendly Slack channel/user target parsing and resolution |
43
- | `@cuylabs/channel-slack/feedback` | Feedback Block Kit and action helpers |
37
+ | Import | Use for |
38
+ | ------------------------------------ | ---------------------------------------------------------------------------------- |
39
+ | `@cuylabs/channel-slack/core` | Activity parsing, formatting, sessions, turn context, shared types |
40
+ | `@cuylabs/channel-slack/policy` | Message admission, duplicate suppression, mentioned-thread state |
41
+ | `@cuylabs/channel-slack/history` | Slack history reading, prompt shaping, supplemental-history visibility |
42
+ | `@cuylabs/channel-slack/bolt` | Bolt app factories, auth options, Socket Mode runtime helpers, installation stores |
43
+ | `@cuylabs/channel-slack/setup` | Required scopes/events/settings, generated manifests, setup inspection |
44
+ | `@cuylabs/channel-slack/diagnostics` | Slack token and scope checks |
45
+ | `@cuylabs/channel-slack/users` | User profile lookup and mention enrichment |
46
+ | `@cuylabs/channel-slack/targets` | Human-friendly Slack channel/user target parsing and resolution |
47
+ | `@cuylabs/channel-slack/feedback` | Feedback Block Kit and action helpers |
44
48
 
45
49
  The package root re-exports `core` and `policy` as a lightweight convenience.
46
50
  Use feature-specific imports for peer-backed helpers such as `bolt`,
@@ -92,6 +96,8 @@ See [Activity](docs/concepts/activity.md).
92
96
  `policy` decides which Slack messages an adapter should process. It accepts DMs
93
97
  and direct mentions by default, can allow passive channel messages by channel or
94
98
  install scope, tracks mentioned threads, and suppresses duplicate Slack events.
99
+ Use `createPostgresSlackMessagePolicyStateStore` when duplicate suppression and
100
+ mentioned-thread state must survive restarts or coordinate across workers.
95
101
 
96
102
  See [Message Policy](docs/concepts/message-policy.md).
97
103
 
@@ -107,7 +113,9 @@ See [Supplemental History](docs/concepts/supplemental-history.md).
107
113
 
108
114
  `bolt` contains app factories for HTTP and Socket Mode, direct auth resolution,
109
115
  OAuth installation-store types, development installation stores, and Socket Mode
110
- process/runtime guards. It does not register agent handlers.
116
+ process/runtime guards. It also exposes a Postgres advisory-lock helper for
117
+ distributed Socket Mode single-instance coordination. It does not register agent
118
+ handlers.
111
119
 
112
120
  See [Bolt Runtime](docs/concepts/bolt-runtime.md).
113
121
 
package/dist/bolt.d.ts CHANGED
@@ -262,6 +262,34 @@ interface SlackSocketModeProcessLock {
262
262
  }
263
263
  declare function acquireSlackSocketModeProcessLock({ appSlug, appToken, enabled, lockDir, logger, }: SlackSocketModeProcessLockOptions): SlackSocketModeProcessLock | undefined;
264
264
 
265
+ interface SlackSocketModePostgresLockClient {
266
+ query<T = unknown>(sql: string, values?: readonly unknown[]): Promise<{
267
+ rows: T[];
268
+ rowCount?: number | null;
269
+ }>;
270
+ release?: () => void;
271
+ }
272
+ interface SlackSocketModePostgresLockPool {
273
+ connect(): Promise<SlackSocketModePostgresLockClient>;
274
+ end?: () => Promise<void>;
275
+ }
276
+ interface SlackSocketModePostgresLockOptions {
277
+ appSlug: string;
278
+ appToken?: string;
279
+ client?: SlackSocketModePostgresLockClient;
280
+ connectionString?: string;
281
+ enabled?: boolean;
282
+ logger?: Logger;
283
+ namespace?: string;
284
+ pool?: SlackSocketModePostgresLockPool;
285
+ }
286
+ interface SlackSocketModePostgresLock {
287
+ path: string;
288
+ close(): Promise<void>;
289
+ release(): Promise<void>;
290
+ }
291
+ declare function acquireSlackSocketModePostgresLock({ appSlug, appToken, client, connectionString, enabled, logger, namespace, pool, }: SlackSocketModePostgresLockOptions): Promise<SlackSocketModePostgresLock | undefined>;
292
+
265
293
  type SlackSocketModeRuntimePolicy = "single-instance" | "external-coordination";
266
294
  interface SlackSocketModeReceiverRuntimeOptions {
267
295
  autoReconnectEnabled?: boolean;
@@ -330,6 +358,7 @@ declare class JsonFileSlackInstallationStore implements SlackInstallationStore {
330
358
  private writeInstallations;
331
359
  }
332
360
  declare function createJsonFileSlackInstallationStore(filePath: string): SlackInstallationStore;
361
+ declare function createSlackInstallationStoreAuthorize(store: SlackInstallationStore): SlackAuthorizeFn;
333
362
  declare function getSlackInstallationKey(input: SlackInstallation | SlackInstallationQuery): string;
334
363
 
335
- export { type CreateSlackBoltAppOptions, type CreateSlackBoltAppResult, type CreateSlackSocketBoltAppOptions, type CreateSlackSocketBoltAppResult, InMemorySlackInstallationStore, JsonFileSlackInstallationStore, type SlackAuthorizeFn, type SlackAuthorizeResult, type SlackAuthorizeSource, type SlackCustomAuthorizeAuthOptions, type SlackDirectAuthMode, type SlackDirectAuthOptions, type SlackInstallation, type SlackInstallationQuery, type SlackInstallationStore, type SlackOAuthAuthOptions, type SlackOAuthCallbackOptions, type SlackOAuthError, type SlackOAuthInstallPathOptions, type SlackSingleWorkspaceAuthOptions, type SlackSocketModeProcessLock, type SlackSocketModeProcessLockOptions, type SlackSocketModeReceiverRuntimeOptions, type SlackSocketModeRestartGuard, type SlackSocketModeRuntime, type SlackSocketModeRuntimeOptions, type SlackSocketModeRuntimePolicy, type SlackStateStore, acquireSlackSocketModeProcessLock, createInMemorySlackInstallationStore, createJsonFileSlackInstallationStore, createSlackBoltApp, createSlackSdkLogger, createSlackSocketBoltApp, createSlackSocketModeRestartGuard, createSlackSocketModeRuntime, getSlackInstallationKey, redactSlackSocketModeLogValue };
364
+ export { type CreateSlackBoltAppOptions, type CreateSlackBoltAppResult, type CreateSlackSocketBoltAppOptions, type CreateSlackSocketBoltAppResult, InMemorySlackInstallationStore, JsonFileSlackInstallationStore, type SlackAuthorizeFn, type SlackAuthorizeResult, type SlackAuthorizeSource, type SlackCustomAuthorizeAuthOptions, type SlackDirectAuthMode, type SlackDirectAuthOptions, type SlackInstallation, type SlackInstallationQuery, type SlackInstallationStore, type SlackOAuthAuthOptions, type SlackOAuthCallbackOptions, type SlackOAuthError, type SlackOAuthInstallPathOptions, type SlackSingleWorkspaceAuthOptions, type SlackSocketModePostgresLock, type SlackSocketModePostgresLockClient, type SlackSocketModePostgresLockOptions, type SlackSocketModePostgresLockPool, type SlackSocketModeProcessLock, type SlackSocketModeProcessLockOptions, type SlackSocketModeReceiverRuntimeOptions, type SlackSocketModeRestartGuard, type SlackSocketModeRuntime, type SlackSocketModeRuntimeOptions, type SlackSocketModeRuntimePolicy, type SlackStateStore, acquireSlackSocketModePostgresLock, acquireSlackSocketModeProcessLock, createInMemorySlackInstallationStore, createJsonFileSlackInstallationStore, createSlackBoltApp, createSlackInstallationStoreAuthorize, createSlackSdkLogger, createSlackSocketBoltApp, createSlackSocketModeRestartGuard, createSlackSocketModeRuntime, getSlackInstallationKey, redactSlackSocketModeLogValue };
package/dist/bolt.js CHANGED
@@ -363,6 +363,141 @@ function formatLockError(error) {
363
363
  return error instanceof Error ? error.message : String(error);
364
364
  }
365
365
 
366
+ // src/bolt/postgres-socket-lock.ts
367
+ import crypto2 from "crypto";
368
+ var DEFAULT_LOCK_NAMESPACE = "channel-slack-socket";
369
+ async function acquireSlackSocketModePostgresLock({
370
+ appSlug,
371
+ appToken,
372
+ client,
373
+ connectionString,
374
+ enabled = true,
375
+ logger,
376
+ namespace = DEFAULT_LOCK_NAMESPACE,
377
+ pool
378
+ }) {
379
+ if (!enabled) {
380
+ logger?.debug?.("Slack Socket Mode Postgres lock skipped", {
381
+ reason: "disabled"
382
+ });
383
+ return void 0;
384
+ }
385
+ if (!appToken?.trim()) {
386
+ throw new Error(
387
+ "Slack app token is required when the Slack Socket Mode Postgres lock is enabled."
388
+ );
389
+ }
390
+ const tokenHash = hashValue(appToken);
391
+ const [key1, key2] = advisoryLockKeys(`${namespace}:${appSlug}:${tokenHash}`);
392
+ const lockPath = `postgres-advisory:${key1}:${key2}`;
393
+ let activePool = pool;
394
+ let ownsPool = false;
395
+ let activeClient = client;
396
+ let released = false;
397
+ try {
398
+ if (!activeClient) {
399
+ if (!activePool) {
400
+ if (!connectionString) {
401
+ throw new Error(
402
+ "connectionString is required when a Postgres Slack Socket Mode lock client or pool is not provided"
403
+ );
404
+ }
405
+ const Pool = await importPostgresPoolConstructor();
406
+ activePool = new Pool({ connectionString });
407
+ ownsPool = true;
408
+ }
409
+ activeClient = await activePool.connect();
410
+ }
411
+ const result = await activeClient.query(
412
+ "SELECT pg_try_advisory_lock($1::integer, $2::integer) AS acquired",
413
+ [key1, key2]
414
+ );
415
+ if (!result.rows[0]?.acquired) {
416
+ if (activeClient !== client) {
417
+ activeClient.release?.();
418
+ activeClient = void 0;
419
+ }
420
+ if (ownsPool) {
421
+ await activePool?.end?.();
422
+ activePool = void 0;
423
+ }
424
+ throw new Error(
425
+ `Another Slack Socket Mode process appears to hold the Postgres advisory lock (${lockPath})`
426
+ );
427
+ }
428
+ logger?.info?.("Slack Socket Mode Postgres lock acquired", {
429
+ lockPath,
430
+ tokenHash
431
+ });
432
+ async function release() {
433
+ if (released) {
434
+ return;
435
+ }
436
+ released = true;
437
+ try {
438
+ const releaseResult = await activeClient?.query(
439
+ "SELECT pg_advisory_unlock($1::integer, $2::integer) AS released",
440
+ [key1, key2]
441
+ );
442
+ if (releaseResult?.rows[0]?.released === false) {
443
+ logger?.warn?.("Slack Socket Mode Postgres lock was not held", {
444
+ lockPath
445
+ });
446
+ }
447
+ logger?.debug?.("Slack Socket Mode Postgres lock released", {
448
+ lockPath
449
+ });
450
+ } finally {
451
+ if (activeClient !== client) {
452
+ activeClient?.release?.();
453
+ }
454
+ if (ownsPool) {
455
+ await activePool?.end?.();
456
+ }
457
+ }
458
+ }
459
+ return {
460
+ path: lockPath,
461
+ close: release,
462
+ release
463
+ };
464
+ } catch (error) {
465
+ if (activeClient && activeClient !== client) {
466
+ activeClient.release?.();
467
+ }
468
+ if (ownsPool) {
469
+ await activePool?.end?.();
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ function hashValue(value) {
475
+ return crypto2.createHash("sha256").update(value).digest("hex").slice(0, 16);
476
+ }
477
+ function advisoryLockKeys(value) {
478
+ const digest = crypto2.createHash("sha256").update(value).digest();
479
+ return [digest.readInt32BE(0), digest.readInt32BE(4)];
480
+ }
481
+ async function importPostgresPoolConstructor() {
482
+ const dynamicImport = new Function(
483
+ "specifier",
484
+ "return import(specifier)"
485
+ );
486
+ try {
487
+ const pg = await dynamicImport("pg");
488
+ return pg.Pool;
489
+ } catch (error) {
490
+ throw new Error(
491
+ `The "pg" package is required when using connectionString with acquireSlackSocketModePostgresLock. Install pg or pass a pool/client. ${formatImportError(
492
+ error
493
+ )}`
494
+ );
495
+ }
496
+ }
497
+ function formatImportError(error) {
498
+ return error instanceof Error ? error.message : String(error);
499
+ }
500
+
366
501
  // src/bolt/socket-runtime.ts
367
502
  var DEFAULT_CLIENT_PING_TIMEOUT_MS = 3e4;
368
503
  var DEFAULT_RESTART_GUARD_MAX_WARNINGS = 3;
@@ -393,10 +528,11 @@ function createSlackSocketModeRuntime({
393
528
  runtimePolicy = "single-instance",
394
529
  serverPingTimeoutMs = DEFAULT_SERVER_PING_TIMEOUT_MS
395
530
  }) {
531
+ const processLockConfigured = lockEnabled && runtimePolicy === "single-instance";
396
532
  const lock = acquireSlackSocketModeProcessLock({
397
533
  appSlug,
398
534
  appToken,
399
- enabled: lockEnabled && runtimePolicy === "single-instance",
535
+ enabled: processLockConfigured,
400
536
  ...lockDir ? { lockDir } : {},
401
537
  ...logger ? { logger } : {}
402
538
  });
@@ -410,8 +546,9 @@ function createSlackSocketModeRuntime({
410
546
  logger?.info?.("Slack Socket Mode runtime guard configured", {
411
547
  autoReconnectEnabled,
412
548
  clientPingTimeoutMs,
413
- lockEnabled: Boolean(lock),
414
549
  pingPongLoggingEnabled,
550
+ processLockAcquired: Boolean(lock),
551
+ processLockConfigured,
415
552
  restartGuardEnabled,
416
553
  restartGuardMaxWarnings,
417
554
  restartGuardWindowMs,
@@ -659,6 +796,32 @@ var JsonFileSlackInstallationStore = class {
659
796
  function createJsonFileSlackInstallationStore(filePath) {
660
797
  return new JsonFileSlackInstallationStore(filePath);
661
798
  }
799
+ function createSlackInstallationStoreAuthorize(store) {
800
+ return async (source) => {
801
+ const installation = await store.fetchInstallation({
802
+ teamId: source.teamId,
803
+ enterpriseId: source.enterpriseId,
804
+ userId: source.userId,
805
+ conversationId: source.conversationId,
806
+ isEnterpriseInstall: source.isEnterpriseInstall
807
+ });
808
+ const botToken = installation.bot?.token;
809
+ if (!botToken) {
810
+ throw new Error(
811
+ "Slack OAuth installation is missing a bot token. Reinstall the Slack app with bot scopes."
812
+ );
813
+ }
814
+ return {
815
+ botToken,
816
+ botId: installation.bot?.id,
817
+ botUserId: installation.bot?.userId,
818
+ teamId: installation.team?.id ?? source.teamId,
819
+ enterpriseId: installation.enterprise?.id ?? source.enterpriseId,
820
+ userId: installation.user.id,
821
+ userToken: installation.user.token
822
+ };
823
+ };
824
+ }
662
825
  function getSlackInstallationKey(input) {
663
826
  if (input.isEnterpriseInstall && "enterprise" in input && input.enterprise?.id) {
664
827
  return `enterprise:${input.enterprise.id}`;
@@ -683,10 +846,12 @@ function isNodeErrorCode(error, code) {
683
846
  export {
684
847
  InMemorySlackInstallationStore,
685
848
  JsonFileSlackInstallationStore,
849
+ acquireSlackSocketModePostgresLock,
686
850
  acquireSlackSocketModeProcessLock,
687
851
  createInMemorySlackInstallationStore,
688
852
  createJsonFileSlackInstallationStore,
689
853
  createSlackBoltApp,
854
+ createSlackInstallationStoreAuthorize,
690
855
  createSlackSdkLogger,
691
856
  createSlackSocketBoltApp,
692
857
  createSlackSocketModeRestartGuard,