@goable-io/sdk 0.3.0 → 0.4.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/src/client.ts CHANGED
@@ -5,22 +5,47 @@
5
5
 
6
6
  import { GoableNetworkError, toApiError } from "./errors.ts"
7
7
  import type {
8
+ AdaptationReportRequest,
9
+ AdaptationReportResponse,
10
+ BindPolicyRequest,
11
+ BindPolicyResponse,
8
12
  BriefingRequest,
9
13
  BriefingResponse,
14
+ CatalogStatsResponse,
10
15
  CounterfactualRequest,
11
16
  CounterfactualResponse,
17
+ CreateStationRequest,
18
+ CreateStationResponse,
12
19
  DecisionRequest,
13
20
  DecisionResponse,
14
21
  DeleteUserDataResult,
22
+ EdgeCaseRequest,
23
+ EdgeCaseResponse,
24
+ EvaluatePolicyResponse,
15
25
  ExplainRequest,
16
26
  ExplainResponse,
17
27
  HealthResponse,
28
+ ListPoliciesQuery,
29
+ ListPoliciesResponse,
30
+ ListStationsResponse,
31
+ PolicyResponse,
32
+ ProjectionsPortfolioRequest,
33
+ ProjectionsPortfolioResponse,
18
34
  ProjectionsRequest,
19
35
  ProjectionsResponse,
36
+ PublicSignupRequest,
37
+ PublicSignupResponse,
38
+ QuoteByIdResponse,
20
39
  QuoteRequest,
21
40
  QuoteResponse,
41
+ RecentObservationsQuery,
42
+ RecentObservationsResponse,
22
43
  RecommendSpotRequest,
23
44
  RecommendSpotResponse,
45
+ ReportOutcomeRequest,
46
+ ReportOutcomeResponse,
47
+ ScoreDifficultyRequest,
48
+ ScoreDifficultyResponse,
24
49
  ScoreHistoricalRequest,
25
50
  ScoreHistoricalResponse,
26
51
  ScoreMultiRequest,
@@ -31,6 +56,15 @@ import type {
31
56
  ScoreResponse,
32
57
  ScoreSeriesRequest,
33
58
  ScoreSeriesResponse,
59
+ SettlePolicyRequest,
60
+ SettlePolicyResponse,
61
+ SubmitObservationsRequest,
62
+ SubmitObservationsResponse,
63
+ SustainabilityIndexQuery,
64
+ SustainabilityIndexResponse,
65
+ UpdateStationRequest,
66
+ UpdateStationResponse,
67
+ VerificationExportQuery,
34
68
  } from "./types.ts"
35
69
 
36
70
  export type FetchLike = (
@@ -141,6 +175,152 @@ export class GoableClient {
141
175
  return this.request<RecommendSpotResponse>("POST", "/v1/recommend-spot", input)
142
176
  }
143
177
 
178
+ /** L15 — skill-conditioned difficulty grids per scoring dimension (Pro+). */
179
+ scoreDifficulty(input: ScoreDifficultyRequest): Promise<ScoreDifficultyResponse> {
180
+ return this.request<ScoreDifficultyResponse>("POST", "/v1/score/difficulty", input)
181
+ }
182
+
183
+ /** Close the calibration loop: report the observed outcome of a scored
184
+ * session. Requires the `outcomes:write` scope. */
185
+ reportOutcome(sessionId: string, input: ReportOutcomeRequest): Promise<ReportOutcomeResponse> {
186
+ return this.request<ReportOutcomeResponse>(
187
+ "POST",
188
+ `/v1/score/${encodeURIComponent(sessionId)}/outcome`,
189
+ input,
190
+ )
191
+ }
192
+
193
+ /** LLM edge-case narrative for a marginal score. */
194
+ edgeCase(input: EdgeCaseRequest): Promise<EdgeCaseResponse> {
195
+ return this.request<EdgeCaseResponse>("POST", "/v1/intelligence/edge-case", input)
196
+ }
197
+
198
+ /** T3 — multi-spot climate-decadal projections (Scale). */
199
+ projectionsPortfolio(input: ProjectionsPortfolioRequest): Promise<ProjectionsPortfolioResponse> {
200
+ return this.request<ProjectionsPortfolioResponse>("POST", "/v1/projections/portfolio", input)
201
+ }
202
+
203
+ /** T3 — adaptation report across months × scenarios × decades (Scale). */
204
+ adaptationReport(input: AdaptationReportRequest): Promise<AdaptationReportResponse> {
205
+ return this.request<AdaptationReportResponse>("POST", "/v1/projections/adaptation-report", input)
206
+ }
207
+
208
+ // ── underwriting policy lifecycle (Scale) ─────────────────────────────────
209
+ /** Fetch a stored quote by id. */
210
+ getQuote(id: string): Promise<QuoteByIdResponse> {
211
+ return this.request<QuoteByIdResponse>("GET", `/v1/underwriting/quote/${encodeURIComponent(id)}`)
212
+ }
213
+
214
+ /**
215
+ * Bind a recent quote into a policy. Responds 201. A watch-level drift event
216
+ * on the resolved cell surfaces as `driftAdvisories` on success; a
217
+ * warning/critical event refuses the bind with `422 DRIFT_ACTIVE`, thrown as
218
+ * a {@link DriftActiveError}.
219
+ */
220
+ bindPolicy(input: BindPolicyRequest): Promise<BindPolicyResponse> {
221
+ return this.request<BindPolicyResponse>("POST", "/v1/underwriting/policy/bind", input)
222
+ }
223
+
224
+ /** List the calling tenant's bound policies (paginated, boundAt DESC). */
225
+ listPolicies(query?: ListPoliciesQuery): Promise<ListPoliciesResponse> {
226
+ return this.request<ListPoliciesResponse>("GET", `/v1/underwriting/policy${toQuery(query)}`)
227
+ }
228
+
229
+ /** Fetch a single policy + its payout events. */
230
+ getPolicy(policyId: string): Promise<PolicyResponse> {
231
+ return this.request<PolicyResponse>("GET", `/v1/underwriting/policy/${encodeURIComponent(policyId)}`)
232
+ }
233
+
234
+ /** Re-evaluate a bound policy against the historical archive; inserts any
235
+ * newly detected payout events. No request body. */
236
+ evaluatePolicy(policyId: string): Promise<EvaluatePolicyResponse> {
237
+ return this.request<EvaluatePolicyResponse>(
238
+ "POST",
239
+ `/v1/underwriting/policy/${encodeURIComponent(policyId)}/evaluate`,
240
+ )
241
+ }
242
+
243
+ /**
244
+ * Settle a bound policy. PLATFORM-OPS ONLY — requires the `platform_admin`
245
+ * scope (a cross-tenant underwriter operation, normally run by the daily
246
+ * settlement cron). Not a policyholder self-service action; tenant
247
+ * integrations should not call this.
248
+ */
249
+ settlePolicy(policyId: string, input: SettlePolicyRequest): Promise<SettlePolicyResponse> {
250
+ return this.request<SettlePolicyResponse>(
251
+ "POST",
252
+ `/v1/underwriting/policy/${encodeURIComponent(policyId)}/settle`,
253
+ input,
254
+ )
255
+ }
256
+
257
+ // ── observations / nowcasting (L5.3) ──────────────────────────────────────
258
+ /** Register a tenant observation station. Responds 201. */
259
+ createStation(input: CreateStationRequest): Promise<CreateStationResponse> {
260
+ return this.request<CreateStationResponse>("POST", "/v1/observations/stations", input)
261
+ }
262
+
263
+ /** List the calling tenant's observation stations. */
264
+ listStations(): Promise<ListStationsResponse> {
265
+ return this.request<ListStationsResponse>("GET", "/v1/observations/stations")
266
+ }
267
+
268
+ /** Patch a station (partial update). */
269
+ updateStation(stationId: string, input: UpdateStationRequest): Promise<UpdateStationResponse> {
270
+ return this.request<UpdateStationResponse>(
271
+ "PATCH",
272
+ `/v1/observations/stations/${encodeURIComponent(stationId)}`,
273
+ input,
274
+ )
275
+ }
276
+
277
+ /** Push station observations into the 0-6h assimilation window (Pro+).
278
+ * Responds 202. */
279
+ submitObservations(input: SubmitObservationsRequest): Promise<SubmitObservationsResponse> {
280
+ return this.request<SubmitObservationsResponse>("POST", "/v1/observations", input)
281
+ }
282
+
283
+ /** Most-recent observations for one of the tenant's stations. */
284
+ recentObservations(
285
+ stationId: string,
286
+ query?: RecentObservationsQuery,
287
+ ): Promise<RecentObservationsResponse> {
288
+ return this.request<RecentObservationsResponse>(
289
+ "GET",
290
+ `/v1/observations/stations/${encodeURIComponent(stationId)}/recent${toQuery(query)}`,
291
+ )
292
+ }
293
+
294
+ // ── public / research (no-auth surfaces) ──────────────────────────────────
295
+ /** Public Goable Sustainability Index (JSON-LD, CC BY 4.0). */
296
+ sustainabilityIndex(query: SustainabilityIndexQuery): Promise<SustainabilityIndexResponse> {
297
+ return this.request<SustainabilityIndexResponse>(
298
+ "GET",
299
+ `/v1/public/sustainability-index${toQuery(query)}`,
300
+ )
301
+ }
302
+
303
+ /** Public Stream F forecast-verification export. Returns the raw NDJSON
304
+ * stream as a string (one cell per line + a trailing meta line). */
305
+ verificationExport(query?: VerificationExportQuery): Promise<string> {
306
+ return this.requestText("GET", `/v1/research/verification/export${toQuery(query)}`)
307
+ }
308
+
309
+ /** Public L15 Difficulty Atlas export. Returns the raw NDJSON stream. */
310
+ difficultyAtlasExport(): Promise<string> {
311
+ return this.requestText("GET", "/v1/research/difficulty-atlas/export.jsonl")
312
+ }
313
+
314
+ /** Self-service tenant signup (no auth). Always 202 on success. */
315
+ publicSignup(input: PublicSignupRequest): Promise<PublicSignupResponse> {
316
+ return this.request<PublicSignupResponse>("POST", "/v1/public/signup", input)
317
+ }
318
+
319
+ /** Open catalogue coverage stats (no auth). */
320
+ catalogStats(): Promise<CatalogStatsResponse> {
321
+ return this.request<CatalogStatsResponse>("GET", "/v1/public/catalog-stats")
322
+ }
323
+
144
324
  /** GDPR Art. 17 erasure. Surfaces the receipt headers from a 204. */
145
325
  async deleteUserData(pseudonym: string): Promise<DeleteUserDataResult> {
146
326
  const res = await this.rawRequest("DELETE", `/v1/decision/user-data/${encodeURIComponent(pseudonym)}`)
@@ -169,6 +349,28 @@ export class GoableClient {
169
349
  return parsed as T
170
350
  }
171
351
 
352
+ /** Like {@link request} but returns the raw response body as text — used for
353
+ * the NDJSON research streams, which are not a single JSON document. */
354
+ private async requestText(method: string, path: string): Promise<string> {
355
+ const res = await this.rawRequest(method, path)
356
+ let text: string
357
+ try {
358
+ text = await res.text()
359
+ } catch (err) {
360
+ throw new GoableNetworkError("Failed to read response body", "parse", err)
361
+ }
362
+ if (!res.ok) {
363
+ let parsed: unknown = text
364
+ try {
365
+ parsed = JSON.parse(text)
366
+ } catch {
367
+ // non-JSON error body — pass the raw text through to toApiError
368
+ }
369
+ throw toApiError(res.status, parsed)
370
+ }
371
+ return text
372
+ }
373
+
172
374
  private async rawRequest(method: string, path: string, body?: unknown): ReturnType<FetchLike> {
173
375
  // `X-Goable-Key` rather than `Authorization: Bearer` — the API
174
376
  // sits behind CloudFront with OAC, which hijacks the standard
@@ -205,6 +407,18 @@ export class GoableClient {
205
407
  }
206
408
  }
207
409
 
410
+ /** Serialise a query object into a leading-`?` string (or "" when empty).
411
+ * Skips undefined/null; isomorphic (URLSearchParams is a Node 18+/browser global). */
412
+ function toQuery(params?: Record<string, unknown>): string {
413
+ if (!params) return ""
414
+ const usp = new URLSearchParams()
415
+ for (const [k, v] of Object.entries(params)) {
416
+ if (v !== undefined && v !== null) usp.append(k, String(v))
417
+ }
418
+ const s = usp.toString()
419
+ return s ? `?${s}` : ""
420
+ }
421
+
208
422
  async function safeJson(res: { text(): Promise<string> }): Promise<unknown> {
209
423
  let text: string
210
424
  try {
package/src/errors.ts CHANGED
@@ -15,7 +15,9 @@ export interface ZodIssueLike {
15
15
  }
16
16
 
17
17
  export class GoableApiError extends Error {
18
- override readonly name = "GoableApiError"
18
+ // Typed `string` (not the literal) so subclasses like DriftActiveError can
19
+ // override it with their own name.
20
+ override readonly name: string = "GoableApiError"
19
21
  /** HTTP status code. */
20
22
  readonly status: number
21
23
  /** Machine-readable code from the `error` field (e.g. "PAYMENT_REQUIRED"). */
@@ -41,6 +43,32 @@ export class GoableApiError extends Error {
41
43
  }
42
44
  }
43
45
 
46
+ /**
47
+ * Raised on a `422 DRIFT_ACTIVE` from `POST /v1/underwriting/policy/bind`:
48
+ * the resolved cell has an open warning/critical L9 drift event, so the bind
49
+ * is refused (a watch-level event is a soft `driftAdvisories` on success, not
50
+ * an error). Subclasses `GoableApiError`, so existing `instanceof
51
+ * GoableApiError` / `.code === "DRIFT_ACTIVE"` checks keep working.
52
+ */
53
+ export class DriftActiveError extends GoableApiError {
54
+ override readonly name = "DriftActiveError"
55
+ /** The blocking cells, from the server's `detail.openDriftEvents`. Shape is
56
+ * per-cell and best-effort (documented, not part of the typed contract). */
57
+ readonly openDriftEvents: Array<Record<string, unknown>>
58
+
59
+ constructor(
60
+ status: number,
61
+ code: string,
62
+ message?: string,
63
+ extra?: { issues?: ZodIssueLike[]; detail?: Record<string, unknown> },
64
+ ) {
65
+ super(status, code, message, extra)
66
+ const raw = extra?.detail?.openDriftEvents
67
+ this.openDriftEvents = Array.isArray(raw) ? (raw as Array<Record<string, unknown>>) : []
68
+ Object.setPrototypeOf(this, DriftActiveError.prototype)
69
+ }
70
+ }
71
+
44
72
  export class GoableNetworkError extends Error {
45
73
  override readonly name = "GoableNetworkError"
46
74
  /** "timeout" when the request was aborted by the configured timeout. */
@@ -71,5 +99,8 @@ export function toApiError(status: number, body: unknown): GoableApiError {
71
99
  const extra: { issues?: ZodIssueLike[]; detail?: Record<string, unknown> } = {}
72
100
  if (Array.isArray(b.issues)) extra.issues = b.issues as ZodIssueLike[]
73
101
  if (b.detail && typeof b.detail === "object") extra.detail = b.detail as Record<string, unknown>
102
+ // Specialise the one error the SDK models with its own class. Stays a
103
+ // GoableApiError subclass, so generic catch sites are unaffected.
104
+ if (code === "DRIFT_ACTIVE") return new DriftActiveError(status, code, message, extra)
74
105
  return new GoableApiError(status, code, message, extra)
75
106
  }