@soundbi/sound-connect 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.
Files changed (42) hide show
  1. package/README.md +111 -0
  2. package/dist/__tests__/ingest.test.d.ts +18 -0
  3. package/dist/__tests__/ingest.test.d.ts.map +1 -0
  4. package/dist/__tests__/ingest.test.js +639 -0
  5. package/dist/__tests__/ingest.test.js.map +1 -0
  6. package/dist/__tests__/isolation.test.d.ts +12 -0
  7. package/dist/__tests__/isolation.test.d.ts.map +1 -0
  8. package/dist/__tests__/isolation.test.js +149 -0
  9. package/dist/__tests__/isolation.test.js.map +1 -0
  10. package/dist/__tests__/retry-queue.test.d.ts +11 -0
  11. package/dist/__tests__/retry-queue.test.d.ts.map +1 -0
  12. package/dist/__tests__/retry-queue.test.js +458 -0
  13. package/dist/__tests__/retry-queue.test.js.map +1 -0
  14. package/dist/auth.d.ts +80 -0
  15. package/dist/auth.d.ts.map +1 -0
  16. package/dist/auth.js +211 -0
  17. package/dist/auth.js.map +1 -0
  18. package/dist/config.d.ts +35 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +66 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +100 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ingest.d.ts +253 -0
  27. package/dist/ingest.d.ts.map +1 -0
  28. package/dist/ingest.js +573 -0
  29. package/dist/ingest.js.map +1 -0
  30. package/dist/proxy.d.ts +79 -0
  31. package/dist/proxy.d.ts.map +1 -0
  32. package/dist/proxy.js +217 -0
  33. package/dist/proxy.js.map +1 -0
  34. package/dist/retry-queue.d.ts +236 -0
  35. package/dist/retry-queue.d.ts.map +1 -0
  36. package/dist/retry-queue.js +461 -0
  37. package/dist/retry-queue.js.map +1 -0
  38. package/dist/tools.d.ts +75 -0
  39. package/dist/tools.d.ts.map +1 -0
  40. package/dist/tools.js +368 -0
  41. package/dist/tools.js.map +1 -0
  42. package/package.json +36 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,qEAAqE;AACrE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,GAAG,CAAC,EAAE,MAAM;CAIzB;AAED,0DAA0D;AAC1D,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAKxC;AAED,gDAAgD;AAChD,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,YAAY,CAAC,EAAE,MAAM;CAQlC;AAED,iDAAiD;AACjD,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM;CAIzC;AAED,2EAA2E;AAC3E,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,MAAM,EAAE,MAAM;CAI3B;AA4FD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,CAAC,CA6BxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,iBAAiB,CAAC,CA2C5B"}
