slackmark 0.5.0 → 0.6.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 (67) hide show
  1. package/README.md +29 -17
  2. package/dist/adapters/block-to-plain-text.d.ts +24 -0
  3. package/dist/adapters/block-to-plain-text.d.ts.map +1 -0
  4. package/dist/adapters/blockquote-adapter.d.ts.map +1 -1
  5. package/dist/adapters/code-fence-adapter.d.ts +1 -1
  6. package/dist/adapters/code-fence-adapter.d.ts.map +1 -1
  7. package/dist/adapters/inline.d.ts.map +1 -1
  8. package/dist/adapters/list-adapter.d.ts.map +1 -1
  9. package/dist/adapters/math-block-adapter.d.ts +1 -1
  10. package/dist/adapters/math-block-adapter.d.ts.map +1 -1
  11. package/dist/adapters/mermaid-fence-adapter.d.ts.map +1 -1
  12. package/dist/adapters/table-adapter.d.ts.map +1 -1
  13. package/dist/adapters/task-list-adapter.d.ts.map +1 -1
  14. package/dist/adapters/try-render-image.d.ts +9 -0
  15. package/dist/adapters/try-render-image.d.ts.map +1 -0
  16. package/dist/assemble/message-assembler.d.ts +10 -0
  17. package/dist/assemble/message-assembler.d.ts.map +1 -1
  18. package/dist/budget/conversion-context.d.ts +5 -1
  19. package/dist/budget/conversion-context.d.ts.map +1 -1
  20. package/dist/budget/operation-scope.d.ts +9 -0
  21. package/dist/budget/operation-scope.d.ts.map +1 -0
  22. package/dist/budget/recording-receipt-collector.d.ts +13 -0
  23. package/dist/budget/recording-receipt-collector.d.ts.map +1 -0
  24. package/dist/capabilities/capabilities.d.ts.map +1 -1
  25. package/dist/constants.d.ts +14 -0
  26. package/dist/constants.d.ts.map +1 -1
  27. package/dist/converter/create-converter.d.ts +1 -1
  28. package/dist/converter/create-converter.d.ts.map +1 -1
  29. package/dist/converter/resolve-config.d.ts +4 -2
  30. package/dist/converter/resolve-config.d.ts.map +1 -1
  31. package/dist/converter/slackmark-converter.d.ts +11 -1
  32. package/dist/converter/slackmark-converter.d.ts.map +1 -1
  33. package/dist/errors.d.ts +81 -3
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/index.d.ts +6 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5080 -3585
  38. package/dist/index.js.map +1 -1
  39. package/dist/receipts.d.ts +37 -0
  40. package/dist/receipts.d.ts.map +1 -0
  41. package/dist/renderers/codecogs-renderer.d.ts +5 -5
  42. package/dist/renderers/codecogs-renderer.d.ts.map +1 -1
  43. package/dist/renderers/kroki-renderer.d.ts +5 -5
  44. package/dist/renderers/kroki-renderer.d.ts.map +1 -1
  45. package/dist/renderers/mermaid-ink-renderer.d.ts +5 -5
  46. package/dist/renderers/mermaid-ink-renderer.d.ts.map +1 -1
  47. package/dist/renderers/quickchart-renderer.d.ts +5 -5
  48. package/dist/renderers/quickchart-renderer.d.ts.map +1 -1
  49. package/dist/renderers/renderer-options.d.ts +4 -0
  50. package/dist/renderers/renderer-options.d.ts.map +1 -0
  51. package/dist/renderers/url-utils.d.ts.map +1 -1
  52. package/dist/renderers.d.ts +1 -0
  53. package/dist/renderers.d.ts.map +1 -1
  54. package/dist/renderers.js +412 -248
  55. package/dist/renderers.js.map +1 -1
  56. package/dist/slack/fetch-slack-uploader.d.ts +11 -2
  57. package/dist/slack/fetch-slack-uploader.d.ts.map +1 -1
  58. package/dist/slack/slack-upload-error.d.ts +18 -2
  59. package/dist/slack/slack-upload-error.d.ts.map +1 -1
  60. package/dist/slack.d.ts +2 -1
  61. package/dist/slack.d.ts.map +1 -1
  62. package/dist/slack.js +753 -230
  63. package/dist/slack.js.map +1 -1
  64. package/dist/types.d.ts +93 -11
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/validate/validate-blocks.d.ts.map +1 -1
  67. package/package.json +5 -4
