Upload to Slack

Your app owns posting. Build the complete envelope before the first post attempt:

interface PreparedSlackPost {
  readonly channel: string;
  readonly threadTs?: string;
  readonly blocks: ConversionResult["blocks"];
  readonly text: string;
  readonly receipts: ConversionResult["receipts"];
}

const result = await converter.convert(markdown, { timeoutMs: 30_000 });
const prepared: PreparedSlackPost = {
  channel,
  threadTs,
  blocks: result.blocks,
  text: result.text,
  receipts: result.receipts,
};
await postPrepared(prepared, { timeoutMs: 10_000 });

On post failure, return or throw this exact envelope and retry posting that.

Persist prepared -> posted only when your app needs crash recovery. That persistence still does not make Slack posting idempotent. slackmark provides no outbox, worker, database, or posting API.

You need three scopes. files:write covers the default external upload flow, files:read is needed only with waitForProcessing: true, and chat:write is for your own posting.

FetchSlackUploader defaults:

OptionDefault
Per-stage request timeout10 seconds
Processing pollingoff
Poll interval / attempts / total window250 ms / 20 / 30 seconds
channelIdabsent (private completion)
Retry / idempotency / orphan cleanupnone

An upload moves through five stages: admission, upload-URL allocation, byte upload, completion, and an optional file-info fetch. A rejection at admission carries phase requested and retry safety safe, because the uploader was never invoked.

Receipts record phase, file ID, sharing, processing, status, Retry-After, provider code, and retry safety. may-duplicate requires reconciliation; never means reuse the completed result. A polling failure after completion returns processing: "unverified" and never triggers replacement upload.

Whether to re-post a message, whether to re-upload a file (retrySafety), and whether to re-run a conversion are three separate questions, each with its own signal.

Do not log receipts or source-bearing URLs broadly. Files complete without a channelId. Confirm in your own workspace that a privately completed file renders for every viewer you expect.