@cosmoops/connect-sdk 0.47.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 +31 -0
- package/dist/cjs/client/components/call-button.js +3 -2
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +88 -5
- package/dist/cjs/server/resources/calls.js +15 -2
- package/dist/cjs/server/resources/data.js +35 -4
- package/dist/esm/client/components/call-button.js +3 -2
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +87 -5
- package/dist/esm/server/resources/calls.js +15 -2
- package/dist/esm/server/resources/data.js +35 -4
- package/dist/types/client/components/call-button.d.ts +7 -2
- 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 +12 -2
- package/dist/types/server/resources/data.d.ts +27 -1
- package/dist/types/types.d.ts +55 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.48.0
|
|
20
|
+
|
|
21
|
+
### Calls declare a purpose
|
|
22
|
+
|
|
23
|
+
`<ConnectCallButton purpose="promotional">`, `startCall({ phone, purpose })`
|
|
24
|
+
and `connect.calls.startBrowserCall({ phone, asUser, purpose })` take an
|
|
25
|
+
optional `ConnectCallPurpose` - `"service"`, `"transactional"` or
|
|
26
|
+
`"promotional"`. Connect records it on the call and places the call from a
|
|
27
|
+
workspace number allowed to carry it: in India a regular number carries
|
|
28
|
+
service and transactional calls only, and promotional calls need a 140-series
|
|
29
|
+
number with an approved DLT header. Omit it and the call is placed as before.
|
|
30
|
+
|
|
31
|
+
While the workspace's Connect deployment runs `CALL_PURPOSE_ENFORCEMENT=shadow`
|
|
32
|
+
(the default), a call no number may carry is still placed and only logged;
|
|
33
|
+
under `enforce` it is refused with a `412` whose message names the number
|
|
34
|
+
series needed. The bridge drops a purpose that is not one of the three
|
|
35
|
+
rather than forwarding it to be refused.
|
|
36
|
+
|
|
6
37
|
## 0.47.0
|
|
7
38
|
|
|
8
39
|
### `connect.integrations.request()` reaches the endpoints a toolkit has no action for
|
|
@@ -243,6 +243,7 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
|
|
|
243
243
|
const client = await signIn();
|
|
244
244
|
const started = await mutateResource("/calls/start", {
|
|
245
245
|
phone: target.phone,
|
|
246
|
+
...(target.purpose ? { purpose: target.purpose } : {}),
|
|
246
247
|
});
|
|
247
248
|
callIdRef.current = started.callId;
|
|
248
249
|
setCall((current) => current ? { ...current, callId: started.callId, number: started.toNumber } : current);
|
|
@@ -308,9 +309,9 @@ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
|
|
|
308
309
|
* above it, and a member with "Make calls" - render it only for them, since a
|
|
309
310
|
* button the server refuses for everyone else helps nobody.
|
|
310
311
|
*/
|
|
311
|
-
function ConnectCallButton({ phone, name, onEnded, variant = "default", children, isDisabled, className, }) {
|
|
312
|
+
function ConnectCallButton({ phone, name, purpose, onEnded, variant = "default", children, isDisabled, className, }) {
|
|
312
313
|
const { isBusy, startCall, availability } = useConnectCall();
|
|
313
|
-
const press = () => void startCall({ phone, name, onEnded });
|
|
314
|
+
const press = () => void startCall({ phone, name, purpose, onEnded });
|
|
314
315
|
// Not offered at all to someone who may not call - a button that always
|
|
315
316
|
// refuses helps nobody.
|
|
316
317
|
if (availability?.status === "no_permission")
|
|
@@ -1,8 +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");
|
|
7
|
+
/** The purposes `/calls/start` accepts - see `ConnectCallPurpose`. */
|
|
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;
|
|
6
17
|
/**
|
|
7
18
|
* A sentinel origin to resolve a candidate destination against. Never reached -
|
|
8
19
|
* `.invalid` is reserved by RFC 2606 precisely so it cannot resolve.
|
|
@@ -308,9 +319,71 @@ function createConnectHandler(options) {
|
|
|
308
319
|
}
|
|
309
320
|
return token;
|
|
310
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
|
+
}
|
|
311
384
|
/** Signed-in identity plus the workspace it belongs to - the first read every other route needs. */
|
|
312
385
|
async function whoami(request) {
|
|
313
|
-
return
|
|
386
|
+
return resolveSession(await requireToken(request));
|
|
314
387
|
}
|
|
315
388
|
/**
|
|
316
389
|
* The caller's identity, plus the workspace every tenant-scoped route below
|
|
@@ -344,7 +417,8 @@ function createConnectHandler(options) {
|
|
|
344
417
|
/**
|
|
345
418
|
* The store paths a browser may reach through the bridge.
|
|
346
419
|
*
|
|
347
|
-
* 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.
|
|
348
422
|
* **`/data-scan/` is deliberately absent**: a scan crosses every tenant, and
|
|
349
423
|
* Connect refuses one made on behalf of a user - but a browser should not be
|
|
350
424
|
* able to *ask*, so the refusal happens here rather than a round trip away.
|
|
@@ -354,7 +428,8 @@ function createConnectHandler(options) {
|
|
|
354
428
|
return (route.startsWith("/data/") ||
|
|
355
429
|
route.startsWith("/kv/") ||
|
|
356
430
|
route.startsWith("/data-aggregate/") ||
|
|
357
|
-
route === "/data-commit"
|
|
431
|
+
route === "/data-commit" ||
|
|
432
|
+
route === "/data-get-many");
|
|
358
433
|
}
|
|
359
434
|
/**
|
|
360
435
|
* Forwards one store call to Connect on behalf of the caller.
|
|
@@ -437,7 +512,7 @@ function createConnectHandler(options) {
|
|
|
437
512
|
clientId,
|
|
438
513
|
});
|
|
439
514
|
}
|
|
440
|
-
return json({ ...(await
|
|
515
|
+
return json({ ...(await resolveSessionFresh(token)), oneTap: oneTapEndpoints, clientId });
|
|
441
516
|
},
|
|
442
517
|
"GET /entitlements": async (request) => {
|
|
443
518
|
const { workspaceId } = await requireWorkspace(request);
|
|
@@ -848,6 +923,9 @@ function createConnectHandler(options) {
|
|
|
848
923
|
});
|
|
849
924
|
}
|
|
850
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);
|
|
851
929
|
// The response is built first so `session.write` has something to set
|
|
852
930
|
// cookies on, matching how `/callback` does it.
|
|
853
931
|
const response = json({
|
|
@@ -1260,7 +1338,10 @@ function createConnectHandler(options) {
|
|
|
1260
1338
|
endpoint: "/calls/start",
|
|
1261
1339
|
});
|
|
1262
1340
|
}
|
|
1263
|
-
|
|
1341
|
+
// Passed on only when it is one of the three - Connect validates it
|
|
1342
|
+
// again, and an unknown value is dropped rather than sent to be refused.
|
|
1343
|
+
const purpose = CALL_PURPOSES.find((value) => value === body.purpose);
|
|
1344
|
+
return json(await client.calls.startBrowserCall({ phone: body.phone, asUser, purpose }), 201);
|
|
1264
1345
|
},
|
|
1265
1346
|
"POST /calls/cancel": async (request) => {
|
|
1266
1347
|
await requireWorkspace(request);
|
|
@@ -1497,6 +1578,8 @@ function createConnectHandler(options) {
|
|
|
1497
1578
|
},
|
|
1498
1579
|
"POST /logout": async (request) => {
|
|
1499
1580
|
const token = await session.read(request);
|
|
1581
|
+
if (token)
|
|
1582
|
+
sessionCache.delete(token);
|
|
1500
1583
|
const response = json({ ok: true });
|
|
1501
1584
|
await session.clear?.(request, response);
|
|
1502
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, {
|
|
@@ -118,13 +130,14 @@ class CallsResource {
|
|
|
118
130
|
*
|
|
119
131
|
* Throws `ConnectError` with status `412` when the workspace has no active
|
|
120
132
|
* number, is not allowed to place calls, or the member is already on one -
|
|
121
|
-
* the message is written to be shown to them.
|
|
133
|
+
* the message is written to be shown to them. With a `purpose`, also when
|
|
134
|
+
* no number may carry it and the workspace enforces purposes.
|
|
122
135
|
*/
|
|
123
136
|
startBrowserCall(input) {
|
|
124
137
|
return (0, http_js_1.request)(this.transport, {
|
|
125
138
|
method: "POST",
|
|
126
139
|
path: "/calls/start",
|
|
127
|
-
json: { phone: input.phone },
|
|
140
|
+
json: input.purpose ? { phone: input.phone, purpose: input.purpose } : { phone: input.phone },
|
|
128
141
|
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
129
142
|
});
|
|
130
143
|
}
|
|
@@ -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()`
|
|
@@ -203,6 +203,7 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
|
|
|
203
203
|
const client = await signIn();
|
|
204
204
|
const started = await mutateResource("/calls/start", {
|
|
205
205
|
phone: target.phone,
|
|
206
|
+
...(target.purpose ? { purpose: target.purpose } : {}),
|
|
206
207
|
});
|
|
207
208
|
callIdRef.current = started.callId;
|
|
208
209
|
setCall((current) => current ? { ...current, callId: started.callId, number: started.toNumber } : current);
|
|
@@ -268,9 +269,9 @@ export function ConnectCallProvider({ children, onCallEnded, onError, className,
|
|
|
268
269
|
* above it, and a member with "Make calls" - render it only for them, since a
|
|
269
270
|
* button the server refuses for everyone else helps nobody.
|
|
270
271
|
*/
|
|
271
|
-
export function ConnectCallButton({ phone, name, onEnded, variant = "default", children, isDisabled, className, }) {
|
|
272
|
+
export function ConnectCallButton({ phone, name, purpose, onEnded, variant = "default", children, isDisabled, className, }) {
|
|
272
273
|
const { isBusy, startCall, availability } = useConnectCall();
|
|
273
|
-
const press = () => void startCall({ phone, name, onEnded });
|
|
274
|
+
const press = () => void startCall({ phone, name, purpose, onEnded });
|
|
274
275
|
// Not offered at all to someone who may not call - a button that always
|
|
275
276
|
// refuses helps nobody.
|
|
276
277
|
if (availability?.status === "no_permission")
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { ConnectError } from "../errors.js";
|
|
2
2
|
import { createPkcePair, createState } from "./resources/identity.js";
|
|
3
|
+
/** The purposes `/calls/start` accepts - see `ConnectCallPurpose`. */
|
|
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;
|
|
3
13
|
/**
|
|
4
14
|
* A sentinel origin to resolve a candidate destination against. Never reached -
|
|
5
15
|
* `.invalid` is reserved by RFC 2606 precisely so it cannot resolve.
|
|
@@ -305,9 +315,71 @@ export function createConnectHandler(options) {
|
|
|
305
315
|
}
|
|
306
316
|
return token;
|
|
307
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
|
+
}
|
|
308
380
|
/** Signed-in identity plus the workspace it belongs to - the first read every other route needs. */
|
|
309
381
|
async function whoami(request) {
|
|
310
|
-
return
|
|
382
|
+
return resolveSession(await requireToken(request));
|
|
311
383
|
}
|
|
312
384
|
/**
|
|
313
385
|
* The caller's identity, plus the workspace every tenant-scoped route below
|
|
@@ -341,7 +413,8 @@ export function createConnectHandler(options) {
|
|
|
341
413
|
/**
|
|
342
414
|
* The store paths a browser may reach through the bridge.
|
|
343
415
|
*
|
|
344
|
-
* 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.
|
|
345
418
|
* **`/data-scan/` is deliberately absent**: a scan crosses every tenant, and
|
|
346
419
|
* Connect refuses one made on behalf of a user - but a browser should not be
|
|
347
420
|
* able to *ask*, so the refusal happens here rather than a round trip away.
|
|
@@ -351,7 +424,8 @@ export function createConnectHandler(options) {
|
|
|
351
424
|
return (route.startsWith("/data/") ||
|
|
352
425
|
route.startsWith("/kv/") ||
|
|
353
426
|
route.startsWith("/data-aggregate/") ||
|
|
354
|
-
route === "/data-commit"
|
|
427
|
+
route === "/data-commit" ||
|
|
428
|
+
route === "/data-get-many");
|
|
355
429
|
}
|
|
356
430
|
/**
|
|
357
431
|
* Forwards one store call to Connect on behalf of the caller.
|
|
@@ -434,7 +508,7 @@ export function createConnectHandler(options) {
|
|
|
434
508
|
clientId,
|
|
435
509
|
});
|
|
436
510
|
}
|
|
437
|
-
return json({ ...(await
|
|
511
|
+
return json({ ...(await resolveSessionFresh(token)), oneTap: oneTapEndpoints, clientId });
|
|
438
512
|
},
|
|
439
513
|
"GET /entitlements": async (request) => {
|
|
440
514
|
const { workspaceId } = await requireWorkspace(request);
|
|
@@ -845,6 +919,9 @@ export function createConnectHandler(options) {
|
|
|
845
919
|
});
|
|
846
920
|
}
|
|
847
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);
|
|
848
925
|
// The response is built first so `session.write` has something to set
|
|
849
926
|
// cookies on, matching how `/callback` does it.
|
|
850
927
|
const response = json({
|
|
@@ -1257,7 +1334,10 @@ export function createConnectHandler(options) {
|
|
|
1257
1334
|
endpoint: "/calls/start",
|
|
1258
1335
|
});
|
|
1259
1336
|
}
|
|
1260
|
-
|
|
1337
|
+
// Passed on only when it is one of the three - Connect validates it
|
|
1338
|
+
// again, and an unknown value is dropped rather than sent to be refused.
|
|
1339
|
+
const purpose = CALL_PURPOSES.find((value) => value === body.purpose);
|
|
1340
|
+
return json(await client.calls.startBrowserCall({ phone: body.phone, asUser, purpose }), 201);
|
|
1261
1341
|
},
|
|
1262
1342
|
"POST /calls/cancel": async (request) => {
|
|
1263
1343
|
await requireWorkspace(request);
|
|
@@ -1494,6 +1574,8 @@ export function createConnectHandler(options) {
|
|
|
1494
1574
|
},
|
|
1495
1575
|
"POST /logout": async (request) => {
|
|
1496
1576
|
const token = await session.read(request);
|
|
1577
|
+
if (token)
|
|
1578
|
+
sessionCache.delete(token);
|
|
1497
1579
|
const response = json({ ok: true });
|
|
1498
1580
|
await session.clear?.(request, response);
|
|
1499
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, {
|
|
@@ -115,13 +127,14 @@ export class CallsResource {
|
|
|
115
127
|
*
|
|
116
128
|
* Throws `ConnectError` with status `412` when the workspace has no active
|
|
117
129
|
* number, is not allowed to place calls, or the member is already on one -
|
|
118
|
-
* the message is written to be shown to them.
|
|
130
|
+
* the message is written to be shown to them. With a `purpose`, also when
|
|
131
|
+
* no number may carry it and the workspace enforces purposes.
|
|
119
132
|
*/
|
|
120
133
|
startBrowserCall(input) {
|
|
121
134
|
return request(this.transport, {
|
|
122
135
|
method: "POST",
|
|
123
136
|
path: "/calls/start",
|
|
124
|
-
json: { phone: input.phone },
|
|
137
|
+
json: input.purpose ? { phone: input.phone, purpose: input.purpose } : { phone: input.phone },
|
|
125
138
|
headers: actingAs(input.asUser),
|
|
126
139
|
});
|
|
127
140
|
}
|
|
@@ -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()`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { CallingAvailability } from "../../types.js";
|
|
2
|
+
import type { CallingAvailability, ConnectCallPurpose } from "../../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Calling from the browser, through the workspace's own number.
|
|
5
5
|
*
|
|
@@ -60,6 +60,11 @@ export interface ConnectCallTarget {
|
|
|
60
60
|
phone: string;
|
|
61
61
|
/** Shown on the in-call bar. Defaults to the number. */
|
|
62
62
|
name?: string;
|
|
63
|
+
/**
|
|
64
|
+
* What the call is for - Connect places it from a number allowed to carry
|
|
65
|
+
* it. See `ConnectCallPurpose`; omit it to place the call as before.
|
|
66
|
+
*/
|
|
67
|
+
purpose?: ConnectCallPurpose;
|
|
63
68
|
/**
|
|
64
69
|
* Called once the call is over, with Connect's id for it - or `null` when it
|
|
65
70
|
* never got as far as being approved. Read the outcome on your server with
|
|
@@ -118,7 +123,7 @@ export interface ConnectCallButtonProps extends ConnectCallTarget {
|
|
|
118
123
|
* above it, and a member with "Make calls" - render it only for them, since a
|
|
119
124
|
* button the server refuses for everyone else helps nobody.
|
|
120
125
|
*/
|
|
121
|
-
export declare function ConnectCallButton({ phone, name, onEnded, variant, children, isDisabled, className, }: ConnectCallButtonProps): import("react").JSX.Element | null;
|
|
126
|
+
export declare function ConnectCallButton({ phone, name, purpose, onEnded, variant, children, isDisabled, className, }: ConnectCallButtonProps): import("react").JSX.Element | null;
|
|
122
127
|
export interface ConnectCallingStatusProps {
|
|
123
128
|
/** Merged with `cn`. */
|
|
124
129
|
className?: string;
|
|
@@ -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, 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;
|
|
@@ -84,11 +92,13 @@ export declare class CallsResource {
|
|
|
84
92
|
*
|
|
85
93
|
* Throws `ConnectError` with status `412` when the workspace has no active
|
|
86
94
|
* number, is not allowed to place calls, or the member is already on one -
|
|
87
|
-
* the message is written to be shown to them.
|
|
95
|
+
* the message is written to be shown to them. With a `purpose`, also when
|
|
96
|
+
* no number may carry it and the workspace enforces purposes.
|
|
88
97
|
*/
|
|
89
98
|
startBrowserCall(input: {
|
|
90
99
|
phone: string;
|
|
91
100
|
asUser: string;
|
|
101
|
+
purpose?: ConnectCallPurpose;
|
|
92
102
|
}): Promise<StartedBrowserCall>;
|
|
93
103
|
/**
|
|
94
104
|
* Closes a call the browser never managed to place - recorded as
|
|
@@ -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;
|
|
@@ -2911,6 +2919,14 @@ export interface ConnectCallAnalysis {
|
|
|
2911
2919
|
* carry a one-time key, valid for two minutes, that lets the call be placed
|
|
2912
2920
|
* once and by nobody else.
|
|
2913
2921
|
*/
|
|
2922
|
+
/**
|
|
2923
|
+
* What a call is for, declared by the caller - Connect places it from a
|
|
2924
|
+
* workspace number allowed to carry it. In India the series decides: regular
|
|
2925
|
+
* numbers carry `service` and `transactional`, 140-series numbers carry
|
|
2926
|
+
* `promotional` only. Omit it and the call is placed as before and recorded
|
|
2927
|
+
* as undeclared.
|
|
2928
|
+
*/
|
|
2929
|
+
export type ConnectCallPurpose = "service" | "transactional" | "promotional";
|
|
2914
2930
|
export interface StartedBrowserCall {
|
|
2915
2931
|
callId: string;
|
|
2916
2932
|
/** The number being rung, E.164. */
|
|
@@ -2943,6 +2959,45 @@ export interface CallSettings {
|
|
|
2943
2959
|
/** Whether this member may change it - their `channel:manage`. Draw the switch disabled when not. */
|
|
2944
2960
|
canManage: boolean;
|
|
2945
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
|
+
}
|
|
2946
3001
|
export interface CallingAvailability {
|
|
2947
3002
|
status: CallingAvailabilityStatus;
|
|
2948
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",
|