@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +16 -2
  3. package/dist/cjs/client/components/apps-grid.js +112 -23
  4. package/dist/cjs/client/components/identity.js +24 -0
  5. package/dist/cjs/client/components/one-tap-state.js +96 -0
  6. package/dist/cjs/client/components/one-tap.js +106 -0
  7. package/dist/cjs/client/components/plan-pricing.js +16 -0
  8. package/dist/cjs/client/components/plans.js +1 -1
  9. package/dist/cjs/client/components/subscribe-dialog.js +2 -1
  10. package/dist/cjs/client/components/user-menu.js +2 -6
  11. package/dist/cjs/client/index.js +11 -3
  12. package/dist/cjs/client/provider.js +88 -0
  13. package/dist/cjs/server/client.js +9 -1
  14. package/dist/cjs/server/handler.js +126 -2
  15. package/dist/cjs/server/resources/calls.js +57 -0
  16. package/dist/cjs/server/resources/channels.js +115 -0
  17. package/dist/cjs/server/resources/identity.js +40 -0
  18. package/dist/esm/client/components/apps-grid.js +115 -26
  19. package/dist/esm/client/components/identity.js +21 -0
  20. package/dist/esm/client/components/one-tap-state.js +90 -0
  21. package/dist/esm/client/components/one-tap.js +103 -0
  22. package/dist/esm/client/components/plan-pricing.js +15 -0
  23. package/dist/esm/client/components/plans.js +3 -3
  24. package/dist/esm/client/components/subscribe-dialog.js +3 -2
  25. package/dist/esm/client/components/user-menu.js +1 -5
  26. package/dist/esm/client/index.js +7 -0
  27. package/dist/esm/client/provider.js +88 -0
  28. package/dist/esm/server/client.js +9 -1
  29. package/dist/esm/server/handler.js +127 -3
  30. package/dist/esm/server/resources/calls.js +53 -0
  31. package/dist/esm/server/resources/channels.js +111 -0
  32. package/dist/esm/server/resources/identity.js +39 -0
  33. package/dist/types/client/components/apps-grid.d.ts +14 -7
  34. package/dist/types/client/components/identity.d.ts +17 -0
  35. package/dist/types/client/components/one-tap-state.d.ts +39 -0
  36. package/dist/types/client/components/one-tap.d.ts +50 -0
  37. package/dist/types/client/components/plan-pricing.d.ts +13 -0
  38. package/dist/types/client/index.d.ts +4 -2
  39. package/dist/types/client/provider.d.ts +81 -0
  40. package/dist/types/server/client.d.ts +7 -1
  41. package/dist/types/server/index.d.ts +2 -0
  42. package/dist/types/server/resources/calls.d.ts +45 -0
  43. package/dist/types/server/resources/channels.d.ts +91 -0
  44. package/dist/types/server/resources/identity.d.ts +35 -0
  45. package/dist/types/types.d.ts +186 -1
  46. package/package.json +1 -1
@@ -0,0 +1,50 @@
1
+ /**
2
+ * "Continue as …" - a signed-out visitor who is already signed into Connect is
3
+ * offered their account, and one tap signs them in **without leaving the page**.
4
+ *
5
+ * The prompt is the visible half; what makes it worth having is that finding
6
+ * out costs one `fetch` rather than a redirect. `ConnectProviderProps.silentSignIn`
7
+ * answers the same question by bouncing the browser to Connect and back, which
8
+ * means a visible full-page navigation on the first load of every signed-out
9
+ * visit, nothing shown to the visitor while it happens, and no way to say no.
10
+ *
11
+ * ```tsx
12
+ * <ConnectProvider>
13
+ * <App />
14
+ * <ConnectOneTap />
15
+ * </ConnectProvider>
16
+ * ```
17
+ *
18
+ * ### It only appears when it can actually help
19
+ *
20
+ * Nothing renders unless the visitor is signed out here, signed in to Connect,
21
+ * and has not recently dismissed the prompt or signed out of this application.
22
+ * A visitor with no Connect session sees nothing at all, ever - which is why
23
+ * this is safe to mount unconditionally, unlike `silentSignIn`, whose cost
24
+ * lands on exactly the people it cannot help.
25
+ *
26
+ * ### Two prompts, because consent is not assumed
27
+ *
28
+ * A visitor who has authorised this application before gets their name, email,
29
+ * workspace and photo, because Connect will release those to an application
30
+ * they have already agreed to. A visitor who has not gets an impersonal prompt
31
+ * and a tap that leads to the real consent screen - Connect does not hand over
32
+ * so much as a first name until that has happened once.
33
+ *
34
+ * ### Placement
35
+ *
36
+ * Bottom of the viewport on a phone, bottom-right from `sm:` up. Deliberately
37
+ * not top-right: on a phone that is where the browser chrome and the
38
+ * application's own header live, and this must never cover the thing the
39
+ * visitor came to use.
40
+ */
41
+ export interface ConnectOneTapProps {
42
+ /**
43
+ * Where to send someone whose tap needs the interactive screen - the first
44
+ * connection to this application, or an account that has an MFA or
45
+ * subscription step outstanding. Defaults to the bridge's own `/signin`.
46
+ */
47
+ onInteractiveSignIn?: () => void;
48
+ className?: string;
49
+ }
50
+ export declare function ConnectOneTap({ onInteractiveSignIn, className }: ConnectOneTapProps): import("react").JSX.Element | null;
@@ -16,6 +16,19 @@
16
16
  * and nothing in it needs to be.
