@cuylabs/channel-slack 0.5.1 → 0.7.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.
Files changed (66) hide show
  1. package/README.md +25 -136
  2. package/dist/app-home.d.ts +23 -0
  3. package/dist/app-home.js +40 -0
  4. package/dist/artifacts/index.d.ts +135 -0
  5. package/dist/artifacts/index.js +299 -0
  6. package/dist/{assistant.d.ts → assistant/index.d.ts} +1 -1
  7. package/dist/{assistant.js → assistant/index.js} +2 -2
  8. package/dist/auth/index.d.ts +56 -0
  9. package/dist/auth/index.js +168 -0
  10. package/dist/{chunk-IDVDMJ5U.js → chunk-6JSGIVQH.js} +110 -3
  11. package/dist/chunk-6WHFQUYQ.js +54 -0
  12. package/dist/{bolt.js → chunk-73QXT7MA.js} +25 -320
  13. package/dist/{chunk-CMR6B76C.js → chunk-DNVSH7H5.js} +407 -1
  14. package/dist/chunk-IRFKUPJN.js +235 -0
  15. package/dist/chunk-QJYCHWN6.js +76 -0
  16. package/dist/chunk-S3SWPYXJ.js +81 -0
  17. package/dist/{chunk-JZG4IETE.js → chunk-X4WBBBYM.js} +0 -52
  18. package/dist/core.js +5 -3
  19. package/dist/diagnostics/index.d.ts +71 -0
  20. package/dist/{diagnostics.js → diagnostics/index.js} +5 -1
  21. package/dist/entrypoints/index.d.ts +120 -0
  22. package/dist/entrypoints/index.js +132 -0
  23. package/dist/{feedback.js → feedback/index.js} +5 -7
  24. package/dist/{history.d.ts → history/index.d.ts} +2 -2
  25. package/dist/{history.js → history/index.js} +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +28 -15
  28. package/dist/{policy.d.ts → policy/index.d.ts} +103 -2
  29. package/dist/{policy.js → policy/index.js} +13 -1
  30. package/dist/runtime-BNBHOZSQ.d.ts +53 -0
  31. package/dist/{setup.d.ts → setup/index.d.ts} +30 -3
  32. package/dist/{setup.js → setup/index.js} +137 -3
  33. package/dist/transports/http/index.d.ts +68 -0
  34. package/dist/transports/http/index.js +8 -0
  35. package/dist/transports/index.d.ts +8 -0
  36. package/dist/transports/index.js +24 -0
  37. package/dist/transports/socket/index.d.ts +94 -0
  38. package/dist/transports/socket/index.js +19 -0
  39. package/dist/types-B9NfCVrk.d.ts +141 -0
  40. package/dist/views/index.d.ts +98 -0
  41. package/dist/views/index.js +22 -0
  42. package/docs/README.md +32 -0
  43. package/docs/concepts/activity.md +3 -3
  44. package/docs/concepts/artifacts.md +56 -0
  45. package/docs/concepts/entrypoints.md +73 -0
  46. package/docs/concepts/setup-requirements.md +23 -0
  47. package/docs/concepts/{bolt-runtime.md → transport-runtime.md} +9 -4
  48. package/docs/concepts/views.md +46 -0
  49. package/docs/recipes/generate-slack-manifest.md +16 -0
  50. package/docs/recipes/publish-artifact.md +45 -0
  51. package/docs/recipes/slash-command-and-shortcut.md +51 -0
  52. package/docs/recipes/socket-mode-app.md +1 -1
  53. package/docs/reference/channel-slack-boundary.md +10 -6
  54. package/docs/reference/exports.md +18 -12
  55. package/docs/reference/source-layout.md +36 -0
  56. package/package.json +68 -39
  57. package/dist/bolt.d.ts +0 -364
  58. package/dist/chunk-NE57BLLU.js +0 -0
  59. package/dist/diagnostics.d.ts +0 -22
  60. package/dist/shared.d.ts +0 -2
  61. package/dist/shared.js +0 -43
  62. /package/dist/{feedback.d.ts → feedback/index.d.ts} +0 -0
  63. /package/dist/{targets.d.ts → targets/index.d.ts} +0 -0
  64. /package/dist/{targets.js → targets/index.js} +0 -0
  65. /package/dist/{users.d.ts → users/index.d.ts} +0 -0
  66. /package/dist/{users.js → users/index.js} +0 -0
