create-safest-tools 0.5.2 → 0.6.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.
@@ -296,10 +296,13 @@ td small { margin-top: 3px; color: var(--muted); }
296
296
  .report-run-record strong { font-size: 10px; }
297
297
  .report-run-record small { margin-top: 4px; color: var(--muted); font-size: 8px; overflow-wrap: anywhere; }
298
298
  .report-timeline { position: relative; display: grid; gap: 0; }
299
- .timeline-event { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 8px; min-height: 48px; }
300
- .timeline-event:not(:last-child)::before { content: ""; position: absolute; top: 25px; bottom: 0; left: 11px; width: 1px; background: #d8e1da; }
301
- .timeline-marker { position: relative; z-index: 1; width: 23px; height: 23px; display: grid; place-items: center; border: 1px solid #cbd9ce; border-radius: 50%; color: #315641; background: #fff; font-size: 9px; font-weight: 850; }
302
- .timeline-marker.action { color: #8a4a20; border-color: #edc69f; background: #fff8ef; }
299
+ .timeline-event { position: relative; display: grid; grid-template-columns: 16px 1fr; gap: 12px; min-height: 48px; }
300
+ .timeline-event:not(:last-child)::before { content: ""; position: absolute; top: 14px; bottom: 0; left: 5px; width: 2px; background: #d8e1da; }
301
+ .timeline-marker { position: relative; z-index: 1; width: 12px; height: 12px; margin-top: 4px; border: 3px solid #fff; border-radius: 50%; background: #527261; box-shadow: 0 0 0 1px #b9cabe; }
302
+ .timeline-marker.message { background: #2f6d91; box-shadow: 0 0 0 1px #a9c9da; }
303
+ .timeline-marker.decision { background: #245d43; box-shadow: 0 0 0 1px #9fc2ad; }
304
+ .timeline-marker.finding { background: #7a6598; box-shadow: 0 0 0 1px #c5b9d5; }
305
+ .timeline-marker.action { background: #a35b27; box-shadow: 0 0 0 1px #e0b48f; }
303
306
  .timeline-event > div { padding: 2px 0 10px; }
304
307
  .timeline-event strong, .timeline-event span, .timeline-event small { display: block; }
305
308
  .timeline-event strong { font-size: 9px; }
@@ -1161,9 +1164,9 @@ body.login-mode .top-actions { margin-left: auto; }
1161
1164
  .case-activity-heading p { margin: 5px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
1162
1165
  .case-activity-heading > span { flex: 0 0 auto; padding: 5px 9px; border-radius: 999px; color: #315b47; background: #edf5ef; font-size: 11px; font-weight: 750; }
1163
1166
  .case-activity-section .report-timeline { padding-left: 2px; }
1164
- .case-activity-section .timeline-event { grid-template-columns: 36px minmax(0, 1fr); gap: 12px; min-height: 76px; }
1165
- .case-activity-section .timeline-event:not(:last-child)::before { top: 32px; bottom: 0; left: 15px; width: 2px; background: #dfe8e1; }
1166
- .case-activity-section .timeline-marker { width: 32px; height: 32px; border-width: 2px; font-size: 13px; }
1167
+ .case-activity-section .timeline-event { grid-template-columns: 18px minmax(0, 1fr); gap: 14px; min-height: 68px; }
1168
+ .case-activity-section .timeline-event:not(:last-child)::before { top: 16px; bottom: 0; left: 6px; width: 2px; background: #dfe8e1; }
1169
+ .case-activity-section .timeline-marker { width: 14px; height: 14px; margin-top: 3px; }
1167
1170
  .case-activity-section .timeline-event > div { padding: 2px 0 18px; }
1168
1171
  .case-activity-section .timeline-event strong { color: #172d23; font-size: 13px; line-height: 1.35; }
1169
1172
  .case-activity-section .timeline-event span { margin-top: 4px; color: #40584c; font-size: 12px; line-height: 1.5; }
@@ -1596,3 +1599,194 @@ body.login-mode .top-actions { margin-left: auto; }
1596
1599
  .builder-row-actions { justify-content: flex-end; }
1597
1600
  .field-option-row button { width: 100%; }
1598
1601
  }
1602
+
1603
+ /* Tools and connections */
1604
+ .registry-workspace { max-width: 1400px; margin-inline: auto; }
1605
+ .registry-workspace > .page-head { margin-inline: 0; }
1606
+ .registry-model { margin-bottom: 20px; padding: 16px 18px; display: grid; grid-template-columns: minmax(150px,1fr) auto minmax(150px,1fr) auto minmax(150px,1fr) auto minmax(150px,1fr); align-items: center; gap: 12px; border: 1px solid #d8e3da; border-radius: 13px; background: linear-gradient(135deg, #fbfdf9, #f1f7f1); }
1607
+ .registry-model > div { min-width: 0; display: grid; grid-template-columns: 28px minmax(0,1fr); column-gap: 10px; align-items: center; }
1608
+ .registry-model > div > span { width: 28px; height: 28px; grid-row: span 2; display: grid; place-items: center; border-radius: 8px; color: var(--forest); background: white; box-shadow: 0 0 0 1px #d7e1d9; font-size: 10px; font-weight: 850; }
1609
+ .registry-model strong { font-size: 12px; }
1610
+ .registry-model small { margin-top: 2px; overflow: hidden; color: var(--muted); font-size: 10px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
1611
+ .registry-model > b { color: #8ba095; font-size: 15px; }
1612
+ .registry-tabs-row { margin-bottom: 13px; display: flex; align-items: center; gap: 16px; }
1613
+ .registry-tabs-row .segmented-tabs { flex: 0 0 auto; margin: 0; }
1614
+ .registry-tabs-row .segmented-tabs button { font-size: 11px; }
1615
+ .registry-tabs-row .segmented-tabs button span { min-width: 20px; margin-left: 4px; padding: 2px 6px; border-radius: 999px; color: #5b6a61; background: #e5eae5; font-size: 9px; }
1616
+ .registry-tabs-row .segmented-tabs button.active span { color: var(--forest); background: var(--mint); }
1617
+ .registry-tabs-row > p { max-width: 720px; margin: 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
1618
+ .registry-toolbar { margin-bottom: 12px; display: flex; align-items: center; gap: 10px; }
1619
+ .registry-toolbar .search-field { position: relative; min-width: 230px; flex: 1 1 340px; }
1620
+ .registry-toolbar .search-field > span { position: absolute; z-index: 1; top: 11px; left: 12px; color: #849087; font-size: 14px; }
1621
+ .registry-toolbar .search-field input { padding-left: 34px; }
1622
+ .registry-filter-chips { display: flex; align-items: center; gap: 5px; }
1623
+ .registry-filter-chips button { min-height: 34px; padding: 7px 10px; border: 1px solid var(--line); border-radius: 999px; color: #58665e; background: white; font-size: 10px; font-weight: 750; }
1624
+ .registry-filter-chips button:hover, .registry-filter-chips button.active { color: var(--forest-dark); border-color: #a8c2b0; background: #eef6f0; }
1625
+ .registry-archived-toggle { margin-left: auto; display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 10px; white-space: nowrap; }
1626
+ .registry-archived-toggle input { width: 15px; min-height: 15px; accent-color: var(--forest); }
1627
+ .registry-resource-list { min-width: 0; border: 1px solid var(--line); border-radius: 13px; background: white; box-shadow: 0 4px 16px rgba(21,55,37,.035); overflow: hidden; }
1628
+ .registry-resource-list > .skeleton-card { min-height: 118px; border-radius: 0; border-bottom: 1px solid var(--line); }
1629
+ .registry-resource-row { min-width: 0; padding: 17px 18px; display: grid; grid-template-columns: 42px minmax(280px,1fr) 92px minmax(150px,190px) auto; align-items: center; gap: 14px; border-top: 1px solid #edf0ec; }
1630
+ .registry-resource-row:first-child { border-top: 0; }
1631
+ .registry-resource-row:hover { background: #fbfcfa; }
1632
+ .registry-resource-row .object-icon { width: 38px; height: 38px; font-size: 15px; }
1633
+ .registry-resource-main { min-width: 0; }
1634
+ .registry-resource-title { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
1635
+ .registry-resource-title h3 { margin: 0; font: 720 15px/1.25 var(--sans); letter-spacing: -.015em; }
1636
+ .registry-owner-badge { padding: 3px 6px; border-radius: 999px; color: #5c4b83; background: #f0edfa; font-size: 8px; font-weight: 800; }
1637
+ .registry-resource-main > p { max-width: 720px; margin: 5px 0 8px; overflow: hidden; color: var(--muted); font-size: 11px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; }
1638
+ .registry-resource-tags { display: flex; flex-wrap: wrap; gap: 5px; }
1639
+ .registry-resource-tags span { padding: 3px 6px; border-radius: 5px; color: #5c6961; background: #f0f3ef; font-size: 8px; font-weight: 700; }
1640
+ .registry-resource-relationship span, .registry-resource-relationship strong, .registry-resource-relationship small { display: block; }
1641
+ .registry-resource-relationship span { color: var(--muted); font-size: 8px; text-transform: uppercase; }
1642
+ .registry-resource-relationship strong { margin-top: 3px; font-size: 15px; }
1643
+ .registry-resource-relationship small { color: var(--muted); font-size: 8px; }
1644
+ .registry-resource-state { min-width: 0; }
1645
+ .registry-status { width: fit-content; padding: 4px 8px; display: inline-flex; align-items: center; gap: 6px; border-radius: 999px; color: #526159; background: #edf1ed; font-size: 9px; font-weight: 800; }
1646
+ .registry-status i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
1647
+ .registry-status.available { color: #2f6845; background: #eaf5ed; }
1648
+ .registry-status.paused { color: #896221; background: #fff3d8; }
1649
+ .registry-status.archived { color: #69736d; background: #ecefed; }
1650
+ .registry-resource-state small { margin-top: 6px; display: block; overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
1651
+ .registry-resource-row > button { justify-self: end; white-space: nowrap; }
1652
+ .registry-resource-list > .empty-product { border: 0; border-radius: 0; }
1653
+
1654
+ .registry-detail-dialog { width: min(820px, calc(100% - 30px)); }
1655
+ .registry-detail-head h2 { margin-top: 2px; font-family: var(--sans); font-size: 21px; }
1656
+ .registry-detail-head p:last-child { max-width: 650px; font-size: 11px; line-height: 1.45; text-transform: none; }
1657
+ .registry-detail-body { padding: 20px 22px 24px; display: grid; gap: 13px; }
1658
+ .registry-detail-summary { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 8px; }
1659
+ .registry-detail-summary > div { min-width: 0; padding: 11px; border-radius: 9px; background: #f5f7f4; }
1660
+ .registry-detail-summary span, .registry-detail-summary strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1661
+ .registry-detail-summary span { color: var(--muted); font-size: 8px; text-transform: uppercase; }
1662
+ .registry-detail-summary strong { margin-top: 5px; font-size: 10px; }
1663
+ .registry-detail-section { padding: 15px; border: 1px solid var(--line); border-radius: 11px; }
1664
+ .registry-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
1665
+ .registry-section-heading h3 { margin: 0; font-size: 13px; }
1666
+ .registry-section-heading p { max-width: 580px; margin: 4px 0 0; color: var(--muted); font-size: 10px; line-height: 1.5; }
1667
+ .registry-section-heading button { flex: 0 0 auto; }
1668
+ .registry-placeholder { margin: 12px 0 0; padding: 11px; border-radius: 8px; color: var(--muted); background: #f7f8f6; font-size: 10px; }
1669
+ .registry-verification { margin-top: 12px; padding: 13px; border: 1px solid; border-radius: 9px; }
1670
+ .registry-verification.verified { color: #315f43; border-color: #c8ddcd; background: #f0f8f2; }
1671
+ .registry-verification.attention { color: #875e1f; border-color: #ead8aa; background: #fff9e9; }
1672
+ .registry-verification > div:first-child strong, .registry-verification > div:first-child span { display: block; }
1673
+ .registry-verification > div:first-child strong { font-size: 11px; }
1674
+ .registry-verification > div:first-child span { margin-top: 3px; font-size: 9px; }
1675
+ .registry-verification ul { margin: 11px 0 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 7px; list-style: none; }
1676
+ .registry-verification li { display: flex; align-items: center; gap: 7px; font-size: 9px; }
1677
+ .registry-verification li span { width: 18px; height: 18px; flex: 0 0 18px; display: grid; place-items: center; border-radius: 50%; color: #2f6845; background: #dceee1; font-size: 8px; font-weight: 900; }
1678
+ .registry-verification li.failed span { color: #9b512e; background: #ffe2cf; }
1679
+ .registry-verification-errors { margin-top: 10px; padding-top: 9px; border-top: 1px solid rgba(74,93,80,.14); }
1680
+ .registry-verification-errors p { margin: 3px 0; font-size: 9px; }
1681
+ .registry-probe-form { margin: 12px 0 10px; display: grid; grid-template-columns: 130px minmax(0,1fr); gap: 9px; }
1682
+ .registry-probe-form label { font-size: 9px; }
1683
+ .registry-probe-body { min-height: 88px; }
1684
+ .registry-count { min-width: 27px; padding: 5px 8px; border-radius: 999px; color: var(--forest); background: var(--mint); font-size: 10px; font-weight: 850; text-align: center; }
1685
+ .registry-dependency-list { margin: 12px 0 0; padding: 0; border: 1px solid var(--line); border-radius: 8px; list-style: none; overflow: hidden; }
1686
+ .registry-dependency-list li { padding: 9px 10px; display: grid; grid-template-columns: 70px minmax(0,1fr) auto; align-items: center; gap: 9px; border-top: 1px solid var(--line); }
1687
+ .registry-dependency-list li:first-child { border-top: 0; }
1688
+ .registry-dependency-list span, .registry-dependency-list small { color: var(--muted); font-size: 8px; }
1689
+ .registry-dependency-list strong { font-size: 9px; }
1690
+ .registry-lifecycle-section { border-color: #e4d7d3; background: #fffcfb; }
1691
+ .registry-lifecycle-actions { margin-top: 12px; display: flex; align-items: center; gap: 9px; }
1692
+ .danger-link { min-height: 38px; padding: 8px 12px; border: 1px solid #e4c6c2; border-radius: 8px; color: #973d36; background: #fff7f5; font-size: 10px; font-weight: 800; }
1693
+ .danger-link:hover { background: #ffefec; }
1694
+ .danger-link:disabled { cursor: not-allowed; opacity: .45; }
1695
+ .registry-blocked-copy { margin: 9px 0 0; color: #82645f; font-size: 9px; }
1696
+ .registry-archive-confirm { margin-top: 12px; padding: 12px; display: flex; align-items: center; justify-content: space-between; gap: 14px; border: 1px solid #e7c6c2; border-radius: 9px; background: #fff1ef; }
1697
+ .registry-archive-confirm strong { font-size: 11px; }
1698
+ .registry-archive-confirm p { margin: 4px 0 0; color: #7e4c47; font-size: 9px; }
1699
+ .registry-archive-confirm > div:last-child { flex: 0 0 auto; display: flex; gap: 7px; }
1700
+ button.danger { min-height: 38px; padding: 9px 13px; border-radius: 8px; color: white; background: #9f4039; font-size: 10px; font-weight: 800; }
1701
+ button.danger:hover { background: #84332e; }
1702
+
1703
+ .registry-wizard { width: min(900px, calc(100% - 28px)); overflow: hidden; }
1704
+ .registry-wizard .detail-head { position: static; }
1705
+ .registry-wizard .detail-head h2 { font-family: var(--sans); font-size: 21px; }
1706
+ .registry-wizard .detail-head p:last-child { font-size: 11px; text-transform: none; }
1707
+ .registry-wizard-steps { margin: 0; padding: 0 22px; display: grid; grid-template-columns: repeat(3,1fr); border-bottom: 1px solid var(--line); background: #fafbf9; list-style: none; }
1708
+ .registry-wizard-steps li { position: relative; padding: 13px 8px; display: flex; align-items: center; gap: 7px; color: #7d8981; border-bottom: 2px solid transparent; font-size: 10px; font-weight: 750; }
1709
+ .registry-wizard-steps li.current { color: var(--forest-dark); border-bottom-color: var(--forest); }
1710
+ .registry-wizard-steps li.complete { color: #47715a; }
1711
+ .registry-wizard-steps li span { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 50%; color: inherit; background: #edf1ed; font-size: 9px; }
1712
+ .registry-wizard-steps li.current span { color: white; background: var(--forest); }
1713
+ .registry-wizard .editor-form { padding: 0; gap: 0; }
1714
+ .registry-wizard-content { min-height: 470px; max-height: calc(100vh - 255px); padding: 22px; overflow-y: auto; }
1715
+ .registry-step-heading { margin-bottom: 16px; }
1716
+ .registry-step-heading h3 { margin: 0; font-size: 17px; }
1717
+ .registry-step-heading p { margin: 5px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
1718
+ .registry-purpose-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
1719
+ .registry-purpose-grid > button { padding: 14px; display: grid; grid-template-columns: 38px minmax(0,1fr); gap: 11px; border: 1px solid var(--line); border-radius: 10px; color: var(--ink); background: white; text-align: left; }
1720
+ .registry-purpose-grid > button:hover { border-color: #9db4a5; background: #fbfdfb; }
1721
+ .registry-purpose-grid > button.selected { border-color: #6e9f80; background: #f0f7f2; box-shadow: 0 0 0 2px rgba(63,127,87,.1); }
1722
+ .registry-purpose-grid i { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 9px; color: var(--forest); background: var(--mint); font-style: normal; font-size: 16px; }
1723
+ .registry-purpose-grid strong, .registry-purpose-grid small, .registry-purpose-grid em { display: block; }
1724
+ .registry-purpose-grid strong { font-size: 12px; }
1725
+ .registry-purpose-grid small { margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.4; }
1726
+ .registry-purpose-grid em { width: fit-content; margin-top: 8px; padding: 3px 6px; border-radius: 5px; color: #4f6256; background: #eef2ee; font-size: 8px; font-style: normal; font-weight: 750; }
1727
+ .registry-identity-fields { margin-top: 16px; }
1728
+ .registry-form label { font-size: 10px; }
1729
+ .registry-form label > small { margin-top: 5px; display: block; color: var(--muted); font-size: 9px; font-weight: 500; line-height: 1.4; }
1730
+ .registry-form input, .registry-form select, .registry-form textarea:not(.code-input) { margin-top: 5px; font-size: 11px; }
1731
+ .registry-action-boundary { color: #7e4740; border-color: #e6cbc7; background: #fff3f1; }
1732
+ .registry-action-boundary strong { color: #8b3731; }
1733
+ .registry-advanced { margin-top: 14px; padding: 11px 13px; border: 1px solid var(--line); border-radius: 9px; background: #fafbf9; }
1734
+ .registry-advanced summary { color: #41544a; cursor: pointer; font-size: 10px; font-weight: 800; }
1735
+ .registry-advanced > p { margin: 8px 0 12px; color: var(--muted); font-size: 9px; }
1736
+ .registry-review { display: grid; gap: 14px; }
1737
+ .registry-review dl { margin: 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
1738
+ .registry-review dl > div { padding: 12px 14px; display: grid; grid-template-columns: 150px minmax(0,1fr); gap: 12px; border-top: 1px solid var(--line); }
1739
+ .registry-review dl > div:first-child { border-top: 0; }
1740
+ .registry-review dt { color: var(--muted); font-size: 9px; }
1741
+ .registry-review dd { margin: 0; overflow-wrap: anywhere; font-size: 10px; font-weight: 750; text-transform: capitalize; }
1742
+ .registry-review-note { padding: 14px; border-radius: 10px; color: #315640; background: #eff7f1; }
1743
+ .registry-review-note strong { font-size: 11px; }
1744
+ .registry-review-note p { margin: 5px 0 0; font-size: 10px; line-height: 1.55; }
1745
+ .registry-secret-next { padding: 14px; border: 1px solid #d9d4e8; border-radius: 10px; color: #4f4969; background: #f7f5fc; }
1746
+ .registry-secret-next strong { font-size: 11px; }
1747
+ .registry-secret-next p { margin: 5px 0 9px; font-size: 10px; line-height: 1.5; }
1748
+ .registry-secret-next code { padding: 8px 10px; display: block; overflow-x: auto; border-radius: 7px; color: #35304c; background: white; font: 9px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
1749
+ .registry-wizard-footer { min-height: 70px; padding: 13px 22px; }
1750
+ .registry-wizard-footer .error { max-width: 500px; }
1751
+
1752
+ @media (max-width: 980px) {
1753
+ .registry-model { grid-template-columns: 1fr 1fr; }
1754
+ .registry-model > b { display: none; }
1755
+ .registry-toolbar { align-items: stretch; flex-wrap: wrap; }
1756
+ .registry-toolbar .search-field { flex-basis: 100%; }
1757
+ .registry-resource-row { grid-template-columns: 42px minmax(0,1fr) 90px auto; }
1758
+ .registry-resource-relationship { display: none; }
1759
+ }
1760
+
1761
+ @media (max-width: 700px) {
1762
+ .registry-model { grid-template-columns: 1fr; }
1763
+ .registry-tabs-row { align-items: flex-start; flex-direction: column; }
1764
+ .registry-filter-chips { width: 100%; overflow-x: auto; }
1765
+ .registry-archived-toggle { margin-left: 0; }
1766
+ .registry-resource-row { grid-template-columns: 38px minmax(0,1fr); align-items: start; }
1767
+ .registry-resource-state { grid-column: 2; }
1768
+ .registry-resource-row > button { width: 100%; grid-column: 2; justify-self: stretch; }
1769
+ .registry-detail-summary { grid-template-columns: 1fr 1fr; }
1770
+ .registry-section-heading { flex-direction: column; }
1771
+ .registry-section-heading button { width: 100%; }
1772
+ .registry-verification ul { grid-template-columns: 1fr; }
1773
+ .registry-archive-confirm { align-items: stretch; flex-direction: column; }
1774
+ .registry-purpose-grid { grid-template-columns: 1fr; }
1775
+ .registry-wizard-content { min-height: 0; }
1776
+ }
1777
+
1778
+ @media (max-width: 460px) {
1779
+ .registry-detail-dialog, .registry-wizard { width: 100%; max-height: 100vh; margin: 0; border: 0; border-radius: 0; }
1780
+ .registry-wizard-steps { padding-inline: 10px; }
1781
+ .registry-wizard-steps li { padding-inline: 4px; font-size: 9px; }
1782
+ .registry-wizard-content, .registry-detail-body { padding: 14px; }
1783
+ .registry-detail-summary { grid-template-columns: 1fr; }
1784
+ .registry-probe-form, .registry-review dl > div { grid-template-columns: 1fr; }
1785
+ .registry-lifecycle-actions, .registry-archive-confirm > div:last-child { align-items: stretch; flex-direction: column; }
1786
+ .registry-lifecycle-actions button, .registry-archive-confirm button { width: 100%; }
1787
+ .registry-form .form-grid { grid-template-columns: 1fr; }
1788
+ .registry-form .wide-field { grid-column: 1; }
1789
+ .registry-wizard-footer { align-items: stretch; flex-direction: column; }
1790
+ .registry-wizard-footer > div { width: 100%; display: grid; grid-template-columns: 1fr 1fr; }
1791
+ .registry-wizard-footer button { width: 100%; }
1792
+ }
@@ -281,10 +281,12 @@ export async function loadWorkflowExecutionContext(env: Env, runId: string, bran
281
281
  WHERE wr.id = ?1 LIMIT 1
282
282
  `).bind(runId).first<RunContextRow>();
283
283
  if (!row) throw new ComponentExecutionError("component_report_context_missing", false);
284
- const [targetResult, answersResult, stepsResult, findingsResult] = await Promise.all([
284
+ const [targetResult, reporterResult, answersResult, stepsResult, findingsResult] = await Promise.all([
285
285
  env.DB.prepare(`SELECT target_type, target_reference, owner_reference, customer_url, signed_facts_json FROM report_targets WHERE report_id = ?1 ORDER BY created_at, id LIMIT 50`).bind(row.reportId).all<{
286
286
  target_type: string; target_reference: string; owner_reference: string | null; customer_url: string | null; signed_facts_json: string;
287
287
  }>(),
288
+ env.DB.prepare(`SELECT reference FROM report_participants WHERE report_id = ?1 AND audience = 'reporter' ORDER BY created_at, id LIMIT 1`)
289
+ .bind(row.reportId).first<{ reference: string | null }>(),
288
290
  env.DB.prepare(`SELECT field_key, value_json FROM report_answers WHERE report_id = ?1 ORDER BY field_key LIMIT 200`).bind(row.reportId).all<{ field_key: string; value_json: string }>(),
289
291
  env.DB.prepare(`
290
292
  SELECT s.node_id, o.inline_json FROM workflow_step_runs s
@@ -327,6 +329,7 @@ export async function loadWorkflowExecutionContext(env: Env, runId: string, bran
327
329
  routing_rule_version_id: row.routingRuleVersionId,
328
330
  },
329
331
  queue: { id: row.queueId, version_id: row.queueVersionId },
332
+ reporter: { reference: reporterResult?.reference ?? null },
330
333
  target: targets[0] ?? null,
331
334
  targets,
332
335
  answers,
@@ -94,7 +94,9 @@ export async function listConnections(db: D1Database, includeArchived = false):
94
94
  v.credential_strategy AS credentialStrategy, v.published_at AS publishedAt,
95
95
  h.state AS healthState, h.last_success_at AS lastSuccessAt,
96
96
  h.last_error_code AS lastErrorCode,
97
- (SELECT COUNT(*) FROM component_versions cv WHERE cv.connection_version_id = v.id) AS dependencyCount
97
+ (SELECT COUNT(*) FROM component_versions cv
98
+ JOIN component_definitions cd ON cd.id = cv.definition_id
99
+ WHERE cv.connection_version_id = v.id AND cv.retired_at IS NULL AND cd.status != 'archived') AS dependencyCount
98
100
  FROM connection_definitions d
99
101
  LEFT JOIN connection_versions v ON v.id = d.active_version_id
100
102
  LEFT JOIN connection_health h ON h.connection_version_id = v.id
@@ -178,8 +180,10 @@ export async function updateConnectionDefinition(
178
180
  }
179
181
  if (status === "archived") {
180
182
  const dependency = await db.prepare(`
181
- SELECT COUNT(*) AS count FROM component_versions cv JOIN connection_versions v ON v.id = cv.connection_version_id
182
- WHERE v.connection_id = ?1 AND cv.retired_at IS NULL
183
+ SELECT COUNT(*) AS count FROM component_versions cv
184
+ JOIN component_definitions d ON d.id = cv.definition_id
185
+ JOIN connection_versions v ON v.id = cv.connection_version_id
186
+ WHERE v.connection_id = ?1 AND cv.retired_at IS NULL AND d.status != 'archived'
183
187
  `).bind(connectionId).first<{ count: number }>();
184
188
  if (Number(dependency?.count ?? 0) > 0) throw new ApiError(409, "connection_has_dependencies", "Retire dependent component versions before archiving this connection.");
185
189
  }
@@ -312,7 +316,7 @@ export async function connectionDependencies(db: D1Database, connectionId: strin
312
316
  SELECT d.id AS componentId, d.name, v.id AS componentVersionId, v.version
313
317
  FROM component_versions v JOIN component_definitions d ON d.id = v.definition_id
314
318
  JOIN connection_versions cv ON cv.id = v.connection_version_id
315
- WHERE cv.connection_id = ?1 AND v.retired_at IS NULL ORDER BY d.name, v.version DESC
319
+ WHERE cv.connection_id = ?1 AND v.retired_at IS NULL AND d.status != 'archived' ORDER BY d.name, v.version DESC
316
320
  `).bind(connectionId).all<Record<string, unknown>>(),
317
321
  db.prepare(`
318
322
  SELECT DISTINCT d.id AS workflowId, d.name, wv.id AS workflowVersionId, wv.version
@@ -320,7 +324,7 @@ export async function connectionDependencies(db: D1Database, connectionId: strin
320
324
  JOIN workflow_versions wv ON wv.id = dep.workflow_version_id
321
325
  JOIN workflow_definitions d ON d.id = wv.workflow_id
322
326
  JOIN connection_versions cv ON cv.id = dep.dependency_version_id
323
- WHERE dep.dependency_type = 'connection' AND cv.connection_id = ?1
327
+ WHERE dep.dependency_type = 'connection' AND cv.connection_id = ?1 AND wv.retired_at IS NULL
324
328
  ORDER BY d.name, wv.version DESC
325
329
  `).bind(connectionId).all<Record<string, unknown>>(),
326
330
  db.prepare(`
@@ -538,8 +542,10 @@ export async function updateComponentDefinition(
538
542
  }
539
543
  if (status === "archived") {
540
544
  const dependency = await db.prepare(`
541
- SELECT COUNT(*) AS count FROM workflow_dependencies dep JOIN component_versions v ON v.id = dep.dependency_version_id
542
- WHERE dep.dependency_type = 'component' AND v.definition_id = ?1
545
+ SELECT COUNT(*) AS count FROM workflow_dependencies dep
546
+ JOIN workflow_versions wv ON wv.id = dep.workflow_version_id
547
+ JOIN component_versions v ON v.id = dep.dependency_version_id
548
+ WHERE dep.dependency_type = 'component' AND v.definition_id = ?1 AND wv.retired_at IS NULL
543
549
  `).bind(componentId).first<{ count: number }>();
544
550
  if (Number(dependency?.count ?? 0) > 0) throw new ApiError(409, "component_has_dependencies", "Retire dependent workflow versions before archiving this component.");
545
551
  }
@@ -674,7 +680,7 @@ export async function componentDependencies(db: D1Database, componentId: string)
674
680
  JOIN workflow_versions wv ON wv.id = dep.workflow_version_id
675
681
  JOIN workflow_definitions d ON d.id = wv.workflow_id
676
682
  JOIN component_versions cv ON cv.id = dep.dependency_version_id
677
- WHERE dep.dependency_type = 'component' AND cv.definition_id = ?1
683
+ WHERE dep.dependency_type = 'component' AND cv.definition_id = ?1 AND wv.retired_at IS NULL
678
684
  ORDER BY d.name, wv.version DESC
679
685
  `).bind(componentId).all<Record<string, unknown>>();
680
686
  return { workflows: workflows.results };
@@ -685,10 +691,13 @@ export async function validateComponentDefinition(db: D1Database, componentId: s
685
691
  SELECT d.status, d.effect_class AS effectClass, d.active_version_id AS activeVersionId,
686
692
  v.implementation_kind AS implementationKind, v.implementation_reference AS implementationReference,
687
693
  v.input_schema_json AS inputSchemaJson, v.output_schema_json AS outputSchemaJson,
688
- v.connection_version_id AS connectionVersionId,
694
+ v.connection_version_id AS connectionVersionId, cd.status AS connectionStatus,
695
+ cv.retired_at AS connectionRetiredAt,
689
696
  (SELECT COUNT(*) FROM component_fixtures f WHERE f.component_version_id = v.id) AS fixtureCount,
690
697
  (SELECT COUNT(*) FROM component_test_runs t WHERE t.component_version_id = v.id AND t.status = 'passed') AS passingTestCount
691
698
  FROM component_definitions d LEFT JOIN component_versions v ON v.id = d.active_version_id
699
+ LEFT JOIN connection_versions cv ON cv.id = v.connection_version_id
700
+ LEFT JOIN connection_definitions cd ON cd.id = cv.connection_id
692
701
  WHERE d.id = ?1 LIMIT 1
693
702
  `).bind(componentId).first<Record<string, unknown>>();
694
703
  if (!row) throw new ApiError(404, "component_not_found", "The component does not exist.");
@@ -697,6 +706,7 @@ export async function validateComponentDefinition(db: D1Database, componentId: s
697
706
  if (!parsedJson(String(row.inputSchemaJson ?? ""), null)) errors.push("input_schema_invalid");
698
707
  if (!parsedJson(String(row.outputSchemaJson ?? ""), null)) errors.push("output_schema_invalid");
699
708
  if (["webhook", "external_api"].includes(String(row.implementationKind)) && !row.connectionVersionId) errors.push("connection_version_missing");
709
+ if (row.connectionVersionId && (row.connectionStatus !== "active" || row.connectionRetiredAt)) errors.push("connection_version_unavailable");
700
710
  if (Number(row.fixtureCount ?? 0) < 1) errors.push("fixture_missing");
701
711
  if (Number(row.passingTestCount ?? 0) < 1) errors.push("passing_test_missing");
702
712
  return { valid: errors.length === 0, errors, ...row };
@@ -101,7 +101,8 @@ export async function receiveReporterEmail(
101
101
  return { reportId: null, stored: false };
102
102
  }
103
103
  const report = await env.DB.prepare(`
104
- SELECT r.id, r.state, p.id AS participantId, p.contact_ciphertext AS contactCiphertext
104
+ SELECT r.id, r.state, p.id AS participantId, p.contact_ciphertext AS contactCiphertext,
105
+ p.audience AS audience
105
106
  FROM reports r
106
107
  JOIN report_participants p ON p.report_id = r.id AND p.audience = ?2
107
108
  WHERE r.public_reference = ?1
@@ -111,9 +112,15 @@ export async function receiveReporterEmail(
111
112
  await recordUnmatchedEmail(env.DB, message, "report_or_reporter_contact_missing");
112
113
  return { reportId: null, stored: false };
113
114
  }
114
- const expectedSender = (await decryptReportContact(
115
- env.CREDENTIAL_ENCRYPTION_KEY, report.contactCiphertext, report.id, report.audience,
116
- )).trim().toLowerCase();
115
+ let expectedSender: string;
116
+ try {
117
+ expectedSender = (await decryptReportContact(
118
+ env.CREDENTIAL_ENCRYPTION_KEY, report.contactCiphertext, report.id, report.audience,
119
+ )).trim().toLowerCase();
120
+ } catch {
121
+ await recordUnmatchedEmail(env.DB, message, "reporter_contact_unreadable");
122
+ return { reportId: report.id, stored: false };
123
+ }
117
124
  if (!await secretsEqual(message.from.trim().toLowerCase(), expectedSender)) {
118
125
  await recordUnmatchedEmail(env.DB, message, "sender_mismatch");
119
126
  return { reportId: null, stored: false };
@@ -67,7 +67,8 @@ export function reporterEmailConfigured(env: unknown): boolean {
67
67
  export type CaseReplyAudience = "reporter" | "affected_user";
68
68
 
69
69
  const legacyReplyTokenPattern = /^r1\.([a-z0-9_-]{16})\.([0-9a-f]{8})\.([0-9a-f]{24})$/u;
70
- const replyTokenPattern = /^r2([ra])\.([a-z0-9_-]{16})\.([0-9a-f]{8})\.([0-9a-f]{24})$/u;
70
+ const replyTokenPattern = /^r([ra])\.([a-z0-9_-]{16})\.([0-9a-f]{8})\.([0-9a-f]{24})$/u;
71
+ const transitionalReplyTokenPattern = /^r2([ra])\.([a-z0-9_-]{16})\.([0-9a-f]{8})\.([0-9a-f]{24})$/u;
71
72
  const encoder = new TextEncoder();
72
73
 
73
74
  function hex(bytes: Uint8Array): string {
@@ -98,7 +99,7 @@ export async function reporterReplyToken(input: {
98
99
  const expiry = input.expiresAt.toString(16).padStart(8, "0");
99
100
  if (!/^[0-9a-f]{8}$/u.test(expiry)) throw new Error("The reporter reply expiry is outside the supported range.");
100
101
  const audience = input.audience === "affected_user" ? "a" : "r";
101
- const payload = `r2${audience}.${reference}.${expiry}`;
102
+ const payload = `r${audience}.${reference}.${expiry}`;
102
103
  return `${payload}.${hex(await replySignature(input.signingSecret, payload))}`;
103
104
  }
104
105
 
@@ -108,21 +109,23 @@ export async function verifyReporterReplyToken(
108
109
  currentTime = Math.floor(Date.now() / 1_000),
109
110
  ): Promise<{ publicReference: string; expiresAt: number; audience: CaseReplyAudience } | null> {
110
111
  const current = replyTokenPattern.exec(token);
111
- const legacy = current ? null : legacyReplyTokenPattern.exec(token);
112
- if (!current && !legacy) return null;
113
- const reference = current?.[2] ?? legacy?.[1] ?? "";
114
- const expiryHex = current?.[3] ?? legacy?.[2] ?? "";
115
- const signatureHex = current?.[4] ?? legacy?.[3] ?? "";
112
+ const transitional = current ? null : transitionalReplyTokenPattern.exec(token);
113
+ const legacy = current || transitional ? null : legacyReplyTokenPattern.exec(token);
114
+ if (!current && !transitional && !legacy) return null;
115
+ const reference = current?.[2] ?? transitional?.[2] ?? legacy?.[1] ?? "";
116
+ const expiryHex = current?.[3] ?? transitional?.[3] ?? legacy?.[2] ?? "";
117
+ const signatureHex = current?.[4] ?? transitional?.[4] ?? legacy?.[3] ?? "";
116
118
  const expiry = Number.parseInt(expiryHex, 16);
117
119
  const supplied = Uint8Array.from(signatureHex.match(/.{2}/gu) ?? [], (pair) => Number.parseInt(pair, 16));
118
120
  if (!expiry || expiry <= currentTime) return null;
119
- const payload = current ? `r2${current[1]}.${reference}.${expiryHex}` : `r1.${reference}.${expiryHex}`;
121
+ const payload = current ? `r${current[1]}.${reference}.${expiryHex}`
122
+ : transitional ? `r2${transitional[1]}.${reference}.${expiryHex}` : `r1.${reference}.${expiryHex}`;
120
123
  const expected = await replySignature(signingSecret, payload);
121
124
  if (!constantTimeEqual(supplied, expected)) return null;
122
125
  return {
123
126
  publicReference: `RPT-${reference.toUpperCase()}`,
124
127
  expiresAt: expiry,
125
- audience: current?.[1] === "a" ? "affected_user" : "reporter",
128
+ audience: (current?.[1] ?? transitional?.[1]) === "a" ? "affected_user" : "reporter",
126
129
  };
127
130
  }
128
131
 
@@ -79,6 +79,7 @@ interface RoutingJobRow {
79
79
  customer_url: string | null;
80
80
  public_reference: string;
81
81
  installation_id: string;
82
+ reporter_reference: string | null;
82
83
  }
83
84
 
84
85
  interface QueueRoutingRow {
@@ -649,7 +650,10 @@ export async function processRoutingJob(
649
650
  SELECT j.id, j.report_id, j.state, r.reason_code, r.source, r.form_version_id,
650
651
  r.installation_id, r.public_reference,
651
652
  t.target_type, t.target_reference, t.owner_reference, t.customer_url,
652
- t.routing_facts_json AS trusted_facts_json, r.state AS report_state
653
+ t.routing_facts_json AS trusted_facts_json, r.state AS report_state,
654
+ (SELECT p.reference FROM report_participants p
655
+ WHERE p.report_id = r.id AND p.audience = 'reporter'
656
+ ORDER BY p.created_at, p.id LIMIT 1) AS reporter_reference
653
657
  FROM background_jobs j
654
658
  JOIN reports r ON r.id = j.report_id
655
659
  JOIN report_targets t ON t.report_id = r.id
@@ -716,6 +720,7 @@ export async function processRoutingJob(
716
720
  id: job.report_id, public_reference: job.public_reference, reason_code: job.reason_code,
717
721
  source: job.source, target_type: job.target_type, priority: 50,
718
722
  },
723
+ reporter: { reference: job.reporter_reference },
719
724
  target: {
720
725
  type: job.target_type, reference: job.target_reference,
721
726
  owner_reference: job.owner_reference, customer_url: job.customer_url,