@nuvlore/extension-access-okta-slack 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +52 -0
  3. package/package.json +64 -0
  4. package/src/slackApi.mjs +68 -0
  5. package/src/slackBotChat.mjs +129 -0
  6. package/src/slackBotRead.mjs +106 -0
  7. package/src/slackCanvasCreate.mjs +165 -0
  8. package/src/slackConfig.mjs +18 -0
  9. package/src/slackMessageSend.mjs +71 -0
  10. package/src/slackSearch.mjs +103 -0
  11. package/src/slackSessionRead.mjs +200 -0
  12. package/src/slackSyncExport.mjs +346 -0
  13. package/src/slackSyncPaths.mjs +47 -0
  14. package/src/slackSyncState.mjs +35 -0
  15. package/tools/okta_slack_okta_read.mjs +41 -0
  16. package/tools/okta_slack_okta_verify.mjs +26 -0
  17. package/tools/slack_okta_ai_capabilities.mjs +26 -0
  18. package/tools/slack_okta_bot_ask.mjs +42 -0
  19. package/tools/slack_okta_bot_history.mjs +32 -0
  20. package/tools/slack_okta_create_canvas.mjs +38 -0
  21. package/tools/slack_okta_search_all.mjs +33 -0
  22. package/tools/slack_okta_search_messages.mjs +34 -0
  23. package/tools/slack_okta_send_message.mjs +42 -0
  24. package/tools/slack_okta_sync_all.mjs +35 -0
  25. package/tools/slack_okta_sync_status.mjs +26 -0
  26. package/vendor/extension-sso/LICENSE +202 -0
  27. package/vendor/extension-sso/README.md +35 -0
  28. package/vendor/extension-sso/commands/okta-login.md +10 -0
  29. package/vendor/extension-sso/package.json +53 -0
  30. package/vendor/extension-sso/skills/workday-okta-auth/SKILL.md +20 -0
  31. package/vendor/extension-sso/src/autoAuth.mjs +25 -0
  32. package/vendor/extension-sso/src/browserLogin.mjs +1 -0
  33. package/vendor/extension-sso/src/chromeCookies.mjs +1 -0
  34. package/vendor/extension-sso/src/cookieJar.mjs +1 -0
  35. package/vendor/extension-sso/src/extensionAccess.mjs +173 -0
  36. package/vendor/extension-sso/src/htmlSessionExtract.mjs +1 -0
  37. package/vendor/extension-sso/src/oktaAppAccess.mjs +133 -0
  38. package/vendor/extension-sso/src/oktaAppCatalog.mjs +118 -0
  39. package/vendor/extension-sso/src/oktaAppRead.mjs +119 -0
  40. package/vendor/extension-sso/src/oktaConfig.mjs +12 -0
  41. package/vendor/extension-sso/src/oktaSession.mjs +32 -0
  42. package/vendor/extension-sso/src/serviceOperations.mjs +407 -0
  43. package/vendor/extension-sso/src/serviceReadSearch.mjs +395 -0
  44. package/vendor/extension-sso/src/serviceReads.mjs +109 -0
  45. package/vendor/extension-sso/src/serviceRegistry.mjs +497 -0
  46. package/vendor/extension-sso/src/sessionAuth.mjs +339 -0
  47. package/vendor/extension-sso/src/sessionManager.mjs +212 -0
  48. package/vendor/extension-sso/src/sessionValidate.mjs +74 -0
  49. package/vendor/extension-sso/src/ssoConfig.mjs +31 -0
  50. package/vendor/extension-sso/src/ssoHttpRead.mjs +292 -0
  51. package/vendor/extension-sso/tools/extension-access-tools.shared.mjs +16 -0
  52. package/vendor/extension-sso/tools/okta-app-tools.shared.mjs +5 -0
  53. package/vendor/extension-sso/tools/okta-tools.shared.mjs +18 -0
  54. package/vendor/extension-sso/tools/workday_extension_read.mjs +51 -0
  55. package/vendor/extension-sso/tools/workday_extension_read_access.mjs +65 -0
  56. package/vendor/extension-sso/tools/workday_list_extension_access.mjs +39 -0
  57. package/vendor/extension-sso/tools/workday_list_okta_apps.mjs +18 -0
  58. package/vendor/extension-sso/tools/workday_list_service_operations.mjs +41 -0
  59. package/vendor/extension-sso/tools/workday_list_service_read_search_capabilities.mjs +34 -0
  60. package/vendor/extension-sso/tools/workday_list_services.mjs +31 -0
  61. package/vendor/extension-sso/tools/workday_okta_app_probe_all.mjs +24 -0
  62. package/vendor/extension-sso/tools/workday_okta_app_read.mjs +30 -0
  63. package/vendor/extension-sso/tools/workday_okta_app_read_access.mjs +25 -0
  64. package/vendor/extension-sso/tools/workday_okta_auth_header.mjs +58 -0
  65. package/vendor/extension-sso/tools/workday_okta_login.mjs +65 -0
  66. package/vendor/extension-sso/tools/workday_okta_open_app.mjs +63 -0
  67. package/vendor/extension-sso/tools/workday_okta_open_service.mjs +63 -0
  68. package/vendor/extension-sso/tools/workday_okta_status.mjs +35 -0
  69. package/vendor/extension-sso/tools/workday_service_auth.mjs +65 -0
  70. package/vendor/extension-sso/tools/workday_service_operation_read.mjs +51 -0
  71. package/vendor/extension-sso/tools/workday_service_read.mjs +56 -0
  72. package/vendor/extension-sso/tools/workday_service_search.mjs +62 -0
  73. package/vendor/extension-sso/tools/workday_sso_cookie_header.mjs +59 -0
  74. package/vendor/extension-sso/vendor/extension-browser-sso/LICENSE +202 -0
  75. package/vendor/extension-sso/vendor/extension-browser-sso/README.md +16 -0
  76. package/vendor/extension-sso/vendor/extension-browser-sso/package.json +47 -0
  77. package/vendor/extension-sso/vendor/extension-browser-sso/src/autoAuth.mjs +47 -0
  78. package/vendor/extension-sso/vendor/extension-browser-sso/src/browserHttpRead.mjs +76 -0
  79. package/vendor/extension-sso/vendor/extension-browser-sso/src/browserLogin.mjs +24 -0
  80. package/vendor/extension-sso/vendor/extension-browser-sso/src/chromeCookies.mjs +192 -0
  81. package/vendor/extension-sso/vendor/extension-browser-sso/src/cookieJar.mjs +132 -0
  82. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-data-paths.mjs +222 -0
  83. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-diagnostics.mjs +440 -0
  84. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/enterprise-api-read.mjs +180 -0
  85. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-command-pack.mjs +1 -0
  86. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-plan.mjs +45 -0
  87. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.d.mts +26 -0
  88. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.mjs +170 -0
  89. package/vendor/extension-sso/vendor/extension-browser-sso/src/htmlSessionExtract.mjs +64 -0
  90. package/vendor/extension-sso/vendor/extension-read-only-toolkit/LICENSE +202 -0
  91. package/vendor/extension-sso/vendor/extension-read-only-toolkit/README.md +29 -0
  92. package/vendor/extension-sso/vendor/extension-read-only-toolkit/package.json +43 -0
  93. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
  94. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
  95. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
  96. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
  97. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
  98. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
  99. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
