clivly 0.10.5 → 0.10.7
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/core-entity-heuristics.cjs +1 -0
- package/core-entity-heuristics.d.cts +2 -2
- package/core-entity-heuristics.d.mts +2 -2
- package/core-entity-heuristics.mjs +2 -2
- package/index.cjs +88 -13
- package/index.mjs +90 -15
- package/internal-entity-heuristics.cjs +49 -0
- package/internal-entity-heuristics.d.cts +9 -1
- package/internal-entity-heuristics.d.mts +9 -1
- package/internal-entity-heuristics.mjs +49 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ require("./deprecate-subpath-B9ngvizt.cjs").warnDeprecatedSubpath("clivly/core/e
|
|
|
6
6
|
exports.TARGET_CONCEPTS = require_internal_entity_heuristics.TARGET_CONCEPTS;
|
|
7
7
|
exports.hasDefiningColumn = require_internal_entity_heuristics.hasDefiningColumn;
|
|
8
8
|
exports.hasSensitiveColumns = require_internal_entity_heuristics.hasSensitiveColumns;
|
|
9
|
+
exports.isFrameworkTable = require_internal_entity_heuristics.isFrameworkTable;
|
|
9
10
|
exports.matchesConcept = require_internal_entity_heuristics.matchesConcept;
|
|
10
11
|
exports.requiredFieldsFor = require_internal_entity_heuristics.requiredFieldsFor;
|
|
11
12
|
exports.scoreConcepts = require_internal_entity_heuristics.scoreConcepts;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.cjs";
|
|
2
|
-
import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.cjs";
|
|
3
|
-
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
2
|
+
import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.cjs";
|
|
3
|
+
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { p as DiscoveredColumnMeta } from "./entity-config-w8oMBWf8.mjs";
|
|
2
|
-
import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
|
|
3
|
-
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
2
|
+
import { FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
|
|
3
|
+
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
|
|
1
|
+
import { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
|
|
2
2
|
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
3
3
|
//#region src/core-entity-heuristics.ts
|
|
4
4
|
warnDeprecatedSubpath("clivly/core/entity-heuristics", "clivly/internal/entity-heuristics");
|
|
5
5
|
//#endregion
|
|
6
|
-
export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
6
|
+
export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
package/index.cjs
CHANGED
|
@@ -480,23 +480,38 @@ function cloudflaredMissingMessage() {
|
|
|
480
480
|
* Warning someone away from a tunnel that was fine is worse than staying quiet,
|
|
481
481
|
* for the same reason the port probe defaults to assuming the port is alive.
|
|
482
482
|
*/
|
|
483
|
-
/** How long
|
|
483
|
+
/** How long any single probe attempt may take. */
|
|
484
484
|
const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
|
|
485
485
|
/**
|
|
486
|
-
*
|
|
486
|
+
* How long to keep retrying before giving up.
|
|
487
487
|
*
|
|
488
|
-
*
|
|
488
|
+
* The probe fires the moment `cloudflared` publishes a URL, which on a
|
|
489
|
+
* Vite/Nuxt app is *before* the dev server has finished building its route
|
|
490
|
+
* table. The first attempt therefore reaches a server that is up but does not
|
|
491
|
+
* yet serve the tick route — a cold start, not a wrong port. The condition
|
|
492
|
+
* being detected (a tunnel aimed at the wrong process) does not resolve
|
|
493
|
+
* itself, so waiting costs nothing and a premature verdict costs a lot.
|
|
489
494
|
*/
|
|
490
|
-
|
|
495
|
+
const IDENTITY_RETRY_BUDGET_MS = 2e4;
|
|
496
|
+
const IDENTITY_RETRY_DELAY_MS = 2e3;
|
|
497
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
498
|
+
setTimeout(resolve, ms);
|
|
499
|
+
});
|
|
500
|
+
/** One attempt. Separated so the retry loop stays readable. */
|
|
501
|
+
async function attempt(triggerUrl, fetchImpl) {
|
|
491
502
|
const controller = new AbortController();
|
|
492
503
|
const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
|
|
493
504
|
try {
|
|
494
|
-
|
|
505
|
+
const response = await fetchImpl(triggerUrl, {
|
|
495
506
|
method: "POST",
|
|
496
507
|
headers: { "content-type": "application/json" },
|
|
497
508
|
body: "{}",
|
|
498
509
|
signal: controller.signal
|
|
499
|
-
})
|
|
510
|
+
});
|
|
511
|
+
if (response.status === 401) return "clivly";
|
|
512
|
+
if (response.status === 403) return "host-rejected";
|
|
513
|
+
if (response.status === 404 || response.status >= 500) return "unknown";
|
|
514
|
+
return "mismatch";
|
|
500
515
|
} catch {
|
|
501
516
|
return "unknown";
|
|
502
517
|
} finally {
|
|
@@ -504,6 +519,40 @@ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
|
|
|
504
519
|
}
|
|
505
520
|
}
|
|
506
521
|
/**
|
|
522
|
+
* Probe the published tunnel URL and report who is on the other end.
|
|
523
|
+
*
|
|
524
|
+
* Retries while the answer is inconclusive, because the first attempt lands
|
|
525
|
+
* during the dev server's boot. Only a confident contradiction is reported.
|
|
526
|
+
*
|
|
527
|
+
* `fetchImpl` and `now` are injectable so tests need no network or clock.
|
|
528
|
+
*/
|
|
529
|
+
async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch, now = Date.now) {
|
|
530
|
+
const deadline = now() + IDENTITY_RETRY_BUDGET_MS;
|
|
531
|
+
let last = "unknown";
|
|
532
|
+
do {
|
|
533
|
+
last = await attempt(triggerUrl, fetchImpl);
|
|
534
|
+
if (last !== "unknown") return last;
|
|
535
|
+
if (now() >= deadline) break;
|
|
536
|
+
await sleep(IDENTITY_RETRY_DELAY_MS);
|
|
537
|
+
} while (now() < deadline);
|
|
538
|
+
return last;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* What to tell someone whose dev server refused the tunnel's Host header.
|
|
542
|
+
*
|
|
543
|
+
* `clivly dev` already prints the allowedHosts hint for Vite frameworks before
|
|
544
|
+
* the tunnel opens; this fires when the tunnel proves it was not applied.
|
|
545
|
+
*/
|
|
546
|
+
function hostRejectedMessage() {
|
|
547
|
+
return [
|
|
548
|
+
"Warning: your dev server refused the tunnel's Host header (403).",
|
|
549
|
+
" Vite rejects hostnames it does not recognise, so Clivly's request never",
|
|
550
|
+
" reaches your tick route. Add the tunnel domain to your Vite config:",
|
|
551
|
+
" server: { allowedHosts: ['.trycloudflare.com'] }",
|
|
552
|
+
" (in nuxt.config.ts, nest it under `vite:`), then restart the dev server."
|
|
553
|
+
].join("\n");
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
507
556
|
* What to tell someone whose tunnel reaches the wrong process.
|
|
508
557
|
*
|
|
509
558
|
* Names the likely cause rather than only the symptom: the overwhelmingly
|
|
@@ -526,6 +575,12 @@ const UNREACHABLE_RESULT = {
|
|
|
526
575
|
ok: false,
|
|
527
576
|
kind: "unreachable"
|
|
528
577
|
};
|
|
578
|
+
/** The line to print for a probe verdict, or null when there is nothing to say. */
|
|
579
|
+
function tunnelWarning(identity, port) {
|
|
580
|
+
if (identity === "mismatch") return tunnelMismatchMessage(port);
|
|
581
|
+
if (identity === "host-rejected") return hostRejectedMessage();
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
529
584
|
function flagValue$1(argv, flag) {
|
|
530
585
|
const i = argv.indexOf(flag);
|
|
531
586
|
return i >= 0 ? argv[i + 1] : void 0;
|
|
@@ -570,9 +625,10 @@ async function runDev(options) {
|
|
|
570
625
|
if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
|
|
571
626
|
}
|
|
572
627
|
const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
|
|
573
|
-
|
|
628
|
+
const warning = tunnelWarning(identity, port);
|
|
629
|
+
if (warning) deps.out(warning);
|
|
574
630
|
const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
|
|
575
|
-
if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
|
|
631
|
+
if (result.ok) deps.out(identity === "mismatch" || identity === "host-rejected" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
|
|
576
632
|
else deps.out(overrideFailureMessage(result.kind, result.status));
|
|
577
633
|
});
|
|
578
634
|
const stopWatching = deps.watchConfig?.(options.cwd, () => {
|
|
@@ -1601,13 +1657,14 @@ function writeCredentials(options, credentials) {
|
|
|
1601
1657
|
const entries = credentialEntries(credentials);
|
|
1602
1658
|
const result = upsertEnvValues(deps.readEnv(options.cwd), entries, { force: options.force });
|
|
1603
1659
|
deps.writeEnv(options.cwd, result.contents);
|
|
1660
|
+
const envFile = deps.envFileName?.(options.cwd) ?? ".env";
|
|
1604
1661
|
deps.out("\nLogin successful.\n");
|
|
1605
1662
|
const valuesByKey = {
|
|
1606
1663
|
[SECRET_KEY_ENV]: credentials.apiKey,
|
|
1607
1664
|
CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
|
|
1608
1665
|
};
|
|
1609
1666
|
for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
|
|
1610
|
-
else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to
|
|
1667
|
+
else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to ${envFile}`);
|
|
1611
1668
|
}
|
|
1612
1669
|
async function runLogin(options) {
|
|
1613
1670
|
const { deps } = options;
|
|
@@ -1650,7 +1707,10 @@ function rankCandidates$1(tables, concept) {
|
|
|
1650
1707
|
m: require_internal_entity_heuristics.matchesConcept(t.tableName, concept)
|
|
1651
1708
|
})).filter((c) => Boolean(c.m)).sort((a, b) => {
|
|
1652
1709
|
const usable = Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(b.t))) - Number(require_internal_entity_heuristics.hasDefiningColumn(concept, columnNames(a.t)));
|
|
1653
|
-
|
|
1710
|
+
if (usable !== 0) return usable;
|
|
1711
|
+
const exact = Number(b.m.exact) - Number(a.m.exact);
|
|
1712
|
+
if (exact !== 0) return exact;
|
|
1713
|
+
return Number(require_internal_entity_heuristics.isFrameworkTable(a.t.tableName)) - Number(require_internal_entity_heuristics.isFrameworkTable(b.t.tableName));
|
|
1654
1714
|
}).map((c) => c.t);
|
|
1655
1715
|
}
|
|
1656
1716
|
function selectEntities(tables) {
|
|
@@ -1893,7 +1953,14 @@ export async function register() {
|
|
|
1893
1953
|
return;
|
|
1894
1954
|
}
|
|
1895
1955
|
try {
|
|
1896
|
-
|
|
1956
|
+
// Built at runtime so the bundler cannot statically trace it. Next
|
|
1957
|
+
// compiles this file for BOTH runtimes, and the guard above only runs on
|
|
1958
|
+
// the edge — it does not stop the edge BUILD from following a literal
|
|
1959
|
+
// import into your config, and from there into your database driver. A
|
|
1960
|
+
// node-postgres app fails that pass on \`require('path')\` and every route
|
|
1961
|
+
// 500s before Clivly is ever reached.
|
|
1962
|
+
const specifier = "${importPath}";
|
|
1963
|
+
const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
|
|
1897
1964
|
await clivly.start();
|
|
1898
1965
|
} catch (error) {
|
|
1899
1966
|
console.warn("[clivly] could not check in:", error);
|
|
@@ -2701,7 +2768,14 @@ function reportSchema(report, out) {
|
|
|
2701
2768
|
const updated = report.mappings.filter((m) => m.updated);
|
|
2702
2769
|
if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
|
|
2703
2770
|
if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
|
|
2704
|
-
|
|
2771
|
+
const skipped = report.mappings.filter((m) => m.skipped === "dashboard-owned");
|
|
2772
|
+
if (skipped.length > 0) {
|
|
2773
|
+
out(` ↩︎ Kept the dashboard's version of ${countOf(skipped.length, "entity", "entities")}: ${skipped.map((m) => m.entityKey).join(", ")}.\n`);
|
|
2774
|
+
out(" The dashboard owns these mappings, so clivly.config did not change them.\n");
|
|
2775
|
+
out(" To let your config own one, delete it in Integrations → Mapping, then push again.\n");
|
|
2776
|
+
}
|
|
2777
|
+
const settled = report.mappings.length - skipped.length;
|
|
2778
|
+
if (settled > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(settled, "mapping")} already match your config.\n`);
|
|
2705
2779
|
}
|
|
2706
2780
|
async function runPushSchema(options) {
|
|
2707
2781
|
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
@@ -4277,7 +4351,8 @@ function defaultLoginDeps() {
|
|
|
4277
4351
|
sleep: (ms) => new Promise((resolve) => {
|
|
4278
4352
|
setTimeout(resolve, ms);
|
|
4279
4353
|
}),
|
|
4280
|
-
writeEnv: defaultWriteEnv
|
|
4354
|
+
writeEnv: defaultWriteEnv,
|
|
4355
|
+
envFileName: (cwd) => (0, node_path.basename)(resolveEnvPath(cwd))
|
|
4281
4356
|
};
|
|
4282
4357
|
}
|
|
4283
4358
|
function runInitFromArgv(argv, cwd) {
|
package/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.mjs";
|
|
3
3
|
import "./mapping-score-C68uAqGB.mjs";
|
|
4
|
-
import { hasDefiningColumn, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
|
|
4
|
+
import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
|
|
5
5
|
import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
|
|
9
9
|
import { homedir, hostname } from "node:os";
|
|
10
|
-
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
12
|
import { createJiti } from "jiti";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -479,23 +479,38 @@ function cloudflaredMissingMessage() {
|
|
|
479
479
|
* Warning someone away from a tunnel that was fine is worse than staying quiet,
|
|
480
480
|
* for the same reason the port probe defaults to assuming the port is alive.
|
|
481
481
|
*/
|
|
482
|
-
/** How long
|
|
482
|
+
/** How long any single probe attempt may take. */
|
|
483
483
|
const IDENTITY_PROBE_TIMEOUT_MS = 8e3;
|
|
484
484
|
/**
|
|
485
|
-
*
|
|
485
|
+
* How long to keep retrying before giving up.
|
|
486
486
|
*
|
|
487
|
-
*
|
|
487
|
+
* The probe fires the moment `cloudflared` publishes a URL, which on a
|
|
488
|
+
* Vite/Nuxt app is *before* the dev server has finished building its route
|
|
489
|
+
* table. The first attempt therefore reaches a server that is up but does not
|
|
490
|
+
* yet serve the tick route — a cold start, not a wrong port. The condition
|
|
491
|
+
* being detected (a tunnel aimed at the wrong process) does not resolve
|
|
492
|
+
* itself, so waiting costs nothing and a premature verdict costs a lot.
|
|
488
493
|
*/
|
|
489
|
-
|
|
494
|
+
const IDENTITY_RETRY_BUDGET_MS = 2e4;
|
|
495
|
+
const IDENTITY_RETRY_DELAY_MS = 2e3;
|
|
496
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
497
|
+
setTimeout(resolve, ms);
|
|
498
|
+
});
|
|
499
|
+
/** One attempt. Separated so the retry loop stays readable. */
|
|
500
|
+
async function attempt(triggerUrl, fetchImpl) {
|
|
490
501
|
const controller = new AbortController();
|
|
491
502
|
const timer = setTimeout(() => controller.abort(), IDENTITY_PROBE_TIMEOUT_MS);
|
|
492
503
|
try {
|
|
493
|
-
|
|
504
|
+
const response = await fetchImpl(triggerUrl, {
|
|
494
505
|
method: "POST",
|
|
495
506
|
headers: { "content-type": "application/json" },
|
|
496
507
|
body: "{}",
|
|
497
508
|
signal: controller.signal
|
|
498
|
-
})
|
|
509
|
+
});
|
|
510
|
+
if (response.status === 401) return "clivly";
|
|
511
|
+
if (response.status === 403) return "host-rejected";
|
|
512
|
+
if (response.status === 404 || response.status >= 500) return "unknown";
|
|
513
|
+
return "mismatch";
|
|
499
514
|
} catch {
|
|
500
515
|
return "unknown";
|
|
501
516
|
} finally {
|
|
@@ -503,6 +518,40 @@ async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch) {
|
|
|
503
518
|
}
|
|
504
519
|
}
|
|
505
520
|
/**
|
|
521
|
+
* Probe the published tunnel URL and report who is on the other end.
|
|
522
|
+
*
|
|
523
|
+
* Retries while the answer is inconclusive, because the first attempt lands
|
|
524
|
+
* during the dev server's boot. Only a confident contradiction is reported.
|
|
525
|
+
*
|
|
526
|
+
* `fetchImpl` and `now` are injectable so tests need no network or clock.
|
|
527
|
+
*/
|
|
528
|
+
async function probeTunnelIdentity(triggerUrl, fetchImpl = fetch, now = Date.now) {
|
|
529
|
+
const deadline = now() + IDENTITY_RETRY_BUDGET_MS;
|
|
530
|
+
let last = "unknown";
|
|
531
|
+
do {
|
|
532
|
+
last = await attempt(triggerUrl, fetchImpl);
|
|
533
|
+
if (last !== "unknown") return last;
|
|
534
|
+
if (now() >= deadline) break;
|
|
535
|
+
await sleep(IDENTITY_RETRY_DELAY_MS);
|
|
536
|
+
} while (now() < deadline);
|
|
537
|
+
return last;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* What to tell someone whose dev server refused the tunnel's Host header.
|
|
541
|
+
*
|
|
542
|
+
* `clivly dev` already prints the allowedHosts hint for Vite frameworks before
|
|
543
|
+
* the tunnel opens; this fires when the tunnel proves it was not applied.
|
|
544
|
+
*/
|
|
545
|
+
function hostRejectedMessage() {
|
|
546
|
+
return [
|
|
547
|
+
"Warning: your dev server refused the tunnel's Host header (403).",
|
|
548
|
+
" Vite rejects hostnames it does not recognise, so Clivly's request never",
|
|
549
|
+
" reaches your tick route. Add the tunnel domain to your Vite config:",
|
|
550
|
+
" server: { allowedHosts: ['.trycloudflare.com'] }",
|
|
551
|
+
" (in nuxt.config.ts, nest it under `vite:`), then restart the dev server."
|
|
552
|
+
].join("\n");
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
506
555
|
* What to tell someone whose tunnel reaches the wrong process.
|
|
507
556
|
*
|
|
508
557
|
* Names the likely cause rather than only the symptom: the overwhelmingly
|
|
@@ -525,6 +574,12 @@ const UNREACHABLE_RESULT = {
|
|
|
525
574
|
ok: false,
|
|
526
575
|
kind: "unreachable"
|
|
527
576
|
};
|
|
577
|
+
/** The line to print for a probe verdict, or null when there is nothing to say. */
|
|
578
|
+
function tunnelWarning(identity, port) {
|
|
579
|
+
if (identity === "mismatch") return tunnelMismatchMessage(port);
|
|
580
|
+
if (identity === "host-rejected") return hostRejectedMessage();
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
528
583
|
function flagValue$1(argv, flag) {
|
|
529
584
|
const i = argv.indexOf(flag);
|
|
530
585
|
return i >= 0 ? argv[i + 1] : void 0;
|
|
@@ -569,9 +624,10 @@ async function runDev(options) {
|
|
|
569
624
|
if (!await deps.isPortListening(port).catch(() => true)) deps.out(deadPortMessage(port, framework));
|
|
570
625
|
}
|
|
571
626
|
const identity = deps.probeTunnel && !flagValue$1(options.argv, "--url") ? await deps.probeTunnel(triggerUrl).catch(() => "unknown") : "unknown";
|
|
572
|
-
|
|
627
|
+
const warning = tunnelWarning(identity, port);
|
|
628
|
+
if (warning) deps.out(warning);
|
|
573
629
|
const result = await deps.register(triggerUrl).catch(() => UNREACHABLE_RESULT);
|
|
574
|
-
if (result.ok) deps.out(identity === "mismatch" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
|
|
630
|
+
if (result.ok) deps.out(identity === "mismatch" || identity === "host-rejected" ? "Trigger URL registered, but see the warning above before relying on it." : "Dashboard-triggered sync now points at this local session.");
|
|
575
631
|
else deps.out(overrideFailureMessage(result.kind, result.status));
|
|
576
632
|
});
|
|
577
633
|
const stopWatching = deps.watchConfig?.(options.cwd, () => {
|
|
@@ -1600,13 +1656,14 @@ function writeCredentials(options, credentials) {
|
|
|
1600
1656
|
const entries = credentialEntries(credentials);
|
|
1601
1657
|
const result = upsertEnvValues(deps.readEnv(options.cwd), entries, { force: options.force });
|
|
1602
1658
|
deps.writeEnv(options.cwd, result.contents);
|
|
1659
|
+
const envFile = deps.envFileName?.(options.cwd) ?? ".env";
|
|
1603
1660
|
deps.out("\nLogin successful.\n");
|
|
1604
1661
|
const valuesByKey = {
|
|
1605
1662
|
[SECRET_KEY_ENV]: credentials.apiKey,
|
|
1606
1663
|
CLIVLY_SYNC_TRIGGER_SECRET: credentials.triggerSecret
|
|
1607
1664
|
};
|
|
1608
1665
|
for (const entry of entries) if (result.skipped.includes(entry.key)) deps.out(` • ${entry.key} already set — skipped (pass --force to overwrite)`);
|
|
1609
|
-
else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to
|
|
1666
|
+
else deps.out(` ✓ ${entry.key}=${maskSecret(valuesByKey[entry.key] ?? "")} written to ${envFile}`);
|
|
1610
1667
|
}
|
|
1611
1668
|
async function runLogin(options) {
|
|
1612
1669
|
const { deps } = options;
|
|
@@ -1649,7 +1706,10 @@ function rankCandidates$1(tables, concept) {
|
|
|
1649
1706
|
m: matchesConcept(t.tableName, concept)
|
|
1650
1707
|
})).filter((c) => Boolean(c.m)).sort((a, b) => {
|
|
1651
1708
|
const usable = Number(hasDefiningColumn(concept, columnNames(b.t))) - Number(hasDefiningColumn(concept, columnNames(a.t)));
|
|
1652
|
-
|
|
1709
|
+
if (usable !== 0) return usable;
|
|
1710
|
+
const exact = Number(b.m.exact) - Number(a.m.exact);
|
|
1711
|
+
if (exact !== 0) return exact;
|
|
1712
|
+
return Number(isFrameworkTable(a.t.tableName)) - Number(isFrameworkTable(b.t.tableName));
|
|
1653
1713
|
}).map((c) => c.t);
|
|
1654
1714
|
}
|
|
1655
1715
|
function selectEntities(tables) {
|
|
@@ -1892,7 +1952,14 @@ export async function register() {
|
|
|
1892
1952
|
return;
|
|
1893
1953
|
}
|
|
1894
1954
|
try {
|
|
1895
|
-
|
|
1955
|
+
// Built at runtime so the bundler cannot statically trace it. Next
|
|
1956
|
+
// compiles this file for BOTH runtimes, and the guard above only runs on
|
|
1957
|
+
// the edge — it does not stop the edge BUILD from following a literal
|
|
1958
|
+
// import into your config, and from there into your database driver. A
|
|
1959
|
+
// node-postgres app fails that pass on \`require('path')\` and every route
|
|
1960
|
+
// 500s before Clivly is ever reached.
|
|
1961
|
+
const specifier = "${importPath}";
|
|
1962
|
+
const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
|
|
1896
1963
|
await clivly.start();
|
|
1897
1964
|
} catch (error) {
|
|
1898
1965
|
console.warn("[clivly] could not check in:", error);
|
|
@@ -2700,7 +2767,14 @@ function reportSchema(report, out) {
|
|
|
2700
2767
|
const updated = report.mappings.filter((m) => m.updated);
|
|
2701
2768
|
if (created.length > 0) out(` ✅ Mapped ${countOf(created.length, "entity", "entities")}: ${created.map((m) => m.entityKey).join(", ")}.\n`);
|
|
2702
2769
|
if (updated.length > 0) out(` ✅ Updated ${countOf(updated.length, "mapping")}: ${updated.map((m) => m.entityKey).join(", ")}.\n`);
|
|
2703
|
-
|
|
2770
|
+
const skipped = report.mappings.filter((m) => m.skipped === "dashboard-owned");
|
|
2771
|
+
if (skipped.length > 0) {
|
|
2772
|
+
out(` ↩︎ Kept the dashboard's version of ${countOf(skipped.length, "entity", "entities")}: ${skipped.map((m) => m.entityKey).join(", ")}.\n`);
|
|
2773
|
+
out(" The dashboard owns these mappings, so clivly.config did not change them.\n");
|
|
2774
|
+
out(" To let your config own one, delete it in Integrations → Mapping, then push again.\n");
|
|
2775
|
+
}
|
|
2776
|
+
const settled = report.mappings.length - skipped.length;
|
|
2777
|
+
if (settled > 0 && created.length === 0 && updated.length === 0) out(` ✅ ${countOf(settled, "mapping")} already match your config.\n`);
|
|
2704
2778
|
}
|
|
2705
2779
|
async function runPushSchema(options) {
|
|
2706
2780
|
const out = options.out ?? ((line) => process.stdout.write(line));
|
|
@@ -4276,7 +4350,8 @@ function defaultLoginDeps() {
|
|
|
4276
4350
|
sleep: (ms) => new Promise((resolve) => {
|
|
4277
4351
|
setTimeout(resolve, ms);
|
|
4278
4352
|
}),
|
|
4279
|
-
writeEnv: defaultWriteEnv
|
|
4353
|
+
writeEnv: defaultWriteEnv,
|
|
4354
|
+
envFileName: (cwd) => basename(resolveEnvPath(cwd))
|
|
4280
4355
|
};
|
|
4281
4356
|
}
|
|
4282
4357
|
function runInitFromArgv(argv, cwd) {
|
|
@@ -283,6 +283,54 @@ const SENSITIVE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
283
283
|
function hasSensitiveColumns(columns) {
|
|
284
284
|
return columns.some((column) => SENSITIVE_COLUMNS.has(column.toLowerCase().replaceAll("-", "_")));
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Table names the supported auth libraries create.
|
|
288
|
+
*
|
|
289
|
+
* `hasSensitiveColumns` cannot catch these. Better Auth's `organization` table
|
|
290
|
+
* is `{id, name, slug, logo}` — no credential column anywhere, a real `name`,
|
|
291
|
+
* and "organizations" is in the company vocabulary above. It is, by every
|
|
292
|
+
* signal this module has, an excellent company candidate. On `vue-academy`
|
|
293
|
+
* `clivly init` duly chose it over `campuses`, the actual business table that
|
|
294
|
+
* every student row points at, and the first sync exported rows nothing could
|
|
295
|
+
* receive.
|
|
296
|
+
*
|
|
297
|
+
* Matched on name, deliberately, and this is the inverse of the rule above:
|
|
298
|
+
* withholding a table on a wrong guess is a real loss, so that is decided on
|
|
299
|
+
* columns; *ranking* one below another is cheap to get wrong, so a name match
|
|
300
|
+
* is enough. These are fixed names the plugins create, and an app whose own
|
|
301
|
+
* business table is called `member` still ranks — just after its rivals.
|
|
302
|
+
*/
|
|
303
|
+
const FRAMEWORK_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
304
|
+
"account",
|
|
305
|
+
"session",
|
|
306
|
+
"user",
|
|
307
|
+
"verification",
|
|
308
|
+
"invitation",
|
|
309
|
+
"member",
|
|
310
|
+
"organization",
|
|
311
|
+
"team",
|
|
312
|
+
"team_member",
|
|
313
|
+
"apikey",
|
|
314
|
+
"api_key",
|
|
315
|
+
"jwks",
|
|
316
|
+
"passkey",
|
|
317
|
+
"rate_limit",
|
|
318
|
+
"ratelimit",
|
|
319
|
+
"two_factor",
|
|
320
|
+
"twofactor",
|
|
321
|
+
"__drizzle_migrations",
|
|
322
|
+
"drizzle_migrations"
|
|
323
|
+
]);
|
|
324
|
+
/**
|
|
325
|
+
* Is this table one your auth library created, rather than one you wrote?
|
|
326
|
+
*
|
|
327
|
+
* Used to rank such tables below the developer's own when both could serve a
|
|
328
|
+
* concept. Never to exclude them: mapping `user` as the contact entity is
|
|
329
|
+
* legitimate for plenty of apps, and it stays selectable.
|
|
330
|
+
*/
|
|
331
|
+
function isFrameworkTable(name) {
|
|
332
|
+
return FRAMEWORK_TABLE_NAMES.has(name.toLowerCase().replaceAll("-", "_"));
|
|
333
|
+
}
|
|
286
334
|
function metaFor(table, column) {
|
|
287
335
|
return table.columnsMeta?.find((c) => c.name === column);
|
|
288
336
|
}
|
|
@@ -497,6 +545,7 @@ function toFieldMap(scored) {
|
|
|
497
545
|
exports.TARGET_CONCEPTS = TARGET_CONCEPTS;
|
|
498
546
|
exports.hasDefiningColumn = hasDefiningColumn;
|
|
499
547
|
exports.hasSensitiveColumns = hasSensitiveColumns;
|
|
548
|
+
exports.isFrameworkTable = isFrameworkTable;
|
|
500
549
|
exports.matchesConcept = matchesConcept;
|
|
501
550
|
exports.requiredFieldsFor = requiredFieldsFor;
|
|
502
551
|
exports.scoreConcepts = scoreConcepts;
|
|
@@ -19,6 +19,14 @@ interface ScoredTable {
|
|
|
19
19
|
* They stay manually selectable — an explicit, informed choice is legitimate.
|
|
20
20
|
*/
|
|
21
21
|
declare function hasSensitiveColumns(columns: string[]): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Is this table one your auth library created, rather than one you wrote?
|
|
24
|
+
*
|
|
25
|
+
* Used to rank such tables below the developer's own when both could serve a
|
|
26
|
+
* concept. Never to exclude them: mapping `user` as the contact entity is
|
|
27
|
+
* legitimate for plenty of apps, and it stays selectable.
|
|
28
|
+
*/
|
|
29
|
+
declare function isFrameworkTable(name: string): boolean;
|
|
22
30
|
declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
|
|
23
31
|
declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
|
|
24
32
|
declare function topConcept(table: ScoredTable): {
|
|
@@ -61,4 +69,4 @@ interface FieldSuggestion {
|
|
|
61
69
|
declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
|
|
62
70
|
declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
|
|
63
71
|
//#endregion
|
|
64
|
-
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
72
|
+
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
@@ -19,6 +19,14 @@ interface ScoredTable {
|
|
|
19
19
|
* They stay manually selectable — an explicit, informed choice is legitimate.
|
|
20
20
|
*/
|
|
21
21
|
declare function hasSensitiveColumns(columns: string[]): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Is this table one your auth library created, rather than one you wrote?
|
|
24
|
+
*
|
|
25
|
+
* Used to rank such tables below the developer's own when both could serve a
|
|
26
|
+
* concept. Never to exclude them: mapping `user` as the contact entity is
|
|
27
|
+
* legitimate for plenty of apps, and it stays selectable.
|
|
28
|
+
*/
|
|
29
|
+
declare function isFrameworkTable(name: string): boolean;
|
|
22
30
|
declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
|
|
23
31
|
declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
|
|
24
32
|
declare function topConcept(table: ScoredTable): {
|
|
@@ -61,4 +69,4 @@ interface FieldSuggestion {
|
|
|
61
69
|
declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
|
|
62
70
|
declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
|
|
63
71
|
//#endregion
|
|
64
|
-
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
72
|
+
export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
@@ -282,6 +282,54 @@ const SENSITIVE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
282
282
|
function hasSensitiveColumns(columns) {
|
|
283
283
|
return columns.some((column) => SENSITIVE_COLUMNS.has(column.toLowerCase().replaceAll("-", "_")));
|
|
284
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Table names the supported auth libraries create.
|
|
287
|
+
*
|
|
288
|
+
* `hasSensitiveColumns` cannot catch these. Better Auth's `organization` table
|
|
289
|
+
* is `{id, name, slug, logo}` — no credential column anywhere, a real `name`,
|
|
290
|
+
* and "organizations" is in the company vocabulary above. It is, by every
|
|
291
|
+
* signal this module has, an excellent company candidate. On `vue-academy`
|
|
292
|
+
* `clivly init` duly chose it over `campuses`, the actual business table that
|
|
293
|
+
* every student row points at, and the first sync exported rows nothing could
|
|
294
|
+
* receive.
|
|
295
|
+
*
|
|
296
|
+
* Matched on name, deliberately, and this is the inverse of the rule above:
|
|
297
|
+
* withholding a table on a wrong guess is a real loss, so that is decided on
|
|
298
|
+
* columns; *ranking* one below another is cheap to get wrong, so a name match
|
|
299
|
+
* is enough. These are fixed names the plugins create, and an app whose own
|
|
300
|
+
* business table is called `member` still ranks — just after its rivals.
|
|
301
|
+
*/
|
|
302
|
+
const FRAMEWORK_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
303
|
+
"account",
|
|
304
|
+
"session",
|
|
305
|
+
"user",
|
|
306
|
+
"verification",
|
|
307
|
+
"invitation",
|
|
308
|
+
"member",
|
|
309
|
+
"organization",
|
|
310
|
+
"team",
|
|
311
|
+
"team_member",
|
|
312
|
+
"apikey",
|
|
313
|
+
"api_key",
|
|
314
|
+
"jwks",
|
|
315
|
+
"passkey",
|
|
316
|
+
"rate_limit",
|
|
317
|
+
"ratelimit",
|
|
318
|
+
"two_factor",
|
|
319
|
+
"twofactor",
|
|
320
|
+
"__drizzle_migrations",
|
|
321
|
+
"drizzle_migrations"
|
|
322
|
+
]);
|
|
323
|
+
/**
|
|
324
|
+
* Is this table one your auth library created, rather than one you wrote?
|
|
325
|
+
*
|
|
326
|
+
* Used to rank such tables below the developer's own when both could serve a
|
|
327
|
+
* concept. Never to exclude them: mapping `user` as the contact entity is
|
|
328
|
+
* legitimate for plenty of apps, and it stays selectable.
|
|
329
|
+
*/
|
|
330
|
+
function isFrameworkTable(name) {
|
|
331
|
+
return FRAMEWORK_TABLE_NAMES.has(name.toLowerCase().replaceAll("-", "_"));
|
|
332
|
+
}
|
|
285
333
|
function metaFor(table, column) {
|
|
286
334
|
return table.columnsMeta?.find((c) => c.name === column);
|
|
287
335
|
}
|
|
@@ -493,4 +541,4 @@ function toFieldMap(scored) {
|
|
|
493
541
|
return map;
|
|
494
542
|
}
|
|
495
543
|
//#endregion
|
|
496
|
-
export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
|
544
|
+
export { TARGET_CONCEPTS, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|