@clivly/sdk 0.4.0 → 0.7.1

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/README.md CHANGED
@@ -18,15 +18,14 @@ npm install clivly
18
18
  Clivly's stable packages publish on npm's `latest` tag. Use `@next` only when
19
19
  you are intentionally validating a prerelease build.
20
20
 
21
- Add `drizzle-orm` when you use `clivly/drizzle` or `clivly/core/drizzle`, and
21
+ Add `drizzle-orm` when you use `clivly/drizzle`, and
22
22
  `vite` when you use the Vite/TanStack Start plugin.
23
23
 
24
24
  ## Usage
25
25
 
26
26
  ```ts
27
- import { createClivlySDK } from "clivly/sdk";
27
+ import { createClivlySDK, defineClivlyConfig } from "clivly/sdk";
28
28
  import { fromDrizzle } from "clivly/drizzle";
29
- import { defineClivlyConfig } from "clivly/core";
30
29
  import { db, participants, accounts } from "./db";
31
30
 
32
31
  // Declarative entity config — the non-deprecated source of truth. It maps your
@@ -48,7 +47,7 @@ const entities = defineClivlyConfig({
48
47
  });
49
48
 
50
49
  const clivly = createClivlySDK({
51
- apiKey: process.env.CLIVLY_API_KEY!, // sk_live_… from Settings → API keys
50
+ apiKey: process.env.CLIVLY_SECRET_KEY!, // sk_live_… from Settings → API keys
52
51
  entities,
53
52
  source: {
54
53
  contacts: fromDrizzle(db, participants, { entity: "participants", cursorField: "updatedAt" }),
@@ -63,7 +62,7 @@ await clivly.start(); // heartbeat + backfill + interval delta sync
63
62
 
64
63
  // Optional: let Clivly's dashboard start a sync in this app.
65
64
  const remote = createClivlySDK({
66
- apiKey: process.env.CLIVLY_API_KEY!,
65
+ apiKey: process.env.CLIVLY_SECRET_KEY!,
67
66
  entities,
68
67
  source: { /* … */ },
69
68
  syncTrigger: {
@@ -71,7 +70,6 @@ const remote = createClivlySDK({
71
70
  url: process.env.CLIVLY_SYNC_TRIGGER_URL,
72
71
  secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET, // whsec_… from Integrations → Remote sync
73
72
  },
74
- verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
75
73
  });
76
74
 
77
75
  export async function POST(request: Request) {
@@ -126,7 +124,7 @@ the heartbeat and sync loop. It is dev-server only; use `runScheduled()` or
126
124
  Put your instance in `clivly.config.ts` (`export default createClivlySDK({...})`), then:
127
125
 
128
126
  ```bash
129
- npx clivly ping # CLIVLY_API_KEY + API reachability
127
+ npx clivly ping # CLIVLY_SECRET_KEY + API reachability
130
128
  npx clivly doctor # env → config → schema → columns → sample fetch → heartbeat
131
129
  ```
132
130
 
@@ -170,7 +168,7 @@ serverless cold start**, so a fresh isolate would re-page the whole entity. Pass
170
168
 
171
169
  ```ts
172
170
  const clivly = createClivlySDK({
173
- apiKey: process.env.CLIVLY_API_KEY!,
171
+ apiKey: process.env.CLIVLY_SECRET_KEY!,
174
172
  entities,
175
173
  source: { /* … */ },
176
174
  cursorStore: {
@@ -36,7 +36,7 @@ function originAllowed(origin, allowedOrigins) {
36
36
  if (!origin) return false;
37
37
  return allowedOrigins.includes(origin);
38
38
  }
39
- function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch }) {
39
+ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch, resolveUser }) {
40
40
  const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
41
41
  return async (request) => {
42
42
  if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
@@ -55,6 +55,12 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
55
55
  return jsonResponse({ error: "invalid_json" }, 400);
56
56
  }
57
57
  if (!isChatSessionRequestBody(body)) return jsonResponse({ error: "invalid_request" }, 400);
58
+ let hostUser = null;
59
+ if (resolveUser) try {
60
+ hostUser = await resolveUser(request);
61
+ } catch {
62
+ console.warn("[clivly] resolveUser threw; continuing without host identity");
63
+ }
58
64
  try {
59
65
  const upstream = await fetchImpl(sessionUrl, {
60
66
  method: "POST",
@@ -67,7 +73,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
67
73
  widgetId: body.widgetId,
68
74
  visitor: body.visitor,
69
75
  visitorToken: body.visitorToken,
70
- origin
76
+ origin,
77
+ ...hostUser ? { hostUser } : {}
71
78
  })
72
79
  });
73
80
  const responseBody = await upstream.text();
@@ -1,11 +1,5 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-jMafEyTv.cjs";
2
-
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-DTaz5mLi.cjs";
3
2
  //#region src/chat-session.d.ts
