@cosmoops/connect-sdk 0.48.0 → 0.49.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
@@ -3,6 +3,19 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.49.0
7
+
8
+ ### Calling readiness
9
+
10
+ `connect.calls.getCallingReadiness({ asUser })` answers whether the
11
+ workspace may legally make each kind of call - `runs` covers service and
12
+ sales (`promotional`) calls, dialled by a rep (`preview`) or by software
13
+ (`automatic`), each with the first `reason` in the way - plus the steps left
14
+ on Connect's calling compliance journey and a `setupUrl` to finish them.
15
+ Member-only, on the `calls:place` scope. Read `runs` rather than deriving
16
+ anything from `steps`: Connect evaluates them with the gates its dialer
17
+ refuses on. Additive; nothing else changed.
18
+
6
19
  ## 0.48.0
7
20
 
8
21
  ### Calls declare a purpose
@@ -183,4 +183,4 @@ class ConnectClient {
183
183
  }
184
184
  exports.ConnectClient = ConnectClient;
185
185
  /** Kept in step with `package.json` by `sdk.test.ts`. */
186
- exports.SDK_VERSION = "0.48.0";
186
+ exports.SDK_VERSION = "0.49.0";
@@ -1,10 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_CACHE_TTL_MS = void 0;
3
4
  exports.createConnectHandler = createConnectHandler;
4
5
  const errors_js_1 = require("../errors.js");
5
6
  const identity_js_1 = require("./resources/identity.js");
6
7
  /** The purposes `/calls/start` accepts - see `ConnectCallPurpose`. */
7
8
  const CALL_PURPOSES = ["service", "transactional", "promotional"];
9
+ /**
10
+ * The bridge's default session reuse window - the longest a revoked session,
11
+ * a removed member or a sign-out on another device keeps working through a
12
+ * warm instance. Seconds on purpose: this gates a person's access.
13
+ */
14
+ exports.SESSION_CACHE_TTL_MS = 15_000;
15
+ /** Sessions held per handler. Least recently used falls out first. */
16
+ const SESSION_CACHE_MAX = 5_000;
8
17
  /**
9
18
  * A sentinel origin to resolve a candidate destination against. Never reached -
10
19
  * `.invalid` is reserved by RFC 2606 precisely so it cannot resolve.
@@ -310,9 +319,71 @@ function createConnectHandler(options) {
310
319
  }
311
320
  return token;
312
321
  }
322
+ /**
323
+ * Resolved sessions, per session token, for `SESSION_CACHE_TTL_MS`.
324
+ *
325
+ * Every tenant-scoped route starts by asking Connect who the caller is, and
326
+ * a page load fires several of them at once - so uncached, the bridge made
327
+ * one `/sso/user` round trip per browser request, and a partner's Connect
328
+ * traffic grew with their users times their routes. A resolved session is
329
+ * kept per token for a few seconds instead, the same trade
330
+ * `crmSession` makes in the reference CRM:
331
+ *
332
+ * - **Staleness is bounded and small.** A revoked session, a removed member
333
+ * or a reduced role keeps working here for at most the TTL.
334
+ * - **Signing out is still immediate.** The cache is keyed by the token the
335
+ * partner's session store hands back; `/logout` drops the entry, and once
336
+ * the cookie is gone there is no token to look up.
337
+ * - **`GET /session` always reads through.** It is what the browser re-reads
338
+ * after a hand-off (a plan bought in a popup, a profile edit), so it must
339
+ * see the change at once; it refreshes the entry for everyone after it.
340
+ *
341
+ * The promise is cached rather than the value, so a burst of parallel
342
+ * requests on a cold token makes one round trip, and a failed lookup is
343
+ * dropped at once - a refusal is never remembered.
344
+ *
345
+ * Per handler and in process, timed on `performance.now()` (a monotonic
346
+ * clock cannot jump when the system clock is corrected).
347
+ */
348
+ const sessionCacheTtlMs = Math.max(0, options.sessionCacheTtlMs ?? exports.SESSION_CACHE_TTL_MS);
349
+ const sessionCache = new Map();
350
+ function rememberSession(token, session) {
351
+ if (sessionCacheTtlMs === 0)
352
+ return session;
353
+ sessionCache.delete(token);
354
+ while (sessionCache.size >= SESSION_CACHE_MAX) {
355
+ const oldest = sessionCache.keys().next();
356
+ if (oldest.done)
357
+ break;
358
+ sessionCache.delete(oldest.value);
359
+ }
360
+ const entry = { session, until: performance.now() + sessionCacheTtlMs };
361
+ sessionCache.set(token, entry);
362
+ session.catch(() => {
363
+ // Only the entry this lookup created - a newer one for the same token
364
+ // must survive an older lookup failing late.
365
+ if (sessionCache.get(token) === entry)
366
+ sessionCache.delete(token);
367
+ });
368
+ return session;
369
+ }
370
+ /** A fresh read from Connect, which then answers the cached routes too. */
371
+ function resolveSessionFresh(token) {
372
+ return rememberSession(token, client.identity.getUser(token));
373
+ }
374
+ function resolveSession(token) {
375
+ const hit = sessionCache.get(token);
376
+ if (hit && hit.until > performance.now()) {
377
+ // Re-inserted so the map's order tracks recency.
378
+ sessionCache.delete(token);
379
+ sessionCache.set(token, hit);
380
+ return hit.session;
381
+ }
382
+ return resolveSessionFresh(token);
383
+ }
313
384
  /** Signed-in identity plus the workspace it belongs to - the first read every other route needs. */
