sidekick-shared 0.26.1 → 0.26.3
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.
- package/CHANGELOG.md +27 -0
- package/README.md +93 -9
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +3 -1
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +134 -35
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.js +3 -1
- package/dist/providerFailure.d.ts +46 -0
- package/dist/providerFailure.js +318 -0
- package/dist/providerServiceStatus.d.ts +8 -0
- package/dist/providerServiceStatus.js +148 -0
- package/dist/providerServiceStatusTypes.d.ts +43 -0
- package/dist/providerServiceStatusTypes.js +2 -0
- package/dist/providerStatus.d.ts +5 -0
- package/dist/providerStatus.js +32 -0
- package/dist/providers/claudeCode.js +11 -2
- package/dist/providers/codex.js +17 -7
- package/dist/search/sessionSearch.d.ts +9 -5
- package/dist/search/sessionSearch.js +61 -90
- package/dist/watchers/factory.js +2 -8
- package/dist/watchers/jsonlTail.js +26 -2
- package/dist/watchers/jsonlWatcher.d.ts +5 -11
- package/dist/watchers/jsonlWatcher.js +21 -150
- package/dist/watchers/providerReaderWatcher.js +5 -3
- package/dist/watchers/sqliteWatcher.js +1 -0
- package/dist/watchers/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AccountProviderId } from './providerIds';
|
|
2
|
+
export type ProviderCredentialKind = 'oauth' | 'api-key' | 'unknown';
|
|
3
|
+
export interface ProviderAuthenticationEvidence {
|
|
4
|
+
credentialKind: ProviderCredentialKind;
|
|
5
|
+
state: 'authenticated' | 'missing' | 'signed-out' | 'expired' | 'rejected' | 'unknown';
|
|
6
|
+
checkedAt: string;
|
|
7
|
+
source: 'local-check' | 'provider-response';
|
|
8
|
+
}
|
|
9
|
+
export interface ProviderFailureInput {
|
|
10
|
+
provider: AccountProviderId;
|
|
11
|
+
credentialKind: ProviderCredentialKind;
|
|
12
|
+
/** The caller, not this helper, determines whether the error is terminal. */
|
|
13
|
+
error: unknown;
|
|
14
|
+
/** The caller owns freshness and relevance of this separate observation. */
|
|
15
|
+
authentication?: ProviderAuthenticationEvidence;
|
|
16
|
+
}
|
|
17
|
+
export type ProviderFailureCode = 'missing_credentials' | 'oauth_reauthentication_required' | 'api_credentials_rejected' | 'authentication_rejected' | 'invalid_provider_session' | 'service_unavailable' | 'connection_failed' | 'timeout' | 'rate_limited' | 'overloaded' | 'execution_policy_denied' | 'runtime_unavailable' | 'context_overflow' | 'unknown';
|
|
18
|
+
export type ProviderRecoveryAction = 'sign_in' | 'update_credentials' | 'check_authentication' | 'start_new_session' | 'retry_later' | 'check_connection' | 'review_execution_policy' | 'repair_runtime' | 'reduce_context' | 'inspect_error';
|
|
19
|
+
export type ProviderRetryAfter = {
|
|
20
|
+
kind: 'delay';
|
|
21
|
+
delayMs: number;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'date';
|
|
24
|
+
at: string;
|
|
25
|
+
};
|
|
26
|
+
export interface ProviderFailureEvidence {
|
|
27
|
+
source: 'structured-error' | 'http-status' | 'message' | 'authentication-check';
|
|
28
|
+
rule: ProviderFailureCode;
|
|
29
|
+
checkedAt?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Safe diagnostic projection. No raw error text, request data, or credentials. */
|
|
32
|
+
export interface ProviderFailureDiagnosis {
|
|
33
|
+
provider: AccountProviderId;
|
|
34
|
+
credentialKind: ProviderCredentialKind;
|
|
35
|
+
diagnosis: ProviderFailureCode;
|
|
36
|
+
recovery: ProviderRecoveryAction;
|
|
37
|
+
evidence: ProviderFailureEvidence[];
|
|
38
|
+
authentication?: ProviderAuthenticationEvidence;
|
|
39
|
+
httpStatus?: number;
|
|
40
|
+
retryAfter?: ProviderRetryAfter;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Classify a caller-designated terminal failure without I/O or recovery actions.
|
|
44
|
+
* Public status and runtime readiness do not establish request authentication.
|
|
45
|
+
*/
|
|
46
|
+
export declare function diagnoseProviderFailure(input: ProviderFailureInput): ProviderFailureDiagnosis;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diagnoseProviderFailure = diagnoseProviderFailure;
|
|
4
|
+
function read(value, key) {
|
|
5
|
+
try {
|
|
6
|
+
return value && typeof value === 'object' ? value[key] : undefined;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function text(value) {
|
|
13
|
+
return typeof value === 'string' ? value.slice(0, 16_384) : '';
|
|
14
|
+
}
|
|
15
|
+
function statusNumber(value) {
|
|
16
|
+
const number = typeof value === 'string' && /^\d{3}$/.test(value) ? Number(value) : value;
|
|
17
|
+
return typeof number === 'number' && Number.isInteger(number) && number >= 100 && number <= 599
|
|
18
|
+
? number
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
function timestamp(value) {
|
|
22
|
+
return typeof value === 'string' &&
|
|
23
|
+
value.length <= 64 &&
|
|
24
|
+
/^(?:\d{4}-\d{2}-\d{2}T|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), )/.test(value) &&
|
|
25
|
+
Number.isFinite(Date.parse(value))
|
|
26
|
+
? new Date(value).toISOString()
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
function retryAfter(value) {
|
|
30
|
+
if (typeof value === 'number' || (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value))) {
|
|
31
|
+
const delayMs = Number(value) * 1000;
|
|
32
|
+
return Number.isFinite(delayMs) && delayMs >= 0 && delayMs <= Number.MAX_SAFE_INTEGER
|
|
33
|
+
? { kind: 'delay', delayMs }
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
const at = timestamp(value);
|
|
37
|
+
return at ? { kind: 'date', at } : undefined;
|
|
38
|
+
}
|
|
39
|
+
function rejected(kind) {
|
|
40
|
+
return kind === 'oauth'
|
|
41
|
+
? 'oauth_reauthentication_required'
|
|
42
|
+
: kind === 'api-key'
|
|
43
|
+
? 'api_credentials_rejected'
|
|
44
|
+
: 'authentication_rejected';
|
|
45
|
+
}
|
|
46
|
+
function classifyCode(code, kind) {
|
|
47
|
+
switch (code.toLowerCase().replace(/[^a-z0-9]/g, '')) {
|
|
48
|
+
case 'missingcredentials':
|
|
49
|
+
case 'notloggedin':
|
|
50
|
+
case 'signedout':
|
|
51
|
+
return 'missing_credentials';
|
|
52
|
+
case 'invalidapikey':
|
|
53
|
+
case 'apikeyrejected':
|
|
54
|
+
return 'api_credentials_rejected';
|
|
55
|
+
case 'oauthtokenexpired':
|
|
56
|
+
case 'refreshtokenexpired':
|
|
57
|
+
case 'refreshtokenreused':
|
|
58
|
+
case 'invalidgrant':
|
|
59
|
+
return 'oauth_reauthentication_required';
|
|
60
|
+
case 'authenticationerror':
|
|
61
|
+
case 'autherror':
|
|
62
|
+
case 'invalidtoken':
|
|
63
|
+
case 'tokenexpired':
|
|
64
|
+
case 'unauthorized':
|
|
65
|
+
return rejected(kind);
|
|
66
|
+
case 'threadnotfound':
|
|
67
|
+
case 'invalidthreadid':
|
|
68
|
+
case 'conversationnotfound':
|
|
69
|
+
case 'invalidconversation':
|
|
70
|
+
case 'conversationexpired':
|
|
71
|
+
case 'invalidsessionid':
|
|
72
|
+
return 'invalid_provider_session';
|
|
73
|
+
case 'executionpolicydenied':
|
|
74
|
+
case 'sandboxdenied':
|
|
75
|
+
case 'approvaldenied':
|
|
76
|
+
return 'execution_policy_denied';
|
|
77
|
+
case 'ratelimiterror':
|
|
78
|
+
case 'ratelimitexceeded':
|
|
79
|
+
case 'toomanyrequests':
|
|
80
|
+
return 'rate_limited';
|
|
81
|
+
case 'overloadederror':
|
|
82
|
+
case 'serveroverloaded':
|
|
83
|
+
return 'overloaded';
|
|
84
|
+
case 'serviceunavailable':
|
|
85
|
+
case 'internalservererror':
|
|
86
|
+
case 'servererror':
|
|
87
|
+
return 'service_unavailable';
|
|
88
|
+
case 'timeouterror':
|
|
89
|
+
case 'apiconnectiontimeouterror':
|
|
90
|
+
case 'etimedout':
|
|
91
|
+
case 'underrconnecttimeout':
|
|
92
|
+
case 'underrheaderstimeout':
|
|
93
|
+
case 'deadlineexceeded':
|
|
94
|
+
return 'timeout';
|
|
95
|
+
case 'econnreset':
|
|
96
|
+
case 'econnrefused':
|
|
97
|
+
case 'enotfound':
|
|
98
|
+
case 'eaiagain':
|
|
99
|
+
case 'enetunreach':
|
|
100
|
+
case 'ehostunreach':
|
|
101
|
+
case 'apiconnectionerror':
|
|
102
|
+
return 'connection_failed';
|
|
103
|
+
case 'runtimenotfound':
|
|
104
|
+
case 'runtimeunavailable':
|
|
105
|
+
case 'modulenotfound':
|
|
106
|
+
case 'errmodulenotfound':
|
|
107
|
+
return 'runtime_unavailable';
|
|
108
|
+
case 'contextlengthexceeded':
|
|
109
|
+
case 'contextwindowexceeded':
|
|
110
|
+
case 'prompttoolong':
|
|
111
|
+
return 'context_overflow';
|
|
112
|
+
default:
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function classifyStatus(status, kind) {
|
|
117
|
+
if (status === 401)
|
|
118
|
+
return rejected(kind);
|
|
119
|
+
if (status === 408 || status === 504)
|
|
120
|
+
return 'timeout';
|
|
121
|
+
if (status === 429)
|
|
122
|
+
return 'rate_limited';
|
|
123
|
+
if (status === 529)
|
|
124
|
+
return 'overloaded';
|
|
125
|
+
if (status >= 500)
|
|
126
|
+
return 'service_unavailable';
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
function classifyMessage(message, kind) {
|
|
130
|
+
if (/\b(execution policy|sandbox policy|approval)\b.{0,60}\b(denied|blocked|rejected|not allowed)\b/i.test(message))
|
|
131
|
+
return 'execution_policy_denied';
|
|
132
|
+
if (/\b(thread|conversation|session id)\b.{0,60}\b(not found|invalid|expired|does not exist)\b|\b(invalid|expired) (thread|conversation|session id)\b/i.test(message))
|
|
133
|
+
return 'invalid_provider_session';
|
|
134
|
+
if (/\b(oauth|refresh token)\b.{0,60}\b(expired|invalid|rejected|revoked)\b/i.test(message))
|
|
135
|
+
return 'oauth_reauthentication_required';
|
|
136
|
+
if (/\b(api[ _-]?key)\b.{0,60}\b(invalid|incorrect|rejected|revoked|expired)\b|\b(invalid|incorrect|rejected) api[ _-]?key\b/i.test(message))
|
|
137
|
+
return 'api_credentials_rejected';
|
|
138
|
+
if (/\b(no|missing) (oauth |api[ _-]?key |authentication )?(credentials|token|api[ _-]?key)\b|\bnot (logged|signed) in\b|\bsigned out\b|\bapi[ _-]?key (not configured|not set)\b/i.test(message))
|
|
139
|
+
return 'missing_credentials';
|
|
140
|
+
if (/\b(authentication failed|invalid authentication|credentials (rejected|expired)|token (expired|rejected|invalid))\b/i.test(message))
|
|
141
|
+
return rejected(kind);
|
|
142
|
+
if (/\b(context (length|window).{0,40}(exceed|overflow)|prompt (is )?too long|maximum context length)\b/i.test(message))
|
|
143
|
+
return 'context_overflow';
|
|
144
|
+
if (/\b(overloaded|over capacity|server busy)\b/i.test(message))
|
|
145
|
+
return 'overloaded';
|
|
146
|
+
if (/\b(rate[ _-]?limit|too many requests)\b/i.test(message))
|
|
147
|
+
return 'rate_limited';
|
|
148
|
+
if (/\b(timed? ?out|deadline exceeded|etimedout)\b/i.test(message))
|
|
149
|
+
return 'timeout';
|
|
150
|
+
if (/\b(service unavailable|bad gateway|internal server error)\b/i.test(message))
|
|
151
|
+
return 'service_unavailable';
|
|
152
|
+
if (/\b(enotfound|eai_again|econnreset|econnrefused|dns (failure|lookup failed)|fetch failed|network (error|unreachable)|connection (reset|refused|terminated|termination)|upstream connect error|socket hang up)\b/i.test(message))
|
|
153
|
+
return 'connection_failed';
|
|
154
|
+
if (/\b(claude|codex|cli|runtime|executable)\b.{0,60}\b(not found|not installed|unavailable|not executable)\b|\bspawn\s+\S+\s+(ENOENT|EACCES)\b/i.test(message))
|
|
155
|
+
return 'runtime_unavailable';
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function recovery(code, kind) {
|
|
159
|
+
switch (code) {
|
|
160
|
+
case 'missing_credentials':
|
|
161
|
+
return kind === 'oauth'
|
|
162
|
+
? 'sign_in'
|
|
163
|
+
: kind === 'api-key'
|
|
164
|
+
? 'update_credentials'
|
|
165
|
+
: 'check_authentication';
|
|
166
|
+
case 'oauth_reauthentication_required':
|
|
167
|
+
return 'sign_in';
|
|
168
|
+
case 'api_credentials_rejected':
|
|
169
|
+
return 'update_credentials';
|
|
170
|
+
case 'authentication_rejected':
|
|
171
|
+
return 'check_authentication';
|
|
172
|
+
case 'invalid_provider_session':
|
|
173
|
+
return 'start_new_session';
|
|
174
|
+
case 'connection_failed':
|
|
175
|
+
return 'check_connection';
|
|
176
|
+
case 'execution_policy_denied':
|
|
177
|
+
return 'review_execution_policy';
|
|
178
|
+
case 'runtime_unavailable':
|
|
179
|
+
return 'repair_runtime';
|
|
180
|
+
case 'context_overflow':
|
|
181
|
+
return 'reduce_context';
|
|
182
|
+
case 'unknown':
|
|
183
|
+
return 'inspect_error';
|
|
184
|
+
default:
|
|
185
|
+
return 'retry_later';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Classify a caller-designated terminal failure without I/O or recovery actions.
|
|
190
|
+
* Public status and runtime readiness do not establish request authentication.
|
|
191
|
+
*/
|
|
192
|
+
function diagnoseProviderFailure(input) {
|
|
193
|
+
const codes = [];
|
|
194
|
+
const messages = [];
|
|
195
|
+
const visited = new Set();
|
|
196
|
+
const queue = [{ value: input.error, depth: 0 }];
|
|
197
|
+
let httpStatus;
|
|
198
|
+
let retry;
|
|
199
|
+
for (let index = 0; index < queue.length && index < 32; index++) {
|
|
200
|
+
const { value, depth } = queue[index];
|
|
201
|
+
if (typeof value === 'string') {
|
|
202
|
+
messages.push(text(value));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!value || typeof value !== 'object' || visited.has(value))
|
|
206
|
+
continue;
|
|
207
|
+
visited.add(value);
|
|
208
|
+
for (const key of ['code', 'type', 'name'])
|
|
209
|
+
codes.push(text(read(value, key)));
|
|
210
|
+
messages.push(text(read(value, 'message')));
|
|
211
|
+
for (const key of ['status', 'statusCode', 'httpStatus', 'status_code'])
|
|
212
|
+
httpStatus ??= statusNumber(read(value, key));
|
|
213
|
+
const headers = read(value, 'headers');
|
|
214
|
+
const header = typeof Headers !== 'undefined' && headers instanceof Headers
|
|
215
|
+
? headers.get('retry-after')
|
|
216
|
+
: (read(headers, 'retry-after') ?? read(headers, 'Retry-After'));
|
|
217
|
+
retry ??= retryAfter(header);
|
|
218
|
+
const delay = read(value, 'retryAfterMs');
|
|
219
|
+
if (!retry &&
|
|
220
|
+
typeof delay === 'number' &&
|
|
221
|
+
Number.isFinite(delay) &&
|
|
222
|
+
delay >= 0 &&
|
|
223
|
+
delay <= Number.MAX_SAFE_INTEGER)
|
|
224
|
+
retry = { kind: 'delay', delayMs: delay };
|
|
225
|
+
// ENOENT by itself can be a missing document. Require a spawn operation.
|
|
226
|
+
if (read(value, 'code') === 'ENOENT' && /^spawn\b/.test(text(read(value, 'syscall'))))
|
|
227
|
+
codes.push('runtime_unavailable');
|
|
228
|
+
if (depth < 5) {
|
|
229
|
+
for (const key of ['error', 'cause', 'response', 'data', 'body']) {
|
|
230
|
+
let nested = read(value, key);
|
|
231
|
+
if (typeof nested === 'string' &&
|
|
232
|
+
nested.length <= 16_384 &&
|
|
233
|
+
nested.trim().startsWith('{')) {
|
|
234
|
+
try {
|
|
235
|
+
nested = JSON.parse(nested);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
/* Keep message evidence. */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (nested != null)
|
|
242
|
+
queue.push({ value: nested, depth: depth + 1 });
|
|
243
|
+
}
|
|
244
|
+
const errors = read(value, 'errors');
|
|
245
|
+
if (Array.isArray(errors))
|
|
246
|
+
for (const error of errors.slice(0, 8))
|
|
247
|
+
queue.push({ value: error, depth: depth + 1 });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
let authentication;
|
|
251
|
+
const auth = input.authentication;
|
|
252
|
+
const checkedAt = timestamp(auth?.checkedAt);
|
|
253
|
+
if (auth &&
|
|
254
|
+
checkedAt &&
|
|
255
|
+
['oauth', 'api-key', 'unknown'].includes(auth.credentialKind) &&
|
|
256
|
+
['authenticated', 'missing', 'signed-out', 'expired', 'rejected', 'unknown'].includes(auth.state) &&
|
|
257
|
+
['local-check', 'provider-response'].includes(auth.source)) {
|
|
258
|
+
authentication = {
|
|
259
|
+
credentialKind: auth.credentialKind,
|
|
260
|
+
state: auth.state,
|
|
261
|
+
checkedAt,
|
|
262
|
+
source: auth.source,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
let diagnosis = codes
|
|
266
|
+
.map((code) => classifyCode(code, input.credentialKind))
|
|
267
|
+
.find((code) => code !== undefined);
|
|
268
|
+
let source = 'structured-error';
|
|
269
|
+
if (!diagnosis && httpStatus !== undefined) {
|
|
270
|
+
diagnosis = classifyStatus(httpStatus, input.credentialKind);
|
|
271
|
+
source = 'http-status';
|
|
272
|
+
}
|
|
273
|
+
if (!diagnosis) {
|
|
274
|
+
source = 'message';
|
|
275
|
+
for (const message of messages) {
|
|
276
|
+
// Message HTTP codes must have an HTTP/status context, not just a stray number.
|
|
277
|
+
const match = message.match(/\b(?:HTTP(?:\/\d(?:\.\d)?)?(?: error)?|(?:unexpected )?status(?: code)?|API error)\s*[:=]?\s*(\d{3})\b|\b(401|403|429|50[02349]|529)\s+(?:Unauthorized|Forbidden|Too Many Requests|Bad Gateway|Service Unavailable|Internal Server Error|Gateway Timeout)\b/i);
|
|
278
|
+
const status = match ? statusNumber(match[1] ?? match[2]) : undefined;
|
|
279
|
+
// A structured status cannot be replaced by a contradictory status in prose.
|
|
280
|
+
if (httpStatus === undefined && status !== undefined) {
|
|
281
|
+
httpStatus = status;
|
|
282
|
+
diagnosis = classifyStatus(status, input.credentialKind);
|
|
283
|
+
}
|
|
284
|
+
diagnosis ??= classifyMessage(message, input.credentialKind);
|
|
285
|
+
if (diagnosis)
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!diagnosis && authentication && authentication.credentialKind === input.credentialKind) {
|
|
290
|
+
if (authentication.state === 'missing' || authentication.state === 'signed-out')
|
|
291
|
+
diagnosis = 'missing_credentials';
|
|
292
|
+
if (authentication.state === 'expired' || authentication.state === 'rejected')
|
|
293
|
+
diagnosis = rejected(input.credentialKind);
|
|
294
|
+
if (diagnosis)
|
|
295
|
+
source = 'authentication-check';
|
|
296
|
+
}
|
|
297
|
+
diagnosis ??= 'unknown';
|
|
298
|
+
return {
|
|
299
|
+
provider: input.provider,
|
|
300
|
+
credentialKind: input.credentialKind,
|
|
301
|
+
diagnosis,
|
|
302
|
+
recovery: recovery(diagnosis, input.credentialKind),
|
|
303
|
+
evidence: diagnosis === 'unknown'
|
|
304
|
+
? []
|
|
305
|
+
: [
|
|
306
|
+
{
|
|
307
|
+
source,
|
|
308
|
+
rule: diagnosis,
|
|
309
|
+
...(source === 'authentication-check'
|
|
310
|
+
? { checkedAt: authentication.checkedAt }
|
|
311
|
+
: {}),
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
...(authentication ? { authentication } : {}),
|
|
315
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
316
|
+
...(retry ? { retryAfter: retry } : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AccountProviderId } from './providerIds';
|
|
2
|
+
import type { FetchProviderServiceStatusOptions, ProviderServiceStatus } from './providerServiceStatusTypes';
|
|
3
|
+
/**
|
|
4
|
+
* Fetch public vendor status without credentials, caching, polling, or retries.
|
|
5
|
+
* Incidents supplement a request diagnosis; they do not prove its cause. Consumers
|
|
6
|
+
* own freshness and component/endpoint correlation. Omitted incidents are null.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fetchProviderServiceStatus(provider: AccountProviderId, { signal }?: FetchProviderServiceStatusOptions): Promise<ProviderServiceStatus>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchProviderServiceStatus = fetchProviderServiceStatus;
|
|
4
|
+
const SOURCES = {
|
|
5
|
+
'claude-code': 'https://status.claude.com/api/v2/summary.json',
|
|
6
|
+
codex: 'https://status.openai.com/api/v2/summary.json',
|
|
7
|
+
};
|
|
8
|
+
function object(value) {
|
|
9
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
10
|
+
throw new Error('Invalid object');
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function string(value) {
|
|
14
|
+
if (typeof value !== 'string' || !value.trim())
|
|
15
|
+
throw new Error('Invalid string');
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function date(value) {
|
|
19
|
+
if (value === undefined || value === null)
|
|
20
|
+
return null;
|
|
21
|
+
const raw = string(value);
|
|
22
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(raw) || !Number.isFinite(Date.parse(raw)))
|
|
23
|
+
throw new Error('Invalid date');
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
function url(value) {
|
|
27
|
+
if (value === undefined || value === null)
|
|
28
|
+
return null;
|
|
29
|
+
const raw = string(value);
|
|
30
|
+
const parsed = new URL(raw);
|
|
31
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
32
|
+
throw new Error('Invalid link');
|
|
33
|
+
return raw;
|
|
34
|
+
}
|
|
35
|
+
function array(value) {
|
|
36
|
+
if (!Array.isArray(value))
|
|
37
|
+
throw new Error('Invalid array');
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function parseSummary(raw) {
|
|
41
|
+
const data = object(raw);
|
|
42
|
+
const status = object(data.status);
|
|
43
|
+
const severity = string(status.indicator);
|
|
44
|
+
if (!['none', 'minor', 'major', 'critical', 'maintenance'].includes(severity))
|
|
45
|
+
throw new Error('Invalid severity');
|
|
46
|
+
const components = array(data.components).map((rawComponent) => {
|
|
47
|
+
const component = object(rawComponent);
|
|
48
|
+
return {
|
|
49
|
+
id: string(component.id),
|
|
50
|
+
name: string(component.name),
|
|
51
|
+
status: string(component.status),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const incidents = data.incidents === undefined
|
|
55
|
+
? null
|
|
56
|
+
: array(data.incidents)
|
|
57
|
+
.map((rawIncident) => {
|
|
58
|
+
const incident = object(rawIncident);
|
|
59
|
+
// Statuspage uses objects; its compatible feeds may supply component IDs directly.
|
|
60
|
+
const associations = incident.components ?? incident.component_ids;
|
|
61
|
+
return {
|
|
62
|
+
id: string(incident.id),
|
|
63
|
+
title: string(incident.name),
|
|
64
|
+
status: string(incident.status),
|
|
65
|
+
impact: string(incident.impact),
|
|
66
|
+
url: url(incident.shortlink ?? incident.url),
|
|
67
|
+
updatedAt: date(incident.updated_at),
|
|
68
|
+
componentIds: associations === undefined
|
|
69
|
+
? null
|
|
70
|
+
: array(associations).map((component) => typeof component === 'string'
|
|
71
|
+
? string(component)
|
|
72
|
+
: string(object(component).id)),
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.filter((incident) => !['resolved', 'postmortem'].includes(incident.status.toLowerCase()));
|
|
76
|
+
return {
|
|
77
|
+
availability: 'observed',
|
|
78
|
+
severity: severity,
|
|
79
|
+
description: string(status.description),
|
|
80
|
+
providerUpdatedAt: date(object(data.page).updated_at),
|
|
81
|
+
components,
|
|
82
|
+
incidents,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fetch public vendor status without credentials, caching, polling, or retries.
|
|
87
|
+
* Incidents supplement a request diagnosis; they do not prove its cause. Consumers
|
|
88
|
+
* own freshness and component/endpoint correlation. Omitted incidents are null.
|
|
89
|
+
*/
|
|
90
|
+
async function fetchProviderServiceStatus(provider, { signal } = {}) {
|
|
91
|
+
const sourceUrl = SOURCES[provider];
|
|
92
|
+
const observation = () => ({ provider, sourceUrl, checkedAt: new Date().toISOString() });
|
|
93
|
+
const unavailable = (reason, httpStatus) => ({
|
|
94
|
+
...observation(),
|
|
95
|
+
availability: 'unavailable',
|
|
96
|
+
reason,
|
|
97
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
98
|
+
});
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
return unavailable('cancelled');
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
let aborted;
|
|
103
|
+
let rejectAbort = () => { };
|
|
104
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
105
|
+
rejectAbort = reject;
|
|
106
|
+
});
|
|
107
|
+
const abort = (reason) => {
|
|
108
|
+
if (aborted)
|
|
109
|
+
return;
|
|
110
|
+
aborted = reason;
|
|
111
|
+
controller.abort();
|
|
112
|
+
rejectAbort(new Error(reason));
|
|
113
|
+
};
|
|
114
|
+
const onCancel = () => abort('cancelled');
|
|
115
|
+
signal?.addEventListener('abort', onCancel, { once: true });
|
|
116
|
+
const timer = setTimeout(() => abort('timeout'), 10_000);
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
try {
|
|
119
|
+
return await Promise.race([
|
|
120
|
+
abortPromise,
|
|
121
|
+
(async () => {
|
|
122
|
+
const response = await fetch(sourceUrl, { signal: controller.signal, credentials: 'omit' });
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
return unavailable('http_error', response.status);
|
|
125
|
+
let body;
|
|
126
|
+
try {
|
|
127
|
+
body = await response.json();
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return unavailable(aborted ?? (error instanceof SyntaxError ? 'invalid_response' : 'network_error'));
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return { ...observation(), ...parseSummary(body) };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return unavailable('invalid_response');
|
|
137
|
+
}
|
|
138
|
+
})(),
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return unavailable(aborted ?? 'network_error');
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
signal?.removeEventListener('abort', onCancel);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AccountProviderId } from './providerIds';
|
|
2
|
+
export type ProviderServiceSeverity = 'none' | 'minor' | 'major' | 'critical' | 'maintenance';
|
|
3
|
+
export interface ProviderServiceComponent {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderServiceIncident {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
status: string;
|
|
12
|
+
impact: string;
|
|
13
|
+
url: string | null;
|
|
14
|
+
updatedAt: string | null;
|
|
15
|
+
/** null means associations were not supplied; [] means explicitly no associations. */
|
|
16
|
+
componentIds: string[] | null;
|
|
17
|
+
}
|
|
18
|
+
export type ProviderServiceStatusFailureReason = 'http_error' | 'network_error' | 'timeout' | 'cancelled' | 'invalid_response';
|
|
19
|
+
interface ProviderServiceObservation {
|
|
20
|
+
provider: AccountProviderId;
|
|
21
|
+
/** Completion time of this check, independent of the provider's update time. */
|
|
22
|
+
checkedAt: string;
|
|
23
|
+
sourceUrl: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ObservedProviderServiceStatus extends ProviderServiceObservation {
|
|
26
|
+
availability: 'observed';
|
|
27
|
+
severity: ProviderServiceSeverity;
|
|
28
|
+
description: string;
|
|
29
|
+
providerUpdatedAt: string | null;
|
|
30
|
+
components: ProviderServiceComponent[];
|
|
31
|
+
/** null means the feed omitted incident evidence, not that there are no incidents. */
|
|
32
|
+
incidents: ProviderServiceIncident[] | null;
|
|
33
|
+
}
|
|
34
|
+
export interface UnavailableProviderServiceStatus extends ProviderServiceObservation {
|
|
35
|
+
availability: 'unavailable';
|
|
36
|
+
reason: ProviderServiceStatusFailureReason;
|
|
37
|
+
httpStatus?: number;
|
|
38
|
+
}
|
|
39
|
+
export type ProviderServiceStatus = ObservedProviderServiceStatus | UnavailableProviderServiceStatus;
|
|
40
|
+
export interface FetchProviderServiceStatusOptions {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
package/dist/providerStatus.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Supports both status.claude.com and status.openai.com (same API format).
|
|
5
5
|
* Polling / eventing is the caller's responsibility.
|
|
6
6
|
*/
|
|
7
|
+
/** Legacy projection. Use ProviderServiceStatus for explicit availability evidence. */
|
|
7
8
|
export interface ProviderStatusState {
|
|
8
9
|
indicator: 'none' | 'minor' | 'major' | 'critical';
|
|
9
10
|
description: string;
|
|
@@ -19,15 +20,19 @@ export interface ProviderStatusState {
|
|
|
19
20
|
} | null;
|
|
20
21
|
updatedAt: string;
|
|
21
22
|
}
|
|
23
|
+
/** Compatibility projection only; it cannot express evidence availability. */
|
|
24
|
+
export declare function toLegacyProviderStatus(status: import('./providerServiceStatusTypes').ProviderServiceStatus): ProviderStatusState;
|
|
22
25
|
/**
|
|
23
26
|
* Fetch current Claude API status from status.claude.com.
|
|
24
27
|
*
|
|
25
28
|
* Single-shot — caller wraps in polling loop, EventEmitter, or interval.
|
|
29
|
+
* Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
|
|
26
30
|
*/
|
|
27
31
|
export declare function fetchProviderStatus(): Promise<ProviderStatusState>;
|
|
28
32
|
/**
|
|
29
33
|
* Fetch current OpenAI API status from status.openai.com.
|
|
30
34
|
*
|
|
31
35
|
* Single-shot — caller wraps in polling loop, EventEmitter, or interval.
|
|
36
|
+
* Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
|
|
32
37
|
*/
|
|
33
38
|
export declare function fetchOpenAIStatus(): Promise<ProviderStatusState>;
|
package/dist/providerStatus.js
CHANGED
|
@@ -6,8 +6,38 @@
|
|
|
6
6
|
* Polling / eventing is the caller's responsibility.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.toLegacyProviderStatus = toLegacyProviderStatus;
|
|
9
10
|
exports.fetchProviderStatus = fetchProviderStatus;
|
|
10
11
|
exports.fetchOpenAIStatus = fetchOpenAIStatus;
|
|
12
|
+
/** Compatibility projection only; it cannot express evidence availability. */
|
|
13
|
+
function toLegacyProviderStatus(status) {
|
|
14
|
+
if (status.availability === 'unavailable') {
|
|
15
|
+
return {
|
|
16
|
+
indicator: 'none',
|
|
17
|
+
description: 'Status unavailable',
|
|
18
|
+
affectedComponents: [],
|
|
19
|
+
activeIncident: null,
|
|
20
|
+
updatedAt: status.checkedAt,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const incident = status.incidents?.[0];
|
|
24
|
+
return {
|
|
25
|
+
indicator: status.severity === 'maintenance' ? 'minor' : status.severity,
|
|
26
|
+
description: status.description,
|
|
27
|
+
affectedComponents: status.components
|
|
28
|
+
.filter((component) => component.status !== 'operational')
|
|
29
|
+
.map(({ name, status }) => ({ name, status })),
|
|
30
|
+
activeIncident: incident
|
|
31
|
+
? {
|
|
32
|
+
name: incident.title,
|
|
33
|
+
impact: incident.impact,
|
|
34
|
+
shortlink: incident.url ?? '',
|
|
35
|
+
updatedAt: incident.updatedAt ?? '',
|
|
36
|
+
}
|
|
37
|
+
: null,
|
|
38
|
+
updatedAt: status.providerUpdatedAt ?? '',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
11
41
|
const CLAUDE_BASE = 'https://status.claude.com';
|
|
12
42
|
const OPENAI_BASE = 'https://status.openai.com';
|
|
13
43
|
function fallbackState() {
|
|
@@ -76,6 +106,7 @@ async function fetchWithTimeout(url) {
|
|
|
76
106
|
* Fetch current Claude API status from status.claude.com.
|
|
77
107
|
*
|
|
78
108
|
* Single-shot — caller wraps in polling loop, EventEmitter, or interval.
|
|
109
|
+
* Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
|
|
79
110
|
*/
|
|
80
111
|
async function fetchProviderStatus() {
|
|
81
112
|
return fetchStatusPage(CLAUDE_BASE);
|
|
@@ -84,6 +115,7 @@ async function fetchProviderStatus() {
|
|
|
84
115
|
* Fetch current OpenAI API status from status.openai.com.
|
|
85
116
|
*
|
|
86
117
|
* Single-shot — caller wraps in polling loop, EventEmitter, or interval.
|
|
118
|
+
* Consumers requiring reliable availability semantics should use fetchProviderServiceStatus.
|
|
87
119
|
*/
|
|
88
120
|
async function fetchOpenAIStatus() {
|
|
89
121
|
return fetchStatusPage(OPENAI_BASE);
|