@kaddo/cli 3.82.0 → 3.83.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/admin-dist/assets/{index-DIfpc20n.js → index-CsOMxNTw.js} +15 -15
- package/dist/admin-dist/index.html +1 -1
- package/dist/admin-server/index.js +136 -0
- package/dist/core.js +322 -10
- package/dist/index.js +143 -10
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>admin</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CsOMxNTw.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-DNkI-1xG.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -71,6 +71,16 @@ import {
|
|
|
71
71
|
getSystemMapProjection as coreGetSystemMapProjection,
|
|
72
72
|
buildTopologyEnrichmentHandoff as coreBuildTopologyHandoff,
|
|
73
73
|
listIntegrations as coreListIntegrations,
|
|
74
|
+
getIntegration as coreGetIntegration,
|
|
75
|
+
getIntegrationSecretStatus as coreGetIntegrationSecretStatus,
|
|
76
|
+
getAvailableIntegrationTypes as coreGetAvailableIntegrationTypes,
|
|
77
|
+
createIntegration as coreCreateIntegration,
|
|
78
|
+
updateIntegration as coreUpdateIntegration,
|
|
79
|
+
deleteIntegration as coreDeleteIntegration,
|
|
80
|
+
enableIntegration as coreEnableIntegration,
|
|
81
|
+
disableIntegration as coreDisableIntegration,
|
|
82
|
+
setIntegrationSecret as coreSetIntegrationSecret,
|
|
83
|
+
removeIntegrationSecret as coreRemoveIntegrationSecret,
|
|
74
84
|
verifyIntegration as coreVerifyIntegration,
|
|
75
85
|
listExternalWorkItems as coreListExternalWorkItems,
|
|
76
86
|
getExternalWorkItem as coreGetExternalWorkItem,
|
|
@@ -395,6 +405,72 @@ async function importIntegrationWorkItem(dir, id, externalId, opts) {
|
|
|
395
405
|
mapIntegrationError(err);
|
|
396
406
|
}
|
|
397
407
|
}
|
|
408
|
+
function getIntegrationDetail(dir, id) {
|
|
409
|
+
try {
|
|
410
|
+
return coreGetIntegration(dir, id);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
mapIntegrationError(err);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function getIntegrationSecretStatusAdmin(dir, id) {
|
|
416
|
+
try {
|
|
417
|
+
return await coreGetIntegrationSecretStatus(dir, id);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
mapIntegrationError(err);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function getAvailableIntegrationTypesAdmin() {
|
|
423
|
+
return coreGetAvailableIntegrationTypes();
|
|
424
|
+
}
|
|
425
|
+
function createIntegrationAdmin(dir, body) {
|
|
426
|
+
try {
|
|
427
|
+
return coreCreateIntegration(dir, body);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
mapIntegrationError(err);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function updateIntegrationAdmin(dir, id, body) {
|
|
433
|
+
try {
|
|
434
|
+
return coreUpdateIntegration(dir, id, body);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
mapIntegrationError(err);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function deleteIntegrationAdmin(dir, id) {
|
|
440
|
+
try {
|
|
441
|
+
coreDeleteIntegration(dir, id);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
mapIntegrationError(err);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function enableIntegrationAdmin(dir, id) {
|
|
447
|
+
try {
|
|
448
|
+
return coreEnableIntegration(dir, id);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
mapIntegrationError(err);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function disableIntegrationAdmin(dir, id) {
|
|
454
|
+
try {
|
|
455
|
+
return coreDisableIntegration(dir, id);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
mapIntegrationError(err);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function setIntegrationSecretAdmin(dir, id, secretName, value) {
|
|
461
|
+
try {
|
|
462
|
+
await coreSetIntegrationSecret(dir, id, secretName, value);
|
|
463
|
+
} catch (err) {
|
|
464
|
+
mapIntegrationError(err);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function removeIntegrationSecretAdmin(dir, id, secretName) {
|
|
468
|
+
try {
|
|
469
|
+
await coreRemoveIntegrationSecret(dir, id, secretName);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
mapIntegrationError(err);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
398
474
|
|
|
399
475
|
// src/contracts/schemas.ts
|
|
400
476
|
import { z } from "zod";
|
|
@@ -896,10 +972,70 @@ async function createAdminServer(opts) {
|
|
|
896
972
|
}
|
|
897
973
|
};
|
|
898
974
|
app.get("/api/v1/admin/integrations", coreRoute(getIntegrations));
|
|
975
|
+
app.get("/api/v1/admin/integrations/types", coreRoute(() => getAvailableIntegrationTypesAdmin()));
|
|
976
|
+
app.get("/api/v1/admin/integrations/:id", async (request, reply) => {
|
|
977
|
+
try {
|
|
978
|
+
return getIntegrationDetail(projectDir, request.params.id);
|
|
979
|
+
} catch (err) {
|
|
980
|
+
if (err instanceof CoreError) return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
981
|
+
throw err;
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
app.get(
|
|
985
|
+
"/api/v1/admin/integrations/:id/secrets",
|
|
986
|
+
async (request, reply) => asyncCore(reply, () => getIntegrationSecretStatusAdmin(projectDir, request.params.id))
|
|
987
|
+
);
|
|
899
988
|
app.get(
|
|
900
989
|
"/api/v1/admin/integrations/:id/status",
|
|
901
990
|
async (request, reply) => asyncCore(reply, () => getIntegrationStatus(projectDir, request.params.id))
|
|
902
991
|
);
|
|
992
|
+
app.post(
|
|
993
|
+
"/api/v1/admin/integrations",
|
|
994
|
+
async (request, reply) => {
|
|
995
|
+
const { id, adapter, enabled, config, secrets } = request.body ?? {};
|
|
996
|
+
if (!id || !adapter) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "id and adapter are required." } });
|
|
997
|
+
return writeHandler(reply, () => createIntegrationAdmin(projectDir, { id, adapter, enabled, config, secrets }));
|
|
998
|
+
}
|
|
999
|
+
);
|
|
1000
|
+
app.put(
|
|
1001
|
+
"/api/v1/admin/integrations/:id",
|
|
1002
|
+
async (request, reply) => writeHandler(reply, () => updateIntegrationAdmin(projectDir, request.params.id, request.body ?? {}))
|
|
1003
|
+
);
|
|
1004
|
+
app.delete("/api/v1/admin/integrations/:id", async (request, reply) => {
|
|
1005
|
+
try {
|
|
1006
|
+
deleteIntegrationAdmin(projectDir, request.params.id);
|
|
1007
|
+
return { ok: true };
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
if (err instanceof CoreError) return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
1010
|
+
throw err;
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
app.post(
|
|
1014
|
+
"/api/v1/admin/integrations/:id/enable",
|
|
1015
|
+
async (request, reply) => writeHandler(reply, () => enableIntegrationAdmin(projectDir, request.params.id))
|
|
1016
|
+
);
|
|
1017
|
+
app.post(
|
|
1018
|
+
"/api/v1/admin/integrations/:id/disable",
|
|
1019
|
+
async (request, reply) => writeHandler(reply, () => disableIntegrationAdmin(projectDir, request.params.id))
|
|
1020
|
+
);
|
|
1021
|
+
app.post(
|
|
1022
|
+
"/api/v1/admin/integrations/:id/secrets/:name",
|
|
1023
|
+
async (request, reply) => {
|
|
1024
|
+
const { value } = request.body ?? {};
|
|
1025
|
+
if (!value || typeof value !== "string") return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "A secret value is required." } });
|
|
1026
|
+
return asyncCore(reply, async () => {
|
|
1027
|
+
await setIntegrationSecretAdmin(projectDir, request.params.id, request.params.name, value);
|
|
1028
|
+
return { ok: true };
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
1032
|
+
app.delete(
|
|
1033
|
+
"/api/v1/admin/integrations/:id/secrets/:name",
|
|
1034
|
+
async (request, reply) => asyncCore(reply, async () => {
|
|
1035
|
+
await removeIntegrationSecretAdmin(projectDir, request.params.id, request.params.name);
|
|
1036
|
+
return { ok: true };
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
903
1039
|
app.get(
|
|
904
1040
|
"/api/v1/admin/integrations/:id/work-items",
|
|
905
1041
|
async (request, reply) => asyncCore(reply, () => getExternalWorkItems(projectDir, request.params.id, {
|
package/dist/core.js
CHANGED
|
@@ -7,6 +7,10 @@ function exists(p2) {
|
|
|
7
7
|
function readFile(p2) {
|
|
8
8
|
return fs.readFileSync(p2, "utf-8");
|
|
9
9
|
}
|
|
10
|
+
function writeFile(filePath, content) {
|
|
11
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
12
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
13
|
+
}
|
|
10
14
|
function readDir(dirPath) {
|
|
11
15
|
if (!exists(dirPath)) return [];
|
|
12
16
|
return fs.readdirSync(dirPath);
|
|
@@ -9371,7 +9375,7 @@ function buildRefinementHandoff(dir, workItemId) {
|
|
|
9371
9375
|
|
|
9372
9376
|
// src/services/integrations.ts
|
|
9373
9377
|
import matter8 from "gray-matter";
|
|
9374
|
-
import { parse as parseYaml9 } from "yaml";
|
|
9378
|
+
import { parse as parseYaml9, stringify as stringifyYaml5 } from "yaml";
|
|
9375
9379
|
|
|
9376
9380
|
// ../integrations/src/errors.ts
|
|
9377
9381
|
var RETRYABLE = /* @__PURE__ */ new Set([
|
|
@@ -9456,6 +9460,26 @@ function parseCredentials(raw, id, findings) {
|
|
|
9456
9460
|
}
|
|
9457
9461
|
return creds;
|
|
9458
9462
|
}
|
|
9463
|
+
function parseSecrets(raw, id, findings) {
|
|
9464
|
+
const secrets = {};
|
|
9465
|
+
if (raw == null) return secrets;
|
|
9466
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
9467
|
+
findings.push({ level: "blocking", id, message: `Integration "${id}": secrets must be a mapping of logical references.` });
|
|
9468
|
+
return secrets;
|
|
9469
|
+
}
|
|
9470
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
9471
|
+
if (typeof value === "string" && value.trim()) {
|
|
9472
|
+
if (value.length > 100 || /^(ghp_|sk-|xox[bpsa]-|glpat-|ey[A-Za-z0-9])/i.test(value)) {
|
|
9473
|
+
findings.push({ level: "blocking", id, message: `Integration "${id}": secret "${key}" appears to contain an actual credential, not a reference name.` });
|
|
9474
|
+
continue;
|
|
9475
|
+
}
|
|
9476
|
+
secrets[key] = value.trim();
|
|
9477
|
+
} else {
|
|
9478
|
+
findings.push({ level: "warning", id, message: `Integration "${id}": ignoring non-string secret entry "${key}".` });
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
return secrets;
|
|
9482
|
+
}
|
|
9459
9483
|
function parseIntegrationsConfig(raw, opts) {
|
|
9460
9484
|
const findings = [];
|
|
9461
9485
|
const integrations = [];
|
|
@@ -9488,12 +9512,13 @@ function parseIntegrationsConfig(raw, opts) {
|
|
|
9488
9512
|
const enabled = o.enabled === void 0 ? true : o.enabled === true || o.enabled === "true";
|
|
9489
9513
|
const config = o.config && typeof o.config === "object" && !Array.isArray(o.config) ? o.config : {};
|
|
9490
9514
|
const credentials = parseCredentials(o.credentials, id, findings);
|
|
9515
|
+
const secrets = parseSecrets(o.secrets, id, findings);
|
|
9491
9516
|
const timeoutMs = typeof o.timeout_ms === "number" ? o.timeout_ms : typeof o.timeoutMs === "number" ? o.timeoutMs : void 0;
|
|
9492
|
-
integrations.push({ id, adapter, enabled, config, credentials, timeoutMs });
|
|
9517
|
+
integrations.push({ id, adapter, enabled, config, credentials, secrets, timeoutMs });
|
|
9493
9518
|
}
|
|
9494
9519
|
return { integrations, findings };
|
|
9495
9520
|
}
|
|
9496
|
-
function
|
|
9521
|
+
async function resolveAllCredentials(integration, resolver, env) {
|
|
9497
9522
|
const credentials = {};
|
|
9498
9523
|
const missing = [];
|
|
9499
9524
|
for (const [name, ref] of Object.entries(integration.credentials)) {
|
|
@@ -9501,8 +9526,47 @@ function resolveCredentials(integration, env) {
|
|
|
9501
9526
|
if (value && value.length > 0) credentials[name] = value;
|
|
9502
9527
|
else missing.push(ref.env);
|
|
9503
9528
|
}
|
|
9529
|
+
for (const [name, ref] of Object.entries(integration.secrets)) {
|
|
9530
|
+
if (credentials[name]) continue;
|
|
9531
|
+
const value = await resolver.resolve(ref);
|
|
9532
|
+
if (value !== void 0) credentials[name] = value;
|
|
9533
|
+
else missing.push(ref);
|
|
9534
|
+
}
|
|
9504
9535
|
return { credentials, missing };
|
|
9505
9536
|
}
|
|
9537
|
+
function serializeIntegrationsConfig(integrations) {
|
|
9538
|
+
return {
|
|
9539
|
+
integrations: integrations.map((i) => {
|
|
9540
|
+
const entry = {
|
|
9541
|
+
id: i.id,
|
|
9542
|
+
adapter: i.adapter,
|
|
9543
|
+
enabled: i.enabled
|
|
9544
|
+
};
|
|
9545
|
+
if (Object.keys(i.config).length > 0) entry.config = i.config;
|
|
9546
|
+
if (Object.keys(i.credentials).length > 0) {
|
|
9547
|
+
const creds = {};
|
|
9548
|
+
for (const [name, ref] of Object.entries(i.credentials)) {
|
|
9549
|
+
creds[`${name}${ENV_SUFFIX}`] = ref.env;
|
|
9550
|
+
}
|
|
9551
|
+
entry.credentials = creds;
|
|
9552
|
+
}
|
|
9553
|
+
if (Object.keys(i.secrets).length > 0) entry.secrets = i.secrets;
|
|
9554
|
+
if (i.timeoutMs !== void 0) entry.timeout_ms = i.timeoutMs;
|
|
9555
|
+
return entry;
|
|
9556
|
+
})
|
|
9557
|
+
};
|
|
9558
|
+
}
|
|
9559
|
+
function integrationConfigFromInput(input) {
|
|
9560
|
+
return {
|
|
9561
|
+
id: input.id,
|
|
9562
|
+
adapter: input.adapter,
|
|
9563
|
+
enabled: input.enabled ?? true,
|
|
9564
|
+
config: input.config ?? {},
|
|
9565
|
+
credentials: {},
|
|
9566
|
+
secrets: input.secrets ?? {},
|
|
9567
|
+
timeoutMs: input.timeoutMs
|
|
9568
|
+
};
|
|
9569
|
+
}
|
|
9506
9570
|
|
|
9507
9571
|
// ../integrations/src/preview.ts
|
|
9508
9572
|
function buildImportPreview(item, opts) {
|
|
@@ -9525,6 +9589,85 @@ function buildImportPreview(item, opts) {
|
|
|
9525
9589
|
};
|
|
9526
9590
|
}
|
|
9527
9591
|
|
|
9592
|
+
// ../integrations/src/secrets.ts
|
|
9593
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, unlinkSync } from "fs";
|
|
9594
|
+
import { dirname as dirname2 } from "path";
|
|
9595
|
+
function secretRefKey(integrationId, secretName) {
|
|
9596
|
+
return `${integrationId}.${secretName}`;
|
|
9597
|
+
}
|
|
9598
|
+
var SECRETS_FILENAME = ".secrets.json";
|
|
9599
|
+
function secretsPath(projectDir) {
|
|
9600
|
+
return `${projectDir}/.kaddo/${SECRETS_FILENAME}`;
|
|
9601
|
+
}
|
|
9602
|
+
function loadStore(filePath) {
|
|
9603
|
+
if (!existsSync2(filePath)) return {};
|
|
9604
|
+
try {
|
|
9605
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
9606
|
+
const parsed = JSON.parse(raw);
|
|
9607
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
9608
|
+
return {};
|
|
9609
|
+
} catch {
|
|
9610
|
+
return {};
|
|
9611
|
+
}
|
|
9612
|
+
}
|
|
9613
|
+
function saveStore(filePath, store) {
|
|
9614
|
+
mkdirSync(dirname2(filePath), { recursive: true });
|
|
9615
|
+
writeFileSync(filePath, JSON.stringify(store, null, 2) + "\n", "utf-8");
|
|
9616
|
+
}
|
|
9617
|
+
function createLocalSecretProvider(projectDir) {
|
|
9618
|
+
const fp = secretsPath(projectDir);
|
|
9619
|
+
return {
|
|
9620
|
+
async get(key) {
|
|
9621
|
+
const store = loadStore(fp);
|
|
9622
|
+
const val = store[key];
|
|
9623
|
+
return val !== void 0 && val !== "" ? val : void 0;
|
|
9624
|
+
},
|
|
9625
|
+
async set(key, value) {
|
|
9626
|
+
const store = loadStore(fp);
|
|
9627
|
+
store[key] = value;
|
|
9628
|
+
saveStore(fp, store);
|
|
9629
|
+
},
|
|
9630
|
+
async delete(key) {
|
|
9631
|
+
const store = loadStore(fp);
|
|
9632
|
+
delete store[key];
|
|
9633
|
+
saveStore(fp, store);
|
|
9634
|
+
},
|
|
9635
|
+
async exists(key) {
|
|
9636
|
+
const store = loadStore(fp);
|
|
9637
|
+
return key in store && store[key] !== void 0 && store[key] !== "";
|
|
9638
|
+
}
|
|
9639
|
+
};
|
|
9640
|
+
}
|
|
9641
|
+
function createEnvSecretProvider(env = process.env) {
|
|
9642
|
+
return {
|
|
9643
|
+
async get(key) {
|
|
9644
|
+
const val = env[key];
|
|
9645
|
+
return val !== void 0 && val !== "" ? val : void 0;
|
|
9646
|
+
},
|
|
9647
|
+
async set() {
|
|
9648
|
+
throw new Error("Environment secret provider is read-only.");
|
|
9649
|
+
},
|
|
9650
|
+
async delete() {
|
|
9651
|
+
throw new Error("Environment secret provider is read-only.");
|
|
9652
|
+
},
|
|
9653
|
+
async exists(key) {
|
|
9654
|
+
const val = env[key];
|
|
9655
|
+
return val !== void 0 && val !== "";
|
|
9656
|
+
}
|
|
9657
|
+
};
|
|
9658
|
+
}
|
|
9659
|
+
function createCompositeResolver(...providers) {
|
|
9660
|
+
return {
|
|
9661
|
+
async resolve(reference) {
|
|
9662
|
+
for (const provider of providers) {
|
|
9663
|
+
const val = await provider.get(reference);
|
|
9664
|
+
if (val !== void 0) return val;
|
|
9665
|
+
}
|
|
9666
|
+
return void 0;
|
|
9667
|
+
}
|
|
9668
|
+
};
|
|
9669
|
+
}
|
|
9670
|
+
|
|
9528
9671
|
// ../integrations/src/registry.ts
|
|
9529
9672
|
var DuplicateAdapterError = class extends Error {
|
|
9530
9673
|
constructor(id) {
|
|
@@ -9609,7 +9752,31 @@ function createMockAdapter(opts = {}) {
|
|
|
9609
9752
|
id: MOCK_ADAPTER_ID,
|
|
9610
9753
|
displayName: "Mock Work Source",
|
|
9611
9754
|
version: "1.0.0",
|
|
9612
|
-
description: "Deterministic offline reference adapter for validating the integration foundation."
|
|
9755
|
+
description: "Deterministic offline reference adapter for validating the integration foundation.",
|
|
9756
|
+
configSchema: {
|
|
9757
|
+
simulate: {
|
|
9758
|
+
type: "select",
|
|
9759
|
+
required: false,
|
|
9760
|
+
label: "Simulation mode",
|
|
9761
|
+
description: "Controls what the mock adapter simulates during verify/read operations.",
|
|
9762
|
+
options: [
|
|
9763
|
+
{ value: "available", label: "Available" },
|
|
9764
|
+
{ value: "unauthorized", label: "Unauthorized" },
|
|
9765
|
+
{ value: "rate-limited", label: "Rate Limited" },
|
|
9766
|
+
{ value: "unavailable", label: "Unavailable" },
|
|
9767
|
+
{ value: "timeout", label: "Timeout" }
|
|
9768
|
+
],
|
|
9769
|
+
defaultValue: "available"
|
|
9770
|
+
}
|
|
9771
|
+
},
|
|
9772
|
+
secretSchema: {
|
|
9773
|
+
token: {
|
|
9774
|
+
type: "string",
|
|
9775
|
+
required: false,
|
|
9776
|
+
label: "API Token",
|
|
9777
|
+
description: "Optional token for testing secret handling (not used by the mock adapter)."
|
|
9778
|
+
}
|
|
9779
|
+
}
|
|
9613
9780
|
},
|
|
9614
9781
|
capabilities: {
|
|
9615
9782
|
workItems: { list: true, read: true, import: true, write: false, statusSync: false, comments: false, webhooks: false }
|
|
@@ -9683,12 +9850,23 @@ function loadRaw(dir) {
|
|
|
9683
9850
|
function loadIntegrations(dir) {
|
|
9684
9851
|
return parseIntegrationsConfig(loadRaw(dir), { adapterIds: registry.ids() });
|
|
9685
9852
|
}
|
|
9853
|
+
function saveIntegrations(dir, integrations) {
|
|
9854
|
+
const abs = join(dir, INTEGRATIONS_FILE);
|
|
9855
|
+
const data = serializeIntegrationsConfig(integrations);
|
|
9856
|
+
writeFile(abs, stringifyYaml5(data, { lineWidth: 120 }));
|
|
9857
|
+
}
|
|
9686
9858
|
function configStatus(integration, findings) {
|
|
9687
9859
|
if (!integration.enabled) return "disabled";
|
|
9688
9860
|
const blocking = findings.some((f) => f.id === integration.id && f.level === "blocking");
|
|
9689
9861
|
if (blocking || !registry.has(integration.adapter)) return "invalid-config";
|
|
9690
9862
|
return "configured";
|
|
9691
9863
|
}
|
|
9864
|
+
function secretProvider(dir) {
|
|
9865
|
+
return createLocalSecretProvider(dir);
|
|
9866
|
+
}
|
|
9867
|
+
function secretResolver(dir, env) {
|
|
9868
|
+
return createCompositeResolver(createLocalSecretProvider(dir), createEnvSecretProvider(env));
|
|
9869
|
+
}
|
|
9692
9870
|
function listIntegrations(dir) {
|
|
9693
9871
|
const { integrations, findings } = loadIntegrations(dir);
|
|
9694
9872
|
return integrations.map((integration) => {
|
|
@@ -9702,23 +9880,70 @@ function listIntegrations(dir) {
|
|
|
9702
9880
|
capabilities: adapter?.capabilities ?? null,
|
|
9703
9881
|
metadata: adapter?.metadata ?? null,
|
|
9704
9882
|
credentialRefs: Object.values(integration.credentials).map((c) => c.env),
|
|
9883
|
+
secretRefs: Object.values(integration.secrets),
|
|
9884
|
+
secretStatus: {},
|
|
9705
9885
|
findings: findings.filter((f) => f.id === integration.id)
|
|
9706
9886
|
};
|
|
9707
9887
|
});
|
|
9708
9888
|
}
|
|
9889
|
+
function getIntegration(dir, id) {
|
|
9890
|
+
const { integrations, findings } = loadIntegrations(dir);
|
|
9891
|
+
const integration = integrations.find((i) => i.id === id);
|
|
9892
|
+
if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured in this project.`);
|
|
9893
|
+
const adapter = registry.get(integration.adapter);
|
|
9894
|
+
return {
|
|
9895
|
+
id: integration.id,
|
|
9896
|
+
adapter: integration.adapter,
|
|
9897
|
+
enabled: integration.enabled,
|
|
9898
|
+
status: configStatus(integration, findings),
|
|
9899
|
+
displayName: adapter?.metadata.displayName ?? integration.adapter,
|
|
9900
|
+
capabilities: adapter?.capabilities ?? null,
|
|
9901
|
+
metadata: adapter?.metadata ?? null,
|
|
9902
|
+
credentialRefs: Object.values(integration.credentials).map((c) => c.env),
|
|
9903
|
+
secretRefs: Object.values(integration.secrets),
|
|
9904
|
+
secretStatus: {},
|
|
9905
|
+
findings: findings.filter((f) => f.id === integration.id)
|
|
9906
|
+
};
|
|
9907
|
+
}
|
|
9908
|
+
async function getIntegrationSecretStatus(dir, id) {
|
|
9909
|
+
const { integrations } = loadIntegrations(dir);
|
|
9910
|
+
const integration = integrations.find((i) => i.id === id);
|
|
9911
|
+
if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured.`);
|
|
9912
|
+
const sp = secretProvider(dir);
|
|
9913
|
+
const status = {};
|
|
9914
|
+
for (const [name, ref] of Object.entries(integration.secrets)) {
|
|
9915
|
+
status[name] = await sp.exists(ref);
|
|
9916
|
+
}
|
|
9917
|
+
for (const [name, ref] of Object.entries(integration.credentials)) {
|
|
9918
|
+
const val = process.env[ref.env];
|
|
9919
|
+
status[name] = val !== void 0 && val !== "";
|
|
9920
|
+
}
|
|
9921
|
+
return status;
|
|
9922
|
+
}
|
|
9923
|
+
function getAvailableIntegrationTypes() {
|
|
9924
|
+
return registry.list().map((a) => ({
|
|
9925
|
+
id: a.id,
|
|
9926
|
+
displayName: a.metadata.displayName,
|
|
9927
|
+
description: a.metadata.description,
|
|
9928
|
+
configSchema: a.metadata.configSchema ?? {},
|
|
9929
|
+
secretSchema: a.metadata.secretSchema ?? {},
|
|
9930
|
+
capabilities: a.capabilities
|
|
9931
|
+
}));
|
|
9932
|
+
}
|
|
9709
9933
|
function requireIntegration(dir, id) {
|
|
9710
9934
|
const { integrations, findings } = loadIntegrations(dir);
|
|
9711
9935
|
const integration = integrations.find((i) => i.id === id);
|
|
9712
9936
|
if (!integration) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured in this project.`);
|
|
9713
|
-
return { integration, findings };
|
|
9937
|
+
return { integration, findings, all: integrations };
|
|
9714
9938
|
}
|
|
9715
9939
|
function resolveAdapter(integration) {
|
|
9716
9940
|
const adapter = registry.get(integration.adapter);
|
|
9717
9941
|
if (!adapter) throw new IntegrationServiceError("ADAPTER_NOT_FOUND", `Unknown integration adapter "${integration.adapter}".`);
|
|
9718
9942
|
return adapter;
|
|
9719
9943
|
}
|
|
9720
|
-
function
|
|
9721
|
-
const
|
|
9944
|
+
async function buildContextWithSecrets(dir, integration, env) {
|
|
9945
|
+
const resolver = secretResolver(dir, env);
|
|
9946
|
+
const { credentials, missing } = await resolveAllCredentials(integration, resolver, env);
|
|
9722
9947
|
return {
|
|
9723
9948
|
context: { integrationId: integration.id, config: integration.config, credentials, timeoutMs: integration.timeoutMs ?? DEFAULT_TIMEOUT_MS },
|
|
9724
9949
|
missing
|
|
@@ -9735,13 +9960,90 @@ async function withTimeout(op, ms) {
|
|
|
9735
9960
|
if (timer) clearTimeout(timer);
|
|
9736
9961
|
}
|
|
9737
9962
|
}
|
|
9963
|
+
function validateId(id) {
|
|
9964
|
+
if (!id || !id.trim()) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "Integration id is required.");
|
|
9965
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(id)) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "Integration id must be alphanumeric with dashes, dots or underscores.");
|
|
9966
|
+
if (id.length > 64) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "Integration id must be 64 characters or fewer.");
|
|
9967
|
+
}
|
|
9968
|
+
function createIntegration(dir, input) {
|
|
9969
|
+
validateId(input.id);
|
|
9970
|
+
if (!input.adapter || !input.adapter.trim()) throw new IntegrationServiceError("INTEGRATION_INVALID_INPUT", "An adapter type is required.");
|
|
9971
|
+
const { integrations } = loadIntegrations(dir);
|
|
9972
|
+
if (integrations.find((i) => i.id === input.id)) {
|
|
9973
|
+
throw new IntegrationServiceError("INTEGRATION_ALREADY_EXISTS", `An integration with id "${input.id}" already exists.`);
|
|
9974
|
+
}
|
|
9975
|
+
const newConfig = integrationConfigFromInput(input);
|
|
9976
|
+
integrations.push(newConfig);
|
|
9977
|
+
saveIntegrations(dir, integrations);
|
|
9978
|
+
return getIntegration(dir, input.id);
|
|
9979
|
+
}
|
|
9980
|
+
function updateIntegration(dir, id, input) {
|
|
9981
|
+
const { integrations } = loadIntegrations(dir);
|
|
9982
|
+
const idx = integrations.findIndex((i) => i.id === id);
|
|
9983
|
+
if (idx < 0) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured.`);
|
|
9984
|
+
const existing = integrations[idx];
|
|
9985
|
+
if (input.enabled !== void 0) existing.enabled = input.enabled;
|
|
9986
|
+
if (input.config !== void 0) existing.config = input.config;
|
|
9987
|
+
if (input.secrets !== void 0) existing.secrets = input.secrets;
|
|
9988
|
+
if (input.timeoutMs !== void 0) existing.timeoutMs = input.timeoutMs;
|
|
9989
|
+
integrations[idx] = existing;
|
|
9990
|
+
saveIntegrations(dir, integrations);
|
|
9991
|
+
return getIntegration(dir, id);
|
|
9992
|
+
}
|
|
9993
|
+
function deleteIntegration(dir, id) {
|
|
9994
|
+
const { integrations } = loadIntegrations(dir);
|
|
9995
|
+
const idx = integrations.findIndex((i) => i.id === id);
|
|
9996
|
+
if (idx < 0) throw new IntegrationServiceError("INTEGRATION_NOT_CONFIGURED", `No integration "${id}" is configured.`);
|
|
9997
|
+
const removed = integrations[idx];
|
|
9998
|
+
integrations.splice(idx, 1);
|
|
9999
|
+
saveIntegrations(dir, integrations);
|
|
10000
|
+
const sp = secretProvider(dir);
|
|
10001
|
+
for (const ref of Object.values(removed.secrets)) {
|
|
10002
|
+
sp.delete(ref).catch(() => {
|
|
10003
|
+
});
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
10006
|
+
function enableIntegration(dir, id) {
|
|
10007
|
+
return updateIntegration(dir, id, { enabled: true });
|
|
10008
|
+
}
|
|
10009
|
+
function disableIntegration(dir, id) {
|
|
10010
|
+
return updateIntegration(dir, id, { enabled: false });
|
|
10011
|
+
}
|
|
10012
|
+
async function setIntegrationSecret(dir, id, secretName, value) {
|
|
10013
|
+
const { integration } = requireIntegration(dir, id);
|
|
10014
|
+
const refKey = secretRefKey(id, secretName);
|
|
10015
|
+
const sp = secretProvider(dir);
|
|
10016
|
+
await sp.set(refKey, value);
|
|
10017
|
+
if (!integration.secrets[secretName] || integration.secrets[secretName] !== refKey) {
|
|
10018
|
+
const { integrations } = loadIntegrations(dir);
|
|
10019
|
+
const idx = integrations.findIndex((i) => i.id === id);
|
|
10020
|
+
if (idx >= 0) {
|
|
10021
|
+
integrations[idx].secrets[secretName] = refKey;
|
|
10022
|
+
saveIntegrations(dir, integrations);
|
|
10023
|
+
}
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
async function removeIntegrationSecret(dir, id, secretName) {
|
|
10027
|
+
const { integration } = requireIntegration(dir, id);
|
|
10028
|
+
const refKey = integration.secrets[secretName];
|
|
10029
|
+
if (refKey) {
|
|
10030
|
+
const sp = secretProvider(dir);
|
|
10031
|
+
await sp.delete(refKey);
|
|
10032
|
+
}
|
|
10033
|
+
const { integrations } = loadIntegrations(dir);
|
|
10034
|
+
const idx = integrations.findIndex((i) => i.id === id);
|
|
10035
|
+
if (idx >= 0 && integrations[idx].secrets[secretName]) {
|
|
10036
|
+
delete integrations[idx].secrets[secretName];
|
|
10037
|
+
saveIntegrations(dir, integrations);
|
|
10038
|
+
}
|
|
10039
|
+
}
|
|
9738
10040
|
async function verifyIntegration(dir, id, env = process.env) {
|
|
9739
10041
|
const { integration, findings } = requireIntegration(dir, id);
|
|
9740
10042
|
const cfgStatus = configStatus(integration, findings);
|
|
9741
10043
|
if (cfgStatus === "disabled") return { id, status: "disabled", connection: null, missingCredentials: [] };
|
|
9742
10044
|
if (cfgStatus === "invalid-config") return { id, status: "invalid-config", connection: null, missingCredentials: [] };
|
|
9743
10045
|
const adapter = resolveAdapter(integration);
|
|
9744
|
-
const { context, missing } =
|
|
10046
|
+
const { context, missing } = await buildContextWithSecrets(dir, integration, env);
|
|
9745
10047
|
try {
|
|
9746
10048
|
const connection = await withTimeout(adapter.verifyConnection(context), context.timeoutMs);
|
|
9747
10049
|
return { id, status: statusOf(connection), connection, missingCredentials: missing, message: connection.message };
|
|
@@ -9766,14 +10068,14 @@ async function listExternalWorkItems(dir, id, opts = {}, env = process.env) {
|
|
|
9766
10068
|
const { integration } = requireIntegration(dir, id);
|
|
9767
10069
|
const adapter = resolveAdapter(integration);
|
|
9768
10070
|
if (!adapter.capabilities.workItems.list) throw integrationError("UNSUPPORTED_CAPABILITY", `Adapter "${adapter.id}" cannot list work items.`);
|
|
9769
|
-
const { context } =
|
|
10071
|
+
const { context } = await buildContextWithSecrets(dir, integration, env);
|
|
9770
10072
|
return withTimeout(adapter.listWorkItems({ context, cursor: opts.cursor, pageSize: opts.pageSize, filters: opts.filters }), context.timeoutMs);
|
|
9771
10073
|
}
|
|
9772
10074
|
async function getExternalWorkItem(dir, id, externalId, env = process.env) {
|
|
9773
10075
|
const { integration } = requireIntegration(dir, id);
|
|
9774
10076
|
const adapter = resolveAdapter(integration);
|
|
9775
10077
|
if (!adapter.capabilities.workItems.read) throw integrationError("UNSUPPORTED_CAPABILITY", `Adapter "${adapter.id}" cannot read work items.`);
|
|
9776
|
-
const { context } =
|
|
10078
|
+
const { context } = await buildContextWithSecrets(dir, integration, env);
|
|
9777
10079
|
return withTimeout(adapter.getWorkItem({ context, externalId }), context.timeoutMs);
|
|
9778
10080
|
}
|
|
9779
10081
|
function findLinkedWorkItem(dir, integrationId, externalId) {
|
|
@@ -9837,15 +10139,22 @@ export {
|
|
|
9837
10139
|
buildRefinementHandoff,
|
|
9838
10140
|
buildTopologyEnrichmentHandoff,
|
|
9839
10141
|
computeRefinementStatus,
|
|
10142
|
+
createIntegration,
|
|
9840
10143
|
createWorkItem,
|
|
9841
10144
|
cwd,
|
|
10145
|
+
deleteIntegration,
|
|
10146
|
+
disableIntegration,
|
|
9842
10147
|
discoverKnowledge,
|
|
9843
10148
|
discoverWorkItems,
|
|
10149
|
+
enableIntegration,
|
|
9844
10150
|
exists,
|
|
9845
10151
|
findLinkedWorkItem,
|
|
9846
10152
|
findSystemPaths,
|
|
10153
|
+
getAvailableIntegrationTypes,
|
|
9847
10154
|
getExternalWorkItem,
|
|
9848
10155
|
getImpactCandidates,
|
|
10156
|
+
getIntegration,
|
|
10157
|
+
getIntegrationSecretStatus,
|
|
9849
10158
|
getSystemMapProjection,
|
|
9850
10159
|
getSystemNeighbors,
|
|
9851
10160
|
getSystemNodeContext,
|
|
@@ -9869,9 +10178,12 @@ export {
|
|
|
9869
10178
|
loadSystemTopology,
|
|
9870
10179
|
previewImport,
|
|
9871
10180
|
readFile,
|
|
10181
|
+
removeIntegrationSecret,
|
|
9872
10182
|
searchSystemNodes,
|
|
10183
|
+
setIntegrationSecret,
|
|
9873
10184
|
topologyRevision,
|
|
9874
10185
|
transitionWorkItem,
|
|
10186
|
+
updateIntegration,
|
|
9875
10187
|
updateWorkItem,
|
|
9876
10188
|
validateSystemTopology,
|
|
9877
10189
|
validateTopologyProposal,
|