package/dist/slack.js CHANGED
@@ -1,255 +1,778 @@
1
- // src/slack/slack-upload-error.ts
2
- var SlackUploadStage = {
3
- GetUploadUrl: "get-upload-url",
4
- UploadBytes: "upload-bytes",
5
- CompleteUpload: "complete-upload",
6
- FileInfo: "file-info",
7
- ProcessingTimeout: "processing-timeout"
1
+ //#region src/errors.ts
2
+ /**
3
+ * Registry-keyed markers, not module-local symbols.
4
+ *
5
+ * Each published entry (`slackmark`, `slackmark/renderers`, `slackmark/slack`) is bundled
6
+ * without code splitting, so every entry carries its own copy of these classes and
7
+ * `instanceof` is false for an error that crosses an entry boundary. `Symbol.for` keys are
8
+ * shared by every copy, which is what makes the exported guards below reliable. Prefer
9
+ * those guards to `instanceof` for any error that can cross an entry.
10
+ */
11
+ const RECEIPT_SNAPSHOT_LOCK = Symbol.for("slackmark.receiptSnapshotLock");
12
+ const CONFIGURATION_ERROR_BRAND = Symbol.for("slackmark.error.configuration");
13
+ const OPERATION_ERROR_BRAND = Symbol.for("slackmark.error.operation");
14
+ /**
15
+ * Marks a class prototype so instances from any bundled copy answer to one identity.
16
+ * Subclasses inherit the brand, and instances carry no extra own property.
17
+ */
18
+ function brandErrorPrototype(prototype, brand) {
19
+ Object.defineProperty(prototype, brand, {
20
+ configurable: false,
21
+ enumerable: false,
22
+ value: true,
23
+ writable: false
24
+ });
25
+ }
26
+ /**
27
+ * Reads a brand without trusting the value: a hostile getter must not escape a guard.
28
+ */
29
+ function hasErrorBrand(value, brand) {
30
+ if (typeof value !== "object" || value === null) return false;
31
+ try {
32
+ return Reflect.get(value, brand) === true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ var ConfigurationError = class extends Error {
38
+ static {
39
+ brandErrorPrototype(this.prototype, CONFIGURATION_ERROR_BRAND);
40
+ }
41
+ name;
42
+ code;
43
+ stage;
44
+ retryable;
45
+ receipts;
46
+ settledReceipts;
47
+ constructor(message, options = {}) {
48
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
49
+ this.name = "ConfigurationError";
50
+ this.code = options.code ?? "invalid_configuration";
51
+ this.stage = options.stage;
52
+ this.retryable = false;
53
+ this.receipts = freezeReceipts(options.receipts ?? []);
54
+ this.settledReceipts = options.settledReceipts ?? Promise.resolve(this.receipts);
55
+ }
56
+ };
57
+ const OperationErrorCode = {
58
+ Aborted: "operation_aborted",
59
+ DeadlineExceeded: "operation_deadline_exceeded",
60
+ Shutdown: "operation_shutdown"
61
+ };
62
+ /**
63
+ * Operational control-flow error. These failures are never degradations.
64
+ */
65
+ var OperationError = class extends Error {
66
+ static {
67
+ brandErrorPrototype(this.prototype, OPERATION_ERROR_BRAND);
68
+ }
69
+ name;
70
+ code;
71
+ stage;
72
+ retryable;
73
+ receipts;
74
+ settledReceipts;
75
+ constructor(message, code, stage, retryable, options = {}) {
76
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
77
+ this.name = "OperationError";
78
+ this.code = code;
79
+ this.stage = stage;
80
+ this.retryable = retryable;
81
+ this.receipts = freezeReceipts(options.receipts ?? []);
82
+ this.settledReceipts = options.settledReceipts ?? Promise.resolve(this.receipts);
83
+ }
84
+ };
85
+ var OperationAbortedError = class extends OperationError {
86
+ name;
87
+ constructor(stage, options = {}) {
88
+ super("Operation aborted by caller", OperationErrorCode.Aborted, stage, false, options);
89
+ this.name = "OperationAbortedError";
90
+ }
91
+ };
92
+ var OperationDeadlineExceededError = class extends OperationError {
93
+ name;
94
+ constructor(stage, options = {}) {
95
+ super("Operation deadline exceeded", OperationErrorCode.DeadlineExceeded, stage, true, options);
96
+ this.name = "OperationDeadlineExceededError";
97
+ }
98
+ };
99
+ /**
100
+ * Identifies a {@link ConfigurationError} from any entry of this package.
101
+ *
102
+ * Use this instead of `instanceof` when the error may have been thrown by a different
103
+ * entry — for example a `ConfigurationError` from `slackmark/renderers` caught by code
104
+ * that imported the class from `slackmark`.
105
+ */
106
+ function isConfigurationError(value) {
107
+ return hasErrorBrand(value, CONFIGURATION_ERROR_BRAND);
108
+ }
109
+ /**
110
+ * Identifies an {@link OperationError} — abort, deadline, or shutdown — from any entry of
111
+ * this package, including its subclasses.
112
+ *
113
+ * Use this instead of `instanceof` when the error may have been thrown by a different
114
+ * entry — for example an abort raised inside `FetchSlackUploader` from `slackmark/slack`
115
+ * caught by code that imported the class from `slackmark`. Narrow further with `code`
116
+ * against {@link OperationErrorCode} rather than testing a subclass with `instanceof`.
117
+ */
118
+ function isOperationError(value) {
119
+ return hasErrorBrand(value, OPERATION_ERROR_BRAND);
120
+ }
121
+ function attachReceipts(error, receipts, settledReceipts) {
122
+ if (!(error instanceof Error)) return error;
123
+ try {
124
+ if (Reflect.get(error, RECEIPT_SNAPSHOT_LOCK) === true) return error;
125
+ } catch {
126
+ return error;
127
+ }
128
+ let existing = [];
129
+ try {
130
+ const candidate = Reflect.get(error, "receipts");
131
+ if (Array.isArray(candidate)) existing = candidate.filter(isUploadReceipt);
132
+ } catch {
133
+ existing = [];
134
+ }
135
+ Reflect.defineProperty(error, "receipts", {
136
+ configurable: true,
137
+ enumerable: true,
138
+ value: freezeReceipts(mergeReceipts(existing, receipts)),
139
+ writable: false
140
+ });
141
+ if (settledReceipts !== void 0) {
142
+ const finalReceipts = freezeSettledReceipts(settledReceipts, receipts);
143
+ Reflect.defineProperty(error, "settledReceipts", {
144
+ configurable: true,
145
+ enumerable: true,
146
+ value: finalReceipts,
147
+ writable: false
148
+ });
149
+ }
150
+ return error;
151
+ }
152
+ async function freezeSettledReceipts(settledReceipts, fallback) {
153
+ try {
154
+ return freezeReceipts(await settledReceipts);
155
+ } catch {
156
+ return freezeReceipts(fallback);
157
+ }
158
+ }
159
+ function mergeReceipts(existing, incoming) {
160
+ const existingGroups = groupReceipts(existing);
161
+ const incomingGroups = groupReceipts(incoming);
162
+ const merged = [...incoming];
163
+ for (const [key, values] of existingGroups) {
164
+ const overlap = incomingGroups.get(key)?.length ?? 0;
165
+ if (values.length > overlap) merged.push(...values.slice(overlap));
166
+ }
167
+ return merged;
168
+ }
169
+ function groupReceipts(receipts) {
170
+ const grouped = /* @__PURE__ */ new Map();
171
+ for (const receipt of receipts) {
172
+ const key = receiptKey(receipt);
173
+ const values = grouped.get(key);
174
+ if (values === void 0) grouped.set(key, [receipt]);
175
+ else values.push(receipt);
176
+ }
177
+ return grouped;
178
+ }
179
+ function receiptKey(receipt) {
180
+ return [
181
+ receipt.provider,
182
+ receipt.phase,
183
+ receipt.fileId ?? "",
184
+ String(receipt.shared),
185
+ receipt.processing,
186
+ String(receipt.status ?? ""),
187
+ String(receipt.retryAfterMs ?? ""),
188
+ receipt.providerCode ?? "",
189
+ receipt.retrySafety
190
+ ].join("\0");
191
+ }
192
+ function freezeReceipts(receipts) {
193
+ return Object.freeze(receipts.map((receipt) => Object.freeze({ ...receipt })));
194
+ }
195
+ function isUploadReceipt(value) {
196
+ return typeof value === "object" && value !== null && Reflect.get(value, "kind") === "upload";
197
+ }
198
+ //#endregion
199
+ //#region src/receipts.ts
200
+ const UploadPhase = {
201
+ Requested: "requested",
202
+ Allocated: "allocated",
203
+ Uploaded: "uploaded",
204
+ Completed: "completed",
205
+ Shared: "shared",
206
+ Unknown: "unknown"
207
+ };
208
+ const UploadProcessing = {
209
+ NotRequested: "not-requested",
210
+ Pending: "pending",
211
+ Verified: "verified",
212
+ Unverified: "unverified"
213
+ };
214
+ const RetrySafety = {
215
+ Never: "never",
216
+ Safe: "safe",
217
+ MayDuplicate: "may-duplicate"
8
218
  };
219
+ //#endregion
220
+ //#region src/slack/slack-upload-error.ts
221
+ /** See the brand rationale in `errors.ts`: this class is bundled into more than one entry. */
222
+ const SLACK_UPLOAD_ERROR_BRAND = Symbol.for("slackmark.error.slackUpload");
223
+ const SlackUploadStage = {
224
+ Admission: "admission",
225
+ GetUploadUrl: "get-upload-url",
226
+ UploadBytes: "upload-bytes",
227
+ CompleteUpload: "complete-upload",
228
+ FileInfo: "file-info",
229
+ ProcessingTimeout: "processing-timeout"
230
+ };
231
+ /**
232
+ * Integration error from one explicit stage of Slack's external-upload flow.
233
+ */
9
234
  var SlackUploadError = class extends Error {
10
- name;
11
- stage;
12
- status;
13
- code;
14
- constructor(message, details) {
15
- super(message, details.cause !== void 0 ? { cause: details.cause } : void 0);
16
- this.name = "SlackUploadError";
17
- this.stage = details.stage;
18
- this.status = details.status;
19
- this.code = details.code;
20
- }
235
+ static {
236
+ brandErrorPrototype(this.prototype, SLACK_UPLOAD_ERROR_BRAND);
237
+ }
238
+ name;
239
+ stage;
240
+ status;
241
+ code;
242
+ retryable;
243
+ retryAfterMs;
244
+ receipt;
245
+ receipts;
246
+ settledReceipts;
247
+ constructor(message, details) {
248
+ super(message, details.cause !== void 0 ? { cause: details.cause } : void 0);
249
+ this.name = "SlackUploadError";
250
+ this.stage = details.stage;
251
+ this.status = details.status;
252
+ this.code = details.code;
253
+ this.retryable = details.retryable ?? isRetryable(details.status, details.code);
254
+ this.retryAfterMs = details.retryAfterMs;
255
+ this.receipt = Object.freeze(details.receipt ?? {
256
+ kind: "upload",
257
+ provider: "slack",
258
+ phase: UploadPhase.Requested,
259
+ fileId: void 0,
260
+ shared: false,
261
+ processing: UploadProcessing.NotRequested,
262
+ status: details.status,
263
+ retryAfterMs: details.retryAfterMs,
264
+ providerCode: details.code,
265
+ retrySafety: RetrySafety.Safe
266
+ });
267
+ this.receipts = Object.freeze([this.receipt]);
268
+ this.settledReceipts = Promise.resolve(this.receipts);
269
+ }
21
270
  };
22
-
23
- // src/slack/fetch-slack-uploader.ts
24
- var SLACK_API_BASE_URL = "https://slack.com/api";
25
- var DEFAULT_POLL_INTERVAL_MS = 250;
26
- var DEFAULT_POLL_MAX_ATTEMPTS = 20;
27
- var DEFAULT_POLL_TIMEOUT_MS = 3e4;
271
+ /**
272
+ * Identifies a {@link SlackUploadError} from any entry of this package. `slackmark` and
273
+ * `slackmark/slack` each bundle their own copy, so `instanceof` is unreliable when the
274
+ * error crosses between them.
275
+ */
276
+ function isSlackUploadError(value) {
277
+ return hasErrorBrand(value, SLACK_UPLOAD_ERROR_BRAND);
278
+ }
279
+ function isRetryable(status, code) {
280
+ return status === 429 || status !== void 0 && status >= 500 || code === "fetch_failed" || code === "request_timeout" || code === "processing_timeout";
281
+ }
282
+ //#endregion
283
+ //#region src/slack/fetch-slack-uploader.ts
284
+ const SLACK_API_BASE_URL = "https://slack.com/api";
285
+ const DEFAULT_POLL_INTERVAL_MS = 250;
286
+ const DEFAULT_POLL_MAX_ATTEMPTS = 20;
287
+ const DEFAULT_POLL_TIMEOUT_MS = 3e4;
288
+ const DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
289
+ /**
290
+ * Web-standard implementation of Slack's external file-upload flow.
291
+ *
292
+ * @see https://docs.slack.dev/messaging/working-with-files
293
+ */
28
294
  var FetchSlackUploader = class {
29
- token;
30
- fetcher;
31
- channelId;
32
- pollIntervalMs;
33
- pollMaxAttempts;
34
- pollTimeoutMs;
35
- sleep;
36
- clock;
37
- constructor(options) {
38
- this.token = options.token;
39
- this.fetcher = options.fetch ?? defaultFetch;
40
- this.channelId = options.channelId;
41
- this.pollIntervalMs = options.poll?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
42
- this.pollMaxAttempts = options.poll?.maxAttempts ?? DEFAULT_POLL_MAX_ATTEMPTS;
43
- this.pollTimeoutMs = options.poll?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
44
- this.sleep = options.poll?.sleep ?? defaultSleep;
45
- this.clock = options.poll?.clock ?? defaultClock;
46
- validateOptions({
47
- token: this.token,
48
- pollIntervalMs: this.pollIntervalMs,
49
- pollMaxAttempts: this.pollMaxAttempts,
50
- pollTimeoutMs: this.pollTimeoutMs
51
- });
52
- }
53
- async upload(img) {
54
- const target = await this.getUploadTarget(img);
55
- await this.uploadBytes(target, img);
56
- await this.completeUpload(target.fileId, img.filename);
57
- await this.waitUntilProcessed(target.fileId);
58
- return { fileId: target.fileId };
59
- }
60
- async getUploadTarget(img) {
61
- const payload = await this.callSlackApi(
62
- "files.getUploadURLExternal",
63
- encodeForm([
64
- ["filename", img.filename],
65
- ["length", String(img.data.byteLength)]
66
- ]),
67
- SlackUploadStage.GetUploadUrl
68
- );
69
- const uploadUrl = payload["upload_url"];
70
- const fileId = payload["file_id"];
71
- if (!isNonEmptyString(uploadUrl) || !isNonEmptyString(fileId)) {
72
- throw new SlackUploadError("files.getUploadURLExternal returned no upload_url or file_id", {
73
- stage: SlackUploadStage.GetUploadUrl,
74
- code: "invalid_response"
75
- });
76
- }
77
- return { uploadUrl, fileId };
78
- }
79
- async uploadBytes(target, img) {
80
- let response;
81
- try {
82
- response = await this.fetcher(target.uploadUrl, {
83
- method: "POST",
84
- headers: { "content-type": img.mimeType },
85
- body: img.data
86
- });
87
- } catch (cause) {
88
- throw new SlackUploadError("Uploading bytes to Slack failed", {
89
- stage: SlackUploadStage.UploadBytes,
90
- code: "fetch_failed",
91
- cause
92
- });
93
- }
94
- if (!response.ok) {
95
- throw new SlackUploadError(`Slack upload URL returned HTTP ${String(response.status)}`, {
96
- stage: SlackUploadStage.UploadBytes,
97
- status: response.status,
98
- code: `http_${String(response.status)}`
99
- });
100
- }
101
- }
102
- async completeUpload(fileId, filename) {
103
- const fields = [
104
- [
105
- "files",
106
- JSON.stringify([
107
- {
108
- id: fileId,
109
- title: filename
110
- }
111
- ])
112
- ]
113
- ];
114
- if (this.channelId !== void 0) {
115
- fields.push(["channel_id", this.channelId]);
116
- }
117
- await this.callSlackApi(
118
- "files.completeUploadExternal",
119
- encodeForm(fields),
120
- SlackUploadStage.CompleteUpload
121
- );
122
- }
123
- async waitUntilProcessed(fileId) {
124
- const startedAt = this.clock();
125
- for (let attempt = 1; attempt <= this.pollMaxAttempts; attempt += 1) {
126
- const payload = await this.callSlackApi(
127
- "files.info",
128
- encodeForm([["file", fileId]]),
129
- SlackUploadStage.FileInfo
130
- );
131
- if (isProcessedImageFile(payload["file"])) {
132
- return;
133
- }
134
- if (attempt === this.pollMaxAttempts || this.clock() - startedAt >= this.pollTimeoutMs) {
135
- throw processingTimeout(fileId, attempt);
136
- }
137
- try {
138
- await this.sleep(this.pollIntervalMs);
139
- } catch (cause) {
140
- throw new SlackUploadError("Slack file-processing poll sleep failed", {
141
- stage: SlackUploadStage.ProcessingTimeout,
142
- code: "sleep_failed",
143
- cause
144
- });
145
- }
146
- if (this.clock() - startedAt >= this.pollTimeoutMs) {
147
- throw processingTimeout(fileId, attempt);
148
- }
149
- }
150
- }
151
- async callSlackApi(method, body, stage) {
152
- let response;
153
- try {
154
- response = await this.fetcher(`${SLACK_API_BASE_URL}/${method}`, {
155
- method: "POST",
156
- headers: {
157
- authorization: `Bearer ${this.token}`,
158
- "content-type": "application/x-www-form-urlencoded;charset=UTF-8"
159
- },
160
- body
161
- });
162
- } catch (cause) {
163
- throw new SlackUploadError(`${method} request failed`, {
164
- stage,
165
- code: "fetch_failed",
166
- cause
167
- });
168
- }
169
- let payload;
170
- try {
171
- payload = await response.json();
172
- } catch (cause) {
173
- throw new SlackUploadError(`${method} returned invalid JSON`, {
174
- stage,
175
- status: response.status,
176
- code: "invalid_json",
177
- cause
178
- });
179
- }
180
- const slackCode = getSlackErrorCode(payload);
181
- if (!response.ok || !isRecord(payload) || payload["ok"] !== true) {
182
- throw new SlackUploadError(`${method} was rejected by Slack`, {
183
- stage,
184
- status: response.status,
185
- code: slackCode ?? (response.ok ? "invalid_response" : `http_${String(response.status)}`)
186
- });
187
- }
188
- return payload;
189
- }
295
+ token;
296
+ fetcher;
297
+ channelId;
298
+ requestTimeoutMs;
299
+ waitForProcessing;
300
+ pollIntervalMs;
301
+ pollMaxAttempts;
302
+ pollTimeoutMs;
303
+ sleep;
304
+ clock;
305
+ constructor(options) {
306
+ const resolved = resolveUploaderOptions(options);
307
+ this.token = resolved.token;
308
+ this.fetcher = resolved.fetcher;
309
+ this.channelId = resolved.channelId;
310
+ this.requestTimeoutMs = resolved.requestTimeoutMs;
311
+ this.waitForProcessing = resolved.waitForProcessing;
312
+ this.pollIntervalMs = resolved.pollIntervalMs;
313
+ this.pollMaxAttempts = resolved.pollMaxAttempts;
314
+ this.pollTimeoutMs = resolved.pollTimeoutMs;
315
+ this.sleep = resolved.sleep;
316
+ this.clock = resolved.clock;
317
+ }
318
+ async upload(img, context) {
319
+ let receipt = slackReceipt({
320
+ phase: UploadPhase.Requested,
321
+ processing: UploadProcessing.NotRequested,
322
+ retrySafety: RetrySafety.Safe
323
+ });
324
+ try {
325
+ const target = await this.getUploadTarget(img, context);
326
+ receipt = slackReceipt({
327
+ phase: UploadPhase.Allocated,
328
+ fileId: target.fileId,
329
+ processing: UploadProcessing.NotRequested,
330
+ retrySafety: RetrySafety.MayDuplicate
331
+ });
332
+ await this.uploadBytes(target, img, context);
333
+ receipt = slackReceipt({
334
+ phase: UploadPhase.Uploaded,
335
+ fileId: target.fileId,
336
+ processing: UploadProcessing.NotRequested,
337
+ retrySafety: RetrySafety.MayDuplicate
338
+ });
339
+ await this.completeUpload(target.fileId, img.filename, context);
340
+ receipt = slackReceipt({
341
+ phase: this.channelId === void 0 ? UploadPhase.Completed : UploadPhase.Shared,
342
+ fileId: target.fileId,
343
+ shared: this.channelId !== void 0,
344
+ processing: this.waitForProcessing ? UploadProcessing.Pending : UploadProcessing.NotRequested,
345
+ retrySafety: RetrySafety.Never
346
+ });
347
+ if (this.waitForProcessing) try {
348
+ await this.waitUntilProcessed(target.fileId, receipt, context);
349
+ receipt = slackReceipt({
350
+ ...receipt,
351
+ processing: UploadProcessing.Verified
352
+ });
353
+ } catch (error) {
354
+ if (isOperationError(error)) throw attachReceipts(error, [receipt]);
355
+ if (isSlackUploadError(error)) receipt = slackReceipt({
356
+ ...receipt,
357
+ processing: UploadProcessing.Unverified,
358
+ status: error.status,
359
+ retryAfterMs: error.retryAfterMs,
360
+ providerCode: error.code
361
+ });
362
+ else receipt = slackReceipt({
363
+ ...receipt,
364
+ processing: UploadProcessing.Unverified,
365
+ providerCode: "processing_failed"
366
+ });
367
+ }
368
+ return {
369
+ fileId: target.fileId,
370
+ receipt
371
+ };
372
+ } catch (error) {
373
+ if (isSlackUploadError(error)) throw withReceipt(error, receipt);
374
+ if (isOperationError(error)) throw attachReceipts(error, [receipt]);
375
+ throw error;
376
+ }
377
+ }
378
+ async getUploadTarget(img, control) {
379
+ const payload = await this.callSlackApi("files.getUploadURLExternal", encodeForm([["filename", img.filename], ["length", String(img.data.byteLength)]]), SlackUploadStage.GetUploadUrl, control);
380
+ const uploadUrl = payload["upload_url"];
381
+ const fileId = payload["file_id"];
382
+ if (!isNonEmptyString(uploadUrl) || !isNonEmptyString(fileId)) throw new SlackUploadError("files.getUploadURLExternal returned no upload_url or file_id", {
383
+ stage: SlackUploadStage.GetUploadUrl,
384
+ code: "invalid_response"
385
+ });
386
+ return {
387
+ uploadUrl,
388
+ fileId
389
+ };
390
+ }
391
+ async uploadBytes(target, img, control) {
392
+ const stageSignal = createStageSignal(control, this.requestTimeoutMs, this.clock);
393
+ let response;
394
+ try {
395
+ response = await this.fetcher(target.uploadUrl, {
396
+ method: "POST",
397
+ headers: { "content-type": img.mimeType },
398
+ body: img.data,
399
+ signal: stageSignal.signal
400
+ });
401
+ } catch (cause) {
402
+ const operationError = stageSignal.operationError(SlackUploadStage.UploadBytes, cause);
403
+ if (operationError !== void 0) throw operationError;
404
+ throw new SlackUploadError("Slack byte upload failed", {
405
+ stage: SlackUploadStage.UploadBytes,
406
+ code: stageSignal.code(),
407
+ cause
408
+ });
409
+ } finally {
410
+ stageSignal.cleanup();
411
+ }
412
+ if (!response.ok) throw new SlackUploadError("Slack byte upload was rejected", {
413
+ stage: SlackUploadStage.UploadBytes,
414
+ status: response.status,
415
+ code: `http_${String(response.status)}`,
416
+ retryAfterMs: retryAfterMs(response)
417
+ });
418
+ }
419
+ async completeUpload(fileId, filename, control) {
420
+ const fields = [["files", JSON.stringify([{
421
+ id: fileId,
422
+ title: filename
423
+ }])]];
424
+ if (this.channelId !== void 0) fields.push(["channel_id", this.channelId]);
425
+ await this.callSlackApi("files.completeUploadExternal", encodeForm(fields), SlackUploadStage.CompleteUpload, control);
426
+ }
427
+ async waitUntilProcessed(fileId, receipt, control) {
428
+ const startedAt = this.clock();
429
+ const pollDeadlineMs = startedAt + this.pollTimeoutMs;
430
+ const pollOwnsDeadline = control.deadlineMs === void 0 || pollDeadlineMs < control.deadlineMs;
431
+ const pollContext = {
432
+ signal: control.signal,
433
+ deadlineMs: Math.min(control.deadlineMs ?? Number.POSITIVE_INFINITY, pollDeadlineMs)
434
+ };
435
+ for (let attempt = 1; attempt <= this.pollMaxAttempts; attempt += 1) {
436
+ if (this.clock() >= pollDeadlineMs) throw processingTimeout(receipt, attempt - 1);
437
+ let payload;
438
+ try {
439
+ payload = await this.callSlackApi("files.info", encodeForm([["file", fileId]]), SlackUploadStage.FileInfo, pollContext);
440
+ } catch (error) {
441
+ if (pollOwnsDeadline && isOperationDeadline(error)) throw processingTimeout(receipt, attempt);
442
+ throw error;
443
+ }
444
+ if (isProcessedImageFile(payload["file"])) return;
445
+ if (attempt === this.pollMaxAttempts || this.clock() - startedAt >= this.pollTimeoutMs) throw processingTimeout(receipt, attempt);
446
+ try {
447
+ await controlledSleep(this.sleep, Math.min(this.pollIntervalMs, Math.max(0, pollDeadlineMs - this.clock())), pollContext, this.clock);
448
+ } catch (error) {
449
+ if (pollOwnsDeadline && isOperationDeadline(error)) throw processingTimeout(receipt, attempt);
450
+ throw error;
451
+ }
452
+ if (this.clock() - startedAt >= this.pollTimeoutMs) throw processingTimeout(receipt, attempt);
453
+ }
454
+ }
455
+ async callSlackApi(method, body, stage, control) {
456
+ const stageSignal = createStageSignal(control, this.requestTimeoutMs, this.clock);
457
+ let response;
458
+ try {
459
+ response = await this.fetcher(`${SLACK_API_BASE_URL}/${method}`, {
460
+ method: "POST",
461
+ headers: {
462
+ authorization: `Bearer ${this.token}`,
463
+ "content-type": "application/x-www-form-urlencoded;charset=UTF-8"
464
+ },
465
+ body,
466
+ signal: stageSignal.signal
467
+ });
468
+ } catch (cause) {
469
+ const operationError = stageSignal.operationError(stage, cause);
470
+ stageSignal.cleanup();
471
+ if (operationError !== void 0) throw operationError;
472
+ throw new SlackUploadError("Slack API request failed", {
473
+ stage,
474
+ code: stageSignal.code(),
475
+ cause
476
+ });
477
+ }
478
+ let payload;
479
+ try {
480
+ payload = await raceWithSignal(response.json(), stageSignal.signal);
481
+ } catch (cause) {
482
+ const operationError = stageSignal.operationError(stage, cause);
483
+ const signalCode = stageSignal.code();
484
+ if (operationError !== void 0) throw operationError;
485
+ throw new SlackUploadError("Slack API returned invalid JSON", {
486
+ stage,
487
+ status: response.status,
488
+ code: signalCode === "fetch_failed" ? "invalid_json" : signalCode,
489
+ cause
490
+ });
491
+ } finally {
492
+ stageSignal.cleanup();
493
+ }
494
+ const slackCode = getSlackErrorCode(payload);
495
+ if (!response.ok || !isRecord(payload) || payload["ok"] !== true) throw new SlackUploadError("Slack API rejected the request", {
496
+ stage,
497
+ status: response.status,
498
+ code: slackCode ?? (response.ok ? "invalid_response" : `http_${String(response.status)}`),
499
+ retryAfterMs: retryAfterMs(response)
500
+ });
501
+ return payload;
502
+ }
190
503
  };
191
- function validateOptions(uploader) {
192
- if (uploader.token.length === 0) {
193
- throw new TypeError("token must not be empty");
194
- }
195
- if (!Number.isInteger(uploader.pollIntervalMs) || uploader.pollIntervalMs < 0) {
196
- throw new RangeError("poll.intervalMs must be a non-negative integer");
197
- }
198
- if (!Number.isInteger(uploader.pollMaxAttempts) || uploader.pollMaxAttempts <= 0) {
199
- throw new RangeError("poll.maxAttempts must be a positive integer");
200
- }
201
- if (!Number.isFinite(uploader.pollTimeoutMs) || uploader.pollTimeoutMs <= 0) {
202
- throw new RangeError("poll.timeoutMs must be positive");
203
- }
504
+ function resolveUploaderOptions(options) {
505
+ assertKnownPlainObject(options, [
506
+ "token",
507
+ "fetch",
508
+ "channelId",
509
+ "requestTimeoutMs",
510
+ "waitForProcessing",
511
+ "poll"
512
+ ], "FetchSlackUploader options");
513
+ let token;
514
+ let fetcher;
515
+ let channelId;
516
+ let requestTimeoutMs;
517
+ let waitForProcessing;
518
+ let poll;
519
+ try {
520
+ token = Reflect.get(options, "token");
521
+ fetcher = Reflect.get(options, "fetch");
522
+ channelId = Reflect.get(options, "channelId");
523
+ requestTimeoutMs = Reflect.get(options, "requestTimeoutMs");
524
+ waitForProcessing = Reflect.get(options, "waitForProcessing");
525
+ poll = Reflect.get(options, "poll");
526
+ } catch (cause) {
527
+ throw invalidUploaderOptions("Uploader options could not be read", cause);
528
+ }
529
+ if (typeof token !== "string" || token.length === 0 || token !== token.trim()) throw invalidUploaderOptions("token must be a nonempty trimmed string");
530
+ if (channelId !== void 0 && (typeof channelId !== "string" || channelId.length === 0 || channelId !== channelId.trim())) throw invalidUploaderOptions("channelId must be a nonempty trimmed string");
531
+ if (fetcher !== void 0 && typeof fetcher !== "function") throw invalidUploaderOptions("fetch must be a function");
532
+ if (requestTimeoutMs !== void 0 && (typeof requestTimeoutMs !== "number" || !Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0)) throw invalidUploaderOptions("requestTimeoutMs must be positive");
533
+ if (waitForProcessing !== void 0 && typeof waitForProcessing !== "boolean") throw invalidUploaderOptions("waitForProcessing must be boolean");
534
+ const resolvedPoll = resolvePollOptions(poll);
535
+ return {
536
+ token,
537
+ fetcher: fetcher ?? defaultFetch,
538
+ channelId,
539
+ requestTimeoutMs: requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
540
+ waitForProcessing: waitForProcessing ?? false,
541
+ pollIntervalMs: resolvedPoll.intervalMs,
542
+ pollMaxAttempts: resolvedPoll.maxAttempts,
543
+ pollTimeoutMs: resolvedPoll.timeoutMs,
544
+ sleep: resolvedPoll.sleep,
545
+ clock: resolvedPoll.clock
546
+ };
547
+ }
548
+ function resolvePollOptions(poll) {
549
+ if (poll === void 0) return {
550
+ intervalMs: DEFAULT_POLL_INTERVAL_MS,
551
+ maxAttempts: DEFAULT_POLL_MAX_ATTEMPTS,
552
+ timeoutMs: DEFAULT_POLL_TIMEOUT_MS,
553
+ sleep: defaultSleep,
554
+ clock: defaultClock
555
+ };
556
+ assertKnownPlainObject(poll, [
557
+ "intervalMs",
558
+ "maxAttempts",
559
+ "timeoutMs",
560
+ "sleep",
561
+ "clock"
562
+ ], "poll options");
563
+ let intervalMs;
564
+ let maxAttempts;
565
+ let timeoutMs;
566
+ let sleep;
567
+ let clock;
568
+ try {
569
+ intervalMs = Reflect.get(poll, "intervalMs");
570
+ maxAttempts = Reflect.get(poll, "maxAttempts");
571
+ timeoutMs = Reflect.get(poll, "timeoutMs");
572
+ sleep = Reflect.get(poll, "sleep");
573
+ clock = Reflect.get(poll, "clock");
574
+ } catch (cause) {
575
+ throw invalidUploaderOptions("poll options could not be read", cause);
576
+ }
577
+ const resolvedInterval = intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
578
+ const resolvedAttempts = maxAttempts ?? DEFAULT_POLL_MAX_ATTEMPTS;
579
+ const resolvedTimeout = timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
580
+ if (!Number.isInteger(resolvedInterval) || resolvedInterval < 0) throw invalidUploaderOptions("poll.intervalMs must be a non-negative integer");
581
+ if (!Number.isInteger(resolvedAttempts) || resolvedAttempts <= 0) throw invalidUploaderOptions("poll.maxAttempts must be a positive integer");
582
+ if (!Number.isFinite(resolvedTimeout) || resolvedTimeout <= 0) throw invalidUploaderOptions("poll.timeoutMs must be positive");
583
+ if (sleep !== void 0 && typeof sleep !== "function") throw invalidUploaderOptions("poll.sleep must be a function");
584
+ if (clock !== void 0 && typeof clock !== "function") throw invalidUploaderOptions("poll.clock must be a function");
585
+ return {
586
+ intervalMs: resolvedInterval,
587
+ maxAttempts: resolvedAttempts,
588
+ timeoutMs: resolvedTimeout,
589
+ sleep: sleep ?? defaultSleep,
590
+ clock: clock ?? defaultClock
591
+ };
592
+ }
593
+ function assertKnownPlainObject(value, allowed, label) {
594
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidUploaderOptions(`${label} must be a plain object`);
595
+ let prototype;
596
+ let keys;
597
+ try {
598
+ prototype = Object.getPrototypeOf(value);
599
+ keys = Object.keys(value);
600
+ } catch (cause) {
601
+ throw invalidUploaderOptions(`${label} could not be inspected`, cause);
602
+ }
603
+ if (prototype !== Object.prototype && prototype !== null) throw invalidUploaderOptions(`${label} must be a plain object`);
604
+ const allowedKeys = new Set(allowed);
605
+ for (const key of keys) if (!allowedKeys.has(key)) throw invalidUploaderOptions(`Unknown ${label} key: ${key}`);
606
+ }
607
+ function invalidUploaderOptions(message, cause) {
608
+ return new ConfigurationError(message, {
609
+ code: "invalid_slack_uploader_options",
610
+ stage: "configuration",
611
+ cause
612
+ });
204
613
  }
205
614
  function encodeForm(fields) {
206
- return fields.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&");
615
+ return fields.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&");
207
616
  }
208
617
  function isRecord(value) {
209
- return typeof value === "object" && value !== null;
618
+ return typeof value === "object" && value !== null;
210
619
  }
211
620
  function isNonEmptyString(value) {
212
- return typeof value === "string" && value.length > 0;
621
+ return typeof value === "string" && value.length > 0;
213
622
  }
214
623
  function isPositiveNumber(value) {
215
- return typeof value === "number" && Number.isFinite(value) && value > 0;
624
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
216
625
  }
217
626
  function isProcessedImageFile(value) {
218
- if (!isRecord(value)) {
219
- return false;
220
- }
221
- return isNonEmptyString(value["mimetype"]) && isNonEmptyString(value["filetype"]) && isPositiveNumber(value["original_w"]) && isPositiveNumber(value["original_h"]);
627
+ if (!isRecord(value)) return false;
628
+ return isNonEmptyString(value["mimetype"]) && isNonEmptyString(value["filetype"]) && isPositiveNumber(value["original_w"]) && isPositiveNumber(value["original_h"]);
222
629
  }
223
630
  function getSlackErrorCode(payload) {
224
- if (!isRecord(payload)) {
225
- return void 0;
226
- }
227
- const error = payload["error"];
228
- return typeof error === "string" ? error : void 0;
229
- }
230
- function processingTimeout(fileId, attempts) {
231
- return new SlackUploadError(
232
- `Slack file ${fileId} was not processed after ${String(attempts)} polls`,
233
- {
234
- stage: SlackUploadStage.ProcessingTimeout,
235
- code: "processing_timeout"
236
- }
237
- );
631
+ if (!isRecord(payload)) return;
632
+ const error = payload["error"];
633
+ return typeof error === "string" ? error : void 0;
634
+ }
635
+ function processingTimeout(receipt, attempts) {
636
+ return new SlackUploadError(`Slack file processing did not complete after ${String(attempts)} polls`, {
637
+ stage: SlackUploadStage.ProcessingTimeout,
638
+ code: "processing_timeout",
639
+ receipt: slackReceipt({
640
+ ...receipt,
641
+ processing: UploadProcessing.Unverified,
642
+ providerCode: "processing_timeout"
643
+ })
644
+ });
645
+ }
646
+ /**
647
+ * Subclass identity is as bundle-sensitive as the base class, so discriminate on `code`.
648
+ */
649
+ function isOperationDeadline(error) {
650
+ return isOperationError(error) && error.code === OperationErrorCode.DeadlineExceeded;
651
+ }
652
+ function createStageSignal(control, requestTimeoutMs, clock) {
653
+ const controller = new AbortController();
654
+ let timeoutKind;
655
+ const abortFromCaller = () => {
656
+ controller.abort(control.signal?.reason);
657
+ };
658
+ if (control.signal?.aborted === true) abortFromCaller();
659
+ else control.signal?.addEventListener("abort", abortFromCaller, { once: true });
660
+ const requestDeadlineMs = clock() + requestTimeoutMs;
661
+ const operationDeadlineMs = control.deadlineMs ?? Number.POSITIVE_INFINITY;
662
+ const remaining = Math.max(0, Math.min(requestDeadlineMs, operationDeadlineMs) - clock());
663
+ const timeout = setTimeout(() => {
664
+ timeoutKind = operationDeadlineMs <= requestDeadlineMs ? "operation" : "request";
665
+ controller.abort(/* @__PURE__ */ new Error("Slack request deadline exceeded"));
666
+ }, remaining);
667
+ return {
668
+ signal: controller.signal,
669
+ cleanup: () => {
670
+ clearTimeout(timeout);
671
+ control.signal?.removeEventListener("abort", abortFromCaller);
672
+ },
673
+ code: () => {
674
+ if (timeoutKind !== void 0) return "request_timeout";
675
+ return control.signal?.aborted === true ? "aborted" : "fetch_failed";
676
+ },
677
+ operationError: (stage, cause) => {
678
+ if (control.signal?.aborted === true) return new OperationAbortedError(stage, { cause });
679
+ if (timeoutKind === "operation") return new OperationDeadlineExceededError(stage, { cause });
680
+ }
681
+ };
682
+ }
683
+ function controlledSleep(sleep, milliseconds, control, clock) {
684
+ if (control.signal?.aborted === true) return Promise.reject(new OperationAbortedError(SlackUploadStage.ProcessingTimeout, { cause: control.signal.reason }));
685
+ if (control.deadlineMs !== void 0 && clock() >= control.deadlineMs) return Promise.reject(new OperationDeadlineExceededError(SlackUploadStage.ProcessingTimeout));
686
+ return new Promise((resolve, reject) => {
687
+ const onAbort = () => {
688
+ cleanup();
689
+ reject(new OperationAbortedError(SlackUploadStage.ProcessingTimeout, { cause: control.signal?.reason }));
690
+ };
691
+ const onDeadline = () => {
692
+ cleanup();
693
+ reject(new OperationDeadlineExceededError(SlackUploadStage.ProcessingTimeout));
694
+ };
695
+ const deadlineTimer = control.deadlineMs === void 0 ? void 0 : setTimeout(onDeadline, Math.max(0, control.deadlineMs - clock()));
696
+ const cleanup = () => {
697
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
698
+ control.signal?.removeEventListener("abort", onAbort);
699
+ };
700
+ control.signal?.addEventListener("abort", onAbort, { once: true });
701
+ sleep(milliseconds).then(() => {
702
+ cleanup();
703
+ resolve();
704
+ }, (error) => {
705
+ cleanup();
706
+ reject(error);
707
+ });
708
+ });
709
+ }
710
+ function raceWithSignal(promise, signal) {
711
+ if (signal.aborted) return Promise.reject(signal.reason);
712
+ return new Promise((resolve, reject) => {
713
+ const onAbort = () => {
714
+ signal.removeEventListener("abort", onAbort);
715
+ reject(signal.reason);
716
+ };
717
+ signal.addEventListener("abort", onAbort, { once: true });
718
+ promise.then((value) => {
719
+ signal.removeEventListener("abort", onAbort);
720
+ resolve(value);
721
+ }, (error) => {
722
+ signal.removeEventListener("abort", onAbort);
723
+ reject(error);
724
+ });
725
+ });
726
+ }
727
+ function slackReceipt(options) {
728
+ return Object.freeze({
729
+ kind: "upload",
730
+ provider: "slack",
731
+ phase: options.phase,
732
+ fileId: options.fileId,
733
+ shared: options.shared ?? false,
734
+ processing: options.processing,
735
+ status: options.status,
736
+ retryAfterMs: options.retryAfterMs,
737
+ providerCode: options.providerCode,
738
+ retrySafety: options.retrySafety ?? RetrySafety.Never
739
+ });
740
+ }
741
+ function withReceipt(error, receipt) {
742
+ return new SlackUploadError(error.message, {
743
+ stage: error.stage,
744
+ status: error.status,
745
+ code: error.code,
746
+ retryable: error.retryable,
747
+ retryAfterMs: error.retryAfterMs,
748
+ receipt: slackReceipt({
749
+ ...receipt,
750
+ status: error.status,
751
+ retryAfterMs: error.retryAfterMs,
752
+ providerCode: error.code,
753
+ retrySafety: receipt.phase === UploadPhase.Requested ? RetrySafety.Safe : receipt.phase === UploadPhase.Completed || receipt.phase === UploadPhase.Shared ? RetrySafety.Never : RetrySafety.MayDuplicate
754
+ }),
755
+ cause: error.cause
756
+ });
757
+ }
758
+ function retryAfterMs(response) {
759
+ const raw = response.headers?.get("retry-after");
760
+ if (raw === void 0 || raw === null) return;
761
+ const seconds = Number(raw);
762
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
238
763
  }
239
764
  function defaultFetch(input, init) {
240
- return globalThis.fetch(input, init);
765
+ return globalThis.fetch(input, init);
241
766
  }
242
767
  function defaultSleep(milliseconds) {
243
- return new Promise((resolve) => {
244
- setTimeout(resolve, milliseconds);
245
- });
768
+ return new Promise((resolve) => {
769
+ setTimeout(resolve, milliseconds);
770
+ });
246
771
  }
247
772
  function defaultClock() {
248
- return Date.now();
773
+ return Date.now();
249
774
  }
250
- export {
251
- FetchSlackUploader,
252
- SlackUploadError,
253
- SlackUploadStage
254
- };
775
+ //#endregion
776
+ export { FetchSlackUploader, SlackUploadError, SlackUploadStage, isConfigurationError, isOperationError, isSlackUploadError };
777
+
255
778
  //# sourceMappingURL=slack.js.map