@goable-io/sdk 0.4.0 → 0.5.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.
- package/README.md +107 -9
- package/dist/client.d.ts +37 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +60 -11
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +34 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -2
- package/dist/errors.js.map +1 -1
- package/dist/generated/api.d.ts +459 -46
- package/dist/generated/api.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +107 -10
- package/src/errors.ts +55 -15
- package/src/generated/api.ts +459 -46
- package/src/index.ts +2 -0
- package/src/types.ts +40 -0
package/src/client.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { GoableNetworkError, toApiError } from "./errors.ts"
|
|
|
7
7
|
import type {
|
|
8
8
|
AdaptationReportRequest,
|
|
9
9
|
AdaptationReportResponse,
|
|
10
|
+
AuditExportQuery,
|
|
11
|
+
AuditExportResponse,
|
|
10
12
|
BindPolicyRequest,
|
|
11
13
|
BindPolicyResponse,
|
|
12
14
|
BriefingRequest,
|
|
@@ -24,10 +26,15 @@ import type {
|
|
|
24
26
|
EvaluatePolicyResponse,
|
|
25
27
|
ExplainRequest,
|
|
26
28
|
ExplainResponse,
|
|
29
|
+
HealthReadyResponse,
|
|
27
30
|
HealthResponse,
|
|
31
|
+
IdempotencyOptions,
|
|
32
|
+
LegalDocumentKind,
|
|
33
|
+
LegalDocumentResponse,
|
|
28
34
|
ListPoliciesQuery,
|
|
29
35
|
ListPoliciesResponse,
|
|
30
36
|
ListStationsResponse,
|
|
37
|
+
LlmKeyStatus,
|
|
31
38
|
PolicyResponse,
|
|
32
39
|
ProjectionsPortfolioRequest,
|
|
33
40
|
ProjectionsPortfolioResponse,
|
|
@@ -56,10 +63,13 @@ import type {
|
|
|
56
63
|
ScoreResponse,
|
|
57
64
|
ScoreSeriesRequest,
|
|
58
65
|
ScoreSeriesResponse,
|
|
66
|
+
SetLlmKeyRequest,
|
|
59
67
|
SettlePolicyRequest,
|
|
60
68
|
SettlePolicyResponse,
|
|
61
69
|
SubmitObservationsRequest,
|
|
62
70
|
SubmitObservationsResponse,
|
|
71
|
+
SubmitOutcomeRequest,
|
|
72
|
+
SubmitOutcomeResponse,
|
|
63
73
|
SustainabilityIndexQuery,
|
|
64
74
|
SustainabilityIndexResponse,
|
|
65
75
|
UpdateStationRequest,
|
|
@@ -119,6 +129,12 @@ export class GoableClient {
|
|
|
119
129
|
return this.request<HealthResponse>("GET", "/v1/health")
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/** Readiness probe (DB + skill lookup + LLM config). Note: a degraded/critical
|
|
133
|
+
* deployment answers `503`, which surfaces here as a {@link GoableApiError}. */
|
|
134
|
+
healthReady(): Promise<HealthReadyResponse> {
|
|
135
|
+
return this.request<HealthReadyResponse>("GET", "/v1/health/ready")
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
score(input: ScoreRequest): Promise<ScoreResponse> {
|
|
123
139
|
return this.request<ScoreResponse>("POST", "/v1/score", input)
|
|
124
140
|
}
|
|
@@ -181,15 +197,29 @@ export class GoableClient {
|
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
/** Close the calibration loop: report the observed outcome of a scored
|
|
184
|
-
* session. Requires the `outcomes:write` scope.
|
|
185
|
-
|
|
200
|
+
* session. Requires the `outcomes:write` scope. Pass `idempotencyKey` so a
|
|
201
|
+
* retry after a network timeout can't record the same outcome twice. */
|
|
202
|
+
reportOutcome(
|
|
203
|
+
sessionId: string,
|
|
204
|
+
input: ReportOutcomeRequest,
|
|
205
|
+
options?: IdempotencyOptions,
|
|
206
|
+
): Promise<ReportOutcomeResponse> {
|
|
186
207
|
return this.request<ReportOutcomeResponse>(
|
|
187
208
|
"POST",
|
|
188
209
|
`/v1/score/${encodeURIComponent(sessionId)}/outcome`,
|
|
189
210
|
input,
|
|
211
|
+
idempotencyHeader(options),
|
|
190
212
|
)
|
|
191
213
|
}
|
|
192
214
|
|
|
215
|
+
/** Report a standalone activity outcome not tied to a scored session — the
|
|
216
|
+
* operator-reported behavioural signal behind the calibration + research
|
|
217
|
+
* datasets. Responds 202. Requires the `outcomes:write` scope. For an
|
|
218
|
+
* outcome linked to a specific score, use {@link reportOutcome} instead. */
|
|
219
|
+
submitOutcome(input: SubmitOutcomeRequest): Promise<SubmitOutcomeResponse> {
|
|
220
|
+
return this.request<SubmitOutcomeResponse>("POST", "/v1/outcomes", input)
|
|
221
|
+
}
|
|
222
|
+
|
|
193
223
|
/** LLM edge-case narrative for a marginal score. */
|
|
194
224
|
edgeCase(input: EdgeCaseRequest): Promise<EdgeCaseResponse> {
|
|
195
225
|
return this.request<EdgeCaseResponse>("POST", "/v1/intelligence/edge-case", input)
|
|
@@ -217,8 +247,13 @@ export class GoableClient {
|
|
|
217
247
|
* warning/critical event refuses the bind with `422 DRIFT_ACTIVE`, thrown as
|
|
218
248
|
* a {@link DriftActiveError}.
|
|
219
249
|
*/
|
|
220
|
-
bindPolicy(input: BindPolicyRequest): Promise<BindPolicyResponse> {
|
|
221
|
-
return this.request<BindPolicyResponse>(
|
|
250
|
+
bindPolicy(input: BindPolicyRequest, options?: IdempotencyOptions): Promise<BindPolicyResponse> {
|
|
251
|
+
return this.request<BindPolicyResponse>(
|
|
252
|
+
"POST",
|
|
253
|
+
"/v1/underwriting/policy/bind",
|
|
254
|
+
input,
|
|
255
|
+
idempotencyHeader(options),
|
|
256
|
+
)
|
|
222
257
|
}
|
|
223
258
|
|
|
224
259
|
/** List the calling tenant's bound policies (paginated, boundAt DESC). */
|
|
@@ -321,6 +356,47 @@ export class GoableClient {
|
|
|
321
356
|
return this.request<CatalogStatsResponse>("GET", "/v1/public/catalog-stats")
|
|
322
357
|
}
|
|
323
358
|
|
|
359
|
+
/** Fetch the current published legal document of a kind (no auth). */
|
|
360
|
+
legalDocument(kind: LegalDocumentKind): Promise<LegalDocumentResponse> {
|
|
361
|
+
return this.request<LegalDocumentResponse>(
|
|
362
|
+
"GET",
|
|
363
|
+
`/v1/legal/${encodeURIComponent(kind)}/current`,
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── audit / compliance ────────────────────────────────────────────────────
|
|
368
|
+
/**
|
|
369
|
+
* Export the calling tenant's own score + outcome audit history for a date
|
|
370
|
+
* range. `format: "csv"` returns the raw CSV as a `string`; the default
|
|
371
|
+
* (`"json"`) returns the parsed {@link AuditExportResponse}. Offset-paginated
|
|
372
|
+
* via `limit` / `offset`.
|
|
373
|
+
*/
|
|
374
|
+
auditExport(query: AuditExportQuery & { format: "csv" }): Promise<string>
|
|
375
|
+
auditExport(query: AuditExportQuery & { format?: "json" }): Promise<AuditExportResponse>
|
|
376
|
+
auditExport(query: AuditExportQuery): Promise<AuditExportResponse | string> {
|
|
377
|
+
const path = `/v1/audit/export${toQuery(query)}`
|
|
378
|
+
if (query.format === "csv") return this.requestText("GET", path)
|
|
379
|
+
return this.request<AuditExportResponse>("GET", path)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── LLM BYOK (bring-your-own Anthropic key) ───────────────────────────────
|
|
383
|
+
/** Set/rotate the tenant's Anthropic API key. The server validates it with
|
|
384
|
+
* one cheap Anthropic call, encrypts it at rest, and never echoes it back.
|
|
385
|
+
* Resolves `void` on the 204. */
|
|
386
|
+
async setLlmKey(input: SetLlmKeyRequest): Promise<void> {
|
|
387
|
+
await this.request<void>("PUT", "/v1/tenant/llm-key", input)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Get the tenant's Anthropic key status (masked — never the key itself). */
|
|
391
|
+
getLlmKey(): Promise<LlmKeyStatus> {
|
|
392
|
+
return this.request<LlmKeyStatus>("GET", "/v1/tenant/llm-key")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Remove the tenant's Anthropic key. Resolves `void` on the 204. */
|
|
396
|
+
async deleteLlmKey(): Promise<void> {
|
|
397
|
+
await this.request<void>("DELETE", "/v1/tenant/llm-key")
|
|
398
|
+
}
|
|
399
|
+
|
|
324
400
|
/** GDPR Art. 17 erasure. Surfaces the receipt headers from a 204. */
|
|
325
401
|
async deleteUserData(pseudonym: string): Promise<DeleteUserDataResult> {
|
|
326
402
|
const res = await this.rawRequest("DELETE", `/v1/decision/user-data/${encodeURIComponent(pseudonym)}`)
|
|
@@ -342,15 +418,21 @@ export class GoableClient {
|
|
|
342
418
|
}
|
|
343
419
|
|
|
344
420
|
// ── transport ─────────────────────────────────────────────────────────────
|
|
345
|
-
private async request<T>(
|
|
346
|
-
|
|
421
|
+
private async request<T>(
|
|
422
|
+
method: string,
|
|
423
|
+
path: string,
|
|
424
|
+
body?: unknown,
|
|
425
|
+
extraHeaders?: Record<string, string>,
|
|
426
|
+
): Promise<T> {
|
|
427
|
+
const res = await this.rawRequest(method, path, body, extraHeaders)
|
|
347
428
|
const parsed = await safeJson(res)
|
|
348
|
-
if (!res.ok) throw toApiError(res.status, parsed)
|
|
429
|
+
if (!res.ok) throw toApiError(res.status, parsed, res.headers)
|
|
349
430
|
return parsed as T
|
|
350
431
|
}
|
|
351
432
|
|
|
352
433
|
/** Like {@link request} but returns the raw response body as text — used for
|
|
353
|
-
* the NDJSON research streams
|
|
434
|
+
* the NDJSON research streams and the `format=csv` audit export, which are
|
|
435
|
+
* not a single JSON document. */
|
|
354
436
|
private async requestText(method: string, path: string): Promise<string> {
|
|
355
437
|
const res = await this.rawRequest(method, path)
|
|
356
438
|
let text: string
|
|
@@ -366,12 +448,17 @@ export class GoableClient {
|
|
|
366
448
|
} catch {
|
|
367
449
|
// non-JSON error body — pass the raw text through to toApiError
|
|
368
450
|
}
|
|
369
|
-
throw toApiError(res.status, parsed)
|
|
451
|
+
throw toApiError(res.status, parsed, res.headers)
|
|
370
452
|
}
|
|
371
453
|
return text
|
|
372
454
|
}
|
|
373
455
|
|
|
374
|
-
private async rawRequest(
|
|
456
|
+
private async rawRequest(
|
|
457
|
+
method: string,
|
|
458
|
+
path: string,
|
|
459
|
+
body?: unknown,
|
|
460
|
+
extraHeaders?: Record<string, string>,
|
|
461
|
+
): ReturnType<FetchLike> {
|
|
375
462
|
// `X-Goable-Key` rather than `Authorization: Bearer` — the API
|
|
376
463
|
// sits behind CloudFront with OAC, which hijacks the standard
|
|
377
464
|
// Authorization header for its own SigV4 signature. Custom header
|
|
@@ -383,6 +470,11 @@ export class GoableClient {
|
|
|
383
470
|
Accept: "application/json",
|
|
384
471
|
}
|
|
385
472
|
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
473
|
+
if (extraHeaders) {
|
|
474
|
+
for (const [k, v] of Object.entries(extraHeaders)) {
|
|
475
|
+
if (v !== undefined) headers[k] = v
|
|
476
|
+
}
|
|
477
|
+
}
|
|
386
478
|
|
|
387
479
|
const controller = this.timeoutMs > 0 ? new AbortController() : undefined
|
|
388
480
|
const timer = controller && this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : undefined
|
|
@@ -419,6 +511,11 @@ function toQuery(params?: Record<string, unknown>): string {
|
|
|
419
511
|
return s ? `?${s}` : ""
|
|
420
512
|
}
|
|
421
513
|
|
|
514
|
+
/** Build the optional `Idempotency-Key` header bag from per-call options. */
|
|
515
|
+
function idempotencyHeader(options?: IdempotencyOptions): Record<string, string> | undefined {
|
|
516
|
+
return options?.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : undefined
|
|
517
|
+
}
|
|
518
|
+
|
|
422
519
|
async function safeJson(res: { text(): Promise<string> }): Promise<unknown> {
|
|
423
520
|
let text: string
|
|
424
521
|
try {
|
package/src/errors.ts
CHANGED
|
@@ -14,6 +14,26 @@ export interface ZodIssueLike {
|
|
|
14
14
|
[k: string]: unknown
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Rate-limit snapshot parsed from `X-RateLimit-*` response headers. Present
|
|
18
|
+
* when the server sent them (currently the `score` + `recommend-spot`
|
|
19
|
+
* endpoints; omitted on unlimited Scale plans). */
|
|
20
|
+
export interface RateLimit {
|
|
21
|
+
/** `X-RateLimit-Limit` — daily safety cap for this endpoint + plan. */
|
|
22
|
+
limit: number
|
|
23
|
+
/** `X-RateLimit-Remaining` — requests left in the current window. */
|
|
24
|
+
remaining: number
|
|
25
|
+
/** `X-RateLimit-Reset` — Unix timestamp (seconds) when the window resets. */
|
|
26
|
+
reset: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Structured extras attached to a {@link GoableApiError}. */
|
|
30
|
+
export interface ApiErrorExtra {
|
|
31
|
+
issues?: ZodIssueLike[]
|
|
32
|
+
detail?: Record<string, unknown>
|
|
33
|
+
retryAfterSeconds?: number | null
|
|
34
|
+
rateLimit?: RateLimit
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
export class GoableApiError extends Error {
|
|
18
38
|
// Typed `string` (not the literal) so subclasses like DriftActiveError can
|
|
19
39
|
// override it with their own name.
|
|
@@ -26,18 +46,21 @@ export class GoableApiError extends Error {
|
|
|
26
46
|
readonly issues?: ZodIssueLike[]
|
|
27
47
|
/** Free-form extra context (e.g. plan info, quote id). */
|
|
28
48
|
readonly detail?: Record<string, unknown>
|
|
49
|
+
/** Seconds to wait before retrying, from the `Retry-After` header. Set on a
|
|
50
|
+
* `429`; `null` when the header is absent or unparseable. Lets a caller
|
|
51
|
+
* implement a compliant back-off (`if (err.status === 429) sleep(err.retryAfterSeconds)`). */
|
|
52
|
+
readonly retryAfterSeconds: number | null
|
|
53
|
+
/** Rate-limit snapshot from `X-RateLimit-*` headers, when the response carried them. */
|
|
54
|
+
readonly rateLimit?: RateLimit
|
|
29
55
|
|
|
30
|
-
constructor(
|
|
31
|
-
status: number,
|
|
32
|
-
code: string,
|
|
33
|
-
message?: string,
|
|
34
|
-
extra?: { issues?: ZodIssueLike[]; detail?: Record<string, unknown> },
|
|
35
|
-
) {
|
|
56
|
+
constructor(status: number, code: string, message?: string, extra?: ApiErrorExtra) {
|
|
36
57
|
super(message ?? code)
|
|
37
58
|
this.status = status
|
|
38
59
|
this.code = code
|
|
39
60
|
if (extra?.issues) this.issues = extra.issues
|
|
40
61
|
if (extra?.detail) this.detail = extra.detail
|
|
62
|
+
this.retryAfterSeconds = extra?.retryAfterSeconds ?? null
|
|
63
|
+
if (extra?.rateLimit) this.rateLimit = extra.rateLimit
|
|
41
64
|
// Restore prototype chain for `instanceof` across transpilation targets.
|
|
42
65
|
Object.setPrototypeOf(this, GoableApiError.prototype)
|
|
43
66
|
}
|
|
@@ -56,12 +79,7 @@ export class DriftActiveError extends GoableApiError {
|
|
|
56
79
|
* per-cell and best-effort (documented, not part of the typed contract). */
|
|
57
80
|
readonly openDriftEvents: Array<Record<string, unknown>>
|
|
58
81
|
|
|
59
|
-
constructor(
|
|
60
|
-
status: number,
|
|
61
|
-
code: string,
|
|
62
|
-
message?: string,
|
|
63
|
-
extra?: { issues?: ZodIssueLike[]; detail?: Record<string, unknown> },
|
|
64
|
-
) {
|
|
82
|
+
constructor(status: number, code: string, message?: string, extra?: ApiErrorExtra) {
|
|
65
83
|
super(status, code, message, extra)
|
|
66
84
|
const raw = extra?.detail?.openDriftEvents
|
|
67
85
|
this.openDriftEvents = Array.isArray(raw) ? (raw as Array<Record<string, unknown>>) : []
|
|
@@ -90,15 +108,37 @@ interface RawErrorBody {
|
|
|
90
108
|
detail?: unknown
|
|
91
109
|
}
|
|
92
110
|
|
|
111
|
+
/** A minimal read-only header bag (the `headers` of a fetch Response). */
|
|
112
|
+
export interface HeaderBag {
|
|
113
|
+
get(name: string): string | null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Parse an integer header, returning null when absent or non-numeric. */
|
|
117
|
+
function intHeader(headers: HeaderBag | undefined, name: string): number | null {
|
|
118
|
+
const v = headers?.get(name)
|
|
119
|
+
if (v == null) return null
|
|
120
|
+
const n = Number(v)
|
|
121
|
+
return Number.isFinite(n) ? n : null
|
|
122
|
+
}
|
|
123
|
+
|
|
93
124
|
/** Map a parsed error body + status into a GoableApiError. Tolerant of a
|
|
94
|
-
* non-conforming body (falls back to the status code as the error code).
|
|
95
|
-
|
|
125
|
+
* non-conforming body (falls back to the status code as the error code).
|
|
126
|
+
* When `headers` are supplied, surfaces `Retry-After` (as `retryAfterSeconds`)
|
|
127
|
+
* and the `X-RateLimit-*` snapshot on the resulting error. */
|
|
128
|
+
export function toApiError(status: number, body: unknown, headers?: HeaderBag): GoableApiError {
|
|
96
129
|
const b = (body ?? {}) as RawErrorBody
|
|
97
130
|
const code = typeof b.error === "string" ? b.error : `HTTP_${status}`
|
|
98
131
|
const message = typeof b.message === "string" ? b.message : undefined
|
|
99
|
-
const extra:
|
|
132
|
+
const extra: ApiErrorExtra = {}
|
|
100
133
|
if (Array.isArray(b.issues)) extra.issues = b.issues as ZodIssueLike[]
|
|
101
134
|
if (b.detail && typeof b.detail === "object") extra.detail = b.detail as Record<string, unknown>
|
|
135
|
+
extra.retryAfterSeconds = intHeader(headers, "Retry-After")
|
|
136
|
+
const limit = intHeader(headers, "X-RateLimit-Limit")
|
|
137
|
+
const remaining = intHeader(headers, "X-RateLimit-Remaining")
|
|
138
|
+
const reset = intHeader(headers, "X-RateLimit-Reset")
|
|
139
|
+
if (limit !== null && remaining !== null && reset !== null) {
|
|
140
|
+
extra.rateLimit = { limit, remaining, reset }
|
|
141
|
+
}
|
|
102
142
|
// Specialise the one error the SDK models with its own class. Stays a
|
|
103
143
|
// GoableApiError subclass, so generic catch sites are unaffected.
|
|
104
144
|
if (code === "DRIFT_ACTIVE") return new DriftActiveError(status, code, message, extra)
|