@@ -651,6 +651,406 @@ function createSlackMessagePolicyResolver(config = {}) {
651
651
  };
652
652
  }
653
653
 
654
+ // src/policy/thread-participation.ts
655
+ var DEFAULT_MAX_THREADS = 1e4;
656
+ var DEFAULT_POSTGRES_TABLE = "channel_slack_thread_participation";
657
+ var DEFAULT_POSTGRES_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
658
+ var DEFAULT_POSTGRES_PRUNE_BATCH_SIZE = 1e3;
659
+ var DEFAULT_POSTGRES_PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1e3;
660
+ function resolveSlackThreadParticipationEligibility({
661
+ activity,
662
+ policy,
663
+ stateStore
664
+ }) {
665
+ if (!stateStore) {
666
+ return { available: false, reason: "state-store-unavailable" };
667
+ }
668
+ if (activity.channelType === "dm") {
669
+ return { available: false, reason: "direct-message" };
670
+ }
671
+ if (activity.channelType !== "channel" && activity.channelType !== "thread") {
672
+ return { available: false, reason: "unsupported-channel-type" };
673
+ }
674
+ if (policy.messagePolicy !== "mentioned-threads") {
675
+ return {
676
+ available: false,
677
+ reason: "message-policy-not-mentioned-threads"
678
+ };
679
+ }
680
+ if (policy.threadReplyPolicy === "mention-required") {
681
+ return {
682
+ available: false,
683
+ reason: "thread-replies-require-mention"
684
+ };
685
+ }
686
+ const threadTs = activity.threadTs ?? activity.messageTs;
687
+ if (!threadTs) {
688
+ return { available: false, reason: "missing-thread-timestamp" };
689
+ }
690
+ return {
691
+ available: true,
692
+ threadKey: createSlackMessagePolicyThreadKey(activity),
693
+ threadTs
694
+ };
695
+ }
696
+ function createInMemorySlackThreadParticipationStateStore({
697
+ maxThreads = DEFAULT_MAX_THREADS,
698
+ now = () => /* @__PURE__ */ new Date()
699
+ } = {}) {
700
+ const states = /* @__PURE__ */ new Map();
701
+ const order = [];
702
+ const resolvedMaxThreads = Math.max(1, maxThreads);
703
+ function rememberKey(key) {
704
+ if (!states.has(key)) {
705
+ order.push(key);
706
+ }
707
+ while (order.length > resolvedMaxThreads) {
708
+ const oldest = order.shift();
709
+ if (oldest !== void 0) {
710
+ states.delete(oldest);
711
+ }
712
+ }
713
+ }
714
+ return {
715
+ async get(key) {
716
+ return states.get(key);
717
+ },
718
+ async setQuiet(key, state) {
719
+ rememberKey(key);
720
+ states.set(key, {
721
+ ...state,
722
+ mode: "quiet",
723
+ updatedAt: now().toISOString()
724
+ });
725
+ },
726
+ async clear(key) {
727
+ states.delete(key);
728
+ const index = order.indexOf(key);
729
+ if (index >= 0) {
730
+ order.splice(index, 1);
731
+ }
732
+ },
733
+ async prune() {
734
+ return { quietThreadsDeleted: 0 };
735
+ }
736
+ };
737
+ }
738
+ function createPostgresSlackThreadParticipationStateStore({
739
+ client,
740
+ connectionString,
741
+ ensureSchema = true,
742
+ onPruneError,
743
+ pruneBatchSize = DEFAULT_POSTGRES_PRUNE_BATCH_SIZE,
744
+ pruneIntervalMs = DEFAULT_POSTGRES_PRUNE_INTERVAL_MS,
745
+ retentionMs = DEFAULT_POSTGRES_RETENTION_MS,
746
+ schema,
747
+ tableName = DEFAULT_POSTGRES_TABLE
748
+ }) {
749
+ let activeClient = client;
750
+ let ownsClient = false;
751
+ let initialized;
752
+ let pruneTimer;
753
+ async function getClient() {
754
+ if (activeClient) {
755
+ return activeClient;
756
+ }
757
+ if (!connectionString) {
758
+ throw new Error(
759
+ "connectionString is required when a Postgres Slack thread participation client is not provided"
760
+ );
761
+ }
762
+ const Pool = await importPostgresPoolConstructor2();
763
+ activeClient = new Pool({ connectionString });
764
+ ownsClient = true;
765
+ return activeClient;
766
+ }
767
+ async function ensureInitialized() {
768
+ const currentClient = await getClient();
769
+ initialized ??= initializePostgresSlackThreadParticipationState({
770
+ client: currentClient,
771
+ ensureSchema,
772
+ schema,
773
+ tableName
774
+ });
775
+ await initialized;
776
+ startPruneTimer();
777
+ return currentClient;
778
+ }
779
+ function startPruneTimer() {
780
+ if (pruneTimer || pruneIntervalMs <= 0) {
781
+ return;
782
+ }
783
+ pruneTimer = setInterval(() => {
784
+ void prune().catch((error) => {
785
+ onPruneError?.(error);
786
+ });
787
+ }, pruneIntervalMs);
788
+ pruneTimer.unref?.();
789
+ }
790
+ async function prune() {
791
+ const currentClient = await ensureInitialized();
792
+ return prunePostgresSlackThreadParticipationState({
793
+ client: currentClient,
794
+ pruneBatchSize,
795
+ retentionMs,
796
+ schema,
797
+ tableName
798
+ });
799
+ }
800
+ return {
801
+ async get(key) {
802
+ const currentClient = await ensureInitialized();
803
+ const result = await currentClient.query(
804
+ `SELECT
805
+ mode,
806
+ reason,
807
+ source_message_ts,
808
+ updated_at,
809
+ updated_by_user_id
810
+ FROM ${qualifiedTableName2({ schema, tableName })}
811
+ WHERE key = $1::text`,
812
+ [key]
813
+ );
814
+ const row = result.rows[0];
815
+ if (!row || row.mode !== "quiet") {
816
+ return void 0;
817
+ }
818
+ return {
819
+ mode: "quiet",
820
+ ...row.reason ? { reason: row.reason } : {},
821
+ ...row.source_message_ts ? { sourceMessageTs: row.source_message_ts } : {},
822
+ updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
823
+ ...row.updated_by_user_id ? { updatedByUserId: row.updated_by_user_id } : {}
824
+ };
825
+ },
826
+ async setQuiet(key, state, context) {
827
+ const currentClient = await ensureInitialized();
828
+ await currentClient.query(
829
+ `INSERT INTO ${qualifiedTableName2({ schema, tableName })} (
830
+ key,
831
+ team_id,
832
+ channel_id,
833
+ thread_ts,
834
+ mode,
835
+ reason,
836
+ updated_by_user_id,
837
+ source_message_ts,
838
+ updated_at
839
+ )
840
+ VALUES ($1::text, $2::text, $3::text, $4::text, 'quiet', $5::text, $6::text, $7::text, now())
841
+ ON CONFLICT (key) DO UPDATE SET
842
+ team_id = EXCLUDED.team_id,
843
+ channel_id = EXCLUDED.channel_id,
844
+ thread_ts = EXCLUDED.thread_ts,
845
+ mode = 'quiet',
846
+ reason = EXCLUDED.reason,
847
+ updated_by_user_id = EXCLUDED.updated_by_user_id,
848
+ source_message_ts = EXCLUDED.source_message_ts,
849
+ updated_at = now()`,
850
+ [
851
+ key,
852
+ context.activity.teamId ?? null,
853
+ context.activity.channelId,
854
+ context.activity.threadTs ?? context.activity.messageTs ?? null,
855
+ state.reason ?? null,
856
+ state.updatedByUserId ?? null,
857
+ state.sourceMessageTs ?? context.activity.messageTs ?? null
858
+ ]
859
+ );
860
+ },
861
+ async clear(key) {
862
+ const currentClient = await ensureInitialized();
863
+ await currentClient.query(
864
+ `DELETE FROM ${qualifiedTableName2({ schema, tableName })}
865
+ WHERE key = $1::text`,
866
+ [key]
867
+ );
868
+ },
869
+ prune,
870
+ async close() {
871
+ if (pruneTimer) {
872
+ clearInterval(pruneTimer);
873
+ pruneTimer = void 0;
874
+ }
875
+ if (ownsClient) {
876
+ await activeClient?.end?.();
877
+ }
878
+ }
879
+ };
880
+ }
881
+ async function initializePostgresSlackThreadParticipationState({
882
+ client,
883
+ ensureSchema = true,
884
+ schema,
885
+ tableName = DEFAULT_POSTGRES_TABLE
886
+ }) {
887
+ if (schema && ensureSchema) {
888
+ await client.query(
889
+ `CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier2(schema)}`
890
+ );
891
+ }
892
+ const table = qualifiedTableName2({ schema, tableName });
893
+ await client.query(`
894
+ CREATE TABLE IF NOT EXISTS ${table} (
895
+ key text PRIMARY KEY,
896
+ team_id text,
897
+ channel_id text NOT NULL,
898
+ thread_ts text,
899
+ mode text NOT NULL CHECK (mode IN ('quiet')),
900
+ reason text,
901
+ updated_by_user_id text,
902
+ source_message_ts text,
903
+ updated_at timestamptz NOT NULL DEFAULT now()
904
+ )
905
+ `);
906
+ const indexPrefix = slackThreadParticipationIndexPrefix({
907
+ schema,
908
+ tableName
909
+ });
910
+ await client.query(
911
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier2(
912
+ `${indexPrefix}_updated_idx`
913
+ )} ON ${table} (updated_at DESC)`
914
+ );
915
+ await client.query(
916
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier2(
917
+ `${indexPrefix}_channel_thread_idx`
918
+ )} ON ${table} (channel_id, thread_ts)`
919
+ );
920
+ }
921
+ async function prunePostgresSlackThreadParticipationState({
922
+ client,
923
+ pruneBatchSize = DEFAULT_POSTGRES_PRUNE_BATCH_SIZE,
924
+ retentionMs = DEFAULT_POSTGRES_RETENTION_MS,
925
+ schema,
926
+ tableName = DEFAULT_POSTGRES_TABLE
927
+ }) {
928
+ if (retentionMs <= 0) {
929
+ return { quietThreadsDeleted: 0 };
930
+ }
931
+ const batchSize = Math.max(1, Math.floor(pruneBatchSize));
932
+ const table = qualifiedTableName2({ schema, tableName });
933
+ const result = await client.query(
934
+ `WITH expired AS (
935
+ SELECT key
936
+ FROM ${table}
937
+ WHERE updated_at < now() - ($1::bigint * interval '1 millisecond')
938
+ ORDER BY updated_at ASC
939
+ LIMIT $2::integer
940
+ )
941
+ DELETE FROM ${table} target
942
+ USING expired
943
+ WHERE target.key = expired.key`,
944
+ [Math.max(1, Math.floor(retentionMs)), batchSize]
945
+ );
946
+ return { quietThreadsDeleted: result.rowCount ?? 0 };
947
+ }
948
+ function qualifiedTableName2({
949
+ schema,
950
+ tableName
951
+ }) {
952
+ return schema ? `${quoteIdentifier2(schema)}.${quoteIdentifier2(tableName)}` : quoteIdentifier2(tableName);
953
+ }
954
+ function quoteIdentifier2(value) {
955
+ return `"${value.replace(/"/g, '""')}"`;
956
+ }
957
+ function slackThreadParticipationIndexPrefix({
958
+ schema,
959
+ tableName
960
+ }) {
961
+ const raw = [schema, tableName].filter(Boolean).join("_");
962
+ return raw.replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "channel_slack_thread_participation";
963
+ }
964
+ async function importPostgresPoolConstructor2() {
965
+ const dynamicImport = new Function(
966
+ "specifier",
967
+ "return import(specifier)"
968
+ );
969
+ try {
970
+ const pg = await dynamicImport("pg");
971
+ return pg.Pool;
972
+ } catch (error) {
973
+ throw new Error(
974
+ `The "pg" package is required when using connectionString with createPostgresSlackThreadParticipationStateStore. Install pg or pass a client. ${formatImportError2(
975
+ error
976
+ )}`
977
+ );
978
+ }
979
+ }
980
+ function formatImportError2(error) {
981
+ return error instanceof Error ? error.message : String(error);
982
+ }
983
+
984
+ // src/policy/thread-message-policy.ts
985
+ function createAsyncSlackThreadAwareMessagePolicyResolver(config = {}) {
986
+ const { threadParticipationStore, ...messagePolicyConfig } = config;
987
+ const resolver = createAsyncSlackMessagePolicyResolver(messagePolicyConfig);
988
+ async function resolve(activity) {
989
+ const threadParticipation = await resolveThreadParticipationState({
990
+ activity,
991
+ config,
992
+ stateStore: threadParticipationStore
993
+ });
994
+ if (threadParticipation?.quiet) {
995
+ if (!activity.isMention) {
996
+ return {
997
+ accepted: false,
998
+ activity,
999
+ reason: "quiet-thread",
1000
+ threadKey: threadParticipation.key,
1001
+ ...threadParticipation.reason ? { threadParticipationReason: threadParticipation.reason } : {},
1002
+ ...threadParticipation.threadTs ? { threadTs: threadParticipation.threadTs } : {}
1003
+ };
1004
+ }
1005
+ await threadParticipationStore?.clear(threadParticipation.key, {
1006
+ activity,
1007
+ key: threadParticipation.key
1008
+ });
1009
+ }
1010
+ const decision = await resolver.resolve(activity);
1011
+ if (!threadParticipation?.quiet || !activity.isMention) {
1012
+ return decision;
1013
+ }
1014
+ return {
1015
+ ...decision,
1016
+ reactivatedThreadParticipation: {
1017
+ threadKey: threadParticipation.key,
1018
+ ...threadParticipation.reason ? { reason: threadParticipation.reason } : {},
1019
+ ...threadParticipation.threadTs ? { threadTs: threadParticipation.threadTs } : {}
1020
+ }
1021
+ };
1022
+ }
1023
+ return {
1024
+ resolve,
1025
+ async resolveMessage(activity) {
1026
+ const decision = await resolve(activity);
1027
+ return decision.accepted ? decision.text : void 0;
1028
+ }
1029
+ };
1030
+ }
1031
+ async function resolveThreadParticipationState({
1032
+ activity,
1033
+ config,
1034
+ stateStore
1035
+ }) {
1036
+ const eligibility = resolveSlackThreadParticipationEligibility({
1037
+ activity,
1038
+ policy: config,
1039
+ stateStore
1040
+ });
1041
+ if (!eligibility.available || !eligibility.threadKey || !stateStore) {
1042
+ return void 0;
1043
+ }
1044
+ const key = eligibility.threadKey;
1045
+ const state = await stateStore.get(key, { activity, key });
1046
+ return state?.mode === "quiet" ? {
1047
+ key,
1048
+ quiet: true,
1049
+ ...state.reason ? { reason: state.reason } : {},
1050
+ ...eligibility.threadTs ? { threadTs: eligibility.threadTs } : {}
1051
+ } : void 0;
1052
+ }
1053
+
654
1054
  export {
655
1055
  createSlackMessagePolicyMessageKey,
656
1056
  createSlackMessagePolicyThreadKey,
@@ -660,5 +1060,11 @@ export {
660
1060
  initializePostgresSlackMessagePolicyState,
661
1061
  prunePostgresSlackMessagePolicyState,
662
1062
  shouldRegisterSlackPassiveChannelMessages,
663
- createSlackMessagePolicyResolver
1063
+ createSlackMessagePolicyResolver,
1064
+ resolveSlackThreadParticipationEligibility,
1065
+ createInMemorySlackThreadParticipationStateStore,
1066
+ createPostgresSlackThreadParticipationStateStore,
1067
+ initializePostgresSlackThreadParticipationState,
1068
+ prunePostgresSlackThreadParticipationState,
1069
+ createAsyncSlackThreadAwareMessagePolicyResolver
664
1070
  };
@@ -0,0 +1,235 @@
1
+ // src/views/client.ts
2
+ async function openSlackModal({
3
+ client,
4
+ triggerId,
5
+ view,
6
+ token
7
+ }) {
8
+ return await requireViewsMethod(
9
+ client,
10
+ "open"
11
+ )({
12
+ ...tokenArgs(token),
13
+ trigger_id: requireTrimmed(triggerId, "triggerId"),
14
+ view
15
+ });
16
+ }
17
+ async function pushSlackModal({
18
+ client,
19
+ triggerId,
20
+ view,
21
+ token
22
+ }) {
23
+ return await requireViewsMethod(
24
+ client,
25
+ "push"
26
+ )({
27
+ ...tokenArgs(token),
28
+ trigger_id: requireTrimmed(triggerId, "triggerId"),
29
+ view
30
+ });
31
+ }
32
+ async function updateSlackView({
33
+ client,
34
+ view,
35
+ viewId,
36
+ externalId,
37
+ hash,
38
+ token
39
+ }) {
40
+ const normalizedViewId = trimOptional(viewId);
41
+ const normalizedExternalId = trimOptional(externalId);
42
+ if (!normalizedViewId && !normalizedExternalId) {
43
+ throw new Error("updateSlackView requires viewId or externalId.");
44
+ }
45
+ return await requireViewsMethod(
46
+ client,
47
+ "update"
48
+ )({
49
+ ...tokenArgs(token),
50
+ ...normalizedViewId ? { view_id: normalizedViewId } : {},
51
+ ...normalizedExternalId ? { external_id: normalizedExternalId } : {},
52
+ ...trimOptional(hash) ? { hash: hash?.trim() } : {},
53
+ view
54
+ });
55
+ }
56
+ async function publishSlackHomeView({
57
+ client,
58
+ userId,
59
+ view,
60
+ hash,
61
+ token
62
+ }) {
63
+ return await requireViewsMethod(
64
+ client,
65
+ "publish"
66
+ )({
67
+ ...tokenArgs(token),
68
+ user_id: requireTrimmed(userId, "userId"),
69
+ ...trimOptional(hash) ? { hash: hash?.trim() } : {},
70
+ view
71
+ });
72
+ }
73
+ function requireViewsMethod(client, methodName) {
74
+ const method = client.views?.[methodName];
75
+ if (typeof method !== "function") {
76
+ throw new Error(`Slack client does not expose views.${methodName}.`);
77
+ }
78
+ return method.bind(client.views);
79
+ }
80
+ function requireTrimmed(value, label) {
81
+ const trimmed = value.trim();
82
+ if (!trimmed) {
83
+ throw new Error(`${label} is required.`);
84
+ }
85
+ return trimmed;
86
+ }
87
+ function trimOptional(value) {
88
+ const trimmed = value?.trim();
89
+ return trimmed ? trimmed : void 0;
90
+ }
91
+ function tokenArgs(token) {
92
+ const trimmed = token?.trim();
93
+ return trimmed ? { token: trimmed } : {};
94
+ }
95
+
96
+ // src/views/metadata.ts
97
+ var SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS = 3e3;
98
+ function encodeSlackViewPrivateMetadata(value) {
99
+ if (value === void 0 || value === null) {
100
+ return "";
101
+ }
102
+ const encoded = typeof value === "string" ? value : JSON.stringify(value);
103
+ if (encoded.length > SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS) {
104
+ throw new Error(
105
+ `Slack view private_metadata exceeds ${SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS} characters.`
106
+ );
107
+ }
108
+ return encoded;
109
+ }
110
+ function decodeSlackViewPrivateMetadata(privateMetadata) {
111
+ const normalized = privateMetadata?.trim();
112
+ if (!normalized) {
113
+ return void 0;
114
+ }
115
+ try {
116
+ return JSON.parse(normalized);
117
+ } catch {
118
+ return normalized;
119
+ }
120
+ }
121
+ function readSlackViewPrivateMetadata(view) {
122
+ return decodeSlackViewPrivateMetadata(view.private_metadata);
123
+ }
124
+
125
+ // src/views/workflow.ts
126
+ function registerSlackViewWorkflow({
127
+ boltApp,
128
+ callbackId,
129
+ decodePrivateMetadata,
130
+ onSubmission,
131
+ onClose,
132
+ onError
133
+ }) {
134
+ if (onSubmission) {
135
+ boltApp.view(
136
+ { callback_id: callbackId, type: "view_submission" },
137
+ // Bolt's listener args are structurally stable, but the exact generic
138
+ // narrows differ between view_submission and view_closed.
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ async ({ ack, body, client, view }) => {
141
+ const baseContext = createWorkflowContext({
142
+ body,
143
+ client,
144
+ view
145
+ });
146
+ try {
147
+ const metadata = resolveMetadata(
148
+ decodePrivateMetadata,
149
+ view,
150
+ baseContext
151
+ );
152
+ const response = await onSubmission({
153
+ ...baseContext,
154
+ metadata
155
+ });
156
+ await ack(response);
157
+ } catch (error) {
158
+ try {
159
+ await ack();
160
+ } catch {
161
+ }
162
+ await onError?.(error, baseContext);
163
+ }
164
+ }
165
+ );
166
+ }
167
+ if (onClose) {
168
+ boltApp.view(
169
+ { callback_id: callbackId, type: "view_closed" },
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ async ({ ack, body, client, view }) => {
172
+ const baseContext = createWorkflowContext({
173
+ body,
174
+ client,
175
+ view
176
+ });
177
+ await ack();
178
+ try {
179
+ const metadata = resolveMetadata(
180
+ decodePrivateMetadata,
181
+ view,
182
+ baseContext
183
+ );
184
+ await onClose({
185
+ ...baseContext,
186
+ metadata
187
+ });
188
+ } catch (error) {
189
+ await onError?.(error, baseContext);
190
+ }
191
+ }
192
+ );
193
+ }
194
+ return { callbackId };
195
+ }
196
+ function createWorkflowContext({
197
+ body,
198
+ client,
199
+ view
200
+ }) {
201
+ const bodyRecord = isRecord(body) ? body : {};
202
+ const user = isRecord(bodyRecord.user) ? bodyRecord.user : {};
203
+ const team = isRecord(bodyRecord.team) ? bodyRecord.team : {};
204
+ const enterprise = isRecord(bodyRecord.enterprise) ? bodyRecord.enterprise : {};
205
+ return {
206
+ body,
207
+ client,
208
+ view,
209
+ ...typeof view.callback_id === "string" ? { callbackId: view.callback_id } : {},
210
+ ...typeof bodyRecord.trigger_id === "string" ? { triggerId: bodyRecord.trigger_id } : {},
211
+ actor: {
212
+ ...typeof user.id === "string" ? { userId: user.id } : {},
213
+ ...typeof team.id === "string" ? { teamId: team.id } : {},
214
+ ...typeof enterprise.id === "string" ? { enterpriseId: enterprise.id } : {}
215
+ }
216
+ };
217
+ }
218
+ function resolveMetadata(decodePrivateMetadata, view, context) {
219
+ return decodePrivateMetadata ? decodePrivateMetadata(view.private_metadata, context) : decodeSlackViewPrivateMetadata(view.private_metadata);
220
+ }
221
+ function isRecord(value) {
222
+ return typeof value === "object" && value !== null;
223
+ }
224
+
225
+ export {
226
+ openSlackModal,
227
+ pushSlackModal,
228
+ updateSlackView,
229
+ publishSlackHomeView,
230
+ SLACK_VIEW_PRIVATE_METADATA_MAX_CHARS,
231
+ encodeSlackViewPrivateMetadata,
232
+ decodeSlackViewPrivateMetadata,
233
+ readSlackViewPrivateMetadata,
234
+ registerSlackViewWorkflow
235
+ };