@cosmoops/connect-sdk 0.30.1 → 0.32.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.
@@ -0,0 +1,136 @@
1
+ "use client";
2
+ import { useCallback, useState, useSyncExternalStore } from "react";
3
+ import { useConnect } from "./provider.js";
4
+ /**
5
+ * Read the browser's own permission as external state.
6
+ *
7
+ * Not `useState` in an effect: the permission belongs to the browser, it can
8
+ * change from the address bar while the page is open, and it does not exist
9
+ * during a server render. `useSyncExternalStore` is the primitive for exactly
10
+ * that - a server snapshot of `"checking"` and a live client one.
11
+ */
12
+ function subscribeToPermission(onChange) {
13
+ let status = null;
14
+ let cancelled = false;
15
+ // Absent on some Safari builds, and throws on others. Neither is fatal:
16
+ // without the subscription the snapshot is still correct on every render, it
17
+ // simply will not update by itself.
18
+ navigator.permissions
19
+ ?.query({ name: "notifications" })
20
+ .then((result) => {
21
+ if (cancelled)
22
+ return;
23
+ status = result;
24
+ result.addEventListener("change", onChange);
25
+ })
26
+ .catch(() => { });
27
+ return () => {
28
+ cancelled = true;
29
+ status?.removeEventListener("change", onChange);
30
+ };
31
+ }
32
+ function permissionSnapshot() {
33
+ if (typeof navigator === "undefined" ||
34
+ !("serviceWorker" in navigator) ||
35
+ typeof window === "undefined" ||
36
+ !("PushManager" in window) ||
37
+ !("Notification" in window)) {
38
+ return "unsupported";
39
+ }
40
+ if (Notification.permission === "denied")
41
+ return "denied";
42
+ return Notification.permission === "granted" ? "granted" : "prompt";
43
+ }
44
+ const serverSnapshot = () => "checking";
45
+ /**
46
+ * Register this browser for the platform's notifications.
47
+ *
48
+ * The subscription is **Connect's, not your application's**: a member who
49
+ * turns notifications on in one app has turned them on for the platform, and
50
+ * the same device receives every other app's notifications and Connect's own.
51
+ * That is the point of centralising it - one browser prompt instead of one per
52
+ * app, and one place to turn everything off.
53
+ *
54
+ * Your application needs a service worker at its own origin that handles the
55
+ * `push` event; the SDK does not install one, because a worker is a
56
+ * site-wide concern with caching implications only the host can decide.
57
+ *
58
+ * @param serviceWorkerUrl Where your worker lives. Root-scoped by convention.
59
+ */
60
+ export function useConnectPush(serviceWorkerUrl = "/sw.js") {
61
+ const { fetchResource, mutateResource } = useConnect();
62
+ const permission = useSyncExternalStore(subscribeToPermission, permissionSnapshot, serverSnapshot);
63
+ const [isEnabling, setIsEnabling] = useState(false);
64
+ const [error, setError] = useState(null);
65
+ const [unconfigured, setUnconfigured] = useState(false);
66
+ const enable = useCallback(async () => {
67
+ setError(null);
68
+ setIsEnabling(true);
69
+ try {
70
+ /*
71
+ * The key first, and the order matters: if the platform cannot send,
72
+ * asking for permission spends the member's one prompt on a feature
73
+ * that will never deliver - and a denial cannot be re-asked.
74
+ */
75
+ const key = await fetchResource("/push/key");
76
+ if (!key.configured || !key.publicKey) {
77
+ setUnconfigured(true);
78
+ return null;
79
+ }
80
+ if ((await Notification.requestPermission()) !== "granted")
81
+ return null;
82
+ const registration = await navigator.serviceWorker.register(serviceWorkerUrl);
83
+ // `ready` rather than the registration itself: one that is still
84
+ // installing has no `pushManager`, and subscribing against it throws in
85
+ // a way that reads as a permission problem.
86
+ await navigator.serviceWorker.ready;
87
+ const subscription = await registration.pushManager.subscribe({
88
+ // Required, and it may not be false - a silent push is reserved for
89
+ // sites with a far higher engagement bar, and asking for one here has
90
+ // the browser refuse the subscription outright.
91
+ userVisibleOnly: true,
92
+ applicationServerKey: base64UrlToBytes(key.publicKey),
93
+ });
94
+ const json = subscription.toJSON();
95
+ const registered = await mutateResource("/push/subscribe", {
96
+ endpoint: json.endpoint,
97
+ keys: { p256dh: json.keys?.p256dh, auth: json.keys?.auth },
98
+ });
99
+ return registered.id;
100
+ }
101
+ catch {
102
+ setError("This browser would not register for notifications. Reload the page and try again.");
103
+ return null;
104
+ }
105
+ finally {
106
+ setIsEnabling(false);
107
+ }
108
+ }, [fetchResource, mutateResource, serviceWorkerUrl]);
109
+ const disable = useCallback(async (deviceId) => {
110
+ await mutateResource("/push/unsubscribe", { id: deviceId });
111
+ }, [mutateResource]);
112
+ return {
113
+ status: unconfigured && permission !== "denied" ? "unconfigured" : permission,
114
+ isEnabling,
115
+ error,
116
+ enable,
117
+ disable,
118
+ };
119
+ }
120
+ /**
121
+ * The VAPID public key as the bytes `pushManager.subscribe()` wants.
122
+ *
123
+ * It travels base64url and the API takes a `BufferSource`. Spelled out rather
124
+ * than inlined because getting it wrong produces an `InvalidAccessError` that
125
+ * reads as a permissions fault rather than an encoding one - and the explicit
126
+ * `ArrayBuffer` matters too: `Uint8Array.from` widens to `ArrayBufferLike`,
127
+ * which `applicationServerKey` will not accept.
128
+ */
129
+ function base64UrlToBytes(base64) {
130
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
131
+ const raw = atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
132
+ const bytes = new Uint8Array(new ArrayBuffer(raw.length));
133
+ for (let index = 0; index < raw.length; index += 1)
134
+ bytes[index] = raw.charCodeAt(index);
135
+ return bytes;
136
+ }
@@ -20,6 +20,7 @@ import { AnalyticsResource } from "./resources/analytics.js";
20
20
  import { OtpResource } from "./resources/otp.js";