17
17
  */
18
18
  import type { AppliedDiscount, Plan, SubscriptionResponse } from "../../types.js";
19
+ /**
20
+ * Whether a plan costs nothing - the SDK's copy of Connect's `isFreeSelection`
21
+ * (`packages/data/src/payments/free-plan.ts`), which is what the server
22
+ * actually decides the free-activation path with.
23
+ *
24
+ * A copy rather than an import for the reason `types.ts` gives: the published
25
+ * package carries none of Connect's internals. The rule is one line and does
26
+ * not drift, and `conformance.test.ts` pins the shape either side of it.
27
+ *
28
+ * @param addonsTotal Paise of the add-ons *selected*, not of the plan's whole
29
+ * catalog. A free plan with a paid add-on ticked is not free.
30
+ */
31
+ export declare function isFreePlan(price: number, addonsTotal?: number): boolean;
19
32
  /** Paise are the wire unit everywhere in Connect; rupees are the display unit. */
20
33
  export declare function formatPrice(paise: number): string;
21
34
  /**
@@ -11,7 +11,7 @@
11
11
  * change lands identically in all of them.
12
12
  */
13
13
  export { ConnectProvider, useConnect, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectPlatformAccess, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectCompanyProfile, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
14
- export type { ConnectProviderProps, ConnectContextValue, ConnectSession } from "./provider.js";
14
+ export type { ConnectProviderProps, ConnectContextValue, ConnectSession, ConnectOneTapHint, ConnectOneTapFailure, } from "./provider.js";
15
15
  export { blockingBlockers, normalizePlanFeatures } from "../helpers.js";
16
16
  export type { WirePlanFeature } from "../helpers.js";
17
17
  export { ConnectError, isConnectError } from "../errors.js";
@@ -23,6 +23,8 @@ export { ConnectLogo } from "./components/logo.js";
23
23
  export type { ConnectLogoProps } from "./components/logo.js";
24
24
  export { ConnectLoginButton, ConnectRegisterButton } from "./components/auth-buttons.js";
25
25
  export type { ConnectAuthButtonProps } from "./components/auth-buttons.js";
26
+ export { ConnectOneTap } from "./components/one-tap.js";
27
+ export type { ConnectOneTapProps } from "./components/one-tap.js";
26
28
  export { ConnectUserButton, ConnectProfileModal,
27
29
  /** @deprecated Renamed to `ConnectUserButton`. */
28
30
  ConnectUserMenu, ConnectManageSubscriptionButton, ConnectInvoicesButton, ConnectProfileButton, ConnectHomeButton, ConnectSignOutButton, } from "./components/user-menu.js";
@@ -82,7 +84,7 @@ export { ConnectFilterTabs } from "./components/filter-tabs.js";
82
84
  export type { ConnectFilterTabsProps, ConnectFilterTabsOption } from "./components/filter-tabs.js";
83
85
  export { useConnectCheckout, buildCheckoutUrl } from "./use-checkout.js";
84
86
  export type { ConnectCheckoutStatus, ConnectCheckoutState, ConnectEntitlements, StartCheckoutOptions, CheckoutUrlOptions, } from "./use-checkout.js";
85
- export { formatPrice, monthlyOf, resolveAction, annualSaving, discountLabel, ACTION_LABEL, groupPlansByTier, resolvePlanGroup, } from "./components/plan-pricing.js";
87
+ export { formatPrice, monthlyOf, resolveAction, annualSaving, discountLabel, ACTION_LABEL, isFreePlan, groupPlansByTier, resolvePlanGroup, } from "./components/plan-pricing.js";
86
88
  export type { PlanAction, PlanTier, ComparablePlan } from "./components/plan-pricing.js";
87
89
  export { ConnectWorkspaceSwitcher } from "./components/workspace-switcher.js";
88
90
  export type { ConnectWorkspaceSwitcherProps } from "./components/workspace-switcher.js";
@@ -20,7 +20,52 @@ export interface ConnectSession {
20
20
  * `null` on a signed-in session.
21
21
  */
22
22
  audience: SessionAudience | null;
23
+ /**
24
+ * Where the browser may ask Connect about the visitor, or `null` when this
25
+ * deployment cannot run One Tap - see `IdentityResource.oneTapEndpoints`.
26
+ *
27
+ * Delivered on the session read the provider already makes, so discovering
28
+ * One Tap costs no extra request. Optional because an older bridge will not
29
+ * send it.
30
+ */
31
+ oneTap?: {
32
+ hintUrl: string;
33
+ authorizeUrl: string;
34
+ } | null;
23
35
  }
36
+ /**
37
+ * What Connect says about the visitor, before anything has been signed.
38
+ *
39
+ * Note what is *not* here when `consented` is `false`: no name, no email, no
40
+ * photo. Connect withholds all of it until the visitor has authorised this
41
+ * application at least once, so the first prompt an application ever shows is
42
+ * necessarily the impersonal one.
43
+ */
44
+ export interface ConnectOneTapHint {
45
+ status: "signed_out" | "signed_in";
46
+ consented?: boolean;
47
+ user?: {
48
+ displayName: string | null;
49
+ email: string | null;
50
+ photoUrl: string | null;
51
+ };
52
+ workspace?: {
53
+ name: string;
54
+ } | null;
55
+ application?: {
56
+ name: string;
57
+ iconUrl: string | null;
58
+ };
59
+ }
60
+ /**
61
+ * Why a tap did not end in a session.
62
+ *
63
+ * Every one of these is ordinary traffic rather than a fault, which is why
64
+ * `oneTapSignIn` reports them as a value instead of throwing: `consent_required`
65
+ * and `interaction_required` both mean "this needs the interactive screen", and
66
+ * that is a hand-off, not an error to show anybody.
67
+ */
68
+ export type ConnectOneTapFailure = "unavailable" | "login_required" | "consent_required" | "interaction_required" | "rate_limited" | "failed";
24
69
  export interface ConnectContextValue {
25
70
  /**
26
71
  * Public OAuth client id, if `clientId` was passed to `<ConnectProvider>` -
@@ -89,6 +134,42 @@ export interface ConnectContextValue {
89
134
  streamResource(path: string, body: unknown, options?: {
90
135
  signal?: AbortSignal;
91
136
  }): Promise<Response>;
137
+ /**
138
+ * Asks Connect whether this visitor is already signed in there, without
139
+ * navigating anywhere.
140
+ *
141
+ * This is the read `<ConnectOneTap>` is built on, and the reason One Tap
142
+ * beats `silentSignIn`: one credentialed `fetch` answers the question that
143
+ * used to cost a full-page redirect to Connect and back.
144
+ *
145
+ * It only works when the application is served from the **same site** as
146
+ * Connect - a `cosmoops.com` subdomain. `SameSite` is judged on the
147
+ * registrable domain, so the visitor's Connect session cookie rides along
148
+ * from `crm.cosmoops.com` and does not from anywhere else. Off-site
149
+ * deployments get `signed_out`, which is why every caller treats that answer
150
+ * as "say nothing" rather than "this visitor has no account".
151
+ *
152
+ * Resolves to `null` when the bridge offers no One Tap endpoints, or when the
153
+ * request itself fails - a Connect that cannot be reached says nothing about
154
+ * whether anyone is signed into it.
155
+ */
156
+ readOneTapHint(): Promise<ConnectOneTapHint | null>;
157
+ /**
158
+ * Completes a One Tap sign-in, with no navigation at any point.
159
+ *
160
+ * Three requests: the bridge mints a PKCE pair and keeps the verifier, Connect
161
+ * hands back an authorization code, the bridge exchanges it and stores the
162
+ * session. The page only ever holds the code, which is worthless without the
163
+ * verifier it never sees.
164
+ *
165
+ * Refreshes the session on success, so consumers re-render signed in.
166
+ */
167
+ oneTapSignIn(): Promise<{
168
+ ok: true;
169
+ } | {
170
+ ok: false;
171
+ reason: ConnectOneTapFailure;
172
+ }>;
92
173
  }
93
174
  export interface ConnectProviderProps {
94
175
  /**
@@ -6,6 +6,8 @@ import { DataResource } from "./resources/data.js";
6
6
  import { WorkspacesResource } from "./resources/workspaces.js";
7
7
  import { SchedulingResource } from "./resources/scheduling.js";
8
8
  import { ContactsResource } from "./resources/contacts.js";
9
+ import { ChannelsResource } from "./resources/channels.js";
10
+ import { CallsResource } from "./resources/calls.js";
9
11
  import { KvResource } from "./resources/kv.js";
10
12
  import { BillingResource } from "./resources/billing.js";
11
13
  import { DriveResource } from "./resources/drive.js";
@@ -81,6 +83,10 @@ export declare class ConnectClient {
81
83
  readonly scheduling: SchedulingResource;
82
84
  /** Who a workspace already knows how to reach, by phone or by id. */
83
85
  readonly contacts: ContactsResource;
86
+ /** The phone numbers Connect has assigned to your application, and where each one routes. */
87
+ readonly channels: ChannelsResource;
88
+ /** The calls that arrived on those channels, with their transcripts. */
89
+ readonly calls: CallsResource;
84
90
  /** RAG chat and structured extraction. */
85
91
  readonly ai: AiResource;
86
92
  /** Your application's own document store - see `DataResource`. */
@@ -115,4 +121,4 @@ export declare class ConnectClient {
115
121
  constructor(options: ConnectClientOptions);
116
122
  }
117
123
  /** Kept in step with `package.json` by `sdk.test.ts`. */
118
- export declare const SDK_VERSION = "0.19.0";
124
+ export declare const SDK_VERSION = "0.21.0";
@@ -29,6 +29,8 @@ export type { ExecuteIntegrationActionInput, IntegrationsResource } from "./reso
29
29
  export type { PlatformResource } from "./resources/platform.js";
30
30
  export type { SchedulingResource, FindSlotsInput, ListBookingsInput, CreateBookingInput, RescheduleBookingInput, } from "./resources/scheduling.js";
31
31
  export type { ContactsResource, LookupContactInput } from "./resources/contacts.js";
32
+ export type { ChannelsResource, ListChannelsInput, ClaimChannelInput, } from "./resources/channels.js";
33
+ export type { CallsResource, ListCallsInput } from "./resources/calls.js";
32
34
  /**
33
35
  * Webhook verification and a ready-made POST route. Server-only for the
34
36
  * obvious reason: the signing secret must never reach a browser bundle.
@@ -0,0 +1,45 @@
1
+ import { type TransportOptions } from "../http.js";
2
+ import type { CallDetail, CallsPage } from "../../types.js";
3
+ export interface ListCallsInput {
4
+ /** Narrow to one channel. A channel belonging to another application simply matches nothing. */
5
+ channelId?: string;
6
+ /** From a previous page's `nextCursor`. */
7
+ cursor?: string;
8
+ /** 1-100, default 50. */
9
+ limit?: number;
10
+ }
11
+ /**
12
+ * Every call that arrived on a channel assigned to your application.
13
+ *
14
+ * The application is resolved from your API key and is never a parameter, so
15
+ * there is no request that reads another partner's calls.
16
+ *
17
+ * `channelId` is the only filter, and that is a budget decision rather than an
18
+ * oversight: each filter costs a Firestore composite index against a project
19
+ * already near its ceiling. Filter the rest client-side, or read the
20
+ * aggregates from `analytics`.
21
+ */
22
+ export declare class CallsResource {
23
+ private readonly transport;
24
+ constructor(transport: TransportOptions);
25
+ /**
26
+ * Your calls, newest first, across all of your subscribing workspaces.
27
+ *
28
+ * **Page until `hasMore` is `false`; do not stop on an empty `items`.** A
29
+ * page can come back empty with more behind it.
30
+ */
31
+ list(input?: ListCallsInput): Promise<CallsPage>;
32
+ /**
33
+ * One call and every transcript turn, ordered by `sequence` - which is the
34
+ * sort to trust, since two turns can share a millisecond.
35
+ *
36
+ * `recordingFileId` and `transcriptFileId` are Drive file ids, readable with
37
+ * a `drive:read` key. Both are `null` until the carrier posts the audio,
38
+ * which is the normal state for a call that has only just ended - branch on
39
+ * `recordingStatus` rather than treating `null` as a failure.
40
+ *
41
+ * Throws `ConnectError` with `code: "not_found"` for a call that does not
42
+ * exist *or* belongs to another application.
43
+ */
44
+ get(callId: string): Promise<CallDetail>;
45
+ }
@@ -0,0 +1,91 @@
1
+ import { type TransportOptions } from "../http.js";
2
+ import type { ChannelKind, ChannelRoutingRecord, ChannelsPage, ClaimedChannel, ConnectChannel } from "../../types.js";
3
+ export interface ListChannelsInput {
4
+ /** Narrow to one workspace. Omit to list every channel assigned to your application. */
5
+ workspaceId?: string;
6
+ /** From a previous page's `nextCursor`. */
7
+ cursor?: string;
8
+ /** 1-100, default 50. */
9
+ limit?: number;
10
+ }
11
+ export interface ClaimChannelInput {
12
+ workspaceId: string;
13
+ /** Defaults to `"voice"`, the only kind Connect stocks inventory for today. */
14
+ kind?: ChannelKind;
15
+ }
16
+ /**
17
+ * The phone numbers Connect has assigned to your application.
18
+ *
19
+ * **Inventory is platform-owned.** A number is a regulated asset bought under
20
+ * Connect's carrier account and reassignable between workspaces, so your
21
+ * application never provisions, prices or releases one - it claims from a pool
22
+ * it cannot see, and reads back what it was given. That is why `claim` takes a
23
+ * workspace and a kind rather than a number, and why nothing here exposes a
24
+ * carrier, a provider reference or a monthly rate.
25
+ *
26
+ * The application is resolved from your API key and is never a parameter, so
27
+ * no request shape here can reach another partner's channels. A channel
28
+ * belonging to someone else answers `404`, not `403` - confirming it exists
29
+ * would make these reads an oracle for enumerating their channel ids.
30
+ */
31
+ export declare class ChannelsResource {
32
+ private readonly transport;
33
+ constructor(transport: TransportOptions);
34
+ /**
35
+ * Every channel assigned to your application, across all of your subscribing
36
+ * workspaces.
37
+ *
38
+ * Page until `hasMore` is `false`.
39
+ */
40
+ list(input?: ListChannelsInput): Promise<ChannelsPage>;
41
+ /**
42
+ * The channel behind an id you already hold - a webhook's `channelId`, or a
43
+ * call record's.
44
+ *
45
+ * Throws `ConnectError` with `code: "not_found"` for an id that does not
46
+ * exist *or* belongs to another application.
47
+ */
48
+ get(channelId: string): Promise<ConnectChannel>;
49
+ /**
50
+ * Takes a number out of Connect's inventory and binds it to one of your
51
+ * subscribing workspaces.
52
+ *
53
+ * **You do not pick the number.** A workspace may hold exactly one, across
54
+ * every application it uses, and it is then fixed for the life of the
55
+ * engagement - there is no re-claim and no swap. Only a Connect operator can
56
+ * unassign one.
57
+ *
58
+ * Two different `409`s, and they need different handling: the workspace
59
+ * already holds a channel of this kind (fix the call), or the pool is empty
60
+ * right now (retry - Connect is notified, and this is not permanent).
61
+ *
62
+ * The claimed channel comes back without routing, because it has none yet.
63
+ * Call `setRouting` next, or the number refuses every call that reaches it.
64
+ */
65
+ claim(input: ClaimChannelInput): Promise<ClaimedChannel>;
66
+ /**
67
+ * Points a channel at wherever inbound conversations should go.
68
+ *
69
+ * `destination` is yours: Connect stores it verbatim and never reads into
70
+ * it. It **replaces** the stored record rather than merging, so send the
71
+ * whole destination each time; it must serialize to 16 KB or fewer.
72
+ *
73
+ * Throws `code: "conflict"` when the channel is suspended or released - use
74
+ * `clearRouting` to stop a suspended number answering, which is deliberately
75
+ * still allowed.
76
+ */
77
+ setRouting(channelId: string, destination: Record<string, unknown>): Promise<ChannelRoutingRecord>;
78
+ /**
79
+ * Takes a number out of service without giving it up - the channel stays
80
+ * assigned to you and stays billable, and inbound calls are refused until
81
+ * you write routing again.
82
+ *
83
+ * This is the only way to say "stop answering": overwriting `destination`
84
+ * can only ever point the number somewhere *else*, and unassigning it is a
85
+ * Console action that returns the number to Connect's inventory.
86
+ *
87
+ * Idempotent - clearing already-clear routing answers `200` with
88
+ * `routing: null`, so a retry after a lost response is safe.
89
+ */
90
+ clearRouting(channelId: string): Promise<ChannelRoutingRecord>;
91
+ }
@@ -27,6 +27,24 @@ export interface ExchangeCodeInput {
27
27
  }
28
28
  /** A URL-safe random string, suitable for `state` or a PKCE verifier. */
29
29
  export declare function createState(byteLength?: number): string;
30
+ /** One Tap's PKCE pair - `challenge` is `BASE64URL(SHA256(verifier))`, RFC 7636 S256. */
31
+ export interface PkcePair {
32
+ verifier: string;
33
+ challenge: string;
34
+ }
35
+ /**
36
+ * Mints a PKCE pair for the One Tap flow.
37
+ *
38
+ * The redirect flow can take PKCE or leave it, because the code is delivered
39
+ * to the application's server on a redirect the browser only forwards. One Tap
40
+ * hands the code *to the page*, so the verifier is the thing that makes a
41
+ * stolen code worthless - it stays in an `HttpOnly` cookie on the application's
42
+ * own origin and is never sent to the browser.
43
+ *
44
+ * `crypto.subtle` rather than a Node hash, so this stays isomorphic and the
45
+ * `./next` entry keeps loading on the edge runtime as well as Node.
46
+ */
47
+ export declare function createPkcePair(): Promise<PkcePair>;
30
48
  /**
31
49
  * "Login with Connect" - the OAuth2 handshake, and the session that outlives
32
50
  * it.
@@ -42,6 +60,23 @@ export declare class IdentityResource {
42
60
  constructor(transport: TransportOptions, clientId: string | null, appBaseUrl: string);
43
61
  /** Step 1 - the URL to send the user's browser to. */
44
62
  authorizeUrl(input: AuthorizeUrlInput): string;
63
+ /**
64
+ * The two Connect endpoints One Tap talks to **from the browser**, or `null`
65
+ * when this client has no `clientId` configured and the flow cannot run.
66
+ *
67
+ * Handed out by the bridge rather than built in the page, so One Tap needs no
68
+ * client-side configuration at all - a `./next` application sets
69
+ * `CONNECT_CLIENT_ID` server-side and nothing else. Both URLs carry only the
70
+ * public `client_id`; neither is a credential.
71
+ *
72
+ * Returning `null` rather than throwing (as `authorizeUrl` does) because this
73
+ * is read on the `/session` route, which must keep answering for an
74
+ * application that has no OAuth client configured at all.
75
+ */
76
+ oneTapEndpoints(): {
77
+ hintUrl: string;
78
+ authorizeUrl: string;
79
+ } | null;
45
80
  /** Step 2 - trade the one-time `code` for a session. Server-side only. */
46
81
  exchangeCode(input: ExchangeCodeInput): Promise<TokenResponse>;
47
82
  /**
@@ -549,13 +549,26 @@ export interface WorkspaceApp {
549
549
  iconUrl: string | null;
550
550
  /** A `lucide-react` icon name from Connect's curated set. */
551
551
  icon: string;
552
+ /** Catalog classification, if you want to group or filter the grid by it. */
553
+ category: WorkspaceAppCategory;
552
554
  /** Where a tile navigates. `null` when the application registered no site. */
553
555
  launchUrl: string | null;
556
+ /**
557
+ * Whether this workspace holds a launchable subscription.
558
+ *
559
+ * `/apps` answers with the platform's whole active catalog, not only what
560
+ * the workspace bought, so this is the split a launcher renders as two
561
+ * groups: the apps you open, and the apps you could. `false` means the tile
562
+ * belongs in the second - send it to Connect, not to `launchUrl`.
563
+ */
564
+ subscribed: boolean;
554
565
  /** True for the application whose API key made the call - render it as the active tile. */
555
566
  isCurrent: boolean;
556
- /** Launchable but not healthy (paused, or cancelling at period end) - badge it, do not hide it. */
567
+ /** Launchable but not healthy (paused, or cancelling at period end) - badge it, do not hide it. Always `false` when `subscribed` is `false`. */
557
568
  needsAttention: boolean;
558
569
  }
