@cosmoops/connect-sdk 0.19.0 → 0.21.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 +158 -0
- package/README.md +16 -2
- package/dist/cjs/client/components/apps-grid.js +112 -23
- package/dist/cjs/client/components/identity.js +24 -0
- package/dist/cjs/client/components/one-tap-state.js +96 -0
- package/dist/cjs/client/components/one-tap.js +106 -0
- package/dist/cjs/client/components/plan-pricing.js +16 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/subscribe-dialog.js +2 -1
- package/dist/cjs/client/components/user-menu.js +2 -6
- package/dist/cjs/client/index.js +11 -3
- package/dist/cjs/client/provider.js +88 -0
- package/dist/cjs/server/client.js +9 -1
- package/dist/cjs/server/handler.js +126 -2
- package/dist/cjs/server/resources/calls.js +57 -0
- package/dist/cjs/server/resources/channels.js +115 -0
- package/dist/cjs/server/resources/identity.js +40 -0
- package/dist/esm/client/components/apps-grid.js +115 -26
- package/dist/esm/client/components/identity.js +21 -0
- package/dist/esm/client/components/one-tap-state.js +90 -0
- package/dist/esm/client/components/one-tap.js +103 -0
- package/dist/esm/client/components/plan-pricing.js +15 -0
- package/dist/esm/client/components/plans.js +3 -3
- package/dist/esm/client/components/subscribe-dialog.js +3 -2
- package/dist/esm/client/components/user-menu.js +1 -5
- package/dist/esm/client/index.js +7 -0
- package/dist/esm/client/provider.js +88 -0
- package/dist/esm/server/client.js +9 -1
- package/dist/esm/server/handler.js +127 -3
- package/dist/esm/server/resources/calls.js +53 -0
- package/dist/esm/server/resources/channels.js +111 -0
- package/dist/esm/server/resources/identity.js +39 -0
- package/dist/types/client/components/apps-grid.d.ts +14 -7
- package/dist/types/client/components/identity.d.ts +17 -0
- package/dist/types/client/components/one-tap-state.d.ts +39 -0
- package/dist/types/client/components/one-tap.d.ts +50 -0
- package/dist/types/client/components/plan-pricing.d.ts +13 -0
- package/dist/types/client/index.d.ts +4 -2
- package/dist/types/client/provider.d.ts +81 -0
- package/dist/types/server/client.d.ts +7 -1
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/resources/calls.d.ts +45 -0
- package/dist/types/server/resources/channels.d.ts +91 -0
- package/dist/types/server/resources/identity.d.ts +35 -0
- package/dist/types/types.d.ts +186 -1
- package/package.json +1 -1
|
@@ -126,7 +126,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
126
126
|
(tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
|
|
127
127
|
const isCurrent = action === "current";
|
|
128
128
|
const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
|
|
129
|
-
return ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "popLayout", initial: false, children: (0, jsx_runtime_1.
|
|
129
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "popLayout", initial: false, children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: (0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: "Free" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: (0, plan_pricing_js_1.formatPrice)(plan.price) }), (0, jsx_runtime_1.jsx)("span", { className: "text-sm text-muted", children: (0, plan_pricing_js_1.pricePerLabel)(plan.billingInterval) })] })) }, plan.billingInterval) }), !(0, plan_pricing_js_1.isFreePlan)(plan.price) && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] })), cadenceUnavailable && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).adverb, "."] }))] }), plan.trialDays && !(0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
|
|
130
130
|
// `name`, not `key` - the key is an identifier
|
|
131
131
|
// ("campaign-module"), and printing it here is what this
|
|
132
132
|
// card used to do. The description rides along as a
|
|
@@ -50,7 +50,8 @@ function ConnectSubscribeDialog({ plan, action, current, coupon, onClose, onConf
|
|
|
50
50
|
// member and the one most in need of it: the two prices differ by an order
|
|
51
51
|
// of magnitude, and showing only the new one reads as a price rise.
|
|
52
52
|
const isChange = action === "upgrade" || action === "downgrade" || isIntervalSwitch;
|
|
53
|
-
return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, { onPress: onClose }), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: HEADING[action] }) }), plan && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-muted", children: currentPlan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(currentPlan.price), "/", (0, plan_pricing_js_1.intervalLabel)(currentPlan.billingInterval).short] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate font-medium text-foreground", children: plan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(plan.price), "/", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).short] })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: plan.description })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["GST (", plan.gstRate, "%)"] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate) - plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Per ", cadenceOf(plan)] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Works out to ", (0, plan_pricing_js_1.formatPrice)((0, plan_pricing_js_1.monthlyOf)(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this application before."] })) : null, coupon?.trim() ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Coupon ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1", children: [action === "subscribe" &&
|
|
53
|
+
return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, { onPress: onClose }), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: HEADING[action] }) }), plan && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-muted", children: currentPlan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(currentPlan.price), "/", (0, plan_pricing_js_1.intervalLabel)(currentPlan.billingInterval).short] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate font-medium text-foreground", children: plan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(plan.price), "/", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).short] })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: plan.description })] })), (0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-3 text-sm font-semibold text-foreground", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: "Free" })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["GST (", plan.gstRate, "%)"] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate) - plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Per ", cadenceOf(plan)] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Works out to ", (0, plan_pricing_js_1.formatPrice)((0, plan_pricing_js_1.monthlyOf)(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] })), plan.trialDays && !(0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this application before."] })) : null, coupon?.trim() ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Coupon ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1", children: [action === "subscribe" &&
|
|
54
|
+
((0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsx)("p", { children: "This plan is free. Connect activates it straight away - there is no payment to make, no card or UPI mandate to set up, and no invoice to expect. This page updates by itself once it is done." })) : ((0, jsx_runtime_1.jsx)("p", { children: "Payment is completed on Connect, where your card or UPI mandate is set up and the tax invoice is issued. This page updates by itself once it is done." }))), action === "upgrade" && ((0, jsx_runtime_1.jsxs)("p", { children: ["Connect prices the difference for the days left in your current cycle and collects just that amount now. From the next cycle you are billed the ", plan.name, " rate."] })), action === "downgrade" && ((0, jsx_runtime_1.jsx)("p", { children: "You keep what you have paid for. Connect works out the unused part of this cycle and applies it as credit or a refund, depending on how this application is configured." })), isIntervalSwitch && (
|
|
54
55
|
// Not a proration. The cycle you are on ends, so this is
|
|
55
56
|
// the one action where the amount collected can exceed a
|
|
56
57
|
// single cycle's list price - or be nothing at all, with
|
|
@@ -14,6 +14,7 @@ const react_1 = require("@heroui/react");
|
|
|
14
14
|
const lucide_react_1 = require("lucide-react");
|
|
15
15
|
const provider_js_1 = require("../provider.js");
|
|
16
16
|
const mark_js_1 = require("../mark.js");
|
|
17
|
+
const identity_js_1 = require("./identity.js");
|
|
17
18
|
const account_sessions_js_1 = require("./account-sessions.js");
|
|
18
19
|
const motion_js_1 = require("./motion.js");
|
|
19
20
|
const shell_context_js_1 = require("./shell-context.js");
|
|
@@ -23,11 +24,6 @@ const THEME_OPTIONS = [
|
|
|
23
24
|
{ value: "dark", label: "Dark", icon: lucide_react_1.Moon },
|
|
24
25
|
{ value: "system", label: "System", icon: lucide_react_1.Laptop },
|
|
25
26
|
];
|
|
26
|
-
function initials(name, email) {
|
|
27
|
-
const source = name?.trim() || email?.trim() || "";
|
|
28
|
-
const parts = source.split(/[\s@._-]+/).filter(Boolean);
|
|
29
|
-
return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
|
|
30
|
-
}
|
|
31
27
|
function openExternal(url) {
|
|
32
28
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
33
29
|
}
|
|
@@ -73,7 +69,7 @@ function UserAvatar({ user, size, }) {
|
|
|
73
69
|
// squircle sitting beside circular status dots reads as a rendering fault
|
|
74
70
|
// rather than a choice. The root is `overflow-hidden`, so rounding it here
|
|
75
71
|
// rounds the image and the fallback with it.
|
|
76
|
-
(0, jsx_runtime_1.jsxs)(react_1.Avatar, { size: size, color: "accent", variant: "soft", className: "rounded-full", children: [user.photoUrl && (0, jsx_runtime_1.jsx)(react_1.Avatar.Image, { alt: "", src: user.photoUrl }), (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: initials(user.displayName, user.email) })] }));
|
|
72
|
+
(0, jsx_runtime_1.jsxs)(react_1.Avatar, { size: size, color: "accent", variant: "soft", className: "rounded-full", children: [user.photoUrl && (0, jsx_runtime_1.jsx)(react_1.Avatar.Image, { alt: "", src: user.photoUrl }), (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: (0, identity_js_1.initials)(user.displayName, user.email) })] }));
|
|
77
73
|
}
|
|
78
74
|
/** `dispatch_admin` / `billing-viewer` -> `Dispatch admin`. */
|
|
79
75
|
function humanizeRole(role) {
|
package/dist/cjs/client/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.
|
|
5
|
-
exports.
|
|
6
|
-
exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useBookableEventTypes = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.ACTION_LABEL = exports.discountLabel = void 0;
|
|
4
|
+
exports.flattenNav = exports.normalizeNav = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
|
|
5
|
+
exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = void 0;
|
|
6
|
+
exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useBookableEventTypes = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Browser half of `@cosmoops/connect-sdk` - React components and hooks.
|
|
9
9
|
*
|
|
@@ -50,6 +50,8 @@ Object.defineProperty(exports, "ConnectLogo", { enumerable: true, get: function
|
|
|
50
50
|
var auth_buttons_js_1 = require("./components/auth-buttons.js");
|
|
51
51
|
Object.defineProperty(exports, "ConnectLoginButton", { enumerable: true, get: function () { return auth_buttons_js_1.ConnectLoginButton; } });
|
|
52
52
|
Object.defineProperty(exports, "ConnectRegisterButton", { enumerable: true, get: function () { return auth_buttons_js_1.ConnectRegisterButton; } });
|
|
53
|
+
var one_tap_js_1 = require("./components/one-tap.js");
|
|
54
|
+
Object.defineProperty(exports, "ConnectOneTap", { enumerable: true, get: function () { return one_tap_js_1.ConnectOneTap; } });
|
|
53
55
|
var user_menu_js_1 = require("./components/user-menu.js");
|
|
54
56
|
Object.defineProperty(exports, "ConnectUserButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectUserButton; } });
|
|
55
57
|
Object.defineProperty(exports, "ConnectProfileModal", { enumerable: true, get: function () { return user_menu_js_1.ConnectProfileModal; } });
|
|
@@ -211,6 +213,12 @@ Object.defineProperty(exports, "resolveAction", { enumerable: true, get: functio
|
|
|
211
213
|
Object.defineProperty(exports, "annualSaving", { enumerable: true, get: function () { return plan_pricing_js_1.annualSaving; } });
|
|
212
214
|
Object.defineProperty(exports, "discountLabel", { enumerable: true, get: function () { return plan_pricing_js_1.discountLabel; } });
|
|
213
215
|
Object.defineProperty(exports, "ACTION_LABEL", { enumerable: true, get: function () { return plan_pricing_js_1.ACTION_LABEL; } });
|
|
216
|
+
// Exported for the same reason `groupPlansByTier` is: a partner writing its
|
|
217
|
+
// own pricing grid has to draw a ₹0 plan as "Free" rather than as a price
|
|
218
|
+
// with a cadence and a GST rate attached, and re-deriving "is this free"
|
|
219
|
+
// from a raw `price === 0` misses the add-on half of the rule Connect
|
|
220
|
+
// actually applies.
|
|
221
|
+
Object.defineProperty(exports, "isFreePlan", { enumerable: true, get: function () { return plan_pricing_js_1.isFreePlan; } });
|
|
214
222
|
// The tier model, exported because a partner writing its own pricing grid
|
|
215
223
|
// needs the same pairing `ConnectPlans` uses. Grouping a flat plan list by
|
|
216
224
|
// hand means matching on `name`, which Connect neither enforces unique nor
|
|
@@ -20,6 +20,7 @@ exports.useConnectSessions = useConnectSessions;
|
|
|
20
20
|
exports.useConnectResource = useConnectResource;
|
|
21
21
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
22
22
|
const react_1 = require("react");
|
|
23
|
+
const one_tap_state_js_1 = require("./components/one-tap-state.js");
|
|
23
24
|
exports.CONNECT_APP_URL = "https://connect.cosmoops.com";
|
|
24
25
|
const ConnectContext = (0, react_1.createContext)(null);
|
|
25
26
|
/** Marks that this tab has already spent its one silent attempt - see `ConnectProviderProps.silentSignIn`. */
|
|
@@ -204,8 +205,91 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
204
205
|
credentials: "same-origin",
|
|
205
206
|
}).catch(() => { });
|
|
206
207
|
cache.current.clear();
|
|
208
|
+
// Someone who just signed out must not be offered the same account back on
|
|
209
|
+
// the next render - see `suppressOneTapAfterSignOut`.
|
|
210
|
+
(0, one_tap_state_js_1.suppressOneTapAfterSignOut)();
|
|
207
211
|
setResult({ session: { user: null, subscription: null, role: null, permissions: [], staff: false, audience: null }, error: null });
|
|
208
212
|
}, [basePath, fetchImpl]);
|
|
213
|
+
/**
|
|
214
|
+
* Where Connect answers One Tap, taken from the session read rather than
|
|
215
|
+
* assembled here - the bridge knows the `client_id` and the browser need not.
|
|
216
|
+
*/
|
|
217
|
+
const oneTapEndpoints = result?.session.oneTap ?? null;
|
|
218
|
+
const readOneTapHint = (0, react_1.useCallback)(async () => {
|
|
219
|
+
if (!oneTapEndpoints)
|
|
220
|
+
return null;
|
|
221
|
+
try {
|
|
222
|
+
// `credentials: "include"` because this is the one request in the SDK
|
|
223
|
+
// that leaves the application's own origin. It carries the visitor's
|
|
224
|
+
// Connect cookie only when the two are same-site; everywhere else it is
|
|
225
|
+
// simply sent without one and comes back `signed_out`.
|
|
226
|
+
const response = await (fetchImpl ?? globalThis.fetch)(oneTapEndpoints.hintUrl, {
|
|
227
|
+
credentials: "include",
|
|
228
|
+
headers: { accept: "application/json" },
|
|
229
|
+
});
|
|
230
|
+
if (!response.ok)
|
|
231
|
+
return null;
|
|
232
|
+
return (await response.json());
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Offline, blocked, or Connect is down. None of those is evidence about
|
|
236
|
+
// whether this visitor has a session, so the honest answer is silence.
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}, [fetchImpl, oneTapEndpoints]);
|
|
240
|
+
const oneTapSignIn = (0, react_1.useCallback)(async () => {
|
|
241
|
+
if (!oneTapEndpoints)
|
|
242
|
+
return { ok: false, reason: "unavailable" };
|
|
243
|
+
try {
|
|
244
|
+
// 1. The bridge mints the PKCE pair and keeps the verifier in an
|
|
245
|
+
// `HttpOnly` cookie on this origin.
|
|
246
|
+
const start = await mutateResource("/one-tap/start");
|
|
247
|
+
// 2. Connect mints a code against that challenge. The custom header is
|
|
248
|
+
// what forces the preflight, so an origin this client has not
|
|
249
|
+
// registered is refused before the request is ever sent.
|
|
250
|
+
const authorize = await (fetchImpl ?? globalThis.fetch)(oneTapEndpoints.authorizeUrl, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
credentials: "include",
|
|
253
|
+
headers: {
|
|
254
|
+
accept: "application/json",
|
|
255
|
+
"content-type": "application/json",
|
|
256
|
+
"x-connect-one-tap": "1",
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
redirect_uri: start.redirectUri,
|
|
260
|
+
code_challenge: start.codeChallenge,
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
const body = (await authorize.json().catch(() => null));
|
|
264
|
+
if (!authorize.ok || !body?.code) {
|
|
265
|
+
// Connect names the reason, and each one has a different next step -
|
|
266
|
+
// `consent_required` wants the interactive screen, `rate_limited` wants
|
|
267
|
+
// to be left alone. Anything unrecognised is reported as a plain
|
|
268
|
+
// failure rather than guessed at.
|
|
269
|
+
const reason = body?.error;
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
reason: reason === "login_required" ||
|
|
273
|
+
reason === "consent_required" ||
|
|
274
|
+
reason === "interaction_required" ||
|
|
275
|
+
reason === "rate_limited"
|
|
276
|
+
? reason
|
|
277
|
+
: "failed",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// 3. The bridge exchanges the code with the verifier the page never saw,
|
|
281
|
+
// and stores the session.
|
|
282
|
+
await mutateResource("/one-tap/complete", { code: body.code });
|
|
283
|
+
// The prompt was accepted, so whatever backoff earlier dismissals had
|
|
284
|
+
// accumulated is spent - it described reluctance that no longer holds.
|
|
285
|
+
(0, one_tap_state_js_1.clearOneTapSuppression)();
|
|
286
|
+
await refresh();
|
|
287
|
+
return { ok: true };
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return { ok: false, reason: "failed" };
|
|
291
|
+
}
|
|
292
|
+
}, [fetchImpl, mutateResource, oneTapEndpoints, refresh]);
|
|
209
293
|
const value = (0, react_1.useMemo)(() => ({
|
|
210
294
|
clientId,
|
|
211
295
|
appUrl,
|
|
@@ -219,6 +303,8 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
219
303
|
mutateResource,
|
|
220
304
|
invalidateResource,
|
|
221
305
|
streamResource,
|
|
306
|
+
readOneTapHint,
|
|
307
|
+
oneTapSignIn,
|
|
222
308
|
}), [
|
|
223
309
|
clientId,
|
|
224
310
|
appUrl,
|
|
@@ -232,6 +318,8 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
232
318
|
mutateResource,
|
|
233
319
|
invalidateResource,
|
|
234
320
|
streamResource,
|
|
321
|
+
readOneTapHint,
|
|
322
|
+
oneTapSignIn,
|
|
235
323
|
]);
|
|
236
324
|
return (0, jsx_runtime_1.jsx)(ConnectContext.Provider, { value: value, children: children });
|
|
237
325
|
}
|
|
@@ -8,6 +8,8 @@ const data_js_1 = require("./resources/data.js");
|
|
|
8
8
|
const workspaces_js_1 = require("./resources/workspaces.js");
|
|
9
9
|
const scheduling_js_1 = require("./resources/scheduling.js");
|
|
10
10
|
const contacts_js_1 = require("./resources/contacts.js");
|
|
11
|
+
const channels_js_1 = require("./resources/channels.js");
|
|
12
|
+
const calls_js_1 = require("./resources/calls.js");
|
|
11
13
|
const kv_js_1 = require("./resources/kv.js");
|
|
12
14
|
const billing_js_1 = require("./resources/billing.js");
|
|
13
15
|
const drive_js_1 = require("./resources/drive.js");
|
|
@@ -85,6 +87,10 @@ class ConnectClient {
|
|
|
85
87
|
scheduling;
|
|
86
88
|
/** Who a workspace already knows how to reach, by phone or by id. */
|
|
87
89
|
contacts;
|
|
90
|
+
/** The phone numbers Connect has assigned to your application, and where each one routes. */
|
|
91
|
+
channels;
|
|
92
|
+
/** The calls that arrived on those channels, with their transcripts. */
|
|
93
|
+
calls;
|
|
88
94
|
/** RAG chat and structured extraction. */
|
|
89
95
|
ai;
|
|
90
96
|
/** Your application's own document store - see `DataResource`. */
|
|
@@ -135,6 +141,8 @@ class ConnectClient {
|
|
|
135
141
|
this.drive = new drive_js_1.DriveResource(transport);
|
|
136
142
|
this.scheduling = new scheduling_js_1.SchedulingResource(transport);
|
|
137
143
|
this.contacts = new contacts_js_1.ContactsResource(transport);
|
|
144
|
+
this.channels = new channels_js_1.ChannelsResource(transport);
|
|
145
|
+
this.calls = new calls_js_1.CallsResource(transport);
|
|
138
146
|
this.ai = new ai_js_1.AiResource(transport);
|
|
139
147
|
this.data = new data_js_1.DataResource(transport);
|
|
140
148
|
this.kv = new kv_js_1.KvResource(transport);
|
|
@@ -150,4 +158,4 @@ class ConnectClient {
|
|
|
150
158
|
}
|
|
151
159
|
exports.ConnectClient = ConnectClient;
|
|
152
160
|
/** Kept in step with `package.json` by `sdk.test.ts`. */
|
|
153
|
-
exports.SDK_VERSION = "0.
|
|
161
|
+
exports.SDK_VERSION = "0.21.0";
|
|
@@ -105,6 +105,7 @@ function createConnectHandler(options) {
|
|
|
105
105
|
const defaultRedirect = options.defaultRedirect ?? "/";
|
|
106
106
|
const stateCookieName = options.stateCookieName ?? "connect_oauth_state";
|
|
107
107
|
const silentCookieName = `${stateCookieName}_silent`;
|
|
108
|
+
const pkceCookieName = `${stateCookieName}_pkce`;
|
|
108
109
|
const callbackPath = `${basePath}/callback`;
|
|
109
110
|
/**
|
|
110
111
|
* Short-lived, httpOnly, and `SameSite=Lax` - `Strict` would not be sent on
|
|
@@ -151,6 +152,35 @@ function createConnectHandler(options) {
|
|
|
151
152
|
.filter(Boolean)
|
|
152
153
|
.join("; ");
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Holds the One Tap PKCE **verifier** for the few seconds between the tap and
|
|
157
|
+
* the exchange.
|
|
158
|
+
*
|
|
159
|
+
* This is the cookie the whole no-navigation flow rests on. Connect hands the
|
|
160
|
+
* authorization code to the *page*, so the code alone must not be enough to
|
|
161
|
+
* mint a session - the verifier is what completes it, and it never leaves
|
|
162
|
+
* this origin's cookie jar. `SameSite=Lax` is sufficient and correct: unlike
|
|
163
|
+
* the state cookie there is no cross-site redirect that has to carry it, and
|
|
164
|
+
* `Lax` additionally means a cross-site `POST` to `/one-tap/complete` arrives
|
|
165
|
+
* without it and simply fails.
|
|
166
|
+
*
|
|
167
|
+
* Five minutes, not the state cookie's fifteen: it is minted on the tap and
|
|
168
|
+
* spent on the next request, so a long window is only a longer chance for a
|
|
169
|
+
* stale one to be lying around.
|
|
170
|
+
*/
|
|
171
|
+
function pkceCookie(value, secure) {
|
|
172
|
+
const cleared = value === "";
|
|
173
|
+
return [
|
|
174
|
+
`${pkceCookieName}=${encodeURIComponent(value)}`,
|
|
175
|
+
`Path=${basePath}`,
|
|
176
|
+
"HttpOnly",
|
|
177
|
+
"SameSite=Lax",
|
|
178
|
+
secure ? "Secure" : "",
|
|
179
|
+
cleared ? "Max-Age=0" : "Max-Age=300",
|
|
180
|
+
]
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.join("; ");
|
|
183
|
+
}
|
|
154
184
|
async function requireToken(request) {
|
|
155
185
|
const token = await session.read(request);
|
|
156
186
|
if (!token) {
|
|
@@ -258,6 +288,14 @@ function createConnectHandler(options) {
|
|
|
258
288
|
},
|
|
259
289
|
});
|
|
260
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Where the browser asks Connect about the visitor, and where it asks for a
|
|
293
|
+
* code once they tap - or `null` when this deployment cannot run One Tap at
|
|
294
|
+
* all (no `clientId`, or a session store that cannot `write`, in which case
|
|
295
|
+
* `/one-tap/complete` does not exist and offering the prompt would strand
|
|
296
|
+
* anyone who used it).
|
|
297
|
+
*/
|
|
298
|
+
const oneTapEndpoints = session.write ? client.identity.oneTapEndpoints() : null;
|
|
261
299
|
const routes = {
|
|
262
300
|
"GET /session": async (request) => {
|
|
263
301
|
const token = await session.read(request);
|
|
@@ -267,9 +305,21 @@ function createConnectHandler(options) {
|
|
|
267
305
|
// the client never has to tell "signed out" from "signed in with no
|
|
268
306
|
// role" by which keys happen to be present.
|
|
269
307
|
if (!token) {
|
|
270
|
-
return json({
|
|
308
|
+
return json({
|
|
309
|
+
user: null,
|
|
310
|
+
subscription: null,
|
|
311
|
+
role: null,
|
|
312
|
+
permissions: [],
|
|
313
|
+
staff: false,
|
|
314
|
+
// Carried on the read the provider already makes on mount, so One Tap
|
|
315
|
+
// costs no extra round trip to discover where to ask. Present on both
|
|
316
|
+
// branches even though only the signed-out one can use it: a response
|
|
317
|
+
// whose shape depends on the answer is a response consumers start
|
|
318
|
+
// testing for key presence instead of value.
|
|
319
|
+
oneTap: oneTapEndpoints,
|
|
320
|
+
});
|
|
271
321
|
}
|
|
272
|
-
return json(await client.identity.getUser(token));
|
|
322
|
+
return json({ ...(await client.identity.getUser(token)), oneTap: oneTapEndpoints });
|
|
273
323
|
},
|
|
274
324
|
"GET /entitlements": async (request) => {
|
|
275
325
|
const { workspaceId } = await requireWorkspace(request);
|
|
@@ -704,6 +754,74 @@ function createConnectHandler(options) {
|
|
|
704
754
|
await session.write(request, response, result.sessionToken);
|
|
705
755
|
return response;
|
|
706
756
|
},
|
|
757
|
+
/**
|
|
758
|
+
* One Tap, step one: mint the PKCE pair, keep the verifier here, hand the
|
|
759
|
+
* challenge to the page.
|
|
760
|
+
*
|
|
761
|
+
* Called on the *tap*, not on mount, so the five-minute verifier is minted
|
|
762
|
+
* seconds before it is spent rather than sitting in a cookie for as long as
|
|
763
|
+
* someone leaves the prompt on screen.
|
|
764
|
+
*
|
|
765
|
+
* A `POST` despite reading nothing: it sets a cookie, and a `GET` that
|
|
766
|
+
* changes state is both wrong and prefetchable - a link prefetcher or a
|
|
767
|
+
* scanner would silently rotate the verifier out from under an open prompt.
|
|
768
|
+
*/
|
|
769
|
+
"POST /one-tap/start": async (request) => {
|
|
770
|
+
const url = new URL(request.url);
|
|
771
|
+
const { verifier, challenge } = await (0, identity_js_1.createPkcePair)();
|
|
772
|
+
const response = json({
|
|
773
|
+
codeChallenge: challenge,
|
|
774
|
+
// The already-registered callback URI. One Tap never navigates to it -
|
|
775
|
+
// it is the value the code is *bound* to, and it must be byte-identical
|
|
776
|
+
// at `/one-tap/complete`, so both ends derive it the same way the
|
|
777
|
+
// redirect flow does rather than accepting it from the page.
|
|
778
|
+
redirectUri: `${url.origin}${callbackPath}`,
|
|
779
|
+
});
|
|
780
|
+
response.headers.append("set-cookie", pkceCookie(verifier, url.protocol === "https:"));
|
|
781
|
+
return response;
|
|
782
|
+
},
|
|
783
|
+
/**
|
|
784
|
+
* One Tap, step two: trade the page's code for a session, using the
|
|
785
|
+
* verifier the page never saw.
|
|
786
|
+
*
|
|
787
|
+
* This is `GET /callback`'s body without the navigation - same exchange,
|
|
788
|
+
* same verify-before-store ordering, same session store. The verifier is
|
|
789
|
+
* what makes the difference: a code lifted out of the browser cannot be
|
|
790
|
+
* redeemed anywhere else, because the exchange is refused without it.
|
|
791
|
+
*/
|
|
792
|
+
"POST /one-tap/complete": async (request) => {
|
|
793
|
+
const url = new URL(request.url);
|
|
794
|
+
const secure = url.protocol === "https:";
|
|
795
|
+
const clearPkce = pkceCookie("", secure);
|
|
796
|
+
const verifier = readCookie(request, pkceCookieName);
|
|
797
|
+
if (!verifier) {
|
|
798
|
+
// The prompt sat unanswered past the cookie's life, or a cross-site
|
|
799
|
+
// `POST` arrived without it. Either way there is nothing to complete,
|
|
800
|
+
// and the client falls back to the interactive flow.
|
|
801
|
+
const expired = json({ error: "expired" }, 400);
|
|
802
|
+
expired.headers.append("set-cookie", clearPkce);
|
|
803
|
+
return expired;
|
|
804
|
+
}
|
|
805
|
+
const body = (await request.json().catch(() => null));
|
|
806
|
+
const code = typeof body?.code === "string" ? body.code : null;
|
|
807
|
+
if (!code) {
|
|
808
|
+
const invalid = json({ error: "invalid_request" }, 400);
|
|
809
|
+
invalid.headers.append("set-cookie", clearPkce);
|
|
810
|
+
return invalid;
|
|
811
|
+
}
|
|
812
|
+
const result = await client.identity.exchangeCode({
|
|
813
|
+
code,
|
|
814
|
+
redirectUri: `${url.origin}${callbackPath}`,
|
|
815
|
+
codeVerifier: verifier,
|
|
816
|
+
});
|
|
817
|
+
// Verified before the session is stored, exactly as the callback does -
|
|
818
|
+
// an unverifiable token must never become a signed-in user.
|
|
819
|
+
await client.identity.verifyIdToken(result.idToken);
|
|
820
|
+
const response = json({ ok: true });
|
|
821
|
+
response.headers.append("set-cookie", clearPkce);
|
|
822
|
+
await session.write(request, response, result.sessionToken);
|
|
823
|
+
return response;
|
|
824
|
+
},
|
|
707
825
|
"POST /logout": async (request) => {
|
|
708
826
|
const token = await session.read(request);
|
|
709
827
|
const response = json({ ok: true });
|
|
@@ -722,6 +840,12 @@ function createConnectHandler(options) {
|
|
|
722
840
|
if (!session.write) {
|
|
723
841
|
delete routes["GET /signin"];
|
|
724
842
|
delete routes["GET /callback"];
|
|
843
|
+
// Same reason, and `oneTapEndpoints` is already `null` above so the client
|
|
844
|
+
// never offers the prompt in the first place - this is the second half of
|
|
845
|
+
// that, so a hand-written call cannot reach a route that would throw on
|
|
846
|
+
// `session.write!`.
|
|
847
|
+
delete routes["POST /one-tap/start"];
|
|
848
|
+
delete routes["POST /one-tap/complete"];
|
|
725
849
|
// A switch rotates the session token. Without somewhere to persist the new
|
|
726
850
|
// one, the old one would be revoked and the user silently signed out - so
|
|
727
851
|
// the route does not exist rather than working once and breaking.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CallsResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/**
|
|
6
|
+
* Every call that arrived on a channel assigned to your application.
|
|
7
|
+
*
|
|
8
|
+
* The application is resolved from your API key and is never a parameter, so
|
|
9
|
+
* there is no request that reads another partner's calls.
|
|
10
|
+
*
|
|
11
|
+
* `channelId` is the only filter, and that is a budget decision rather than an
|
|
12
|
+
* oversight: each filter costs a Firestore composite index against a project
|
|
13
|
+
* already near its ceiling. Filter the rest client-side, or read the
|
|
14
|
+
* aggregates from `analytics`.
|
|
15
|
+
*/
|
|
16
|
+
class CallsResource {
|
|
17
|
+
transport;
|
|
18
|
+
constructor(transport) {
|
|
19
|
+
this.transport = transport;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Your calls, newest first, across all of your subscribing workspaces.
|
|
23
|
+
*
|
|
24
|
+
* **Page until `hasMore` is `false`; do not stop on an empty `items`.** A
|
|
25
|
+
* page can come back empty with more behind it.
|
|
26
|
+
*/
|
|
27
|
+
list(input = {}) {
|
|
28
|
+
return (0, http_js_1.request)(this.transport, {
|
|
29
|
+
method: "GET",
|
|
30
|
+
path: "/calls",
|
|
31
|
+
query: {
|
|
32
|
+
channelId: input.channelId,
|
|
33
|
+
cursor: input.cursor,
|
|
34
|
+
limit: input.limit === undefined ? undefined : String(input.limit),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One call and every transcript turn, ordered by `sequence` - which is the
|
|
40
|
+
* sort to trust, since two turns can share a millisecond.
|
|
41
|
+
*
|
|
42
|
+
* `recordingFileId` and `transcriptFileId` are Drive file ids, readable with
|
|
43
|
+
* a `drive:read` key. Both are `null` until the carrier posts the audio,
|
|
44
|
+
* which is the normal state for a call that has only just ended - branch on
|
|
45
|
+
* `recordingStatus` rather than treating `null` as a failure.
|
|
46
|
+
*
|
|
47
|
+
* Throws `ConnectError` with `code: "not_found"` for a call that does not
|
|
48
|
+
* exist *or* belongs to another application.
|
|
49
|
+
*/
|
|
50
|
+
get(callId) {
|
|
51
|
+
return (0, http_js_1.request)(this.transport, {
|
|
52
|
+
method: "GET",
|
|
53
|
+
path: `/calls/${encodeURIComponent(callId)}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.CallsResource = CallsResource;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChannelsResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/**
|
|
6
|
+
* The phone numbers Connect has assigned to your application.
|
|
7
|
+
*
|
|
8
|
+
* **Inventory is platform-owned.** A number is a regulated asset bought under
|
|
9
|
+
* Connect's carrier account and reassignable between workspaces, so your
|
|
10
|
+
* application never provisions, prices or releases one - it claims from a pool
|
|
11
|
+
* it cannot see, and reads back what it was given. That is why `claim` takes a
|
|
12
|
+
* workspace and a kind rather than a number, and why nothing here exposes a
|
|
13
|
+
* carrier, a provider reference or a monthly rate.
|
|
14
|
+
*
|
|
15
|
+
* The application is resolved from your API key and is never a parameter, so
|
|
16
|
+
* no request shape here can reach another partner's channels. A channel
|
|
17
|
+
* belonging to someone else answers `404`, not `403` - confirming it exists
|
|
18
|
+
* would make these reads an oracle for enumerating their channel ids.
|
|
19
|
+
*/
|
|
20
|
+
class ChannelsResource {
|
|
21
|
+
transport;
|
|
22
|
+
constructor(transport) {
|
|
23
|
+
this.transport = transport;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Every channel assigned to your application, across all of your subscribing
|
|
27
|
+
* workspaces.
|
|
28
|
+
*
|
|
29
|
+
* Page until `hasMore` is `false`.
|
|
30
|
+
*/
|
|
31
|
+
list(input = {}) {
|
|
32
|
+
return (0, http_js_1.request)(this.transport, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
path: "/channels",
|
|
35
|
+
query: {
|
|
36
|
+
workspaceId: input.workspaceId,
|
|
37
|
+
cursor: input.cursor,
|
|
38
|
+
limit: input.limit === undefined ? undefined : String(input.limit),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The channel behind an id you already hold - a webhook's `channelId`, or a
|
|
44
|
+
* call record's.
|
|
45
|
+
*
|
|
46
|
+
* Throws `ConnectError` with `code: "not_found"` for an id that does not
|
|
47
|
+
* exist *or* belongs to another application.
|
|
48
|
+
*/
|
|
49
|
+
get(channelId) {
|
|
50
|
+
return (0, http_js_1.request)(this.transport, {
|
|
51
|
+
method: "GET",
|
|
52
|
+
path: `/channels/${encodeURIComponent(channelId)}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Takes a number out of Connect's inventory and binds it to one of your
|
|
57
|
+
* subscribing workspaces.
|
|
58
|
+
*
|
|
59
|
+
* **You do not pick the number.** A workspace may hold exactly one, across
|
|
60
|
+
* every application it uses, and it is then fixed for the life of the
|
|
61
|
+
* engagement - there is no re-claim and no swap. Only a Connect operator can
|
|
62
|
+
* unassign one.
|
|
63
|
+
*
|
|
64
|
+
* Two different `409`s, and they need different handling: the workspace
|
|
65
|
+
* already holds a channel of this kind (fix the call), or the pool is empty
|
|
66
|
+
* right now (retry - Connect is notified, and this is not permanent).
|
|
67
|
+
*
|
|
68
|
+
* The claimed channel comes back without routing, because it has none yet.
|
|
69
|
+
* Call `setRouting` next, or the number refuses every call that reaches it.
|
|
70
|
+
*/
|
|
71
|
+
claim(input) {
|
|
72
|
+
return (0, http_js_1.request)(this.transport, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
path: "/channels/claim",
|
|
75
|
+
json: { workspaceId: input.workspaceId, kind: input.kind ?? "voice" },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Points a channel at wherever inbound conversations should go.
|
|
80
|
+
*
|
|
81
|
+
* `destination` is yours: Connect stores it verbatim and never reads into
|
|
82
|
+
* it. It **replaces** the stored record rather than merging, so send the
|
|
83
|
+
* whole destination each time; it must serialize to 16 KB or fewer.
|
|
84
|
+
*
|
|
85
|
+
* Throws `code: "conflict"` when the channel is suspended or released - use
|
|
86
|
+
* `clearRouting` to stop a suspended number answering, which is deliberately
|
|
87
|
+
* still allowed.
|
|
88
|
+
*/
|
|
89
|
+
setRouting(channelId, destination) {
|
|
90
|
+
return (0, http_js_1.request)(this.transport, {
|
|
91
|
+
method: "PUT",
|
|
92
|
+
path: `/channels/${encodeURIComponent(channelId)}/routing`,
|
|
93
|
+
json: { destination },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Takes a number out of service without giving it up - the channel stays
|
|
98
|
+
* assigned to you and stays billable, and inbound calls are refused until
|
|
99
|
+
* you write routing again.
|
|
100
|
+
*
|
|
101
|
+
* This is the only way to say "stop answering": overwriting `destination`
|
|
102
|
+
* can only ever point the number somewhere *else*, and unassigning it is a
|
|
103
|
+
* Console action that returns the number to Connect's inventory.
|
|
104
|
+
*
|
|
105
|
+
* Idempotent - clearing already-clear routing answers `200` with
|
|
106
|
+
* `routing: null`, so a retry after a lost response is safe.
|
|
107
|
+
*/
|
|
108
|
+
clearRouting(channelId) {
|
|
109
|
+
return (0, http_js_1.request)(this.transport, {
|
|
110
|
+
method: "DELETE",
|
|
111
|
+
path: `/channels/${encodeURIComponent(channelId)}/routing`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.ChannelsResource = ChannelsResource;
|