21
21
  import { SessionsResource } from "./resources/sessions.js";
22
22
  import { PlatformResource } from "./resources/platform.js";
23
+ import { PushResource } from "./resources/push.js";
23
24
  export const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
24
25
  const APPLICATION_API_PREFIX = "/api/v1/application";
25
26
  /**
@@ -113,6 +114,11 @@ export class ConnectClient {
113
114
  workspaces;
114
115
  /** The platform's own legal identity and DPDP grievance channel - see `PlatformResource`. */
115
116
  platform;
117
+ /**
118
+ * Web Push. Connect holds the VAPID keys, the devices and the member's own
119
+ * preferences; you supply a title, a body and a URL. See `PushResource`.
120
+ */
121
+ push;
116
122
  /**
117
123
  * Relays one `/data/*` or `/kv/*` request verbatim - `createConnectHandler`'s
118
124
  * store route and nothing else. See `storePassthrough`.
@@ -157,8 +163,9 @@ export class ConnectClient {
157
163
  this.integrations = new IntegrationsResource(transport);
158
164
  this.workspaces = new WorkspacesResource(transport);
159
165
  this.platform = new PlatformResource(transport);
166
+ this.push = new PushResource(transport);
160
167
  this.storeRelay = (spec) => storePassthrough(transport, spec);
161
168
  }
162
169
  }
163
170
  /** Kept in step with `package.json` by `sdk.test.ts`. */
