@nopaque/sdk 0.7.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,39 @@ 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
+
7
40
  ## [0.7.0] - 2026-08-19
8
41
 
9
42
  ### Fixed
@@ -205,5 +238,6 @@ client.mapping.create({
205
238
  - Typed error class hierarchy.
206
239
  - Dual ESM + CJS output with TypeScript definitions.
207
240
 
208
- [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
209
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.7.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 {
@@ -1549,6 +1719,7 @@ var Nopaque = class {
1549
1719
  digitalTesting;
1550
1720
  digitalTestConfigs;
1551
1721
  digitalCompliance;
1722
+ surveys;
1552
1723
  transport;
1553
1724
  constructor(options = {}) {
1554
1725
  const config = resolveConfig(options);
@@ -1569,6 +1740,7 @@ var Nopaque = class {
1569
1740
  this.digitalTesting = new DigitalTestingResource(this.transport);
1570
1741
  this.digitalTestConfigs = new DigitalTestConfigsResource(this.transport);
1571
1742
  this.digitalCompliance = new DigitalComplianceResource(this.transport);
1743
+ this.surveys = new SurveysResource(this.transport);
1572
1744
  }
1573
1745
  close() {
1574
1746
  }