@clivly/sdk 0.10.7 → 0.10.9
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/dist/chat-session.cjs +35 -4
- package/dist/chat-session.d.cts +1 -1
- package/dist/chat-session.d.mts +1 -1
- package/dist/chat-session.mjs +35 -4
- package/dist/drizzle.cjs +2 -0
- package/dist/drizzle.d.cts +1 -1
- package/dist/drizzle.d.mts +1 -1
- package/dist/drizzle.mjs +2 -0
- package/dist/index.cjs +78 -12
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +78 -12
- package/dist/{types-B7HKG2Ph.d.cts → types-CvhxV-IX.d.cts} +16 -0
- package/dist/{types-B7HKG2Ph.d.mts → types-CvhxV-IX.d.mts} +16 -0
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.mts +1 -1
- package/package.json +4 -3
package/dist/chat-session.cjs
CHANGED
|
@@ -12,8 +12,18 @@ function jsonResponse(body, status) {
|
|
|
12
12
|
function normalizeApiUrl(apiUrl) {
|
|
13
13
|
return apiUrl.replace(TRAILING_SLASH, "");
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The origin *claimed* by the request, for reporting upstream.
|
|
17
|
+
*
|
|
18
|
+
* Falls back to the request URL so Clivly still receives an origin for a
|
|
19
|
+
* non-browser caller. This value is reported, never trusted: only a real
|
|
20
|
+
* `Origin` header is evidence of where a request came from, so the access
|
|
21
|
+
* decision uses {@link trustedOrigin} instead. Keeping the two apart is the
|
|
22
|
+
* point — conflating them let a caller that simply omitted the header be
|
|
23
|
+
* judged against the server's own origin, which is usually in the allowlist.
|
|
24
|
+
*/
|
|
25
|
+
function reportedOrigin(req) {
|
|
26
|
+
const headerOrigin = trustedOrigin(req);
|
|
17
27
|
if (headerOrigin) return headerOrigin;
|
|
18
28
|
try {
|
|
19
29
|
return new URL(req.url).origin;
|
|
@@ -21,6 +31,15 @@ function resolveOrigin(req) {
|
|
|
21
31
|
return null;
|
|
22
32
|
}
|
|
23
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The only origin worth an access decision: one the browser set itself.
|
|
36
|
+
*
|
|
37
|
+
* A missing header means "not a browser request we can vouch for", which is
|
|
38
|
+
* untrusted — never the server's own origin.
|
|
39
|
+
*/
|
|
40
|
+
function trustedOrigin(req) {
|
|
41
|
+
return req.headers.get("origin");
|
|
42
|
+
}
|
|
24
43
|
function isChatSessionRequestBody(value) {
|
|
25
44
|
if (typeof value !== "object" || value === null) return false;
|
|
26
45
|
const body = value;
|
|
@@ -31,6 +50,17 @@ function isChatSessionRequestBody(value) {
|
|
|
31
50
|
if (typeof visitor.email !== "string" || visitor.email.trim().length === 0) return false;
|
|
32
51
|
return body.visitorToken === void 0 || body.visitorToken === null || typeof body.visitorToken === "string";
|
|
33
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Judge a request against the configured allowlist.
|
|
55
|
+
*
|
|
56
|
+
* `origin` must be a *trusted* origin (see {@link trustedOrigin}) — passing
|
|
57
|
+
* the reported one would reinstate the bypass this guards against.
|
|
58
|
+
*
|
|
59
|
+
* An unset allowlist still admits every origin, because requiring it would
|
|
60
|
+
* break every existing deployment on upgrade. The unset case is instead
|
|
61
|
+
* warned about once at construction, so the permissive default is loud
|
|
62
|
+
* rather than silent.
|
|
63
|
+
*/
|
|
34
64
|
function originAllowed(origin, allowedOrigins) {
|
|
35
65
|
if (!allowedOrigins || allowedOrigins.length === 0) return true;
|
|
36
66
|
if (!origin) return false;
|
|
@@ -38,6 +68,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
38
68
|
}
|
|
39
69
|
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch, resolveUser }) {
|
|
40
70
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
71
|
+
if (!allowedOrigins || allowedOrigins.length === 0) console.warn("[clivly] createChatSessionHandler has no allowedOrigins; every origin can open a chat session against this workspace. Pass allowedOrigins to restrict it.");
|
|
41
72
|
return async (request) => {
|
|
42
73
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
43
74
|
status: 405,
|
|
@@ -46,8 +77,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
46
77
|
allow: "POST"
|
|
47
78
|
}
|
|
48
79
|
});
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
if (!originAllowed(trustedOrigin(request), allowedOrigins)) return jsonResponse({ error: "origin_not_allowed" }, 403);
|
|
81
|
+
const origin = reportedOrigin(request);
|
|
51
82
|
let body;
|
|
52
83
|
try {
|
|
53
84
|
body = await request.json();
|
package/dist/chat-session.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as CreateChatSessionHandlerOptions } from "./types-
|
|
1
|
+
import { c as CreateChatSessionHandlerOptions } from "./types-CvhxV-IX.cjs";
|
|
2
2
|
//#region src/chat-session.d.ts
|
|
3
3
|
declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
|
|
4
4
|
//#endregion
|
package/dist/chat-session.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as CreateChatSessionHandlerOptions } from "./types-
|
|
1
|
+
import { c as CreateChatSessionHandlerOptions } from "./types-CvhxV-IX.mjs";
|
|
2
2
|
//#region src/chat-session.d.ts
|
|
3
3
|
declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
|
|
4
4
|
//#endregion
|
package/dist/chat-session.mjs
CHANGED
|
@@ -11,8 +11,18 @@ function jsonResponse(body, status) {
|
|
|
11
11
|
function normalizeApiUrl(apiUrl) {
|
|
12
12
|
return apiUrl.replace(TRAILING_SLASH, "");
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The origin *claimed* by the request, for reporting upstream.
|
|
16
|
+
*
|
|
17
|
+
* Falls back to the request URL so Clivly still receives an origin for a
|
|
18
|
+
* non-browser caller. This value is reported, never trusted: only a real
|
|
19
|
+
* `Origin` header is evidence of where a request came from, so the access
|
|
20
|
+
* decision uses {@link trustedOrigin} instead. Keeping the two apart is the
|
|
21
|
+
* point — conflating them let a caller that simply omitted the header be
|
|
22
|
+
* judged against the server's own origin, which is usually in the allowlist.
|
|
23
|
+
*/
|
|
24
|
+
function reportedOrigin(req) {
|
|
25
|
+
const headerOrigin = trustedOrigin(req);
|
|
16
26
|
if (headerOrigin) return headerOrigin;
|
|
17
27
|
try {
|
|
18
28
|
return new URL(req.url).origin;
|
|
@@ -20,6 +30,15 @@ function resolveOrigin(req) {
|
|
|
20
30
|
return null;
|
|
21
31
|
}
|
|
22
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The only origin worth an access decision: one the browser set itself.
|
|
35
|
+
*
|
|
36
|
+
* A missing header means "not a browser request we can vouch for", which is
|
|
37
|
+
* untrusted — never the server's own origin.
|
|
38
|
+
*/
|
|
39
|
+
function trustedOrigin(req) {
|
|
40
|
+
return req.headers.get("origin");
|
|
41
|
+
}
|
|
23
42
|
function isChatSessionRequestBody(value) {
|
|
24
43
|
if (typeof value !== "object" || value === null) return false;
|
|
25
44
|
const body = value;
|
|
@@ -30,6 +49,17 @@ function isChatSessionRequestBody(value) {
|
|
|
30
49
|
if (typeof visitor.email !== "string" || visitor.email.trim().length === 0) return false;
|
|
31
50
|
return body.visitorToken === void 0 || body.visitorToken === null || typeof body.visitorToken === "string";
|
|
32
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Judge a request against the configured allowlist.
|
|
54
|
+
*
|
|
55
|
+
* `origin` must be a *trusted* origin (see {@link trustedOrigin}) — passing
|
|
56
|
+
* the reported one would reinstate the bypass this guards against.
|
|
57
|
+
*
|
|
58
|
+
* An unset allowlist still admits every origin, because requiring it would
|
|
59
|
+
* break every existing deployment on upgrade. The unset case is instead
|
|
60
|
+
* warned about once at construction, so the permissive default is loud
|
|
61
|
+
* rather than silent.
|
|
62
|
+
*/
|
|
33
63
|
function originAllowed(origin, allowedOrigins) {
|
|
34
64
|
if (!allowedOrigins || allowedOrigins.length === 0) return true;
|
|
35
65
|
if (!origin) return false;
|
|
@@ -37,6 +67,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
37
67
|
}
|
|
38
68
|
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch, resolveUser }) {
|
|
39
69
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
70
|
+
if (!allowedOrigins || allowedOrigins.length === 0) console.warn("[clivly] createChatSessionHandler has no allowedOrigins; every origin can open a chat session against this workspace. Pass allowedOrigins to restrict it.");
|
|
40
71
|
return async (request) => {
|
|
41
72
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
42
73
|
status: 405,
|
|
@@ -45,8 +76,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
45
76
|
allow: "POST"
|
|
46
77
|
}
|
|
47
78
|
});
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
if (!originAllowed(trustedOrigin(request), allowedOrigins)) return jsonResponse({ error: "origin_not_allowed" }, 403);
|
|
80
|
+
const origin = reportedOrigin(request);
|
|
50
81
|
let body;
|
|
51
82
|
try {
|
|
52
83
|
body = await request.json();
|
package/dist/drizzle.cjs
CHANGED
|
@@ -59,6 +59,7 @@ function fromDrizzleProjection(db, projection, opts) {
|
|
|
59
59
|
const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
|
|
60
60
|
return {
|
|
61
61
|
entity: opts.entity,
|
|
62
|
+
pushSource: projection.baseTable,
|
|
62
63
|
cursorField: cursorToken,
|
|
63
64
|
idField: "id",
|
|
64
65
|
...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
|
|
@@ -88,6 +89,7 @@ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromC
|
|
|
88
89
|
const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
|
|
89
90
|
return {
|
|
90
91
|
entity: opts.entity,
|
|
92
|
+
pushSource: projection.baseTable,
|
|
91
93
|
cursorField: "id",
|
|
92
94
|
idField: "id",
|
|
93
95
|
...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
|
package/dist/drizzle.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as SyncSource, y as NamespaceIntrospector } from "./types-
|
|
1
|
+
import { _ as SyncSource, y as NamespaceIntrospector } from "./types-CvhxV-IX.cjs";
|
|
2
2
|
import { ProjectionMapping, ProjectionRef } from "@clivly/core";
|
|
3
3
|
import { Table } from "drizzle-orm";
|
|
4
4
|
import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
|
package/dist/drizzle.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as SyncSource, y as NamespaceIntrospector } from "./types-
|
|
1
|
+
import { _ as SyncSource, y as NamespaceIntrospector } from "./types-CvhxV-IX.mjs";
|
|
2
2
|
import { ProjectionMapping, ProjectionRef } from "@clivly/core";
|
|
3
3
|
import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
4
4
|
import { Table } from "drizzle-orm";
|
package/dist/drizzle.mjs
CHANGED
|
@@ -58,6 +58,7 @@ function fromDrizzleProjection(db, projection, opts) {
|
|
|
58
58
|
const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
|
|
59
59
|
return {
|
|
60
60
|
entity: opts.entity,
|
|
61
|
+
pushSource: projection.baseTable,
|
|
61
62
|
cursorField: cursorToken,
|
|
62
63
|
idField: "id",
|
|
63
64
|
...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
|
|
@@ -87,6 +88,7 @@ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromC
|
|
|
87
88
|
const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
|
|
88
89
|
return {
|
|
89
90
|
entity: opts.entity,
|
|
91
|
+
pushSource: projection.baseTable,
|
|
90
92
|
cursorField: "id",
|
|
91
93
|
idField: "id",
|
|
92
94
|
...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
|
package/dist/index.cjs
CHANGED
|
@@ -79,6 +79,43 @@ function resolveEntitySource(config, entityKey) {
|
|
|
79
79
|
function expectedSourceKey(concept) {
|
|
80
80
|
return CONCEPT_KEYS[concept] ?? "custom";
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The `source` label a push must carry for the server to find its mapping.
|
|
84
|
+
*
|
|
85
|
+
* `entity` is the SDK's own name for a source — the cursor-store key, and what
|
|
86
|
+
* the developer wrote in `config.source`. It is NOT always what the server
|
|
87
|
+
* filed the mapping under: for a projection, `ensureSchema` stores
|
|
88
|
+
* `sourceTable` as `projection.baseTable`, because a projection entity's key
|
|
89
|
+
* doubles as a kebab-case object slug and cannot also be the snake_case
|
|
90
|
+
* `projection.entityKey`.
|
|
91
|
+
*
|
|
92
|
+
* Pushing under `entity` therefore dropped 100% of a projection's rows while
|
|
93
|
+
* every local signal said the setup was correct. One function so the three
|
|
94
|
+
* push sites cannot drift apart again.
|
|
95
|
+
*/
|
|
96
|
+
function pushSourceOf(source) {
|
|
97
|
+
return source.pushSource ?? source.entity;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The push label for a custom source, resolved against the config.
|
|
101
|
+
*
|
|
102
|
+
* `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
|
|
103
|
+
* can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
|
|
104
|
+
* what the README's own example leads you to, and next-finance did exactly
|
|
105
|
+
* that. Such a source has no `pushSource`, so the label has to come from the
|
|
106
|
+
* one place that is true either way: the entity's declared
|
|
107
|
+
* `projection.baseTable`.
|
|
108
|
+
*
|
|
109
|
+
* Matched on the source's `entity`, which for a custom source is also the
|
|
110
|
+
* config entity key (that is what `resolveEntitySource` above pairs them by).
|
|
111
|
+
* Falls back to `pushSourceOf`, so a non-projection custom source and an
|
|
112
|
+
* already-stamped one both keep their current label.
|
|
113
|
+
*/
|
|
114
|
+
function customPushSource(config, source) {
|
|
115
|
+
if (source.pushSource) return source.pushSource;
|
|
116
|
+
const entities = config.entities?.entities;
|
|
117
|
+
return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
|
|
118
|
+
}
|
|
82
119
|
//#endregion
|
|
83
120
|
//#region src/concept-coverage.ts
|
|
84
121
|
const CHECK_NAME$1 = "Entity → source coverage";
|
|
@@ -357,11 +394,15 @@ function errorMessage(error) {
|
|
|
357
394
|
/**
|
|
358
395
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
359
396
|
*
|
|
360
|
-
* `source` is the
|
|
397
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
361
398
|
* `ingestPush` as `input.source` and is compared against `mapping.sourceTable`.
|
|
399
|
+
* Usually that is `SyncSource.entity`, but a projection pushes under its base
|
|
400
|
+
* table, so this asks `customPushSource` rather than reading `entity` directly.
|
|
401
|
+
*
|
|
362
402
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
363
|
-
* the two are routinely conflated, and using the wrong one
|
|
364
|
-
*
|
|
403
|
+
* the two are routinely conflated, and using the wrong one reports a correctly
|
|
404
|
+
* mapped workspace as unmapped — which is exactly what happened to every
|
|
405
|
+
* projection until this used the push label.
|
|
365
406
|
*/
|
|
366
407
|
function readinessEntitiesFromConfig(config) {
|
|
367
408
|
const entities = [];
|
|
@@ -370,7 +411,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
370
411
|
if (!resolved) continue;
|
|
371
412
|
const objectTypeSlug = resolved.source.objectType;
|
|
372
413
|
entities.push({
|
|
373
|
-
source: resolved.source
|
|
414
|
+
source: customPushSource(config, resolved.source),
|
|
374
415
|
concept: resolved.concept,
|
|
375
416
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
376
417
|
});
|
|
@@ -1205,7 +1246,7 @@ async function pushRemoteProjections(args) {
|
|
|
1205
1246
|
}
|
|
1206
1247
|
try {
|
|
1207
1248
|
const ok = await args.push(SYNC_CUSTOM_PATH$1, {
|
|
1208
|
-
source: source
|
|
1249
|
+
source: pushSourceOf(source),
|
|
1209
1250
|
rows: all,
|
|
1210
1251
|
mode: "full",
|
|
1211
1252
|
fetched: all.length,
|
|
@@ -1280,7 +1321,7 @@ async function syncSource(args) {
|
|
|
1280
1321
|
if (rows.length === 0) break;
|
|
1281
1322
|
const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
|
|
1282
1323
|
if (!await push(path, {
|
|
1283
|
-
source: source
|
|
1324
|
+
source: pushSourceOf(source),
|
|
1284
1325
|
rows: capped,
|
|
1285
1326
|
mode: "delta",
|
|
1286
1327
|
fetched: rows.length,
|
|
@@ -1324,7 +1365,7 @@ async function fullSyncSource(args) {
|
|
|
1324
1365
|
ok: true
|
|
1325
1366
|
};
|
|
1326
1367
|
const ok = await push(path, {
|
|
1327
|
-
source: source
|
|
1368
|
+
source: pushSourceOf(source),
|
|
1328
1369
|
rows: all,
|
|
1329
1370
|
mode: "full",
|
|
1330
1371
|
fetched: all.length,
|
|
@@ -1577,6 +1618,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1577
1618
|
function triggerErrorStatus(error) {
|
|
1578
1619
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1579
1620
|
}
|
|
1621
|
+
const SENTENCE_END_RE = /[.!?]$/;
|
|
1622
|
+
async function readPushFailureMessage(response) {
|
|
1623
|
+
if (!response || typeof response.json !== "function") return null;
|
|
1624
|
+
try {
|
|
1625
|
+
const body = await response.json();
|
|
1626
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1627
|
+
const { message } = body;
|
|
1628
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1580
1633
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1581
1634
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1582
1635
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1799,17 +1852,27 @@ function createClivlySDK(config) {
|
|
|
1799
1852
|
}, response);
|
|
1800
1853
|
return true;
|
|
1801
1854
|
}
|
|
1802
|
-
lastPushFailure ??= {
|
|
1855
|
+
lastPushFailure ??= {
|
|
1856
|
+
status: response?.status ?? null,
|
|
1857
|
+
message: await readPushFailureMessage(response)
|
|
1858
|
+
};
|
|
1803
1859
|
return false;
|
|
1804
1860
|
};
|
|
1805
1861
|
const describePushFailure = (entities) => {
|
|
1806
1862
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1807
1863
|
const status = lastPushFailure?.status ?? null;
|
|
1864
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1808
1865
|
let cause;
|
|
1809
1866
|
if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
|
|
1810
1867
|
else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
|
|
1868
|
+
else if (serverMessage) cause = `Clivly returned HTTP ${status}: ${serverMessage}`;
|
|
1811
1869
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1812
|
-
|
|
1870
|
+
const separator = SENTENCE_END_RE.test(cause) ? "" : ".";
|
|
1871
|
+
return `${base}: ${cause}${separator} Run \`clivly doctor\` to diagnose.`;
|
|
1872
|
+
};
|
|
1873
|
+
const pushFailureError = (message) => {
|
|
1874
|
+
const status = lastPushFailure?.status ?? null;
|
|
1875
|
+
return status !== null && status >= CLIENT_ERROR_FLOOR && status < RETRYABLE_STATUS_FLOOR ? new TriggerRequestError(status, message) : new Error(message);
|
|
1813
1876
|
};
|
|
1814
1877
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1815
1878
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -1835,7 +1898,10 @@ function createClivlySDK(config) {
|
|
|
1835
1898
|
};
|
|
1836
1899
|
const syncCustomSources = async (customSources, mode, runId) => {
|
|
1837
1900
|
const failed = [];
|
|
1838
|
-
for (const customSource of customSources ?? [])
|
|
1901
|
+
for (const customSource of customSources ?? []) {
|
|
1902
|
+
customSource.pushSource = customPushSource(config, customSource);
|
|
1903
|
+
if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
1904
|
+
}
|
|
1839
1905
|
return failed;
|
|
1840
1906
|
};
|
|
1841
1907
|
const syncRemoteSources = async (sources, runId) => {
|
|
@@ -1896,7 +1962,7 @@ function createClivlySDK(config) {
|
|
|
1896
1962
|
failed: true,
|
|
1897
1963
|
error: message
|
|
1898
1964
|
};
|
|
1899
|
-
throw
|
|
1965
|
+
throw pushFailureError(message);
|
|
1900
1966
|
}
|
|
1901
1967
|
lastPush = { failed: false };
|
|
1902
1968
|
};
|
|
@@ -1904,7 +1970,7 @@ function createClivlySDK(config) {
|
|
|
1904
1970
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1905
1971
|
if (!resolved) throw new TriggerRequestError(400, `Clivly sync: no source is configured for entity "${entityKey}". The dashboard maps this table, but clivly.config declares no matching entity — add it to \`entities\`, or remove the mapping in Integrations → Mapping.`);
|
|
1906
1972
|
lastPushFailure = null;
|
|
1907
|
-
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw
|
|
1973
|
+
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
|
|
1908
1974
|
};
|
|
1909
1975
|
const describeSyncTrigger = () => ({
|
|
1910
1976
|
configured: config.syncTrigger?.url,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-
|
|
1
|
+
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-CvhxV-IX.cjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.cjs";
|
|
3
3
|
import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
4
|
//#region src/trigger-url.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-
|
|
1
|
+
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-CvhxV-IX.mjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
3
|
import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
4
|
//#region src/trigger-url.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -78,6 +78,43 @@ function resolveEntitySource(config, entityKey) {
|
|
|
78
78
|
function expectedSourceKey(concept) {
|
|
79
79
|
return CONCEPT_KEYS[concept] ?? "custom";
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The `source` label a push must carry for the server to find its mapping.
|
|
83
|
+
*
|
|
84
|
+
* `entity` is the SDK's own name for a source — the cursor-store key, and what
|
|
85
|
+
* the developer wrote in `config.source`. It is NOT always what the server
|
|
86
|
+
* filed the mapping under: for a projection, `ensureSchema` stores
|
|
87
|
+
* `sourceTable` as `projection.baseTable`, because a projection entity's key
|
|
88
|
+
* doubles as a kebab-case object slug and cannot also be the snake_case
|
|
89
|
+
* `projection.entityKey`.
|
|
90
|
+
*
|
|
91
|
+
* Pushing under `entity` therefore dropped 100% of a projection's rows while
|
|
92
|
+
* every local signal said the setup was correct. One function so the three
|
|
93
|
+
* push sites cannot drift apart again.
|
|
94
|
+
*/
|
|
95
|
+
function pushSourceOf(source) {
|
|
96
|
+
return source.pushSource ?? source.entity;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The push label for a custom source, resolved against the config.
|
|
100
|
+
*
|
|
101
|
+
* `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
|
|
102
|
+
* can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
|
|
103
|
+
* what the README's own example leads you to, and next-finance did exactly
|
|
104
|
+
* that. Such a source has no `pushSource`, so the label has to come from the
|
|
105
|
+
* one place that is true either way: the entity's declared
|
|
106
|
+
* `projection.baseTable`.
|
|
107
|
+
*
|
|
108
|
+
* Matched on the source's `entity`, which for a custom source is also the
|
|
109
|
+
* config entity key (that is what `resolveEntitySource` above pairs them by).
|
|
110
|
+
* Falls back to `pushSourceOf`, so a non-projection custom source and an
|
|
111
|
+
* already-stamped one both keep their current label.
|
|
112
|
+
*/
|
|
113
|
+
function customPushSource(config, source) {
|
|
114
|
+
if (source.pushSource) return source.pushSource;
|
|
115
|
+
const entities = config.entities?.entities;
|
|
116
|
+
return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
|
|
117
|
+
}
|
|
81
118
|
//#endregion
|
|
82
119
|
//#region src/concept-coverage.ts
|
|
83
120
|
const CHECK_NAME$1 = "Entity → source coverage";
|
|
@@ -356,11 +393,15 @@ function errorMessage(error) {
|
|
|
356
393
|
/**
|
|
357
394
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
358
395
|
*
|
|
359
|
-
* `source` is the
|
|
396
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
360
397
|
* `ingestPush` as `input.source` and is compared against `mapping.sourceTable`.
|
|
398
|
+
* Usually that is `SyncSource.entity`, but a projection pushes under its base
|
|
399
|
+
* table, so this asks `customPushSource` rather than reading `entity` directly.
|
|
400
|
+
*
|
|
361
401
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
362
|
-
* the two are routinely conflated, and using the wrong one
|
|
363
|
-
*
|
|
402
|
+
* the two are routinely conflated, and using the wrong one reports a correctly
|
|
403
|
+
* mapped workspace as unmapped — which is exactly what happened to every
|
|
404
|
+
* projection until this used the push label.
|
|
364
405
|
*/
|
|
365
406
|
function readinessEntitiesFromConfig(config) {
|
|
366
407
|
const entities = [];
|
|
@@ -369,7 +410,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
369
410
|
if (!resolved) continue;
|
|
370
411
|
const objectTypeSlug = resolved.source.objectType;
|
|
371
412
|
entities.push({
|
|
372
|
-
source: resolved.source
|
|
413
|
+
source: customPushSource(config, resolved.source),
|
|
373
414
|
concept: resolved.concept,
|
|
374
415
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
375
416
|
});
|
|
@@ -1204,7 +1245,7 @@ async function pushRemoteProjections(args) {
|
|
|
1204
1245
|
}
|
|
1205
1246
|
try {
|
|
1206
1247
|
const ok = await args.push(SYNC_CUSTOM_PATH$1, {
|
|
1207
|
-
source: source
|
|
1248
|
+
source: pushSourceOf(source),
|
|
1208
1249
|
rows: all,
|
|
1209
1250
|
mode: "full",
|
|
1210
1251
|
fetched: all.length,
|
|
@@ -1279,7 +1320,7 @@ async function syncSource(args) {
|
|
|
1279
1320
|
if (rows.length === 0) break;
|
|
1280
1321
|
const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
|
|
1281
1322
|
if (!await push(path, {
|
|
1282
|
-
source: source
|
|
1323
|
+
source: pushSourceOf(source),
|
|
1283
1324
|
rows: capped,
|
|
1284
1325
|
mode: "delta",
|
|
1285
1326
|
fetched: rows.length,
|
|
@@ -1323,7 +1364,7 @@ async function fullSyncSource(args) {
|
|
|
1323
1364
|
ok: true
|
|
1324
1365
|
};
|
|
1325
1366
|
const ok = await push(path, {
|
|
1326
|
-
source: source
|
|
1367
|
+
source: pushSourceOf(source),
|
|
1327
1368
|
rows: all,
|
|
1328
1369
|
mode: "full",
|
|
1329
1370
|
fetched: all.length,
|
|
@@ -1576,6 +1617,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1576
1617
|
function triggerErrorStatus(error) {
|
|
1577
1618
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1578
1619
|
}
|
|
1620
|
+
const SENTENCE_END_RE = /[.!?]$/;
|
|
1621
|
+
async function readPushFailureMessage(response) {
|
|
1622
|
+
if (!response || typeof response.json !== "function") return null;
|
|
1623
|
+
try {
|
|
1624
|
+
const body = await response.json();
|
|
1625
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1626
|
+
const { message } = body;
|
|
1627
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
1628
|
+
} catch {
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1579
1632
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1580
1633
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1581
1634
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1798,17 +1851,27 @@ function createClivlySDK(config) {
|
|
|
1798
1851
|
}, response);
|
|
1799
1852
|
return true;
|
|
1800
1853
|
}
|
|
1801
|
-
lastPushFailure ??= {
|
|
1854
|
+
lastPushFailure ??= {
|
|
1855
|
+
status: response?.status ?? null,
|
|
1856
|
+
message: await readPushFailureMessage(response)
|
|
1857
|
+
};
|
|
1802
1858
|
return false;
|
|
1803
1859
|
};
|
|
1804
1860
|
const describePushFailure = (entities) => {
|
|
1805
1861
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1806
1862
|
const status = lastPushFailure?.status ?? null;
|
|
1863
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1807
1864
|
let cause;
|
|
1808
1865
|
if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
|
|
1809
1866
|
else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
|
|
1867
|
+
else if (serverMessage) cause = `Clivly returned HTTP ${status}: ${serverMessage}`;
|
|
1810
1868
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1811
|
-
|
|
1869
|
+
const separator = SENTENCE_END_RE.test(cause) ? "" : ".";
|
|
1870
|
+
return `${base}: ${cause}${separator} Run \`clivly doctor\` to diagnose.`;
|
|
1871
|
+
};
|
|
1872
|
+
const pushFailureError = (message) => {
|
|
1873
|
+
const status = lastPushFailure?.status ?? null;
|
|
1874
|
+
return status !== null && status >= CLIENT_ERROR_FLOOR && status < RETRYABLE_STATUS_FLOOR ? new TriggerRequestError(status, message) : new Error(message);
|
|
1812
1875
|
};
|
|
1813
1876
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1814
1877
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -1834,7 +1897,10 @@ function createClivlySDK(config) {
|
|
|
1834
1897
|
};
|
|
1835
1898
|
const syncCustomSources = async (customSources, mode, runId) => {
|
|
1836
1899
|
const failed = [];
|
|
1837
|
-
for (const customSource of customSources ?? [])
|
|
1900
|
+
for (const customSource of customSources ?? []) {
|
|
1901
|
+
customSource.pushSource = customPushSource(config, customSource);
|
|
1902
|
+
if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
1903
|
+
}
|
|
1838
1904
|
return failed;
|
|
1839
1905
|
};
|
|
1840
1906
|
const syncRemoteSources = async (sources, runId) => {
|
|
@@ -1895,7 +1961,7 @@ function createClivlySDK(config) {
|
|
|
1895
1961
|
failed: true,
|
|
1896
1962
|
error: message
|
|
1897
1963
|
};
|
|
1898
|
-
throw
|
|
1964
|
+
throw pushFailureError(message);
|
|
1899
1965
|
}
|
|
1900
1966
|
lastPush = { failed: false };
|
|
1901
1967
|
};
|
|
@@ -1903,7 +1969,7 @@ function createClivlySDK(config) {
|
|
|
1903
1969
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1904
1970
|
if (!resolved) throw new TriggerRequestError(400, `Clivly sync: no source is configured for entity "${entityKey}". The dashboard maps this table, but clivly.config declares no matching entity — add it to \`entities\`, or remove the mapping in Integrations → Mapping.`);
|
|
1905
1971
|
lastPushFailure = null;
|
|
1906
|
-
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw
|
|
1972
|
+
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
|
|
1907
1973
|
};
|
|
1908
1974
|
const describeSyncTrigger = () => ({
|
|
1909
1975
|
configured: config.syncTrigger?.url,
|
|
@@ -376,6 +376,22 @@ interface SyncSource {
|
|
|
376
376
|
* absent from another, and read by the runtime.
|
|
377
377
|
*/
|
|
378
378
|
objectType?: string;
|
|
379
|
+
/**
|
|
380
|
+
* The `source` label a push carries, when it differs from `entity`.
|
|
381
|
+
*
|
|
382
|
+
* Only a projection sets this, and only because a projection entity has no
|
|
383
|
+
* choice but to carry two names: its entity key doubles as the custom-object
|
|
384
|
+
* slug and must be kebab-case, while `projection.entityKey` is validated by
|
|
385
|
+
* `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
|
|
386
|
+
* storing the mapping's `sourceTable` as `projection.baseTable` — so a push
|
|
387
|
+
* labelled with the entity key matched no mapping and every row was dropped,
|
|
388
|
+
* while `push-schema` reported the mapping created and exited 0.
|
|
389
|
+
*
|
|
390
|
+
* Deliberately NOT folded into `entity`: that value is also the cursor-store
|
|
391
|
+
* key, so redefining it would silently reset the sync cursor of every
|
|
392
|
+
* existing projection and re-push its whole history.
|
|
393
|
+
*/
|
|
394
|
+
pushSource?: string;
|
|
379
395
|
/** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
|
|
380
396
|
[DRIZZLE_META]?: DrizzleSourceMeta;
|
|
381
397
|
}
|
|
@@ -376,6 +376,22 @@ interface SyncSource {
|
|
|
376
376
|
* absent from another, and read by the runtime.
|
|
377
377
|
*/
|
|
378
378
|
objectType?: string;
|
|
379
|
+
/**
|
|
380
|
+
* The `source` label a push carries, when it differs from `entity`.
|
|
381
|
+
*
|
|
382
|
+
* Only a projection sets this, and only because a projection entity has no
|
|
383
|
+
* choice but to carry two names: its entity key doubles as the custom-object
|
|
384
|
+
* slug and must be kebab-case, while `projection.entityKey` is validated by
|
|
385
|
+
* `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
|
|
386
|
+
* storing the mapping's `sourceTable` as `projection.baseTable` — so a push
|
|
387
|
+
* labelled with the entity key matched no mapping and every row was dropped,
|
|
388
|
+
* while `push-schema` reported the mapping created and exited 0.
|
|
389
|
+
*
|
|
390
|
+
* Deliberately NOT folded into `entity`: that value is also the cursor-store
|
|
391
|
+
* key, so redefining it would silently reset the sync cursor of every
|
|
392
|
+
* existing projection and re-push its whole history.
|
|
393
|
+
*/
|
|
394
|
+
pushSource?: string;
|
|
379
395
|
/** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
|
|
380
396
|
[DRIZZLE_META]?: DrizzleSourceMeta;
|
|
381
397
|
}
|
package/dist/vite.d.cts
CHANGED
package/dist/vite.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,10 +49,11 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
|
-
"dist"
|
|
52
|
+
"dist",
|
|
53
|
+
"README.md"
|
|
53
54
|
],
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@clivly/core": "0.10.
|
|
56
|
+
"@clivly/core": "0.10.9"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@electric-sql/pglite": "^0.5.4",
|