164
- export const SDK_VERSION = "0.30.1";
171
+ export const SDK_VERSION = "0.32.0";
@@ -513,6 +513,60 @@ export function createConnectHandler(options) {
513
513
  * switching is simply absent for them rather than half-working - the same
514
514
  * treatment `/signin` and `/callback` already get.
515
515
  */
516
+ /*
517
+ * Push notifications - the browser half, and deliberately only that half.
518
+ *
519
+ * **There is no `POST /push` here**, and its absence is the design: a
520
+ * notification anybody can trigger from a devtools console is one an
521
+ * attacker can trigger from a compromised page, wearing the styling of an
522
+ * app the person trusts. Sending is a server-to-server act with your API
523
+ * key; Connect's own route refuses the session-token path outright.
524
+ *
525
+ * What a browser legitimately does is register itself, because
526
+ * `pushManager.subscribe()` produces a subscription no server can make on
527
+ * its behalf. All three routes resolve the member from their session, so
528
+ * there is no `userId` anywhere on this surface - a page cannot register a
529
+ * device against somebody else's account, nor list theirs.
530
+ */
531
+ "GET /push/key": async (request) => json(await client.push.key({ asUser: await requireToken(request) })),
532
+ "POST /push/subscribe": async (request) => {
533
+ const token = await requireToken(request);
534
+ const body = (await request.json().catch(() => null));
535
+ // Shape-checked here rather than relayed blindly, so a malformed
536
+ // subscription fails with a sentence naming what is missing instead of
537
+ // a Zod message from a server the page cannot see.
538
+ if (typeof body?.endpoint !== "string" ||
539
+ typeof body.keys?.p256dh !== "string" ||
540
+ typeof body.keys?.auth !== "string") {
541
+ throw new ConnectError({
542
+ status: 400,
543
+ code: "validation_error",
544
+ message: "endpoint and keys.p256dh/keys.auth are required",
545
+ endpoint: "POST /push/subscribe",
546
+ });
547
+ }
548
+ return json(await client.push.registerDevice({
549
+ endpoint: body.endpoint,
550
+ keys: { p256dh: body.keys.p256dh, auth: body.keys.auth },
551
+ // Taken from the request rather than the body: a page can put
552
+ // anything in a field, and this one is shown back on a device
553
+ // list where a spoofed entry would be the point of spoofing it.
554
+ userAgent: (request.headers.get("user-agent") ?? "").slice(0, 512),
555
+ }, { asUser: token }));
556
+ },
557
+ "POST /push/unsubscribe": async (request) => {
558
+ const token = await requireToken(request);
559
+ const body = (await request.json().catch(() => null));
560
+ if (typeof body?.id !== "string") {
561
+ throw new ConnectError({
562
+ status: 400,
563
+ code: "validation_error",
564
+ message: "id is required",
565
+ endpoint: "POST /push/unsubscribe",
566
+ });
567
+ }
568
+ return json(await client.push.revokeDevice(body.id, { asUser: token }));
569
+ },
516
570
  "GET /workspaces": async (request) => json({ workspaces: await client.workspaces.list({ asUser: await requireToken(request) }) }),
517
571
  "GET /workspaces/members": async (request) => json({ members: await client.workspaces.members({ asUser: await requireToken(request) }) }),
518
572
  "POST /workspaces/switch": async (request) => {
@@ -104,4 +104,39 @@ export class AiResource {
104
104
  });
105
105
  return result.data;
106
106
  }
107
+ /**
108
+ * Draft every section of a platform-defined assessment from one sentence.
109
+ *
110
+ * The intended flow is draft first, then let the person correct it section
111
+ * by section: a blank questionnaire is the reason most people abandon one,
112
+ * and editing a strawman is a far smaller ask than authoring from nothing.
113
+ *
114
+ * Unattributed - assessments run against platform-wide definitions and the
115
+ * first consumers run before anyone has signed in, so there is no `asUser`
116
+ * and no workspace here.
117
+ */
118
+ async draftAssessment(input) {
119
+ const result = await request(this.transport, {
120
+ method: "POST",
121
+ path: "/ai/assessment/draft",
122
+ json: input,
123
+ });
124
+ return result.data;
125
+ }
126
+ /**
127
+ * Critique a completed assessment - the tensions between sections, the
128
+ * riskiest assumption, and a coarse score.
129
+ *
130
+ * Unknown section keys in `answers` are ignored rather than rejected, so a
131
+ * definition changing under a half-finished assessment costs the reader
132
+ * nothing.
133
+ */
134
+ async reviewAssessment(input) {
135
+ const result = await request(this.transport, {
136
+ method: "POST",
137
+ path: "/ai/assessment/review",
138
+ json: input,
139
+ });
140
+ return result.data;
141
+ }
107
142
  }