4
- declare function createChatSessionHandler({
5
- allowedOrigins,
6
- apiKey,
7
- apiUrl,
8
- fetchImpl
9
- }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
3
+ declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
10
4
  //#endregion
11
5
  export { createChatSessionHandler };
@@ -1,11 +1,5 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-B7wBCHRT.mjs";
2
-
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-DTaz5mLi.mjs";
3
2
  //#region src/chat-session.d.ts
4
- declare function createChatSessionHandler({
5
- allowedOrigins,
6
- apiKey,
7
- apiUrl,
8
- fetchImpl
9
- }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
3
+ declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
10
4
  //#endregion
11
5
  export { createChatSessionHandler };
@@ -35,7 +35,7 @@ function originAllowed(origin, allowedOrigins) {
35
35
  if (!origin) return false;
36
36
  return allowedOrigins.includes(origin);
37
37
  }
38
- function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch }) {
38
+ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch, resolveUser }) {
39
39
  const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
40
40
  return async (request) => {
41
41
  if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
@@ -54,6 +54,12 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
54
54
  return jsonResponse({ error: "invalid_json" }, 400);
55
55
  }
56
56
  if (!isChatSessionRequestBody(body)) return jsonResponse({ error: "invalid_request" }, 400);
57
+ let hostUser = null;
58
+ if (resolveUser) try {
59
+ hostUser = await resolveUser(request);
60
+ } catch {
61
+ console.warn("[clivly] resolveUser threw; continuing without host identity");
62
+ }
57
63
  try {
58
64
  const upstream = await fetchImpl(sessionUrl, {
59
65
  method: "POST",
@@ -66,7 +72,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
66
72
  widgetId: body.widgetId,
67
73
  visitor: body.visitor,
68
74
  visitorToken: body.visitorToken,
69
- origin
75
+ origin,
76
+ ...hostUser ? { hostUser } : {}
70
77
  })
71
78
  });
72
79
  const responseBody = await upstream.text();
package/dist/drizzle.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_drizzle_meta = require("./drizzle-meta-iAu8w1hj.cjs");
2
+ const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
3
3
  let drizzle_orm = require("drizzle-orm");
4
4
  let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
5
+ let _clivly_core_drizzle = require("@clivly/core/drizzle");
5
6
  //#region src/drizzle.ts
6
7
  function buildColumnsMeta(table) {
7
8
  const columns = (0, drizzle_orm.getTableColumns)(table);
@@ -103,5 +104,11 @@ function drizzleIntrospector(db) {
103
104
  };
104
105
  }
105
106
  //#endregion
107
+ Object.defineProperty(exports, "discoverFromDrizzle", {
108
+ enumerable: true,
109
+ get: function() {
110
+ return _clivly_core_drizzle.discoverFromDrizzle;
111
+ }
112
+ });
106
113
  exports.drizzleIntrospector = drizzleIntrospector;
107
114
  exports.fromDrizzle = fromDrizzle;
@@ -1,6 +1,6 @@
1
- import { m as NamespaceIntrospector, p as SyncSource } from "./types-jMafEyTv.cjs";
1
+ import { m as NamespaceIntrospector, p as SyncSource } from "./types-DTaz5mLi.cjs";
2
2
  import { Table } from "drizzle-orm";
3
-
3
+ import { discoverFromDrizzle } from "@clivly/core/drizzle";
4
4
  //#region src/drizzle.d.ts