@@ -0,0 +1,165 @@
1
+ import { loadSlackSession } from "./slackSessionRead.mjs";
2
+ import { readTokenFromKeychain } from "@nuvlore/extension-read-only-toolkit/enterprise-api-read";
3
+
4
+ const CHANNEL_ID_PATTERN = /^[CG][A-Z0-9]+$/;
5
+ const DEFAULT_KEYCHAIN_CANDIDATES = [
6
+ { service: "nuvlore", account: "nuvlore.slack_canvas" },
7
+ { service: "nuvlore", account: "nuvlore.slack" },
8
+ { service: "workday", account: "workday.slack_canvas" },
9
+ { service: "devops-tool", account: "devops-tool.slack_canvas" }
10
+ ];
11
+
12
+ function resolveEnvCanvasToken(options = {}) {
13
+ const env = options.env ?? process.env;
14
+ return (
15
+ options.token ||
16
+ env.WORKDAY_SLACK_CANVAS_TOKEN ||
17
+ env.SLACK_CANVAS_TOKEN ||
18
+ env.WORKDAY_SLACK_BOT_TOKEN ||
19
+ env.SLACK_BOT_TOKEN ||
20
+ ""
21
+ );
22
+ }
23
+
24
+ function parseKeychainCandidates(options = {}) {
25
+ const env = options.env ?? process.env;
26
+ if (Array.isArray(options.keychainCandidates)) return options.keychainCandidates;
27
+ const service = options.keychainService || env.WORKDAY_SLACK_CANVAS_KEYCHAIN_SERVICE;
28
+ const account = options.keychainAccount || env.WORKDAY_SLACK_CANVAS_KEYCHAIN_ACCOUNT;
29
+ if (service && account) return [{ service, account }];
30
+ return DEFAULT_KEYCHAIN_CANDIDATES;
31
+ }
32
+
33
+ async function resolveCanvasToken(options = {}) {
34
+ const envToken = resolveEnvCanvasToken(options);
35
+ if (envToken) return { token: envToken, tokenSource: options.token ? "input" : "env" };
36
+
37
+ for (const candidate of parseKeychainCandidates(options)) {
38
+ try {
39
+ const token = await readTokenFromKeychain({
40
+ execFileImpl: options.execFileImpl,
41
+ keychainService: candidate.service,
42
+ keychainAccount: candidate.account
43
+ });
44
+ if (token) {
45
+ return {
46
+ token,
47
+ tokenSource: `keychain:${candidate.service}/${candidate.account}`
48
+ };
49
+ }
50
+ } catch {
51
+ // Try the next configured Keychain alias without exposing secret metadata in errors.
52
+ }
53
+ }
54
+
55
+ return { token: "", tokenSource: "" };
56
+ }
57
+
58
+ function normalizeCanvasInput(input = {}) {
59
+ const channelId = String(input.channelId ?? input.channel_id ?? "").trim();
60
+ const title = String(input.title ?? "").trim();
61
+ const content = String(input.content ?? input.markdown ?? "").trim();
62
+ const channelCanvas = input.channelCanvas ?? input.channel_canvas ?? true;
63
+
64
+ if (!channelId) throw new Error("Slack channelId is required.");
65
+ if (!CHANNEL_ID_PATTERN.test(channelId)) {
66
+ throw new Error("Slack channelId must be a channel conversation id such as C... or G...");
67
+ }
68
+ if (!title) throw new Error("Slack canvas title is required.");
69
+ if (!content) throw new Error("Slack canvas content is required.");
70
+ if (title.length > 255) throw new Error("Slack canvas title must be 255 characters or fewer.");
71
+
72
+ return {
73
+ channelId,
74
+ title,
75
+ content,
76
+ channelCanvas: Boolean(channelCanvas)
77
+ };
78
+ }
79
+
80
+ function canvasLink(baseUrl, canvasId) {
81
+ return `${baseUrl}/canvas/${canvasId}`;
82
+ }
83
+
84
+ async function postSlackCanvasApi({ baseUrl, token, cookieHeader, method, params, fetchImpl }) {
85
+ const headers = {
86
+ "content-type": "application/x-www-form-urlencoded"
87
+ };
88
+ const body = {
89
+ ...params,
90
+ document_content: JSON.stringify({
91
+ type: "markdown",
92
+ markdown: params.content
93
+ })
94
+ };
95
+ delete body.content;
96
+
97
+ if (cookieHeader) headers.cookie = cookieHeader;
98
+ if (token && !cookieHeader) headers.authorization = `Bearer ${token}`;
99
+ if (token && cookieHeader) body.token = token;
100
+
101
+ const response = await fetchImpl(`${baseUrl}/api/${method}`, {
102
+ method: "POST",
103
+ headers,
104
+ body: new URLSearchParams(body)
105
+ });
106
+ return response.json();
107
+ }
108
+
109
+ export async function createSlackCanvas(input, options = {}) {
110
+ const normalized = normalizeCanvasInput(input);
111
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
112
+ const resolved = await resolveCanvasToken(options);
113
+ const baseUrl = options.baseUrl ?? "https://workday.enterprise.slack.com";
114
+ const method = normalized.channelCanvas ? "conversations.canvases.create" : "canvases.create";
115
+ const params = {
116
+ channel_id: normalized.channelId,
117
+ title: normalized.title,
118
+ content: normalized.content
119
+ };
120
+
121
+ let tokenSource = resolved.tokenSource;
122
+ let token = resolved.token;
123
+ let cookieHeader = "";
124
+ let apiBaseUrl = baseUrl;
125
+
126
+ if (!token) {
127
+ const session = options.session ?? await loadSlackSession(options);
128
+ token = session.token;
129
+ cookieHeader = session.cookieHeader;
130
+ apiBaseUrl = session.baseUrl ?? baseUrl;
131
+ tokenSource = "browser-session";
132
+ }
133
+
134
+ const data = await postSlackCanvasApi({
135
+ baseUrl: apiBaseUrl,
136
+ token,
137
+ cookieHeader,
138
+ method,
139
+ params,
140
+ fetchImpl
141
+ });
142
+
143
+ if (!data.ok) {
144
+ const tokenHint =
145
+ data.error === "not_allowed_token_type" && tokenSource === "browser-session"
146
+ ? "Chrome Slack session tokens cannot create canvases through Slack Canvas API. Set WORKDAY_SLACK_CANVAS_TOKEN or SLACK_CANVAS_TOKEN, or save the token in Keychain as service=nuvlore account=nuvlore.slack_canvas, with canvases:write scope, then retry."
147
+ : undefined;
148
+ const error = new Error(
149
+ `Slack ${method} failed: ${data.error || "unknown_error"}${tokenHint ? `. ${tokenHint}` : ""}`
150
+ );
151
+ error.slackError = data.error;
152
+ error.detail = data.detail;
153
+ throw error;
154
+ }
155
+
156
+ return {
157
+ readOnly: false,
158
+ ok: true,
159
+ method,
160
+ channel: normalized.channelId,
161
+ canvasId: data.canvas_id,
162
+ canvasLink: data.canvas_id ? canvasLink(apiBaseUrl, data.canvas_id) : undefined,
163
+ tokenSource
164
+ };
165
+ }
@@ -0,0 +1,18 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
4
+ export function getSlackConfig(env = process.env) {
5
+ const baseDir = join(homedir(), ".nuvlore", "workday");
6
+ const exportDir = env.WORKDAY_SLACK_EXPORT_DIR?.trim() || join(baseDir, "slack-export");
7
+ const syncIntervalMs = Number(env.WORKDAY_SLACK_SYNC_INTERVAL_MS ?? 3_600_000);
8
+ const workspaceUrl =
9
+ env.WORKDAY_SLACK_WORKSPACE_URL?.trim() || "https://workday.enterprise.slack.com";
10
+
11
+ return {
12
+ exportDir,
13
+ syncIntervalMs,
14
+ workspaceUrl,
15
+ syncStatePath: join(exportDir, "sync-state.json"),
16
+ manifestPath: join(exportDir, "manifest.json")
17
+ };
18
+ }
@@ -0,0 +1,71 @@
1
+ import { loadSlackSession } from "./slackSessionRead.mjs";
2
+
3
+ const SLACK_TS_PATTERN = /^\d+\.\d+$/;
4
+
5
+ function normalizeMessageInput(input = {}) {
6
+ const channelId = String(input.channelId ?? input.channel_id ?? "").trim();
7
+ const message = String(input.message ?? input.text ?? "").trim();
8
+ const threadTs = input.threadTs ?? input.thread_ts;
9
+
10
+ if (!channelId) throw new Error("Slack channelId is required.");
11
+ if (!/^[A-Z][A-Z0-9]+$/.test(channelId)) {
12
+ throw new Error("Slack channelId must be a Slack conversation id such as C..., G..., or D...");
13
+ }
14
+ if (!message) throw new Error("Slack message is required.");
15
+ if (message.length > 5000) throw new Error("Slack message must be 5000 characters or fewer.");
16
+ if (threadTs && !SLACK_TS_PATTERN.test(String(threadTs))) {
17
+ throw new Error("Slack threadTs must be a Slack timestamp such as 1234567890.123456.");
18
+ }
19
+
20
+ return {
21
+ channelId,
22
+ message,
23
+ threadTs: threadTs ? String(threadTs) : undefined,
24
+ replyBroadcast: input.replyBroadcast ?? input.reply_broadcast,
25
+ unfurlLinks: input.unfurlLinks ?? input.unfurl_links ?? false,
26
+ unfurlMedia: input.unfurlMedia ?? input.unfurl_media ?? false
27
+ };
28
+ }
29
+
30
+ export function slackMessageLink(baseUrl, channelId, ts) {
31
+ return `${baseUrl}/archives/${channelId}/p${String(ts).replace(".", "")}`;
32
+ }
33
+
34
+ export async function sendSlackMessage(input, options = {}) {
35
+ const normalized = normalizeMessageInput(input);
36
+ const session = options.session ?? await loadSlackSession(options);
37
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
38
+
39
+ const params = {
40
+ token: session.token,
41
+ channel: normalized.channelId,
42
+ text: normalized.message,
43
+ unfurl_links: normalized.unfurlLinks ? "true" : "false",
44
+ unfurl_media: normalized.unfurlMedia ? "true" : "false"
45
+ };
46
+ if (normalized.threadTs) params.thread_ts = normalized.threadTs;
47
+ if (normalized.replyBroadcast !== undefined) {
48
+ params.reply_broadcast = normalized.replyBroadcast ? "true" : "false";
49
+ }
50
+
51
+ const response = await fetchImpl(`${session.baseUrl}/api/chat.postMessage`, {
52
+ method: "POST",
53
+ headers: {
54
+ cookie: session.cookieHeader,
55
+ "content-type": "application/x-www-form-urlencoded"
56
+ },
57
+ body: new URLSearchParams(params)
58
+ });
59
+ const data = await response.json();
60
+ if (!data.ok) {
61
+ throw new Error(`Slack chat.postMessage failed: ${data.error || "unknown_error"}`);
62
+ }
63
+
64
+ return {
65
+ readOnly: false,
66
+ ok: true,
67
+ channel: data.channel,
68
+ ts: data.ts,
69
+ messageLink: slackMessageLink(session.baseUrl, data.channel, data.ts)
70
+ };
71
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ createSlackApiClient,
3
+ loadSlackSession,
4
+ slackUserBoot
5
+ } from "./slackSessionRead.mjs";
6
+
7
+ function compactMatch(match) {
8
+ return {
9
+ type: match.type,
10
+ channel: match.channel?.name || match.channel?.id,
11
+ channelId: match.channel?.id,
12
+ user: match.username || match.user,
13
+ text: match.text,
14
+ ts: match.ts,
15
+ threadTs: match.thread_ts,
16
+ permalink: match.permalink
17
+ };
18
+ }
19
+
20
+ export async function searchSlackMessages(query, options = {}) {
21
+ const session = await loadSlackSession(options);
22
+ const slackApi = createSlackApiClient(session, options);
23
+ const result = await slackApi("search.messages", {
24
+ query: String(query),
25
+ count: String(options.count ?? 20),
26
+ sort: options.sort ?? "timestamp",
27
+ sort_dir: options.sortDir ?? "desc",
28
+ highlight: options.highlight ? "1" : "0"
29
+ });
30
+ return {
31
+ readOnly: true,
32
+ query,
33
+ total: result.messages?.total ?? 0,
34
+ matches: (result.messages?.matches ?? []).map(compactMatch)
35
+ };
36
+ }
37
+
38
+ export async function searchSlackAll(query, options = {}) {
39
+ const session = await loadSlackSession(options);
40
+ const slackApi = createSlackApiClient(session, options);
41
+ const result = await slackApi("search.all", {
42
+ query: String(query),
43
+ count: String(options.count ?? 20),
44
+ sort: options.sort ?? "timestamp",
45
+ sort_dir: options.sortDir ?? "desc"
46
+ });
47
+ return {
48
+ readOnly: true,
49
+ query,
50
+ messages: (result.messages?.matches ?? []).map(compactMatch),
51
+ files: result.files?.matches ?? [],
52
+ posts: result.posts?.matches ?? []
53
+ };
54
+ }
55
+
56
+ export async function searchSlackFiles(query, options = {}) {
57
+ const session = await loadSlackSession(options);
58
+ const slackApi = createSlackApiClient(session, options);
59
+ const result = await slackApi("search.files", {
60
+ query: String(query),
61
+ count: String(options.count ?? 20),
62
+ sort: options.sort ?? "timestamp",
63
+ sort_dir: options.sortDir ?? "desc"
64
+ });
65
+ return {
66
+ readOnly: true,
67
+ query,
68
+ total: result.files?.total ?? 0,
69
+ files: result.files?.matches ?? []
70
+ };
71
+ }
72
+
73
+ export async function searchSlackAutocomplete(query, options = {}) {
74
+ const session = await loadSlackSession(options);
75
+ const slackApi = createSlackApiClient(session, options);
76
+ const result = await slackApi("search.autocomplete", {
77
+ query: String(query),
78
+ types: options.types ?? "channels,users"
79
+ });
80
+ return { readOnly: true, query, results: result };
81
+ }
82
+
83
+ export async function getSlackAiCapabilities(options = {}) {
84
+ const session = await loadSlackSession(options);
85
+ const slackApi = createSlackApiClient(session, options);
86
+ const boot = await slackUserBoot(session, options);
87
+ const paidFeatures = boot.paid_features ?? [];
88
+ const hasSlackAi = paidFeatures.includes("slack_ai_enterprise_grid_workspaces");
89
+ let assistantSearch = null;
90
+ try {
91
+ assistantSearch = await slackApi("assistant.search.info", {});
92
+ } catch (error) {
93
+ assistantSearch = { ok: false, error: error.message };
94
+ }
95
+ return {
96
+ readOnly: true,
97
+ slackAiEnabled: hasSlackAi,
98
+ paidFeatures,
99
+ assistantSearch,
100
+ note:
101
+ "Browser session supports keyword search (search.messages). Slack AI semantic answers (assistant.search.context) require OAuth user tokens, not xoxc session tokens."
102
+ };
103
+ }
@@ -0,0 +1,200 @@
1
+ import { importCookiesFromChrome } from "@nuvlore/extension-sso/session-manager";
2
+ import { resolveAutoAuthOptions } from "@nuvlore/extension-sso/auto-auth";
3
+ import { ensureOktaAppSession, openOktaAppForAuth } from "@nuvlore/extension-sso/session-auth";
4
+ import { getSsoConfig } from "@nuvlore/extension-sso/sso-config";
5
+
6
+ /** Slack Web API methods that only fetch data (read-only session POST). */
7
+ export const SLACK_READ_ONLY_METHODS = new Set([
8
+ "client.userBoot",
9
+ "conversations.history",
10
+ "conversations.replies",
11
+ "conversations.info",
12
+ "users.info",
13
+ "files.info",
14
+ "search.messages",
15
+ "search.all",
16
+ "search.files",
17
+ "search.autocomplete",
18
+ "team.info"
19
+ ]);
20
+
21
+ const SLACK_DOMAINS = [
22
+ "workday.enterprise.slack.com",
23
+ "workday.slack.com",
24
+ "workday-dev.slack.com",
25
+ ".slack.com",
26
+ "slack.com",
27
+ "app.slack.com"
28
+ ];
29
+
30
+ export const DEFAULT_SLACK_WORKSPACE = "https://workday.enterprise.slack.com";
31
+
32
+ export function assertSlackReadOnlyMethod(method) {
33
+ const name = String(method || "").trim();
34
+ if (!SLACK_READ_ONLY_METHODS.has(name)) {
35
+ throw new Error(`Read-only Slack: method ${name} is not allowed.`);
36
+ }
37
+ return name;
38
+ }
39
+
40
+ export function buildSlackCookieHeader(cookies) {
41
+ return cookies
42
+ .filter((c) => c.domain?.includes("slack"))
43
+ .map((c) => `${c.name}=${c.value}`)
44
+ .join("; ");
45
+ }
46
+
47
+ export function extractSlackApiToken(html) {
48
+ const text = String(html || "");
49
+ return (
50
+ text.match(/"api_token":"(xoxc-[^"]+)"/)?.[1] ||
51
+ text.match(/(xoxc-[A-Za-z0-9-]+)/)?.[1] ||
52
+ ""
53
+ );
54
+ }
55
+
56
+ export async function loadSlackSession(options = {}) {
57
+ const config = options.config ?? getSsoConfig();
58
+ const authOptions = resolveAutoAuthOptions(options);
59
+ const baseUrl = options.baseUrl ?? DEFAULT_SLACK_WORKSPACE;
60
+ const host = new URL(baseUrl).host;
61
+
62
+ try {
63
+ await ensureOktaAppSession("Slack", { ...authOptions, config });
64
+ } catch {
65
+ // Cookie import below may still succeed when Okta probe is stale.
66
+ }
67
+
68
+ const imported = await importCookiesFromChrome(config, [host, ...SLACK_DOMAINS]);
69
+ const cookies = imported?.cookies ?? [];
70
+ const cookieHeader = buildSlackCookieHeader(cookies);
71
+ if (!cookieHeader) {
72
+ throw new Error("No Slack cookies in Chrome. Open Slack via Okta and retry.");
73
+ }
74
+
75
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
76
+
77
+ async function fetchSlackToken(cookieHdr) {
78
+ const browserHeaders = {
79
+ cookie: cookieHdr,
80
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
81
+ "accept-language": "en-US,en;q=0.9",
82
+ "user-agent":
83
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
84
+ };
85
+ for (const path of ["/messages", "/slack-ai", "/"]) {
86
+ const page = await fetchImpl(`${baseUrl}${path}`, { headers: browserHeaders });
87
+ const html = await page.text();
88
+ const token = extractSlackApiToken(html);
89
+ if (token) return token;
90
+ }
91
+ return "";
92
+ }
93
+
94
+ let activeCookieHeader = cookieHeader;
95
+ let token = await fetchSlackToken(activeCookieHeader);
96
+ if (!token && authOptions.openBrowserOnMissing) {
97
+ await openOktaAppForAuth("Slack", { ...authOptions, config });
98
+ const refreshed = await importCookiesFromChrome(config, [host, ...SLACK_DOMAINS]);
99
+ const refreshedHeader = buildSlackCookieHeader(refreshed?.cookies ?? []);
100
+ if (refreshedHeader) {
101
+ activeCookieHeader = refreshedHeader;
102
+ token = await fetchSlackToken(activeCookieHeader);
103
+ }
104
+ }
105
+ if (!token) {
106
+ throw new Error(
107
+ "Could not extract Slack session token (xoxc). Open Slack via Okta in Chrome, then retry."
108
+ );
109
+ }
110
+
111
+ return { baseUrl, host, cookieHeader: activeCookieHeader, token, config };
112
+ }
113
+
114
+ export function createSlackApiClient(session, options = {}) {
115
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
116
+ const rateLimitMs = options.rateLimitMs ?? 1200;
117
+ let lastCall = 0;
118
+
119
+ async function throttle() {
120
+ const wait = rateLimitMs - (Date.now() - lastCall);
121
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
122
+ lastCall = Date.now();
123
+ }
124
+
125
+ return async function slackApi(method, params = {}) {
126
+ assertSlackReadOnlyMethod(method);
127
+ await throttle();
128
+ const body = new URLSearchParams({ token: session.token, ...params });
129
+ const response = await fetchImpl(`${session.baseUrl}/api/${method}`, {
130
+ method: "POST",
131
+ headers: {
132
+ cookie: session.cookieHeader,
133
+ "content-type": "application/x-www-form-urlencoded"
134
+ },
135
+ body
136
+ });
137
+ const data = await response.json();
138
+ if (!data.ok) {
139
+ if (data.error === "ratelimited") {
140
+ const retryAfter = Number(response.headers.get("retry-after") || 30);
141
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
142
+ return slackApi(method, params);
143
+ }
144
+ throw new Error(`Slack ${method} failed: ${data.error || "unknown_error"}`);
145
+ }
146
+ return data;
147
+ };
148
+ }
149
+
150
+ export async function slackUserBoot(session, options = {}) {
151
+ const slackApi = createSlackApiClient(session, options);
152
+ return slackApi("client.userBoot", { version_ts: String(Date.now()) });
153
+ }
154
+
155
+ export function listChannelsFromBoot(boot) {
156
+ const channels = Object.values(boot.channels ?? {});
157
+ const ims = Object.values(boot.ims ?? {}).map((im) => ({
158
+ ...im,
159
+ is_im: true,
160
+ name: im.user ? `dm-${im.user}` : `im-${im.id}`
161
+ }));
162
+ const mpims = Object.values(boot.mpims ?? {}).map((mp) => ({
163
+ ...mp,
164
+ is_mpim: true,
165
+ name: mp.name || `mpim-${mp.id}`
166
+ }));
167
+ return [...channels, ...ims, ...mpims].filter((c) => c?.id);
168
+ }
169
+
170
+ export async function fetchAllHistory(slackApi, channelId, { oldest } = {}) {
171
+ const messages = [];
172
+ let cursor = "";
173
+ do {
174
+ const page = await slackApi("conversations.history", {
175
+ channel: channelId,
176
+ limit: "200",
177
+ ...(oldest ? { oldest: String(oldest) } : {}),
178
+ ...(cursor ? { cursor } : {})
179
+ });
180
+ messages.push(...(page.messages ?? []));
181
+ cursor = page.response_metadata?.next_cursor ?? "";
182
+ } while (cursor);
183
+ return messages;
184
+ }
185
+
186
+ export async function fetchAllReplies(slackApi, channelId, threadTs) {
187
+ const messages = [];
188
+ let cursor = "";
189
+ do {
190
+ const page = await slackApi("conversations.replies", {
191
+ channel: channelId,
192
+ ts: threadTs,
193
+ limit: "200",
194
+ ...(cursor ? { cursor } : {})
195
+ });
196
+ messages.push(...(page.messages ?? []));
197
+ cursor = page.response_metadata?.next_cursor ?? "";
198
+ } while (cursor);
199
+ return messages;
200
+ }