@@ -49,4 +49,52 @@ export class EnrichmentResource {
49
49
  signal: options.signal,
50
50
  });
51
51
  }
52
+ /**
53
+ * What the company at a domain **looks like** - logo, palette, typefaces,
54
+ * and the radius, spacing and breakpoint scales its own stylesheets declare.
55
+ *
56
+ * The sibling of `company()` above: that one answers what a company does,
57
+ * this one answers what it looks like. Useful for white-labelling a portal
58
+ * to a customer's brand, pre-filling a design brief, or showing a real
59
+ * account card instead of a grey placeholder.
60
+ *
61
+ * Needs an API key scoped to **`enrichment:brand`** - separate from
62
+ * `enrichment:read` because it is far more expensive: it crawls
63
+ * stylesheets, may render the page in a headless browser, and caches image
64
+ * bytes.
65
+ *
66
+ * ```ts
67
+ * const result = await connect.enrichment.brand({ domain: "acme.com" });
68
+ * if (result.status === "extracted") {
69
+ * const primary = result.profile.brandKit.palette.find((c) => c.role === "primary");
70
+ * }
71
+ * ```
72
+ *
73
+ * Three things to know before wiring it in:
74
+ *
75
+ * - **Set your client timeout to at least 90 seconds.** A cold read runs
76
+ * the whole extraction before answering and can take up to a minute; many
77
+ * HTTP clients default to 30s and abandon a request that was about to
78
+ * succeed. Cached reads return in milliseconds, and cached is the normal
79
+ * case - profiles are held for 90 days, so you pay the cold path roughly
80
+ * once per domain.
81
+ * - **Never store `brandKit.logos[].url`.** It is a short-lived signed URL
82
+ * minted per read. Render it directly or re-read the profile; persist
83
+ * `sourceUrl` if you need something stable.
84
+ * - **The values were measured, not generated.** Check `palette[].source` -
85
+ * a `css-var` entry is the company naming its own colour, a `frequency`
86
+ * entry is a ranked count and much weaker.
87
+ */
88
+ brand(input, options = {}) {
89
+ return request(this.transport, {
90
+ method: "POST",
91
+ path: "/enrichment/brand",
92
+ json: {
93
+ domain: input.domain,
94
+ ...(input.refresh ? { refresh: true } : {}),
95
+ },
96
+ headers: actingAs(input.asUser),
97
+ signal: options.signal,
98
+ });
99
+ }
52
100
  }