570
+ /** Connect's closed set of catalog categories - `WorkspaceApp.category`. */
571
+ export type WorkspaceAppCategory = "communication" | "storage" | "analytics" | "security" | "billing" | "productivity" | "sales" | "operations" | "infrastructure" | "other";
559
572
  /**
560
573
  * The platform's DPDP grievance channel - `companyProfile.grievance`.
561
574
  *
@@ -1269,3 +1282,175 @@ export interface ConnectContact {
1269
1282
  createdAt: string;
1270
1283
  updatedAt: string;
1271
1284
  }
1285
+ /**
1286
+ * What kind of conversation a channel carries. `voice` is the only kind
1287
+ * Connect stocks inventory for today - `whatsapp` and `meeting` are modelled
1288
+ * end to end and have no runtime yet, so `claim` refuses them.
1289
+ */
1290
+ export type ChannelKind = "voice" | "meeting" | "whatsapp";
1291
+ /**
1292
+ * `suspended` and `released` both stop the channel answering. The difference
1293
+ * is whose act it was: a Connect operator suspended it, or the number went
1294
+ * back to inventory.
1295
+ */
1296
+ export type ChannelStatus = "active" | "suspended" | "released";
1297
+ /**
1298
+ * Where inbound conversations on a channel go.
1299
+ *
1300
+ * `destination` is **yours**: an arbitrary JSON object Connect stores verbatim
1301
+ * and never reads into. Which agent answers, business hours, overflow - all of
1302
+ * it is your application's model, not Connect's.
1303
+ */
1304
+ export interface ChannelRouting {
1305
+ destination: Record<string, unknown>;
1306
+ updatedAt: string;
1307
+ }
1308
+ /**
1309
+ * A number assigned to your application, as your application sees it.
1310
+ *
1311
+ * Two things are deliberately withheld and will not appear later:
1312
+ * `createdBy` names a Connect operator, and the carrier-side record (which
1313
+ * carrier, which provider resource, what Connect pays per month) is Connect's
1314
+ * cost and Connect's credential surface. An application reads what was
1315
+ * assigned to it; it never provisions, prices or releases a number.
1316
+ */
1317
+ export interface ConnectChannel {
1318
+ id: string;
1319
+ kind: ChannelKind;
1320
+ /** E.164 for a voice channel - the number a caller dials. */
1321
+ address: string;
1322
+ label: string | null;
1323
+ workspaceId: string;
1324
+ status: ChannelStatus;
1325
+ /** `null` when nothing has been routed yet, and after a `clearRouting()`. */
1326
+ routing: ChannelRouting | null;
1327
+ }
1328
+ /**
1329
+ * What a claim answers with - narrower than `ConnectChannel`, because a
1330
+ * just-claimed number has no label and no routing yet. Read it back with
1331
+ * `channels.get(id)` once you have written routing.
1332
+ */
1333
+ export interface ClaimedChannel {
1334
+ id: string;
1335
+ kind: ChannelKind;
1336
+ address: string;
1337
+ workspaceId: string;
1338
+ status: ChannelStatus;
1339
+ }
1340
+ /**
1341
+ * What both routing writes echo back - the stored record, not the whole
1342
+ * channel. `routing` is `null` after `clearRouting()`, which is the same shape
1343
+ * a channel that was never routed has: to the answer path "cleared" and "never
1344
+ * set" are the same fact.
1345
+ */
1346
+ export interface ChannelRoutingRecord {
1347
+ id: string;
1348
+ routing: ChannelRouting | null;
1349
+ }
1350
+ /** One page of channels. Page until `hasMore` is `false`. */
1351
+ export interface ChannelsPage {
1352
+ items: ConnectChannel[];
1353
+ nextCursor: string | null;
1354
+ hasMore: boolean;
1355
+ }
1356
+ /** Which way the call went. */
1357
+ export type CallDirection = "inbound" | "outbound";
1358
+ /**
1359
+ * `rejected` means Connect never connected it - see `rejectReason`.
1360
+ * `answered` is a call still in progress; `completed` is one that ran and
1361
+ * ended.
1362
+ */
1363
+ export type CallStatus = "answered" | "rejected" | "completed";
1364
+ /**
1365
+ * Why a call was refused. Widened at the point of use with `(string & {})`,
1366
+ * the same way `ReadinessBlocker.code` is: Connect adds reasons as it learns
1367
+ * to tell failures apart, and a new one should reach you as a value to handle
1368
+ * rather than as a type error on a published package you cannot patch. Handle
1369
+ * the ones you branch on and treat the rest as "refused".
1370
+ */
1371
+ export type CallRejectReason = "unknown_number" | "channel_suspended" | "channel_released" | "channel_unassigned" | "no_routing" | "workspace_blocked" | "answer_unavailable" | "runtime_unconfigured";
1372
+ /** How a call that actually ran came to an end. Widened at its use site for the same reason as `CallRejectReason`. */
1373
+ export type CallDisconnectReason = "caller_hangup" | "agent_hangup" | "max_duration" | "transferred" | "error" | "abandoned";
1374
+ /**
1375
+ * Where the recording has got to. `pending` is normal for a call that has just
1376
+ * ended - the carrier posts the audio afterwards - and `failed` is kept
1377
+ * distinct from `none` so "we never recorded this" is never read as "we
1378
+ * recorded it and lost it".
1379
+ */
1380
+ export type CallRecordingStatus = "none" | "pending" | "stored" | "failed";
1381
+ /** What the model leg of a call cost. */
1382
+ export interface CallTokenUsage {
1383
+ promptTokens: number;
1384
+ responseTokens: number;
1385
+ totalTokens: number;
1386
+ }
1387
+ /** One tool the agent called mid-conversation. Arguments and results are JSON strings, not parsed objects. */
1388
+ export interface CallToolInvocation {
1389
+ name: string;
1390
+ argumentsJson: string;
1391
+ resultJson: string | null;
1392
+ durationMs: number | null;
1393
+ /** `null` for calls closed out before the runtime reported it - which is not zero. */
1394
+ startedAt: string | null;
1395
+ error: string | null;
1396
+ }
1397
+ /**
1398
+ * One call that arrived on a channel assigned to your application.
1399
+ *
1400
+ * `durationSeconds` and `billDurationSeconds` are the two durations Connect
1401
+ * stands behind - the carrier's own measure of the audio is deliberately not
1402
+ * published, since a third near-identical number only invites a billing
1403
+ * argument settled off the wrong one.
1404
+ */
1405
+ export interface ConnectCall {
1406
+ id: string;
1407
+ channelId: string;
1408
+ workspaceId: string;
1409
+ direction: CallDirection;
1410
+ fromNumber: string;
1411
+ toNumber: string;
1412
+ status: CallStatus;
1413
+ rejectReason: CallRejectReason | (string & {}) | null;
1414
+ disconnectReason: CallDisconnectReason | (string & {}) | null;
1415
+ startedAt: string;
1416
+ answeredAt: string | null;
1417
+ endedAt: string | null;
1418
+ durationSeconds: number | null;
1419
+ billDurationSeconds: number | null;
1420
+ recordingStatus: CallRecordingStatus;
1421
+ /** A Drive file id - read it with a `drive:read` key. `null` until the carrier posts the audio. */
1422
+ recordingFileId: string | null;
1423
+ transcriptFileId: string | null;
1424
+ turnCount: number | null;
1425
+ tokenUsage: CallTokenUsage | null;
1426
+ toolInvocations: CallToolInvocation[];
1427
+ /** Your agent's own post-call structured output, keyed by its extraction schema. Connect stores it without interpreting it. */
1428
+ extraction: Record<string, unknown> | null;
1429
+ agentId: string | null;
1430
+ agentVersion: number | null;
1431
+ }
1432
+ /** Who spoke. */
1433
+ export type CallTurnRole = "user" | "agent";
1434
+ /**
1435
+ * One transcript turn. Order by `sequence`, never by `startedAt` - two turns
1436
+ * can share a millisecond.
1437
+ */
1438
+ export interface CallTranscriptTurn {
1439
+ sequence: number;
1440
+ role: CallTurnRole;
1441
+ text: string;
1442
+ startedAt: string | null;
1443
+ /** True only ever for an `agent` turn - the caller talked over it. */
1444
+ interrupted: boolean;
1445
+ }
1446
+ /** A call and its transcript. Long calls are capped at 2,000 turns. */
1447
+ export interface CallDetail {
1448
+ call: ConnectCall;
1449
+ turns: CallTranscriptTurn[];
1450
+ }
1451
+ /** One page of calls, newest first. Page until `hasMore` is `false` - do not stop on an empty `items`. */
1452
+ export interface CallsPage {
1453
+ items: ConnectCall[];
1454
+ nextCursor: string | null;
1455
+ hasMore: boolean;
1456
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",