@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.
- package/CHANGELOG.md +127 -0
- package/dist/cjs/client/index.js +10 -3
- package/dist/cjs/client/use-push.js +139 -0
- package/dist/cjs/server/client.js +8 -1
- package/dist/cjs/server/handler.js +54 -0
- package/dist/cjs/server/resources/ai.js +35 -0
- package/dist/cjs/server/resources/enrichment.js +48 -0
- package/dist/cjs/server/resources/push.js +92 -0
- package/dist/esm/client/index.js +6 -0
- package/dist/esm/client/use-push.js +136 -0
- package/dist/esm/server/client.js +8 -1
- package/dist/esm/server/handler.js +54 -0
- package/dist/esm/server/resources/ai.js +35 -0
- package/dist/esm/server/resources/enrichment.js +48 -0
- package/dist/esm/server/resources/push.js +88 -0
- package/dist/types/client/index.d.ts +7 -0
- package/dist/types/client/use-push.d.ts +56 -0
- package/dist/types/server/client.d.ts +7 -1
- package/dist/types/server/resources/ai.d.ts +28 -1
- package/dist/types/server/resources/enrichment.d.ts +46 -1
- package/dist/types/server/resources/push.d.ts +73 -0
- package/dist/types/types.d.ts +270 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
}
|