@@ -0,0 +1,88 @@
1
+ import { request } from "../http.js";
2
+ import { actingAs } from "./acting-as.js";
3
+ /**
4
+ * Web Push, with Connect holding everything that makes it work.
5
+ *
6
+ * Your application supplies a title, a body, a URL and the members to reach.
7
+ * Connect owns the VAPID keys it signs with, the devices each member has
8
+ * registered across every application on the platform, that member's own
9
+ * notification preferences, and the bookkeeping when a browser goes away.
10
+ *
11
+ * So you never hold a push credential, never store an endpoint, and cannot
12
+ * reach anybody who is not an active member of the workspace you name -
13
+ * membership is re-checked on every send rather than trusted from the ids you
14
+ * pass.
15
+ *
16
+ * ## Two halves, and only `send` belongs on your server
17
+ *
18
+ * Registering a device happens in the **browser**, because a subscription is
19
+ * produced by `pushManager.subscribe()` against the member's own browser and
20
+ * no server can make one for them. `registerDevice` and `revokeDevice` below
21
+ * therefore take a session token and are what the SDK bridge calls on the
22
+ * browser's behalf - see `createConnectHandler`. You will normally reach them
23
+ * through `useConnectPush()` in `@cosmoops/connect-sdk/client` rather than
24
+ * directly.
25
+ */
26
+ export class PushResource {
27
+ transport;
28
+ constructor(transport) {
29
+ this.transport = transport;
30
+ }
31
+ /**
32
+ * Notify members of a workspace.
33
+ *
34
+ * **`delivered: 0` is a success, not a failure.** The member may have
35
+ * registered no device, silenced notifications from their apps, or denied
36
+ * the browser prompt - all ordinary. A caller that retries on zero will
37
+ * retry forever against somebody who has opted out.
38
+ *
39
+ * Treat every send as a nudge toward something already true in your own
40
+ * data, and never let a failure here fail the thing that triggered it: the
41
+ * reminder is worth less than the record it is about.
42
+ *
43
+ * `url` must be absolute and on an origin this application registered as an
44
+ * SSO redirect URI. Connect refuses anything else, because a URL it did not
45
+ * check is an open redirect delivered to a lock screen.
46
+ */
47
+ send(input) {
48
+ return request(this.transport, { method: "POST", path: "/push", json: input });
49
+ }
50
+ /**
51
+ * The VAPID public key a browser needs before it can subscribe, and whether
52
+ * the platform can send at all.
53
+ *
54
+ * Safe to hand to any signed-in caller: it is the verification half of the
55
+ * pair, so holding it lets a browser *receive* what Connect signs and never
56
+ * send anything.
57
+ */
58
+ key(options) {
59
+ return request(this.transport, {
60
+ method: "GET",
61
+ path: "/push/subscriptions",
62
+ headers: actingAs(options.asUser),
63
+ });
64
+ }
65
+ /** Register one browser, from a subscription that browser just produced. */
66
+ registerDevice(input, options) {
67
+ return request(this.transport, {
68
+ method: "POST",
69
+ path: "/push/subscriptions",
70
+ json: input,
71
+ headers: actingAs(options.asUser),
72
+ });
73
+ }
74
+ /**
75
+ * Stop notifications on one browser.
76
+ *
77
+ * The id is the one `registerDevice` returned. Connect re-checks that it
78
+ * belongs to the calling member, so a guessed id is a refusal rather than
79
+ * somebody else's device going quiet.
80
+ */
81
+ revokeDevice(id, options) {
82
+ return request(this.transport, {
83
+ method: "DELETE",
84
+ path: `/push/subscriptions?id=${encodeURIComponent(id)}`,
85
+ headers: actingAs(options.asUser),
86
+ });
87
+ }
88
+ }
@@ -41,6 +41,13 @@ export type { ConnectShellContextValue } from "./components/shell-context.js";
41
41
  export { ConnectSidebarNav, ConnectSideNav } from "./components/side-nav.js";
42
42
  export type { ConnectSidebarNavProps, ConnectSideNavProps } from "./components/side-nav.js";
43
43
  export { useActivePath } from "./use-active-path.js";
44
+ /**
45
+ * Web Push. The hook owns the browser half - permission, the worker, the
46
+ * subscription - and Connect owns the keys, the devices and the member's
47
+ * preferences. Sending is server-side, through `connect.push.send`.
48
+ */
49
+ export { useConnectPush } from "./use-push.js";
50
+ export type { ConnectPushStatus, UseConnectPush } from "./use-push.js";
44
51
  export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemActive, gateExemptPaths, filterNavForAudience, } from "./components/nav.js";
45
52
  export type { ConnectNav, ConnectNavItem, ConnectNavSection, ConnectNavLinkProps, ConnectLinkRenderer, } from "./components/nav.js";
