@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 +13 -0
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +82 -4
- package/dist/cjs/server/resources/calls.js +12 -0
- package/dist/cjs/server/resources/data.js +35 -4
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +81 -4
- package/dist/esm/server/resources/calls.js +12 -0
- package/dist/esm/server/resources/data.js +35 -4
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/handler.d.ts +13 -0
- package/dist/types/server/resources/calls.d.ts +9 -1
- package/dist/types/server/resources/data.d.ts +27 -1
- package/dist/types/types.d.ts +47 -0
- package/package.json +1 -1
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
|
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
345
|
-
* key/value store, aggregations, and atomic commits.
|
|
346
|
-
* `/data-scan/tickets` cannot slip through on a
|
|
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()`
|
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
340
|
-
* key/value store, aggregations, and atomic commits.
|
|
341
|
-
* `/data-scan/tickets` cannot slip through on a
|
|
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()`
|
|
@@ -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
|
*
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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.
|
|
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",
|