@cosmoops/connect-sdk 0.32.0 → 0.34.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 +251 -0
- package/README.md +17 -5
- package/dist/cjs/client/components/access-gate.js +98 -2
- package/dist/cjs/client/components/visitor-chat.js +387 -0
- package/dist/cjs/client/components/visitor-stream.js +124 -0
- package/dist/cjs/client/index.js +16 -3
- package/dist/cjs/client/provider.js +29 -2
- package/dist/cjs/client/use-checkout.js +3 -1
- package/dist/cjs/helpers.js +46 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/next/index.js +10 -3
- package/dist/cjs/server/client.js +8 -1
- package/dist/cjs/server/handler.js +142 -0
- package/dist/cjs/server/resources/ai.js +50 -0
- package/dist/cjs/server/resources/billing.js +5 -0
- package/dist/cjs/server/resources/enrichment.js +59 -0
- package/dist/cjs/server/resources/entitlements.js +47 -0
- package/dist/cjs/server/resources/visitor.js +227 -0
- package/dist/esm/client/components/access-gate.js +99 -3
- package/dist/esm/client/components/visitor-chat.js +384 -0
- package/dist/esm/client/components/visitor-stream.js +120 -0
- package/dist/esm/client/index.js +11 -1
- package/dist/esm/client/provider.js +28 -2
- package/dist/esm/client/use-checkout.js +3 -1
- package/dist/esm/helpers.js +43 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +10 -3
- package/dist/esm/server/client.js +8 -1
- package/dist/esm/server/handler.js +142 -0
- package/dist/esm/server/resources/ai.js +50 -0
- package/dist/esm/server/resources/billing.js +5 -0
- package/dist/esm/server/resources/enrichment.js +59 -0
- package/dist/esm/server/resources/entitlements.js +47 -0
- package/dist/esm/server/resources/visitor.js +222 -0
- package/dist/types/client/components/access-gate.d.ts +9 -0
- package/dist/types/client/components/visitor-chat.d.ts +120 -0
- package/dist/types/client/components/visitor-stream.d.ts +42 -0
- package/dist/types/client/index.d.ts +6 -1
- package/dist/types/client/provider.d.ts +176 -51
- package/dist/types/client/use-checkout.d.ts +18 -1
- package/dist/types/helpers.d.ts +36 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/server/client.d.ts +7 -1
- package/dist/types/server/handler.d.ts +0 -24
- package/dist/types/server/resources/ai.d.ts +48 -1
- package/dist/types/server/resources/billing.d.ts +4 -0
- package/dist/types/server/resources/enrichment.d.ts +57 -1
- package/dist/types/server/resources/entitlements.d.ts +40 -1
- package/dist/types/server/resources/visitor.d.ts +200 -0
- package/dist/types/types.d.ts +562 -0
- package/package.json +3 -2
package/dist/cjs/helpers.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_PLAN_LIMIT = void 0;
|
|
3
4
|
exports.blockingBlockers = blockingBlockers;
|
|
4
5
|
exports.normalizePlanFeatures = normalizePlanFeatures;
|
|
6
|
+
exports.planLimitFor = planLimitFor;
|
|
7
|
+
exports.planLimitAllows = planLimitAllows;
|
|
5
8
|
/**
|
|
6
9
|
* The blockers that actually deny access, in the order to show them.
|
|
7
10
|
*
|
|
@@ -28,3 +31,46 @@ function blockingBlockers(readiness) {
|
|
|
28
31
|
function normalizePlanFeatures(features) {
|
|
29
32
|
return (features ?? []).map((feature) => typeof feature === "string" ? { key: feature, name: feature, description: "" } : feature);
|
|
30
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* What a key the plan carries no answer for reads as - see
|
|
36
|
+
* `SubscribedPlan.limits`, which is sparse.
|
|
37
|
+
*
|
|
38
|
+
* `"none"` rather than `"unlimited"`: a plan that was never asked about a
|
|
39
|
+
* limit has not sold it, and defaulting the other way would hand an unbounded
|
|
40
|
+
* allowance to every tier that predates the question.
|
|
41
|
+
*/
|
|
42
|
+
exports.NO_PLAN_LIMIT = { model: "none", value: null };
|
|
43
|
+
/**
|
|
44
|
+
* One plan's answer for one limit key, with the sparse-map default applied.
|
|
45
|
+
*
|
|
46
|
+
* Indexing `plan.limits` directly is what makes a missing key `undefined`,
|
|
47
|
+
* which then reads as unlimited through the first `?.` somebody writes. This
|
|
48
|
+
* returns a `PlanLimit` either way.
|
|
49
|
+
*/
|
|
50
|
+
function planLimitFor(limits, key) {
|
|
51
|
+
return limits?.[key] ?? exports.NO_PLAN_LIMIT;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether `additional` more of the resource fit under this limit, given how
|
|
55
|
+
* many the subscriber already has. The comparison, written once.
|
|
56
|
+
*
|
|
57
|
+
* Every way of writing it at a call site is wrong in a different direction:
|
|
58
|
+
* `count < limit.value` is off by one as soon as you add more than one row,
|
|
59
|
+
* `if (!limit.value)` refuses the unlimited tier, and
|
|
60
|
+
* `limit.value ?? Infinity` grants unlimited to a plan that allows none. This
|
|
61
|
+
* is the same implementation Connect's own server uses to validate a plan.
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* const limit = await connect.entitlements.getLimit(workspaceId, "sites");
|
|
65
|
+
* if (!planLimitAllows(limit, await countSites(workspaceId))) {
|
|
66
|
+
* throw new Error("Your plan's site limit is reached.");
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function planLimitAllows(limit, currentCount, additional = 1) {
|
|
71
|
+
if (limit.model === "unlimited")
|
|
72
|
+
return true;
|
|
73
|
+
if (limit.model === "none")
|
|
74
|
+
return false;
|
|
75
|
+
return currentCount + additional <= (limit.value ?? 0);
|
|
76
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.storeIndexCovers = exports.describeQueryRejection = exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = exports.serverTimestamp = exports.increment = exports.documentExists = exports.deleteField = exports.arrayUnion = exports.arrayRemove = exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
|
|
3
|
+
exports.storeIndexCovers = exports.describeQueryRejection = exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = exports.serverTimestamp = exports.increment = exports.documentExists = exports.deleteField = exports.arrayUnion = exports.arrayRemove = exports.planLimitFor = exports.planLimitAllows = exports.NO_PLAN_LIMIT = exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `@cosmoops/connect-sdk` - the official client for the CosmoOps Connect
|
|
6
6
|
* **application-partner API** (`/api/v1/application/*`).
|
|
@@ -22,6 +22,10 @@ Object.defineProperty(exports, "isConnectError", { enumerable: true, get: functi
|
|
|
22
22
|
var helpers_js_1 = require("./helpers.js");
|
|
23
23
|
Object.defineProperty(exports, "blockingBlockers", { enumerable: true, get: function () { return helpers_js_1.blockingBlockers; } });
|
|
24
24
|
Object.defineProperty(exports, "normalizePlanFeatures", { enumerable: true, get: function () { return helpers_js_1.normalizePlanFeatures; } });
|
|
25
|
+
var helpers_js_2 = require("./helpers.js");
|
|
26
|
+
Object.defineProperty(exports, "NO_PLAN_LIMIT", { enumerable: true, get: function () { return helpers_js_2.NO_PLAN_LIMIT; } });
|
|
27
|
+
Object.defineProperty(exports, "planLimitAllows", { enumerable: true, get: function () { return helpers_js_2.planLimitAllows; } });
|
|
28
|
+
Object.defineProperty(exports, "planLimitFor", { enumerable: true, get: function () { return helpers_js_2.planLimitFor; } });
|
|
25
29
|
var store_js_1 = require("./store.js");
|
|
26
30
|
Object.defineProperty(exports, "arrayRemove", { enumerable: true, get: function () { return store_js_1.arrayRemove; } });
|
|
27
31
|
Object.defineProperty(exports, "arrayUnion", { enumerable: true, get: function () { return store_js_1.arrayUnion; } });
|
package/dist/cjs/next/index.js
CHANGED
|
@@ -79,11 +79,18 @@ const errors_js_1 = require("../errors.js");
|
|
|
79
79
|
* | --- | --- | --- |
|
|
80
80
|
* | `CONNECT_API_KEY` | yes | server-side credential |
|
|
81
81
|
* | `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
82
|
-
* | `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
83
|
-
* | `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
82
|
+
* | `CONNECT_APP_URL` | no | non-production Connect instance; unset means production |
|
|
83
|
+
* | `CONNECT_SESSION_COOKIE` | no | session cookie name; unset means `connect_session` |
|
|
84
|
+
*
|
|
85
|
+
* `CONNECT_APP_URL` is server-side only and does not reach the browser:
|
|
86
|
+
* `ConnectProvider` defaults `appUrl` to the production origin independently,
|
|
87
|
+
* so an app pointed at another instance must pass that prop as well or its
|
|
88
|
+
* portal links and its browser-built authorize URL still go to production.
|
|
84
89
|
*
|
|
85
90
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
86
|
-
* application's Connect client section. That is the only step this cannot do
|
|
91
|
+
* application's Connect client section. That is the only step this cannot do
|
|
92
|
+
* for you, and Connect matches it exactly - every origin you run from needs
|
|
93
|
+
* its own entry, localhost included.
|
|
87
94
|
*
|
|
88
95
|
* No `next.config.ts` changes needed either, including `transpilePackages`:
|
|
89
96
|
* `dist/` is already ES2022 with `"use client"` hoisted to the top of every
|
|
@@ -23,6 +23,7 @@ const analytics_js_1 = require("./resources/analytics.js");
|
|
|
23
23
|
const otp_js_1 = require("./resources/otp.js");
|
|
24
24
|
const sessions_js_1 = require("./resources/sessions.js");
|
|
25
25
|
const platform_js_1 = require("./resources/platform.js");
|
|
26
|
+
const visitor_js_1 = require("./resources/visitor.js");
|
|
26
27
|
const push_js_1 = require("./resources/push.js");
|
|
27
28
|
exports.DEFAULT_BASE_URL = "https://connect.cosmoops.com";
|
|
28
29
|
const APPLICATION_API_PREFIX = "/api/v1/application";
|
|
@@ -115,6 +116,11 @@ class ConnectClient {
|
|
|
115
116
|
integrations;
|
|
116
117
|
/** Which workspaces the signed-in user belongs to, switching between them, and the team - see `WorkspacesResource`. */
|
|
117
118
|
workspaces;
|
|
119
|
+
/**
|
|
120
|
+
* The public chat bubble on your own site - see `VisitorResource`. The one
|
|
121
|
+
* resource here whose caller is not one of your users.
|
|
122
|
+
*/
|
|
123
|
+
visitor;
|
|
118
124
|
/** The platform's own legal identity and DPDP grievance channel - see `PlatformResource`. */
|
|
119
125
|
platform;
|
|
120
126
|
/**
|
|
@@ -165,6 +171,7 @@ class ConnectClient {
|
|
|
165
171
|
this.sessions = new sessions_js_1.SessionsResource(transport, options.clientId ?? null);
|
|
166
172
|
this.integrations = new integrations_js_1.IntegrationsResource(transport);
|
|
167
173
|
this.workspaces = new workspaces_js_1.WorkspacesResource(transport);
|
|
174
|
+
this.visitor = new visitor_js_1.VisitorResource(transport);
|
|
168
175
|
this.platform = new platform_js_1.PlatformResource(transport);
|
|
169
176
|
this.push = new push_js_1.PushResource(transport);
|
|
170
177
|
this.storeRelay = (spec) => (0, data_js_1.storePassthrough)(transport, spec);
|
|
@@ -172,4 +179,4 @@ class ConnectClient {
|
|
|
172
179
|
}
|
|
173
180
|
exports.ConnectClient = ConnectClient;
|
|
174
181
|
/** Kept in step with `package.json` by `sdk.test.ts`. */
|
|
175
|
-
exports.SDK_VERSION = "0.
|
|
182
|
+
exports.SDK_VERSION = "0.34.0";
|
|
@@ -99,6 +99,24 @@ function redirect(location, cookies = []) {
|
|
|
99
99
|
* workspace it is not signed into, and a client-supplied `workspaceId`
|
|
100
100
|
* is exactly that hole.
|
|
101
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* The visitor's own address, for Connect's per-visitor limiter.
|
|
104
|
+
*
|
|
105
|
+
* **Left-most `x-forwarded-for` entry**, which is the same precedence Connect's
|
|
106
|
+
* own `/api/book` and `/api/auth/session` use, and the same caveat applies: the
|
|
107
|
+
* left-most entry is the client as the first proxy saw it, and a client can
|
|
108
|
+
* prepend to that header. That makes it unsuitable for authorization and fine
|
|
109
|
+
* for this - it partitions your application's own budget between visitors, and
|
|
110
|
+
* the ceilings that actually cap spending are per key and per application.
|
|
111
|
+
*
|
|
112
|
+
* `null` where nothing forwarded one - a local dev server, or a platform that
|
|
113
|
+
* does not set it. Connect then counts every visitor in one shared bucket,
|
|
114
|
+
* which is a stricter limit rather than an exemption.
|
|
115
|
+
*/
|
|
116
|
+
function visitorIpOf(request) {
|
|
117
|
+
const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
118
|
+
return forwarded || request.headers.get("x-real-ip") || null;
|
|
119
|
+
}
|
|
102
120
|
function createConnectHandler(options) {
|
|
103
121
|
const basePath = (options.basePath ?? "/api/connect").replace(/\/+$/, "");
|
|
104
122
|
const { client, session } = options;
|
|
@@ -330,6 +348,130 @@ function createConnectHandler(options) {
|
|
|
330
348
|
return json({ readiness, subscription });
|
|
331
349
|
},
|
|
332
350
|
"GET /plans": async () => json(await client.billing.listPlans()),
|
|
351
|
+
/* ---------------------------------------------------------------------- */
|
|
352
|
+
/* The visitor assistant - the only routes here with no session at all */
|
|
353
|
+
/* ---------------------------------------------------------------------- */
|
|
354
|
+
/**
|
|
355
|
+
* `POST /visitor/session`, `/visitor/chat`, `/visitor/book`.
|
|
356
|
+
*
|
|
357
|
+
* Three routes that take **no session**, which makes them the exception to
|
|
358
|
+
* the rule every other route here follows. That is not a hole: the caller
|
|
359
|
+
* is a stranger on a public page, and there is no account for a session to
|
|
360
|
+
* identify. The question "whose data is this" has a different answer here -
|
|
361
|
+
* it is the *application's*, and the application is the one thing this
|
|
362
|
+
* bridge does hold a credential for.
|
|
363
|
+
*
|
|
364
|
+
* So the scoping that `requireWorkspace` provides elsewhere is provided
|
|
365
|
+
* here by Connect's own configuration: the workspace a call books into and
|
|
366
|
+
* the knowledge it answers from come from the application record, and no
|
|
367
|
+
* field in any of these bodies names a tenant. A browser cannot widen that
|
|
368
|
+
* by asking, because there is nothing to ask with.
|
|
369
|
+
*
|
|
370
|
+
* **`GET /company-profile` is the precedent and the limit of it.** That one
|
|
371
|
+
* is unauthenticated because the answer is identical for every caller.
|
|
372
|
+
* These are unauthenticated because the *caller* has no identity - but the
|
|
373
|
+
* answer is still per application, and spending is still per application.
|
|
374
|
+
* Which is why they are the only routes here that pass the visitor's own
|
|
375
|
+
* address along: the limiters on the far side count a visitor, and this
|
|
376
|
+
* bridge is the only thing that knows which one.
|
|
377
|
+
*/
|
|
378
|
+
"POST /visitor/session": async (request) => {
|
|
379
|
+
const body = (await request.json().catch(() => null));
|
|
380
|
+
/*
|
|
381
|
+
* **The workspace is the browser's claim here, and that is the one thing
|
|
382
|
+
* this bridge must not simply relay.** Every other route on this bridge
|
|
383
|
+
* resolves the tenant from the caller's session; a visitor has none, so
|
|
384
|
+
* the widget names the customer whose site it is on - and a page can name
|
|
385
|
+
* any of them.
|
|
386
|
+
*
|
|
387
|
+
* Connect checks the workspace against your subscriptions, so the worst
|
|
388
|
+
* case is bounded to *your own* customers rather than the platform's. That
|
|
389
|
+
* is still one customer's budget spent from another's page, so an
|
|
390
|
+
* application serving several customers should resolve the workspace from
|
|
391
|
+
* its own widget record - by the embed token in the snippet, say - rather
|
|
392
|
+
* than trusting this field. The SDK cannot do that for you: it does not
|
|
393
|
+
* know what your widgets are keyed by.
|
|
394
|
+
*/
|
|
395
|
+
if (typeof body?.workspaceId !== "string") {
|
|
396
|
+
throw new errors_js_1.ConnectError({
|
|
397
|
+
status: 400,
|
|
398
|
+
code: "validation_error",
|
|
399
|
+
message: "workspaceId is required",
|
|
400
|
+
endpoint: "POST /visitor/session",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return json(await client.visitor.startSession({
|
|
404
|
+
workspaceId: body.workspaceId,
|
|
405
|
+
page: typeof body.page === "string" ? body.page : null,
|
|
406
|
+
resume: typeof body.resume === "string" ? body.resume : null,
|
|
407
|
+
}, { visitorIp: visitorIpOf(request) }));
|
|
408
|
+
},
|
|
409
|
+
"POST /visitor/chat": async (request) => {
|
|
410
|
+
const body = (await request.json().catch(() => null));
|
|
411
|
+
if (typeof body?.sessionId !== "string" ||
|
|
412
|
+
typeof body?.workspaceId !== "string" ||
|
|
413
|
+
typeof body?.message !== "string") {
|
|
414
|
+
throw new errors_js_1.ConnectError({
|
|
415
|
+
status: 400,
|
|
416
|
+
code: "validation_error",
|
|
417
|
+
message: "sessionId, workspaceId and message are required",
|
|
418
|
+
endpoint: "POST /visitor/chat",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const upstream = await client.visitor.chat({
|
|
422
|
+
sessionId: body.sessionId,
|
|
423
|
+
workspaceId: body.workspaceId,
|
|
424
|
+
message: body.message,
|
|
425
|
+
...(typeof body.assistantName === "string"
|
|
426
|
+
? { assistantName: body.assistantName }
|
|
427
|
+
: {}),
|
|
428
|
+
...(typeof body.bookingEventTypeSlug === "string"
|
|
429
|
+
? { bookingEventTypeSlug: body.bookingEventTypeSlug }
|
|
430
|
+
: {}),
|
|
431
|
+
}, { visitorIp: visitorIpOf(request) });
|
|
432
|
+
/*
|
|
433
|
+
* Piped, never buffered - awaiting the body would collect the whole reply
|
|
434
|
+
* server-side and deliver it in one lump, which is the only thing
|
|
435
|
+
* streaming was for. The browser hanging up propagates down this pipe and
|
|
436
|
+
* stops generation upstream, which on this surface also stops the
|
|
437
|
+
* spending.
|
|
438
|
+
*/
|
|
439
|
+
return new Response(upstream.body, {
|
|
440
|
+
status: upstream.status,
|
|
441
|
+
headers: {
|
|
442
|
+
"content-type": upstream.headers.get("content-type") ?? "text/event-stream",
|
|
443
|
+
"cache-control": "no-store",
|
|
444
|
+
// Without this a proxy in front of your app may buffer the whole
|
|
445
|
+
// stream to inspect it, which undoes the streaming end to end.
|
|
446
|
+
"x-accel-buffering": "no",
|
|
447
|
+
"x-content-type-options": "nosniff",
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
"POST /visitor/book": async (request) => {
|
|
452
|
+
const body = (await request.json().catch(() => null));
|
|
453
|
+
if (typeof body?.sessionId !== "string" ||
|
|
454
|
+
typeof body?.workspaceId !== "string" ||
|
|
455
|
+
typeof body?.eventTypeSlug !== "string" ||
|
|
456
|
+
typeof body?.startAt !== "string") {
|
|
457
|
+
throw new errors_js_1.ConnectError({
|
|
458
|
+
status: 400,
|
|
459
|
+
code: "validation_error",
|
|
460
|
+
message: "sessionId, workspaceId, eventTypeSlug and startAt are required",
|
|
461
|
+
endpoint: "POST /visitor/book",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return json(await client.visitor.book({
|
|
465
|
+
sessionId: body.sessionId,
|
|
466
|
+
workspaceId: body.workspaceId,
|
|
467
|
+
eventTypeSlug: body.eventTypeSlug,
|
|
468
|
+
startAt: body.startAt,
|
|
469
|
+
name: typeof body.name === "string" ? body.name : "",
|
|
470
|
+
phone: typeof body.phone === "string" ? body.phone : "",
|
|
471
|
+
email: typeof body.email === "string" ? body.email : null,
|
|
472
|
+
notes: typeof body.notes === "string" ? body.notes : null,
|
|
473
|
+
}, { visitorIp: visitorIpOf(request) }));
|
|
474
|
+
},
|
|
333
475
|
/**
|
|
334
476
|
* The platform's legal identity and grievance channel.
|
|
335
477
|
*
|
|
@@ -142,5 +142,55 @@ class AiResource {
|
|
|
142
142
|
});
|
|
143
143
|
return result.data;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* How ready the site at a domain is for AI agents.
|
|
147
|
+
*
|
|
148
|
+
* Probes for an MCP server, an OpenAPI description, `llms.txt`, structured
|
|
149
|
+
* data and a markdown twin; evaluates the site's `robots.txt` against the
|
|
150
|
+
* assistants that actually fetch pages; checks whether its content survives
|
|
151
|
+
* without JavaScript and whether a bot wall refuses agents outright. Then
|
|
152
|
+
* grades what it found and says what to fix first.
|
|
153
|
+
*
|
|
154
|
+
* Needs an API key scoped to **`ai:readiness`**, which is separate from every
|
|
155
|
+
* other AI scope because it is the widest outbound surface on the API - it
|
|
156
|
+
* POSTs to candidate MCP endpoints and fetches the home page under two user
|
|
157
|
+
* agents.
|
|
158
|
+
*
|
|
159
|
+
* ```ts
|
|
160
|
+
* const result = await connect.ai.readiness({ domain: "acme.com" });
|
|
161
|
+
* if (result.status === "assessed") {
|
|
162
|
+
* console.log(result.report.grade, result.report.topFix?.fix);
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* Four things to know before wiring it in:
|
|
167
|
+
*
|
|
168
|
+
* - **Set your client timeout to at least 90 seconds.** A cold assessment
|
|
169
|
+
* runs the whole probe before answering and can take up to a minute; many
|
|
170
|
+
* HTTP clients default to 30s and abandon a request that was about to
|
|
171
|
+
* succeed. Cached reads return in milliseconds and are the normal case -
|
|
172
|
+
* reports are held for a week.
|
|
173
|
+
* - **A week, not ninety days**, because a report is a to-do list somebody is
|
|
174
|
+
* working through. Pass `refresh` after shipping a fix to see it land.
|
|
175
|
+
* - **Handle `status: "error"` checks as unknown, not as failures.** They mean
|
|
176
|
+
* Connect could not measure that check, and they are excluded from the
|
|
177
|
+
* score rather than counted against it.
|
|
178
|
+
* - **`skipped` says whose problem it is.** `unreachable` means the site did
|
|
179
|
+
* not answer; `provider_unavailable` means Connect's own assessment service
|
|
180
|
+
* did. Only the second is worth retrying silently, and neither should be
|
|
181
|
+
* shown as a failing grade.
|
|
182
|
+
*/
|
|
183
|
+
readiness(input, options = {}) {
|
|
184
|
+
return (0, http_js_1.request)(this.transport, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: "/ai/readiness",
|
|
187
|
+
json: {
|
|
188
|
+
domain: input.domain,
|
|
189
|
+
...(input.refresh ? { refresh: true } : {}),
|
|
190
|
+
},
|
|
191
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
192
|
+
signal: options.signal,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
145
195
|
}
|
|
146
196
|
exports.AiResource = AiResource;
|
|
@@ -17,6 +17,10 @@ class BillingResource {
|
|
|
17
17
|
* independently of the instance it talks to. Without the coercion a partner
|
|
18
18
|
* pointed at a Connect that has not upgraded yet renders a feature list of
|
|
19
19
|
* blanks - which is exactly what happened. See `normalizePlanFeatures`.
|
|
20
|
+
*
|
|
21
|
+
* `limits` is defaulted for the same reason, one version later: an instance
|
|
22
|
+
* that predates limits sends no such field, and a pricing grid mapping over
|
|
23
|
+
* `plan.limits` would throw rather than simply show no limit rows.
|
|
20
24
|
*/
|
|
21
25
|
async listPlans() {
|
|
22
26
|
const response = await (0, http_js_1.request)(this.transport, { method: "GET", path: "/plans" });
|
|
@@ -24,6 +28,7 @@ class BillingResource {
|
|
|
24
28
|
plans: (response.plans ?? []).map((plan) => ({
|
|
25
29
|
...plan,
|
|
26
30
|
features: (0, helpers_js_1.normalizePlanFeatures)(plan.features),
|
|
31
|
+
limits: plan.limits ?? [],
|
|
27
32
|
})),
|
|
28
33
|
};
|
|
29
34
|
}
|
|
@@ -100,5 +100,64 @@ class EnrichmentResource {
|
|
|
100
100
|
signal: options.signal,
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* How **visible** the business at a domain is online, scored out of 100.
|
|
105
|
+
*
|
|
106
|
+
* The third sibling of `company()` and `brand()`: that one answers what a
|
|
107
|
+
* business does, the other what it looks like, and this one answers how
|
|
108
|
+
* findable it is - by a search engine, by a customer, and by the assistants
|
|
109
|
+
* that increasingly answer questions about a business before anybody visits
|
|
110
|
+
* its site.
|
|
111
|
+
*
|
|
112
|
+
* Needs an API key scoped to **`enrichment:presence`** - separate from
|
|
113
|
+
* `enrichment:read` and `enrichment:brand` because it is the widest outbound
|
|
114
|
+
* read of the three: roughly twenty fetches across the site's well-known
|
|
115
|
+
* paths, and sometimes a headless browser.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* const result = await connect.enrichment.presence({ domain: "acme.com" });
|
|
119
|
+
* if (result.status === "scanned") {
|
|
120
|
+
* for (const next of result.scan.recommendations) {
|
|
121
|
+
* console.log(`${next.priority}. ${next.title} - ${next.fix}`);
|
|
122
|
+
* }
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* Four things to know before wiring it in:
|
|
127
|
+
*
|
|
128
|
+
* - **The score is computed, not generated.** Every point comes from a check
|
|
129
|
+
* run against something actually observed, and every check ships with its
|
|
130
|
+
* weight, what was seen and the URL that settles it - so the total is
|
|
131
|
+
* reconstructable. A model only orders `recommendations` and writes the
|
|
132
|
+
* prose; it cannot move the number. That is the whole reason this is worth
|
|
133
|
+
* showing a customer: ask a model how good a company's web presence is and
|
|
134
|
+
* it answers 72 with total confidence for a site it never read.
|
|
135
|
+
* - **`social` is claimed, never verified.** Those are URLs the site itself
|
|
136
|
+
* printed. Nothing proves the account belongs to that business - render it
|
|
137
|
+
* as "linked from their site". Check `machineReadable` too: `false` means
|
|
138
|
+
* a real profile that search engines have to infer from a footer link,
|
|
139
|
+
* which is a specific, common and easily fixed finding.
|
|
140
|
+
* - **A low score is a success, not a skip.** Every check failing is the most
|
|
141
|
+
* useful report this produces. Only `unreachable` means the site did not
|
|
142
|
+
* answer - and `provider_unavailable` means *Connect* did not, which is
|
|
143
|
+
* never something to tell your user about their own site.
|
|
144
|
+
* - **Set your client timeout to at least 90 seconds.** A cold scan runs the
|
|
145
|
+
* whole probe before answering; many HTTP clients default to 30s and
|
|
146
|
+
* abandon a request that was about to succeed. Cached scans return in
|
|
147
|
+
* milliseconds, and cached is the normal case - results are held for a
|
|
148
|
+
* fortnight, so you pay the cold path roughly once per domain.
|
|
149
|
+
*/
|
|
150
|
+
presence(input, options = {}) {
|
|
151
|
+
return (0, http_js_1.request)(this.transport, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
path: "/enrichment/presence",
|
|
154
|
+
json: {
|
|
155
|
+
domain: input.domain,
|
|
156
|
+
...(input.refresh ? { refresh: true } : {}),
|
|
157
|
+
},
|
|
158
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
159
|
+
signal: options.signal,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
103
162
|
}
|
|
104
163
|
exports.EnrichmentResource = EnrichmentResource;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EntitlementsResource = void 0;
|
|
4
4
|
const http_js_1 = require("../http.js");
|
|
5
|
+
const helpers_js_1 = require("../../helpers.js");
|
|
5
6
|
/**
|
|
6
7
|
* The two checks worth making on every request: may this workspace be here at
|
|
7
8
|
* all, and what is it paying for.
|
|
@@ -45,6 +46,52 @@ class EntitlementsResource {
|
|
|
45
46
|
const result = await this.getSubscription(workspaceId);
|
|
46
47
|
return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* How much of a metered resource this workspace may have - the numeric
|
|
51
|
+
* counterpart of `hasFeature`, for the resources your application counts
|
|
52
|
+
* itself (sites, projects, pipelines).
|
|
53
|
+
*
|
|
54
|
+
* **This is the effective ceiling, not the plan's own number.** Connect has
|
|
55
|
+
* already folded in whatever the add-ons on the subscription raise it by, so
|
|
56
|
+
* "Starter includes 1 site, they bought two 5-site packs" arrives here as
|
|
57
|
+
* `{ model: "limited", value: 11 }`. You never join the plan against the
|
|
58
|
+
* add-on list yourself, and a customer who buys another pack needs no change
|
|
59
|
+
* on your side.
|
|
60
|
+
*
|
|
61
|
+
* Returns `{ model: "none" }` rather than throwing when there is no active
|
|
62
|
+
* subscription, when the plan carries no answer for this key, and when the
|
|
63
|
+
* Connect instance predates limits entirely - the same reasoning
|
|
64
|
+
* `hasFeature` gives for answering `false`: "not entitled" is the same answer
|
|
65
|
+
* either way, and a limit check should not need a try/catch around it.
|
|
66
|
+
*
|
|
67
|
+
* Connect does not enforce this. It cannot see what you are counting, so it
|
|
68
|
+
* hands you the number and your code refuses the next row - `canAdd` is that
|
|
69
|
+
* comparison if you already have the count.
|
|
70
|
+
*/
|
|
71
|
+
async getLimit(workspaceId, key) {
|
|
72
|
+
const result = await this.getSubscription(workspaceId);
|
|
73
|
+
if (!result.hasActiveSubscription)
|
|
74
|
+
return helpers_js_1.NO_PLAN_LIMIT;
|
|
75
|
+
return (0, helpers_js_1.planLimitFor)(result.plan?.limits, key);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether this workspace may add `additional` more of a limited resource,
|
|
79
|
+
* given how many it already has.
|
|
80
|
+
*
|
|
81
|
+
* You supply the count, because only your application can produce it. This
|
|
82
|
+
* is `getLimit` plus `planLimitAllows`, which exists so the comparison is
|
|
83
|
+
* not rewritten per call site - see that helper for the three ways of
|
|
84
|
+
* getting it wrong.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites.length))) {
|
|
88
|
+
* return refuse("Your plan's site limit is reached.");
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
async canAdd(workspaceId, key, currentCount, additional = 1) {
|
|
93
|
+
return (0, helpers_js_1.planLimitAllows)(await this.getLimit(workspaceId, key), currentCount, additional);
|
|
94
|
+
}
|
|
48
95
|
/**
|
|
49
96
|
* One page of the workspaces subscribed to your application, newest first.
|
|
50
97
|
*
|