@kweaver-ai/kweaver-sdk 0.6.10 → 0.7.2

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.
@@ -1,4 +1,5 @@
1
1
  import { isNoAuth } from "../config/no-auth.js";
2
+ import { assertNotStatelessForWrite } from "../config/stateless.js";
2
3
  import { autoSelectBusinessDomain, clearPlatformSession, deletePlatform, deleteUser, getActiveUser, getConfigDir, getCurrentPlatform, getPlatformAlias, hasPlatform, listPlatforms, listUserProfiles, loadClientConfig, loadTokenConfig, loadUserTokenConfig, resolveBusinessDomain, resolvePlatformIdentifier, resolveUserId, saveNoAuthPlatform, setActiveUser, setCurrentPlatform, setPlatformAlias, } from "../config/store.js";
3
4
  import { decodeJwtPayload } from "../config/jwt.js";
4
5
  import { eacpModifyPassword } from "../auth/eacp-modify-password.js";
@@ -71,6 +72,13 @@ Login options:
71
72
  const LOGIN_SUBCOMMANDS = new Set(["status", "list", "use", "delete", "logout", "export", "whoami", "users", "switch"]);
72
73
  if (target && !LOGIN_SUBCOMMANDS.has(target)) {
73
74
  try {
75
+ try {
76
+ assertNotStatelessForWrite("auth login");
77
+ }
78
+ catch (err) {
79
+ console.error(err instanceof Error ? err.message : String(err));
80
+ return 1;
81
+ }
74
82
  const normalizedTarget = normalizeBaseUrl(target);
75
83
  const alias = readOption(args, "--alias");
76
84
  let username = readOption(args, "--username") ?? readOption(args, "-u");
@@ -268,7 +276,8 @@ Login options:
268
276
  }
269
277
  console.log(`Config directory: ${getConfigDir()}`);
270
278
  console.log(`Platform: ${active.url} (KWEAVER_BASE_URL)`);
271
- console.log(`Token present: yes (KWEAVER_TOKEN)`);
279
+ const tokenProvenance = process.env.KWEAVER_TOKEN_SOURCE === "flag" ? "CLI (flag: --token)" : "KWEAVER_TOKEN";
280
+ console.log(`Token present: yes (${tokenProvenance})`);
272
281
  console.log(`Refresh token: n/a (env)`);
273
282
  return 0;
274
283
  }
@@ -358,6 +367,13 @@ Login options:
358
367
  console.error(`No saved token for ${useTarget}. Run \`kweaver auth login ${useTarget}\` first.`);
359
368
  return 1;
360
369
  }
370
+ try {
371
+ assertNotStatelessForWrite("auth use");
372
+ }
373
+ catch (err) {
374
+ console.error(err instanceof Error ? err.message : String(err));
375
+ return 1;
376
+ }
361
377
  setCurrentPlatform(useTarget);
362
378
  console.log(`Current platform: ${useTarget}`);
363
379
  return 0;
@@ -375,6 +391,13 @@ Login options:
375
391
  console.error(`No saved token for ${deleteTarget}.`);
376
392
  return 1;
377
393
  }
394
+ try {
395
+ assertNotStatelessForWrite("auth delete");
396
+ }
397
+ catch (err) {
398
+ console.error(err instanceof Error ? err.message : String(err));
399
+ return 1;
400
+ }
378
401
  if (deleteUserArg) {
379
402
  const deleteUserId = resolveUserId(deleteTarget, deleteUserArg) ?? deleteUserArg;
380
403
  deleteUser(deleteTarget, deleteUserId);
@@ -404,6 +427,13 @@ Login options:
404
427
  console.error(`No saved token for ${logoutTarget}.`);
405
428
  return 1;
406
429
  }
430
+ try {
431
+ assertNotStatelessForWrite("auth logout");
432
+ }
433
+ catch (err) {
434
+ console.error(err instanceof Error ? err.message : String(err));
435
+ return 1;
436
+ }
407
437
  const logoutUserId = logoutUserArg ? resolveUserId(logoutTarget, logoutUserArg) ?? logoutUserArg : undefined;
408
438
  clearPlatformSession(logoutTarget, logoutUserId);
409
439
  const userHint = logoutUserId ? ` (user: ${logoutUserId})` : "";
