@ran-sh/dsh-crew 0.5.7 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,8 +46,14 @@ dsh-crew inspect # live capabilities and readiness
46
46
  dsh-crew jobs list # jobs and Result Contracts
47
47
  dsh-crew providers list # 3210 Harness provider inventory (secret-free)
48
48
  dsh-crew providers probe <provider-id>
49
+ curl http://127.0.0.1:3210/_dsh/dsh-crew/credential-references # references/orphans only
50
+ dsh-crew credentials list # secret-free reference inventory
51
+ dsh-crew credentials purge-plan env:NAME
52
+ dsh-crew credentials purge env:NAME --plan <plan-id> --expected-revision <sha256> --confirm
49
53
  dsh-crew providers delete-plan <provider-id> --replacement-default <provider-id>
50
54
  dsh-crew providers delete <provider-id> --plan <plan-id> --expected-revision <sha256> --confirm
55
+ dsh-crew releases list # retained validated payloads
56
+ dsh-crew rollback <version> # switch payload and verify the 3210 runtime
51
57
  dsh-crew update # update and repair enabled integrations
52
58
  dsh-crew uninstall # remove managed files, keep backups/config
53
59
  ```
package/README.zh.md CHANGED
@@ -42,8 +42,14 @@ dsh-crew inspect # 实时能力与就绪度
42
42
  dsh-crew jobs list # 任务与 Result Contract
43
43
  dsh-crew providers list # 3210 Harness Provider 清单(不含密钥)
44
44
  dsh-crew providers probe <provider-id>
45
+ curl http://127.0.0.1:3210/_dsh/dsh-crew/credential-references # 仅查看引用/孤儿报告
46
+ dsh-crew credentials list # 不含密钥值的引用清单
47
+ dsh-crew credentials purge-plan env:NAME
48
+ dsh-crew credentials purge env:NAME --plan <plan-id> --expected-revision <sha256> --confirm
45
49
  dsh-crew providers delete-plan <provider-id> --replacement-default <provider-id>
46
50
  dsh-crew providers delete <provider-id> --plan <plan-id> --expected-revision <sha256> --confirm
51
+ dsh-crew releases list # 查看保留且已验证的版本
52
+ dsh-crew rollback <version> # 切换版本并验证 3210 runtime
47
53
  dsh-crew update # 更新并修复已启用集成
48
54
  dsh-crew uninstall # 移除受管文件,保留配置/备份
49
55
  ```
package/lib/client.js CHANGED
@@ -179,7 +179,16 @@ function componentState(status, keys, { aligned = false } = {}) {
179
179
  if (evidence.some((value) => typeof value !== "boolean")) return READINESS_STATES.UNKNOWN;
180
180
  return evidence.every(Boolean) && (!aligned || status.components.target_alignment === true) ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
181
181
  }
182
- function runtimeState(runtime) {
182
+ function runtimeState(runtime, readinessSnapshot) {
183
+ if (readinessSnapshot && typeof readinessSnapshot === "object") {
184
+ const identity = readinessSnapshot.runtime;
185
+ if (!(identity?.execution_plane === "hub-3210" && identity.profile === "dsh-crew" && Number(identity.listen_port) === 3210 && typeof identity.runtime_id === "string" && identity.runtime_id.trim())) return READINESS_STATES.UNAVAILABLE;
186
+ const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows) ? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === "hub_compatibility") : void 0;
187
+ if (row?.status === "PASS") return READINESS_STATES.READY;
188
+ if (row?.status === "FAIL") return READINESS_STATES.UNAVAILABLE;
189
+ if (row?.status === "NOT_RUN" || row?.status === "SKIP") return READINESS_STATES.DEGRADED;
190
+ return READINESS_STATES.UNKNOWN;
191
+ }
183
192
  if (runtime === void 0) return READINESS_STATES.UNKNOWN;
184
193
  if (runtime === null) return READINESS_STATES.UNAVAILABLE;
185
194
  return runtime?.ok === true && runtime.service === "dsh-crew-hub" ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
@@ -190,7 +199,7 @@ function bridgeState(surface) {
190
199
  return READINESS_STATES.UNKNOWN;
191
200
  }
192
201
  /** Project only structured, non-secret installer/runtime evidence. */