314
385
  async function whoami(request) {
315
- return client.identity.getUser(await requireToken(request));
386
+ return resolveSession(await requireToken(request));
316
387
  }
317
388
  /**
318
389
  * The caller's identity, plus the workspace every tenant-scoped route below
@@ -346,7 +417,8 @@ function createConnectHandler(options) {
346
417
  /**
347
418
  * The store paths a browser may reach through the bridge.
348
419
  *
349
- * Documents, the key/value store, aggregations and atomic commits.
420
+ * Documents (one at a time or a batch by id), the key/value store,
421
+ * aggregations and atomic commits.
350
422
  * **`/data-scan/` is deliberately absent**: a scan crosses every tenant, and
351
423
  * Connect refuses one made on behalf of a user - but a browser should not be
352
424
  * able to *ask*, so the refusal happens here rather than a round trip away.
@@ -356,7 +428,8 @@ function createConnectHandler(options) {
356
428
  return (route.startsWith("/data/") ||
357
429
  route.startsWith("/kv/") ||
358
430
  route.startsWith("/data-aggregate/") ||
359
- route === "/data-commit");
431
+ route === "/data-commit" ||
432
+ route === "/data-get-many");
360
433
  }
361
434
  /**
362
435
  * Forwards one store call to Connect on behalf of the caller.
@@ -439,7 +512,7 @@ function createConnectHandler(options) {
439
512
  clientId,
440
513
  });
441
514
  }
442
- return json({ ...(await client.identity.getUser(token)), oneTap: oneTapEndpoints, clientId });
515
+ return json({ ...(await resolveSessionFresh(token)), oneTap: oneTapEndpoints, clientId });
443
516
  },
444
517
  "GET /entitlements": async (request) => {
445
518
  const { workspaceId } = await requireWorkspace(request);
@@ -850,6 +923,9 @@ function createConnectHandler(options) {
850
923
  });
851
924
  }
852
925
  const result = await client.workspaces.switch({ workspaceId: body.workspaceId }, { asUser: token });
926
+ // The old token is retired by the switch; its cached session names the
927
+ // workspace just left.
928
+ sessionCache.delete(token);
853
929
  // The response is built first so `session.write` has something to set
854
930
  // cookies on, matching how `/callback` does it.
855
931
  const response = json({
@@ -1502,6 +1578,8 @@ function createConnectHandler(options) {
1502
1578
  },
1503
1579
  "POST /logout": async (request) => {
1504
1580
  const token = await session.read(request);
1581
+ if (token)
1582
+ sessionCache.delete(token);
1505
1583
  const response = json({ ok: true });
1506
1584
  await session.clear?.(request, response);
1507
1585
  // Connect is told last, and a failure is swallowed: the user's own
@@ -104,6 +104,18 @@ class CallsResource {
104
104
  headers: (0, acting_as_js_1.actingAs)(input.asUser),
105
105
  });
106
106
  }
107
+ /**
108
+ * Whether the workspace may legally make each kind of call - service or
109
+ * sales, dialled by a rep or by software - and the steps left on Connect's
110
+ * calling compliance journey. Send the member to `setupUrl` to finish them.
111
+ */
112
+ getCallingReadiness(input) {
113
+ return (0, http_js_1.request)(this.transport, {
114
+ method: "GET",
115
+ path: "/calling/readiness",
116
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
117
+ });
118
+ }
107
119
  /** A carrier token the member's browser signs in to the phone network with. */
