clivly 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/README.md +31 -1
- package/drizzle.cjs +2 -0
- package/drizzle.d.cts +1 -1
- package/drizzle.d.mts +1 -1
- package/drizzle.mjs +2 -0
- package/index.cjs +281 -28
- package/index.d.cts +2 -0
- package/index.d.mts +2 -0
- package/index.mjs +281 -28
- package/{namespace-probe-ouHKZas2.d.mts → namespace-probe-BmxtnOfc.d.mts} +16 -0
- package/{namespace-probe-lj_0bJrc.d.cts → namespace-probe-C3xMOqWE.d.cts} +16 -0
- package/package.json +1 -1
- package/{sdk-DbvqXNSh.cjs → sdk-BIYu0N3V.cjs} +113 -16
- package/{sdk-BTzaM8bF.mjs → sdk-DofHordq.mjs} +113 -16
- package/sdk.cjs +1 -1
- package/sdk.d.cts +1 -1
- package/sdk.d.mts +1 -1
- package/sdk.mjs +1 -1
- package/vite.d.cts +1 -1
- package/vite.d.mts +1 -1
|
@@ -77,6 +77,43 @@ function resolveEntitySource(config, entityKey) {
|
|
|
77
77
|
function expectedSourceKey(concept) {
|
|
78
78
|
return CONCEPT_KEYS[concept] ?? "custom";
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The `source` label a push must carry for the server to find its mapping.
|
|
82
|
+
*
|
|
83
|
+
* `entity` is the SDK's own name for a source — the cursor-store key, and what
|
|
84
|
+
* the developer wrote in `config.source`. It is NOT always what the server
|
|
85
|
+
* filed the mapping under: for a projection, `ensureSchema` stores
|
|
86
|
+
* `sourceTable` as `projection.baseTable`, because a projection entity's key
|
|
87
|
+
* doubles as a kebab-case object slug and cannot also be the snake_case
|
|
88
|
+
* `projection.entityKey`.
|
|
89
|
+
*
|
|
90
|
+
* Pushing under `entity` therefore dropped 100% of a projection's rows while
|
|
91
|
+
* every local signal said the setup was correct. One function so the three
|
|
92
|
+
* push sites cannot drift apart again.
|
|
93
|
+
*/
|
|
94
|
+
function pushSourceOf(source) {
|
|
95
|
+
return source.pushSource ?? source.entity;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The push label for a custom source, resolved against the config.
|
|
99
|
+
*
|
|
100
|
+
* `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
|
|
101
|
+
* can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
|
|
102
|
+
* what the README's own example leads you to, and next-finance did exactly
|
|
103
|
+
* that. Such a source has no `pushSource`, so the label has to come from the
|
|
104
|
+
* one place that is true either way: the entity's declared
|
|
105
|
+
* `projection.baseTable`.
|
|
106
|
+
*
|
|
107
|
+
* Matched on the source's `entity`, which for a custom source is also the
|
|
108
|
+
* config entity key (that is what `resolveEntitySource` above pairs them by).
|
|
109
|
+
* Falls back to `pushSourceOf`, so a non-projection custom source and an
|
|
110
|
+
* already-stamped one both keep their current label.
|
|
111
|
+
*/
|
|
112
|
+
function customPushSource(config, source) {
|
|
113
|
+
if (source.pushSource) return source.pushSource;
|
|
114
|
+
const entities = config.entities?.entities;
|
|
115
|
+
return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
|
|
116
|
+
}
|
|
80
117
|
//#endregion
|
|
81
118
|
//#region ../sdk/src/concept-coverage.ts
|
|
82
119
|
const CHECK_NAME$1 = "Entity → source coverage";
|
|
@@ -355,11 +392,15 @@ function errorMessage(error) {
|
|
|
355
392
|
/**
|
|
356
393
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
357
394
|
*
|
|
358
|
-
* `source` is the
|
|
395
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
359
396
|
* `ingestPush` as `input.source` and is compared against `mapping.sourceTable`.
|
|
397
|
+
* Usually that is `SyncSource.entity`, but a projection pushes under its base
|
|
398
|
+
* table, so this asks `customPushSource` rather than reading `entity` directly.
|
|
399
|
+
*
|
|
360
400
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
361
|
-
* the two are routinely conflated, and using the wrong one
|
|
362
|
-
*
|
|
401
|
+
* the two are routinely conflated, and using the wrong one reports a correctly
|
|
402
|
+
* mapped workspace as unmapped — which is exactly what happened to every
|
|
403
|
+
* projection until this used the push label.
|
|
363
404
|
*/
|
|
364
405
|
function readinessEntitiesFromConfig(config) {
|
|
365
406
|
const entities = [];
|
|
@@ -368,7 +409,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
368
409
|
if (!resolved) continue;
|
|
369
410
|
const objectTypeSlug = resolved.source.objectType;
|
|
370
411
|
entities.push({
|
|
371
|
-
source: resolved.source
|
|
412
|
+
source: customPushSource(config, resolved.source),
|
|
372
413
|
concept: resolved.concept,
|
|
373
414
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
374
415
|
});
|
|
@@ -1203,7 +1244,7 @@ async function pushRemoteProjections(args) {
|
|
|
1203
1244
|
}
|
|
1204
1245
|
try {
|
|
1205
1246
|
const ok = await args.push(SYNC_CUSTOM_PATH$1, {
|
|
1206
|
-
source: source
|
|
1247
|
+
source: pushSourceOf(source),
|
|
1207
1248
|
rows: all,
|
|
1208
1249
|
mode: "full",
|
|
1209
1250
|
fetched: all.length,
|
|
@@ -1278,7 +1319,7 @@ async function syncSource(args) {
|
|
|
1278
1319
|
if (rows.length === 0) break;
|
|
1279
1320
|
const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
|
|
1280
1321
|
if (!await push(path, {
|
|
1281
|
-
source: source
|
|
1322
|
+
source: pushSourceOf(source),
|
|
1282
1323
|
rows: capped,
|
|
1283
1324
|
mode: "delta",
|
|
1284
1325
|
fetched: rows.length,
|
|
@@ -1322,7 +1363,7 @@ async function fullSyncSource(args) {
|
|
|
1322
1363
|
ok: true
|
|
1323
1364
|
};
|
|
1324
1365
|
const ok = await push(path, {
|
|
1325
|
-
source: source
|
|
1366
|
+
source: pushSourceOf(source),
|
|
1326
1367
|
rows: all,
|
|
1327
1368
|
mode: "full",
|
|
1328
1369
|
fetched: all.length,
|
|
@@ -1431,8 +1472,18 @@ function jsonResponse$1(body, status) {
|
|
|
1431
1472
|
function normalizeApiUrl(apiUrl) {
|
|
1432
1473
|
return apiUrl.replace(TRAILING_SLASH$1, "");
|
|
1433
1474
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* The origin *claimed* by the request, for reporting upstream.
|
|
1477
|
+
*
|
|
1478
|
+
* Falls back to the request URL so Clivly still receives an origin for a
|
|
1479
|
+
* non-browser caller. This value is reported, never trusted: only a real
|
|
1480
|
+
* `Origin` header is evidence of where a request came from, so the access
|
|
1481
|
+
* decision uses {@link trustedOrigin} instead. Keeping the two apart is the
|
|
1482
|
+
* point — conflating them let a caller that simply omitted the header be
|
|
1483
|
+
* judged against the server's own origin, which is usually in the allowlist.
|
|
1484
|
+
*/
|
|
1485
|
+
function reportedOrigin(req) {
|
|
1486
|
+
const headerOrigin = trustedOrigin(req);
|
|
1436
1487
|
if (headerOrigin) return headerOrigin;
|
|
1437
1488
|
try {
|
|
1438
1489
|
return new URL(req.url).origin;
|
|
@@ -1440,6 +1491,15 @@ function resolveOrigin(req) {
|
|
|
1440
1491
|
return null;
|
|
1441
1492
|
}
|
|
1442
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* The only origin worth an access decision: one the browser set itself.
|
|
1496
|
+
*
|
|
1497
|
+
* A missing header means "not a browser request we can vouch for", which is
|
|
1498
|
+
* untrusted — never the server's own origin.
|
|
1499
|
+
*/
|
|
1500
|
+
function trustedOrigin(req) {
|
|
1501
|
+
return req.headers.get("origin");
|
|
1502
|
+
}
|
|
1443
1503
|
function isChatSessionRequestBody(value) {
|
|
1444
1504
|
if (typeof value !== "object" || value === null) return false;
|
|
1445
1505
|
const body = value;
|
|
@@ -1450,6 +1510,17 @@ function isChatSessionRequestBody(value) {
|
|
|
1450
1510
|
if (typeof visitor.email !== "string" || visitor.email.trim().length === 0) return false;
|
|
1451
1511
|
return body.visitorToken === void 0 || body.visitorToken === null || typeof body.visitorToken === "string";
|
|
1452
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Judge a request against the configured allowlist.
|
|
1515
|
+
*
|
|
1516
|
+
* `origin` must be a *trusted* origin (see {@link trustedOrigin}) — passing
|
|
1517
|
+
* the reported one would reinstate the bypass this guards against.
|
|
1518
|
+
*
|
|
1519
|
+
* An unset allowlist still admits every origin, because requiring it would
|
|
1520
|
+
* break every existing deployment on upgrade. The unset case is instead
|
|
1521
|
+
* warned about once at construction, so the permissive default is loud
|
|
1522
|
+
* rather than silent.
|
|
1523
|
+
*/
|
|
1453
1524
|
function originAllowed(origin, allowedOrigins) {
|
|
1454
1525
|
if (!allowedOrigins || allowedOrigins.length === 0) return true;
|
|
1455
1526
|
if (!origin) return false;
|
|
@@ -1457,6 +1528,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
1457
1528
|
}
|
|
1458
1529
|
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL$1, fetchImpl = fetch, resolveUser }) {
|
|
1459
1530
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
1531
|
+
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.");
|
|
1460
1532
|
return async (request) => {
|
|
1461
1533
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
1462
1534
|
status: 405,
|
|
@@ -1465,8 +1537,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
1465
1537
|
allow: "POST"
|
|
1466
1538
|
}
|
|
1467
1539
|
});
|
|
1468
|
-
|
|
1469
|
-
|
|
1540
|
+
if (!originAllowed(trustedOrigin(request), allowedOrigins)) return jsonResponse$1({ error: "origin_not_allowed" }, 403);
|
|
1541
|
+
const origin = reportedOrigin(request);
|
|
1470
1542
|
let body;
|
|
1471
1543
|
try {
|
|
1472
1544
|
body = await request.json();
|
|
@@ -1664,6 +1736,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1664
1736
|
function triggerErrorStatus(error) {
|
|
1665
1737
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1666
1738
|
}
|
|
1739
|
+
const SENTENCE_END_RE = /[.!?]$/;
|
|
1740
|
+
async function readPushFailureMessage(response) {
|
|
1741
|
+
if (!response || typeof response.json !== "function") return null;
|
|
1742
|
+
try {
|
|
1743
|
+
const body = await response.json();
|
|
1744
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1745
|
+
const { message } = body;
|
|
1746
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1667
1751
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1668
1752
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1669
1753
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1886,17 +1970,27 @@ function createClivlySDK(config) {
|
|
|
1886
1970
|
}, response);
|
|
1887
1971
|
return true;
|
|
1888
1972
|
}
|
|
1889
|
-
lastPushFailure ??= {
|
|
1973
|
+
lastPushFailure ??= {
|
|
1974
|
+
status: response?.status ?? null,
|
|
1975
|
+
message: await readPushFailureMessage(response)
|
|
1976
|
+
};
|
|
1890
1977
|
return false;
|
|
1891
1978
|
};
|
|
1892
1979
|
const describePushFailure = (entities) => {
|
|
1893
1980
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1894
1981
|
const status = lastPushFailure?.status ?? null;
|
|
1982
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1895
1983
|
let cause;
|
|
1896
1984
|
if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
|
|
1897
1985
|
else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
|
|
1986
|
+
else if (serverMessage) cause = `Clivly returned HTTP ${status}: ${serverMessage}`;
|
|
1898
1987
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1899
|
-
|
|
1988
|
+
const separator = SENTENCE_END_RE.test(cause) ? "" : ".";
|
|
1989
|
+
return `${base}: ${cause}${separator} Run \`clivly doctor\` to diagnose.`;
|
|
1990
|
+
};
|
|
1991
|
+
const pushFailureError = (message) => {
|
|
1992
|
+
const status = lastPushFailure?.status ?? null;
|
|
1993
|
+
return status !== null && status >= CLIENT_ERROR_FLOOR && status < RETRYABLE_STATUS_FLOOR ? new TriggerRequestError(status, message) : new Error(message);
|
|
1900
1994
|
};
|
|
1901
1995
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1902
1996
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -1922,7 +2016,10 @@ function createClivlySDK(config) {
|
|
|
1922
2016
|
};
|
|
1923
2017
|
const syncCustomSources = async (customSources, mode, runId) => {
|
|
1924
2018
|
const failed = [];
|
|
1925
|
-
for (const customSource of customSources ?? [])
|
|
2019
|
+
for (const customSource of customSources ?? []) {
|
|
2020
|
+
customSource.pushSource = customPushSource(config, customSource);
|
|
2021
|
+
if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
2022
|
+
}
|
|
1926
2023
|
return failed;
|
|
1927
2024
|
};
|
|
1928
2025
|
const syncRemoteSources = async (sources, runId) => {
|
|
@@ -1983,7 +2080,7 @@ function createClivlySDK(config) {
|
|
|
1983
2080
|
failed: true,
|
|
1984
2081
|
error: message
|
|
1985
2082
|
};
|
|
1986
|
-
throw
|
|
2083
|
+
throw pushFailureError(message);
|
|
1987
2084
|
}
|
|
1988
2085
|
lastPush = { failed: false };
|
|
1989
2086
|
};
|
|
@@ -1991,7 +2088,7 @@ function createClivlySDK(config) {
|
|
|
1991
2088
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1992
2089
|
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.`);
|
|
1993
2090
|
lastPushFailure = null;
|
|
1994
|
-
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw
|
|
2091
|
+
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
|
|
1995
2092
|
};
|
|
1996
2093
|
const describeSyncTrigger = () => ({
|
|
1997
2094
|
configured: config.syncTrigger?.url,
|
|
@@ -77,6 +77,43 @@ function resolveEntitySource(config, entityKey) {
|
|
|
77
77
|
function expectedSourceKey(concept) {
|
|
78
78
|
return CONCEPT_KEYS[concept] ?? "custom";
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The `source` label a push must carry for the server to find its mapping.
|
|
82
|
+
*
|
|
83
|
+
* `entity` is the SDK's own name for a source — the cursor-store key, and what
|
|
84
|
+
* the developer wrote in `config.source`. It is NOT always what the server
|
|
85
|
+
* filed the mapping under: for a projection, `ensureSchema` stores
|
|
86
|
+
* `sourceTable` as `projection.baseTable`, because a projection entity's key
|
|
87
|
+
* doubles as a kebab-case object slug and cannot also be the snake_case
|
|
88
|
+
* `projection.entityKey`.
|
|
89
|
+
*
|
|
90
|
+
* Pushing under `entity` therefore dropped 100% of a projection's rows while
|
|
91
|
+
* every local signal said the setup was correct. One function so the three
|
|
92
|
+
* push sites cannot drift apart again.
|
|
93
|
+
*/
|
|
94
|
+
function pushSourceOf(source) {
|
|
95
|
+
return source.pushSource ?? source.entity;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The push label for a custom source, resolved against the config.
|
|
99
|
+
*
|
|
100
|
+
* `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
|
|
101
|
+
* can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
|
|
102
|
+
* what the README's own example leads you to, and next-finance did exactly
|
|
103
|
+
* that. Such a source has no `pushSource`, so the label has to come from the
|
|
104
|
+
* one place that is true either way: the entity's declared
|
|
105
|
+
* `projection.baseTable`.
|
|
106
|
+
*
|
|
107
|
+
* Matched on the source's `entity`, which for a custom source is also the
|
|
108
|
+
* config entity key (that is what `resolveEntitySource` above pairs them by).
|
|
109
|
+
* Falls back to `pushSourceOf`, so a non-projection custom source and an
|
|
110
|
+
* already-stamped one both keep their current label.
|
|
111
|
+
*/
|
|
112
|
+
function customPushSource(config, source) {
|
|
113
|
+
if (source.pushSource) return source.pushSource;
|
|
114
|
+
const entities = config.entities?.entities;
|
|
115
|
+
return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
|
|
116
|
+
}
|
|
80
117
|
//#endregion
|
|
81
118
|
//#region ../sdk/src/concept-coverage.ts
|
|
82
119
|
const CHECK_NAME$1 = "Entity → source coverage";
|
|
@@ -355,11 +392,15 @@ function errorMessage(error) {
|
|
|
355
392
|
/**
|
|
356
393
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
357
394
|
*
|
|
358
|
-
* `source` is the
|
|
395
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
359
396
|
* `ingestPush` as `input.source` and is compared against `mapping.sourceTable`.
|
|
397
|
+
* Usually that is `SyncSource.entity`, but a projection pushes under its base
|
|
398
|
+
* table, so this asks `customPushSource` rather than reading `entity` directly.
|
|
399
|
+
*
|
|
360
400
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
361
|
-
* the two are routinely conflated, and using the wrong one
|
|
362
|
-
*
|
|
401
|
+
* the two are routinely conflated, and using the wrong one reports a correctly
|
|
402
|
+
* mapped workspace as unmapped — which is exactly what happened to every
|
|
403
|
+
* projection until this used the push label.
|
|
363
404
|
*/
|
|
364
405
|
function readinessEntitiesFromConfig(config) {
|
|
365
406
|
const entities = [];
|
|
@@ -368,7 +409,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
368
409
|
if (!resolved) continue;
|
|
369
410
|
const objectTypeSlug = resolved.source.objectType;
|
|
370
411
|
entities.push({
|
|
371
|
-
source: resolved.source
|
|
412
|
+
source: customPushSource(config, resolved.source),
|
|
372
413
|
concept: resolved.concept,
|
|
373
414
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
374
415
|
});
|
|
@@ -1203,7 +1244,7 @@ async function pushRemoteProjections(args) {
|
|
|
1203
1244
|
}
|
|
1204
1245
|
try {
|
|
1205
1246
|
const ok = await args.push(SYNC_CUSTOM_PATH$1, {
|
|
1206
|
-
source: source
|
|
1247
|
+
source: pushSourceOf(source),
|
|
1207
1248
|
rows: all,
|
|
1208
1249
|
mode: "full",
|
|
1209
1250
|
fetched: all.length,
|
|
@@ -1278,7 +1319,7 @@ async function syncSource(args) {
|
|
|
1278
1319
|
if (rows.length === 0) break;
|
|
1279
1320
|
const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
|
|
1280
1321
|
if (!await push(path, {
|
|
1281
|
-
source: source
|
|
1322
|
+
source: pushSourceOf(source),
|
|
1282
1323
|
rows: capped,
|
|
1283
1324
|
mode: "delta",
|
|
1284
1325
|
fetched: rows.length,
|
|
@@ -1322,7 +1363,7 @@ async function fullSyncSource(args) {
|
|
|
1322
1363
|
ok: true
|
|
1323
1364
|
};
|
|
1324
1365
|
const ok = await push(path, {
|
|
1325
|
-
source: source
|
|
1366
|
+
source: pushSourceOf(source),
|
|
1326
1367
|
rows: all,
|
|
1327
1368
|
mode: "full",
|
|
1328
1369
|
fetched: all.length,
|
|
@@ -1431,8 +1472,18 @@ function jsonResponse$1(body, status) {
|
|
|
1431
1472
|
function normalizeApiUrl(apiUrl) {
|
|
1432
1473
|
return apiUrl.replace(TRAILING_SLASH$1, "");
|
|
1433
1474
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* The origin *claimed* by the request, for reporting upstream.
|
|
1477
|
+
*
|
|
1478
|
+
* Falls back to the request URL so Clivly still receives an origin for a
|
|
1479
|
+
* non-browser caller. This value is reported, never trusted: only a real
|
|
1480
|
+
* `Origin` header is evidence of where a request came from, so the access
|
|
1481
|
+
* decision uses {@link trustedOrigin} instead. Keeping the two apart is the
|
|
1482
|
+
* point — conflating them let a caller that simply omitted the header be
|
|
1483
|
+
* judged against the server's own origin, which is usually in the allowlist.
|
|
1484
|
+
*/
|
|
1485
|
+
function reportedOrigin(req) {
|
|
1486
|
+
const headerOrigin = trustedOrigin(req);
|
|
1436
1487
|
if (headerOrigin) return headerOrigin;
|
|
1437
1488
|
try {
|
|
1438
1489
|
return new URL(req.url).origin;
|
|
@@ -1440,6 +1491,15 @@ function resolveOrigin(req) {
|
|
|
1440
1491
|
return null;
|
|
1441
1492
|
}
|
|
1442
1493
|
}
|
|
1494
|
+
/**
|
|
1495
|
+
* The only origin worth an access decision: one the browser set itself.
|
|
1496
|
+
*
|
|
1497
|
+
* A missing header means "not a browser request we can vouch for", which is
|
|
1498
|
+
* untrusted — never the server's own origin.
|
|
1499
|
+
*/
|
|
1500
|
+
function trustedOrigin(req) {
|
|
1501
|
+
return req.headers.get("origin");
|
|
1502
|
+
}
|
|
1443
1503
|
function isChatSessionRequestBody(value) {
|
|
1444
1504
|
if (typeof value !== "object" || value === null) return false;
|
|
1445
1505
|
const body = value;
|
|
@@ -1450,6 +1510,17 @@ function isChatSessionRequestBody(value) {
|
|
|
1450
1510
|
if (typeof visitor.email !== "string" || visitor.email.trim().length === 0) return false;
|
|
1451
1511
|
return body.visitorToken === void 0 || body.visitorToken === null || typeof body.visitorToken === "string";
|
|
1452
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Judge a request against the configured allowlist.
|
|
1515
|
+
*
|
|
1516
|
+
* `origin` must be a *trusted* origin (see {@link trustedOrigin}) — passing
|
|
1517
|
+
* the reported one would reinstate the bypass this guards against.
|
|
1518
|
+
*
|
|
1519
|
+
* An unset allowlist still admits every origin, because requiring it would
|
|
1520
|
+
* break every existing deployment on upgrade. The unset case is instead
|
|
1521
|
+
* warned about once at construction, so the permissive default is loud
|
|
1522
|
+
* rather than silent.
|
|
1523
|
+
*/
|
|
1453
1524
|
function originAllowed(origin, allowedOrigins) {
|
|
1454
1525
|
if (!allowedOrigins || allowedOrigins.length === 0) return true;
|
|
1455
1526
|
if (!origin) return false;
|
|
@@ -1457,6 +1528,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
1457
1528
|
}
|
|
1458
1529
|
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL$1, fetchImpl = fetch, resolveUser }) {
|
|
1459
1530
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
1531
|
+
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.");
|
|
1460
1532
|
return async (request) => {
|
|
1461
1533
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
1462
1534
|
status: 405,
|
|
@@ -1465,8 +1537,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
1465
1537
|
allow: "POST"
|
|
1466
1538
|
}
|
|
1467
1539
|
});
|
|
1468
|
-
|
|
1469
|
-
|
|
1540
|
+
if (!originAllowed(trustedOrigin(request), allowedOrigins)) return jsonResponse$1({ error: "origin_not_allowed" }, 403);
|
|
1541
|
+
const origin = reportedOrigin(request);
|
|
1470
1542
|
let body;
|
|
1471
1543
|
try {
|
|
1472
1544
|
body = await request.json();
|
|
@@ -1664,6 +1736,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1664
1736
|
function triggerErrorStatus(error) {
|
|
1665
1737
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1666
1738
|
}
|
|
1739
|
+
const SENTENCE_END_RE = /[.!?]$/;
|
|
1740
|
+
async function readPushFailureMessage(response) {
|
|
1741
|
+
if (!response || typeof response.json !== "function") return null;
|
|
1742
|
+
try {
|
|
1743
|
+
const body = await response.json();
|
|
1744
|
+
if (typeof body !== "object" || body === null) return null;
|
|
1745
|
+
const { message } = body;
|
|
1746
|
+
return typeof message === "string" && message.trim().length > 0 ? message : null;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1667
1751
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1668
1752
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1669
1753
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1886,17 +1970,27 @@ function createClivlySDK(config) {
|
|
|
1886
1970
|
}, response);
|
|
1887
1971
|
return true;
|
|
1888
1972
|
}
|
|
1889
|
-
lastPushFailure ??= {
|
|
1973
|
+
lastPushFailure ??= {
|
|
1974
|
+
status: response?.status ?? null,
|
|
1975
|
+
message: await readPushFailureMessage(response)
|
|
1976
|
+
};
|
|
1890
1977
|
return false;
|
|
1891
1978
|
};
|
|
1892
1979
|
const describePushFailure = (entities) => {
|
|
1893
1980
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1894
1981
|
const status = lastPushFailure?.status ?? null;
|
|
1982
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1895
1983
|
let cause;
|
|
1896
1984
|
if (status === 401 || status === 403) cause = "the API key was rejected — check CLIVLY_API_KEY is set to a valid key for this org";
|
|
1897
1985
|
else if (status === null) cause = `couldn't reach Clivly at ${apiUrl} — check network access and CLIVLY_API_URL`;
|
|
1986
|
+
else if (serverMessage) cause = `Clivly returned HTTP ${status}: ${serverMessage}`;
|
|
1898
1987
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1899
|
-
|
|
1988
|
+
const separator = SENTENCE_END_RE.test(cause) ? "" : ".";
|
|
1989
|
+
return `${base}: ${cause}${separator} Run \`clivly doctor\` to diagnose.`;
|
|
1990
|
+
};
|
|
1991
|
+
const pushFailureError = (message) => {
|
|
1992
|
+
const status = lastPushFailure?.status ?? null;
|
|
1993
|
+
return status !== null && status >= CLIENT_ERROR_FLOOR && status < RETRYABLE_STATUS_FLOOR ? new TriggerRequestError(status, message) : new Error(message);
|
|
1900
1994
|
};
|
|
1901
1995
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1902
1996
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -1922,7 +2016,10 @@ function createClivlySDK(config) {
|
|
|
1922
2016
|
};
|
|
1923
2017
|
const syncCustomSources = async (customSources, mode, runId) => {
|
|
1924
2018
|
const failed = [];
|
|
1925
|
-
for (const customSource of customSources ?? [])
|
|
2019
|
+
for (const customSource of customSources ?? []) {
|
|
2020
|
+
customSource.pushSource = customPushSource(config, customSource);
|
|
2021
|
+
if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
2022
|
+
}
|
|
1926
2023
|
return failed;
|
|
1927
2024
|
};
|
|
1928
2025
|
const syncRemoteSources = async (sources, runId) => {
|
|
@@ -1983,7 +2080,7 @@ function createClivlySDK(config) {
|
|
|
1983
2080
|
failed: true,
|
|
1984
2081
|
error: message
|
|
1985
2082
|
};
|
|
1986
|
-
throw
|
|
2083
|
+
throw pushFailureError(message);
|
|
1987
2084
|
}
|
|
1988
2085
|
lastPush = { failed: false };
|
|
1989
2086
|
};
|
|
@@ -1991,7 +2088,7 @@ function createClivlySDK(config) {
|
|
|
1991
2088
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1992
2089
|
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.`);
|
|
1993
2090
|
lastPushFailure = null;
|
|
1994
|
-
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw
|
|
2091
|
+
if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw pushFailureError(describePushFailure([resolved.source.entity]));
|
|
1995
2092
|
};
|
|
1996
2093
|
const describeSyncTrigger = () => ({
|
|
1997
2094
|
configured: config.syncTrigger?.url,
|
package/sdk.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
|
|
3
|
-
const require_sdk = require("./sdk-
|
|
3
|
+
const require_sdk = require("./sdk-BIYu0N3V.cjs");
|
|
4
4
|
exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
|
|
5
5
|
exports.ClivlyError = require_sdk.ClivlyError;
|
|
6
6
|
exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
|
package/sdk.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-w8oMBWf8.cjs";
|
|
2
|
-
import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-
|
|
2
|
+
import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-C3xMOqWE.cjs";
|
|
3
3
|
//#region ../sdk/src/trigger-url.d.ts
|
|
4
4
|
declare function isPlaceholderUrl(value: string): boolean;
|
|
5
5
|
//#endregion
|
package/sdk.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-w8oMBWf8.mjs";
|
|
2
|
-
import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-
|
|
2
|
+
import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-BmxtnOfc.mjs";
|
|
3
3
|
//#region ../sdk/src/trigger-url.d.ts
|
|
4
4
|
declare function isPlaceholderUrl(value: string): boolean;
|
|
5
5
|
//#endregion
|
package/sdk.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { c as defineClivlyConfig, g as validateEntitiesConfig, s as customObjectDeclarations } from "./entity-config-bC3DX9f1.mjs";
|
|
2
|
-
import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-
|
|
2
|
+
import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-DofHordq.mjs";
|
|
3
3
|
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
package/vite.d.cts
CHANGED
package/vite.d.mts
CHANGED