193
- function projectHostReadiness({ installStatus, runtime, surface } = {}) {
202
+ function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
194
203
  const codex = installStatus?.codex;
195
204
  const claude = installStatus?.claude;
196
205
  const zcode = installStatus?.zcode;
@@ -230,7 +239,7 @@ function projectHostReadiness({ installStatus, runtime, surface } = {}) {
230
239
  },
231
240
  {
232
241
  id: "crew_harness",
233
- state: runtimeState(runtime),
242
+ state: runtimeState(runtime, readinessSnapshot),
234
243
  detail: runtime?.runtime_version ?? null
235
244
  },
236
245
  {
@@ -434,7 +443,7 @@ const COPY = {
434
443
  "follow-dsh": "跟随 Harness · 使用各档独立的模型优先级",
435
444
  "deepseek-official": "DeepSeek Official · 使用旧版固定模型路由"
436
445
  },
437
- workerProviderHint: "仅作用于 Hub 模式;Standalone 始终用 deepseek-official + DEEPSEEK_API_KEY。",
446
+ workerProviderHint: "仅作用于 Hub 模式;Standalone 仅保留为旧版迁移元数据,不是生产执行路径。",
438
447
  workerModels: "Worker 模型",
439
448
  refreshHarnessModels: "刷新 Harness 模型",
440
449
  refreshingModels: "刷新中…",
@@ -503,11 +512,23 @@ const COPY = {
503
512
  providerDeletePlan: "生成删除计划",
504
513
  providerDelete: "确认删除",
505
514
  providerPlanReady: "计划已生成;再次确认后才会修改 3210 配置。",
515
+ providerRollback: "回滚",
516
+ providerRollbackConfirm: "按已保存事务恢复这个 Harness Provider 及原有路由?",
517
+ providerRollbackUnavailable: "没有可验证的回滚事务",
506
518
  providerReplacement: "替换 Harness 默认 Provider(输入 Provider id)",
507
519
  providerDeleteConfirm: "确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。",
508
520
  providerDeleteBlocked: "当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。",
509
521
  providerLifecycleError: "Provider 生命周期操作失败",
510
522
  providerNoInventory: "暂时无法读取 3210 Provider inventory。",
523
+ credentialReferences: "Credential 引用",
524
+ credentialReferenceHint: "仅显示引用名、归属和孤儿状态;不会读取或删除密钥。",
525
+ credentialOrphan: "孤儿 · 可单独申请清理",
526
+ credentialInUse: (count) => `使用中 · ${count} 个 Provider`,
527
+ credentialPurgePlan: "申请清理",
528
+ credentialPurgeConfirm: "这是不可恢复的 Crew-owned 凭据清理,确认继续?",
529
+ credentialPurging: "清理中…",
530
+ credentialPurgeUnavailable: "凭据清理不可用",
531
+ credentialPurgeUnverified: "已执行清理,但验证失败;引用仍保留用于恢复检查",
511
532
  customProviders: "自定义 Provider",
512
533
  addProvider: "+ 添加 Provider",
513
534
  noCustomProviders: "暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。",
@@ -745,7 +766,7 @@ const COPY = {
745
766
  "follow-dsh": "Follow Harness · use each tier's ordered model priority",
746
767
  "deepseek-official": "DeepSeek Official · use legacy fixed model routing"
747
768
  },
748
- workerProviderHint: "Applies to Hub workers only; Standalone always uses deepseek-official + DEEPSEEK_API_KEY.",
769
+ workerProviderHint: "Applies to Hub workers only; Standalone is legacy migration metadata only and is not a production execution path.",
749
770
  workerModels: "Worker Models",
750
771
  refreshHarnessModels: "Refresh Harness Models",
751
772
  refreshingModels: "Refreshing…",
@@ -814,11 +835,23 @@ const COPY = {
814
835
  providerDeletePlan: "Plan deletion",
815
836
  providerDelete: "Confirm delete",
816
837
  providerPlanReady: "Plan ready; a second confirmation is required before changing 3210 config.",
838
+ providerRollback: "Rollback",
839
+ providerRollbackConfirm: "Restore this Harness Provider and its previous routing from the saved transaction?",
840
+ providerRollbackUnavailable: "No verified rollback transaction is available",
817
841
  providerReplacement: "Replacement Harness Default provider id",
818
842
  providerDeleteConfirm: "Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.",
819
843
  providerDeleteBlocked: "This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).",
820
844
  providerLifecycleError: "Provider lifecycle operation failed",
821
845
  providerNoInventory: "3210 Provider inventory is temporarily unavailable.",
846
+ credentialReferences: "Credential references",
847
+ credentialReferenceHint: "Names, ownership, and orphan status only; values are never read or deleted.",
848
+ credentialOrphan: "orphan · separate purge approval required",
849
+ credentialInUse: (count) => `in use · ${count} provider(s)`,
850
+ credentialPurgePlan: "Purge…",
851
+ credentialPurgeConfirm: "This irreversibly removes a Crew-owned credential. Continue?",
852
+ credentialPurging: "Purging…",
853
+ credentialPurgeUnavailable: "Credential purge unavailable",
854
+ credentialPurgeUnverified: "Purge ran but verification failed; the reference remains visible for recovery",
822
855
  customProviders: "Custom providers",
823
856
  addProvider: "+ Add provider",
824
857
  noCustomProviders: "None yet. Added providers appear in the vision / image-gen selects above.",
@@ -1406,6 +1439,9 @@ function WorkersPanel({ ctx }) {
1406
1439
  const [modelCatalogBusy, setModelCatalogBusy] = (0, react.useState)(false);
1407
1440
  const [providerInventory, setProviderInventory] = (0, react.useState)(null);
1408
1441
  const [providerHealth, setProviderHealth] = (0, react.useState)([]);
1442
+ const [credentialRefs, setCredentialRefs] = (0, react.useState)([]);
1443
+ const [credentialUnverified, setCredentialUnverified] = (0, react.useState)([]);
1444
+ const [credentialLifecycleBusy, setCredentialLifecycleBusy] = (0, react.useState)(null);
1409
1445
  const [providerInventoryError, setProviderInventoryError] = (0, react.useState)("");
1410
1446
  const [providerLifecycleBusy, setProviderLifecycleBusy] = (0, react.useState)(null);
1411
1447
  const [modelPickerTier, setModelPickerTier] = (0, react.useState)(null);
@@ -1414,6 +1450,7 @@ function WorkersPanel({ ctx }) {
1414
1450
  const [expandedSections, setExpandedSections] = (0, react.useState)(() => readSectionState(sectionStorage()));
1415
1451
  const [surface, setSurface] = (0, react.useState)("detecting");
1416
1452
  const [runtimeInfo, setRuntimeInfo] = (0, react.useState)(void 0);
1453
+ const [readinessSnapshot, setReadinessSnapshot] = (0, react.useState)(void 0);
1417
1454
  const toggleSection = (sectionId) => {
1418
1455
  setExpandedSections((current) => ({
1419
1456
  ...current,
@@ -1494,13 +1531,15 @@ function WorkersPanel({ ctx }) {
1494
1531
  }, [detectSurface]);
1495
1532
  const refreshAll = (0, react.useCallback)(async () => {
1496
1533
  try {
1497
- const [j, s, c, pr, pi, ph] = await Promise.all([
1534
+ const [j, s, c, pr, pi, ph, cr, ext] = await Promise.all([
1498
1535
  get("/jobs"),
1499
1536
  get("/install/status"),
1500
1537
  get("/config"),
1501
1538
  get("/presets"),
1502
1539
  get("/providers").catch(() => null),
1503
- get("/provider-health").catch(() => null)
1540
+ get("/provider-health").catch(() => null),
1541
+ get("/credential-references").catch(() => null),
1542
+ get("/extension").catch(() => null)
1504
1543
  ]);
1505
1544
  if (j.ok) setJobs(j.jobs ?? []);
1506
1545
  if (s.ok) setStatus(s.status);
@@ -1514,6 +1553,11 @@ function WorkersPanel({ ctx }) {
1514
1553
  }))]);
1515
1554
  if (pi?.ok) setProviderInventory(pi);
1516
1555
  if (ph?.ok) setProviderHealth(ph.health ?? []);
1556
+ if (cr?.ok) {
1557
+ setCredentialRefs(cr.records ?? []);
1558
+ setCredentialUnverified(cr.unverified_purges ?? []);
1559
+ }
1560
+ if (ext?.ok) setReadinessSnapshot(ext.extension?.readiness_snapshot ?? void 0);
1517
1561
  } catch {}
1518
1562
  }, [get]);
1519
1563
  (0, react.useEffect)(() => {
@@ -1547,14 +1591,59 @@ function WorkersPanel({ ctx }) {
1547
1591
  const refreshProviderInventory = (0, react.useCallback)(async () => {
1548
1592
  setProviderInventoryError("");
1549
1593
  try {
1550
- const [result, health] = await Promise.all([get("/providers"), get("/provider-health").catch(() => null)]);
1594
+ const [result, health, refs] = await Promise.all([
1595
+ get("/providers"),
1596
+ get("/provider-health").catch(() => null),
1597
+ get("/credential-references").catch(() => null)
1598
+ ]);
1551
1599
  if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
1552
1600
  setProviderInventory(result);
1553
1601
  if (health?.ok) setProviderHealth(health.health ?? []);
1602
+ if (refs?.ok) {
1603
+ setCredentialRefs(refs.records ?? []);
1604
+ setCredentialUnverified(refs.unverified_purges ?? []);
1605
+ }
1554
1606
  } catch (error) {
1555
1607
  setProviderInventoryError(error?.message ?? copy.providerNoInventory);
1556
1608
  }
1557
1609
  }, [get, copy]);
1610
+ const purgeCredentialRef = (0, react.useCallback)(async (ref) => {
1611
+ if (ref?.ownership !== "crew-owned" || ref?.orphan !== true || ref?.purge_capability !== "eligible") return;
1612
+ const referenceId = String(ref.reference_id ?? "");
1613
+ if (!referenceId || !window.confirm(copy.credentialPurgeConfirm)) return;
1614
+ setCredentialLifecycleBusy(referenceId);
1615
+ setNotice(copy.working);
1616
+ try {
1617
+ const encoded = encodeURIComponent(referenceId);
1618
+ const planned = await post(`/credential-references/${encoded}/purge-plan`, {});
1619
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.credentialPurgeUnavailable);
1620
+ if (!window.confirm(`${copy.credentialPurgeConfirm}\n\n${referenceId}`)) return;
1621
+ const result = await readJson(await fetch(withLang(`/credential-references/${encoded}`), {
1622
+ method: "DELETE",
1623
+ headers: { "content-type": "application/json" },
1624
+ body: JSON.stringify({
1625
+ plan_id: planned.plan.plan_id,
1626
+ expected_revision: planned.plan.expected_revision,
1627
+ confirm: true,
1628
+ lang: locale === "zh" ? "zh" : "en"
1629
+ })
1630
+ }), `/credential-references/${encoded}`);
1631
+ if (!result.ok || result.state !== "VERIFIED") throw new Error(result.code ?? copy.credentialPurgeUnavailable);
1632
+ setNotice(copy.saved);
1633
+ await refreshProviderInventory();
1634
+ } catch (error) {
1635
+ setNotice(String(error?.message ?? error));
1636
+ } finally {
1637
+ setCredentialLifecycleBusy(null);
1638
+ }
1639
+ }, [
1640
+ copy,
1641
+ locale,
1642
+ post,
1643
+ readJson,
1644
+ refreshProviderInventory,
1645
+ withLang
1646
+ ]);
1558
1647
  (0, react.useEffect)(() => {
1559
1648
  if (surface === CREW_UI_SURFACES.OFFICIAL) refreshHarnessModels();
1560
1649
  }, [surface, refreshHarnessModels]);
@@ -1922,6 +2011,43 @@ function WorkersPanel({ ctx }) {
1922
2011
  setProviderLifecycleBusy(null);
1923
2012
  }
1924
2013
  };
2014
+ const rollbackHarnessProvider = async (record, transactionId) => {
2015
+ if (!record || !transactionId || record.desired_state !== "absent") {
2016
+ setNotice(copy.providerRollbackUnavailable);
2017
+ return;
2018
+ }
2019
+ if (!window.confirm(copy.providerRollbackConfirm)) return;
2020
+ setProviderLifecycleBusy(`rollback:${record.id}`);
2021
+ setNotice(copy.working);
2022
+ try {
2023
+ const encoded = encodeURIComponent(record.id);
2024
+ const result = await post(`/providers/${encoded}/rollback`, {
2025
+ transaction_id: transactionId,
2026
+ confirm: true
2027
+ });
2028
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
2029
+ if (result.restart_required === true && result.state === "ROLLBACK_PENDING") {
2030
+ const restartPath = "http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart";
2031
+ const restarted = await readJson(await fetch(restartPath, {
2032
+ method: "POST",
2033
+ headers: { "content-type": "application/json" },
2034
+ body: JSON.stringify({ confirm: true })
2035
+ }), restartPath);
2036
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
2037
+ const verified = await post(`/providers/${encoded}/verify-rollback`, {
2038
+ transaction_id: transactionId,
2039
+ confirm: true
2040
+ });
2041
+ if (!verified.ok || verified.state !== "ROLLED_BACK") throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
2042
+ } else if (result.state !== "ROLLED_BACK") throw new Error(result.code ?? copy.providerLifecycleError);
2043
+ setNotice(copy.saved);
2044
+ await refreshProviderInventory();
2045
+ } catch (error) {
2046
+ setNotice(String(error?.message ?? error));
2047
+ } finally {
2048
+ setProviderLifecycleBusy(null);
2049
+ }
2050
+ };
1925
2051
  /** Card: title + one-line description header, then its fields. */
1926
2052
  const block = (text, fields, gridded = true) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1927
2053
  style: S.block,
@@ -2021,7 +2147,8 @@ function WorkersPanel({ ctx }) {
2021
2147
  const hostReadiness = projectHostReadiness({
2022
2148
  installStatus: status,
2023
2149
  runtime: runtimeInfo,
2024
- surface
2150
+ surface,
2151
+ readinessSnapshot
2025
2152
  });
2026
2153
  if (!currentSurfaceResponsibilities.fullControlPlane) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinimalCrewPanel, {
2027
2154
  locale,
@@ -3463,6 +3590,7 @@ function WorkersPanel({ ctx }) {
3463
3590
  (providerInventory?.records ?? []).map((record) => {
3464
3591
  const blocked = record.ownership === "harness" || record.desired_state === "absent" || Number(record.references?.active_jobs ?? 0) > 0;
3465
3592
  const lifecycle = record.desired_state === "absent" ? "absent" : record.lifecycle?.enabled === false ? "disabled" : record.lifecycle?.catalogued ? "catalogued" : "configured";
3593
+ const rollbackTransaction = (providerInventory?.lifecycle_transactions ?? []).find((entry) => entry.provider_id === record.id && ["VERIFIED", "RESTART_PENDING"].includes(entry.state));
3466
3594
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3467
3595
  style: {
3468
3596
  display: "flex",
@@ -3507,9 +3635,95 @@ function WorkersPanel({ ctx }) {
3507
3635
  deleteHarnessProvider(record);
3508
3636
  },
3509
3637
  children: providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan
3638
+ }),
3639
+ record.desired_state === "absent" && rollbackTransaction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3640
+ type: "button",
3641
+ style: {
3642
+ ...S.btn,
3643
+ padding: "2px 8px"
3644
+ },
3645
+ disabled: providerLifecycleBusy !== null,
3646
+ onClick: () => {
3647
+ rollbackHarnessProvider(record, rollbackTransaction.transaction_id);
3648
+ },
3649
+ children: providerLifecycleBusy === `rollback:${record.id}` ? copy.working : copy.providerRollback
3510
3650
  })
3511
3651
  ]
3512
3652
  }, record.id);
3653
+ }),
3654
+ (credentialRefs.length > 0 || credentialUnverified.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3655
+ style: {
3656
+ marginTop: 10,
3657
+ borderTop: "1px solid rgba(128,128,128,0.16)",
3658
+ paddingTop: 8
3659
+ },
3660
+ children: [
3661
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3662
+ style: {
3663
+ fontWeight: 600,
3664
+ fontSize: 12.5
3665
+ },
3666
+ children: copy.credentialReferences
3667
+ }),
3668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3669
+ style: {
3670
+ fontSize: 11,
3671
+ opacity: .6,
3672
+ margin: "3px 0 7px"
3673
+ },
3674
+ children: copy.credentialReferenceHint
3675
+ }),
3676
+ credentialUnverified.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3677
+ style: {
3678
+ color: "#c55",
3679
+ fontSize: 11.5
3680
+ },
3681
+ children: [
3682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3683
+ style: S.mono,
3684
+ children: entry.reference_id
3685
+ }),
3686
+ " · ",
3687
+ copy.credentialPurgeUnverified
3688
+ ]
3689
+ }, `unverified:${entry.reference_id}`)),
3690
+ credentialRefs.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3691
+ style: {
3692
+ display: "flex",
3693
+ alignItems: "center",
3694
+ gap: 8,
3695
+ fontSize: 11.5,
3696
+ flexWrap: "wrap"
3697
+ },
3698
+ children: [
3699
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3700
+ style: S.mono,
3701
+ children: ref.reference_id
3702
+ }),
3703
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3704
+ style: { opacity: .65 },
3705
+ children: ref.ownership
3706
+ }),
3707
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3708
+ style: { color: ref.orphan ? "#c98735" : void 0 },
3709
+ children: ref.orphan ? copy.credentialOrphan : copy.credentialInUse(ref.consumer_count ?? 0)
3710
+ }),
3711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
3712
+ ref.ownership === "crew-owned" && ref.orphan === true && ref.purge_capability === "eligible" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3713
+ type: "button",
3714
+ style: {
3715
+ ...S.btn,
3716
+ padding: "2px 8px"
3717
+ },
3718
+ disabled: credentialLifecycleBusy !== null,
3719
+ onClick: () => {
3720
+ purgeCredentialRef(ref);
3721
+ },
3722
+ children: credentialLifecycleBusy === ref.reference_id ? copy.credentialPurging : copy.credentialPurgePlan
3723
+ })
3724
+ ]
3725
+ }, ref.reference_id))
3726
+ ]
3513
3727
  })
3514
3728
  ]
3515
3729
  }),