108
120
  getBrowserCallingToken(input) {
109
121
  return (0, http_js_1.request)(this.transport, {
@@ -60,6 +60,36 @@ class DataResource {
60
60
  headers: (0, acting_as_js_1.actingAs)(input.asUser),
61
61
  });
62
62
  }
63
+ /**
64
+ * Reads up to 200 documents of one collection by id, in **one request** -
65
+ * the batch form of `get`.
66
+ *
67
+ * Reach for it wherever you would loop over `get`: the contacts on a page of
68
+ * deals, the plans behind a list of subscriptions. It is gated exactly like
69
+ * `get` and spends one request of your rate limit however many ids it names.
70
+ *
71
+ * ```ts
72
+ * const { items } = await connect.data.getMany({
73
+ * workspaceId,
74
+ * collection: "contacts",
75
+ * ids: deals.map((deal) => deal.data.contactId as string),
76
+ * });
77
+ * // items[i] belongs to ids[i]; null where that contact no longer exists.
78
+ * ```
79
+ *
80
+ * Answers positionally, `null` where a document does not exist - unlike
81
+ * `get`, a missing document does not throw.
82
+ */
83
+ getMany(input) {
84
+ return (0, http_js_1.request)(this.transport, {
85
+ method: "POST",
86
+ path: "/data-get-many",
87
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
88
+ // A POST that writes nothing: two hundred ids do not fit in a URL.
89
+ idempotent: true,
90
+ json: { workspaceId: input.workspaceId, collection: input.collection, ids: input.ids },
91
+ });
92
+ }
63
93
  /**
64
94
  * Lists a collection, one page at a time.
65
95
  *
@@ -341,11 +371,12 @@ function isConflict(error) {
341
371
  * a round trip away.
342
372
  */
343
373
  /**
344
- * The store paths a browser may reach through the bridge: documents, the
345
- * key/value store, aggregations, and atomic commits. Anchored, so a path like
346
- * `/data-scan/tickets` cannot slip through on a prefix match.
374
+ * The store paths a browser may reach through the bridge: documents (singly or
375
+ * a batch by id), the key/value store, aggregations, and atomic commits.
376
+ * Anchored, so a path like `/data-scan/tickets` cannot slip through on a
377
+ * prefix match.
347
378
  */
348
- const STORE_PATHS = /^\/(?:data|kv|data-aggregate)\/|^\/data-commit$/;
379
+ const STORE_PATHS = /^\/(?:data|kv|data-aggregate)\/|^\/data-commit$|^\/data-get-many$/;
349
380
  // `async` so the refusal below arrives as a rejected promise rather than a
350
381
  // synchronous throw. A function typed `Promise<Response>` that sometimes
351
382
  // throws before returning one is the kind of thing a caller's `.catch()`
@@ -179,4 +179,4 @@ export class ConnectClient {
179
179
  }
180
180
  }
181
181
  /** Kept in step with `package.json` by `sdk.test.ts`. */
182
- export const SDK_VERSION = "0.48.0";
182
+ export const SDK_VERSION = "0.49.0";
@@ -2,6 +2,14 @@ import { ConnectError } from "../errors.js";
2
2
  import { createPkcePair, createState } from "./resources/identity.js";
3
3
  /** The purposes `/calls/start` accepts - see `ConnectCallPurpose`. */
4
4
  const CALL_PURPOSES = ["service", "transactional", "promotional"];
