@nopaque/sdk 0.6.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.8.0] - 2026-09-30
10
+
11
+ ### Fixed
12
+
13
+ - An error body carrying `message` instead of `error` now surfaces its text.
14
+ API Gateway's own 401, 403 and 429 responses, and a few older handlers, send
15
+ `message`; the SDK read only `error`, so those errors showed as just
16
+ `HTTP 403` with no reason.
17
+ - `pnpm test:integration` allows 30 s per test. The live-API schedule
18
+ round-trip makes three calls and could pass Vitest's 5 s default on cold
19
+ Lambdas.
20
+
21
+ ### Added
22
+
23
+ - `surveys` resource for survey tests, where your platform sends the survey and
24
+ nopaque answers as the respondent.
25
+ - `surveys.start()`, `stop()` and `list()` over `/testing/survey-runs`.
26
+ `start()` takes `sender` — the number your survey platform sends from — and
27
+ sends it as `endUserE164`. `list()` returns the runs plus the workspace's
28
+ `numbers: { total, busy, free }`.
29
+ - `surveys.results.list()` / `listPage()` / `get()` over
30
+ `/testing/survey-results`. A result carries the conversation as `turns`.
31
+ - `surveys.configs.create()` / `list()` / `get()` / `update()` / `delete()`
32
+ over `/testing/survey-test-configs`. `configs.list()` is not paginated and
33
+ returns a plain array.
34
+ - `surveys.waitForResult(runId)` polls until `capture.status` is `final` or
35
+ `failed` and returns the result either way. A 404 counts as "not written
36
+ yet" for `notFoundGrace` (default 60 s). Without a `timeout`, the deadline
37
+ is the test's `expiresAt` plus 10 minutes. Throws `NopaqueTimeoutError` on
38
+ the deadline without stopping the test.
39
+
40
+ ## [0.7.0] - 2026-08-19
41
+
42
+ ### Fixed
43
+
44
+ - `waitForRun()` could return before the run's verdict was written, giving
45
+ `outcome: undefined` and no step results on a run that had actually passed.
46
+ A re-fetch of the same id moments later returned the real verdict. The
47
+ container emits `run_status_changed{status:'completed'}` and
48
+ `run_completed{outcome}` as separate SQS messages with no ordering
49
+ guarantee, and the API skips undefined fields on write, so a status-only
50
+ message marks the run terminal with no outcome. Polling keyed on `status`
51
+ alone, so it returned that intermediate row. `completed` now also requires a
52
+ decided verdict; `failed` and `cancelled` still settle immediately, since
53
+ neither carries one.
54
+
55
+ ### Added
56
+
57
+ - `TestStepResult` and `TestRunDetails`. `testing.runs.get()` and
58
+ `waitForRun()` return `TestRunDetails` — the run row plus `stepResults`,
59
+ `fullTranscript` and an inline `config` snapshot, all of which the API has
60
+ always sent and neither SDK declared.
61
+ - Named `TestStepResult`, not `StepResult`: the latter is already exported
62
+ for mapping and is an unrelated shape.
63
+
7
64
  ## [0.6.0] - 2026-08-19
8
65
 
9
66
  ### Fixed
