@outposted/node 0.1.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.
@@ -0,0 +1,424 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // @outposted/node/resources/contents — Content publishing surface.
3
+ //
4
+ // A Content is one publish intent that fans out into N Jobs (one per target
5
+ // platform). The API contract (apps/api/.../contents) is the source of truth;
6
+ // shapes here mirror what the routes emit. We intentionally re-export the
7
+ // canonical domain types (`Content`, `ContentPayload`, `MediaItem`, etc) via
8
+ // type-only imports from @outposted/domain so the SDK and the API can never
9
+ // drift apart.
10
+ //
11
+ // Endpoints proxied here:
12
+ // - POST /v1/workspaces/:wsId/contents → create + dispatch
13
+ // - GET /v1/workspaces/:wsId/contents → list summaries
14
+ // - GET /v1/workspaces/:wsId/contents/:contentId → fetch one + jobs
15
+ // - DELETE /v1/workspaces/:wsId/contents/:contentId → cancel (idempotent)
16
+ // - POST /v1/workspaces/:wsId/contents/:contentId/retry → re-attempt publish
17
+ //
18
+ // Note: the API exposes cancel as `DELETE /contents/:id` (no `/cancel` suffix).
19
+ // The SDK still names the method `cancel(...)` because that's what consumers
20
+ // reach for — the HTTP verb is an implementation detail.
21
+ //
22
+ // Response shapes are unwrapped at the boundary. `list` returns
23
+ // `{ data, pagination }` from the API as `{ items, total }` so the caller
24
+ // works with a flat shape (and `total` is the only pagination field most
25
+ // callers need; limit/offset they passed themselves).
26
+ // ────────────────────────────────────────────────────────────────────────────
27
+
28
+ import type {
29
+ Content,
30
+ ContentPayload,
31
+ ContentStatus,
32
+ ContentType,
33
+ } from '@outposted/domain';
34
+ import type { HttpClient } from '../transport';
35
+
36
+ /**
37
+ * Body accepted by `ContentsResource.create`. Mirrors the POST body the API
38
+ * expects (`workspace_id` is taken from the URL, so we omit it here).
39
+ *
40
+ * `target_connections` is optional — the API auto-resolves a single connection
41
+ * per target platform when omitted. Pass an explicit list of
42
+ * `connected_account.id` values when a brand has ≥2 connections on a platform
43
+ * (otherwise the API returns 422 `connections_required`).
44
+ *
45
+ * `schedule_at` is optional ISO-8601 — omit for immediate publish.
46
+ */
47
+ export interface ContentCreateInput {
48
+ brand_id: string;
49
+ content_type: ContentType;
50
+ target_platforms: string[];
51
+ /**
52
+ * Optional explicit disambiguator when a brand has multiple connections on
53
+ * one of the target platforms. Each entry MUST be a `connected_account.id`.
54
+ */
55
+ target_connections?: string[];
56
+ content: ContentPayload;
57
+ /** ISO-8601 UTC timestamp. Omit for immediate publish. */
58
+ schedule_at?: string;
59
+ }
60
+
61
+ /**
62
+ * Per-job summary embedded in the `create` and `get` responses — one row per
63
+ * target platform (or per target connection when disambiguated).
64
+ */
65
+ export interface ContentJobSummary {
66
+ /** Job id in the create-response shape uses `job_id`; we expose it as `id` here. */
67
+ job_id: string;
68
+ platform: string;
69
+ status: string;
70
+ }
71
+
72
+ /**
73
+ * Per-slot upload descriptor returned by `create` when the request opted into
74
+ * direct-to-R2 large-media upload (`media[i].upload === true`). Clients PUT
75
+ * raw bytes to `upload_url`; the worker takes over once every slot lands.
76
+ */
77
+ export interface ContentUploadSlot {
78
+ position: number;
79
+ r2_key: string;
80
+ upload_url: string;
81
+ }
82
+
83
+ /**
84
+ * Response from `ContentsResource.create`. The API returns ONE of two shapes
85
+ * depending on whether the request opted into direct large-media upload:
86
+ *
87
+ * - `awaiting_media` branch: the client must PUT bytes to each
88
+ * `uploads[i].upload_url`; jobs are dispatched once every slot lands.
89
+ * - `queued` / `scheduled` branch: jobs were dispatched immediately (or
90
+ * scheduled for `scheduled_at`).
91
+ *
92
+ * Both shapes share `content_id`, `status`, and `created_at`. The remaining
93
+ * fields are split via a discriminated union so TypeScript can narrow on
94
+ * `status`.
95
+ */
96
+ export type ContentCreateResponse =
97
+ | {
98
+ content_id: string;
99
+ status: 'awaiting_media';
100
+ created_at: string;
101
+ uploads: ContentUploadSlot[];
102
+ }
103
+ | {
104
+ content_id: string;
105
+ status: 'queued' | 'scheduled';
106
+ created_at: string;
107
+ scheduled_at?: string;
108
+ jobs: ContentJobSummary[];
109
+ };
110
+
111
+ /**
112
+ * Bandwidth-conscious summary returned by `list` — no full payload. Use `get`
113
+ * for the full payload + nested jobs.
114
+ *
115
+ * Mirrors `@outposted/domain` `ContentSummary`.
116
+ */
117
+ export interface ContentSummary {
118
+ id: string;
119
+ brand_id: string;
120
+ content_type: ContentType;
121
+ status: ContentStatus;
122
+ target_platforms: string[];
123
+ jobs_count: number;
124
+ created_at: string;
125
+ scheduled_at: string | null;
126
+ }
127
+
128
+ /**
129
+ * Filters accepted by `ContentsResource.list`.
130
+ */
131
+ export interface ContentListQuery {
132
+ status?: ContentStatus;
133
+ brand_id?: string;
134
+ limit?: number;
135
+ offset?: number;
136
+ }
137
+
138
+ /**
139
+ * Detail shape returned by `get(contentId)`. The API rebuilds the payload from
140
+ * the persisted row and attaches the per-platform jobs; we surface that exactly
141
+ * as-is. Note this is a different shape from the canonical `Content` entity
142
+ * (which has no embedded `jobs`) — the API enriches it for clients.
143
+ */
144
+ export interface ContentDetailJob {
145
+ id: string;
146
+ platform_slug: string;
147
+ provider_slug: string;
148
+ status: string;
149
+ external_post_id: string | null;
150
+ external_post_url: string | null;
151
+ error_code: string | null;
152
+ error_message: string | null;
153
+ attempt_count: number;
154
+ queued_at: string;
155
+ completed_at: string | null;
156
+ poll_count: number | null;
157
+ processing_ms: number | null;
158
+ }
159
+
160
+ /**
161
+ * Full content detail returned by `get(contentId)`. Note that this is a wire
162
+ * shape — it overlaps with the domain `Content` entity but adds `jobs`,
163
+ * `completed_at`, and optional `media`/`media_progress`. The `payload` is the
164
+ * publishable union of fields any platform might consume; see
165
+ * `ContentPayload` for the exhaustive list.
166
+ */
167
+ export interface ContentDetail
168
+ extends Pick<
169
+ Content,
170
+ 'id' | 'brand_id' | 'content_type' | 'status' | 'target_platforms' | 'created_at' | 'scheduled_at'
171
+ > {
172
+ payload: ContentPayload;
173
+ completed_at: string | null;
174
+ jobs: ContentDetailJob[];
175
+ media_progress?: { uploaded: number; total: number };
176
+ media?: Array<{
177
+ position: number;
178
+ media_type: 'image' | 'video';
179
+ status: string;
180
+ uploaded_at: string | null;
181
+ }>;
182
+ }
183
+
184
+ /**
185
+ * Cancel response shape. The API returns the same envelope for both the
186
+ * "actually cancelled now" and the idempotent "already cancelled" cases —
187
+ * callers can treat both as success.
188
+ */
189
+ export interface ContentCancelResponse {
190
+ id: string;
191
+ status: 'cancelled';
192
+ scheduled_at: string | null;
193
+ cancelled_at: string | null;
194
+ }
195
+
196
+ /**
197
+ * Retry response shape. `retried_jobs` is the number of non-`published` jobs
198
+ * that were reset to `queued` (already-published jobs are left untouched).
199
+ */
200
+ export interface ContentRetryResponse {
201
+ content_id: string;
202
+ status: 'queued';
203
+ retried_jobs: number;
204
+ }
205
+
206
+ interface ContentListEnvelope {
207
+ data: ContentSummary[];
208
+ pagination: { limit: number; offset: number; total: number };
209
+ }
210
+
211
+ /**
212
+ * SDK accessor for the `/v1/workspaces/:wsId/contents` collection. The core
213
+ * publishing surface of the SDK — `create` is what most callers exercise.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
218
+ * const result = await outposted.contents.create('ws_123', {
219
+ * brand_id: 'brand_abc',
220
+ * content_type: 'post',
221
+ * target_platforms: ['instagram'],
222
+ * content: {
223
+ * caption: 'Hello from the SDK',
224
+ * media: [{ type: 'image', url: 'https://cdn.example.com/cover.jpg' }],
225
+ * },
226
+ * });
227
+ * ```
228
+ */
229
+ export class ContentsResource {
230
+ constructor(private readonly http: HttpClient) {}
231
+
232
+ /**
233
+ * Create a Content and dispatch a publish (or schedule it). For
234
+ * `content_type: 'post'` on Instagram with an image URL, the API queues a
235
+ * single job and returns 202 with the job descriptor.
236
+ *
237
+ * Pass an `Idempotency-Key` header at the transport level if you need
238
+ * exactly-once semantics across retries (V0 only honours the API-side
239
+ * `Idempotency-Key`; the SDK does not auto-generate one).
240
+ *
241
+ * @example curl
242
+ * ```bash
243
+ * curl -X POST -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
244
+ * -H "Content-Type: application/json" \
245
+ * -d '{
246
+ * "brand_id": "brand_abc",
247
+ * "content_type": "post",
248
+ * "target_platforms": ["instagram"],
249
+ * "content": {
250
+ * "caption": "Hello from curl",
251
+ * "media": [{ "type": "image", "url": "https://cdn.example.com/cover.jpg" }]
252
+ * }
253
+ * }' \
254
+ * https://api.outposted.one/api/v1/workspaces/ws_123/contents
255
+ * ```
256
+ *
257
+ * @example SDK (Instagram image post)
258
+ * ```ts
259
+ * const result = await outposted.contents.create('ws_123', {
260
+ * brand_id: 'brand_abc',
261
+ * content_type: 'post',
262
+ * target_platforms: ['instagram'],
263
+ * content: {
264
+ * caption: 'Hello from the SDK',
265
+ * media: [{ type: 'image', url: 'https://cdn.example.com/cover.jpg' }],
266
+ * },
267
+ * });
268
+ * if (result.status !== 'awaiting_media') {
269
+ * console.log(`queued ${result.jobs.length} job(s)`);
270
+ * }
271
+ * ```
272
+ *
273
+ * @example SDK (multi-connection disambiguation)
274
+ * ```ts
275
+ * await outposted.contents.create('ws_123', {
276
+ * brand_id: 'brand_abc',
277
+ * content_type: 'post',
278
+ * target_platforms: ['linkedin'],
279
+ * target_connections: ['conn_personal', 'conn_company'],
280
+ * content: { text: 'Cross-posted to both LinkedIn surfaces' },
281
+ * });
282
+ * ```
283
+ *
284
+ * Throws `OutpostedValidationError` (HTTP 400/422) when payload shape is
285
+ * invalid, content-type/platform matrix is violated, per-platform specs
286
+ * fail, or multiple connections are available without an explicit
287
+ * `target_connections`.
288
+ */
289
+ async create(
290
+ workspaceId: string,
291
+ input: ContentCreateInput,
292
+ ): Promise<ContentCreateResponse> {
293
+ return this.http.request<ContentCreateResponse>({
294
+ method: 'POST',
295
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/contents`,
296
+ body: input,
297
+ });
298
+ }
299
+
300
+ /**
301
+ * List contents in a workspace, newest first. Returns lightweight summaries
302
+ * (no full payload) — the API caps `limit` at 100 (default 50) and supports
303
+ * `status` / `brand_id` filters.
304
+ *
305
+ * Returns `{ items, total }` — the API envelope is unwrapped here so callers
306
+ * don't have to dig through `{ data, pagination }`.
307
+ *
308
+ * @example curl
309
+ * ```bash
310
+ * curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
311
+ * "https://api.outposted.one/api/v1/workspaces/ws_123/contents?status=published&limit=20"
312
+ * ```
313
+ *
314
+ * @example SDK
315
+ * ```ts
316
+ * const { items, total } = await outposted.contents.list('ws_123', {
317
+ * status: 'published',
318
+ * limit: 20,
319
+ * });
320
+ * console.log(`${items.length} of ${total}`);
321
+ * ```
322
+ */
323
+ async list(
324
+ workspaceId: string,
325
+ query?: ContentListQuery,
326
+ ): Promise<{ items: ContentSummary[]; total: number }> {
327
+ const response = await this.http.request<ContentListEnvelope>({
328
+ method: 'GET',
329
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/contents`,
330
+ query: query
331
+ ? {
332
+ status: query.status,
333
+ brand_id: query.brand_id,
334
+ limit: query.limit,
335
+ offset: query.offset,
336
+ }
337
+ : undefined,
338
+ });
339
+ return { items: response.data, total: response.pagination.total };
340
+ }
341
+
342
+ /**
343
+ * Fetch a single content by id, including the rebuilt publishable payload
344
+ * and the nested per-platform jobs. The primary status-polling endpoint for
345
+ * V0 — webhook delivery (E2) lets you avoid polling in most flows.
346
+ *
347
+ * Throws `OutpostedNotFoundError` (HTTP 404) for missing or cross-tenant
348
+ * content ids (the API does not leak existence across workspaces).
349
+ *
350
+ * @example curl
351
+ * ```bash
352
+ * curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
353
+ * https://api.outposted.one/api/v1/workspaces/ws_123/contents/cnt_abc
354
+ * ```
355
+ *
356
+ * @example SDK
357
+ * ```ts
358
+ * const content = await outposted.contents.get('ws_123', 'cnt_abc');
359
+ * console.log(content.status, content.jobs.length);
360
+ * ```
361
+ */
362
+ async get(workspaceId: string, contentId: string): Promise<ContentDetail> {
363
+ return this.http.request<ContentDetail>({
364
+ method: 'GET',
365
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/contents/${encodeURIComponent(contentId)}`,
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Cancel a queued or scheduled content. Idempotent — a second cancel on an
371
+ * already-cancelled content returns success (the API never 404s here).
372
+ *
373
+ * The underlying HTTP verb is `DELETE` (RESTful cancel semantics in the
374
+ * API); the SDK exposes it as `cancel(...)` because that's what consumers
375
+ * expect to call. Content in `publishing` returns 409
376
+ * `cancel_too_late` — surfaced here as `OutpostedConflictError`.
377
+ *
378
+ * @example curl
379
+ * ```bash
380
+ * curl -X DELETE -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
381
+ * https://api.outposted.one/api/v1/workspaces/ws_123/contents/cnt_abc
382
+ * ```
383
+ *
384
+ * @example SDK
385
+ * ```ts
386
+ * await outposted.contents.cancel('ws_123', 'cnt_abc');
387
+ * ```
388
+ */
389
+ async cancel(workspaceId: string, contentId: string): Promise<ContentCancelResponse> {
390
+ return this.http.request<ContentCancelResponse>({
391
+ method: 'DELETE',
392
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/contents/${encodeURIComponent(contentId)}`,
393
+ });
394
+ }
395
+
396
+ /**
397
+ * Re-attempt publishing a content that failed, partially failed, or got
398
+ * stuck in `publishing` past the in-flight window. No media re-upload —
399
+ * the persisted payload (R2 URLs included) is reused.
400
+ *
401
+ * Only non-`published` jobs are reset; the response's `retried_jobs` is the
402
+ * count that were actually re-enqueued. Returns 409 `not_retryable` (via
403
+ * `OutpostedConflictError`) for terminal-success or actively-publishing
404
+ * content.
405
+ *
406
+ * @example curl
407
+ * ```bash
408
+ * curl -X POST -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
409
+ * https://api.outposted.one/api/v1/workspaces/ws_123/contents/cnt_abc/retry
410
+ * ```
411
+ *
412
+ * @example SDK
413
+ * ```ts
414
+ * const { retried_jobs } = await outposted.contents.retry('ws_123', 'cnt_abc');
415
+ * console.log(`re-queued ${retried_jobs} job(s)`);
416
+ * ```
417
+ */
418
+ async retry(workspaceId: string, contentId: string): Promise<ContentRetryResponse> {
419
+ return this.http.request<ContentRetryResponse>({
420
+ method: 'POST',
421
+ path: `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/contents/${encodeURIComponent(contentId)}/retry`,
422
+ });
423
+ }
424
+ }
@@ -0,0 +1,186 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // @outposted/node/resources/deliveries — Webhook delivery inspection + retry.
3
+ //
4
+ // A WebhookDelivery is one attempt to POST a signed payload at a webhook
5
+ // endpoint. Each endpoint accumulates a delivery history (one row per
6
+ // outbound POST) which the customer inspects to debug receiver failures and
7
+ // manually re-queue stuck ones.
8
+ //
9
+ // Endpoints proxied here (see apps/api/src/app/api/v1/...):
10
+ // - GET /v1/workspaces/:wsId/webhooks/:id/deliveries
11
+ // → list deliveries for an endpoint, paginated, filterable by status
12
+ // + since/until ISO windows. API envelope is `{ data, total, limit,
13
+ // offset }`; we map `data` → `items` for symmetry with the rest of
14
+ // the SDK (every list method returns `{ items, ... }`).
15
+ // - POST /v1/workspaces/:wsId/deliveries/:deliveryId/redeliver
16
+ // → re-queue a delivery for another attempt. Typically used on
17
+ // `failed` or `dlq` rows — the original payload and signing key are
18
+ // reused, so the receiver sees a byte-identical retry (same body,
19
+ // new timestamp/signature). 202 Accepted.
20
+ //
21
+ // Errors are surfaced as the typed OutpostedError subclasses produced by the
22
+ // transport layer (e.g. OutpostedNotFoundError on 404).
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+
25
+ import type { WebhookDelivery, WebhookDeliveryStatus } from '@outposted/domain';
26
+ import type { HttpClient } from '../transport';
27
+
28
+ /** Query params accepted by `DeliveriesResource.list`. */
29
+ export interface DeliveriesListQuery {
30
+ /** Filter by terminal/in-flight state. Omit for "all statuses". */
31
+ status?: WebhookDeliveryStatus;
32
+ /** ISO-8601 datetime — only deliveries created at-or-after this instant. */
33
+ since?: string;
34
+ /** ISO-8601 datetime — only deliveries created at-or-before this instant. */
35
+ until?: string;
36
+ /** Page size, 1..100 (API default: 25). */
37
+ limit?: number;
38
+ /** Page offset, >=0 (API default: 0). */
39
+ offset?: number;
40
+ }
41
+
42
+ /**
43
+ * Paginated list of webhook deliveries. The `items` field is named `data` on
44
+ * the API wire — we rename it here so every SDK list method has a uniform
45
+ * `{ items, total, limit, offset }` shape.
46
+ */
47
+ export interface DeliveriesListResult {
48
+ items: WebhookDelivery[];
49
+ total: number;
50
+ limit: number;
51
+ offset: number;
52
+ }
53
+
54
+ /** Response shape returned by `POST .../redeliver` (HTTP 202). */
55
+ export interface DeliveryRedeliverResult {
56
+ ok: boolean;
57
+ delivery_id: string;
58
+ /** Lifecycle hint — typically `"queued"` once the workflow trigger lands. */
59
+ status: string;
60
+ }
61
+
62
+ interface DeliveriesListWireResponse {
63
+ data: WebhookDelivery[];
64
+ total: number;
65
+ limit: number;
66
+ offset: number;
67
+ }
68
+
69
+ /**
70
+ * SDK accessor for webhook delivery introspection and manual retry.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const outposted = new Outposted({ apiKey: process.env.OUTPOSTED_API_KEY! });
75
+ *
76
+ * // Inspect the last 25 failed deliveries for an endpoint
77
+ * const { items } = await outposted.deliveries.list('ws_123', 'wh_456', {
78
+ * status: 'failed',
79
+ * });
80
+ *
81
+ * // Force a retry on a single delivery
82
+ * await outposted.deliveries.redeliver('ws_123', items[0]!.id);
83
+ * ```
84
+ */
85
+ export class DeliveriesResource {
86
+ constructor(private readonly http: HttpClient) {}
87
+
88
+ /**
89
+ * List webhook deliveries for an endpoint, newest first.
90
+ *
91
+ * Supports status filtering plus a `since`/`until` ISO-8601 window for
92
+ * targeted post-incident audits. Pagination is offset-based; the response
93
+ * always echoes `limit`/`offset` so callers can drive a "load more"
94
+ * scroller without tracking client-side state.
95
+ *
96
+ * @example curl
97
+ * ```bash
98
+ * curl -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
99
+ * "https://api.outposted.one/api/v1/workspaces/ws_123/webhooks/wh_456/deliveries?status=failed&limit=50"
100
+ * ```
101
+ *
102
+ * @example SDK
103
+ * ```ts
104
+ * const page = await outposted.deliveries.list('ws_123', 'wh_456', {
105
+ * status: 'dlq',
106
+ * since: '2026-06-01T00:00:00.000Z',
107
+ * limit: 50,
108
+ * });
109
+ * for (const delivery of page.items) {
110
+ * console.log(delivery.id, delivery.last_response_status);
111
+ * }
112
+ * ```
113
+ *
114
+ * Throws `OutpostedNotFoundError` (404) if the endpoint id does not exist
115
+ * in the workspace, or `OutpostedValidationError` (400) if the query
116
+ * parameters are malformed (bad ISO date, status out of enum, etc.).
117
+ */
118
+ async list(
119
+ workspaceId: string,
120
+ webhookId: string,
121
+ query?: DeliveriesListQuery,
122
+ ): Promise<DeliveriesListResult> {
123
+ const path = `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(webhookId)}/deliveries`;
124
+ const response = await this.http.request<DeliveriesListWireResponse>({
125
+ method: 'GET',
126
+ path,
127
+ query: {
128
+ status: query?.status,
129
+ since: query?.since,
130
+ until: query?.until,
131
+ limit: query?.limit,
132
+ offset: query?.offset,
133
+ },
134
+ });
135
+ return {
136
+ items: response.data,
137
+ total: response.total,
138
+ limit: response.limit,
139
+ offset: response.offset,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Force a new delivery attempt for a previously-attempted delivery.
145
+ *
146
+ * Typically used on `failed` or `dlq` rows after the receiver was fixed —
147
+ * the original signed payload is reused, so the customer receives a
148
+ * byte-identical retry (only the `t=` timestamp in `X-Outposted-Signature`
149
+ * changes, by design, since signatures bind the timestamp). The API
150
+ * returns 202 immediately; the actual POST runs asynchronously on the
151
+ * delivery worker.
152
+ *
153
+ * @example curl
154
+ * ```bash
155
+ * curl -X POST -H "Authorization: Bearer $OUTPOSTED_API_KEY" \
156
+ * https://api.outposted.one/api/v1/workspaces/ws_123/deliveries/del_789/redeliver
157
+ * ```
158
+ *
159
+ * @example SDK
160
+ * ```ts
161
+ * // After fixing a receiver, replay every dlq delivery from yesterday
162
+ * const { items } = await outposted.deliveries.list('ws_123', 'wh_456', {
163
+ * status: 'dlq',
164
+ * since: '2026-06-02T00:00:00.000Z',
165
+ * until: '2026-06-03T00:00:00.000Z',
166
+ * });
167
+ * for (const delivery of items) {
168
+ * await outposted.deliveries.redeliver('ws_123', delivery.id);
169
+ * }
170
+ * ```
171
+ *
172
+ * Throws `OutpostedNotFoundError` (404) if the delivery id does not exist
173
+ * in the workspace, or `OutpostedServerError` (500) if the upstream queue
174
+ * (QStash) is misconfigured or rejected the trigger.
175
+ */
176
+ async redeliver(
177
+ workspaceId: string,
178
+ deliveryId: string,
179
+ ): Promise<DeliveryRedeliverResult> {
180
+ const path = `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/deliveries/${encodeURIComponent(deliveryId)}/redeliver`;
181
+ return this.http.request<DeliveryRedeliverResult>({
182
+ method: 'POST',
183
+ path,
184
+ });
185
+ }
186
+ }