46
53
  export type { ConnectScreen } from "../types.js";
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Whether this browser can show notifications, and whether it has been asked.
3
+ *
4
+ * Five states, not a boolean - and that is the whole reason this hook exists
5
+ * rather than a `<Switch>`:
6
+ *
7
+ * - `unsupported` - no service worker or no Push API. On iOS this is every
8
+ * browser until the site is added to the home screen, which is worth saying
9
+ * out loud because it is most of the phones in some markets.
10
+ * - `unconfigured` - the platform has no VAPID keys, so nothing could be
11
+ * delivered even if permission were granted.
12
+ * - `denied` - **and this one cannot be undone by script.** A browser will not
13
+ * show the prompt again once told no; only the site-settings panel in the
14
+ * address bar can reverse it. A control that silently does nothing when
15
+ * pressed is the worst version of this screen, so the state is surfaced and
16
+ * the UI is expected to explain it.
17
+ * - `prompt` - askable.
18
+ * - `granted` - permission held. It does **not** follow that this browser is
19
+ * registered with Connect; permission and registration are separate facts,
20
+ * and a member who cleared site data holds the first without the second.
21
+ */
22
+ export type ConnectPushStatus = "checking" | "unsupported" | "unconfigured" | "denied" | "prompt" | "granted";
23
+ export interface UseConnectPush {
24
+ status: ConnectPushStatus;
25
+ /** True while `enable()` is in flight. */
26
+ isEnabling: boolean;
27
+ /** The last failure, in words a person can act on. Cleared by the next attempt. */
28
+ error: string | null;
29
+ /**
30
+ * Ask for permission, register this browser with Connect, and resolve the
31
+ * device id - or `null` if the member declined or anything refused.
32
+ *
33
+ * Must be called from a user gesture: browsers refuse
34
+ * `Notification.requestPermission()` outside one, and the refusal is
35
+ * indistinguishable from the member saying no.
36
+ */
37
+ enable: () => Promise<string | null>;
38
+ /** Stop notifications on one browser, by the id `enable()` returned. */
39
+ disable: (deviceId: string) => Promise<void>;
40
+ }
41
+ /**
42
+ * Register this browser for the platform's notifications.
43
+ *
44
+ * The subscription is **Connect's, not your application's**: a member who
45
+ * turns notifications on in one app has turned them on for the platform, and
46
+ * the same device receives every other app's notifications and Connect's own.
47
+ * That is the point of centralising it - one browser prompt instead of one per
48
+ * app, and one place to turn everything off.
49
+ *
50
+ * Your application needs a service worker at its own origin that handles the
51
+ * `push` event; the SDK does not install one, because a worker is a
52
+ * site-wide concern with caching implications only the host can decide.
53
+ *
54
+ * @param serviceWorkerUrl Where your worker lives. Root-scoped by convention.
55
+ */
56
+ export declare function useConnectPush(serviceWorkerUrl?: string): UseConnectPush;
@@ -21,6 +21,7 @@ import { AnalyticsResource } from "./resources/analytics.js";
21
21
  import { OtpResource } from "./resources/otp.js";
22
22
  import { SessionsResource } from "./resources/sessions.js";
23
23
  import { PlatformResource } from "./resources/platform.js";
24
+ import { PushResource } from "./resources/push.js";
24
25
  export declare const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
25
26
  export interface ConnectClientOptions {
26
27
  /**
@@ -112,6 +113,11 @@ export declare class ConnectClient {
112
113
  readonly workspaces: WorkspacesResource;
113
114
  /** The platform's own legal identity and DPDP grievance channel - see `PlatformResource`. */
114
115
  readonly platform: PlatformResource;
116
+ /**
117
+ * Web Push. Connect holds the VAPID keys, the devices and the member's own
118
+ * preferences; you supply a title, a body and a URL. See `PushResource`.
119
+ */
120
+ readonly push: PushResource;
115
121
  /**
116
122
  * Relays one `/data/*` or `/kv/*` request verbatim - `createConnectHandler`'s
117
123
  * store route and nothing else. See `storePassthrough`.
@@ -126,4 +132,4 @@ export declare class ConnectClient {
126
132
  constructor(options: ConnectClientOptions);
127
133
  }
128
134
  /** Kept in step with `package.json` by `sdk.test.ts`. */
129
- export declare const SDK_VERSION = "0.30.1";
135
+ export declare const SDK_VERSION = "0.32.0";
@@ -1,5 +1,5 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { AiConversationDetail, AiConversationList, ChatMessage, ConnectScreen } from "../../types.js";
2
+ import type { AiConversationDetail, AiConversationList, AssessmentAnswers, AssessmentDraft, AssessmentReview, ChatMessage, ConnectScreen } from "../../types.js";
3
3
  /**
4
4
  * RAG chat and structured extraction against a workspace's data.
5
5
  *
@@ -87,4 +87,31 @@ export declare class AiResource {
87
87
  schemaId: string;
88
88
  fileUrl: string;
89
89
  }): Promise<T>;
90
+ /**
91
+ * Draft every section of a platform-defined assessment from one sentence.
92
+ *
93
+ * The intended flow is draft first, then let the person correct it section
94
+ * by section: a blank questionnaire is the reason most people abandon one,
95
+ * and editing a strawman is a far smaller ask than authoring from nothing.
96
+ *
97
+ * Unattributed - assessments run against platform-wide definitions and the
98
+ * first consumers run before anyone has signed in, so there is no `asUser`
99
+ * and no workspace here.
100
+ */
101
+ draftAssessment(input: {
102
+ assessmentKey: string;
103
+ seed: string;
104
+ }): Promise<AssessmentDraft>;
105
+ /**
106
+ * Critique a completed assessment - the tensions between sections, the
107
+ * riskiest assumption, and a coarse score.
108
+ *
109
+ * Unknown section keys in `answers` are ignored rather than rejected, so a
110
+ * definition changing under a half-finished assessment costs the reader
111
+ * nothing.
112
+ */
113
+ reviewAssessment(input: {
114
+ assessmentKey: string;
115
+ answers: AssessmentAnswers;
116
+ }): Promise<AssessmentReview>;
90
117
  }