@@ -487,6 +517,13 @@ You can specify either the userId (sub claim) or the username (preferred_usernam
487
517
  }
488
518
  return 1;
489
519
  }
520
+ try {
521
+ assertNotStatelessForWrite("auth switch");
522
+ }
523
+ catch (err) {
524
+ console.error(err instanceof Error ? err.message : String(err));
525
+ return 1;
526
+ }
490
527
  setActiveUser(platform, resolvedId);
491
528
  const profiles = listUserProfiles(platform);
492
529
  const profile = profiles.find((p) => p.userId === resolvedId);
@@ -535,7 +572,10 @@ Options:
535
572
  // complete picture without forcing them to pick a mode.
536
573
  const jwtPayload = decodeJwtPayload(accessToken);
537
574
  if (jsonOutput) {
538
- const out = { platform: envUrl, source: "env" };
575
+ const out = {
576
+ platform: envUrl,
577
+ source: process.env.KWEAVER_TOKEN_SOURCE === "flag" ? "flag" : "env",
578
+ };
539
579
  if (userInfo)
540
580
  out.userInfo = userInfo;
541
581
  if (jwtPayload)
@@ -544,7 +584,7 @@ Options:
544
584
  return 0;
545
585
  }
546
586
  console.log(`Platform: ${envUrl}`);