package/dist/proxy.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Backend proxy module for the Sound Connect bridge (STORY-008, ADR-002, ADR-003).
3
+ *
4
+ * Implements two operations against the backend `/mcp/:slug` endpoint:
5
+ * 1. fetchBackendTools — issues a `tools/list` JSON-RPC call and returns the tool list.
6
+ * 2. callBackendTool — issues a `tools/call` JSON-RPC call and returns the result.
7
+ *
8
+ * Both operations authenticate via `Authorization: Bearer <token>` (ADR-003).
9
+ * The backend uses StreamableHTTPServerTransport in stateless mode — every call is an
10
+ * independent HTTP POST; no session handshake is required.
11
+ *
12
+ * ADR-011 error handling:
13
+ * - 401 → BackendAuthError (token invalid / expired — user must re-login)
14
+ * - 403 → BackendForbiddenError (peer not a member of this client)
15
+ * - 429 → BackendRateLimitError (back off and retry)
16
+ * - 5xx → BackendUnavailableError (backend unreachable / internal error)
17
+ * - JSON parse failure → BackendProtocolError (unexpected response shape)
18
+ *
19
+ * These are surfaced as MCP tool errors in tools.ts — never as silent failures.
20
+ */
21
+ // ── Typed error classes (ADR-011) ─────────────────────────────────────────────
22
+ /** The bearer token was rejected (401). Peer must re-run `login`. */
23
+ export class BackendAuthError extends Error {
24
+ constructor(msg) {
25
+ super(msg ?? 'Bearer token rejected by backend — run `npx @soundbi/sound-connect login` to re-authenticate.');
26
+ this.name = 'BackendAuthError';
27
+ }
28
+ }
29
+ /** Peer is not a member of the requested client (403). */
30
+ export class BackendForbiddenError extends Error {
31
+ constructor(slug, hint) {
32
+ const base = `Access denied to client "${slug}".`;
33
+ super(hint ? `${base} ${hint}` : `${base} Verify your Sound Connect membership.`);
34
+ this.name = 'BackendForbiddenError';
35
+ }
36
+ }
37
+ /** Backend is rate-limiting this peer (429). */
38
+ export class BackendRateLimitError extends Error {
39
+ constructor(retryAfterMs) {
40
+ super(retryAfterMs !== undefined
41
+ ? `Rate limited by backend. Retry after ${Math.ceil(retryAfterMs / 1000)} seconds.`
42
+ : 'Rate limited by backend. Wait a moment and try again.');
43
+ this.name = 'BackendRateLimitError';
44
+ }
45
+ }
46
+ /** Backend returned a 5xx or was unreachable. */
47
+ export class BackendUnavailableError extends Error {
48
+ constructor(status, msg) {
49
+ super(msg ?? `Backend returned HTTP ${status}. The server may be temporarily unavailable — try again.`);
50
+ this.name = 'BackendUnavailableError';
51
+ }
52
+ }
53
+ /** The backend response did not conform to the expected JSON-RPC shape. */
54
+ export class BackendProtocolError extends Error {
55
+ constructor(detail) {
56
+ super(`Unexpected backend response: ${detail}`);
57
+ this.name = 'BackendProtocolError';
58
+ }
59
+ }
60
+ // ── JSON-RPC helpers ───────────────────────────────────────────────────────────
61
+ let _rpcId = 1;
62
+ function makeRpcId() {
63
+ return _rpcId++;
64
+ }
65
+ function buildHeaders(token) {
66
+ return {
67
+ 'Content-Type': 'application/json',
68
+ 'Accept': 'application/json, text/event-stream',
69
+ 'Authorization': `Bearer ${token}`,
70
+ };
71
+ }
72
+ function mcpUrl(backendUrl, slug) {
73
+ return `${backendUrl}/mcp/${encodeURIComponent(slug)}`;
74
+ }
75
+ // ── HTTP error classification (ADR-011) ───────────────────────────────────────
76
+ async function throwForStatus(res, slug) {
77
+ if (res.ok)
78
+ return;
79
+ let bodyText = '';
80
+ try {
81
+ bodyText = await res.text();
82
+ }
83
+ catch { /* ignore body read failure */ }
84
+ let hint;
85
+ try {
86
+ const parsed = JSON.parse(bodyText);
87
+ hint = (parsed['hint'] ?? parsed['error']);
88
+ }
89
+ catch { /* body was not JSON */ }
90
+ switch (res.status) {
91
+ case 401:
92
+ throw new BackendAuthError(hint);
93
+ case 403:
94
+ throw new BackendForbiddenError(slug, hint);
95
+ case 429: {
96
+ const retryHeader = res.headers.get('retry-after') ?? res.headers.get('x-retry-after');
97
+ const retryMs = retryHeader ? parseInt(retryHeader, 10) * 1000 : (() => {
98
+ try {
99
+ return JSON.parse(bodyText)['retry_after_ms'];
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ })();
105
+ throw new BackendRateLimitError(retryMs);
106
+ }
107
+ default:
108
+ if (res.status >= 500) {
109
+ throw new BackendUnavailableError(res.status, hint);
110
+ }
111
+ throw new BackendProtocolError(`HTTP ${res.status}: ${bodyText.slice(0, 200)}`);
112
+ }
113
+ }
114
+ // ── SSE / JSON response parsing ───────────────────────────────────────────────
115
+ async function parseRpcResponse(res) {
116
+ const contentType = res.headers.get('content-type') ?? '';
117
+ if (contentType.includes('text/event-stream')) {
118
+ const text = await res.text();
119
+ for (const line of text.split('\n')) {
120
+ const trimmed = line.trim();
121
+ if (trimmed.startsWith('data:')) {
122
+ const json = trimmed.slice(5).trim();
123
+ if (json) {
124
+ try {
125
+ return JSON.parse(json);
126
+ }
127
+ catch { /* try next line */ }
128
+ }
129
+ }
130
+ }
131
+ throw new BackendProtocolError('SSE stream contained no parseable data line');
132
+ }
133
+ const text = await res.text();
134
+ try {
135
+ return JSON.parse(text);
136
+ }
137
+ catch {
138
+ throw new BackendProtocolError(`Non-JSON response body: ${text.slice(0, 200)}`);
139
+ }
140
+ }
141
+ // ── Public API ────────────────────────────────────────────────────────────────
142
+ /**
143
+ * Fetch the backend's tool list for the bound client.
144
+ *
145
+ * Sends `tools/list` JSON-RPC to `/mcp/:slug` with the peer's bearer token.
146
+ * Returns the tool array from the response; throws a typed BackendXxxError on failure.
147
+ *
148
+ * STORY-008 AC1: bridge fetches the backend tool list for the bound client.
149
+ */
150
+ export async function fetchBackendTools(backendUrl, slug, token) {
151
+ const url = mcpUrl(backendUrl, slug);
152
+ const body = JSON.stringify({
153
+ jsonrpc: '2.0',
154
+ id: makeRpcId(),
155
+ method: 'tools/list',
156
+ params: {},
157
+ });
158
+ let res;
159
+ try {
160
+ res = await fetch(url, { method: 'POST', headers: buildHeaders(token), body });
161
+ }
162
+ catch (err) {
163
+ throw new BackendUnavailableError(0, `Backend unreachable at ${url}: ${err.message}`);
164
+ }
165
+ await throwForStatus(res, slug);
166
+ const rpc = await parseRpcResponse(res);
167
+ const result = rpc?.['result'] ?? rpc;
168
+ const tools = result?.['tools'];
169
+ if (!Array.isArray(tools)) {
170
+ throw new BackendProtocolError(`tools/list response missing "tools" array. Got: ${JSON.stringify(rpc).slice(0, 200)}`);
171
+ }
172
+ return tools;
173
+ }
174
+ /**
175
+ * Forward a tool call to the backend `/mcp/:slug` endpoint.
176
+ *
177
+ * Sends `tools/call` JSON-RPC with the provided arguments, authenticated via bearer token.
178
+ * Parses the result and returns it as a BackendToolResult.
179
+ *
180
+ * STORY-008 AC2: tool calls forwarded with Authorization: Bearer <token>.
181
+ * STORY-008 AC4: backend errors surface as MCP tool errors (isError: true), not exceptions.
182
+ */
183
+ export async function callBackendTool(backendUrl, slug, token, toolName, toolArgs) {
184
+ const url = mcpUrl(backendUrl, slug);
185
+ const body = JSON.stringify({
186
+ jsonrpc: '2.0',
187
+ id: makeRpcId(),
188
+ method: 'tools/call',
189
+ params: {
190
+ name: toolName,
191
+ arguments: toolArgs,
192
+ },
193
+ });
194
+ let res;
195
+ try {
196
+ res = await fetch(url, { method: 'POST', headers: buildHeaders(token), body });
197
+ }
198
+ catch (err) {
199
+ throw new BackendUnavailableError(0, `Backend unreachable at ${url}: ${err.message}`);
200
+ }
201
+ await throwForStatus(res, slug);
202
+ const rpc = await parseRpcResponse(res);
203
+ const result = rpc?.['result'] ?? rpc;
204
+ if (!result || typeof result !== 'object') {
205
+ throw new BackendProtocolError(`tools/call response missing result. Got: ${JSON.stringify(rpc).slice(0, 200)}`);
206
+ }
207
+ const r = result;
208
+ const content = r['content'];
209
+ if (!Array.isArray(content)) {
210
+ throw new BackendProtocolError(`tools/call result missing "content" array. Got: ${JSON.stringify(result).slice(0, 200)}`);
211
+ }
212
+ return {
213
+ content: content,
214
+ isError: typeof r['isError'] === 'boolean' ? r['isError'] : false,
215
+ };
216
+ }
217
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Local ingest retry queue — STORY-013, ADR-011.
3
+ *
4
+ * When the Sound Connect backend is briefly unreachable (network error or 5xx),
5
+ * failed /ingest POST chunks are written to a local file-based queue so they are
6
+ * not silently lost. A background retry worker drains the queue with exponential
7
+ * backoff; the ADR-004 sha256 idempotency key makes re-sends safe.
8
+ *
9
+ * Queue state machine per item:
10
+ * pending → retry attempts < MAX_RETRY_ATTEMPTS and no permanent 4xx failure
11
+ * dead → 4xx response from backend (auth/permission error — do not retry)
12
+ *
13
+ * Queue storage: OS temp dir / sound-connect-queue / <clientSlug> /
14
+ * Each item is a JSON file: <enqueuedAt>-<sha256Prefix>.json
15
+ * A dead item is renamed to: <name>.dead
16
+ *
17
+ * Restart resilience: items survive a bridge restart because they live on disk.
18
+ * On startup, call loadQueue() to discover and resume pending items.
19
+ *
20
+ * Permanent failures (4xx): written to disk as .dead files and surfaced via
21
+ * ingest_status so the user can see them. They are never silently retried.
22
+ *
23
+ * ADR-011: fail closed on auth (4xx → dead, not retried), safe retry on transient
24
+ * outages, idempotency from ADR-004 makes retries harmless.
25
+ */
26
+ /** Maximum number of retry attempts before giving up (and marking dead). */
27
+ export declare const MAX_RETRY_ATTEMPTS = 8;
28
+ /** Base delay in milliseconds for exponential backoff (doubles each attempt). */
29
+ export declare const BACKOFF_BASE_MS = 1000;
30
+ /** Maximum backoff cap in milliseconds (~2 minutes). */
31
+ export declare const BACKOFF_MAX_MS = 120000;
32
+ /** Interval in milliseconds between retry worker sweeps. */
33
+ export declare const WORKER_INTERVAL_MS = 10000;
34
+ /** The ingest payload stored in each queue item. */
35
+ export interface QueuedIngestPayload {
36
+ source_type: 'markdown' | 'transcript';
37
+ filename: string;
38
+ content: string;
39
+ sha256: string;
40
+ timestamp: string;
41
+ author_email: string;
42
+ workstream_slug?: string;
43
+ }
44
+ /** A single queue item persisted to disk. */
45
+ export interface QueueItem {
46
+ /** Unique ID (timestamp-sha256prefix). */
47
+ id: string;
48
+ /** Backend base URL at the time of enqueue. */
49
+ backendUrl: string;
50
+ /** Client slug (ADR-005: must match the bound instance slug). */
51
+ clientSlug: string;
52
+ /** ISO timestamp when the item was first enqueued. */
53
+ enqueuedAt: string;
54
+ /** Number of retry attempts so far (0 = never tried after enqueue). */
55
+ attempts: number;
56
+ /** ISO timestamp of the last retry attempt (undefined if never retried). */
57
+ lastAttemptAt?: string;
58
+ /** Last error message (for surfacing in ingest_status). */
59
+ lastError?: string;
60
+ /** The ingest payload to re-POST. */
61
+ payload: QueuedIngestPayload;
62
+ }
63
+ /** Summary of the current queue state (for ingest_status tool). */
64
+ export interface QueueStatus {
65
+ /** Number of items waiting to be retried (pending). */
66
+ pending: number;
67
+ /** Number of permanently-failed items (4xx — need user action). */
68
+ dead: number;
69
+ /** Details of dead items for surfacing. */
70
+ deadItems: Array<{
71
+ id: string;
72
+ filename: string;
73
+ enqueuedAt: string;
74
+ lastError: string;
75
+ }>;
76
+ /** Details of pending items (oldest first). */
77
+ pendingItems: Array<{
78
+ id: string;
79
+ filename: string;
80
+ enqueuedAt: string;
81
+ attempts: number;
82
+ lastError?: string;
83
+ }>;
84
+ }
85
+ /**
86
+ * Returns the absolute path to the queue directory for a given client slug.
87
+ * Uses OS temp dir so the queue is writable without special permissions.
88
+ * Items survive bridge restarts as long as the OS does not clean temp on reboot.
89
+ *
90
+ * Overridable via SC_QUEUE_DIR env var for tests.
91
+ */
92
+ export declare function queueDir(clientSlug: string): string;
93
+ /**
94
+ * Enqueue a failed ingest chunk to the local retry queue.
95
+ *
96
+ * Writes a JSON file to the queue directory. The file is named by a unique ID
97
+ * derived from the enqueue time and the chunk's sha256 prefix (stable, not random —
98
+ * so if the same chunk is enqueued twice due to a crash, we don't duplicate items;
99
+ * but the timestamp disambiguates distinct enqueue events for the same content).
100
+ *
101
+ * @param backendUrl Backend URL that was unreachable.
102
+ * @param clientSlug Bound client slug.
103
+ * @param payload The ingest payload that failed.
104
+ * @param lastError The error message from the failed attempt.
105
+ * @returns The created QueueItem.
106
+ */
107
+ export declare function enqueueFailedChunk(backendUrl: string, clientSlug: string, payload: QueuedIngestPayload, lastError: string): Promise<QueueItem>;
108
+ /**
109
+ * Load all pending queue items from disk.
110
+ *
111
+ * Reads all .json files in the queue directory. Dead (.dead) files are NOT
112
+ * returned here — use loadDeadItems() for those.
113
+ *
114
+ * @param clientSlug Bound client slug.
115
+ * @returns Array of pending QueueItems, sorted by enqueuedAt ascending.
116
+ */
117
+ export declare function loadPendingItems(clientSlug: string): Promise<QueueItem[]>;
118
+ /**
119
+ * Load all dead items from disk.
120
+ *
121
+ * Dead items are .dead files in the queue directory (4xx permanent failures).
122
+ *
123
+ * @param clientSlug Bound client slug.
124
+ * @returns Array of dead QueueItems.
125
+ */
126
+ export declare function loadDeadItems(clientSlug: string): Promise<QueueItem[]>;
127
+ /**
128
+ * Returns a summary of the current queue state for the `ingest_status` MCP tool.
129
+ *
130
+ * STORY-013 AC3: reports pending/failed queue depth.
131
+ *
132
+ * @param clientSlug Bound client slug.
133
+ */
134
+ export declare function getQueueStatus(clientSlug: string): Promise<QueueStatus>;
135
+ /**
136
+ * Compute the exponential backoff delay for a given attempt number.
137
+ *
138
+ * Attempt 1 → BACKOFF_BASE_MS (1s)
139
+ * Attempt 2 → 2s
140
+ * Attempt 3 → 4s
141
+ * ...capped at BACKOFF_MAX_MS.
142
+ */
143
+ export declare function backoffMs(attempt: number): number;
144
+ /**
145
+ * Returns true if an item is ready to retry based on its attempt count and the
146
+ * time of the last attempt.
147
+ *
148
+ * An item with 0 attempts is always ready (never tried yet after enqueue).
149
+ * An item that has been attempted is ready once the backoff window has elapsed.
150
+ */
151
+ export declare function isReadyToRetry(item: QueueItem, now?: Date): boolean;
152
+ /**
153
+ * Result of a single retry POST attempt.
154
+ */
155
+ export type RetryAttemptResult = {
156
+ outcome: 'success';
157
+ deduped: boolean;
158
+ } | {
159
+ outcome: 'transient';
160
+ error: string;
161
+ } | {
162
+ outcome: 'permanent';
163
+ error: string;
164
+ };
165
+ /**
166
+ * Attempt to POST a queued ingest chunk to the backend.
167
+ *
168
+ * Distinguishes:
169
+ * - success (2xx) → remove from queue
170
+ * - transient (network / 5xx) → increment attempts, retry later
171
+ * - permanent (4xx) → mark dead, surface to user (ADR-011)
172
+ *
173
+ * @param item The queue item to retry.
174
+ * @param token Current bearer token (acquired silently before calling).
175
+ */
176
+ export declare function attemptRetry(item: QueueItem, token: string): Promise<RetryAttemptResult>;
177
+ /** Callback type for acquiring a fresh token before each retry sweep. */
178
+ export type TokenProvider = () => Promise<string | null>;
179
+ /**
180
+ * Run one sweep of the retry worker:
181
+ * 1. Load all pending items.
182
+ * 2. For each item that is ready to retry (backoff elapsed):
183
+ * a. Acquire a fresh token (silent).
184
+ * b. Attempt the POST.
185
+ * c. On success → remove from disk.
186
+ * d. On transient → increment attempts; if exhausted → mark dead.
187
+ * e. On permanent → mark dead.
188
+ *
189
+ * @param clientSlug Bound client slug.
190
+ * @param tokenProvider Async function that returns a fresh access token or null.
191
+ * @returns Summary of this sweep.
192
+ */
193
+ export declare function runRetrySweep(clientSlug: string, tokenProvider: TokenProvider): Promise<{
194
+ delivered: number;
195
+ exhausted: number;
196
+ skipped: number;
197
+ tokenMissing: boolean;
198
+ }>;
199
+ /**
200
+ * Start the background retry worker.
201
+ *
202
+ * Runs runRetrySweep() every WORKER_INTERVAL_MS milliseconds.
203
+ * Safe to call multiple times — subsequent calls are no-ops if already running.
204
+ *
205
+ * STORY-013 AC2: The worker drains the queue with exponential backoff.
206
+ * STORY-013 AC4: Items survive restarts; the worker picks them up on next sweep.
207
+ *
208
+ * @param clientSlug Bound client slug.
209
+ * @param tokenProvider Async function that returns a fresh access token or null.
210
+ */
211
+ export declare function startRetryWorker(clientSlug: string, tokenProvider: TokenProvider): void;
212
+ /**
213
+ * Stop the background retry worker.
214
+ * Called during tests or graceful shutdown.
215
+ */
216
+ export declare function stopRetryWorker(): void;
217
+ /**
218
+ * Remove all pending and dead items from the queue directory.
219
+ * Intended for test teardown only — do not call in production paths.
220
+ *
221
+ * @param clientSlug Bound client slug.
222
+ */
223
+ export declare function clearQueue(clientSlug: string): Promise<void>;
224
+ /**
225
+ * Clear all dead items (permanent failures) from the queue.
226
+ * Useful after the user re-authenticates and wants to purge the dead letter list.
227
+ *
228
+ * @param clientSlug Bound client slug.
229
+ */
230
+ export declare function clearDeadItems(clientSlug: string): Promise<void>;
231
+ /**
232
+ * Get the base filename of a queue item (without extension).
233
+ * Exported for use in tests.
234
+ */
235
+ export declare function itemBasename(item: QueueItem): string;
236
+ //# sourceMappingURL=retry-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-queue.d.ts","sourceRoot":"","sources":["../src/retry-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,iFAAiF;AACjF,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,wDAAwD;AACxD,eAAO,MAAM,cAAc,SAAU,CAAC;AAEtC,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAIzC,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,UAAU,GAAG,YAAY,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,SAAS,EAAE,KAAK,CAAC;QACf,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,+CAA+C;IAC/C,YAAY,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAID;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGnD;AAqBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,SAAS,CAAC,CAwBpB;AAID;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CA0B/E;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAyB5E;AAID;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAuB7E;AAID;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAO/E;AAID;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,OAAO,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACxC;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,CAAC,CA+D7B;AAuCD,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC,CAoD3F;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,aAAa,GAC3B,IAAI,CAcN;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAKtC;AAID;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlE;AAID;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAatE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEpD"}