@@ -1,5 +1,5 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { CompanyEnrichmentResult } from "../../types.js";
2
+ import type { BrandExtractionResult, CompanyEnrichmentResult } from "../../types.js";
3
3
  /**
4
4
  * What Connect knows about the company at a domain.
5
5
  *
@@ -41,4 +41,49 @@ export declare class EnrichmentResource {
41
41
  }, options?: {
42
42
  signal?: AbortSignal;
43
43
  }): Promise<CompanyEnrichmentResult>;
44
+ /**
45
+ * What the company at a domain **looks like** - logo, palette, typefaces,
46
+ * and the radius, spacing and breakpoint scales its own stylesheets declare.
47
+ *
48
+ * The sibling of `company()` above: that one answers what a company does,
49
+ * this one answers what it looks like. Useful for white-labelling a portal
50
+ * to a customer's brand, pre-filling a design brief, or showing a real
51
+ * account card instead of a grey placeholder.
52
+ *
53
+ * Needs an API key scoped to **`enrichment:brand`** - separate from
54
+ * `enrichment:read` because it is far more expensive: it crawls
55
+ * stylesheets, may render the page in a headless browser, and caches image
56
+ * bytes.
57
+ *
58
+ * ```ts
59
+ * const result = await connect.enrichment.brand({ domain: "acme.com" });
60
+ * if (result.status === "extracted") {
61
+ * const primary = result.profile.brandKit.palette.find((c) => c.role === "primary");
62
+ * }
63
+ * ```
64
+ *
65
+ * Three things to know before wiring it in:
66
+ *
67
+ * - **Set your client timeout to at least 90 seconds.** A cold read runs
68
+ * the whole extraction before answering and can take up to a minute; many
69
+ * HTTP clients default to 30s and abandon a request that was about to
70
+ * succeed. Cached reads return in milliseconds, and cached is the normal
71
+ * case - profiles are held for 90 days, so you pay the cold path roughly
72
+ * once per domain.
73
+ * - **Never store `brandKit.logos[].url`.** It is a short-lived signed URL
74
+ * minted per read. Render it directly or re-read the profile; persist
75
+ * `sourceUrl` if you need something stable.
76
+ * - **The values were measured, not generated.** Check `palette[].source` -
77
+ * a `css-var` entry is the company naming its own colour, a `frequency`
78
+ * entry is a ranked count and much weaker.
79
+ */
80
+ brand(input: {
81
+ /** A domain or a URL in any form - `acme.com`, `www.acme.com`, `https://acme.com/pricing`. */
82
+ domain: string;
83
+ asUser?: string;
84
+ /** Re-extract even if Connect has a fresh profile cached. For an operator pressing "refresh" - not for automatic calls. */
85
+ refresh?: boolean;
86
+ }, options?: {
87
+ signal?: AbortSignal;
88
+ }): Promise<BrandExtractionResult>;
44
89
  }