547
- console.log(`Source: env (KWEAVER_TOKEN)`);
587
+ console.log(`Source: ${process.env.KWEAVER_TOKEN_SOURCE === "flag" ? "CLI (flag: --token)" : "env (KWEAVER_TOKEN)"}`);
548
588
  if (userInfo) {
549
589
  console.log(`Type: ${userInfo.type}`);
550
590
  console.log(`User ID: ${userInfo.id}`);
@@ -1,4 +1,6 @@
1
1
  import { type BknEncodingImportOptions } from "../utils/bkn-encoding.js";
2
+ export declare const BKN_OBJECT_NAME_MAX_LENGTH = 40;
3
+ export declare function assertValidBknObjectNames(names: string[], context: string): void;
2
4
  export declare function parseKnBuildArgs(args: string[]): {
3
5
  knId: string;
4
6
  wait: boolean;
@@ -34,6 +36,7 @@ export declare function parseKnCreateFromDsArgs(args: string[]): {
34
36
  timeout: number;
35
37
  businessDomain: string;
36
38
  pretty: boolean;
39
+ noRollback: boolean;
37
40
  };
38
41
  /** Generate a BKN ObjectType YAML markdown file for a table. */
39
42
  export declare function generateObjectTypeBkn(tableName: string, dvId: string, pk: string, dk: string, columns: Array<{
@@ -52,6 +55,7 @@ export declare function parseKnCreateFromCsvArgs(args: string[]): {
52
55
  recreate: boolean;
53
56
  timeout: number;
54
57
  businessDomain: string;
58
+ noRollback: boolean;
55
59
  };
56
60
  export declare function runKnCreateFromCsvCommand(args: string[]): Promise<number>;
57
61
  export interface ActionScheduleParsed {
@@ -4,14 +4,36 @@ import { resolve } from "node:path";
4
4
  import { loadNetwork, allObjects, allRelations, allActions, generateChecksum, validateNetwork } from "@kweaver-ai/bkn";
5
5
  import { prepareBknDirectoryForImport, stripBknEncodingCliArgs, } from "../utils/bkn-encoding.js";
6
6
  import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
7
- import { createKnowledgeNetwork, createObjectTypes, buildKnowledgeNetwork, getBuildStatus, } from "../api/knowledge-networks.js";
7
+ import { createKnowledgeNetwork, createObjectTypes, deleteKnowledgeNetwork, buildKnowledgeNetwork, getBuildStatus, } from "../api/knowledge-networks.js";
8
8
  import { listTablesWithColumns, scanMetadata, getDatasource } from "../api/datasources.js";
9
9
  import { createDataView, findDataView } from "../api/dataviews.js";
10
+ import { resolveFiles } from "./ds.js";
11
+ import { buildTableName } from "./import-csv.js";
10
12
  import { downloadBkn, uploadBkn, listActionSchedules, getActionSchedule, createActionSchedule, updateActionSchedule, setActionScheduleStatus, deleteActionSchedules, listJobs, getJob, getJobTasks, deleteJobs, } from "../api/bkn-backend.js";
11
13
  import { formatCallOutput } from "./call.js";
12
14
  import { resolveBusinessDomain } from "../config/store.js";
13
15
  import { runDsImportCsv } from "./ds.js";
14
16
  import { pollWithBackoff, detectPrimaryKey, detectDisplayKey, confirmYes, } from "./bkn-utils.js";
17
+ // ── BKN object name validation ──────────────────────────────────────────────
18
+ // Mirrors bkn-backend OBJECT_NAME_MAX_LENGTH (interfaces/common.go:28) and
19
+ // validateObjectName (driveradapters/validate.go:85). 40 utf-8 codepoints,
20
+ // non-empty. Backend rejects the whole batch on first violation, so we surface
21
+ // every offender locally before any side-effecting call.
22
+ export const BKN_OBJECT_NAME_MAX_LENGTH = 40;
23
+ export function assertValidBknObjectNames(names, context) {
24
+ const offenders = [];
25
+ for (const name of names) {
26
+ const len = [...name].length;
27
+ if (len === 0 || len > BKN_OBJECT_NAME_MAX_LENGTH) {
28
+ offenders.push({ name, length: len });
29
+ }
30
+ }
31
+ if (offenders.length === 0)
32
+ return;
33
+ const lines = offenders.map((o) => ` - ${o.name} (${o.length} chars)`);
34
+ throw new Error(`${context}: ${offenders.length} name(s) violate BKN object-name limit ` +
35
+ `(1..${BKN_OBJECT_NAME_MAX_LENGTH} utf-8 chars):\n${lines.join("\n")}`);
36
+ }
15
37
  // ── Build ───────────────────────────────────────────────────────────────────
16
38
  const KN_BUILD_HELP = `kweaver bkn build <kn-id> [options]
17
39
 
@@ -461,6 +483,7 @@ Options:
461
483
  --build (default) Build after creation
462
484
  --no-build Skip build after creation
463
485
  --timeout <n> Build timeout in seconds (default: 300)
486
+ --no-rollback Keep partially-created KN on failure (debug; default: rollback)
464
487
  -bd, --biz-domain Business domain (default: bd_public)
465
488
  --pretty Pretty-print output (default)`;
466
489
  export function parseKnCreateFromDsArgs(args) {
@@ -471,6 +494,7 @@ export function parseKnCreateFromDsArgs(args) {
471
494
  let timeout = 300;
472
495
  let businessDomain = "";
473
496
  let pretty = true;
497
+ let noRollback = false;
474
498
  for (let i = 0; i < args.length; i += 1) {
475
499
  const arg = args[i];
476
500
  if (arg === "--help" || arg === "-h")
@@ -491,6 +515,10 @@ export function parseKnCreateFromDsArgs(args) {
491
515
  build = false;
492
516
  continue;
493
517
  }
518
+ if (arg === "--no-rollback") {
519
+ noRollback = true;
520
+ continue;
521
+ }
494
522
  if (arg === "--timeout" && args[i + 1]) {
495
523
  timeout = parseInt(args[++i], 10);
496
524
  if (Number.isNaN(timeout) || timeout < 1)
@@ -515,7 +543,7 @@ export function parseKnCreateFromDsArgs(args) {
515
543
  }
516
544
  if (!businessDomain)
517
545
  businessDomain = resolveBusinessDomain();
518
- return { dsId, name, tables, build, timeout, businessDomain, pretty };
546
+ return { dsId, name, tables, build, timeout, businessDomain, pretty, noRollback };
519
547
  }
520
548
  /** Sanitize a table name into a BKN-safe ID (alphanumeric + underscore). */
521
549
  function sanitizeBknId(name) {
@@ -576,7 +604,12 @@ export async function runKnCreateFromDsCommand(args, sampleRows) {
576
604
  console.error("No tables available");
577
605
  return 1;
578
606
  }
579
- // Phase 1: Create DataViews for each table
607
+ // Pre-flight: catch every offending OT name before any side effect.
608
+ // Backend rejects the whole batch on first violation (validate.go:90),
609
+ // so retroactive rollback is wasted work if we can fail fast here.
610
+ assertValidBknObjectNames(targetTables.map((t) => t.name), "Object type names derived from table names");
611
+ // Phase 1: Create DataViews for each table. findDataView is idempotent;
612
+ // not tracked for rollback so a retry can reuse what's already there.
580
613
  console.error(`Creating data views for ${targetTables.length} table(s) ...`);
581
614
  const viewMap = {};
582
615
  for (const t of targetTables) {
@@ -597,7 +630,8 @@ export async function runKnCreateFromDsCommand(args, sampleRows) {
597
630
  }));
598
631
  viewMap[t.name] = dvId;
599
632
  }
600
- // Phase 2: Create the KN record
633
+ // Phase 2: Create the KN. If any subsequent step fails we DELETE this
634
+ // KN — backend cascades to OTs (knowledge_network_service.go:917-969).
601
635
  const knBody = JSON.stringify({
602
636
  name: options.name,
603
637
  branch: "main",
@@ -611,72 +645,91 @@ export async function runKnCreateFromDsCommand(args, sampleRows) {
611
645
  const knItem = Array.isArray(knParsed) ? knParsed[0] : knParsed;
612
646
  const knId = String(knItem?.id ?? "");
613
647
  console.error(`Knowledge network created: ${knId}`);
614
- // Phase 3: Create object types via REST API
615
- console.error(`Creating ${targetTables.length} object type(s) ...`);
648
+ let createdKnId = knId;
616
649
  const otResults = [];
617
- for (const t of targetTables) {
618
- const pk = detectPrimaryKey(t, sampleRows?.[t.name]);
619
- const dk = detectDisplayKey(t, pk);
620
- const uniqueProps = [pk, dk].filter((x, i, a) => a.indexOf(x) === i);
621
- const entry = {
622
- branch: "main",
623
- name: t.name,
624
- data_source: { type: "data_view", id: viewMap[t.name] },
625
- primary_keys: [pk],
626
- display_key: dk,
627
- data_properties: t.columns.map((c) => ({
628
- name: c.name,
629
- display_name: c.name,
630
- type: "string",
631
- mapped_field: { name: c.name, type: c.type || "varchar" },
632
- })),
633
- };
634
- const otBody = JSON.stringify({ entries: [entry] });
650
+ let statusStr = "skipped";
651
+ try {
652
+ // Phase 3: Single batched POST. Backend wraps all entries in one tx
653
+ // (object_type_service.go:213-355) all-or-nothing.
654
+ console.error(`Creating ${targetTables.length} object type(s) ...`);
655
+ const entries = targetTables.map((t) => {
656
+ const pk = detectPrimaryKey(t, sampleRows?.[t.name]);
657
+ const dk = detectDisplayKey(t, pk);
658
+ return {
659
+ branch: "main",
660
+ name: t.name,
661
+ data_source: { type: "data_view", id: viewMap[t.name] },
662
+ primary_keys: [pk],
663
+ display_key: dk,
664
+ data_properties: t.columns.map((c) => ({
665
+ name: c.name,
666
+ display_name: c.name,
667
+ type: "string",
668
+ mapped_field: { name: c.name, type: c.type || "varchar" },
669
+ })),
670
+ _meta: { pk, dk },
671
+ };
672
+ });
673
+ const wireEntries = entries.map(({ _meta: _, ...rest }) => rest);
674
+ const otBody = JSON.stringify({ entries: wireEntries });
635
675
  const otResponse = await createObjectTypes({
636
676
  ...base,
637
677
  knId,
638
678
  body: otBody,
639
679
  });
640
680
  const otParsed = JSON.parse(otResponse);
641
- const otItem = otParsed.entries?.[0];
642
- otResults.push({
643
- name: t.name,
644
- id: otItem?.id ?? "",
645
- field_count: t.columns.length,
646
- });
647
- console.error(` Created: ${t.name} (${t.columns.length} fields, pk=${pk}, dk=${dk})`);
648
- }
649
- if (otResults.length === 0) {
650
- const errorOutput = {
651
- kn_id: knId,
652
- kn_name: options.name,
653
- error: "No object types were created",
654
- };
655
- console.log(JSON.stringify(errorOutput, null, options.pretty ? 2 : 0));
656
- return 1;
657
- }
658
- let statusStr = "skipped";
659
- if (options.build) {
660
- console.error("Building ...");
661
- await buildKnowledgeNetwork({ ...base, knId });
662
- const TERMINAL = ["completed", "failed", "success"];
663
- try {
664
- statusStr = await pollWithBackoff({
665
- fn: async () => {
666
- const statusBody = await getBuildStatus({ ...base, knId });
667
- const statusParsed = JSON.parse(statusBody);
668
- const jobs = Array.isArray(statusParsed) ? statusParsed : (statusParsed.entries ?? []);
669
- const state = (jobs[0]?.state ?? "running").toLowerCase();
670
- if (TERMINAL.includes(state))
671
- return { done: true, value: state };
672
- return { done: false, value: "running" };
673
- },
674
- interval: 2000,
675
- timeout: options.timeout * 1000,
681
+ const otItems = Array.isArray(otParsed) ? otParsed : (otParsed.entries ?? []);
682
+ for (let i = 0; i < entries.length; i += 1) {
683
+ const t = targetTables[i];
684
+ const meta = entries[i]._meta;
685
+ otResults.push({
686
+ name: t.name,
687
+ id: otItems[i]?.id ?? "",
688
+ field_count: t.columns.length,
676
689
  });
690
+ console.error(` Created: ${t.name} (${t.columns.length} fields, pk=${meta.pk}, dk=${meta.dk})`);
677
691
  }
678
- catch {
679
- // timeout — statusStr remains "skipped"
692
+ if (options.build) {
693
+ console.error("Building ...");
694
+ await buildKnowledgeNetwork({ ...base, knId });
695
+ const TERMINAL = ["completed", "failed", "success"];
696
+ try {
697
+ statusStr = await pollWithBackoff({
698
+ fn: async () => {
699
+ const statusBody = await getBuildStatus({ ...base, knId });
700
+ const statusParsed = JSON.parse(statusBody);
701
+ const jobs = Array.isArray(statusParsed) ? statusParsed : (statusParsed.entries ?? []);
702
+ const state = (jobs[0]?.state ?? "running").toLowerCase();
703
+ if (TERMINAL.includes(state))
704
+ return { done: true, value: state };
705
+ return { done: false, value: "running" };
706
+ },
707
+ interval: 2000,
708
+ timeout: options.timeout * 1000,
709
+ });
710
+ }
711
+ catch {
712
+ // build timeout — KN itself is fine, just mark skipped
713
+ }
714
+ }
715
+ // Reached the end without throwing — clear the rollback handle.
716
+ createdKnId = undefined;
717
+ }
718
+ finally {
719
+ if (createdKnId !== undefined) {
720
+ if (options.noRollback) {
721
+ console.error(`Leaving partial KN ${createdKnId} in place (--no-rollback)`);
722
+ }
723
+ else {
724
+ console.error(`Rolling back KN ${createdKnId} ...`);
725
+ try {
726
+ await deleteKnowledgeNetwork({ ...base, knId: createdKnId });
727
+ console.error(`Rolled back KN ${createdKnId}`);
728
+ }
729
+ catch (rollbackErr) {
730
+ console.error(`Rollback failed for KN ${createdKnId}: ${formatHttpError(rollbackErr)}`);
731
+ }
732
+ }
680
733
  }
681
734
  }
682
735
  const output = {
@@ -708,6 +761,7 @@ Options:
708
761
  --no-build Skip build
709
762
  --recreate Use "insert" mode on first batch (only effective for new tables)
710
763
  --timeout <n> Build timeout in seconds (default: 300)
764
+ --no-rollback Keep partially-created KN on failure (debug; default: rollback)
711
765
  -bd, --biz-domain Business domain (default: bd_public)`;
712
766
  export function parseKnCreateFromCsvArgs(args) {
713
767
  let dsId = "";
@@ -720,6 +774,7 @@ export function parseKnCreateFromCsvArgs(args) {
720
774
  let recreate = false;
721
775
  let timeout = 300;
722
776
  let businessDomain = "";
777
+ let noRollback = false;
723
778
  for (let i = 0; i < args.length; i += 1) {
724
779
  const arg = args[i];
725
780
  if (arg === "--help" || arg === "-h")
@@ -758,6 +813,10 @@ export function parseKnCreateFromCsvArgs(args) {
758
813
  recreate = true;
759
814
  continue;
760
815
  }
816
+ if (arg === "--no-rollback") {
817
+ noRollback = true;
818
+ continue;
819
+ }
761
820
  if (arg === "--timeout" && args[i + 1]) {
762
821
  timeout = parseInt(args[++i], 10);
763
822
  if (Number.isNaN(timeout) || timeout < 1)
@@ -778,7 +837,7 @@ export function parseKnCreateFromCsvArgs(args) {
778
837
  }
779
838
  if (!businessDomain)
780
839
  businessDomain = resolveBusinessDomain();
781
- return { dsId, files, name, tablePrefix, batchSize, tables, build, recreate, timeout, businessDomain };
840
+ return { dsId, files, name, tablePrefix, batchSize, tables, build, recreate, timeout, businessDomain, noRollback };
782
841
  }
783
842
  export async function runKnCreateFromCsvCommand(args) {
784
843
  let options;
@@ -793,6 +852,20 @@ export async function runKnCreateFromCsvCommand(args) {
793
852
  console.error(formatHttpError(error));
794
853
  return 1;
795
854
  }
855
+ // Pre-flight: predict OT names from (table-prefix + csv basename) and
856
+ // reject before any CSV is imported. CSV import is expensive; failing
857
+ // here saves the user a multi-minute round trip.
858
+ try {
859
+ const filePaths = await resolveFiles(options.files);
860
+ const predictedNames = options.tables.length > 0
861
+ ? options.tables
862
+ : filePaths.map((p) => buildTableName(p, options.tablePrefix));
863
+ assertValidBknObjectNames(predictedNames, "Object type names derived from CSV file names");
864
+ }
865
+ catch (error) {
866
+ console.error(formatHttpError(error));
867
+ return 1;
868
+ }
796
869
  // Phase 1: Import CSVs
797
870
  console.error("Phase 1: Importing CSVs ...");
798
871
  const importArgs = [
@@ -844,6 +917,7 @@ export async function runKnCreateFromCsvCommand(args) {
844
917
  options.build ? "--build" : "--no-build",
845
918
  "--timeout", String(options.timeout),
846
919
  "-bd", options.businessDomain,
920
+ ...(options.noRollback ? ["--no-rollback"] : []),
847
921
  ];
848
922
  return runKnCreateFromDsCommand(knArgs, importResult.sampleRows);
849
923
  }
@@ -31,13 +31,18 @@ Query subgraph via ontology-query API. JSON body format see references/json-form
31
31
  return 1;
32
32
  }
33
33
  try {
34
- // Auto-detect query_type=relation_path when body contains source_object_type_id
34
+ // Map body shape to ontology-query subgraph query_type:
35
+ // - relation_type_paths mode → ?query_type=relation_path
36
+ // - source_object_type_id mode → omit query_type (default path; do not send relation_path)
35
37
  let queryType;
36
38
  try {
37
39
  const parsedBody = JSON.parse(body);
38
- if (parsedBody.source_object_type_id) {
40
+ if (Array.isArray(parsedBody.relation_type_paths)) {
39
41
  queryType = "relation_path";
40
42
  }
43
+ else if (parsedBody.source_object_type_id) {
44
+ queryType = "";
45
+ }
41
46
  }
42
47
  catch {
43
48
  // Not valid JSON — let the API return the error
@@ -2,6 +2,7 @@ import { listBusinessDomains } from "../api/business-domains.js";
2
2
  import { fetchEacpUserInfo, resolveActivePlatform, withTokenRetry } from "../auth/oauth.js";
3
3
  import { HttpError } from "../utils/http.js";
4
4
  import { loadPlatformBusinessDomain, resolveBusinessDomain, savePlatformBusinessDomain, } from "../config/store.js";
5
+ import { assertNotStatelessForWrite } from "../config/stateless.js";
5
6
  const HELP = `kweaver config
6
7
 
7
8
  Subcommands:
@@ -50,6 +51,13 @@ export async function runConfigCommand(args) {
50
51
  return 1;
51
52
  }
52
53
  const platform = active.url;
54
+ try {
55
+ assertNotStatelessForWrite("config set-bd");
56
+ }
57
+ catch (err) {
58
+ console.error(err instanceof Error ? err.message : String(err));
59
+ return 1;
60
+ }
53
61
  savePlatformBusinessDomain(platform, value);
54
62
  const provenance = active.source === "env" ? `${platform} via KWEAVER_BASE_URL` : platform;
55
63
  console.log(`Business domain set to: ${value} (${provenance})`);