@cosmoops/connect-sdk 0.49.0 → 0.51.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 +30 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/next/index.js +5 -1
- package/dist/cjs/server/client.js +5 -1
- package/dist/cjs/server/index.js +9 -1
- package/dist/cjs/server/resources/calls.js +21 -0
- package/dist/cjs/server/resources/tasks.js +26 -0
- package/dist/cjs/server/tasks.js +175 -0
- package/dist/esm/client/components/plans.js +1 -1
- package/dist/esm/next/index.js +1 -0
- package/dist/esm/server/client.js +5 -1
- package/dist/esm/server/index.js +5 -0
- package/dist/esm/server/resources/calls.js +21 -0
- package/dist/esm/server/resources/tasks.js +22 -0
- package/dist/esm/server/tasks.js +168 -0
- package/dist/types/next/index.d.ts +1 -0
- package/dist/types/server/client.d.ts +4 -1
- package/dist/types/server/index.d.ts +7 -0
- package/dist/types/server/resources/calls.d.ts +13 -0
- package/dist/types/server/resources/tasks.d.ts +19 -0
- package/dist/types/server/tasks.d.ts +51 -0
- package/dist/types/types.d.ts +48 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
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.51.0
|
|
7
|
+
|
|
8
|
+
### Inbound call routing
|
|
9
|
+
|
|
10
|
+
`connect.calls.getRouting({ asUser })` reads the workspace's inbound call
|
|
11
|
+
routing flow and its published agents; `connect.calls.setRouting({ routing,
|
|
12
|
+
asUser })` sets it (smart CRM owner routing, AI agent, single number, team,
|
|
13
|
+
fallback). Needs `CHANNEL_MANAGE` to write.
|
|
14
|
+
|
|
15
|
+
### Add-ons billed yearly
|
|
16
|
+
|
|
17
|
+
`Addon.billingCadence` is `"annual"` for an add-on billed once on the first
|
|
18
|
+
cycle that holds it and on each anniversary, even on a monthly plan; render
|
|
19
|
+
its price as "/year". `"plan"` or absent means every plan cycle, as before.
|
|
20
|
+
`ConnectPlans` already draws the suffix. Additive; nothing else changed.
|
|
21
|
+
|
|
22
|
+
## 0.50.0
|
|
23
|
+
|
|
24
|
+
### Background tasks
|
|
25
|
+
|
|
26
|
+
`connect.tasks.enqueue({ workspaceId, queue, payload, name?, scheduleAt? })`
|
|
27
|
+
puts a task on one of your application's Cloud Tasks queues, declared in
|
|
28
|
+
Console (scope `tasks:write`). Cloud Tasks delivers `{ workspaceId, queue,
|
|
29
|
+
payload }` to the queue's URL with a Google-signed OIDC token;
|
|
30
|
+
`createConnectTaskHandler({ handle })` - or `verifyConnectTask(request)` -
|
|
31
|
+
from `./server` or `./next` verifies it. Set `CONNECT_TASK_INVOKER` to the
|
|
32
|
+
invoker account the platform gives you. `name` dedups per workspace;
|
|
33
|
+
`status: "unavailable"` means the Connect deployment has no queue, so keep a
|
|
34
|
+
fallback. Additive; nothing else changed.
|
|
35
|
+
|
|
6
36
|
## 0.49.0
|
|
7
37
|
|
|
8
38
|
### Calling readiness
|
|
@@ -140,7 +140,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
140
140
|
// card used to do. The description rides along as a
|
|
141
141
|
// title so a feature that needs explaining can have one
|
|
142
142
|
// without a second line in every card.
|
|
143
|
-
(0, jsx_runtime_1.jsxs)("li", { className: "flex items-start gap-2 text-sm text-foreground", title: feature.description || undefined, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "mt-0.5 size-4 shrink-0 text-accent", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: feature.name })] }, feature.key))) })), plan.addons.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 border-t border-separator pt-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: "Add-ons" }), plan.addons.map((addon) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-foreground", children: [addon.name, addon.consumable && ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted", children: [" ", "- ", addon.consumable.includedPerCycle.toLocaleString("en-IN"), " ", addon.consumable.unit, "/cycle"] }))] }), (0, jsx_runtime_1.
|
|
143
|
+
(0, jsx_runtime_1.jsxs)("li", { className: "flex items-start gap-2 text-sm text-foreground", title: feature.description || undefined, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "mt-0.5 size-4 shrink-0 text-accent", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: feature.name })] }, feature.key))) })), plan.addons.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 border-t border-separator pt-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: "Add-ons" }), plan.addons.map((addon) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-foreground", children: [addon.name, addon.consumable && ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted", children: [" ", "- ", addon.consumable.includedPerCycle.toLocaleString("en-IN"), " ", addon.consumable.unit, "/cycle"] }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "shrink-0 text-muted", children: [(0, plan_pricing_js_1.formatPrice)(addon.price), addon.billingCadence === "annual" && "/year"] })] }, addon.id)))] })), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [plan.seats.model === "unlimited" && "Unlimited seats", plan.seats.model === "fixed" && `${plan.seats.included} seats included`, plan.seats.model === "per-seat" &&
|
|
144
144
|
`${plan.seats.included ?? 0} seats included, then ${(0, plan_pricing_js_1.formatPrice)(plan.seats.pricePerExtraSeat ?? 0)} each`] })] }), (0, jsx_runtime_1.jsx)(react_1.Card.Footer, { children: (0, jsx_runtime_1.jsxs)(react_1.Button, { className: "w-full", variant: action === "upgrade" || action === "subscribe" || action === "switch-to-annual"
|
|
145
145
|
? "primary"
|
|
146
146
|
: "secondary",
|
package/dist/cjs/next/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.ConnectClient = exports.createConnectHandler = void 0;
|
|
37
|
+
exports.ConnectTaskVerificationError = exports.createConnectTaskHandler = exports.verifyConnectTask = exports.ConnectClient = exports.createConnectHandler = void 0;
|
|
38
38
|
exports.connectClient = connectClient;
|
|
39
39
|
exports.connectProviderProps = connectProviderProps;
|
|
40
40
|
exports.cookieSessionStore = cookieSessionStore;
|
|
@@ -486,3 +486,7 @@ var handler_js_2 = require("../server/handler.js");
|
|
|
486
486
|
Object.defineProperty(exports, "createConnectHandler", { enumerable: true, get: function () { return handler_js_2.createConnectHandler; } });
|
|
487
487
|
var client_js_2 = require("../server/client.js");
|
|
488
488
|
Object.defineProperty(exports, "ConnectClient", { enumerable: true, get: function () { return client_js_2.ConnectClient; } });
|
|
489
|
+
var tasks_js_1 = require("../server/tasks.js");
|
|
490
|
+
Object.defineProperty(exports, "verifyConnectTask", { enumerable: true, get: function () { return tasks_js_1.verifyConnectTask; } });
|
|
491
|
+
Object.defineProperty(exports, "createConnectTaskHandler", { enumerable: true, get: function () { return tasks_js_1.createConnectTaskHandler; } });
|
|
492
|
+
Object.defineProperty(exports, "ConnectTaskVerificationError", { enumerable: true, get: function () { return tasks_js_1.ConnectTaskVerificationError; } });
|
|
@@ -25,6 +25,7 @@ const sessions_js_1 = require("./resources/sessions.js");
|
|
|
25
25
|
const platform_js_1 = require("./resources/platform.js");
|
|
26
26
|
const visitor_js_1 = require("./resources/visitor.js");
|
|
27
27
|
const push_js_1 = require("./resources/push.js");
|
|
28
|
+
const tasks_js_1 = require("./resources/tasks.js");
|
|
28
29
|
const whatsapp_js_1 = require("./resources/whatsapp.js");
|
|
29
30
|
exports.DEFAULT_BASE_URL = "https://connect.cosmoops.com";
|
|
30
31
|
const APPLICATION_API_PREFIX = "/api/v1/application";
|
|
@@ -129,6 +130,8 @@ class ConnectClient {
|
|
|
129
130
|
* preferences; you supply a title, a body and a URL. See `PushResource`.
|
|
130
131
|
*/
|
|
131
132
|
push;
|
|
133
|
+
/** Background tasks on this application's declared Cloud Tasks queues - see `TasksResource`. */
|
|
134
|
+
tasks;
|
|
132
135
|
/** Your application's own approved WhatsApp templates, to a workspace's customers. */
|
|
133
136
|
whatsapp;
|
|
134
137
|
/**
|
|
@@ -177,10 +180,11 @@ class ConnectClient {
|
|
|
177
180
|
this.visitor = new visitor_js_1.VisitorResource(transport);
|
|
178
181
|
this.platform = new platform_js_1.PlatformResource(transport);
|
|
179
182
|
this.push = new push_js_1.PushResource(transport);
|
|
183
|
+
this.tasks = new tasks_js_1.TasksResource(transport);
|
|
180
184
|
this.whatsapp = new whatsapp_js_1.WhatsAppResource(transport);
|
|
181
185
|
this.storeRelay = (spec) => (0, data_js_1.storePassthrough)(transport, spec);
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
exports.ConnectClient = ConnectClient;
|
|
185
189
|
/** Kept in step with `package.json` by `sdk.test.ts`. */
|
|
186
|
-
exports.SDK_VERSION = "0.
|
|
190
|
+
exports.SDK_VERSION = "0.51.0";
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.verifyConnectWebhook = exports.createConnectWebhookHandler = exports.createState = exports.createConnectHandler = exports.SDK_VERSION = exports.DEFAULT_BASE_URL = exports.ConnectClient = void 0;
|
|
3
|
+
exports.ConnectTaskVerificationError = exports.createConnectTaskHandler = exports.verifyConnectTask = exports.verifyConnectWebhook = exports.createConnectWebhookHandler = exports.createState = exports.createConnectHandler = exports.SDK_VERSION = exports.DEFAULT_BASE_URL = exports.ConnectClient = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Server-only half of `@cosmoops/connect-sdk`.
|
|
6
6
|
*
|
|
@@ -23,3 +23,11 @@ Object.defineProperty(exports, "createState", { enumerable: true, get: function
|
|
|
23
23
|
var webhooks_js_1 = require("./webhooks.js");
|
|
24
24
|
Object.defineProperty(exports, "createConnectWebhookHandler", { enumerable: true, get: function () { return webhooks_js_1.createConnectWebhookHandler; } });
|
|
25
25
|
Object.defineProperty(exports, "verifyConnectWebhook", { enumerable: true, get: function () { return webhooks_js_1.verifyConnectWebhook; } });
|
|
26
|
+
/**
|
|
27
|
+
* Receiving a Connect task - verify Google's OIDC token, then run. See
|
|
28
|
+
* `TasksResource` for the sending half.
|
|
29
|
+
*/
|
|
30
|
+
var tasks_js_1 = require("./tasks.js");
|
|
31
|
+
Object.defineProperty(exports, "verifyConnectTask", { enumerable: true, get: function () { return tasks_js_1.verifyConnectTask; } });
|
|
32
|
+
Object.defineProperty(exports, "createConnectTaskHandler", { enumerable: true, get: function () { return tasks_js_1.createConnectTaskHandler; } });
|
|
33
|
+
Object.defineProperty(exports, "ConnectTaskVerificationError", { enumerable: true, get: function () { return tasks_js_1.ConnectTaskVerificationError; } });
|
|
@@ -80,6 +80,27 @@ class CallsResource {
|
|
|
80
80
|
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Reads the inbound call routing flow and published agents for the workspace.
|
|
85
|
+
*/
|
|
86
|
+
getRouting(input) {
|
|
87
|
+
return (0, http_js_1.request)(this.transport, {
|
|
88
|
+
method: "GET",
|
|
89
|
+
path: "/calls/routing",
|
|
90
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Sets the inbound call routing flow (smart CRM owner routing, AI agent, single number, team, fallback).
|
|
95
|
+
*/
|
|
96
|
+
setRouting(input) {
|
|
97
|
+
return (0, http_js_1.request)(this.transport, {
|
|
98
|
+
method: "PUT",
|
|
99
|
+
path: "/calls/routing",
|
|
100
|
+
json: input.routing,
|
|
101
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
83
104
|
/**
|
|
84
105
|
* Whether the workspace records its member calls, and whether this member
|
|
85
106
|
* may change that. The same switch as Connect's Calling screen.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TasksResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/**
|
|
6
|
+
* Background work, delivered back to your own endpoint by Google Cloud Tasks.
|
|
7
|
+
*
|
|
8
|
+
* Declare a queue in Console - its URL, its rate, its retries - then enqueue
|
|
9
|
+
* here. Cloud Tasks `POST`s `{ workspaceId, queue, payload }` to the queue's
|
|
10
|
+
* URL with a Google-signed OIDC token; verify it with `verifyConnectTask` (or
|
|
11
|
+
* let `createConnectTaskHandler` do it) before trusting a byte of the body.
|
|
12
|
+
*
|
|
13
|
+
* Deliveries are **at least once**. Make each step safe to run twice, and
|
|
14
|
+
* give each enqueue a `name` derived from the step so a retried enqueue is a
|
|
15
|
+
* `duplicate` rather than a second task.
|
|
16
|
+
*/
|
|
17
|
+
class TasksResource {
|
|
18
|
+
transport;
|
|
19
|
+
constructor(transport) {
|
|
20
|
+
this.transport = transport;
|
|
21
|
+
}
|
|
22
|
+
enqueue(input) {
|
|
23
|
+
return (0, http_js_1.request)(this.transport, { method: "POST", path: "/tasks", json: input });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.TasksResource = TasksResource;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectTaskVerificationError = void 0;
|
|
4
|
+
exports.clearConnectTaskKeyCache = clearConnectTaskKeyCache;
|
|
5
|
+
exports.verifyConnectTask = verifyConnectTask;
|
|
6
|
+
exports.createConnectTaskHandler = createConnectTaskHandler;
|
|
7
|
+
/**
|
|
8
|
+
* Receiving a task Connect enqueued: verify, then run.
|
|
9
|
+
*
|
|
10
|
+
* Cloud Tasks calls your queue's URL directly - not through Connect - with a
|
|
11
|
+
* bearer **OIDC token Google signs** for Connect's invoker service account.
|
|
12
|
+
* Three claims make a request a genuine delivery, and all three are needed:
|
|
13
|
+
*
|
|
14
|
+
* - the signature, against Google's published keys - or anyone can mint one;
|
|
15
|
+
* - `email`, the invoker account - or any Google-signed token for any
|
|
16
|
+
* service account, including one an attacker owns, would pass;
|
|
17
|
+
* - `aud`, your own origin - or a token minted for somebody else's endpoint
|
|
18
|
+
* could be replayed against yours.
|
|
19
|
+
*
|
|
20
|
+
* Checked with WebCrypto, so this needs no JWT library and runs on Node and
|
|
21
|
+
* on edge runtimes alike.
|
|
22
|
+
*/
|
|
23
|
+
const GOOGLE_CERTS_URL = "https://www.googleapis.com/oauth2/v3/certs";
|
|
24
|
+
const GOOGLE_ISSUERS = new Set(["https://accounts.google.com", "accounts.google.com"]);
|
|
25
|
+
/** Clock skew tolerated between Google and this server. */
|
|
26
|
+
const SKEW_MS = 60_000;
|
|
27
|
+
const DEFAULT_CERTS_TTL_MS = 60 * 60 * 1000;
|
|
28
|
+
class ConnectTaskVerificationError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ConnectTaskVerificationError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.ConnectTaskVerificationError = ConnectTaskVerificationError;
|
|
35
|
+
let certsCache = null;
|
|
36
|
+
/** Test seam - drops the cached Google keys. */
|
|
37
|
+
function clearConnectTaskKeyCache() {
|
|
38
|
+
certsCache = null;
|
|
39
|
+
}
|
|
40
|
+
async function googleKeys(fetchImpl, now, force) {
|
|
41
|
+
if (!force && certsCache && certsCache.expiresAt > now)
|
|
42
|
+
return certsCache.keys;
|
|
43
|
+
const response = await fetchImpl(GOOGLE_CERTS_URL, { method: "GET" });
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new ConnectTaskVerificationError(`Could not fetch Google's signing keys (${response.status}).`);
|
|
46
|
+
const body = (await response.json());
|
|
47
|
+
const maxAge = /max-age=(\d+)/.exec(response.headers.get("cache-control") ?? "")?.[1];
|
|
48
|
+
certsCache = {
|
|
49
|
+
keys: body.keys ?? [],
|
|
50
|
+
expiresAt: now + (maxAge ? Number(maxAge) * 1000 : DEFAULT_CERTS_TTL_MS),
|
|
51
|
+
};
|
|
52
|
+
return certsCache.keys;
|
|
53
|
+
}
|
|
54
|
+
function base64UrlToBytes(segment) {
|
|
55
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(segment.length / 4) * 4, "=");
|
|
56
|
+
const binary = atob(base64);
|
|
57
|
+
const bytes = new Uint8Array(binary.length);
|
|
58
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
59
|
+
bytes[i] = binary.charCodeAt(i);
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
62
|
+
function decodeSegment(segment) {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(new TextDecoder().decode(base64UrlToBytes(segment)));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new ConnectTaskVerificationError("The token is not a JWT.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify a delivery and return it, or throw `ConnectTaskVerificationError`.
|
|
72
|
+
*
|
|
73
|
+
* Reads the body, so call it once and use what it returns.
|
|
74
|
+
*/
|
|
75
|
+
async function verifyConnectTask(request, options = {}) {
|
|
76
|
+
const invoker = options.invoker ?? process.env.CONNECT_TASK_INVOKER;
|
|
77
|
+
if (!invoker) {
|
|
78
|
+
throw new ConnectTaskVerificationError("No invoker configured - set CONNECT_TASK_INVOKER to the invoker service account the platform gave you.");
|
|
79
|
+
}
|
|
80
|
+
const audience = options.audience ?? process.env.CONNECT_TASK_AUDIENCE ?? new URL(request.url).origin;
|
|
81
|
+
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
82
|
+
const now = (options.now ?? Date.now)();
|
|
83
|
+
const header = request.headers.get("authorization") ?? "";
|
|
84
|
+
const token = /^Bearer\s+(.+)$/i.exec(header)?.[1];
|
|
85
|
+
if (!token)
|
|
86
|
+
throw new ConnectTaskVerificationError("No bearer token.");
|
|
87
|
+
const parts = token.split(".");
|
|
88
|
+
if (parts.length !== 3)
|
|
89
|
+
throw new ConnectTaskVerificationError("The token is not a JWT.");
|
|
90
|
+
const [headerSegment, claimsSegment, signatureSegment] = parts;
|
|
91
|
+
const jwtHeader = decodeSegment(headerSegment);
|
|
92
|
+
if (jwtHeader.alg !== "RS256" || typeof jwtHeader.kid !== "string") {
|
|
93
|
+
throw new ConnectTaskVerificationError("Unexpected token algorithm.");
|
|
94
|
+
}
|
|
95
|
+
// An unknown `kid` is usually a rotation the cache has not seen - refetch once.
|
|
96
|
+
let jwk = (await googleKeys(fetchImpl, now, false)).find((key) => key.kid === jwtHeader.kid);
|
|
97
|
+
if (!jwk)
|
|
98
|
+
jwk = (await googleKeys(fetchImpl, now, true)).find((key) => key.kid === jwtHeader.kid);
|
|
99
|
+
if (!jwk)
|
|
100
|
+
throw new ConnectTaskVerificationError("The token was not signed by a current Google key.");
|
|
101
|
+
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"]);
|
|
102
|
+
const valid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, base64UrlToBytes(signatureSegment), new TextEncoder().encode(`${headerSegment}.${claimsSegment}`));
|
|
103
|
+
if (!valid)
|
|
104
|
+
throw new ConnectTaskVerificationError("The token's signature does not verify.");
|
|
105
|
+
const claims = decodeSegment(claimsSegment);
|
|
106
|
+
if (typeof claims.iss !== "string" || !GOOGLE_ISSUERS.has(claims.iss)) {
|
|
107
|
+
throw new ConnectTaskVerificationError("The token was not issued by Google.");
|
|
108
|
+
}
|
|
109
|
+
if (claims.aud !== audience)
|
|
110
|
+
throw new ConnectTaskVerificationError("The token was minted for another audience.");
|
|
111
|
+
if (claims.email !== invoker || claims.email_verified !== true) {
|
|
112
|
+
throw new ConnectTaskVerificationError("The token is not Connect's invoker.");
|
|
113
|
+
}
|
|
114
|
+
if (typeof claims.exp !== "number" || claims.exp * 1000 < now - SKEW_MS) {
|
|
115
|
+
throw new ConnectTaskVerificationError("The token has expired.");
|
|
116
|
+
}
|
|
117
|
+
if (typeof claims.iat === "number" && claims.iat * 1000 > now + SKEW_MS) {
|
|
118
|
+
throw new ConnectTaskVerificationError("The token is from the future.");
|
|
119
|
+
}
|
|
120
|
+
let body;
|
|
121
|
+
try {
|
|
122
|
+
body = (await request.json());
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new ConnectTaskVerificationError("The delivery body is not JSON.");
|
|
126
|
+
}
|
|
127
|
+
if (typeof body.workspaceId !== "string" || typeof body.queue !== "string") {
|
|
128
|
+
throw new ConnectTaskVerificationError("The delivery body is not a Connect task.");
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
workspaceId: body.workspaceId,
|
|
132
|
+
queue: body.queue,
|
|
133
|
+
payload: body.payload,
|
|
134
|
+
retryCount: Number(request.headers.get("x-cloudtasks-taskretrycount") ?? 0) || 0,
|
|
135
|
+
taskName: request.headers.get("x-cloudtasks-taskname"),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A ready-made `POST` route for a queue's URL:
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* // app/api/tasks/campaign/route.ts
|
|
143
|
+
* export const POST = createConnectTaskHandler<{ campaignId: string }>({
|
|
144
|
+
* handle: async ({ workspaceId, payload }) => runCampaignPage(workspaceId, payload.campaignId),
|
|
145
|
+
* });
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* `401` for anything that does not verify, `500` when `handle` throws (or
|
|
149
|
+
* Google's keys cannot be fetched), `200` otherwise.
|
|
150
|
+
*/
|
|
151
|
+
function createConnectTaskHandler(options) {
|
|
152
|
+
const { handle, ...verifyOptions } = options;
|
|
153
|
+
return async (request) => {
|
|
154
|
+
let delivery;
|
|
155
|
+
try {
|
|
156
|
+
delivery = await verifyConnectTask(request, verifyOptions);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof ConnectTaskVerificationError) {
|
|
160
|
+
return Response.json({ error: error.message }, { status: 401 });
|
|
161
|
+
}
|
|
162
|
+
// Google's keys unreachable, say - not the sender's fault, so retryable.
|
|
163
|
+
console.error("[connect-task] could not verify a delivery:", error);
|
|
164
|
+
return Response.json({ error: "Could not verify the delivery." }, { status: 500 });
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await handle(delivery);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error(`[connect-task] ${delivery.queue} failed (attempt ${delivery.retryCount + 1}):`, error);
|
|
171
|
+
return Response.json({ error: "Task failed - Cloud Tasks will retry it." }, { status: 500 });
|
|
172
|
+
}
|
|
173
|
+
return Response.json({ ok: true });
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -137,7 +137,7 @@ export function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
137
137
|
// card used to do. The description rides along as a
|
|
138
138
|
// title so a feature that needs explaining can have one
|
|
139
139
|
// without a second line in every card.
|
|
140
|
-
_jsxs("li", { className: "flex items-start gap-2 text-sm text-foreground", title: feature.description || undefined, children: [_jsx(Check, { className: "mt-0.5 size-4 shrink-0 text-accent", "aria-hidden": "true" }), _jsx("span", { children: feature.name })] }, feature.key))) })), plan.addons.length > 0 && (_jsxs("div", { className: "flex flex-col gap-1.5 border-t border-separator pt-3", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: "Add-ons" }), plan.addons.map((addon) => (_jsxs("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [_jsxs("span", { className: "text-foreground", children: [addon.name, addon.consumable && (_jsxs("span", { className: "text-muted", children: [" ", "- ", addon.consumable.includedPerCycle.toLocaleString("en-IN"), " ", addon.consumable.unit, "/cycle"] }))] }),
|
|
140
|
+
_jsxs("li", { className: "flex items-start gap-2 text-sm text-foreground", title: feature.description || undefined, children: [_jsx(Check, { className: "mt-0.5 size-4 shrink-0 text-accent", "aria-hidden": "true" }), _jsx("span", { children: feature.name })] }, feature.key))) })), plan.addons.length > 0 && (_jsxs("div", { className: "flex flex-col gap-1.5 border-t border-separator pt-3", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: "Add-ons" }), plan.addons.map((addon) => (_jsxs("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [_jsxs("span", { className: "text-foreground", children: [addon.name, addon.consumable && (_jsxs("span", { className: "text-muted", children: [" ", "- ", addon.consumable.includedPerCycle.toLocaleString("en-IN"), " ", addon.consumable.unit, "/cycle"] }))] }), _jsxs("span", { className: "shrink-0 text-muted", children: [formatPrice(addon.price), addon.billingCadence === "annual" && "/year"] })] }, addon.id)))] })), _jsxs("p", { className: "text-xs text-muted", children: [plan.seats.model === "unlimited" && "Unlimited seats", plan.seats.model === "fixed" && `${plan.seats.included} seats included`, plan.seats.model === "per-seat" &&
|
|
141
141
|
`${plan.seats.included ?? 0} seats included, then ${formatPrice(plan.seats.pricePerExtraSeat ?? 0)} each`] })] }), _jsx(Card.Footer, { children: _jsxs(Button, { className: "w-full", variant: action === "upgrade" || action === "subscribe" || action === "switch-to-annual"
|
|
142
142
|
? "primary"
|
|
143
143
|
: "secondary",
|
package/dist/esm/next/index.js
CHANGED
|
@@ -452,3 +452,4 @@ async function redirectTo(url) {
|
|
|
452
452
|
}
|
|
453
453
|
export { createConnectHandler } from "../server/handler.js";
|
|
454
454
|
export { ConnectClient } from "../server/client.js";
|
|
455
|
+
export { verifyConnectTask, createConnectTaskHandler, ConnectTaskVerificationError } from "../server/tasks.js";
|
|
@@ -22,6 +22,7 @@ import { SessionsResource } from "./resources/sessions.js";
|
|
|
22
22
|
import { PlatformResource } from "./resources/platform.js";
|
|
23
23
|
import { VisitorResource } from "./resources/visitor.js";
|
|
24
24
|
import { PushResource } from "./resources/push.js";
|
|
25
|
+
import { TasksResource } from "./resources/tasks.js";
|
|
25
26
|
import { WhatsAppResource } from "./resources/whatsapp.js";
|
|
26
27
|
export const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
|
|
27
28
|
const APPLICATION_API_PREFIX = "/api/v1/application";
|
|
@@ -126,6 +127,8 @@ export class ConnectClient {
|
|
|
126
127
|
* preferences; you supply a title, a body and a URL. See `PushResource`.
|
|
127
128
|
*/
|
|
128
129
|
push;
|
|
130
|
+
/** Background tasks on this application's declared Cloud Tasks queues - see `TasksResource`. */
|
|
131
|
+
tasks;
|
|
129
132
|
/** Your application's own approved WhatsApp templates, to a workspace's customers. */
|
|
130
133
|
whatsapp;
|
|
131
134
|
/**
|
|
@@ -174,9 +177,10 @@ export class ConnectClient {
|
|
|
174
177
|
this.visitor = new VisitorResource(transport);
|
|
175
178
|
this.platform = new PlatformResource(transport);
|
|
176
179
|
this.push = new PushResource(transport);
|
|
180
|
+
this.tasks = new TasksResource(transport);
|
|
177
181
|
this.whatsapp = new WhatsAppResource(transport);
|
|
178
182
|
this.storeRelay = (spec) => storePassthrough(transport, spec);
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
/** Kept in step with `package.json` by `sdk.test.ts`. */
|
|
182
|
-
export const SDK_VERSION = "0.
|
|
186
|
+
export const SDK_VERSION = "0.51.0";
|
package/dist/esm/server/index.js
CHANGED
|
@@ -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";
|
|
@@ -77,6 +77,27 @@ export class CallsResource {
|
|
|
77
77
|
headers: actingAs(input.asUser),
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Reads the inbound call routing flow and published agents for the workspace.
|
|
82
|
+
*/
|
|
83
|
+
getRouting(input) {
|
|
84
|
+
return request(this.transport, {
|
|
85
|
+
method: "GET",
|
|
86
|
+
path: "/calls/routing",
|
|
87
|
+
headers: actingAs(input.asUser),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sets the inbound call routing flow (smart CRM owner routing, AI agent, single number, team, fallback).
|
|
92
|
+
*/
|
|
93
|
+
setRouting(input) {
|
|
94
|
+
return request(this.transport, {
|
|
95
|
+
method: "PUT",
|
|
96
|
+
path: "/calls/routing",
|
|
97
|
+
json: input.routing,
|
|
98
|
+
headers: actingAs(input.asUser),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
80
101
|
/**
|
|
81
102
|
* Whether the workspace records its member calls, and whether this member
|
|
82
103
|
* may change that. The same switch as Connect's Calling screen.
|
|
@@ -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.
|
|
147
|
+
export declare const SDK_VERSION = "0.51.0";
|
|
@@ -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";
|
|
@@ -59,6 +59,19 @@ export declare class CallsResource {
|
|
|
59
59
|
getCallingAvailability(input: {
|
|
60
60
|
asUser: string;
|
|
61
61
|
}): Promise<CallingAvailability>;
|
|
62
|
+
/**
|
|
63
|
+
* Reads the inbound call routing flow and published agents for the workspace.
|
|
64
|
+
*/
|
|
65
|
+
getRouting(input: {
|
|
66
|
+
asUser: string;
|
|
67
|
+
}): Promise<any>;
|
|
68
|
+
/**
|
|
69
|
+
* Sets the inbound call routing flow (smart CRM owner routing, AI agent, single number, team, fallback).
|
|
70
|
+
*/
|
|
71
|
+
setRouting(input: {
|
|
72
|
+
routing: Record<string, unknown>;
|
|
73
|
+
asUser: string;
|
|
74
|
+
}): Promise<any>;
|
|
62
75
|
/**
|
|
63
76
|
* Whether the workspace records its member calls, and whether this member
|
|
64
77
|
* may change that. The same switch as Connect's Calling screen.
|
|
@@ -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>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -267,9 +267,16 @@ export interface PlanAddonConsumable {
|
|
|
267
267
|
export interface PlanAddon {
|
|
268
268
|
id: string;
|
|
269
269
|
name: string;
|
|
270
|
-
/** GST-exclusive, in paise. */
|
|
270
|
+
/** GST-exclusive, in paise. Per plan cycle, or per year when `billingCadence` is `"annual"`. */
|
|
271
271
|
price: number;
|
|
272
272
|
consumable?: PlanAddonConsumable | null;
|
|
273
|
+
/**
|
|
274
|
+
* When the add-on is billed. `"annual"` = once on the first cycle that holds
|
|
275
|
+
* it, then on each anniversary - on a monthly plan too - so render its price
|
|
276
|
+
* as "/year". `"plan"` (or absent, from a Connect that predates the field) =
|
|
277
|
+
* every plan cycle, as before.
|
|
278
|
+
*/
|
|
279
|
+
billingCadence?: "plan" | "annual" | null;
|
|
273
280
|
}
|
|
274
281
|
/**
|
|
275
282
|
* A plan feature with its label, as the **catalogue** read reports it.
|
|
@@ -3237,3 +3244,43 @@ export interface RegisterPushDeviceResponse {
|
|
|
3237
3244
|
/** Hand this to `revokeDevice` to turn the same browser off again. */
|
|
3238
3245
|
id: string;
|
|
3239
3246
|
}
|
|
3247
|
+
/**
|
|
3248
|
+
* One task, onto one of this application's declared task queues, for one
|
|
3249
|
+
* workspace it serves. Hand-written like every wire type here;
|
|
3250
|
+
* `conformance.test.ts` holds it to `EnqueueApplicationTaskInputSchema`.
|
|
3251
|
+
*/
|
|
3252
|
+
export interface EnqueueTaskInput {
|
|
3253
|
+
workspaceId: string;
|
|
3254
|
+
/** A queue declared for this application in Console. */
|
|
3255
|
+
queue: string;
|
|
3256
|
+
/** Any JSON. Delivered back as `payload`. Keep it to ids - 100 KB at most. */
|
|
3257
|
+
payload?: unknown;
|
|
3258
|
+
/**
|
|
3259
|
+
* A dedup key. A second enqueue with the same name for the same workspace
|
|
3260
|
+
* answers `duplicate` and queues nothing - name a self-chaining step after
|
|
3261
|
+
* the step (`campaign-42-page-3`) so two runners cannot both queue it.
|
|
3262
|
+
*/
|
|
3263
|
+
name?: string;
|
|
3264
|
+
/** ISO-8601, at most 30 days ahead. Omit to run as soon as the queue's rate allows. */
|
|
3265
|
+
scheduleAt?: string;
|
|
3266
|
+
}
|
|
3267
|
+
export interface EnqueueTaskResponse {
|
|
3268
|
+
/**
|
|
3269
|
+
* `unavailable` means this Connect deployment has no task queue (local
|
|
3270
|
+
* development). Nothing was queued; fall back to whatever you would do
|
|
3271
|
+
* without one.
|
|
3272
|
+
*/
|
|
3273
|
+
status: "queued" | "duplicate" | "unavailable";
|
|
3274
|
+
scheduleAt: string | null;
|
|
3275
|
+
}
|
|
3276
|
+
/** What Cloud Tasks POSTs to a queue's URL - and what `verifyConnectTask` returns. */
|
|
3277
|
+
export interface ConnectTaskDelivery<TPayload = unknown> {
|
|
3278
|
+
/** The workspace the enqueue was gated on - trust this, not anything in `payload`. */
|
|
3279
|
+
workspaceId: string;
|
|
3280
|
+
queue: string;
|
|
3281
|
+
payload: TPayload;
|
|
3282
|
+
/** From Cloud Tasks' own headers: 0 on the first attempt. */
|
|
3283
|
+
retryCount: number;
|
|
3284
|
+
/** Cloud Tasks' task id, stable across retries. */
|
|
3285
|
+
taskName: string | null;
|
|
3286
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmoops/connect-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.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",
|