@cosmoops/connect-sdk 0.48.0 → 0.50.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.
@@ -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
@@ -13,3 +13,8 @@ export { createState } from "./resources/identity.js";
13
13
  * obvious reason: the signing secret must never reach a browser bundle.
14
14
  */
15
15
  export { createConnectWebhookHandler, verifyConnectWebhook, } from "./webhooks.js";
16
+ /**
17
+ * Receiving a Connect task - verify Google's OIDC token, then run. See
18
+ * `TasksResource` for the sending half.
19
+ */
20
+ export { verifyConnectTask, createConnectTaskHandler, ConnectTaskVerificationError, } from "./tasks.js";
@@ -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()`
@@ -0,0 +1,22 @@
1
+ import { request } from "../http.js";
2
+ /**
3
+ * Background work, delivered back to your own endpoint by Google Cloud Tasks.
4
+ *
5
+ * Declare a queue in Console - its URL, its rate, its retries - then enqueue
6
+ * here. Cloud Tasks `POST`s `{ workspaceId, queue, payload }` to the queue's
7
+ * URL with a Google-signed OIDC token; verify it with `verifyConnectTask` (or
8
+ * let `createConnectTaskHandler` do it) before trusting a byte of the body.
9
+ *
10
+ * Deliveries are **at least once**. Make each step safe to run twice, and
11
+ * give each enqueue a `name` derived from the step so a retried enqueue is a
12
+ * `duplicate` rather than a second task.
13
+ */
14
+ export class TasksResource {
15
+ transport;
16
+ constructor(transport) {
17
+ this.transport = transport;
18
+ }
19
+ enqueue(input) {
20
+ return request(this.transport, { method: "POST", path: "/tasks", json: input });
21
+ }
22
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Receiving a task Connect enqueued: verify, then run.
3
+ *
4
+ * Cloud Tasks calls your queue's URL directly - not through Connect - with a
5
+ * bearer **OIDC token Google signs** for Connect's invoker service account.
6
+ * Three claims make a request a genuine delivery, and all three are needed:
7
+ *
8
+ * - the signature, against Google's published keys - or anyone can mint one;
9
+ * - `email`, the invoker account - or any Google-signed token for any
10
+ * service account, including one an attacker owns, would pass;
11
+ * - `aud`, your own origin - or a token minted for somebody else's endpoint
12
+ * could be replayed against yours.
13
+ *
14
+ * Checked with WebCrypto, so this needs no JWT library and runs on Node and
15
+ * on edge runtimes alike.
16
+ */
17
+ const GOOGLE_CERTS_URL = "https://www.googleapis.com/oauth2/v3/certs";
18
+ const GOOGLE_ISSUERS = new Set(["https://accounts.google.com", "accounts.google.com"]);
19
+ /** Clock skew tolerated between Google and this server. */
20
+ const SKEW_MS = 60_000;
21
+ const DEFAULT_CERTS_TTL_MS = 60 * 60 * 1000;
22
+ export class ConnectTaskVerificationError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "ConnectTaskVerificationError";
26
+ }
27
+ }
28
+ let certsCache = null;
29
+ /** Test seam - drops the cached Google keys. */
30
+ export function clearConnectTaskKeyCache() {
31
+ certsCache = null;
32
+ }
33
+ async function googleKeys(fetchImpl, now, force) {
34
+ if (!force && certsCache && certsCache.expiresAt > now)
35
+ return certsCache.keys;
36
+ const response = await fetchImpl(GOOGLE_CERTS_URL, { method: "GET" });
37
+ if (!response.ok)
38
+ throw new ConnectTaskVerificationError(`Could not fetch Google's signing keys (${response.status}).`);
39
+ const body = (await response.json());
40
+ const maxAge = /max-age=(\d+)/.exec(response.headers.get("cache-control") ?? "")?.[1];
41
+ certsCache = {
42
+ keys: body.keys ?? [],
43
+ expiresAt: now + (maxAge ? Number(maxAge) * 1000 : DEFAULT_CERTS_TTL_MS),
44
+ };
45
+ return certsCache.keys;
46
+ }
47
+ function base64UrlToBytes(segment) {
48
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(segment.length / 4) * 4, "=");
49
+ const binary = atob(base64);
50
+ const bytes = new Uint8Array(binary.length);
51
+ for (let i = 0; i < binary.length; i += 1)
52
+ bytes[i] = binary.charCodeAt(i);
53
+ return bytes;
54
+ }
55
+ function decodeSegment(segment) {
56
+ try {
57
+ return JSON.parse(new TextDecoder().decode(base64UrlToBytes(segment)));
58
+ }
59
+ catch {
60
+ throw new ConnectTaskVerificationError("The token is not a JWT.");
61
+ }
62
+ }
63
+ /**
64
+ * Verify a delivery and return it, or throw `ConnectTaskVerificationError`.
65
+ *
66
+ * Reads the body, so call it once and use what it returns.
67
+ */
68
+ export async function verifyConnectTask(request, options = {}) {
69
+ const invoker = options.invoker ?? process.env.CONNECT_TASK_INVOKER;
70
+ if (!invoker) {
71
+ throw new ConnectTaskVerificationError("No invoker configured - set CONNECT_TASK_INVOKER to the invoker service account the platform gave you.");
72
+ }
73
+ const audience = options.audience ?? process.env.CONNECT_TASK_AUDIENCE ?? new URL(request.url).origin;
74
+ const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
75
+ const now = (options.now ?? Date.now)();
76
+ const header = request.headers.get("authorization") ?? "";
77
+ const token = /^Bearer\s+(.+)$/i.exec(header)?.[1];
78
+ if (!token)
79
+ throw new ConnectTaskVerificationError("No bearer token.");
80
+ const parts = token.split(".");
81
+ if (parts.length !== 3)
82
+ throw new ConnectTaskVerificationError("The token is not a JWT.");
83
+ const [headerSegment, claimsSegment, signatureSegment] = parts;
84
+ const jwtHeader = decodeSegment(headerSegment);
85
+ if (jwtHeader.alg !== "RS256" || typeof jwtHeader.kid !== "string") {
86
+ throw new ConnectTaskVerificationError("Unexpected token algorithm.");
87
+ }
88
+ // An unknown `kid` is usually a rotation the cache has not seen - refetch once.
89
+ let jwk = (await googleKeys(fetchImpl, now, false)).find((key) => key.kid === jwtHeader.kid);
90
+ if (!jwk)
91
+ jwk = (await googleKeys(fetchImpl, now, true)).find((key) => key.kid === jwtHeader.kid);
92
+ if (!jwk)
93
+ throw new ConnectTaskVerificationError("The token was not signed by a current Google key.");
94
+ const key = await crypto.subtle.importKey("jwk", { kty: jwk.kty, n: jwk.n, e: jwk.e, alg: "RS256", ext: true }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
95
+ const valid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, base64UrlToBytes(signatureSegment), new TextEncoder().encode(`${headerSegment}.${claimsSegment}`));
96
+ if (!valid)
97
+ throw new ConnectTaskVerificationError("The token's signature does not verify.");
98
+ const claims = decodeSegment(claimsSegment);
99
+ if (typeof claims.iss !== "string" || !GOOGLE_ISSUERS.has(claims.iss)) {
100
+ throw new ConnectTaskVerificationError("The token was not issued by Google.");
101
+ }
102
+ if (claims.aud !== audience)
103
+ throw new ConnectTaskVerificationError("The token was minted for another audience.");
104
+ if (claims.email !== invoker || claims.email_verified !== true) {
105
+ throw new ConnectTaskVerificationError("The token is not Connect's invoker.");
106
+ }
107
+ if (typeof claims.exp !== "number" || claims.exp * 1000 < now - SKEW_MS) {
108
+ throw new ConnectTaskVerificationError("The token has expired.");
109
+ }
110
+ if (typeof claims.iat === "number" && claims.iat * 1000 > now + SKEW_MS) {
111
+ throw new ConnectTaskVerificationError("The token is from the future.");
112
+ }
113
+ let body;
114
+ try {
115
+ body = (await request.json());
116
+ }
117
+ catch {
118
+ throw new ConnectTaskVerificationError("The delivery body is not JSON.");
119
+ }
120
+ if (typeof body.workspaceId !== "string" || typeof body.queue !== "string") {
121
+ throw new ConnectTaskVerificationError("The delivery body is not a Connect task.");
122
+ }
123
+ return {
124
+ workspaceId: body.workspaceId,
125
+ queue: body.queue,
126
+ payload: body.payload,
127
+ retryCount: Number(request.headers.get("x-cloudtasks-taskretrycount") ?? 0) || 0,
128
+ taskName: request.headers.get("x-cloudtasks-taskname"),
129
+ };
130
+ }
131
+ /**
132
+ * A ready-made `POST` route for a queue's URL:
133
+ *
134
+ * ```ts
135
+ * // app/api/tasks/campaign/route.ts
136
+ * export const POST = createConnectTaskHandler<{ campaignId: string }>({
137
+ * handle: async ({ workspaceId, payload }) => runCampaignPage(workspaceId, payload.campaignId),
138
+ * });
139
+ * ```
140
+ *
141
+ * `401` for anything that does not verify, `500` when `handle` throws (or
142
+ * Google's keys cannot be fetched), `200` otherwise.
143
+ */
144
+ export function createConnectTaskHandler(options) {
145
+ const { handle, ...verifyOptions } = options;
146
+ return async (request) => {
147
+ let delivery;
148
+ try {
149
+ delivery = await verifyConnectTask(request, verifyOptions);
150
+ }
151
+ catch (error) {
152
+ if (error instanceof ConnectTaskVerificationError) {
153
+ return Response.json({ error: error.message }, { status: 401 });
154
+ }
155
+ // Google's keys unreachable, say - not the sender's fault, so retryable.
156
+ console.error("[connect-task] could not verify a delivery:", error);
157
+ return Response.json({ error: "Could not verify the delivery." }, { status: 500 });
158
+ }
159
+ try {
160
+ await handle(delivery);
161
+ }
162
+ catch (error) {
163
+ console.error(`[connect-task] ${delivery.queue} failed (attempt ${delivery.retryCount + 1}):`, error);
164
+ return Response.json({ error: "Task failed - Cloud Tasks will retry it." }, { status: 500 });
165
+ }
166
+ return Response.json({ ok: true });
167
+ };
168
+ }
@@ -237,3 +237,4 @@ export declare function requireConnectSession(request?: Request, options?: {
237
237
  export { createConnectHandler } from "../server/handler.js";
238
238
  export type { ConnectSessionStore } from "../server/handler.js";
239
239
  export { ConnectClient } from "../server/client.js";
240
+ export { verifyConnectTask, createConnectTaskHandler, ConnectTaskVerificationError } from "../server/tasks.js";
@@ -23,6 +23,7 @@ import { SessionsResource } from "./resources/sessions.js";
23
23
  import { PlatformResource } from "./resources/platform.js";
24
24
  import { VisitorResource } from "./resources/visitor.js";
25
25
  import { PushResource } from "./resources/push.js";
26
+ import { TasksResource } from "./resources/tasks.js";
26
27
  import { WhatsAppResource } from "./resources/whatsapp.js";
27
28
  export declare const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
28
29
  export interface ConnectClientOptions {
@@ -125,6 +126,8 @@ export declare class ConnectClient {
125
126
  * preferences; you supply a title, a body and a URL. See `PushResource`.
126
127
  */
127
128
  readonly push: PushResource;
129
+ /** Background tasks on this application's declared Cloud Tasks queues - see `TasksResource`. */
130
+ readonly tasks: TasksResource;
128
131
  /** Your application's own approved WhatsApp templates, to a workspace's customers. */
129
132
  readonly whatsapp: WhatsAppResource;
130
133
  /**
@@ -141,4 +144,4 @@ export declare class ConnectClient {
141
144
  constructor(options: ConnectClientOptions);
142
145
  }
143
146
  /** Kept in step with `package.json` by `sdk.test.ts`. */
144
- export declare const SDK_VERSION = "0.48.0";
147
+ export declare const SDK_VERSION = "0.50.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>;
@@ -25,6 +25,7 @@ export type { WorkspacesResource } from "./resources/workspaces.js";
25
25
  export type { OtpResource } from "./resources/otp.js";
26
26
  export type { LogsResource } from "./resources/logs.js";
27
27
  export type { AnalyticsResource } from "./resources/analytics.js";
28
+ export type { TasksResource } from "./resources/tasks.js";
28
29
  export type { SessionsResource } from "./resources/sessions.js";
29
30
  export type { ExecuteIntegrationActionInput, InitiateIntegrationConnectionInput, IntegrationsResource, ProxyIntegrationRequestInput, } from "./resources/integrations.js";
30
31
  export type { PlatformResource } from "./resources/platform.js";
@@ -37,3 +38,9 @@ export type { CallsResource, ListCallsInput } from "./resources/calls.js";
37
38
  * obvious reason: the signing secret must never reach a browser bundle.
38
39
  */
39
40
  export { createConnectWebhookHandler, verifyConnectWebhook, type ConnectWebhookHandlers, type ConnectWebhookHandlerOptions, } from "./webhooks.js";
41
+ /**
42
+ * Receiving a Connect task - verify Google's OIDC token, then run. See
43
+ * `TasksResource` for the sending half.
44
+ */
45
+ export { verifyConnectTask, createConnectTaskHandler, ConnectTaskVerificationError, } from "./tasks.js";
46
+ export type { VerifyConnectTaskOptions, ConnectTaskHandlerOptions } from "./tasks.js";
@@ -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
  *
@@ -0,0 +1,19 @@
1
+ import { type TransportOptions } from "../http.js";
2
+ import type { EnqueueTaskInput, EnqueueTaskResponse } from "../../types.js";
3
+ /**
4
+ * Background work, delivered back to your own endpoint by Google Cloud Tasks.
5
+ *
6
+ * Declare a queue in Console - its URL, its rate, its retries - then enqueue
7
+ * here. Cloud Tasks `POST`s `{ workspaceId, queue, payload }` to the queue's
8
+ * URL with a Google-signed OIDC token; verify it with `verifyConnectTask` (or
9
+ * let `createConnectTaskHandler` do it) before trusting a byte of the body.
10
+ *
11
+ * Deliveries are **at least once**. Make each step safe to run twice, and
12
+ * give each enqueue a `name` derived from the step so a retried enqueue is a
13
+ * `duplicate` rather than a second task.
14
+ */
15
+ export declare class TasksResource {
16
+ private readonly transport;
17
+ constructor(transport: TransportOptions);
18
+ enqueue(input: EnqueueTaskInput): Promise<EnqueueTaskResponse>;
19
+ }
@@ -0,0 +1,51 @@
1
+ import type { FetchLike } from "./http.js";
2
+ import type { ConnectTaskDelivery } from "../types.js";
3
+ export declare class ConnectTaskVerificationError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export interface VerifyConnectTaskOptions {
7
+ /**
8
+ * The invoker service account's email - the one the platform gives you
9
+ * with your API key. Defaults to `CONNECT_TASK_INVOKER`.
10
+ */
11
+ invoker?: string;
12
+ /**
13
+ * Your own origin, which Connect sets as the token's audience. Defaults to
14
+ * `CONNECT_TASK_AUDIENCE`, then to the request URL's origin.
15
+ */
16
+ audience?: string;
17
+ /** Injectable for tests. Defaults to the global `fetch`. */
18
+ fetch?: FetchLike;
19
+ /** Injectable for tests. Milliseconds since the epoch. */
20
+ now?: () => number;
21
+ }
22
+ /** Test seam - drops the cached Google keys. */
23
+ export declare function clearConnectTaskKeyCache(): void;
24
+ /**
25
+ * Verify a delivery and return it, or throw `ConnectTaskVerificationError`.
26
+ *
27
+ * Reads the body, so call it once and use what it returns.
28
+ */
29
+ export declare function verifyConnectTask<TPayload = unknown>(request: Request, options?: VerifyConnectTaskOptions): Promise<ConnectTaskDelivery<TPayload>>;
30
+ export interface ConnectTaskHandlerOptions<TPayload> extends VerifyConnectTaskOptions {
31
+ /**
32
+ * Run one task. Return normally and Cloud Tasks counts it done; **throw**
33
+ * and it is retried with backoff, up to the queue's declared attempts. A
34
+ * failure no retry can fix (the record is gone) should return, not throw.
35
+ */
36
+ handle: (delivery: ConnectTaskDelivery<TPayload>) => Promise<void> | void;
37
+ }
38
+ /**
39
+ * A ready-made `POST` route for a queue's URL:
40
+ *
41
+ * ```ts
42
+ * // app/api/tasks/campaign/route.ts
43
+ * export const POST = createConnectTaskHandler<{ campaignId: string }>({
44
+ * handle: async ({ workspaceId, payload }) => runCampaignPage(workspaceId, payload.campaignId),
45
+ * });
46
+ * ```
47
+ *
48
+ * `401` for anything that does not verify, `500` when `handle` throws (or
49
+ * Google's keys cannot be fetched), `200` otherwise.
50
+ */
51
+ export declare function createConnectTaskHandler<TPayload = unknown>(options: ConnectTaskHandlerOptions<TPayload>): (request: Request) => Promise<Response>;