5
+ /**
6
+ * The bridge's default session reuse window - the longest a revoked session,
7
+ * a removed member or a sign-out on another device keeps working through a
8
+ * warm instance. Seconds on purpose: this gates a person's access.
9
+ */
10
+ export const SESSION_CACHE_TTL_MS = 15_000;
11
+ /** Sessions held per handler. Least recently used falls out first. */
12
+ const SESSION_CACHE_MAX = 5_000;
5
13
  /**
6
14
  * A sentinel origin to resolve a candidate destination against. Never reached -
7
15
  * `.invalid` is reserved by RFC 2606 precisely so it cannot resolve.
@@ -307,9 +315,71 @@ export function createConnectHandler(options) {
307
315
  }
308
316
  return token;
309
317
  }
318
+ /**
319
+ * Resolved sessions, per session token, for `SESSION_CACHE_TTL_MS`.
320
+ *
321
+ * Every tenant-scoped route starts by asking Connect who the caller is, and
322
+ * a page load fires several of them at once - so uncached, the bridge made
323
+ * one `/sso/user` round trip per browser request, and a partner's Connect
324
+ * traffic grew with their users times their routes. A resolved session is
325
+ * kept per token for a few seconds instead, the same trade
326
+ * `crmSession` makes in the reference CRM:
327
+ *
328
+ * - **Staleness is bounded and small.** A revoked session, a removed member
329
+ * or a reduced role keeps working here for at most the TTL.
330
+ * - **Signing out is still immediate.** The cache is keyed by the token the
331
+ * partner's session store hands back; `/logout` drops the entry, and once
332
+ * the cookie is gone there is no token to look up.
333
+ * - **`GET /session` always reads through.** It is what the browser re-reads
334
+ * after a hand-off (a plan bought in a popup, a profile edit), so it must
335
+ * see the change at once; it refreshes the entry for everyone after it.
336
+ *
337
+ * The promise is cached rather than the value, so a burst of parallel
338
+ * requests on a cold token makes one round trip, and a failed lookup is
339
+ * dropped at once - a refusal is never remembered.
340
+ *
341
+ * Per handler and in process, timed on `performance.now()` (a monotonic
342
+ * clock cannot jump when the system clock is corrected).
343
+ */
344
+ const sessionCacheTtlMs = Math.max(0, options.sessionCacheTtlMs ?? SESSION_CACHE_TTL_MS);
345
+ const sessionCache = new Map();
346
+ function rememberSession(token, session) {
347
+ if (sessionCacheTtlMs === 0)
348
+ return session;
349
+ sessionCache.delete(token);
350
+ while (sessionCache.size >= SESSION_CACHE_MAX) {
351
+ const oldest = sessionCache.keys().next();
352
+ if (oldest.done)
353
+ break;
354
+ sessionCache.delete(oldest.value);
355
+ }
356
+ const entry = { session, until: performance.now() + sessionCacheTtlMs };
357
+ sessionCache.set(token, entry);
358
+ session.catch(() => {
359
+ // Only the entry this lookup created - a newer one for the same token
360
+ // must survive an older lookup failing late.
361
+ if (sessionCache.get(token) === entry)
362
+ sessionCache.delete(token);
363
+ });
364
+ return session;
365
+ }
366
+ /** A fresh read from Connect, which then answers the cached routes too. */
367
+ function resolveSessionFresh(token) {
368
+ return rememberSession(token, client.identity.getUser(token));
369
+ }
370
+ function resolveSession(token) {
371
+ const hit = sessionCache.get(token);
372
+ if (hit && hit.until > performance.now()) {
373
+ // Re-inserted so the map's order tracks recency.
374
+ sessionCache.delete(token);
375
+ sessionCache.set(token, hit);
376
+ return hit.session;
377
+ }
378
+ return resolveSessionFresh(token);
379
+ }
310
380
  /** Signed-in identity plus the workspace it belongs to - the first read every other route needs. */
311
381
  async function whoami(request) {
312
- return client.identity.getUser(await requireToken(request));
382
+ return resolveSession(await requireToken(request));
313
383
  }