@@ -181,5 +238,6 @@ client.mapping.create({
181
238
  - Typed error class hierarchy.
182
239
  - Dual ESM + CJS output with TypeScript definitions.
183
240
 
184
- [Unreleased]: https://github.com/nopaque/node-sdk/compare/v0.1.0...HEAD
241
+ [Unreleased]: https://github.com/nopaque/node-sdk/compare/v0.8.0...HEAD
242
+ [0.8.0]: https://github.com/nopaque/node-sdk/releases/tag/v0.8.0
185
243
  [0.1.0]: https://github.com/nopaque/node-sdk/releases/tag/v0.1.0
package/README.md CHANGED
@@ -55,6 +55,31 @@ const finished = await client.digitalTesting.waitForRun(run.id);
55
55
  console.log(finished.status, finished.outcome, finished.passRate);
56
56
  ```
57
57
 
58
+ ### Survey tests
59
+
60
+ Survey testing reverses the direction: your platform sends the survey, and
61
+ nopaque answers as the respondent. Start a test, point your survey at the
62
+ number it returns, then wait for the conversation.
63
+
64
+ ```ts
65
+ const test = await client.surveys.start({
66
+ configId: 'cfg_123',
67
+ sender: '+447700900123', // the number your survey platform sends FROM
68
+ windowSecs: 300,
69
+ });
70
+
71
+ // Trigger your survey from `sender` to this number before `expiresAt`.
72
+ console.log('Send the survey to', test.agentE164);
73
+
74
+ const result = await client.surveys.waitForResult(test.runId);
75
+ // A failed capture is a RESULT, not an error: check `capture.error`.
76
+ console.log(result.outcome, result.capture.status);
77
+ for (const turn of result.turns) console.log(`${turn.from}: ${turn.text}`);
78
+ ```
79
+
80
+ `start()` throws `ConflictError` when the workspace has no free survey number
81
+ for that sender. `surveys.list()` shows how many are free.
82
+
58
83
  ## Features
59
84
 
60
85
  - Full coverage of the Nopaque REST API via API-key auth
package/dist/index.cjs CHANGED
@@ -133,7 +133,7 @@ function resolveConfig(opts) {
133
133
  }
134
134
 
135
135
  // src/version.ts
136
- var VERSION = true ? "0.6.0" : "0.0.0-dev";
136
+ var VERSION = true ? "0.8.0" : "0.0.0-dev";
137
137
 
138
138
  // src/userAgent.ts
139
139
  function composeUserAgent() {
@@ -274,7 +274,7 @@ async function classifyResponse(response) {
274
274
  body = null;
275
275
  }
276
276
  const bodyObj = body && typeof body === "object" ? body : {};
277
- const message = typeof bodyObj.error === "string" ? bodyObj.error : `HTTP ${response.status}`;
277
+ const message = typeof bodyObj.error === "string" ? bodyObj.error : typeof bodyObj.message === "string" ? bodyObj.message : `HTTP ${response.status}`;
278
278
  const code = typeof bodyObj.code === "string" ? bodyObj.code : null;
279
279
  const details = bodyObj.details ?? null;
280
280
  const requestId = response.headers.get("x-request-id");
@@ -1317,6 +1317,176 @@ var SchedulerResource = class extends Resource {
1317
1317
  }
1318
1318
  };
1319
1319
 
1320
+ // src/resources/surveys.ts
1321
+ var TERMINAL_CAPTURE = /* @__PURE__ */ new Set(["final", "failed"]);
1322
+ var DEFAULT_NOT_FOUND_GRACE = 6e4;
1323
+ var CAPTURE_GRACE = 6e5;
1324
+ var SurveyResultsResource = class extends Resource {
1325
+ /** Paginated list of result summaries, newest first. Transcripts are omitted. */
1326
+ list(params = {}, requestOptions) {
1327
+ return new Paginator({
1328
+ fetchPage: async (p) => {
1329
+ const { nextToken, cursor, ...rest } = p;
1330
+ const raw = await this.transport.request(
1331
+ "GET",
1332
+ "/testing/survey-results",
1333
+ {
1334
+ // `nextToken` first: the Paginator advances by writing it, and a
1335
+ // caller-supplied `cursor` would otherwise pin every page to page one.
1336
+ params: { ...rest, cursor: nextToken ?? cursor },
1337
+ requestOptions
1338
+ }
1339
+ );
1340
+ return {
1341
+ items: raw.results ?? raw.items ?? [],
1342
+ nextToken: raw.nextCursor ?? raw.nextToken ?? null
1343
+ };
1344
+ },
1345
+ params: { ...params }
1346
+ });
1347
+ }
1348
+ /** One page of result summaries. */
1349
+ async listPage(params = {}, requestOptions) {
1350
+ const { nextToken, cursor, ...rest } = params;
1351
+ const raw = await this.transport.request("GET", "/testing/survey-results", {
1352
+ params: { ...rest, cursor: nextToken ?? cursor },
1353
+ requestOptions
1354
+ });
1355
+ return new Page(raw.results ?? raw.items ?? [], raw.nextCursor ?? raw.nextToken ?? null);
1356
+ }
1357
+ /**
1358
+ * One result with its conversation. While the test is live the capture may be
1359
+ * `provisional` and still grow.
1360
+ */
1361
+ async get(runId, requestOptions) {
1362
+ return await this.transport.request("GET", `/testing/survey-results/${runId}`, {
1363
+ requestOptions
1364
+ });
1365
+ }
1366
+ };
1367
+ var SurveyConfigsResource = class extends Resource {
1368
+ async create(body, requestOptions) {
1369
+ return await this.transport.request("POST", "/testing/survey-test-configs", {
1370
+ body,
1371
+ requestOptions
1372
+ });
1373
+ }
1374
+ /** Every config in the workspace. Not paginated; `mission` and `acceptance` are omitted. */
1375
+ async list(requestOptions) {
1376
+ const raw = await this.transport.request(
1377
+ "GET",
1378
+ "/testing/survey-test-configs",
1379
+ { requestOptions }
1380
+ );
1381
+ return raw.configs ?? [];
1382
+ }
1383
+ async get(configId, requestOptions) {
1384
+ return await this.transport.request("GET", `/testing/survey-test-configs/${configId}`, {
1385
+ requestOptions
1386
+ });
1387
+ }
1388
+ /** PATCH - only the fields passed are sent. `null` clears `description`, `tags` or `voiceId`. */
1389
+ async update(configId, body, requestOptions) {
1390
+ return await this.transport.request("PATCH", `/testing/survey-test-configs/${configId}`, {
1391
+ body,
1392
+ requestOptions
1393
+ });
1394
+ }
1395
+ /** Tests already started from the config are unaffected. */
1396
+ async delete(configId, requestOptions) {
1397
+ await this.transport.request("DELETE", `/testing/survey-test-configs/${configId}`, {
1398
+ requestOptions
1399
+ });
1400
+ }
1401
+ };
1402
+ var SurveysResource = class extends Resource {
1403
+ results;
1404
+ configs;
1405
+ constructor(transport) {
1406
+ super(transport);
1407
+ this.results = new SurveyResultsResource(transport);
1408
+ this.configs = new SurveyConfigsResource(transport);
1409
+ }
1410
+ /**
1411
+ * Start a survey test. Nothing runs yet: nopaque begins WAITING for a survey
1412
+ * sent from `sender` to the returned `agentE164`, until `expiresAt`.
1413
+ *
1414
+ * Throws `ConflictError` (409) when the workspace has no live survey numbers,
1415
+ * or when every one of them is already running a test against this sender -
1416
+ * the message tells the two apart. `NotFoundError` (404) for an unknown
1417
+ * config. A 503 means the number could not be put into service and nothing
1418
+ * was reserved; it is safe to try again, but this method does not.
1419
+ */
1420
+ async start(params, requestOptions) {
1421
+ const { configId, sender, windowSecs, expectedTurns, maxMessages } = params;
1422
+ const body = {
1423
+ configId,
1424
+ endUserE164: sender,
1425
+ ...windowSecs !== void 0 && { windowSecs },
1426
+ ...expectedTurns !== void 0 && { expectedTurns },
1427
+ ...maxMessages !== void 0 && { maxMessages }
1428
+ };
1429
+ return await this.transport.request("POST", "/testing/survey-runs", { body, requestOptions });
1430
+ }
1431
+ /** Stop a test before its window closes, freeing its number. */
1432
+ async stop(runId, requestOptions) {
1433
+ await this.transport.request("DELETE", `/testing/survey-runs/${runId}`, { requestOptions });
1434
+ }
1435
+ /** Every survey test holding a number, plus how many numbers are free. Not paginated. */
1436
+ async list(requestOptions) {
1437
+ return await this.transport.request("GET", "/testing/survey-runs", { requestOptions });
1438
+ }
1439
+ /**
1440
+ * Poll `results.get(runId)` until the capture is `final` or `failed`, and
1441
+ * return the result either way. A `failed` capture is a RESULT, not an error -
1442
+ * inspect `capture.error`.
1443
+ *
1444
+ * The result row is written asynchronously after `start()`, so a 404 is
1445
+ * treated as "not yet" for `notFoundGrace` and rethrown after that. Throws
1446
+ * `NopaqueTimeoutError` on the deadline; the test itself is NOT stopped.
1447
+ */
1448
+ async waitForResult(runId, opts = {}) {
1449
+ const notFoundGrace = opts.notFoundGrace ?? DEFAULT_NOT_FOUND_GRACE;
1450
+ const started = Date.now();
1451
+ let deadline = started + (opts.timeout ?? notFoundGrace);
1452
+ let deadlineDerived = opts.timeout !== void 0;
1453
+ let step = 0;
1454
+ while (true) {
1455
+ let result;
1456
+ try {
1457
+ result = await this.results.get(runId, opts.requestOptions);
1458
+ } catch (err) {
1459
+ if (!(err instanceof NotFoundError) || Date.now() - started >= notFoundGrace) throw err;
1460
+ }
1461
+ if (result) {
1462
+ if (opts.onUpdate) {
1463
+ try {
1464
+ opts.onUpdate(result);
1465
+ } catch {
1466
+ }
1467
+ }
1468
+ if (TERMINAL_CAPTURE.has(result.capture?.status)) return result;
1469
+ if (!deadlineDerived) {
1470
+ deadline = result.expiresAt * 1e3 + CAPTURE_GRACE;
1471
+ deadlineDerived = true;
1472
+ }
1473
+ }
1474
+ const now = Date.now();
1475
+ if (now >= deadline) {
1476
+ throw new NopaqueTimeoutError(
1477
+ `waitForResult timed out after ${now - started}ms waiting for survey result ${runId}`
1478
+ );
1479
+ }
1480
+ const interval = Math.min(
1481
+ pollIntervalCurve(step, { base: opts.pollInterval, cap: opts.intervalCap }),
1482
+ Math.max(0, deadline - now)
1483
+ );
1484
+ await new Promise((r) => setTimeout(r, interval));
1485
+ step++;
1486
+ }
1487
+ }
1488
+ };
1489
+
1320
1490
  // src/resources/sweeps.ts
1321
1491
  var RUN_TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
1322
1492
  var SweepsResource = class extends Resource {
@@ -1388,6 +1558,11 @@ var SweepsResource = class extends Resource {
1388
1558
 
1389
1559
  // src/resources/testing.ts
1390
1560
  var RUN_TERMINAL_STATUSES3 = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
1561
+ function isRunSettled(run) {
1562
+ if (run.status === void 0 || !RUN_TERMINAL_STATUSES3.has(run.status)) return false;
1563
+ if (run.status !== "completed") return true;
1564
+ return run.outcome != null && run.outcome !== "pending";
1565
+ }
1391
1566
  var TestingConfigsResource = class extends Resource {
1392
1567
  async create(body, requestOptions) {
1393
1568
  return await this.transport.request("POST", "/testing/configs", { body, requestOptions });
@@ -1488,7 +1663,7 @@ var TestingRunsResource = class extends Resource {
1488
1663
  async waitForRun(runId, opts = {}) {
1489
1664
  return await waitFor({
1490
1665
  fetch: () => this.get(runId, opts.requestOptions),
1491
- isTerminal: (run) => run.status !== void 0 && RUN_TERMINAL_STATUSES3.has(run.status),
1666
+ isTerminal: isRunSettled,
1492
1667
  timeout: opts.timeout,
1493
1668
  initialInterval: opts.pollInterval,
1494
1669
  intervalCap: opts.intervalCap,
@@ -1544,6 +1719,7 @@ var Nopaque = class {
1544
1719
  digitalTesting;
1545
1720
  digitalTestConfigs;
1546
1721
  digitalCompliance;
1722
+ surveys;
1547
1723
  transport;
1548
1724
  constructor(options = {}) {
1549
1725
  const config = resolveConfig(options);
@@ -1564,6 +1740,7 @@ var Nopaque = class {
1564
1740
  this.digitalTesting = new DigitalTestingResource(this.transport);
1565
1741
  this.digitalTestConfigs = new DigitalTestConfigsResource(this.transport);
1566
1742
  this.digitalCompliance = new DigitalComplianceResource(this.transport);
1743
+ this.surveys = new SurveysResource(this.transport);
1567
1744
  }
1568
1745
  close() {
1569
1746
  }