@clivly/sdk 0.10.8 → 0.11.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-session.cjs +35 -4
- package/dist/chat-session.mjs +35 -4
- package/dist/index.cjs +34 -8
- package/dist/index.mjs +34 -8
- package/package.json +5 -4
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.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/index.cjs
CHANGED
|
@@ -394,11 +394,15 @@ function errorMessage(error) {
|
|
|
394
394
|
/**
|
|
395
395
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
396
396
|
*
|
|
397
|
-
* `source` is the
|
|
397
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
398
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
|
+
*
|
|
399
402
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
400
|
-
* the two are routinely conflated, and using the wrong one
|
|
401
|
-
*
|
|
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.
|
|
402
406
|
*/
|
|
403
407
|
function readinessEntitiesFromConfig(config) {
|
|
404
408
|
const entities = [];
|
|
@@ -407,7 +411,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
407
411
|
if (!resolved) continue;
|
|
408
412
|
const objectTypeSlug = resolved.source.objectType;
|
|
409
413
|
entities.push({
|
|
410
|
-
source: resolved.source
|
|
414
|
+
source: customPushSource(config, resolved.source),
|
|
411
415
|
concept: resolved.concept,
|
|
412
416
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
413
417
|
});
|
|
@@ -1614,6 +1618,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1614
1618
|
function triggerErrorStatus(error) {
|
|
1615
1619
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1616
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
|
+
}
|
|
1617
1633
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1618
1634
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1619
1635
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1836,17 +1852,27 @@ function createClivlySDK(config) {
|
|
|
1836
1852
|
}, response);
|
|
1837
1853
|
return true;
|
|
1838
1854
|
}
|
|
1839
|
-
lastPushFailure ??= {
|
|
1855
|
+
lastPushFailure ??= {
|
|
1856
|
+
status: response?.status ?? null,
|
|
1857
|
+
message: await readPushFailureMessage(response)
|
|
1858
|
+
};
|
|
1840
1859
|
return false;
|
|
1841
1860
|
};
|
|
1842
1861
|
const describePushFailure = (entities) => {
|
|
1843
1862
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1844
1863
|
const status = lastPushFailure?.status ?? null;
|
|
1864
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1845
1865
|
let cause;
|
|
1846
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";
|
|
1847
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}`;
|
|
1848
1869
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1849
|
-
|
|
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);
|
|
1850
1876
|
};
|
|
1851
1877
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1852
1878
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -1936,7 +1962,7 @@ function createClivlySDK(config) {
|
|
|
1936
1962
|
failed: true,
|
|
1937
1963
|
error: message
|
|
1938
1964
|
};
|
|
1939
|
-
throw
|
|
1965
|
+
throw pushFailureError(message);
|
|
1940
1966
|
}
|
|
1941
1967
|
lastPush = { failed: false };
|
|
1942
1968
|
};
|
|
@@ -1944,7 +1970,7 @@ function createClivlySDK(config) {
|
|
|
1944
1970
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1945
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.`);
|
|
1946
1972
|
lastPushFailure = null;
|
|
1947
|
-
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]));
|
|
1948
1974
|
};
|
|
1949
1975
|
const describeSyncTrigger = () => ({
|
|
1950
1976
|
configured: config.syncTrigger?.url,
|
package/dist/index.mjs
CHANGED
|
@@ -393,11 +393,15 @@ function errorMessage(error) {
|
|
|
393
393
|
/**
|
|
394
394
|
* The entities to ask about, in the exact terms the sync pushes them.
|
|
395
395
|
*
|
|
396
|
-
* `source` is the
|
|
396
|
+
* `source` is whatever the push will actually send — the value that arrives at
|
|
397
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
|
+
*
|
|
398
401
|
* It is NOT the entity config's own `source` field, which names a host table;
|
|
399
|
-
* the two are routinely conflated, and using the wrong one
|
|
400
|
-
*
|
|
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.
|
|
401
405
|
*/
|
|
402
406
|
function readinessEntitiesFromConfig(config) {
|
|
403
407
|
const entities = [];
|
|
@@ -406,7 +410,7 @@ function readinessEntitiesFromConfig(config) {
|
|
|
406
410
|
if (!resolved) continue;
|
|
407
411
|
const objectTypeSlug = resolved.source.objectType;
|
|
408
412
|
entities.push({
|
|
409
|
-
source: resolved.source
|
|
413
|
+
source: customPushSource(config, resolved.source),
|
|
410
414
|
concept: resolved.concept,
|
|
411
415
|
...resolved.concept === "custom" && objectTypeSlug ? { objectTypeSlug } : {}
|
|
412
416
|
});
|
|
@@ -1613,6 +1617,18 @@ var TriggerRequestError = class extends Error {
|
|
|
1613
1617
|
function triggerErrorStatus(error) {
|
|
1614
1618
|
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1615
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
|
+
}
|
|
1616
1632
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1617
1633
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1618
1634
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1835,17 +1851,27 @@ function createClivlySDK(config) {
|
|
|
1835
1851
|
}, response);
|
|
1836
1852
|
return true;
|
|
1837
1853
|
}
|
|
1838
|
-
lastPushFailure ??= {
|
|
1854
|
+
lastPushFailure ??= {
|
|
1855
|
+
status: response?.status ?? null,
|
|
1856
|
+
message: await readPushFailureMessage(response)
|
|
1857
|
+
};
|
|
1839
1858
|
return false;
|
|
1840
1859
|
};
|
|
1841
1860
|
const describePushFailure = (entities) => {
|
|
1842
1861
|
const base = `Clivly sync: push failed for ${entities.join(", ")}`;
|
|
1843
1862
|
const status = lastPushFailure?.status ?? null;
|
|
1863
|
+
const serverMessage = lastPushFailure?.message ?? null;
|
|
1844
1864
|
let cause;
|
|
1845
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";
|
|
1846
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}`;
|
|
1847
1868
|
else cause = `Clivly returned HTTP ${status}`;
|
|
1848
|
-
|
|
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);
|
|
1849
1875
|
};
|
|
1850
1876
|
const syncOne = async (source, path, mode, objectType, runId, batchSizeOverride, maxRowsOverride, persistCursor = true) => {
|
|
1851
1877
|
const since = mode === "full" ? null : await cursorStore.get(source.entity) ?? null;
|
|
@@ -1935,7 +1961,7 @@ function createClivlySDK(config) {
|
|
|
1935
1961
|
failed: true,
|
|
1936
1962
|
error: message
|
|
1937
1963
|
};
|
|
1938
|
-
throw
|
|
1964
|
+
throw pushFailureError(message);
|
|
1939
1965
|
}
|
|
1940
1966
|
lastPush = { failed: false };
|
|
1941
1967
|
};
|
|
@@ -1943,7 +1969,7 @@ function createClivlySDK(config) {
|
|
|
1943
1969
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1944
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.`);
|
|
1945
1971
|
lastPushFailure = null;
|
|
1946
|
-
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]));
|
|
1947
1973
|
};
|
|
1948
1974
|
const describeSyncTrigger = () => ({
|
|
1949
1975
|
configured: config.syncTrigger?.url,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-next.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,15 +49,16 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
|
-
"dist"
|
|
52
|
+
"dist",
|
|
53
|
+
"README.md"
|
|
53
54
|
],
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@clivly/core": "0.
|
|
56
|
+
"@clivly/core": "0.11.0-next.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@electric-sql/pglite": "^0.5.4",
|
|
59
60
|
"drizzle-orm": "^0.45.2",
|
|
60
|
-
"tsdown": "^0.
|
|
61
|
+
"tsdown": "^0.23.0",
|
|
61
62
|
"typescript": "^6",
|
|
62
63
|
"vite": "^8.0.8",
|
|
63
64
|
"vitest": "^3.2.0"
|