314
384
  /**
315
385
  * The caller's identity, plus the workspace every tenant-scoped route below
@@ -343,7 +413,8 @@ export function createConnectHandler(options) {
343
413
  /**
344
414
  * The store paths a browser may reach through the bridge.
345
415
  *
346
- * Documents, the key/value store, aggregations and atomic commits.
416
+ * Documents (one at a time or a batch by id), the key/value store,
417
+ * aggregations and atomic commits.
347
418
  * **`/data-scan/` is deliberately absent**: a scan crosses every tenant, and
348
419
  * Connect refuses one made on behalf of a user - but a browser should not be
349
420
  * able to *ask*, so the refusal happens here rather than a round trip away.
@@ -353,7 +424,8 @@ export function createConnectHandler(options) {
353
424
  return (route.startsWith("/data/") ||
354
425
  route.startsWith("/kv/") ||
355
426
  route.startsWith("/data-aggregate/") ||
356
- route === "/data-commit");
427
+ route === "/data-commit" ||
428
+ route === "/data-get-many");
357
429
  }
358
430
  /**
359
431
  * Forwards one store call to Connect on behalf of the caller.
@@ -436,7 +508,7 @@ export function createConnectHandler(options) {
436
508
  clientId,
437
509
  });
438
510
  }
439
- return json({ ...(await client.identity.getUser(token)), oneTap: oneTapEndpoints, clientId });
511
+ return json({ ...(await resolveSessionFresh(token)), oneTap: oneTapEndpoints, clientId });
440
512
  },
441
513
  "GET /entitlements": async (request) => {
442
514
  const { workspaceId } = await requireWorkspace(request);
@@ -847,6 +919,9 @@ export function createConnectHandler(options) {
847
919
  });
848
920
  }
849
921
  const result = await client.workspaces.switch({ workspaceId: body.workspaceId }, { asUser: token });
922
+ // The old token is retired by the switch; its cached session names the
923
+ // workspace just left.
924
+ sessionCache.delete(token);
850
925
  // The response is built first so `session.write` has something to set
851
926
  // cookies on, matching how `/callback` does it.
852
927
  const response = json({
@@ -1499,6 +1574,8 @@ export function createConnectHandler(options) {
1499
1574
  },
1500
1575
  "POST /logout": async (request) => {
1501
1576
  const token = await session.read(request);
1577
+ if (token)
1578
+ sessionCache.delete(token);
1502
1579
  const response = json({ ok: true });
1503
1580
  await session.clear?.(request, response);
1504
1581
  // Connect is told last, and a failure is swallowed: the user's own
@@ -101,6 +101,18 @@ export class CallsResource {
101
101
  headers: actingAs(input.asUser),
102
102
  });
103
103
  }
104
+ /**
105
+ * Whether the workspace may legally make each kind of call - service or
106
+ * sales, dialled by a rep or by software - and the steps left on Connect's
107
+ * calling compliance journey. Send the member to `setupUrl` to finish them.
108
+ */
109
+ getCallingReadiness(input) {
110
+ return request(this.transport, {
111
+ method: "GET",
112
+ path: "/calling/readiness",
113
+ headers: actingAs(input.asUser),
114
+ });
115
+ }
104
116
  /** A carrier token the member's browser signs in to the phone network with. */