5
5
  interface DrizzleDb {
6
6
  select: (...args: any[]) => any;
@@ -25,4 +25,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
25
25
  */
26
26
  declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
27
27
  //#endregion
28
- export { drizzleIntrospector, fromDrizzle };
28
+ export { discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
@@ -1,6 +1,6 @@
1
- import { m as NamespaceIntrospector, p as SyncSource } from "./types-B7wBCHRT.mjs";
1
+ import { m as NamespaceIntrospector, p as SyncSource } from "./types-DTaz5mLi.mjs";
2
2
  import { Table } from "drizzle-orm";
3
-
3
+ import { discoverFromDrizzle } from "@clivly/core/drizzle";
4
4
  //#region src/drizzle.d.ts
5
5
  interface DrizzleDb {
6
6
  select: (...args: any[]) => any;
@@ -25,4 +25,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
25
25
  */
26
26
  declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
27
27
  //#endregion
28
- export { drizzleIntrospector, fromDrizzle };
28
+ export { discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
package/dist/drizzle.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { t as DRIZZLE_META } from "./drizzle-meta-DMx_9nlj.mjs";
1
+ import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
2
2
  import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
3
3
  import { getTableConfig } from "drizzle-orm/pg-core";
4
+ import { discoverFromDrizzle } from "@clivly/core/drizzle";
4
5
  //#region src/drizzle.ts
5
6
  function buildColumnsMeta(table) {
6
7
  const columns = getTableColumns(table);
@@ -102,4 +103,4 @@ function drizzleIntrospector(db) {
102
103
  };
103
104
  }
104
105
  //#endregion
105
- export { drizzleIntrospector, fromDrizzle };
106
+ export { discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
package/dist/index.cjs CHANGED
@@ -1,6 +1,86 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_drizzle_meta = require("./drizzle-meta-iAu8w1hj.cjs");
2
+ const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
3
3
  const require_chat_session = require("./chat-session.cjs");
4
+ let _clivly_core_entity_config = require("@clivly/core/entity-config");
5
+ //#region src/entity-source.ts
6
+ const CONCEPT_KEYS = {
7
+ contact: "contacts",
8
+ company: "companies",
9
+ deal: "deals"
10
+ };
11
+ function resolveEntitySource(config, entityKey) {
12
+ const entity = config.entities?.entities?.[entityKey];
13
+ const src = config.source;
14
+ if (!(entity && src)) return null;
15
+ const conceptKey = CONCEPT_KEYS[entity.concept];
16
+ if (conceptKey) {
17
+ const source = src[conceptKey];
18
+ return source ? {
19
+ concept: entity.concept,
20
+ expectedKey: conceptKey,
21
+ source
22
+ } : null;
23
+ }
24
+ if (entity.concept === "custom") {
25
+ const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
26
+ return match ? {
27
+ concept: "custom",
28
+ expectedKey: "custom",
29
+ source: match
30
+ } : null;
31
+ }
32
+ return null;
33
+ }
34
+ /** The `config.source` key an entity's concept requires, present or not. */
35
+ function expectedSourceKey(concept) {
36
+ return CONCEPT_KEYS[concept] ?? "custom";
37
+ }
38
+ //#endregion
39
+ //#region src/concept-coverage.ts
40
+ const CHECK_NAME = "Entity → source coverage";
41
+ function configuredKeys(config) {
42
+ const src = config.source;
43
+ if (!src) return [];
44
+ return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
45
+ }
46
+ /**
47
+ * Every configured entity must resolve to a source, or it silently syncs
48
+ * nothing. This is the check that catches `source` keyed by entity name rather
49
+ * than by CRM concept — a mistake that otherwise shows up only as a run that
50
+ * succeeds with zero rows.
51
+ */
52
+ function checkConceptCoverage(config) {
53
+ const entities = config.entities?.entities ?? {};
54
+ const keys = Object.keys(entities);
55
+ if (keys.length === 0) return {
56
+ name: CHECK_NAME,
57
+ ok: false,
58
+ skipped: true,
59
+ detail: "No entities configured — nothing to cross-check."
60
+ };
61
+ const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
62
+ if (unresolved.length === 0) return {
63
+ name: CHECK_NAME,
64
+ ok: true,
65
+ detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
66
+ };
67
+ const available = configuredKeys(config);
68
+ const listed = available.length > 0 ? available.join(", ") : "(none)";
69
+ const problems = unresolved.map((key) => {
70
+ const concept = entities[key]?.concept ?? "unknown";
71
+ if (concept === "custom") {
72
+ const custom = config.source?.custom ?? [];
73
+ if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
74
+ }
75
+ return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
76
+ });
77
+ return {
78
+ name: CHECK_NAME,
79
+ ok: false,
80
+ detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
81
+ };
82
+ }
83
+ //#endregion
4
84
  //#region src/custom-slug.ts
5
85
  const NON_ALNUM = /[^a-z0-9]+/g;
6
86
  const EDGE_HYPHENS = /(^-|-$)/g;
@@ -81,6 +161,56 @@ function describeFetchError(entity, error) {
81
161
  return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
82
162
  }
83
163
  //#endregion
164
+ //#region src/mapping-dry-run.ts
165
+ /**
166
+ * Fetch one real row and push it through the entity's field map.
167
+ *
168
+ * The distinction from "Sample fetch": that proves the query runs, this proves
169
+ * the row that comes back actually carries the columns `fields` names. A
170
+ * mapping that points at a column the row lacks produces empty CRM records
171
+ * rather than an error, so it has to be caught here.
172
+ *
173
+ * An empty table SKIPS rather than fails — a fresh database is not a
174
+ * misconfiguration, and failing on it would train people to ignore the check.
175
+ */
176
+ async function runMappingDryRun(config, entityKey) {
177
+ const name = `Field mapping dry run (${entityKey})`;
178
+ const resolved = resolveEntitySource(config, entityKey);
179
+ if (!resolved) return {
180
+ name,
181
+ ok: false,
182
+ skipped: true,
183
+ detail: `"${entityKey}" resolves to no source — see the coverage check above.`
184
+ };
185
+ const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
186
+ let rows;
187
+ try {
188
+ rows = await resolved.source.fetchPage({
189
+ since: null,
190
+ limit: 1
191
+ });
192
+ } catch (error) {
193
+ return {
194
+ name,
195
+ ok: false,
196
+ detail: describeFetchError(entityKey, error)
197
+ };
198
+ }
199
+ const row = rows[0];
200
+ if (row === void 0) return {
201
+ name,
202
+ ok: false,
203
+ skipped: true,
204
+ detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
205
+ };
206
+ const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
207
+ return {
208
+ name,
209
+ ok: missing.length === 0,
210
+ detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
211
+ };
212
+ }
213
+ //#endregion
84
214
  //#region src/namespace-probe.ts
85
215
  /**
86
216
  * Verify the tables the integration depends on are genuinely reachable:
@@ -171,6 +301,11 @@ async function namespaceCheck(config) {
171
301
  detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
172
302
  };
173
303
  }
304
+ async function mappingDryRunChecks(config) {
305
+ const checks = [];
306
+ for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
307
+ return checks;
308
+ }
174
309
  /**
175
310
  * Run the read-only setup checks and return a structured report. Every check
176
311
  * captures its own error into `detail` rather than throwing, so one failure never
@@ -197,6 +332,7 @@ async function runDoctor(args) {
197
332
  ok: sources.length > 0,
198
333
  detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
199
334
  });
335
+ checks.push(checkConceptCoverage(config));
200
336
  for (const source of sources) {
201
337
  const meta = source[require_drizzle_meta.DRIZZLE_META];
202
338
  if (!meta) {
@@ -235,6 +371,7 @@ async function runDoctor(args) {
235
371
  detail: describeFetchError(source.entity, error)
236
372
  });
237
373
  }
374
+ checks.push(...await mappingDryRunChecks(config));
238
375
  const result = await connect();
239
376
  checks.push({
240
377
  name: "Heartbeat",
@@ -278,6 +415,7 @@ async function syncSource(args) {
278
415
  const idField = source.idField ?? DEFAULT_ID_FIELD;
279
416
  let cursor = since;
280
417
  let pushed = 0;
418
+ const resumedFrom = since === null ? null : since.time.toISOString();
281
419
  for (;;) {
282
420
  const rows = await source.fetchPage({
283
421
  since: cursor,
@@ -289,6 +427,8 @@ async function syncSource(args) {
289
427
  source: source.entity,
290
428
  rows: capped,
291
429
  mode: "delta",
430
+ fetched: rows.length,
431
+ cursor: resumedFrom,
292
432
  ...objectType ? { objectType } : {}
293
433
  })) return {
294
434
  pushed,
@@ -331,6 +471,8 @@ async function fullSyncSource(args) {
331
471
  source: source.entity,
332
472
  rows: all,
333
473
  mode: "full",
474
+ fetched: all.length,
475
+ cursor: null,
334
476
  ...objectType ? { objectType } : {}
335
477
  });
336
478
  return {
@@ -395,9 +537,7 @@ function observedTriggerUrl(req) {
395
537
  const proto = forwardedProto || requestUrl.protocol.replace(":", "");
396
538
  try {
397
539
  return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
398
- } catch {
399
- return;
400
- }
540
+ } catch {}
401
541
  }
402
542
  /**
403
543
  * Decide which trigger URL to report, in precedence order:
@@ -595,9 +735,7 @@ function connectReason(status) {
595
735
  async function resolveOrg(response) {
596
736
  try {
597
737
  return (await response.json())?.org;
598
- } catch {
599
- return;
600
- }
738
+ } catch {}
601
739
  }
602
740
  function retryAfterMs(res) {
603
741
  const header = res.headers.get("retry-after");
@@ -620,35 +758,30 @@ function createFetcher(retry) {
620
758
  }
621
759
  if (!(res === null || isRetryableStatus(res.status)) || attempt >= maxAttempts) return res;
622
760
  const backoff = baseDelayMs * 2 ** (attempt - 1) + Math.random() * baseDelayMs;
623
- await sleep(res ? retryAfterMs(res) ?? backoff : backoff);
761
+ const wait = res ? retryAfterMs(res) ?? backoff : backoff;
762
+ await sleep(wait);
624
763
  }
625
764
  };
626
765
  }
766
+ const CONCEPT_PATHS = {
767
+ contacts: SYNC_CONTACTS_PATH,
768
+ companies: SYNC_COMPANIES_PATH,
769
+ deals: SYNC_DEALS_PATH,
770
+ custom: SYNC_CUSTOM_PATH
771
+ };
627
772
  function resolveSampleSource(config, entityKey) {
628
- const entity = config.entities?.entities?.[entityKey];
629
- const src = config.source;
630
- if (!(entity && src)) return null;
631
- if (entity.concept === "contact" && src.contacts) return {
632
- source: src.contacts,
633
- path: SYNC_CONTACTS_PATH
634
- };
635
- if (entity.concept === "company" && src.companies) return {
636
- source: src.companies,
637
- path: SYNC_COMPANIES_PATH
773
+ const resolved = resolveEntitySource(config, entityKey);
774
+ if (!resolved) return null;
775
+ const path = CONCEPT_PATHS[resolved.expectedKey];
776
+ if (!path) return null;
777
+ return resolved.expectedKey === "custom" ? {
778
+ source: resolved.source,
779
+ path,
780
+ objectType: resolved.source.objectType
781
+ } : {
782
+ source: resolved.source,
783
+ path
638
784
  };
639
- if (entity.concept === "deal" && src.deals) return {
640
- source: src.deals,
641
- path: SYNC_DEALS_PATH
642
- };
643
- if (entity.concept === "custom") {
644
- const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
645
- return custom ? {
646
- source: custom,
647
- path: SYNC_CUSTOM_PATH,
648
- objectType: custom.objectType
649
- } : null;
650
- }
651
- return null;
652
785
  }
653
786
  const PREVIEW_LIMIT = 5;
654
787
  function resolvePreviewSource(config, sourceTable) {
@@ -677,7 +810,7 @@ async function handlePreviewRequest(config, preview) {
677
810
  if (!source) return jsonResponse({ error: `No configured source for table "${preview.sourceTable}"` }, 400);
678
811
  const idField = source.idField ?? "id";
679
812
  const filterColumns = preview.filter ? Object.keys(preview.filter).filter((k) => k !== "$raw") : [];
680
- const needed = new Set([
813
+ const needed = /* @__PURE__ */ new Set([
681
814
  idField,
682
815
  ...Object.values(preview.fieldMap),
683
816
  ...filterColumns
@@ -939,3 +1072,9 @@ exports.ClivlyNetworkError = ClivlyNetworkError;
939
1072
  exports.ClivlyRateLimitError = ClivlyRateLimitError;
940
1073
  exports.createChatSessionHandler = require_chat_session.createChatSessionHandler;
941
1074
  exports.createClivlySDK = createClivlySDK;
1075
+ Object.defineProperty(exports, "defineClivlyConfig", {
1076
+ enumerable: true,
1077
+ get: function() {
1078
+ return _clivly_core_entity_config.defineClivlyConfig;
1079
+ }
1080
+ });
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-jMafEyTv.cjs";
1
+ import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-DTaz5mLi.cjs";
2
2
  import { createChatSessionHandler } from "./chat-session.cjs";
3
-
3
+ import { ClivlyEntitiesConfig, defineClivlyConfig } from "@clivly/core/entity-config";
4
4
  //#region src/errors.d.ts
5
5
  declare class ClivlyError extends Error {
6
6
  /** HTTP status, or null when the request never completed (network error). */
@@ -31,4 +31,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
31
31
  */
32
32
  declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
33
33
  //#endregion
34
- export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK };
34
+ export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-B7wBCHRT.mjs";
1
+ import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-DTaz5mLi.mjs";
2
2
  import { createChatSessionHandler } from "./chat-session.mjs";
3
-
3
+ import { ClivlyEntitiesConfig, defineClivlyConfig } from "@clivly/core/entity-config";
4
4
  //#region src/errors.d.ts
5
5
  declare class ClivlyError extends Error {
6
6
  /** HTTP status, or null when the request never completed (network error). */
@@ -31,4 +31,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
31
31
  */
32
32
  declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
33
33
  //#endregion
34
- export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK };
34
+ export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
package/dist/index.mjs CHANGED
@@ -1,5 +1,85 @@
1
- import { t as DRIZZLE_META } from "./drizzle-meta-DMx_9nlj.mjs";
1
+ import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
2
2
  import { createChatSessionHandler } from "./chat-session.mjs";
3
+ import { defineClivlyConfig } from "@clivly/core/entity-config";
4
+ //#region src/entity-source.ts
5
+ const CONCEPT_KEYS = {
6
+ contact: "contacts",
7
+ company: "companies",
8
+ deal: "deals"
9
+ };
10
+ function resolveEntitySource(config, entityKey) {
11
+ const entity = config.entities?.entities?.[entityKey];
12
+ const src = config.source;
13
+ if (!(entity && src)) return null;
14
+ const conceptKey = CONCEPT_KEYS[entity.concept];
15
+ if (conceptKey) {
16
+ const source = src[conceptKey];
17
+ return source ? {
18
+ concept: entity.concept,
19
+ expectedKey: conceptKey,
20
+ source
21
+ } : null;
22
+ }
23
+ if (entity.concept === "custom") {
24
+ const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
25
+ return match ? {
26
+ concept: "custom",
27
+ expectedKey: "custom",
28
+ source: match
29
+ } : null;
30
+ }
31
+ return null;
32
+ }
33
+ /** The `config.source` key an entity's concept requires, present or not. */
34
+ function expectedSourceKey(concept) {
35
+ return CONCEPT_KEYS[concept] ?? "custom";
36
+ }
37
+ //#endregion
38
+ //#region src/concept-coverage.ts
39
+ const CHECK_NAME = "Entity → source coverage";
40
+ function configuredKeys(config) {
41
+ const src = config.source;
42
+ if (!src) return [];
43
+ return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
44
+ }
45
+ /**
46
+ * Every configured entity must resolve to a source, or it silently syncs
47
+ * nothing. This is the check that catches `source` keyed by entity name rather
48
+ * than by CRM concept — a mistake that otherwise shows up only as a run that
49
+ * succeeds with zero rows.
50
+ */
51
+ function checkConceptCoverage(config) {
52
+ const entities = config.entities?.entities ?? {};
53
+ const keys = Object.keys(entities);
54
+ if (keys.length === 0) return {
55
+ name: CHECK_NAME,
56
+ ok: false,
57
+ skipped: true,
58
+ detail: "No entities configured — nothing to cross-check."
59
+ };
60
+ const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
61
+ if (unresolved.length === 0) return {
62
+ name: CHECK_NAME,
63
+ ok: true,
64
+ detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
65
+ };
66
+ const available = configuredKeys(config);
67
+ const listed = available.length > 0 ? available.join(", ") : "(none)";
68
+ const problems = unresolved.map((key) => {
69
+ const concept = entities[key]?.concept ?? "unknown";
70
+ if (concept === "custom") {
71
+ const custom = config.source?.custom ?? [];
72
+ if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
73
+ }
74
+ return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
75
+ });
76
+ return {
77
+ name: CHECK_NAME,
78
+ ok: false,
79
+ detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
80
+ };
81
+ }
82
+ //#endregion
3
83
  //#region src/custom-slug.ts
4
84
  const NON_ALNUM = /[^a-z0-9]+/g;
5
85
  const EDGE_HYPHENS = /(^-|-$)/g;
@@ -80,6 +160,56 @@ function describeFetchError(entity, error) {
80
160
  return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
81
161
  }
82
162
  //#endregion
163
+ //#region src/mapping-dry-run.ts
164
+ /**
165
+ * Fetch one real row and push it through the entity's field map.
166
+ *
167
+ * The distinction from "Sample fetch": that proves the query runs, this proves
168
+ * the row that comes back actually carries the columns `fields` names. A
169
+ * mapping that points at a column the row lacks produces empty CRM records
170
+ * rather than an error, so it has to be caught here.
171
+ *
172
+ * An empty table SKIPS rather than fails — a fresh database is not a
173
+ * misconfiguration, and failing on it would train people to ignore the check.
174
+ */
175
+ async function runMappingDryRun(config, entityKey) {
176
+ const name = `Field mapping dry run (${entityKey})`;
177
+ const resolved = resolveEntitySource(config, entityKey);
178
+ if (!resolved) return {
179
+ name,
180
+ ok: false,
181
+ skipped: true,
182
+ detail: `"${entityKey}" resolves to no source — see the coverage check above.`
183
+ };
184
+ const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
185
+ let rows;
186
+ try {
187
+ rows = await resolved.source.fetchPage({
188
+ since: null,
189
+ limit: 1
190
+ });
191
+ } catch (error) {
192
+ return {
193
+ name,
194
+ ok: false,
195
+ detail: describeFetchError(entityKey, error)
196
+ };
197
+ }
198
+ const row = rows[0];
199
+ if (row === void 0) return {
200
+ name,
201
+ ok: false,
202
+ skipped: true,
203
+ detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
204
+ };
205
+ const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
206
+ return {
207
+ name,
208
+ ok: missing.length === 0,
209
+ detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
210
+ };
211
+ }
212
+ //#endregion
83
213
  //#region src/namespace-probe.ts
84
214
  /**
85
215
  * Verify the tables the integration depends on are genuinely reachable:
@@ -170,6 +300,11 @@ async function namespaceCheck(config) {
170
300
  detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
171
301
  };
172
302
  }
303
+ async function mappingDryRunChecks(config) {
304
+ const checks = [];
305
+ for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
306
+ return checks;
307
+ }
173
308
  /**
174
309
  * Run the read-only setup checks and return a structured report. Every check
175
310
  * captures its own error into `detail` rather than throwing, so one failure never
@@ -196,6 +331,7 @@ async function runDoctor(args) {
196
331
  ok: sources.length > 0,
197
332
  detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
198
333
  });
334
+ checks.push(checkConceptCoverage(config));
199
335
  for (const source of sources) {
200
336
  const meta = source[DRIZZLE_META];
201
337
  if (!meta) {
@@ -234,6 +370,7 @@ async function runDoctor(args) {
234
370
  detail: describeFetchError(source.entity, error)
235
371
  });
236
372
  }
373
+ checks.push(...await mappingDryRunChecks(config));
237
374
  const result = await connect();
238
375
  checks.push({
239
376
  name: "Heartbeat",
@@ -277,6 +414,7 @@ async function syncSource(args) {
277
414
  const idField = source.idField ?? DEFAULT_ID_FIELD;
278
415
  let cursor = since;
279
416
  let pushed = 0;
417
+ const resumedFrom = since === null ? null : since.time.toISOString();
280
418
  for (;;) {
281
419
  const rows = await source.fetchPage({
282
420
  since: cursor,
@@ -288,6 +426,8 @@ async function syncSource(args) {
288
426
  source: source.entity,
289
427
  rows: capped,
290
428
  mode: "delta",
429
+ fetched: rows.length,
430
+ cursor: resumedFrom,
291
431
  ...objectType ? { objectType } : {}
292
432
  })) return {
293
433
  pushed,
@@ -330,6 +470,8 @@ async function fullSyncSource(args) {
330
470
  source: source.entity,
331
471
  rows: all,
332
472
  mode: "full",
473
+ fetched: all.length,
474
+ cursor: null,
333
475
  ...objectType ? { objectType } : {}
334
476
  });
335
477
  return {
@@ -394,9 +536,7 @@ function observedTriggerUrl(req) {
394
536
  const proto = forwardedProto || requestUrl.protocol.replace(":", "");
395
537
  try {
396
538
  return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
397
- } catch {
398
- return;
399
- }
539
+ } catch {}
400
540
  }
401
541
  /**
402
542
  * Decide which trigger URL to report, in precedence order:
@@ -594,9 +734,7 @@ function connectReason(status) {
594
734
  async function resolveOrg(response) {
595
735
  try {
596
736
  return (await response.json())?.org;
597
- } catch {
598
- return;
599
- }
737
+ } catch {}
600
738
  }
601
739
  function retryAfterMs(res) {
602
740
  const header = res.headers.get("retry-after");
@@ -619,35 +757,30 @@ function createFetcher(retry) {
619
757
  }
620
758
  if (!(res === null || isRetryableStatus(res.status)) || attempt >= maxAttempts) return res;
621
759
  const backoff = baseDelayMs * 2 ** (attempt - 1) + Math.random() * baseDelayMs;
622
- await sleep(res ? retryAfterMs(res) ?? backoff : backoff);
760
+ const wait = res ? retryAfterMs(res) ?? backoff : backoff;
761
+ await sleep(wait);
623
762
  }
624
763
  };
625
764
  }
765
+ const CONCEPT_PATHS = {
766
+ contacts: SYNC_CONTACTS_PATH,
767
+ companies: SYNC_COMPANIES_PATH,
768
+ deals: SYNC_DEALS_PATH,
769
+ custom: SYNC_CUSTOM_PATH
770
+ };
626
771
  function resolveSampleSource(config, entityKey) {
627
- const entity = config.entities?.entities?.[entityKey];
628
- const src = config.source;
629
- if (!(entity && src)) return null;
630
- if (entity.concept === "contact" && src.contacts) return {
631
- source: src.contacts,
632
- path: SYNC_CONTACTS_PATH
633
- };
634
- if (entity.concept === "company" && src.companies) return {
635
- source: src.companies,
636
- path: SYNC_COMPANIES_PATH
637
- };
638
- if (entity.concept === "deal" && src.deals) return {
639
- source: src.deals,
640
- path: SYNC_DEALS_PATH
772
+ const resolved = resolveEntitySource(config, entityKey);
773
+ if (!resolved) return null;
774
+ const path = CONCEPT_PATHS[resolved.expectedKey];
775
+ if (!path) return null;
776
+ return resolved.expectedKey === "custom" ? {
777
+ source: resolved.source,
778
+ path,
779
+ objectType: resolved.source.objectType
780
+ } : {
781
+ source: resolved.source,
782
+ path
641
783
  };
642
- if (entity.concept === "custom") {
643
- const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
644
- return custom ? {
645
- source: custom,
646
- path: SYNC_CUSTOM_PATH,
647
- objectType: custom.objectType
648
- } : null;
649
- }
650
- return null;
651
784
  }
652
785
  const PREVIEW_LIMIT = 5;
653
786
  function resolvePreviewSource(config, sourceTable) {
@@ -676,7 +809,7 @@ async function handlePreviewRequest(config, preview) {
676
809
  if (!source) return jsonResponse({ error: `No configured source for table "${preview.sourceTable}"` }, 400);
677
810
  const idField = source.idField ?? "id";
678
811
  const filterColumns = preview.filter ? Object.keys(preview.filter).filter((k) => k !== "$raw") : [];
679
- const needed = new Set([
812
+ const needed = /* @__PURE__ */ new Set([
680
813
  idField,
681
814
  ...Object.values(preview.fieldMap),
682
815
  ...filterColumns
@@ -932,4 +1065,4 @@ function createClivlySDK(config) {
932
1065
  };
933
1066
  }
934
1067
  //#endregion
935
- export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK };
1068
+ export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
@@ -1,5 +1,4 @@
1
1
  import { ClivlyEntitiesConfig } from "@clivly/core";
2
-
3
2
  //#region src/drizzle-meta.d.ts
4
3
  declare const DRIZZLE_META: "__clivly_drizzle_source_meta__";
5
4
  interface DrizzleSourceMeta {
@@ -85,9 +84,12 @@ interface ClivlySDKConfig {
85
84
  * above is unambiguous; nothing here is part of the supported API.
86
85
  */
87
86
  experimental?: {
88
- /** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */tunnelToken?: string; /** CRM write-back target — reserved. Not wired up yet. */
87
+ /** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */
88
+ tunnelToken?: string;
89
+ /** CRM write-back target — reserved. Not wired up yet. */
89
90
  writeBack?: {
90
- enabled?: boolean; /** Only crm_* tables are ever accepted; this narrows further. */
91
+ enabled?: boolean;
92
+ /** Only crm_* tables are ever accepted; this narrows further. */
91
93
  tables?: string[];
92
94
  };
93
95
  };
@@ -116,7 +118,9 @@ interface ClivlySDKConfig {
116
118
  * responses (e.g. 4xx other than 429) are returned immediately, not retried.
117
119
  */
118
120
  retry?: {
119
- /** Total attempts including the first. Defaults to 3. Min 1. */maxAttempts?: number; /** Base backoff in ms (doubled each attempt). Defaults to 500. */
121
+ /** Total attempts including the first. Defaults to 3. Min 1. */
122
+ maxAttempts?: number;
123
+ /** Base backoff in ms (doubled each attempt). Defaults to 500. */
120
124
  baseDelayMs?: number;
121
125
  };
122
126
  /** Tables/columns to report for entity discovery + mapping. */
@@ -126,7 +130,8 @@ interface ClivlySDKConfig {
126
130
  source?: {
127
131
  contacts: SyncSource;
128
132
  companies?: SyncSource;
129
- deals?: SyncSource; /** Org-defined custom objects to mirror. Each carries its object-type slug. */
133
+ deals?: SyncSource;
134
+ /** Org-defined custom objects to mirror. Each carries its object-type slug. */
130
135
  custom?: Array<SyncSource & {
131
136
  objectType: string;
132
137
  }>;
@@ -178,7 +183,9 @@ interface ClivlySDKConfig {
178
183
  /**
179
184
  * Shared secret Clivly Cloud presents (as `Authorization: Bearer <token>`) when
180
185
  * calling the `/clivly/auth/verify` endpoint. Required for the verify handler to
181
- * authenticate the caller; without it the handler fails closed (HTTP 503).
186
+ * authenticate the caller; without it the handler fails closed (HTTP 503). No
187
+ * cloud consumer calls this endpoint yet — it is an inert primitive reserved for
188
+ * a future CRM/dashboard auth-passthrough.
182
189
  */
183
190
  verifyToken?: string;
184
191
  }
@@ -201,6 +208,13 @@ interface CreateChatSessionHandlerOptions {
201
208
  apiKey: string;
202
209
  apiUrl?: string;
203
210
  fetchImpl?: typeof fetch;
211
+ /**
212
+ * Optional host-auth hook. When provided, it runs on the incoming browser
213
+ * request (session cookies intact) and, on a non-null `ClivlyUser`, the host
214
+ * vouches for that verified identity to Clivly Cloud. Absent/null/throwing =
215
+ * the visitor stays self-reported. Never fails the session.
216
+ */
217
+ resolveUser?: (req: Request) => Promise<ClivlyUser | null> | ClivlyUser | null;
204
218
  }
205
219
  interface DoctorCheck {
206
220
  /** Human-readable outcome — what passed, or why it failed. */
@@ -1,5 +1,4 @@
1
1
  import { ClivlyEntitiesConfig } from "@clivly/core";
2
-
3
2
  //#region src/drizzle-meta.d.ts
4
3
  declare const DRIZZLE_META: "__clivly_drizzle_source_meta__";
5
4
  interface DrizzleSourceMeta {
@@ -85,9 +84,12 @@ interface ClivlySDKConfig {
85
84
  * above is unambiguous; nothing here is part of the supported API.
86
85
  */
87
86
  experimental?: {
88
- /** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */tunnelToken?: string; /** CRM write-back target — reserved. Not wired up yet. */
87
+ /** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */
88
+ tunnelToken?: string;
89
+ /** CRM write-back target — reserved. Not wired up yet. */
89
90
  writeBack?: {
90
- enabled?: boolean; /** Only crm_* tables are ever accepted; this narrows further. */
91
+ enabled?: boolean;
92
+ /** Only crm_* tables are ever accepted; this narrows further. */
91
93
  tables?: string[];
92
94
  };
93
95
  };
@@ -116,7 +118,9 @@ interface ClivlySDKConfig {
116
118
  * responses (e.g. 4xx other than 429) are returned immediately, not retried.
117
119
  */
118
120
  retry?: {
119
- /** Total attempts including the first. Defaults to 3. Min 1. */maxAttempts?: number; /** Base backoff in ms (doubled each attempt). Defaults to 500. */
121
+ /** Total attempts including the first. Defaults to 3. Min 1. */
122
+ maxAttempts?: number;
123
+ /** Base backoff in ms (doubled each attempt). Defaults to 500. */
120
124
  baseDelayMs?: number;
121
125
  };
122
126
  /** Tables/columns to report for entity discovery + mapping. */
@@ -126,7 +130,8 @@ interface ClivlySDKConfig {
126
130
  source?: {
127
131
  contacts: SyncSource;
128
132
  companies?: SyncSource;
129
- deals?: SyncSource; /** Org-defined custom objects to mirror. Each carries its object-type slug. */
133
+ deals?: SyncSource;
134
+ /** Org-defined custom objects to mirror. Each carries its object-type slug. */
130
135
  custom?: Array<SyncSource & {
131
136
  objectType: string;
132
137
  }>;
@@ -178,7 +183,9 @@ interface ClivlySDKConfig {
178
183
  /**
179
184
  * Shared secret Clivly Cloud presents (as `Authorization: Bearer <token>`) when
180
185
  * calling the `/clivly/auth/verify` endpoint. Required for the verify handler to
181
- * authenticate the caller; without it the handler fails closed (HTTP 503).
186
+ * authenticate the caller; without it the handler fails closed (HTTP 503). No
187
+ * cloud consumer calls this endpoint yet — it is an inert primitive reserved for
188
+ * a future CRM/dashboard auth-passthrough.
182
189
  */
183
190
  verifyToken?: string;
184
191
  }
@@ -201,6 +208,13 @@ interface CreateChatSessionHandlerOptions {
201
208
  apiKey: string;
202
209
  apiUrl?: string;
203
210
  fetchImpl?: typeof fetch;
211
+ /**
212
+ * Optional host-auth hook. When provided, it runs on the incoming browser
213
+ * request (session cookies intact) and, on a non-null `ClivlyUser`, the host
214
+ * vouches for that verified identity to Clivly Cloud. Absent/null/throwing =
215
+ * the visitor stays self-reported. Never fails the session.
216
+ */
217
+ resolveUser?: (req: Request) => Promise<ClivlyUser | null> | ClivlyUser | null;
204
218
  }
205
219
  interface DoctorCheck {
206
220
  /** Human-readable outcome — what passed, or why it failed. */
package/dist/vite.d.cts CHANGED
@@ -1,6 +1,5 @@
1
- import { a as ClivlySDK } from "./types-jMafEyTv.cjs";
1
+ import { a as ClivlySDK } from "./types-DTaz5mLi.cjs";
2
2
  import { Plugin } from "vite";
3
-
4
3
  //#region src/vite.d.ts
5
4
  interface ClivlyViteOptions {
6
5
  /** Log prefix for Clivly's boot lines. Defaults to "clivly". */
package/dist/vite.d.mts CHANGED
@@ -1,6 +1,5 @@
1
- import { a as ClivlySDK } from "./types-B7wBCHRT.mjs";
1
+ import { a as ClivlySDK } from "./types-DTaz5mLi.mjs";
2
2
  import { Plugin } from "vite";
3
-
4
3
  //#region src/vite.d.ts
5
4
  interface ClivlyViteOptions {
6
5
  /** Log prefix for Clivly's boot lines. Defaults to "clivly". */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
@@ -55,10 +55,10 @@
55
55
  "access": "public"
56
56
  },
57
57
  "dependencies": {
58
- "@clivly/core": "0.4.0"
58
+ "@clivly/core": "0.7.1"
59
59
  },
60
60
  "peerDependencies": {
61
- "drizzle-orm": ">=0.30",
61
+ "drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
62
62
  "vite": ">=5"
63
63
  },
64
64
  "peerDependenciesMeta": {
@@ -72,11 +72,14 @@
72
72
  "devDependencies": {
73
73
  "@electric-sql/pglite": "^0.5.4",
74
74
  "drizzle-orm": "^0.45.2",
75
- "tsdown": "^0.21.9",
75
+ "tsdown": "^0.22.14",
76
76
  "typescript": "^6",
77
77
  "vite": "^8.0.8",
78
78
  "vitest": "^3.2.0"
79
79
  },
80
+ "engines": {
81
+ "node": ">=22"
82
+ },
80
83
  "scripts": {
81
84
  "build": "tsdown",
82
85
  "check-types": "tsc --noEmit",