105
117
  getBrowserCallingToken(input) {
106
118
  return request(this.transport, {
@@ -56,6 +56,36 @@ export class DataResource {
56
56
  headers: actingAs(input.asUser),
57
57
  });
58
58
  }
59
+ /**
60
+ * Reads up to 200 documents of one collection by id, in **one request** -
61
+ * the batch form of `get`.
62
+ *
63
+ * Reach for it wherever you would loop over `get`: the contacts on a page of
64
+ * deals, the plans behind a list of subscriptions. It is gated exactly like
65
+ * `get` and spends one request of your rate limit however many ids it names.
66
+ *
67
+ * ```ts
68
+ * const { items } = await connect.data.getMany({
69
+ * workspaceId,
70
+ * collection: "contacts",
71
+ * ids: deals.map((deal) => deal.data.contactId as string),
72
+ * });
73
+ * // items[i] belongs to ids[i]; null where that contact no longer exists.
74
+ * ```
75
+ *
76
+ * Answers positionally, `null` where a document does not exist - unlike
77
+ * `get`, a missing document does not throw.
78
+ */
79
+ getMany(input) {
80
+ return request(this.transport, {
81
+ method: "POST",
82
+ path: "/data-get-many",
83
+ headers: actingAs(input.asUser),
84
+ // A POST that writes nothing: two hundred ids do not fit in a URL.
85
+ idempotent: true,
86
+ json: { workspaceId: input.workspaceId, collection: input.collection, ids: input.ids },
87
+ });
88
+ }
59
89
  /**
60
90
  * Lists a collection, one page at a time.
61
91
  *
@@ -336,11 +366,12 @@ function isConflict(error) {
336
366
  * a round trip away.
337
367
  */
338
368
  /**
339
- * The store paths a browser may reach through the bridge: documents, the
340
- * key/value store, aggregations, and atomic commits. Anchored, so a path like
341
- * `/data-scan/tickets` cannot slip through on a prefix match.
369
+ * The store paths a browser may reach through the bridge: documents (singly or
370
+ * a batch by id), the key/value store, aggregations, and atomic commits.
371
+ * Anchored, so a path like `/data-scan/tickets` cannot slip through on a
372
+ * prefix match.
342
373
  */
343
- const STORE_PATHS = /^\/(?:data|kv|data-aggregate)\/|^\/data-commit$/;
374
+ const STORE_PATHS = /^\/(?:data|kv|data-aggregate)\/|^\/data-commit$|^\/data-get-many$/;
344
375
  // `async` so the refusal below arrives as a rejected promise rather than a
345
376
  // synchronous throw. A function typed `Promise<Response>` that sometimes
346
377
  // throws before returning one is the kind of thing a caller's `.catch()`
@@ -141,4 +141,4 @@ export declare class ConnectClient {
141
141
  constructor(options: ConnectClientOptions);
142
142
  }
143
143
  /** Kept in step with `package.json` by `sdk.test.ts`. */
144
- export declare const SDK_VERSION = "0.48.0";
144
+ export declare const SDK_VERSION = "0.49.0";
@@ -39,5 +39,18 @@ export interface ConnectHandlerOptions {
39
39
  * if it collides with something you already set.
40
40
  */
41
41
  stateCookieName?: string;
42
+ /**
43
+ * How long, in milliseconds, this handler reuses a resolved session for the
44
+ * routes that only need to know *whose* request it is. Defaults to
45
+ * `SESSION_CACHE_TTL_MS` (15 seconds); `0` turns the cache off. See
46
+ * `createConnectHandler` for exactly what it trades.
47
+ */
48
+ sessionCacheTtlMs?: number;
42
49
  }
50
+ /**
51
+ * The bridge's default session reuse window - the longest a revoked session,
52
+ * a removed member or a sign-out on another device keeps working through a
53
+ * warm instance. Seconds on purpose: this gates a person's access.
54
+ */
55
+ export declare const SESSION_CACHE_TTL_MS = 15000;
43
56
  export declare function createConnectHandler(options: ConnectHandlerOptions): (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { BrowserCallingToken, CallingAvailability, CallSettings, CallDetail, CallsPage, ConnectCallPurpose, StartedBrowserCall } from "../../types.js";
2
+ import type { BrowserCallingToken, CallingAvailability, CallSettings, CallingReadiness, CallDetail, CallsPage, ConnectCallPurpose, StartedBrowserCall } from "../../types.js";
3
3
  export interface ListCallsInput {
4
4
  /** Narrow to one channel. A channel belonging to another application simply matches nothing. */
5
5
  channelId?: string;
@@ -75,6 +75,14 @@ export declare class CallsResource {
75
75
  recordingEnabled: boolean;
76
76
  asUser: string;
77
77
  }): Promise<CallSettings>;
78
+ /**
79
+ * Whether the workspace may legally make each kind of call - service or
80
+ * sales, dialled by a rep or by software - and the steps left on Connect's
81
+ * calling compliance journey. Send the member to `setupUrl` to finish them.
82
+ */
83
+ getCallingReadiness(input: {
84
+ asUser: string;
85
+ }): Promise<CallingReadiness>;
78
86
  /** A carrier token the member's browser signs in to the phone network with. */
79
87
  getBrowserCallingToken(input: {
80
88
  asUser: string;
@@ -1,5 +1,5 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { StoreAggregatePage, StoreAggregation, StoreCommitOperation, StoreCommitResult, StoreDocument, StoreQueryFilter, StoreQueryPage, StoreOrderBy, StoreScanPage } from "../../types.js";
2
+ import type { StoreAggregatePage, StoreAggregation, StoreCommitOperation, StoreCommitResult, StoreDocument, StoreGetManyResult, StoreQueryFilter, StoreQueryPage, StoreOrderBy, StoreScanPage } from "../../types.js";
3
3
  /**
4
4
  * Your application's own document store - a dedicated Firestore database that
5
5
  * only Connect can reach.
@@ -51,6 +51,32 @@ export declare class DataResource {
51
51
  id: string;
52
52
  asUser?: string;
53
53
  }): Promise<StoreDocument>;
54
+ /**
55
+ * Reads up to 200 documents of one collection by id, in **one request** -
56
+ * the batch form of `get`.
57
+ *
58
+ * Reach for it wherever you would loop over `get`: the contacts on a page of
59
+ * deals, the plans behind a list of subscriptions. It is gated exactly like
60
+ * `get` and spends one request of your rate limit however many ids it names.
61
+ *
62
+ * ```ts
63
+ * const { items } = await connect.data.getMany({
64
+ * workspaceId,
65
+ * collection: "contacts",
66
+ * ids: deals.map((deal) => deal.data.contactId as string),
67
+ * });
68
+ * // items[i] belongs to ids[i]; null where that contact no longer exists.
69
+ * ```
70
+ *
71
+ * Answers positionally, `null` where a document does not exist - unlike
72
+ * `get`, a missing document does not throw.
73
+ */
74
+ getMany(input: {
75
+ workspaceId?: string;
76
+ collection: string;
77
+ ids: string[];
78
+ asUser?: string;
79
+ }): Promise<StoreGetManyResult>;
54
80
  /**
55
81
  * Lists a collection, one page at a time.
56
82
  *
@@ -1786,6 +1786,14 @@ export interface StoreCommitResult {
1786
1786
  /** The document id each operation wrote, positionally aligned with the operations sent. */
1787
1787
  ids: string[];
1788
1788
  }
1789
+ /** What `data.getMany` answers - see `POST /data-get-many`. */
1790
+ export interface StoreGetManyResult {
1791
+ /**
1792
+ * Positionally aligned with the ids asked for, `null` where a document does
1793
+ * not exist. A missing document is an ordinary answer here, not a 404.
1794
+ */
1795
+ items: Array<StoreDocument | null>;
1796
+ }
1789
1797
  /** One entry from the `kv` store. */
1790
1798
  export interface KvEntry {
1791
1799
  key: string;
@@ -2951,6 +2959,45 @@ export interface CallSettings {
2951
2959
  /** Whether this member may change it - their `channel:manage`. Draw the switch disabled when not. */
2952
2960
  canManage: boolean;
2953
2961
  }
2962
+ /** A step of Connect's calling compliance journey - `connect.calls.getCallingReadiness()`. */
2963
+ export interface CallingReadinessStep {
2964
+ key: "intent" | "business" | "number" | "header" | "autodialer" | "consent" | "rules";
2965
+ label: string;
2966
+ status: "not_started" | "submitted" | "returned" | "done";
2967
+ /** Whether the business needs it for the calls it said it makes. */
2968
+ required: boolean;
2969
+ /** A returned step's reason, or what the numbers say. */
2970
+ note: string | null;
2971
+ }
2972
+ /** One kind of calling, and whether the workspace may run it today. */
2973
+ export interface CallingReadinessRun {
2974
+ /** `service` covers existing customers; `promotional` is any sales call. */
2975
+ purpose: "service" | "promotional";
2976
+ /** `preview`: a rep presses dial. `automatic`: software paces the dialing. */
2977
+ mode: "preview" | "automatic";
2978
+ allowed: boolean;
2979
+ /** The first thing in the way, worded for the member. `null` when allowed. */
2980
+ reason: string | null;
2981
+ }
2982
+ /**
2983
+ * Where the workspace stands on the steps Indian telecom rules ask of a
2984
+ * business before it calls - `connect.calls.getCallingReadiness()`. Read
2985
+ * `runs`, not `steps`, to decide what to offer: Connect evaluates them with
2986
+ * the gates the dialer refuses on.
2987
+ */
2988
+ export interface CallingReadiness {
2989
+ intent: "service_only" | "service_and_sales" | null;
2990
+ progress: {
2991
+ done: number;
2992
+ required: number;
2993
+ };
2994
+ steps: CallingReadinessStep[];
2995
+ runs: CallingReadinessRun[];
2996
+ /** Connect's journey screen, carrying your `client_id` when SSO is on. */
2997
+ setupUrl: string;
2998
+ /** Whether this member may take the steps - their `channel:manage`. */
2999
+ canManage: boolean;
3000
+ }
2954
3001
  export interface CallingAvailability {
2955
3002
  status: CallingAvailabilityStatus;
2956
3003
  /** The workspace's business number, E.164, when it holds one. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",