ATProto Social Bookmark
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat : バックエンドのテストクラス

usounds d3c786ae a9f87cc9

+5575 -11
+224
backend/coverage/base.css
··· 1 + body, html { 2 + margin:0; padding: 0; 3 + height: 100%; 4 + } 5 + body { 6 + font-family: Helvetica Neue, Helvetica, Arial; 7 + font-size: 14px; 8 + color:#333; 9 + } 10 + .small { font-size: 12px; } 11 + *, *:after, *:before { 12 + -webkit-box-sizing:border-box; 13 + -moz-box-sizing:border-box; 14 + box-sizing:border-box; 15 + } 16 + h1 { font-size: 20px; margin: 0;} 17 + h2 { font-size: 14px; } 18 + pre { 19 + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 + margin: 0; 21 + padding: 0; 22 + -moz-tab-size: 2; 23 + -o-tab-size: 2; 24 + tab-size: 2; 25 + } 26 + a { color:#0074D9; text-decoration:none; } 27 + a:hover { text-decoration:underline; } 28 + .strong { font-weight: bold; } 29 + .space-top1 { padding: 10px 0 0 0; } 30 + .pad2y { padding: 20px 0; } 31 + .pad1y { padding: 10px 0; } 32 + .pad2x { padding: 0 20px; } 33 + .pad2 { padding: 20px; } 34 + .pad1 { padding: 10px; } 35 + .space-left2 { padding-left:55px; } 36 + .space-right2 { padding-right:20px; } 37 + .center { text-align:center; } 38 + .clearfix { display:block; } 39 + .clearfix:after { 40 + content:''; 41 + display:block; 42 + height:0; 43 + clear:both; 44 + visibility:hidden; 45 + } 46 + .fl { float: left; } 47 + @media only screen and (max-width:640px) { 48 + .col3 { width:100%; max-width:100%; } 49 + .hide-mobile { display:none!important; } 50 + } 51 + 52 + .quiet { 53 + color: #7f7f7f; 54 + color: rgba(0,0,0,0.5); 55 + } 56 + .quiet a { opacity: 0.7; } 57 + 58 + .fraction { 59 + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 + font-size: 10px; 61 + color: #555; 62 + background: #E8E8E8; 63 + padding: 4px 5px; 64 + border-radius: 3px; 65 + vertical-align: middle; 66 + } 67 + 68 + div.path a:link, div.path a:visited { color: #333; } 69 + table.coverage { 70 + border-collapse: collapse; 71 + margin: 10px 0 0 0; 72 + padding: 0; 73 + } 74 + 75 + table.coverage td { 76 + margin: 0; 77 + padding: 0; 78 + vertical-align: top; 79 + } 80 + table.coverage td.line-count { 81 + text-align: right; 82 + padding: 0 5px 0 20px; 83 + } 84 + table.coverage td.line-coverage { 85 + text-align: right; 86 + padding-right: 10px; 87 + min-width:20px; 88 + } 89 + 90 + table.coverage td span.cline-any { 91 + display: inline-block; 92 + padding: 0 5px; 93 + width: 100%; 94 + } 95 + .missing-if-branch { 96 + display: inline-block; 97 + margin-right: 5px; 98 + border-radius: 3px; 99 + position: relative; 100 + padding: 0 4px; 101 + background: #333; 102 + color: yellow; 103 + } 104 + 105 + .skip-if-branch { 106 + display: none; 107 + margin-right: 10px; 108 + position: relative; 109 + padding: 0 4px; 110 + background: #ccc; 111 + color: white; 112 + } 113 + .missing-if-branch .typ, .skip-if-branch .typ { 114 + color: inherit !important; 115 + } 116 + .coverage-summary { 117 + border-collapse: collapse; 118 + width: 100%; 119 + } 120 + .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 + .keyline-all { border: 1px solid #ddd; } 122 + .coverage-summary td, .coverage-summary th { padding: 10px; } 123 + .coverage-summary tbody { border: 1px solid #bbb; } 124 + .coverage-summary td { border-right: 1px solid #bbb; } 125 + .coverage-summary td:last-child { border-right: none; } 126 + .coverage-summary th { 127 + text-align: left; 128 + font-weight: normal; 129 + white-space: nowrap; 130 + } 131 + .coverage-summary th.file { border-right: none !important; } 132 + .coverage-summary th.pct { } 133 + .coverage-summary th.pic, 134 + .coverage-summary th.abs, 135 + .coverage-summary td.pct, 136 + .coverage-summary td.abs { text-align: right; } 137 + .coverage-summary td.file { white-space: nowrap; } 138 + .coverage-summary td.pic { min-width: 120px !important; } 139 + .coverage-summary tfoot td { } 140 + 141 + .coverage-summary .sorter { 142 + height: 10px; 143 + width: 7px; 144 + display: inline-block; 145 + margin-left: 0.5em; 146 + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 + } 148 + .coverage-summary .sorted .sorter { 149 + background-position: 0 -20px; 150 + } 151 + .coverage-summary .sorted-desc .sorter { 152 + background-position: 0 -10px; 153 + } 154 + .status-line { height: 10px; } 155 + /* yellow */ 156 + .cbranch-no { background: yellow !important; color: #111; } 157 + /* dark red */ 158 + .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 159 + .low .chart { border:1px solid #C21F39 } 160 + .highlighted, 161 + .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ 162 + background: #C21F39 !important; 163 + } 164 + /* medium red */ 165 + .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 166 + /* light red */ 167 + .low, .cline-no { background:#FCE1E5 } 168 + /* light green */ 169 + .high, .cline-yes { background:rgb(230,245,208) } 170 + /* medium green */ 171 + .cstat-yes { background:rgb(161,215,106) } 172 + /* dark green */ 173 + .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 174 + .high .chart { border:1px solid rgb(77,146,33) } 175 + /* dark yellow (gold) */ 176 + .status-line.medium, .medium .cover-fill { background: #f9cd0b; } 177 + .medium .chart { border:1px solid #f9cd0b; } 178 + /* light yellow */ 179 + .medium { background: #fff4c2; } 180 + 181 + .cstat-skip { background: #ddd; color: #111; } 182 + .fstat-skip { background: #ddd; color: #111 !important; } 183 + .cbranch-skip { background: #ddd !important; color: #111; } 184 + 185 + span.cline-neutral { background: #eaeaea; } 186 + 187 + .coverage-summary td.empty { 188 + opacity: .5; 189 + padding-top: 4px; 190 + padding-bottom: 4px; 191 + line-height: 1; 192 + color: #888; 193 + } 194 + 195 + .cover-fill, .cover-empty { 196 + display:inline-block; 197 + height: 12px; 198 + } 199 + .chart { 200 + line-height: 0; 201 + } 202 + .cover-empty { 203 + background: white; 204 + } 205 + .cover-full { 206 + border-right: none !important; 207 + } 208 + pre.prettyprint { 209 + border: none !important; 210 + padding: 0 !important; 211 + margin: 0 !important; 212 + } 213 + .com { color: #999 !important; } 214 + .ignore-none { color: #999; font-weight: normal; } 215 + 216 + .wrapper { 217 + min-height: 100%; 218 + height: auto !important; 219 + height: 100%; 220 + margin: 0 auto -48px; 221 + } 222 + .footer, .push { 223 + height: 48px; 224 + }
+87
backend/coverage/block-navigation.js
··· 1 + /* eslint-disable */ 2 + var jumpToCode = (function init() { 3 + // Classes of code we would like to highlight in the file view 4 + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 + 6 + // Elements to highlight in the file listing view 7 + var fileListingElements = ['td.pct.low']; 8 + 9 + // We don't want to select elements that are direct descendants of another match 10 + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 + 12 + // Selector that finds elements on the page to which we can jump 13 + var selector = 14 + fileListingElements.join(', ') + 15 + ', ' + 16 + notSelector + 17 + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 + 19 + // The NodeList of matching elements 20 + var missingCoverageElements = document.querySelectorAll(selector); 21 + 22 + var currentIndex; 23 + 24 + function toggleClass(index) { 25 + missingCoverageElements 26 + .item(currentIndex) 27 + .classList.remove('highlighted'); 28 + missingCoverageElements.item(index).classList.add('highlighted'); 29 + } 30 + 31 + function makeCurrent(index) { 32 + toggleClass(index); 33 + currentIndex = index; 34 + missingCoverageElements.item(index).scrollIntoView({ 35 + behavior: 'smooth', 36 + block: 'center', 37 + inline: 'center' 38 + }); 39 + } 40 + 41 + function goToPrevious() { 42 + var nextIndex = 0; 43 + if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 + nextIndex = missingCoverageElements.length - 1; 45 + } else if (missingCoverageElements.length > 1) { 46 + nextIndex = currentIndex - 1; 47 + } 48 + 49 + makeCurrent(nextIndex); 50 + } 51 + 52 + function goToNext() { 53 + var nextIndex = 0; 54 + 55 + if ( 56 + typeof currentIndex === 'number' && 57 + currentIndex < missingCoverageElements.length - 1 58 + ) { 59 + nextIndex = currentIndex + 1; 60 + } 61 + 62 + makeCurrent(nextIndex); 63 + } 64 + 65 + return function jump(event) { 66 + if ( 67 + document.getElementById('fileSearch') === document.activeElement && 68 + document.activeElement != null 69 + ) { 70 + // if we're currently focused on the search input, we don't want to navigate 71 + return; 72 + } 73 + 74 + switch (event.which) { 75 + case 78: // n 76 + case 74: // j 77 + goToNext(); 78 + break; 79 + case 66: // b 80 + case 75: // k 81 + case 80: // p 82 + goToPrevious(); 83 + break; 84 + } 85 + }; 86 + })(); 87 + window.addEventListener('keydown', jumpToCode);
+109
backend/coverage/config.ts.html
··· 1 + 2 + <!doctype html> 3 + <html lang="en"> 4 + 5 + <head> 6 + <title>Code coverage report for config.ts</title> 7 + <meta charset="utf-8" /> 8 + <link rel="stylesheet" href="prettify.css" /> 9 + <link rel="stylesheet" href="base.css" /> 10 + <link rel="shortcut icon" type="image/x-icon" href="favicon.png" /> 11 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 12 + <style type='text/css'> 13 + .coverage-summary .sorter { 14 + background-image: url(sort-arrow-sprite.png); 15 + } 16 + </style> 17 + </head> 18 + 19 + <body> 20 + <div class='wrapper'> 21 + <div class='pad1'> 22 + <h1><a href="index.html">All files</a> config.ts</h1> 23 + <div class='clearfix'> 24 + 25 + <div class='fl pad1y space-right2'> 26 + <span class="strong">100% </span> 27 + <span class="quiet">Statements</span> 28 + <span class='fraction'>6/6</span> 29 + </div> 30 + 31 + 32 + <div class='fl pad1y space-right2'> 33 + <span class="strong">75% </span> 34 + <span class="quiet">Branches</span> 35 + <span class='fraction'>3/4</span> 36 + </div> 37 + 38 + 39 + <div class='fl pad1y space-right2'> 40 + <span class="strong">100% </span> 41 + <span class="quiet">Functions</span> 42 + <span class='fraction'>0/0</span> 43 + </div> 44 + 45 + 46 + <div class='fl pad1y space-right2'> 47 + <span class="strong">100% </span> 48 + <span class="quiet">Lines</span> 49 + <span class='fraction'>6/6</span> 50 + </div> 51 + 52 + 53 + </div> 54 + <p class="quiet"> 55 + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. 56 + </p> 57 + <template id="filterTemplate"> 58 + <div class="quiet"> 59 + Filter: 60 + <input type="search" id="fileSearch"> 61 + </div> 62 + </template> 63 + </div> 64 + <div class='status-line high'></div> 65 + <pre><table class="coverage"> 66 + <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> 67 + <a name='L2'></a><a href='#L2'>2</a> 68 + <a name='L3'></a><a href='#L3'>3</a> 69 + <a name='L4'></a><a href='#L4'>4</a> 70 + <a name='L5'></a><a href='#L5'>5</a> 71 + <a name='L6'></a><a href='#L6'>6</a> 72 + <a name='L7'></a><a href='#L7'>7</a> 73 + <a name='L8'></a><a href='#L8'>8</a> 74 + <a name='L9'></a><a href='#L9'>9</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span> 75 + <span class="cline-any cline-neutral">&nbsp;</span> 76 + <span class="cline-any cline-yes">6x</span> 77 + <span class="cline-any cline-yes">6x</span> 78 + <span class="cline-any cline-yes">6x</span> 79 + <span class="cline-any cline-yes">6x</span> 80 + <span class="cline-any cline-yes">6x</span> 81 + <span class="cline-any cline-neutral">&nbsp;</span> 82 + <span class="cline-any cline-yes">6x</span></td><td class="text"><pre class="prettyprint lang-js">import 'dotenv/config'; 83 + &nbsp; 84 + export const JETSREAM_URL = process.env.JETSREAM_URL ?? 'wss://jetstream2.us-west.bsky.network/subscribe'; 85 + export const BOOKMARK = 'blue.rito.feed.bookmark' 86 + export const LIKE = 'blue.rito.feed.like' 87 + export const SERVICE = 'blue.rito.service.schema' 88 + export const POST_COLLECTION = "app.bsky.feed.post" as const; 89 + export const CURSOR_UPDATE_INTERVAL = 90 + process.env.CURSOR_UPDATE_INTERVAL ? <span class="branch-0 cbranch-no" title="branch not covered" >Number(process.env.CURSOR_UPDATE_INTERVAL) : 6</span>0000;</pre></td></tr></table></pre> 91 + 92 + <div class='push'></div><!-- for sticky footer --> 93 + </div><!-- /wrapper --> 94 + <div class='footer quiet pad2 space-top1 center small'> 95 + Code coverage generated by 96 + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> 97 + at 2026-01-21T23:33:32.350Z 98 + </div> 99 + <script src="prettify.js"></script> 100 + <script> 101 + window.onload = function () { 102 + prettyPrint(); 103 + }; 104 + </script> 105 + <script src="sorter.js"></script> 106 + <script src="block-navigation.js"></script> 107 + </body> 108 + </html> 109 +
+4
backend/coverage/coverage-final.json
··· 1 + {"/Users/usounds/Program/Rito/backend/src/config.ts": {"path":"/Users/usounds/Program/Rito/backend/src/config.ts","statementMap":{"0":{"start":{"line":3,"column":28},"end":{"line":3,"column":null}},"1":{"start":{"line":4,"column":24},"end":{"line":4,"column":null}},"2":{"start":{"line":5,"column":20},"end":{"line":5,"column":null}},"3":{"start":{"line":6,"column":23},"end":{"line":6,"column":null}},"4":{"start":{"line":7,"column":31},"end":{"line":7,"column":null}},"5":{"start":{"line":9,"column":2},"end":{"line":9,"column":null}}},"fnMap":{},"branchMap":{"0":{"loc":{"start":{"line":3,"column":28},"end":{"line":3,"column":null}},"type":"binary-expr","locations":[{"start":{"line":3,"column":28},"end":{"line":3,"column":56}},{"start":{"line":3,"column":56},"end":{"line":3,"column":null}}],"line":3},"1":{"loc":{"start":{"line":9,"column":2},"end":{"line":9,"column":null}},"type":"cond-expr","locations":[{"start":{"line":9,"column":39},"end":{"line":9,"column":84}},{"start":{"line":9,"column":84},"end":{"line":9,"column":null}}],"line":9}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6},"f":{},"b":{"0":[6,5],"1":[0,6]},"meta":{"lastBranch":2,"lastFunction":0,"lastStatement":6,"seen":{"s:3:28:3:Infinity":0,"b:3:28:3:56:3:56:3:Infinity":0,"s:4:24:4:Infinity":1,"s:5:20:5:Infinity":2,"s:6:23:6:Infinity":3,"s:7:31:7:Infinity":4,"s:9:2:9:Infinity":5,"b:9:39:9:84:9:84:9:Infinity":1}}} 2 + ,"/Users/usounds/Program/Rito/backend/src/logger.ts": {"path":"/Users/usounds/Program/Rito/backend/src/logger.ts","statementMap":{"0":{"start":{"line":3,"column":6},"end":{"line":16,"column":null}}},"fnMap":{},"branchMap":{"0":{"loc":{"start":{"line":4,"column":11},"end":{"line":4,"column":null}},"type":"binary-expr","locations":[{"start":{"line":4,"column":11},"end":{"line":4,"column":36}},{"start":{"line":4,"column":36},"end":{"line":4,"column":null}}],"line":4}},"s":{"0":1},"f":{},"b":{"0":[1,1]},"meta":{"lastBranch":1,"lastFunction":0,"lastStatement":1,"seen":{"s:3:6:16:Infinity":0,"b:4:11:4:36:4:36:4:Infinity":0}}} 3 + ,"/Users/usounds/Program/Rito/backend/src/utils.ts": {"path":"/Users/usounds/Program/Rito/backend/src/utils.ts","statementMap":{"0":{"start":{"line":59,"column":4},"end":{"line":59,"column":null}},"1":{"start":{"line":66,"column":4},"end":{"line":86,"column":null}},"2":{"start":{"line":67,"column":18},"end":{"line":67,"column":null}},"3":{"start":{"line":70,"column":8},"end":{"line":70,"column":null}},"4":{"start":{"line":70,"column":42},"end":{"line":70,"column":null}},"5":{"start":{"line":73,"column":22},"end":{"line":73,"column":null}},"6":{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},"7":{"start":{"line":76,"column":30},"end":{"line":76,"column":null}},"8":{"start":{"line":79,"column":8},"end":{"line":81,"column":null}},"9":{"start":{"line":80,"column":12},"end":{"line":80,"column":null}},"10":{"start":{"line":83,"column":8},"end":{"line":83,"column":null}},"11":{"start":{"line":85,"column":8},"end":{"line":85,"column":null}},"12":{"start":{"line":93,"column":17},"end":{"line":93,"column":null}},"13":{"start":{"line":96,"column":4},"end":{"line":96,"column":null}},"14":{"start":{"line":99,"column":4},"end":{"line":102,"column":null}},"15":{"start":{"line":105,"column":4},"end":{"line":109,"column":null}},"16":{"start":{"line":111,"column":4},"end":{"line":111,"column":null}},"17":{"start":{"line":118,"column":4},"end":{"line":118,"column":null}},"18":{"start":{"line":125,"column":4},"end":{"line":136,"column":null}},"19":{"start":{"line":126,"column":20},"end":{"line":126,"column":null}},"20":{"start":{"line":127,"column":23},"end":{"line":127,"column":null}},"21":{"start":{"line":129,"column":8},"end":{"line":132,"column":null}},"22":{"start":{"line":131,"column":12},"end":{"line":131,"column":null}},"23":{"start":{"line":133,"column":8},"end":{"line":133,"column":null}},"24":{"start":{"line":135,"column":8},"end":{"line":135,"column":null}},"25":{"start":{"line":144,"column":20},"end":{"line":144,"column":null}},"26":{"start":{"line":145,"column":21},"end":{"line":145,"column":null}},"27":{"start":{"line":146,"column":4},"end":{"line":146,"column":null}},"28":{"start":{"line":154,"column":4},"end":{"line":154,"column":null}},"29":{"start":{"line":161,"column":4},"end":{"line":161,"column":null}},"30":{"start":{"line":168,"column":8},"end":{"line":170,"column":null}},"31":{"start":{"line":169,"column":34},"end":{"line":169,"column":64}},"32":{"start":{"line":170,"column":34},"end":{"line":170,"column":67}},"33":{"start":{"line":172,"column":4},"end":{"line":174,"column":null}},"34":{"start":{"line":173,"column":8},"end":{"line":173,"column":null}},"35":{"start":{"line":176,"column":4},"end":{"line":176,"column":null}},"36":{"start":{"line":184,"column":18},"end":{"line":184,"column":null}},"37":{"start":{"line":185,"column":4},"end":{"line":185,"column":null}},"38":{"start":{"line":192,"column":28},"end":{"line":192,"column":null}},"39":{"start":{"line":194,"column":4},"end":{"line":196,"column":null}},"40":{"start":{"line":195,"column":8},"end":{"line":195,"column":null}},"41":{"start":{"line":198,"column":4},"end":{"line":198,"column":null}},"42":{"start":{"line":198,"column":63},"end":{"line":198,"column":66}},"43":{"start":{"line":205,"column":27},"end":{"line":205,"column":null}},"44":{"start":{"line":207,"column":4},"end":{"line":217,"column":null}},"45":{"start":{"line":208,"column":8},"end":{"line":216,"column":null}},"46":{"start":{"line":209,"column":12},"end":{"line":215,"column":null}},"47":{"start":{"line":210,"column":16},"end":{"line":214,"column":null}},"48":{"start":{"line":211,"column":20},"end":{"line":213,"column":null}},"49":{"start":{"line":212,"column":24},"end":{"line":212,"column":null}},"50":{"start":{"line":219,"column":4},"end":{"line":219,"column":null}},"51":{"start":{"line":226,"column":4},"end":{"line":228,"column":null}},"52":{"start":{"line":227,"column":8},"end":{"line":227,"column":null}},"53":{"start":{"line":230,"column":4},"end":{"line":232,"column":null}},"54":{"start":{"line":231,"column":8},"end":{"line":231,"column":null}},"55":{"start":{"line":234,"column":4},"end":{"line":234,"column":null}},"56":{"start":{"line":241,"column":4},"end":{"line":246,"column":null}},"57":{"start":{"line":242,"column":20},"end":{"line":242,"column":null}},"58":{"start":{"line":243,"column":8},"end":{"line":243,"column":null}},"59":{"start":{"line":245,"column":8},"end":{"line":245,"column":null}}},"fnMap":{"0":{"name":"epochUsToDateTime","decl":{"start":{"line":58,"column":16},"end":{"line":58,"column":34}},"loc":{"start":{"line":58,"column":67},"end":{"line":60,"column":null}},"line":58},"1":{"name":"isValidTangledUrl","decl":{"start":{"line":65,"column":16},"end":{"line":65,"column":34}},"loc":{"start":{"line":65,"column":80},"end":{"line":87,"column":null}},"line":65},"2":{"name":"normalizeComment","decl":{"start":{"line":92,"column":16},"end":{"line":92,"column":33}},"loc":{"start":{"line":92,"column":55},"end":{"line":112,"column":null}},"line":92},"3":{"name":"extractHandleFromDidDoc","decl":{"start":{"line":117,"column":16},"end":{"line":117,"column":40}},"loc":{"start":{"line":117,"column":107},"end":{"line":119,"column":null}},"line":117},"4":{"name":"checkDomainVerification","decl":{"start":{"line":124,"column":16},"end":{"line":124,"column":40}},"loc":{"start":{"line":124,"column":82},"end":{"line":137,"column":null}},"line":124},"5":{"name":"parseTxtRecordForDid","decl":{"start":{"line":142,"column":16},"end":{"line":142,"column":37}},"loc":{"start":{"line":142,"column":69},"end":{"line":147,"column":null}},"line":142},"6":{"name":"reverseHandleToNsid","decl":{"start":{"line":153,"column":16},"end":{"line":153,"column":36}},"loc":{"start":{"line":153,"column":60},"end":{"line":155,"column":null}},"line":153},"7":{"name":"buildAtUri","decl":{"start":{"line":160,"column":16},"end":{"line":160,"column":27}},"loc":{"start":{"line":160,"column":82},"end":{"line":162,"column":null}},"line":160},"8":{"name":"normalizeTagsArray","decl":{"start":{"line":167,"column":16},"end":{"line":167,"column":35}},"loc":{"start":{"line":167,"column":97},"end":{"line":177,"column":null}},"line":167},"9":{"name":"(anonymous_9)","decl":{"start":{"line":169,"column":16},"end":{"line":169,"column":17}},"loc":{"start":{"line":169,"column":34},"end":{"line":169,"column":64}},"line":169},"10":{"name":"(anonymous_10)","decl":{"start":{"line":170,"column":16},"end":{"line":170,"column":17}},"loc":{"start":{"line":170,"column":34},"end":{"line":170,"column":67}},"line":170},"11":{"name":"buildDnsTxtSubdomain","decl":{"start":{"line":183,"column":16},"end":{"line":183,"column":37}},"loc":{"start":{"line":183,"column":59},"end":{"line":186,"column":null}},"line":183},"12":{"name":"extractLinksFromPost","decl":{"start":{"line":191,"column":16},"end":{"line":191,"column":37}},"loc":{"start":{"line":191,"column":60},"end":{"line":199,"column":null}},"line":191},"13":{"name":"(anonymous_13)","decl":{"start":{"line":198,"column":43},"end":{"line":198,"column":44}},"loc":{"start":{"line":198,"column":63},"end":{"line":198,"column":66}},"line":198},"14":{"name":"extractTagsFromFacets","decl":{"start":{"line":204,"column":16},"end":{"line":204,"column":38}},"loc":{"start":{"line":204,"column":63},"end":{"line":220,"column":null}},"line":204},"15":{"name":"shouldProcessAsRitoPost","decl":{"start":{"line":225,"column":16},"end":{"line":225,"column":40}},"loc":{"start":{"line":225,"column":79},"end":{"line":235,"column":null}},"line":225},"16":{"name":"parseDomainFromUrl","decl":{"start":{"line":240,"column":16},"end":{"line":240,"column":35}},"loc":{"start":{"line":240,"column":69},"end":{"line":247,"column":null}},"line":240}},"branchMap":{"0":{"loc":{"start":{"line":70,"column":8},"end":{"line":70,"column":null}},"type":"if","locations":[{"start":{"line":70,"column":8},"end":{"line":70,"column":null}},{"start":{},"end":{}}],"line":70},"1":{"loc":{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},"type":"if","locations":[{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},{"start":{},"end":{}}],"line":76},"2":{"loc":{"start":{"line":79,"column":8},"end":{"line":81,"column":null}},"type":"if","locations":[{"start":{"line":79,"column":8},"end":{"line":81,"column":null}},{"start":{},"end":{}}],"line":79},"3":{"loc":{"start":{"line":79,"column":12},"end":{"line":79,"column":78}},"type":"binary-expr","locations":[{"start":{"line":79,"column":12},"end":{"line":79,"column":43}},{"start":{"line":79,"column":43},"end":{"line":79,"column":78}}],"line":79},"4":{"loc":{"start":{"line":117,"column":62},"end":{"line":117,"column":107}},"type":"default-arg","locations":[{"start":{"line":117,"column":86},"end":{"line":117,"column":107}}],"line":117},"5":{"loc":{"start":{"line":118,"column":11},"end":{"line":118,"column":null}},"type":"binary-expr","locations":[{"start":{"line":118,"column":11},"end":{"line":118,"column":64}},{"start":{"line":118,"column":64},"end":{"line":118,"column":null}}],"line":118},"6":{"loc":{"start":{"line":129,"column":8},"end":{"line":132,"column":null}},"type":"if","locations":[{"start":{"line":129,"column":8},"end":{"line":132,"column":null}},{"start":{},"end":{}}],"line":129},"7":{"loc":{"start":{"line":129,"column":8},"end":{"line":130,"column":66}},"type":"binary-expr","locations":[{"start":{"line":129,"column":13},"end":{"line":129,"column":37}},{"start":{"line":129,"column":37},"end":{"line":129,"column":null}},{"start":{"line":130,"column":13},"end":{"line":130,"column":34}},{"start":{"line":130,"column":34},"end":{"line":130,"column":66}}],"line":129},"8":{"loc":{"start":{"line":146,"column":11},"end":{"line":146,"column":null}},"type":"cond-expr","locations":[{"start":{"line":146,"column":22},"end":{"line":146,"column":36}},{"start":{"line":146,"column":36},"end":{"line":146,"column":null}}],"line":146},"9":{"loc":{"start":{"line":167,"column":51},"end":{"line":167,"column":97}},"type":"default-arg","locations":[{"start":{"line":167,"column":80},"end":{"line":167,"column":97}}],"line":167},"10":{"loc":{"start":{"line":168,"column":18},"end":{"line":168,"column":null}},"type":"binary-expr","locations":[{"start":{"line":168,"column":18},"end":{"line":168,"column":26}},{"start":{"line":168,"column":26},"end":{"line":168,"column":null}}],"line":168},"11":{"loc":{"start":{"line":169,"column":34},"end":{"line":169,"column":64}},"type":"binary-expr","locations":[{"start":{"line":169,"column":34},"end":{"line":169,"column":42}},{"start":{"line":169,"column":42},"end":{"line":169,"column":64}}],"line":169},"12":{"loc":{"start":{"line":172,"column":4},"end":{"line":174,"column":null}},"type":"if","locations":[{"start":{"line":172,"column":4},"end":{"line":174,"column":null}},{"start":{},"end":{}}],"line":172},"13":{"loc":{"start":{"line":194,"column":4},"end":{"line":196,"column":null}},"type":"if","locations":[{"start":{"line":194,"column":4},"end":{"line":196,"column":null}},{"start":{},"end":{}}],"line":194},"14":{"loc":{"start":{"line":194,"column":8},"end":{"line":194,"column":89}},"type":"binary-expr","locations":[{"start":{"line":194,"column":8},"end":{"line":194,"column":61}},{"start":{"line":194,"column":61},"end":{"line":194,"column":89}}],"line":194},"15":{"loc":{"start":{"line":207,"column":4},"end":{"line":217,"column":null}},"type":"if","locations":[{"start":{"line":207,"column":4},"end":{"line":217,"column":null}},{"start":{},"end":{}}],"line":207},"16":{"loc":{"start":{"line":209,"column":12},"end":{"line":215,"column":null}},"type":"if","locations":[{"start":{"line":209,"column":12},"end":{"line":215,"column":null}},{"start":{},"end":{}}],"line":209},"17":{"loc":{"start":{"line":211,"column":20},"end":{"line":213,"column":null}},"type":"if","locations":[{"start":{"line":211,"column":20},"end":{"line":213,"column":null}},{"start":{},"end":{}}],"line":211},"18":{"loc":{"start":{"line":211,"column":24},"end":{"line":211,"column":88}},"type":"binary-expr","locations":[{"start":{"line":211,"column":24},"end":{"line":211,"column":75}},{"start":{"line":211,"column":75},"end":{"line":211,"column":88}}],"line":211},"19":{"loc":{"start":{"line":226,"column":4},"end":{"line":228,"column":null}},"type":"if","locations":[{"start":{"line":226,"column":4},"end":{"line":228,"column":null}},{"start":{},"end":{}}],"line":226},"20":{"loc":{"start":{"line":230,"column":4},"end":{"line":232,"column":null}},"type":"if","locations":[{"start":{"line":230,"column":4},"end":{"line":232,"column":null}},{"start":{},"end":{}}],"line":230},"21":{"loc":{"start":{"line":230,"column":8},"end":{"line":230,"column":40}},"type":"binary-expr","locations":[{"start":{"line":230,"column":8},"end":{"line":230,"column":24}},{"start":{"line":230,"column":24},"end":{"line":230,"column":40}}],"line":230}},"s":{"0":4,"1":8,"2":8,"3":8,"4":1,"5":5,"6":5,"7":1,"8":4,"9":1,"10":3,"11":2,"12":10,"13":10,"14":10,"15":10,"16":10,"17":4,"18":6,"19":6,"20":6,"21":6,"22":3,"23":2,"24":1,"25":5,"26":5,"27":5,"28":4,"29":2,"30":4,"31":10,"32":8,"33":4,"34":1,"35":4,"36":2,"37":2,"38":4,"39":4,"40":2,"41":4,"42":2,"43":4,"44":4,"45":3,"46":2,"47":2,"48":4,"49":3,"50":4,"51":5,"52":1,"53":4,"54":2,"55":2,"56":4,"57":4,"58":4,"59":2},"f":{"0":4,"1":8,"2":10,"3":4,"4":6,"5":5,"6":4,"7":2,"8":4,"9":10,"10":8,"11":2,"12":4,"13":2,"14":4,"15":5,"16":4},"b":{"0":[1,7],"1":[1,4],"2":[1,3],"3":[4,3],"4":[4],"5":[4,3],"6":[3,3],"7":[6,1,4,2],"8":[3,2],"9":[4],"10":[4,0],"11":[10,9],"12":[1,3],"13":[2,2],"14":[4,2],"15":[3,1],"16":[2,0],"17":[3,1],"18":[4,3],"19":[1,4],"20":[2,2],"21":[4,3]},"meta":{"lastBranch":22,"lastFunction":17,"lastStatement":60,"seen":{"f:58:16:58:34":0,"s:59:4:59:Infinity":0,"f:65:16:65:34":1,"s:66:4:86:Infinity":1,"s:67:18:67:Infinity":2,"b:70:8:70:Infinity:undefined:undefined:undefined:undefined":0,"s:70:8:70:Infinity":3,"s:70:42:70:Infinity":4,"s:73:22:73:Infinity":5,"b:76:8:76:Infinity:undefined:undefined:undefined:undefined":1,"s:76:8:76:Infinity":6,"s:76:30:76:Infinity":7,"b:79:8:81:Infinity:undefined:undefined:undefined:undefined":2,"s:79:8:81:Infinity":8,"b:79:12:79:43:79:43:79:78":3,"s:80:12:80:Infinity":9,"s:83:8:83:Infinity":10,"s:85:8:85:Infinity":11,"f:92:16:92:33":2,"s:93:17:93:Infinity":12,"s:96:4:96:Infinity":13,"s:99:4:102:Infinity":14,"s:105:4:109:Infinity":15,"s:111:4:111:Infinity":16,"f:117:16:117:40":3,"b:117:86:117:107":4,"s:118:4:118:Infinity":17,"b:118:11:118:64:118:64:118:Infinity":5,"f:124:16:124:40":4,"s:125:4:136:Infinity":18,"s:126:20:126:Infinity":19,"s:127:23:127:Infinity":20,"b:129:8:132:Infinity:undefined:undefined:undefined:undefined":6,"s:129:8:132:Infinity":21,"b:129:13:129:37:129:37:129:Infinity:130:13:130:34:130:34:130:66":7,"s:131:12:131:Infinity":22,"s:133:8:133:Infinity":23,"s:135:8:135:Infinity":24,"f:142:16:142:37":5,"s:144:20:144:Infinity":25,"s:145:21:145:Infinity":26,"s:146:4:146:Infinity":27,"b:146:22:146:36:146:36:146:Infinity":8,"f:153:16:153:36":6,"s:154:4:154:Infinity":28,"f:160:16:160:27":7,"s:161:4:161:Infinity":29,"f:167:16:167:35":8,"b:167:80:167:97":9,"s:168:8:170:Infinity":30,"b:168:18:168:26:168:26:168:Infinity":10,"f:169:16:169:17":9,"s:169:34:169:64":31,"b:169:34:169:42:169:42:169:64":11,"f:170:16:170:17":10,"s:170:34:170:67":32,"b:172:4:174:Infinity:undefined:undefined:undefined:undefined":12,"s:172:4:174:Infinity":33,"s:173:8:173:Infinity":34,"s:176:4:176:Infinity":35,"f:183:16:183:37":11,"s:184:18:184:Infinity":36,"s:185:4:185:Infinity":37,"f:191:16:191:37":12,"s:192:28:192:Infinity":38,"b:194:4:196:Infinity:undefined:undefined:undefined:undefined":13,"s:194:4:196:Infinity":39,"b:194:8:194:61:194:61:194:89":14,"s:195:8:195:Infinity":40,"s:198:4:198:Infinity":41,"f:198:43:198:44":13,"s:198:63:198:66":42,"f:204:16:204:38":14,"s:205:27:205:Infinity":43,"b:207:4:217:Infinity:undefined:undefined:undefined:undefined":15,"s:207:4:217:Infinity":44,"s:208:8:216:Infinity":45,"b:209:12:215:Infinity:undefined:undefined:undefined:undefined":16,"s:209:12:215:Infinity":46,"s:210:16:214:Infinity":47,"b:211:20:213:Infinity:undefined:undefined:undefined:undefined":17,"s:211:20:213:Infinity":48,"b:211:24:211:75:211:75:211:88":18,"s:212:24:212:Infinity":49,"s:219:4:219:Infinity":50,"f:225:16:225:40":15,"b:226:4:228:Infinity:undefined:undefined:undefined:undefined":19,"s:226:4:228:Infinity":51,"s:227:8:227:Infinity":52,"b:230:4:232:Infinity:undefined:undefined:undefined:undefined":20,"s:230:4:232:Infinity":53,"b:230:8:230:24:230:24:230:40":21,"s:231:8:231:Infinity":54,"s:234:4:234:Infinity":55,"f:240:16:240:35":16,"s:241:4:246:Infinity":56,"s:242:20:242:Infinity":57,"s:243:8:243:Infinity":58,"s:245:8:245:Infinity":59}}} 4 + }
backend/coverage/favicon.png

This is a binary file and will not be displayed.

+146
backend/coverage/index.html
··· 1 + 2 + <!doctype html> 3 + <html lang="en"> 4 + 5 + <head> 6 + <title>Code coverage report for All files</title> 7 + <meta charset="utf-8" /> 8 + <link rel="stylesheet" href="prettify.css" /> 9 + <link rel="stylesheet" href="base.css" /> 10 + <link rel="shortcut icon" type="image/x-icon" href="favicon.png" /> 11 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 12 + <style type='text/css'> 13 + .coverage-summary .sorter { 14 + background-image: url(sort-arrow-sprite.png); 15 + } 16 + </style> 17 + </head> 18 + 19 + <body> 20 + <div class='wrapper'> 21 + <div class='pad1'> 22 + <h1>All files</h1> 23 + <div class='clearfix'> 24 + 25 + <div class='fl pad1y space-right2'> 26 + <span class="strong">100% </span> 27 + <span class="quiet">Statements</span> 28 + <span class='fraction'>67/67</span> 29 + </div> 30 + 31 + 32 + <div class='fl pad1y space-right2'> 33 + <span class="strong">94% </span> 34 + <span class="quiet">Branches</span> 35 + <span class='fraction'>47/50</span> 36 + </div> 37 + 38 + 39 + <div class='fl pad1y space-right2'> 40 + <span class="strong">100% </span> 41 + <span class="quiet">Functions</span> 42 + <span class='fraction'>17/17</span> 43 + </div> 44 + 45 + 46 + <div class='fl pad1y space-right2'> 47 + <span class="strong">100% </span> 48 + <span class="quiet">Lines</span> 49 + <span class='fraction'>64/64</span> 50 + </div> 51 + 52 + 53 + </div> 54 + <p class="quiet"> 55 + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. 56 + </p> 57 + <template id="filterTemplate"> 58 + <div class="quiet"> 59 + Filter: 60 + <input type="search" id="fileSearch"> 61 + </div> 62 + </template> 63 + </div> 64 + <div class='status-line high'></div> 65 + <div class="pad1"> 66 + <table class="coverage-summary"> 67 + <thead> 68 + <tr> 69 + <th data-col="file" data-fmt="html" data-html="true" class="file">File</th> 70 + <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th> 71 + <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th> 72 + <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th> 73 + <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th> 74 + <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th> 75 + <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th> 76 + <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th> 77 + <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th> 78 + <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th> 79 + </tr> 80 + </thead> 81 + <tbody><tr> 82 + <td class="file high" data-value="config.ts"><a href="config.ts.html">config.ts</a></td> 83 + <td data-value="100" class="pic high"> 84 + <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div> 85 + </td> 86 + <td data-value="100" class="pct high">100%</td> 87 + <td data-value="6" class="abs high">6/6</td> 88 + <td data-value="75" class="pct medium">75%</td> 89 + <td data-value="4" class="abs medium">3/4</td> 90 + <td data-value="100" class="pct high">100%</td> 91 + <td data-value="0" class="abs high">0/0</td> 92 + <td data-value="100" class="pct high">100%</td> 93 + <td data-value="6" class="abs high">6/6</td> 94 + </tr> 95 + 96 + <tr> 97 + <td class="file high" data-value="logger.ts"><a href="logger.ts.html">logger.ts</a></td> 98 + <td data-value="100" class="pic high"> 99 + <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div> 100 + </td> 101 + <td data-value="100" class="pct high">100%</td> 102 + <td data-value="1" class="abs high">1/1</td> 103 + <td data-value="100" class="pct high">100%</td> 104 + <td data-value="2" class="abs high">2/2</td> 105 + <td data-value="100" class="pct high">100%</td> 106 + <td data-value="0" class="abs high">0/0</td> 107 + <td data-value="100" class="pct high">100%</td> 108 + <td data-value="1" class="abs high">1/1</td> 109 + </tr> 110 + 111 + <tr> 112 + <td class="file high" data-value="utils.ts"><a href="utils.ts.html">utils.ts</a></td> 113 + <td data-value="100" class="pic high"> 114 + <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div> 115 + </td> 116 + <td data-value="100" class="pct high">100%</td> 117 + <td data-value="60" class="abs high">60/60</td> 118 + <td data-value="95.45" class="pct high">95.45%</td> 119 + <td data-value="44" class="abs high">42/44</td> 120 + <td data-value="100" class="pct high">100%</td> 121 + <td data-value="17" class="abs high">17/17</td> 122 + <td data-value="100" class="pct high">100%</td> 123 + <td data-value="57" class="abs high">57/57</td> 124 + </tr> 125 + 126 + </tbody> 127 + </table> 128 + </div> 129 + <div class='push'></div><!-- for sticky footer --> 130 + </div><!-- /wrapper --> 131 + <div class='footer quiet pad2 space-top1 center small'> 132 + Code coverage generated by 133 + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> 134 + at 2026-01-21T23:33:32.350Z 135 + </div> 136 + <script src="prettify.js"></script> 137 + <script> 138 + window.onload = function () { 139 + prettyPrint(); 140 + }; 141 + </script> 142 + <script src="sorter.js"></script> 143 + <script src="block-navigation.js"></script> 144 + </body> 145 + </html> 146 +
+136
backend/coverage/logger.ts.html
··· 1 + 2 + <!doctype html> 3 + <html lang="en"> 4 + 5 + <head> 6 + <title>Code coverage report for logger.ts</title> 7 + <meta charset="utf-8" /> 8 + <link rel="stylesheet" href="prettify.css" /> 9 + <link rel="stylesheet" href="base.css" /> 10 + <link rel="shortcut icon" type="image/x-icon" href="favicon.png" /> 11 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 12 + <style type='text/css'> 13 + .coverage-summary .sorter { 14 + background-image: url(sort-arrow-sprite.png); 15 + } 16 + </style> 17 + </head> 18 + 19 + <body> 20 + <div class='wrapper'> 21 + <div class='pad1'> 22 + <h1><a href="index.html">All files</a> logger.ts</h1> 23 + <div class='clearfix'> 24 + 25 + <div class='fl pad1y space-right2'> 26 + <span class="strong">100% </span> 27 + <span class="quiet">Statements</span> 28 + <span class='fraction'>1/1</span> 29 + </div> 30 + 31 + 32 + <div class='fl pad1y space-right2'> 33 + <span class="strong">100% </span> 34 + <span class="quiet">Branches</span> 35 + <span class='fraction'>2/2</span> 36 + </div> 37 + 38 + 39 + <div class='fl pad1y space-right2'> 40 + <span class="strong">100% </span> 41 + <span class="quiet">Functions</span> 42 + <span class='fraction'>0/0</span> 43 + </div> 44 + 45 + 46 + <div class='fl pad1y space-right2'> 47 + <span class="strong">100% </span> 48 + <span class="quiet">Lines</span> 49 + <span class='fraction'>1/1</span> 50 + </div> 51 + 52 + 53 + </div> 54 + <p class="quiet"> 55 + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. 56 + </p> 57 + <template id="filterTemplate"> 58 + <div class="quiet"> 59 + Filter: 60 + <input type="search" id="fileSearch"> 61 + </div> 62 + </template> 63 + </div> 64 + <div class='status-line high'></div> 65 + <pre><table class="coverage"> 66 + <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> 67 + <a name='L2'></a><a href='#L2'>2</a> 68 + <a name='L3'></a><a href='#L3'>3</a> 69 + <a name='L4'></a><a href='#L4'>4</a> 70 + <a name='L5'></a><a href='#L5'>5</a> 71 + <a name='L6'></a><a href='#L6'>6</a> 72 + <a name='L7'></a><a href='#L7'>7</a> 73 + <a name='L8'></a><a href='#L8'>8</a> 74 + <a name='L9'></a><a href='#L9'>9</a> 75 + <a name='L10'></a><a href='#L10'>10</a> 76 + <a name='L11'></a><a href='#L11'>11</a> 77 + <a name='L12'></a><a href='#L12'>12</a> 78 + <a name='L13'></a><a href='#L13'>13</a> 79 + <a name='L14'></a><a href='#L14'>14</a> 80 + <a name='L15'></a><a href='#L15'>15</a> 81 + <a name='L16'></a><a href='#L16'>16</a> 82 + <a name='L17'></a><a href='#L17'>17</a> 83 + <a name='L18'></a><a href='#L18'>18</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span> 84 + <span class="cline-any cline-neutral">&nbsp;</span> 85 + <span class="cline-any cline-yes">1x</span> 86 + <span class="cline-any cline-neutral">&nbsp;</span> 87 + <span class="cline-any cline-neutral">&nbsp;</span> 88 + <span class="cline-any cline-neutral">&nbsp;</span> 89 + <span class="cline-any cline-neutral">&nbsp;</span> 90 + <span class="cline-any cline-neutral">&nbsp;</span> 91 + <span class="cline-any cline-neutral">&nbsp;</span> 92 + <span class="cline-any cline-neutral">&nbsp;</span> 93 + <span class="cline-any cline-neutral">&nbsp;</span> 94 + <span class="cline-any cline-neutral">&nbsp;</span> 95 + <span class="cline-any cline-neutral">&nbsp;</span> 96 + <span class="cline-any cline-neutral">&nbsp;</span> 97 + <span class="cline-any cline-neutral">&nbsp;</span> 98 + <span class="cline-any cline-neutral">&nbsp;</span> 99 + <span class="cline-any cline-neutral">&nbsp;</span> 100 + <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { pino } from 'pino'; // 名前付きエクスポート 101 + &nbsp; 102 + const logger = pino({ 103 + level: process.env.LOG_LEVEL || 'info', 104 + transport: { 105 + targets: [ 106 + { 107 + target: 'pino-pretty', 108 + options: { 109 + colorize: true, // 色付け 110 + translateTime: 'yyyy-mm-dd HH:MM:ss.l', // 日付と時間を表示 111 + }, 112 + }, 113 + ], 114 + }, 115 + }); 116 + &nbsp; 117 + export default logger;</pre></td></tr></table></pre> 118 + 119 + <div class='push'></div><!-- for sticky footer --> 120 + </div><!-- /wrapper --> 121 + <div class='footer quiet pad2 space-top1 center small'> 122 + Code coverage generated by 123 + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> 124 + at 2026-01-21T23:33:32.350Z 125 + </div> 126 + <script src="prettify.js"></script> 127 + <script> 128 + window.onload = function () { 129 + prettyPrint(); 130 + }; 131 + </script> 132 + <script src="sorter.js"></script> 133 + <script src="block-navigation.js"></script> 134 + </body> 135 + </html> 136 +
+1
backend/coverage/prettify.css
··· 1 + .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
+2
backend/coverage/prettify.js
··· 1 + /* eslint-disable */ 2 + window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V<U;++V){var ae=Z[V];if(ae.ignoreCase){ac=true}else{if(/[a-z]/i.test(ae.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi,""))){S=true;ac=false;break}}}var Y={b:8,t:9,n:10,v:11,f:12,r:13};function ab(ah){var ag=ah.charCodeAt(0);if(ag!==92){return ag}var af=ah.charAt(1);ag=Y[af];if(ag){return ag}else{if("0"<=af&&af<="7"){return parseInt(ah.substring(1),8)}else{if(af==="u"||af==="x"){return parseInt(ah.substring(2),16)}else{return ah.charCodeAt(1)}}}}function T(af){if(af<32){return(af<16?"\\x0":"\\x")+af.toString(16)}var ag=String.fromCharCode(af);if(ag==="\\"||ag==="-"||ag==="["||ag==="]"){ag="\\"+ag}return ag}function X(am){var aq=am.substring(1,am.length-1).match(new RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));var ak=[];var af=[];var ao=aq[0]==="^";for(var ar=ao?1:0,aj=aq.length;ar<aj;++ar){var ah=aq[ar];if(/\\[bdsw]/i.test(ah)){ak.push(ah)}else{var ag=ab(ah);var al;if(ar+2<aj&&"-"===aq[ar+1]){al=ab(aq[ar+2]);ar+=2}else{al=ag}af.push([ag,al]);if(!(al<65||ag>122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;ar<af.length;++ar){var at=af[ar];if(at[0]<=ap[1]+1){ap[1]=Math.max(ap[1],at[1])}else{ai.push(ap=at)}}var an=["["];if(ao){an.push("^")}an.push.apply(an,ak);for(var ar=0;ar<ai.length;++ar){var at=ai[ar];an.push(T(at[0]));if(at[1]>at[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak<ah;++ak){var ag=aj[ak];if(ag==="("){++am}else{if("\\"===ag.charAt(0)){var af=+ag.substring(1);if(af&&af<=am){an[af]=-1}}}}for(var ak=1;ak<an.length;++ak){if(-1===an[ak]){an[ak]=++ad}}for(var ak=0,am=0;ak<ah;++ak){var ag=aj[ak];if(ag==="("){++am;if(an[am]===undefined){aj[ak]="(?:"}}else{if("\\"===ag.charAt(0)){var af=+ag.substring(1);if(af&&af<=am){aj[ak]="\\"+an[am]}}}}for(var ak=0,am=0;ak<ah;++ak){if("^"===aj[ak]&&"^"!==aj[ak+1]){aj[ak]=""}}if(al.ignoreCase&&S){for(var ak=0;ak<ah;++ak){var ag=aj[ak];var ai=ag.charAt(0);if(ag.length>=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V<U;++V){var ae=Z[V];if(ae.global||ae.multiline){throw new Error(""+ae)}aa.push("(?:"+W(ae)+")")}return new RegExp(aa.join("|"),ac?"gi":"g")}function a(V){var U=/(?:^|\s)nocode(?:\s|$)/;var X=[];var T=0;var Z=[];var W=0;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=document.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Y=S&&"pre"===S.substring(0,3);function aa(ab){switch(ab.nodeType){case 1:if(U.test(ab.className)){return}for(var ae=ab.firstChild;ae;ae=ae.nextSibling){aa(ae)}var ad=ab.nodeName;if("BR"===ad||"LI"===ad){X[W]="\n";Z[W<<1]=T++;Z[(W++<<1)|1]=ab}break;case 3:case 4:var ac=ab.nodeValue;if(ac.length){if(!Y){ac=ac.replace(/[ \t\r\n]+/g," ")}else{ac=ac.replace(/\r\n?/g,"\n")}X[W]=ac;Z[W<<1]=T;T+=ac.length;Z[(W++<<1)|1]=ab}break}}aa(V);return{sourceCode:X.join("").replace(/\n$/,""),spans:Z}}function B(S,U,W,T){if(!U){return}var V={sourceCode:U,basePos:S};W(V);T.push.apply(T,V.decorations)}var v=/\S/;function o(S){var V=undefined;for(var U=S.firstChild;U;U=U.nextSibling){var T=U.nodeType;V=(T===1)?(V?S:U):(T===3)?(v.test(U.nodeValue)?S:V):V}return V===S?undefined:V}function g(U,T){var S={};var V;(function(){var ad=U.concat(T);var ah=[];var ag={};for(var ab=0,Z=ad.length;ab<Z;++ab){var Y=ad[ab];var ac=Y[3];if(ac){for(var ae=ac.length;--ae>=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae<aq;++ae){var ag=an[ae];var ap=aj[ag];var ai=void 0;var am;if(typeof ap==="string"){am=false}else{var aa=S[ag.charAt(0)];if(aa){ai=ag.match(aa[1]);ap=aa[0]}else{for(var ao=0;ao<X;++ao){aa=T[ao];ai=ag.match(aa[1]);if(ai){ap=aa[0];break}}if(!ai){ap=F}}am=ap.length>=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y<W.length;++Y){ae(W[Y])}if(ag===(ag|0)){W[0].setAttribute("value",ag)}var aa=ac.createElement("OL");aa.className="linenums";var X=Math.max(0,((ag-1))|0)||0;for(var Y=0,T=W.length;Y<T;++Y){af=W[Y];af.className="L"+((Y+X)%10);if(!af.firstChild){af.appendChild(ac.createTextNode("\xA0"))}aa.appendChild(af)}V.appendChild(aa)}function D(ac){var aj=/\bMSIE\b/.test(navigator.userAgent);var am=/\n/g;var al=ac.sourceCode;var an=al.length;var V=0;var aa=ac.spans;var T=aa.length;var ah=0;var X=ac.decorations;var Y=X.length;var Z=0;X[Y]=an;var ar,aq;for(aq=ar=0;aq<Y;){if(X[aq]!==X[aq+2]){X[ar++]=X[aq++];X[ar++]=X[aq++]}else{aq+=2}}Y=ar;for(aq=ar=0;aq<Y;){var at=X[aq];var ab=X[aq+1];var W=aq+2;while(W+2<=Y&&X[W+1]===ab){W+=2}X[ar++]=at;X[ar++]=ab;aq=W}Y=X.length=ar;var ae=null;while(ah<T){var af=aa[ah];var S=aa[ah+2]||an;var ag=X[Z];var ap=X[Z+2]||an;var W=Math.min(S,ap);var ak=aa[ah+1];var U;if(ak.nodeType!==1&&(U=al.substring(V,W))){if(aj){U=U.replace(am,"\r")}ak.nodeValue=U;var ai=ak.ownerDocument;var ao=ai.createElement("SPAN");ao.className=X[Z+1];var ad=ak.parentNode;ad.replaceChild(ao,ak);ao.appendChild(ak);if(V<S){aa[ah+1]=ak=ai.createTextNode(al.substring(W,S));ad.insertBefore(ak,ao.nextSibling)}}V=W;if(V>=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*</.test(S)?"default-markup":"default-code"}return t[T]}c(K,["default-code"]);c(g([],[[F,/^[^<?]+/],[E,/^<!\w[^>]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa<ac.length;++aa){for(var Z=0,V=ac[aa].length;Z<V;++Z){T.push(ac[aa][Z])}}ac=null;var W=Date;if(!W.now){W={now:function(){return +(new Date)}}}var X=0;var S;var ab=/\blang(?:uage)?-([\w.]+)(?!\S)/;var ae=/\bprettyprint\b/;function U(){var ag=(window.PR_SHOULD_USE_CONTINUATION?W.now()+250:Infinity);for(;X<T.length&&W.now()<ag;X++){var aj=T[X];var ai=aj.className;if(ai.indexOf("prettyprint")>=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X<T.length){setTimeout(U,250)}else{if(ad){ad()}}}U()}window.prettyPrintOne=y;window.prettyPrint=b;window.PR={createSimpleLexer:g,registerLangHandler:c,sourceDecorator:i,PR_ATTRIB_NAME:P,PR_ATTRIB_VALUE:n,PR_COMMENT:j,PR_DECLARATION:E,PR_KEYWORD:z,PR_LITERAL:G,PR_NOCODE:N,PR_PLAIN:F,PR_PUNCTUATION:L,PR_SOURCE:J,PR_STRING:C,PR_TAG:m,PR_TYPE:O}})();PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_DECLARATION,/^<!\w[^>]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^<script\b[^>]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:<!--|-->)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]);
backend/coverage/sort-arrow-sprite.png

This is a binary file and will not be displayed.

+210
backend/coverage/sorter.js
··· 1 + /* eslint-disable */ 2 + var addSorting = (function() { 3 + 'use strict'; 4 + var cols, 5 + currentSort = { 6 + index: 0, 7 + desc: false 8 + }; 9 + 10 + // returns the summary table element 11 + function getTable() { 12 + return document.querySelector('.coverage-summary'); 13 + } 14 + // returns the thead element of the summary table 15 + function getTableHeader() { 16 + return getTable().querySelector('thead tr'); 17 + } 18 + // returns the tbody element of the summary table 19 + function getTableBody() { 20 + return getTable().querySelector('tbody'); 21 + } 22 + // returns the th element for nth column 23 + function getNthColumn(n) { 24 + return getTableHeader().querySelectorAll('th')[n]; 25 + } 26 + 27 + function onFilterInput() { 28 + const searchValue = document.getElementById('fileSearch').value; 29 + const rows = document.getElementsByTagName('tbody')[0].children; 30 + 31 + // Try to create a RegExp from the searchValue. If it fails (invalid regex), 32 + // it will be treated as a plain text search 33 + let searchRegex; 34 + try { 35 + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive 36 + } catch (error) { 37 + searchRegex = null; 38 + } 39 + 40 + for (let i = 0; i < rows.length; i++) { 41 + const row = rows[i]; 42 + let isMatch = false; 43 + 44 + if (searchRegex) { 45 + // If a valid regex was created, use it for matching 46 + isMatch = searchRegex.test(row.textContent); 47 + } else { 48 + // Otherwise, fall back to the original plain text search 49 + isMatch = row.textContent 50 + .toLowerCase() 51 + .includes(searchValue.toLowerCase()); 52 + } 53 + 54 + row.style.display = isMatch ? '' : 'none'; 55 + } 56 + } 57 + 58 + // loads the search box 59 + function addSearchBox() { 60 + var template = document.getElementById('filterTemplate'); 61 + var templateClone = template.content.cloneNode(true); 62 + templateClone.getElementById('fileSearch').oninput = onFilterInput; 63 + template.parentElement.appendChild(templateClone); 64 + } 65 + 66 + // loads all columns 67 + function loadColumns() { 68 + var colNodes = getTableHeader().querySelectorAll('th'), 69 + colNode, 70 + cols = [], 71 + col, 72 + i; 73 + 74 + for (i = 0; i < colNodes.length; i += 1) { 75 + colNode = colNodes[i]; 76 + col = { 77 + key: colNode.getAttribute('data-col'), 78 + sortable: !colNode.getAttribute('data-nosort'), 79 + type: colNode.getAttribute('data-type') || 'string' 80 + }; 81 + cols.push(col); 82 + if (col.sortable) { 83 + col.defaultDescSort = col.type === 'number'; 84 + colNode.innerHTML = 85 + colNode.innerHTML + '<span class="sorter"></span>'; 86 + } 87 + } 88 + return cols; 89 + } 90 + // attaches a data attribute to every tr element with an object 91 + // of data values keyed by column name 92 + function loadRowData(tableRow) { 93 + var tableCols = tableRow.querySelectorAll('td'), 94 + colNode, 95 + col, 96 + data = {}, 97 + i, 98 + val; 99 + for (i = 0; i < tableCols.length; i += 1) { 100 + colNode = tableCols[i]; 101 + col = cols[i]; 102 + val = colNode.getAttribute('data-value'); 103 + if (col.type === 'number') { 104 + val = Number(val); 105 + } 106 + data[col.key] = val; 107 + } 108 + return data; 109 + } 110 + // loads all row data 111 + function loadData() { 112 + var rows = getTableBody().querySelectorAll('tr'), 113 + i; 114 + 115 + for (i = 0; i < rows.length; i += 1) { 116 + rows[i].data = loadRowData(rows[i]); 117 + } 118 + } 119 + // sorts the table using the data for the ith column 120 + function sortByIndex(index, desc) { 121 + var key = cols[index].key, 122 + sorter = function(a, b) { 123 + a = a.data[key]; 124 + b = b.data[key]; 125 + return a < b ? -1 : a > b ? 1 : 0; 126 + }, 127 + finalSorter = sorter, 128 + tableBody = document.querySelector('.coverage-summary tbody'), 129 + rowNodes = tableBody.querySelectorAll('tr'), 130 + rows = [], 131 + i; 132 + 133 + if (desc) { 134 + finalSorter = function(a, b) { 135 + return -1 * sorter(a, b); 136 + }; 137 + } 138 + 139 + for (i = 0; i < rowNodes.length; i += 1) { 140 + rows.push(rowNodes[i]); 141 + tableBody.removeChild(rowNodes[i]); 142 + } 143 + 144 + rows.sort(finalSorter); 145 + 146 + for (i = 0; i < rows.length; i += 1) { 147 + tableBody.appendChild(rows[i]); 148 + } 149 + } 150 + // removes sort indicators for current column being sorted 151 + function removeSortIndicators() { 152 + var col = getNthColumn(currentSort.index), 153 + cls = col.className; 154 + 155 + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 156 + col.className = cls; 157 + } 158 + // adds sort indicators for current column being sorted 159 + function addSortIndicators() { 160 + getNthColumn(currentSort.index).className += currentSort.desc 161 + ? ' sorted-desc' 162 + : ' sorted'; 163 + } 164 + // adds event listeners for all sorter widgets 165 + function enableUI() { 166 + var i, 167 + el, 168 + ithSorter = function ithSorter(i) { 169 + var col = cols[i]; 170 + 171 + return function() { 172 + var desc = col.defaultDescSort; 173 + 174 + if (currentSort.index === i) { 175 + desc = !currentSort.desc; 176 + } 177 + sortByIndex(i, desc); 178 + removeSortIndicators(); 179 + currentSort.index = i; 180 + currentSort.desc = desc; 181 + addSortIndicators(); 182 + }; 183 + }; 184 + for (i = 0; i < cols.length; i += 1) { 185 + if (cols[i].sortable) { 186 + // add the click event handler on the th so users 187 + // dont have to click on those tiny arrows 188 + el = getNthColumn(i).querySelector('.sorter').parentElement; 189 + if (el.addEventListener) { 190 + el.addEventListener('click', ithSorter(i)); 191 + } else { 192 + el.attachEvent('onclick', ithSorter(i)); 193 + } 194 + } 195 + } 196 + } 197 + // adds sorting functionality to the UI 198 + return function() { 199 + if (!getTable()) { 200 + return; 201 + } 202 + cols = loadColumns(); 203 + loadData(); 204 + addSearchBox(); 205 + addSortIndicators(); 206 + enableUI(); 207 + }; 208 + })(); 209 + 210 + window.addEventListener('load', addSorting);
+826
backend/coverage/utils.ts.html
··· 1 + 2 + <!doctype html> 3 + <html lang="en"> 4 + 5 + <head> 6 + <title>Code coverage report for utils.ts</title> 7 + <meta charset="utf-8" /> 8 + <link rel="stylesheet" href="prettify.css" /> 9 + <link rel="stylesheet" href="base.css" /> 10 + <link rel="shortcut icon" type="image/x-icon" href="favicon.png" /> 11 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 12 + <style type='text/css'> 13 + .coverage-summary .sorter { 14 + background-image: url(sort-arrow-sprite.png); 15 + } 16 + </style> 17 + </head> 18 + 19 + <body> 20 + <div class='wrapper'> 21 + <div class='pad1'> 22 + <h1><a href="index.html">All files</a> utils.ts</h1> 23 + <div class='clearfix'> 24 + 25 + <div class='fl pad1y space-right2'> 26 + <span class="strong">100% </span> 27 + <span class="quiet">Statements</span> 28 + <span class='fraction'>60/60</span> 29 + </div> 30 + 31 + 32 + <div class='fl pad1y space-right2'> 33 + <span class="strong">95.45% </span> 34 + <span class="quiet">Branches</span> 35 + <span class='fraction'>42/44</span> 36 + </div> 37 + 38 + 39 + <div class='fl pad1y space-right2'> 40 + <span class="strong">100% </span> 41 + <span class="quiet">Functions</span> 42 + <span class='fraction'>17/17</span> 43 + </div> 44 + 45 + 46 + <div class='fl pad1y space-right2'> 47 + <span class="strong">100% </span> 48 + <span class="quiet">Lines</span> 49 + <span class='fraction'>57/57</span> 50 + </div> 51 + 52 + 53 + </div> 54 + <p class="quiet"> 55 + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. 56 + </p> 57 + <template id="filterTemplate"> 58 + <div class="quiet"> 59 + Filter: 60 + <input type="search" id="fileSearch"> 61 + </div> 62 + </template> 63 + </div> 64 + <div class='status-line high'></div> 65 + <pre><table class="coverage"> 66 + <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> 67 + <a name='L2'></a><a href='#L2'>2</a> 68 + <a name='L3'></a><a href='#L3'>3</a> 69 + <a name='L4'></a><a href='#L4'>4</a> 70 + <a name='L5'></a><a href='#L5'>5</a> 71 + <a name='L6'></a><a href='#L6'>6</a> 72 + <a name='L7'></a><a href='#L7'>7</a> 73 + <a name='L8'></a><a href='#L8'>8</a> 74 + <a name='L9'></a><a href='#L9'>9</a> 75 + <a name='L10'></a><a href='#L10'>10</a> 76 + <a name='L11'></a><a href='#L11'>11</a> 77 + <a name='L12'></a><a href='#L12'>12</a> 78 + <a name='L13'></a><a href='#L13'>13</a> 79 + <a name='L14'></a><a href='#L14'>14</a> 80 + <a name='L15'></a><a href='#L15'>15</a> 81 + <a name='L16'></a><a href='#L16'>16</a> 82 + <a name='L17'></a><a href='#L17'>17</a> 83 + <a name='L18'></a><a href='#L18'>18</a> 84 + <a name='L19'></a><a href='#L19'>19</a> 85 + <a name='L20'></a><a href='#L20'>20</a> 86 + <a name='L21'></a><a href='#L21'>21</a> 87 + <a name='L22'></a><a href='#L22'>22</a> 88 + <a name='L23'></a><a href='#L23'>23</a> 89 + <a name='L24'></a><a href='#L24'>24</a> 90 + <a name='L25'></a><a href='#L25'>25</a> 91 + <a name='L26'></a><a href='#L26'>26</a> 92 + <a name='L27'></a><a href='#L27'>27</a> 93 + <a name='L28'></a><a href='#L28'>28</a> 94 + <a name='L29'></a><a href='#L29'>29</a> 95 + <a name='L30'></a><a href='#L30'>30</a> 96 + <a name='L31'></a><a href='#L31'>31</a> 97 + <a name='L32'></a><a href='#L32'>32</a> 98 + <a name='L33'></a><a href='#L33'>33</a> 99 + <a name='L34'></a><a href='#L34'>34</a> 100 + <a name='L35'></a><a href='#L35'>35</a> 101 + <a name='L36'></a><a href='#L36'>36</a> 102 + <a name='L37'></a><a href='#L37'>37</a> 103 + <a name='L38'></a><a href='#L38'>38</a> 104 + <a name='L39'></a><a href='#L39'>39</a> 105 + <a name='L40'></a><a href='#L40'>40</a> 106 + <a name='L41'></a><a href='#L41'>41</a> 107 + <a name='L42'></a><a href='#L42'>42</a> 108 + <a name='L43'></a><a href='#L43'>43</a> 109 + <a name='L44'></a><a href='#L44'>44</a> 110 + <a name='L45'></a><a href='#L45'>45</a> 111 + <a name='L46'></a><a href='#L46'>46</a> 112 + <a name='L47'></a><a href='#L47'>47</a> 113 + <a name='L48'></a><a href='#L48'>48</a> 114 + <a name='L49'></a><a href='#L49'>49</a> 115 + <a name='L50'></a><a href='#L50'>50</a> 116 + <a name='L51'></a><a href='#L51'>51</a> 117 + <a name='L52'></a><a href='#L52'>52</a> 118 + <a name='L53'></a><a href='#L53'>53</a> 119 + <a name='L54'></a><a href='#L54'>54</a> 120 + <a name='L55'></a><a href='#L55'>55</a> 121 + <a name='L56'></a><a href='#L56'>56</a> 122 + <a name='L57'></a><a href='#L57'>57</a> 123 + <a name='L58'></a><a href='#L58'>58</a> 124 + <a name='L59'></a><a href='#L59'>59</a> 125 + <a name='L60'></a><a href='#L60'>60</a> 126 + <a name='L61'></a><a href='#L61'>61</a> 127 + <a name='L62'></a><a href='#L62'>62</a> 128 + <a name='L63'></a><a href='#L63'>63</a> 129 + <a name='L64'></a><a href='#L64'>64</a> 130 + <a name='L65'></a><a href='#L65'>65</a> 131 + <a name='L66'></a><a href='#L66'>66</a> 132 + <a name='L67'></a><a href='#L67'>67</a> 133 + <a name='L68'></a><a href='#L68'>68</a> 134 + <a name='L69'></a><a href='#L69'>69</a> 135 + <a name='L70'></a><a href='#L70'>70</a> 136 + <a name='L71'></a><a href='#L71'>71</a> 137 + <a name='L72'></a><a href='#L72'>72</a> 138 + <a name='L73'></a><a href='#L73'>73</a> 139 + <a name='L74'></a><a href='#L74'>74</a> 140 + <a name='L75'></a><a href='#L75'>75</a> 141 + <a name='L76'></a><a href='#L76'>76</a> 142 + <a name='L77'></a><a href='#L77'>77</a> 143 + <a name='L78'></a><a href='#L78'>78</a> 144 + <a name='L79'></a><a href='#L79'>79</a> 145 + <a name='L80'></a><a href='#L80'>80</a> 146 + <a name='L81'></a><a href='#L81'>81</a> 147 + <a name='L82'></a><a href='#L82'>82</a> 148 + <a name='L83'></a><a href='#L83'>83</a> 149 + <a name='L84'></a><a href='#L84'>84</a> 150 + <a name='L85'></a><a href='#L85'>85</a> 151 + <a name='L86'></a><a href='#L86'>86</a> 152 + <a name='L87'></a><a href='#L87'>87</a> 153 + <a name='L88'></a><a href='#L88'>88</a> 154 + <a name='L89'></a><a href='#L89'>89</a> 155 + <a name='L90'></a><a href='#L90'>90</a> 156 + <a name='L91'></a><a href='#L91'>91</a> 157 + <a name='L92'></a><a href='#L92'>92</a> 158 + <a name='L93'></a><a href='#L93'>93</a> 159 + <a name='L94'></a><a href='#L94'>94</a> 160 + <a name='L95'></a><a href='#L95'>95</a> 161 + <a name='L96'></a><a href='#L96'>96</a> 162 + <a name='L97'></a><a href='#L97'>97</a> 163 + <a name='L98'></a><a href='#L98'>98</a> 164 + <a name='L99'></a><a href='#L99'>99</a> 165 + <a name='L100'></a><a href='#L100'>100</a> 166 + <a name='L101'></a><a href='#L101'>101</a> 167 + <a name='L102'></a><a href='#L102'>102</a> 168 + <a name='L103'></a><a href='#L103'>103</a> 169 + <a name='L104'></a><a href='#L104'>104</a> 170 + <a name='L105'></a><a href='#L105'>105</a> 171 + <a name='L106'></a><a href='#L106'>106</a> 172 + <a name='L107'></a><a href='#L107'>107</a> 173 + <a name='L108'></a><a href='#L108'>108</a> 174 + <a name='L109'></a><a href='#L109'>109</a> 175 + <a name='L110'></a><a href='#L110'>110</a> 176 + <a name='L111'></a><a href='#L111'>111</a> 177 + <a name='L112'></a><a href='#L112'>112</a> 178 + <a name='L113'></a><a href='#L113'>113</a> 179 + <a name='L114'></a><a href='#L114'>114</a> 180 + <a name='L115'></a><a href='#L115'>115</a> 181 + <a name='L116'></a><a href='#L116'>116</a> 182 + <a name='L117'></a><a href='#L117'>117</a> 183 + <a name='L118'></a><a href='#L118'>118</a> 184 + <a name='L119'></a><a href='#L119'>119</a> 185 + <a name='L120'></a><a href='#L120'>120</a> 186 + <a name='L121'></a><a href='#L121'>121</a> 187 + <a name='L122'></a><a href='#L122'>122</a> 188 + <a name='L123'></a><a href='#L123'>123</a> 189 + <a name='L124'></a><a href='#L124'>124</a> 190 + <a name='L125'></a><a href='#L125'>125</a> 191 + <a name='L126'></a><a href='#L126'>126</a> 192 + <a name='L127'></a><a href='#L127'>127</a> 193 + <a name='L128'></a><a href='#L128'>128</a> 194 + <a name='L129'></a><a href='#L129'>129</a> 195 + <a name='L130'></a><a href='#L130'>130</a> 196 + <a name='L131'></a><a href='#L131'>131</a> 197 + <a name='L132'></a><a href='#L132'>132</a> 198 + <a name='L133'></a><a href='#L133'>133</a> 199 + <a name='L134'></a><a href='#L134'>134</a> 200 + <a name='L135'></a><a href='#L135'>135</a> 201 + <a name='L136'></a><a href='#L136'>136</a> 202 + <a name='L137'></a><a href='#L137'>137</a> 203 + <a name='L138'></a><a href='#L138'>138</a> 204 + <a name='L139'></a><a href='#L139'>139</a> 205 + <a name='L140'></a><a href='#L140'>140</a> 206 + <a name='L141'></a><a href='#L141'>141</a> 207 + <a name='L142'></a><a href='#L142'>142</a> 208 + <a name='L143'></a><a href='#L143'>143</a> 209 + <a name='L144'></a><a href='#L144'>144</a> 210 + <a name='L145'></a><a href='#L145'>145</a> 211 + <a name='L146'></a><a href='#L146'>146</a> 212 + <a name='L147'></a><a href='#L147'>147</a> 213 + <a name='L148'></a><a href='#L148'>148</a> 214 + <a name='L149'></a><a href='#L149'>149</a> 215 + <a name='L150'></a><a href='#L150'>150</a> 216 + <a name='L151'></a><a href='#L151'>151</a> 217 + <a name='L152'></a><a href='#L152'>152</a> 218 + <a name='L153'></a><a href='#L153'>153</a> 219 + <a name='L154'></a><a href='#L154'>154</a> 220 + <a name='L155'></a><a href='#L155'>155</a> 221 + <a name='L156'></a><a href='#L156'>156</a> 222 + <a name='L157'></a><a href='#L157'>157</a> 223 + <a name='L158'></a><a href='#L158'>158</a> 224 + <a name='L159'></a><a href='#L159'>159</a> 225 + <a name='L160'></a><a href='#L160'>160</a> 226 + <a name='L161'></a><a href='#L161'>161</a> 227 + <a name='L162'></a><a href='#L162'>162</a> 228 + <a name='L163'></a><a href='#L163'>163</a> 229 + <a name='L164'></a><a href='#L164'>164</a> 230 + <a name='L165'></a><a href='#L165'>165</a> 231 + <a name='L166'></a><a href='#L166'>166</a> 232 + <a name='L167'></a><a href='#L167'>167</a> 233 + <a name='L168'></a><a href='#L168'>168</a> 234 + <a name='L169'></a><a href='#L169'>169</a> 235 + <a name='L170'></a><a href='#L170'>170</a> 236 + <a name='L171'></a><a href='#L171'>171</a> 237 + <a name='L172'></a><a href='#L172'>172</a> 238 + <a name='L173'></a><a href='#L173'>173</a> 239 + <a name='L174'></a><a href='#L174'>174</a> 240 + <a name='L175'></a><a href='#L175'>175</a> 241 + <a name='L176'></a><a href='#L176'>176</a> 242 + <a name='L177'></a><a href='#L177'>177</a> 243 + <a name='L178'></a><a href='#L178'>178</a> 244 + <a name='L179'></a><a href='#L179'>179</a> 245 + <a name='L180'></a><a href='#L180'>180</a> 246 + <a name='L181'></a><a href='#L181'>181</a> 247 + <a name='L182'></a><a href='#L182'>182</a> 248 + <a name='L183'></a><a href='#L183'>183</a> 249 + <a name='L184'></a><a href='#L184'>184</a> 250 + <a name='L185'></a><a href='#L185'>185</a> 251 + <a name='L186'></a><a href='#L186'>186</a> 252 + <a name='L187'></a><a href='#L187'>187</a> 253 + <a name='L188'></a><a href='#L188'>188</a> 254 + <a name='L189'></a><a href='#L189'>189</a> 255 + <a name='L190'></a><a href='#L190'>190</a> 256 + <a name='L191'></a><a href='#L191'>191</a> 257 + <a name='L192'></a><a href='#L192'>192</a> 258 + <a name='L193'></a><a href='#L193'>193</a> 259 + <a name='L194'></a><a href='#L194'>194</a> 260 + <a name='L195'></a><a href='#L195'>195</a> 261 + <a name='L196'></a><a href='#L196'>196</a> 262 + <a name='L197'></a><a href='#L197'>197</a> 263 + <a name='L198'></a><a href='#L198'>198</a> 264 + <a name='L199'></a><a href='#L199'>199</a> 265 + <a name='L200'></a><a href='#L200'>200</a> 266 + <a name='L201'></a><a href='#L201'>201</a> 267 + <a name='L202'></a><a href='#L202'>202</a> 268 + <a name='L203'></a><a href='#L203'>203</a> 269 + <a name='L204'></a><a href='#L204'>204</a> 270 + <a name='L205'></a><a href='#L205'>205</a> 271 + <a name='L206'></a><a href='#L206'>206</a> 272 + <a name='L207'></a><a href='#L207'>207</a> 273 + <a name='L208'></a><a href='#L208'>208</a> 274 + <a name='L209'></a><a href='#L209'>209</a> 275 + <a name='L210'></a><a href='#L210'>210</a> 276 + <a name='L211'></a><a href='#L211'>211</a> 277 + <a name='L212'></a><a href='#L212'>212</a> 278 + <a name='L213'></a><a href='#L213'>213</a> 279 + <a name='L214'></a><a href='#L214'>214</a> 280 + <a name='L215'></a><a href='#L215'>215</a> 281 + <a name='L216'></a><a href='#L216'>216</a> 282 + <a name='L217'></a><a href='#L217'>217</a> 283 + <a name='L218'></a><a href='#L218'>218</a> 284 + <a name='L219'></a><a href='#L219'>219</a> 285 + <a name='L220'></a><a href='#L220'>220</a> 286 + <a name='L221'></a><a href='#L221'>221</a> 287 + <a name='L222'></a><a href='#L222'>222</a> 288 + <a name='L223'></a><a href='#L223'>223</a> 289 + <a name='L224'></a><a href='#L224'>224</a> 290 + <a name='L225'></a><a href='#L225'>225</a> 291 + <a name='L226'></a><a href='#L226'>226</a> 292 + <a name='L227'></a><a href='#L227'>227</a> 293 + <a name='L228'></a><a href='#L228'>228</a> 294 + <a name='L229'></a><a href='#L229'>229</a> 295 + <a name='L230'></a><a href='#L230'>230</a> 296 + <a name='L231'></a><a href='#L231'>231</a> 297 + <a name='L232'></a><a href='#L232'>232</a> 298 + <a name='L233'></a><a href='#L233'>233</a> 299 + <a name='L234'></a><a href='#L234'>234</a> 300 + <a name='L235'></a><a href='#L235'>235</a> 301 + <a name='L236'></a><a href='#L236'>236</a> 302 + <a name='L237'></a><a href='#L237'>237</a> 303 + <a name='L238'></a><a href='#L238'>238</a> 304 + <a name='L239'></a><a href='#L239'>239</a> 305 + <a name='L240'></a><a href='#L240'>240</a> 306 + <a name='L241'></a><a href='#L241'>241</a> 307 + <a name='L242'></a><a href='#L242'>242</a> 308 + <a name='L243'></a><a href='#L243'>243</a> 309 + <a name='L244'></a><a href='#L244'>244</a> 310 + <a name='L245'></a><a href='#L245'>245</a> 311 + <a name='L246'></a><a href='#L246'>246</a> 312 + <a name='L247'></a><a href='#L247'>247</a> 313 + <a name='L248'></a><a href='#L248'>248</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span> 314 + <span class="cline-any cline-neutral">&nbsp;</span> 315 + <span class="cline-any cline-neutral">&nbsp;</span> 316 + <span class="cline-any cline-neutral">&nbsp;</span> 317 + <span class="cline-any cline-neutral">&nbsp;</span> 318 + <span class="cline-any cline-neutral">&nbsp;</span> 319 + <span class="cline-any cline-neutral">&nbsp;</span> 320 + <span class="cline-any cline-neutral">&nbsp;</span> 321 + <span class="cline-any cline-neutral">&nbsp;</span> 322 + <span class="cline-any cline-neutral">&nbsp;</span> 323 + <span class="cline-any cline-neutral">&nbsp;</span> 324 + <span class="cline-any cline-neutral">&nbsp;</span> 325 + <span class="cline-any cline-neutral">&nbsp;</span> 326 + <span class="cline-any cline-neutral">&nbsp;</span> 327 + <span class="cline-any cline-neutral">&nbsp;</span> 328 + <span class="cline-any cline-neutral">&nbsp;</span> 329 + <span class="cline-any cline-neutral">&nbsp;</span> 330 + <span class="cline-any cline-neutral">&nbsp;</span> 331 + <span class="cline-any cline-neutral">&nbsp;</span> 332 + <span class="cline-any cline-neutral">&nbsp;</span> 333 + <span class="cline-any cline-neutral">&nbsp;</span> 334 + <span class="cline-any cline-neutral">&nbsp;</span> 335 + <span class="cline-any cline-neutral">&nbsp;</span> 336 + <span class="cline-any cline-neutral">&nbsp;</span> 337 + <span class="cline-any cline-neutral">&nbsp;</span> 338 + <span class="cline-any cline-neutral">&nbsp;</span> 339 + <span class="cline-any cline-neutral">&nbsp;</span> 340 + <span class="cline-any cline-neutral">&nbsp;</span> 341 + <span class="cline-any cline-neutral">&nbsp;</span> 342 + <span class="cline-any cline-neutral">&nbsp;</span> 343 + <span class="cline-any cline-neutral">&nbsp;</span> 344 + <span class="cline-any cline-neutral">&nbsp;</span> 345 + <span class="cline-any cline-neutral">&nbsp;</span> 346 + <span class="cline-any cline-neutral">&nbsp;</span> 347 + <span class="cline-any cline-neutral">&nbsp;</span> 348 + <span class="cline-any cline-neutral">&nbsp;</span> 349 + <span class="cline-any cline-neutral">&nbsp;</span> 350 + <span class="cline-any cline-neutral">&nbsp;</span> 351 + <span class="cline-any cline-neutral">&nbsp;</span> 352 + <span class="cline-any cline-neutral">&nbsp;</span> 353 + <span class="cline-any cline-neutral">&nbsp;</span> 354 + <span class="cline-any cline-neutral">&nbsp;</span> 355 + <span class="cline-any cline-neutral">&nbsp;</span> 356 + <span class="cline-any cline-neutral">&nbsp;</span> 357 + <span class="cline-any cline-neutral">&nbsp;</span> 358 + <span class="cline-any cline-neutral">&nbsp;</span> 359 + <span class="cline-any cline-neutral">&nbsp;</span> 360 + <span class="cline-any cline-neutral">&nbsp;</span> 361 + <span class="cline-any cline-neutral">&nbsp;</span> 362 + <span class="cline-any cline-neutral">&nbsp;</span> 363 + <span class="cline-any cline-neutral">&nbsp;</span> 364 + <span class="cline-any cline-neutral">&nbsp;</span> 365 + <span class="cline-any cline-neutral">&nbsp;</span> 366 + <span class="cline-any cline-neutral">&nbsp;</span> 367 + <span class="cline-any cline-neutral">&nbsp;</span> 368 + <span class="cline-any cline-neutral">&nbsp;</span> 369 + <span class="cline-any cline-neutral">&nbsp;</span> 370 + <span class="cline-any cline-neutral">&nbsp;</span> 371 + <span class="cline-any cline-yes">4x</span> 372 + <span class="cline-any cline-neutral">&nbsp;</span> 373 + <span class="cline-any cline-neutral">&nbsp;</span> 374 + <span class="cline-any cline-neutral">&nbsp;</span> 375 + <span class="cline-any cline-neutral">&nbsp;</span> 376 + <span class="cline-any cline-neutral">&nbsp;</span> 377 + <span class="cline-any cline-neutral">&nbsp;</span> 378 + <span class="cline-any cline-yes">8x</span> 379 + <span class="cline-any cline-yes">8x</span> 380 + <span class="cline-any cline-neutral">&nbsp;</span> 381 + <span class="cline-any cline-neutral">&nbsp;</span> 382 + <span class="cline-any cline-yes">8x</span> 383 + <span class="cline-any cline-neutral">&nbsp;</span> 384 + <span class="cline-any cline-neutral">&nbsp;</span> 385 + <span class="cline-any cline-yes">5x</span> 386 + <span class="cline-any cline-neutral">&nbsp;</span> 387 + <span class="cline-any cline-neutral">&nbsp;</span> 388 + <span class="cline-any cline-yes">5x</span> 389 + <span class="cline-any cline-neutral">&nbsp;</span> 390 + <span class="cline-any cline-neutral">&nbsp;</span> 391 + <span class="cline-any cline-yes">4x</span> 392 + <span class="cline-any cline-yes">1x</span> 393 + <span class="cline-any cline-neutral">&nbsp;</span> 394 + <span class="cline-any cline-neutral">&nbsp;</span> 395 + <span class="cline-any cline-yes">3x</span> 396 + <span class="cline-any cline-neutral">&nbsp;</span> 397 + <span class="cline-any cline-yes">2x</span> 398 + <span class="cline-any cline-neutral">&nbsp;</span> 399 + <span class="cline-any cline-neutral">&nbsp;</span> 400 + <span class="cline-any cline-neutral">&nbsp;</span> 401 + <span class="cline-any cline-neutral">&nbsp;</span> 402 + <span class="cline-any cline-neutral">&nbsp;</span> 403 + <span class="cline-any cline-neutral">&nbsp;</span> 404 + <span class="cline-any cline-neutral">&nbsp;</span> 405 + <span class="cline-any cline-yes">10x</span> 406 + <span class="cline-any cline-neutral">&nbsp;</span> 407 + <span class="cline-any cline-neutral">&nbsp;</span> 408 + <span class="cline-any cline-yes">10x</span> 409 + <span class="cline-any cline-neutral">&nbsp;</span> 410 + <span class="cline-any cline-neutral">&nbsp;</span> 411 + <span class="cline-any cline-yes">10x</span> 412 + <span class="cline-any cline-neutral">&nbsp;</span> 413 + <span class="cline-any cline-neutral">&nbsp;</span> 414 + <span class="cline-any cline-neutral">&nbsp;</span> 415 + <span class="cline-any cline-neutral">&nbsp;</span> 416 + <span class="cline-any cline-neutral">&nbsp;</span> 417 + <span class="cline-any cline-yes">10x</span> 418 + <span class="cline-any cline-neutral">&nbsp;</span> 419 + <span class="cline-any cline-neutral">&nbsp;</span> 420 + <span class="cline-any cline-neutral">&nbsp;</span> 421 + <span class="cline-any cline-neutral">&nbsp;</span> 422 + <span class="cline-any cline-neutral">&nbsp;</span> 423 + <span class="cline-any cline-yes">10x</span> 424 + <span class="cline-any cline-neutral">&nbsp;</span> 425 + <span class="cline-any cline-neutral">&nbsp;</span> 426 + <span class="cline-any cline-neutral">&nbsp;</span> 427 + <span class="cline-any cline-neutral">&nbsp;</span> 428 + <span class="cline-any cline-neutral">&nbsp;</span> 429 + <span class="cline-any cline-neutral">&nbsp;</span> 430 + <span class="cline-any cline-yes">4x</span> 431 + <span class="cline-any cline-neutral">&nbsp;</span> 432 + <span class="cline-any cline-neutral">&nbsp;</span> 433 + <span class="cline-any cline-neutral">&nbsp;</span> 434 + <span class="cline-any cline-neutral">&nbsp;</span> 435 + <span class="cline-any cline-neutral">&nbsp;</span> 436 + <span class="cline-any cline-neutral">&nbsp;</span> 437 + <span class="cline-any cline-yes">6x</span> 438 + <span class="cline-any cline-yes">6x</span> 439 + <span class="cline-any cline-yes">6x</span> 440 + <span class="cline-any cline-neutral">&nbsp;</span> 441 + <span class="cline-any cline-yes">6x</span> 442 + <span class="cline-any cline-neutral">&nbsp;</span> 443 + <span class="cline-any cline-yes">3x</span> 444 + <span class="cline-any cline-neutral">&nbsp;</span> 445 + <span class="cline-any cline-yes">2x</span> 446 + <span class="cline-any cline-neutral">&nbsp;</span> 447 + <span class="cline-any cline-yes">1x</span> 448 + <span class="cline-any cline-neutral">&nbsp;</span> 449 + <span class="cline-any cline-neutral">&nbsp;</span> 450 + <span class="cline-any cline-neutral">&nbsp;</span> 451 + <span class="cline-any cline-neutral">&nbsp;</span> 452 + <span class="cline-any cline-neutral">&nbsp;</span> 453 + <span class="cline-any cline-neutral">&nbsp;</span> 454 + <span class="cline-any cline-neutral">&nbsp;</span> 455 + <span class="cline-any cline-neutral">&nbsp;</span> 456 + <span class="cline-any cline-yes">5x</span> 457 + <span class="cline-any cline-yes">5x</span> 458 + <span class="cline-any cline-yes">5x</span> 459 + <span class="cline-any cline-neutral">&nbsp;</span> 460 + <span class="cline-any cline-neutral">&nbsp;</span> 461 + <span class="cline-any cline-neutral">&nbsp;</span> 462 + <span class="cline-any cline-neutral">&nbsp;</span> 463 + <span class="cline-any cline-neutral">&nbsp;</span> 464 + <span class="cline-any cline-neutral">&nbsp;</span> 465 + <span class="cline-any cline-neutral">&nbsp;</span> 466 + <span class="cline-any cline-yes">4x</span> 467 + <span class="cline-any cline-neutral">&nbsp;</span> 468 + <span class="cline-any cline-neutral">&nbsp;</span> 469 + <span class="cline-any cline-neutral">&nbsp;</span> 470 + <span class="cline-any cline-neutral">&nbsp;</span> 471 + <span class="cline-any cline-neutral">&nbsp;</span> 472 + <span class="cline-any cline-neutral">&nbsp;</span> 473 + <span class="cline-any cline-yes">2x</span> 474 + <span class="cline-any cline-neutral">&nbsp;</span> 475 + <span class="cline-any cline-neutral">&nbsp;</span> 476 + <span class="cline-any cline-neutral">&nbsp;</span> 477 + <span class="cline-any cline-neutral">&nbsp;</span> 478 + <span class="cline-any cline-neutral">&nbsp;</span> 479 + <span class="cline-any cline-neutral">&nbsp;</span> 480 + <span class="cline-any cline-yes">4x</span> 481 + <span class="cline-any cline-yes">10x</span> 482 + <span class="cline-any cline-yes">8x</span> 483 + <span class="cline-any cline-neutral">&nbsp;</span> 484 + <span class="cline-any cline-yes">4x</span> 485 + <span class="cline-any cline-yes">1x</span> 486 + <span class="cline-any cline-neutral">&nbsp;</span> 487 + <span class="cline-any cline-neutral">&nbsp;</span> 488 + <span class="cline-any cline-yes">4x</span> 489 + <span class="cline-any cline-neutral">&nbsp;</span> 490 + <span class="cline-any cline-neutral">&nbsp;</span> 491 + <span class="cline-any cline-neutral">&nbsp;</span> 492 + <span class="cline-any cline-neutral">&nbsp;</span> 493 + <span class="cline-any cline-neutral">&nbsp;</span> 494 + <span class="cline-any cline-neutral">&nbsp;</span> 495 + <span class="cline-any cline-neutral">&nbsp;</span> 496 + <span class="cline-any cline-yes">2x</span> 497 + <span class="cline-any cline-yes">2x</span> 498 + <span class="cline-any cline-neutral">&nbsp;</span> 499 + <span class="cline-any cline-neutral">&nbsp;</span> 500 + <span class="cline-any cline-neutral">&nbsp;</span> 501 + <span class="cline-any cline-neutral">&nbsp;</span> 502 + <span class="cline-any cline-neutral">&nbsp;</span> 503 + <span class="cline-any cline-neutral">&nbsp;</span> 504 + <span class="cline-any cline-yes">4x</span> 505 + <span class="cline-any cline-neutral">&nbsp;</span> 506 + <span class="cline-any cline-yes">4x</span> 507 + <span class="cline-any cline-yes">2x</span> 508 + <span class="cline-any cline-neutral">&nbsp;</span> 509 + <span class="cline-any cline-neutral">&nbsp;</span> 510 + <span class="cline-any cline-yes">4x</span> 511 + <span class="cline-any cline-neutral">&nbsp;</span> 512 + <span class="cline-any cline-neutral">&nbsp;</span> 513 + <span class="cline-any cline-neutral">&nbsp;</span> 514 + <span class="cline-any cline-neutral">&nbsp;</span> 515 + <span class="cline-any cline-neutral">&nbsp;</span> 516 + <span class="cline-any cline-neutral">&nbsp;</span> 517 + <span class="cline-any cline-yes">4x</span> 518 + <span class="cline-any cline-neutral">&nbsp;</span> 519 + <span class="cline-any cline-yes">4x</span> 520 + <span class="cline-any cline-yes">3x</span> 521 + <span class="cline-any cline-yes">2x</span> 522 + <span class="cline-any cline-yes">2x</span> 523 + <span class="cline-any cline-yes">4x</span> 524 + <span class="cline-any cline-yes">3x</span> 525 + <span class="cline-any cline-neutral">&nbsp;</span> 526 + <span class="cline-any cline-neutral">&nbsp;</span> 527 + <span class="cline-any cline-neutral">&nbsp;</span> 528 + <span class="cline-any cline-neutral">&nbsp;</span> 529 + <span class="cline-any cline-neutral">&nbsp;</span> 530 + <span class="cline-any cline-neutral">&nbsp;</span> 531 + <span class="cline-any cline-yes">4x</span> 532 + <span class="cline-any cline-neutral">&nbsp;</span> 533 + <span class="cline-any cline-neutral">&nbsp;</span> 534 + <span class="cline-any cline-neutral">&nbsp;</span> 535 + <span class="cline-any cline-neutral">&nbsp;</span> 536 + <span class="cline-any cline-neutral">&nbsp;</span> 537 + <span class="cline-any cline-neutral">&nbsp;</span> 538 + <span class="cline-any cline-yes">5x</span> 539 + <span class="cline-any cline-yes">1x</span> 540 + <span class="cline-any cline-neutral">&nbsp;</span> 541 + <span class="cline-any cline-neutral">&nbsp;</span> 542 + <span class="cline-any cline-yes">4x</span> 543 + <span class="cline-any cline-yes">2x</span> 544 + <span class="cline-any cline-neutral">&nbsp;</span> 545 + <span class="cline-any cline-neutral">&nbsp;</span> 546 + <span class="cline-any cline-yes">2x</span> 547 + <span class="cline-any cline-neutral">&nbsp;</span> 548 + <span class="cline-any cline-neutral">&nbsp;</span> 549 + <span class="cline-any cline-neutral">&nbsp;</span> 550 + <span class="cline-any cline-neutral">&nbsp;</span> 551 + <span class="cline-any cline-neutral">&nbsp;</span> 552 + <span class="cline-any cline-neutral">&nbsp;</span> 553 + <span class="cline-any cline-yes">4x</span> 554 + <span class="cline-any cline-yes">4x</span> 555 + <span class="cline-any cline-yes">4x</span> 556 + <span class="cline-any cline-neutral">&nbsp;</span> 557 + <span class="cline-any cline-yes">2x</span> 558 + <span class="cline-any cline-neutral">&nbsp;</span> 559 + <span class="cline-any cline-neutral">&nbsp;</span> 560 + <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">/** 561 + * Utility functions for the backend that are testable in isolation. 562 + * These are extracted from index.ts for better testability. 563 + */ 564 + &nbsp; 565 + // Type definitions for API responses 566 + export interface DidDocument { 567 + alsoKnownAs?: string[]; 568 + } 569 + &nbsp; 570 + export interface DomainCheckResult { 571 + result: boolean; 572 + } 573 + &nbsp; 574 + export interface OgpResult { 575 + result: { 576 + ogTitle?: string; 577 + ogDescription?: string; 578 + ogImage?: { url: string }[]; 579 + }; 580 + } 581 + &nbsp; 582 + export interface DnsAnswer { 583 + data: string; 584 + } 585 + &nbsp; 586 + export interface DnsResponse { 587 + Answer?: DnsAnswer[]; 588 + } 589 + &nbsp; 590 + export interface PostToBookmarkRecord { 591 + sub: string; 592 + lang?: string; 593 + } 594 + &nbsp; 595 + // Comment locale type 596 + export interface CommentLocale { 597 + lang: string; 598 + title?: string; 599 + comment?: string; 600 + } 601 + &nbsp; 602 + // Bookmark record type (the inner object, not the full record schema) 603 + export interface BookmarkRecord { 604 + $type: 'blue.rito.feed.bookmark'; 605 + subject: string; 606 + createdAt?: string; 607 + comments?: CommentLocale[]; 608 + ogpTitle?: string; 609 + ogpDescription?: string; 610 + ogpImage?: string; 611 + tags?: string[]; 612 + } 613 + &nbsp; 614 + /** 615 + * Convert epoch microseconds to ISO datetime string 616 + */ 617 + export function epochUsToDateTime(cursor: string | number): string { 618 + return new Date(Number(cursor) / 1000).toISOString(); 619 + } 620 + &nbsp; 621 + /** 622 + * Validate if URL is a valid tangled.org URL for the given user handle 623 + */ 624 + export function isValidTangledUrl(url: string, userProfHandle: string): boolean { 625 + try { 626 + const u = new URL(url); 627 + &nbsp; 628 + // ドメインが tangled.org であることを確認 629 + if (u.hostname !== "tangled.org") return false; 630 + &nbsp; 631 + // パスを分解 632 + const parts = u.pathname.split("/").filter(Boolean); 633 + &nbsp; 634 + // 最低でも2要素必要(例: ["@rito.blue", "skeet.el"]) 635 + if (parts.length &lt; 2) return false; 636 + &nbsp; 637 + // 1個目が @handle であることを確認 638 + if (parts[0] !== userProfHandle &amp;&amp; parts[0] !== `@${userProfHandle}`) { 639 + return false; 640 + } 641 + &nbsp; 642 + return true; 643 + } catch { 644 + return false; 645 + } 646 + } 647 + &nbsp; 648 + /** 649 + * Normalize comment text by removing hashtags, URLs, and compressing whitespace 650 + */ 651 + export function normalizeComment(text: string): string { 652 + let result = text; 653 + &nbsp; 654 + // #tags を削除 655 + result = result.replace(/#[^\s#]+/g, ''); 656 + &nbsp; 657 + // URL / ドメイン(パス付き含む)を根こそぎ削除 658 + result = result.replace( 659 + /\bhttps?:\/\/[^\s]+|\b[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\/[^\s]*)?/g, 660 + '' 661 + ); 662 + &nbsp; 663 + // 空白を 1 つに圧縮 664 + result = result 665 + // 半角スペース + 全角スペースを 1 つに 666 + .replace(/[  ]+/g, ' ') 667 + // 行頭・行末のスペースだけ除去(改行は残る) 668 + .replace(/^[  ]+|[  ]+$/gm, ''); 669 + &nbsp; 670 + return result; 671 + } 672 + &nbsp; 673 + /** 674 + * Extract handle from DID document's alsoKnownAs field 675 + */ 676 + export function extractHandleFromDidDoc(didData: DidDocument, defaultHandle: string = 'no handle'): string { 677 + return didData.alsoKnownAs?.[0]?.replace(/^at:\/\//, '') ?? defaultHandle; 678 + } 679 + &nbsp; 680 + /** 681 + * Check if URL domain matches user handle for verification 682 + */ 683 + export function checkDomainVerification(subject: string, handle: string): boolean { 684 + try { 685 + const url = new URL(subject); 686 + const domain = url.hostname; 687 + &nbsp; 688 + if ((url.pathname === '/' || url.pathname === '') &amp;&amp; 689 + (domain === handle || domain.endsWith(`.${handle}`))) { 690 + return true; 691 + } 692 + return false; 693 + } catch { 694 + return false; 695 + } 696 + } 697 + &nbsp; 698 + /** 699 + * Parse DID from DNS TXT record data 700 + */ 701 + export function parseTxtRecordForDid(txtData: string): string | null { 702 + // Remove quotes and join 703 + const cleaned = txtData.replace(/^"|"$/g, "").replace(/"/g, ""); 704 + const didMatch = cleaned.match(/did:[\w:.]+/); 705 + return didMatch ? didMatch[0] : null; 706 + } 707 + &nbsp; 708 + /** 709 + * Reverse a handle to NSID prefix format 710 + * Example: "rito.blue" -&gt; "blue.rito" 711 + */ 712 + export function reverseHandleToNsid(handle: string): string { 713 + return handle.split('.').reverse().join('.'); 714 + } 715 + &nbsp; 716 + /** 717 + * Build AT URI from components 718 + */ 719 + export function buildAtUri(did: string, collection: string, rkey: string): string { 720 + return `at://${did}/${collection}/${rkey}`; 721 + } 722 + &nbsp; 723 + /** 724 + * Filter and normalize tags array 725 + */ 726 + export function normalizeTagsArray(tags: string[], shouldAddVerified: boolean = false): string[] { 727 + let result = (tags ?? <span class="branch-1 cbranch-no" title="branch not covered" >[])</span> 728 + .filter((name: string) =&gt; name &amp;&amp; name.trim().length &gt; 0) 729 + .filter((name: string) =&gt; name.toLowerCase() !== "verified"); 730 + &nbsp; 731 + if (shouldAddVerified) { 732 + result.push("Verified"); 733 + } 734 + &nbsp; 735 + return result; 736 + } 737 + &nbsp; 738 + /** 739 + * Build subdomain for DNS TXT lookup 740 + * Example: "uk.skyblur.post" -&gt; "_lexicon.skyblur.uk" 741 + */ 742 + export function buildDnsTxtSubdomain(nsid: string): string { 743 + const parts = nsid.split('.').reverse(); 744 + return `_lexicon.${parts.slice(1).join('.')}`; 745 + } 746 + &nbsp; 747 + /** 748 + * Extract unique links from post facets and embed 749 + */ 750 + export function extractLinksFromPost(record: any): string[] { 751 + const links: string[] = []; 752 + &nbsp; 753 + if (record.embed?.$type === 'app.bsky.embed.external' &amp;&amp; record.embed.external?.uri) { 754 + links.push(record.embed.external.uri); 755 + } 756 + &nbsp; 757 + return Array.from(new Set(links.filter((l): l is string =&gt; !!l))); 758 + } 759 + &nbsp; 760 + /** 761 + * Extract hashtags from post facets 762 + */ 763 + export function extractTagsFromFacets(facets: any[]): string[] { 764 + const tags: string[] = []; 765 + &nbsp; 766 + if (facets) { 767 + for (const facet of facets) { 768 + <span class="missing-if-branch" title="else path not taken" >E</span>if (facet.features) { 769 + for (const feature of facet.features) { 770 + if (feature.$type === 'app.bsky.richtext.facet#tag' &amp;&amp; feature.tag) { 771 + tags.push(feature.tag); 772 + } 773 + } 774 + } 775 + } 776 + } 777 + &nbsp; 778 + return tags; 779 + } 780 + &nbsp; 781 + /** 782 + * Check if post should be processed as rito.blue bookmark 783 + */ 784 + export function shouldProcessAsRitoPost(tags: string[], via?: string): boolean { 785 + if (!tags.includes('rito.blue')) { 786 + return false; 787 + } 788 + &nbsp; 789 + if (via === 'リト' || via === 'Rito') { 790 + return false; 791 + } 792 + &nbsp; 793 + return true; 794 + } 795 + &nbsp; 796 + /** 797 + * Parse domain from URL string 798 + */ 799 + export function parseDomainFromUrl(urlString: string): string | null { 800 + try { 801 + const url = new URL(urlString); 802 + return url.hostname; 803 + } catch { 804 + return null; 805 + } 806 + } 807 + &nbsp;</pre></td></tr></table></pre> 808 + 809 + <div class='push'></div><!-- for sticky footer --> 810 + </div><!-- /wrapper --> 811 + <div class='footer quiet pad2 space-top1 center small'> 812 + Code coverage generated by 813 + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> 814 + at 2026-01-21T23:33:32.350Z 815 + </div> 816 + <script src="prettify.js"></script> 817 + <script> 818 + window.onload = function () { 819 + prettyPrint(); 820 + }; 821 + </script> 822 + <script src="sorter.js"></script> 823 + <script src="block-navigation.js"></script> 824 + </body> 825 + </html> 826 +
+1623 -9
backend/package-lock.json
··· 10 10 "license": "ISC", 11 11 "dependencies": { 12 12 "@atcute/client": "^4.1.0", 13 + "@atcute/tid": "^1.0.3", 14 + "@atproto/api": "^0.18.8", 15 + "@atproto/jwk-jose": "^0.1.11", 16 + "@atproto/oauth-client-node": "^0.3.13", 13 17 "@prisma/client": "^6.19.0", 14 18 "@skyware/jetstream": "^0.2.5", 19 + "dotenv": "^17.2.3", 15 20 "openai": "^6.10.0", 16 21 "p-limit": "^7.2.0", 17 22 "p-queue": "^9.0.1", ··· 23 28 "devDependencies": { 24 29 "@types/node": "^24.10.1", 25 30 "@types/ws": "^8.18.1", 26 - "prisma": "^6.19.0" 31 + "@vitest/coverage-v8": "^4.0.17", 32 + "prisma": "^6.19.0", 33 + "vitest": "^4.0.17" 27 34 } 28 35 }, 29 36 "node_modules/@atcute/atproto": { ··· 75 82 "esm-env": "^1.2.2" 76 83 } 77 84 }, 85 + "node_modules/@atcute/tid": { 86 + "version": "1.1.1", 87 + "resolved": "https://registry.npmjs.org/@atcute/tid/-/tid-1.1.1.tgz", 88 + "integrity": "sha512-djJ8UGhLkTU5V51yCnBEruMg35qETjWzWy5sJG/2gEOl2Gd7rQWHSaf+yrO6vMS5EFA38U2xOWE3EDUPzvc2ZQ==", 89 + "license": "0BSD", 90 + "dependencies": { 91 + "@atcute/time-ms": "^1.0.0" 92 + } 93 + }, 94 + "node_modules/@atcute/time-ms": { 95 + "version": "1.2.0", 96 + "resolved": "https://registry.npmjs.org/@atcute/time-ms/-/time-ms-1.2.0.tgz", 97 + "integrity": "sha512-dtNKebVIbr1+yu3a6vgtL4sfkNgxkL3aA+ohHsjtW83WWMjjGvX8GVTVmYCJ2dYSxIoxK0q1yWs11PmlqzmQ/A==", 98 + "hasInstallScript": true, 99 + "license": "0BSD", 100 + "dependencies": { 101 + "@types/bun": "^1.3.6", 102 + "node-gyp-build": "^4.8.4" 103 + } 104 + }, 105 + "node_modules/@atproto-labs/did-resolver": { 106 + "version": "0.2.5", 107 + "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.5.tgz", 108 + "integrity": "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==", 109 + "license": "MIT", 110 + "dependencies": { 111 + "@atproto-labs/fetch": "0.2.3", 112 + "@atproto-labs/pipe": "0.1.1", 113 + "@atproto-labs/simple-store": "0.3.0", 114 + "@atproto-labs/simple-store-memory": "0.1.4", 115 + "@atproto/did": "0.2.4", 116 + "zod": "^3.23.8" 117 + } 118 + }, 119 + "node_modules/@atproto-labs/fetch": { 120 + "version": "0.2.3", 121 + "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", 122 + "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==", 123 + "license": "MIT", 124 + "dependencies": { 125 + "@atproto-labs/pipe": "0.1.1" 126 + } 127 + }, 128 + "node_modules/@atproto-labs/fetch-node": { 129 + "version": "0.2.0", 130 + "resolved": "https://registry.npmjs.org/@atproto-labs/fetch-node/-/fetch-node-0.2.0.tgz", 131 + "integrity": "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q==", 132 + "license": "MIT", 133 + "dependencies": { 134 + "@atproto-labs/fetch": "0.2.3", 135 + "@atproto-labs/pipe": "0.1.1", 136 + "ipaddr.js": "^2.1.0", 137 + "undici": "^6.14.1" 138 + }, 139 + "engines": { 140 + "node": ">=18.7.0" 141 + } 142 + }, 143 + "node_modules/@atproto-labs/handle-resolver": { 144 + "version": "0.3.5", 145 + "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.5.tgz", 146 + "integrity": "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA==", 147 + "license": "MIT", 148 + "dependencies": { 149 + "@atproto-labs/simple-store": "0.3.0", 150 + "@atproto-labs/simple-store-memory": "0.1.4", 151 + "@atproto/did": "0.2.4", 152 + "zod": "^3.23.8" 153 + } 154 + }, 155 + "node_modules/@atproto-labs/handle-resolver-node": { 156 + "version": "0.1.24", 157 + "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver-node/-/handle-resolver-node-0.1.24.tgz", 158 + "integrity": "sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg==", 159 + "license": "MIT", 160 + "dependencies": { 161 + "@atproto-labs/fetch-node": "0.2.0", 162 + "@atproto-labs/handle-resolver": "0.3.5", 163 + "@atproto/did": "0.2.4" 164 + }, 165 + "engines": { 166 + "node": ">=18.7.0" 167 + } 168 + }, 169 + "node_modules/@atproto-labs/identity-resolver": { 170 + "version": "0.3.5", 171 + "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.5.tgz", 172 + "integrity": "sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ==", 173 + "license": "MIT", 174 + "dependencies": { 175 + "@atproto-labs/did-resolver": "0.2.5", 176 + "@atproto-labs/handle-resolver": "0.3.5" 177 + } 178 + }, 179 + "node_modules/@atproto-labs/pipe": { 180 + "version": "0.1.1", 181 + "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz", 182 + "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==", 183 + "license": "MIT" 184 + }, 185 + "node_modules/@atproto-labs/simple-store": { 186 + "version": "0.3.0", 187 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz", 188 + "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==", 189 + "license": "MIT" 190 + }, 191 + "node_modules/@atproto-labs/simple-store-memory": { 192 + "version": "0.1.4", 193 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz", 194 + "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==", 195 + "license": "MIT", 196 + "dependencies": { 197 + "@atproto-labs/simple-store": "0.3.0", 198 + "lru-cache": "^10.2.0" 199 + } 200 + }, 201 + "node_modules/@atproto/api": { 202 + "version": "0.18.16", 203 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.16.tgz", 204 + "integrity": "sha512-tRGKSWr83pP5CQpSboePU21pE+GqLDYy1XHae4HH4hjaT0pr5V8wNgu70kbKB0B02GVUumeDRpJnlHKD+eMzLg==", 205 + "license": "MIT", 206 + "dependencies": { 207 + "@atproto/common-web": "^0.4.12", 208 + "@atproto/lexicon": "^0.6.0", 209 + "@atproto/syntax": "^0.4.2", 210 + "@atproto/xrpc": "^0.7.7", 211 + "await-lock": "^2.2.2", 212 + "multiformats": "^9.9.0", 213 + "tlds": "^1.234.0", 214 + "zod": "^3.23.8" 215 + } 216 + }, 217 + "node_modules/@atproto/common-web": { 218 + "version": "0.4.12", 219 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.12.tgz", 220 + "integrity": "sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw==", 221 + "license": "MIT", 222 + "dependencies": { 223 + "@atproto/lex-data": "0.0.8", 224 + "@atproto/lex-json": "0.0.8", 225 + "zod": "^3.23.8" 226 + } 227 + }, 228 + "node_modules/@atproto/did": { 229 + "version": "0.2.4", 230 + "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.4.tgz", 231 + "integrity": "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==", 232 + "license": "MIT", 233 + "dependencies": { 234 + "zod": "^3.23.8" 235 + } 236 + }, 237 + "node_modules/@atproto/jwk": { 238 + "version": "0.6.0", 239 + "resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.6.0.tgz", 240 + "integrity": "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==", 241 + "license": "MIT", 242 + "dependencies": { 243 + "multiformats": "^9.9.0", 244 + "zod": "^3.23.8" 245 + } 246 + }, 247 + "node_modules/@atproto/jwk-jose": { 248 + "version": "0.1.11", 249 + "resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.11.tgz", 250 + "integrity": "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==", 251 + "license": "MIT", 252 + "dependencies": { 253 + "@atproto/jwk": "0.6.0", 254 + "jose": "^5.2.0" 255 + } 256 + }, 257 + "node_modules/@atproto/jwk-webcrypto": { 258 + "version": "0.2.0", 259 + "resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.2.0.tgz", 260 + "integrity": "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==", 261 + "license": "MIT", 262 + "dependencies": { 263 + "@atproto/jwk": "0.6.0", 264 + "@atproto/jwk-jose": "0.1.11", 265 + "zod": "^3.23.8" 266 + } 267 + }, 268 + "node_modules/@atproto/lex-data": { 269 + "version": "0.0.8", 270 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.8.tgz", 271 + "integrity": "sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA==", 272 + "license": "MIT", 273 + "dependencies": { 274 + "@atproto/syntax": "0.4.2", 275 + "multiformats": "^9.9.0", 276 + "tslib": "^2.8.1", 277 + "uint8arrays": "3.0.0", 278 + "unicode-segmenter": "^0.14.0" 279 + } 280 + }, 281 + "node_modules/@atproto/lex-json": { 282 + "version": "0.0.8", 283 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.8.tgz", 284 + "integrity": "sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg==", 285 + "license": "MIT", 286 + "dependencies": { 287 + "@atproto/lex-data": "0.0.8", 288 + "tslib": "^2.8.1" 289 + } 290 + }, 291 + "node_modules/@atproto/lexicon": { 292 + "version": "0.6.0", 293 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.0.tgz", 294 + "integrity": "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==", 295 + "license": "MIT", 296 + "dependencies": { 297 + "@atproto/common-web": "^0.4.7", 298 + "@atproto/syntax": "^0.4.2", 299 + "iso-datestring-validator": "^2.2.2", 300 + "multiformats": "^9.9.0", 301 + "zod": "^3.23.8" 302 + } 303 + }, 304 + "node_modules/@atproto/oauth-client": { 305 + "version": "0.5.13", 306 + "resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.5.13.tgz", 307 + "integrity": "sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA==", 308 + "license": "MIT", 309 + "dependencies": { 310 + "@atproto-labs/did-resolver": "0.2.5", 311 + "@atproto-labs/fetch": "0.2.3", 312 + "@atproto-labs/handle-resolver": "0.3.5", 313 + "@atproto-labs/identity-resolver": "0.3.5", 314 + "@atproto-labs/simple-store": "0.3.0", 315 + "@atproto-labs/simple-store-memory": "0.1.4", 316 + "@atproto/did": "0.2.4", 317 + "@atproto/jwk": "0.6.0", 318 + "@atproto/oauth-types": "0.6.1", 319 + "@atproto/xrpc": "0.7.7", 320 + "core-js": "^3", 321 + "multiformats": "^9.9.0", 322 + "zod": "^3.23.8" 323 + } 324 + }, 325 + "node_modules/@atproto/oauth-client-node": { 326 + "version": "0.3.15", 327 + "resolved": "https://registry.npmjs.org/@atproto/oauth-client-node/-/oauth-client-node-0.3.15.tgz", 328 + "integrity": "sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA==", 329 + "license": "MIT", 330 + "dependencies": { 331 + "@atproto-labs/did-resolver": "0.2.5", 332 + "@atproto-labs/handle-resolver-node": "0.1.24", 333 + "@atproto-labs/simple-store": "0.3.0", 334 + "@atproto/did": "0.2.4", 335 + "@atproto/jwk": "0.6.0", 336 + "@atproto/jwk-jose": "0.1.11", 337 + "@atproto/jwk-webcrypto": "0.2.0", 338 + "@atproto/oauth-client": "0.5.13", 339 + "@atproto/oauth-types": "0.6.1" 340 + }, 341 + "engines": { 342 + "node": ">=18.7.0" 343 + } 344 + }, 345 + "node_modules/@atproto/oauth-types": { 346 + "version": "0.6.1", 347 + "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.6.1.tgz", 348 + "integrity": "sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA==", 349 + "license": "MIT", 350 + "dependencies": { 351 + "@atproto/did": "0.2.4", 352 + "@atproto/jwk": "0.6.0", 353 + "zod": "^3.23.8" 354 + } 355 + }, 356 + "node_modules/@atproto/syntax": { 357 + "version": "0.4.2", 358 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 359 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 360 + "license": "MIT" 361 + }, 362 + "node_modules/@atproto/xrpc": { 363 + "version": "0.7.7", 364 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", 365 + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", 366 + "license": "MIT", 367 + "dependencies": { 368 + "@atproto/lexicon": "^0.6.0", 369 + "zod": "^3.23.8" 370 + } 371 + }, 372 + "node_modules/@babel/helper-string-parser": { 373 + "version": "7.27.1", 374 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 375 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 376 + "dev": true, 377 + "license": "MIT", 378 + "engines": { 379 + "node": ">=6.9.0" 380 + } 381 + }, 382 + "node_modules/@babel/helper-validator-identifier": { 383 + "version": "7.28.5", 384 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", 385 + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", 386 + "dev": true, 387 + "license": "MIT", 388 + "engines": { 389 + "node": ">=6.9.0" 390 + } 391 + }, 392 + "node_modules/@babel/parser": { 393 + "version": "7.28.6", 394 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", 395 + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", 396 + "dev": true, 397 + "license": "MIT", 398 + "dependencies": { 399 + "@babel/types": "^7.28.6" 400 + }, 401 + "bin": { 402 + "parser": "bin/babel-parser.js" 403 + }, 404 + "engines": { 405 + "node": ">=6.0.0" 406 + } 407 + }, 408 + "node_modules/@babel/types": { 409 + "version": "7.28.6", 410 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", 411 + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", 412 + "dev": true, 413 + "license": "MIT", 414 + "dependencies": { 415 + "@babel/helper-string-parser": "^7.27.1", 416 + "@babel/helper-validator-identifier": "^7.28.5" 417 + }, 418 + "engines": { 419 + "node": ">=6.9.0" 420 + } 421 + }, 78 422 "node_modules/@badrap/valita": { 79 423 "version": "0.4.6", 80 424 "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz", ··· 82 426 "license": "MIT", 83 427 "engines": { 84 428 "node": ">= 18" 429 + } 430 + }, 431 + "node_modules/@bcoe/v8-coverage": { 432 + "version": "1.0.2", 433 + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", 434 + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", 435 + "dev": true, 436 + "license": "MIT", 437 + "engines": { 438 + "node": ">=18" 85 439 } 86 440 }, 87 441 "node_modules/@esbuild/aix-ppc64": { ··· 500 854 "node": ">=18" 501 855 } 502 856 }, 857 + "node_modules/@jridgewell/resolve-uri": { 858 + "version": "3.1.2", 859 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 860 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 861 + "dev": true, 862 + "license": "MIT", 863 + "engines": { 864 + "node": ">=6.0.0" 865 + } 866 + }, 867 + "node_modules/@jridgewell/sourcemap-codec": { 868 + "version": "1.5.5", 869 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 870 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 871 + "dev": true, 872 + "license": "MIT" 873 + }, 874 + "node_modules/@jridgewell/trace-mapping": { 875 + "version": "0.3.31", 876 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 877 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 878 + "dev": true, 879 + "license": "MIT", 880 + "dependencies": { 881 + "@jridgewell/resolve-uri": "^3.1.0", 882 + "@jridgewell/sourcemap-codec": "^1.4.14" 883 + } 884 + }, 503 885 "node_modules/@pinojs/redact": { 504 886 "version": "0.4.0", 505 887 "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", ··· 591 973 "@prisma/debug": "6.19.0" 592 974 } 593 975 }, 976 + "node_modules/@rollup/rollup-android-arm-eabi": { 977 + "version": "4.55.3", 978 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.3.tgz", 979 + "integrity": "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==", 980 + "cpu": [ 981 + "arm" 982 + ], 983 + "dev": true, 984 + "license": "MIT", 985 + "optional": true, 986 + "os": [ 987 + "android" 988 + ] 989 + }, 990 + "node_modules/@rollup/rollup-android-arm64": { 991 + "version": "4.55.3", 992 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.3.tgz", 993 + "integrity": "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==", 994 + "cpu": [ 995 + "arm64" 996 + ], 997 + "dev": true, 998 + "license": "MIT", 999 + "optional": true, 1000 + "os": [ 1001 + "android" 1002 + ] 1003 + }, 1004 + "node_modules/@rollup/rollup-darwin-arm64": { 1005 + "version": "4.55.3", 1006 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.3.tgz", 1007 + "integrity": "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==", 1008 + "cpu": [ 1009 + "arm64" 1010 + ], 1011 + "dev": true, 1012 + "license": "MIT", 1013 + "optional": true, 1014 + "os": [ 1015 + "darwin" 1016 + ] 1017 + }, 1018 + "node_modules/@rollup/rollup-darwin-x64": { 1019 + "version": "4.55.3", 1020 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.3.tgz", 1021 + "integrity": "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==", 1022 + "cpu": [ 1023 + "x64" 1024 + ], 1025 + "dev": true, 1026 + "license": "MIT", 1027 + "optional": true, 1028 + "os": [ 1029 + "darwin" 1030 + ] 1031 + }, 1032 + "node_modules/@rollup/rollup-freebsd-arm64": { 1033 + "version": "4.55.3", 1034 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.3.tgz", 1035 + "integrity": "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==", 1036 + "cpu": [ 1037 + "arm64" 1038 + ], 1039 + "dev": true, 1040 + "license": "MIT", 1041 + "optional": true, 1042 + "os": [ 1043 + "freebsd" 1044 + ] 1045 + }, 1046 + "node_modules/@rollup/rollup-freebsd-x64": { 1047 + "version": "4.55.3", 1048 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.3.tgz", 1049 + "integrity": "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==", 1050 + "cpu": [ 1051 + "x64" 1052 + ], 1053 + "dev": true, 1054 + "license": "MIT", 1055 + "optional": true, 1056 + "os": [ 1057 + "freebsd" 1058 + ] 1059 + }, 1060 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1061 + "version": "4.55.3", 1062 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.3.tgz", 1063 + "integrity": "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==", 1064 + "cpu": [ 1065 + "arm" 1066 + ], 1067 + "dev": true, 1068 + "license": "MIT", 1069 + "optional": true, 1070 + "os": [ 1071 + "linux" 1072 + ] 1073 + }, 1074 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1075 + "version": "4.55.3", 1076 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.3.tgz", 1077 + "integrity": "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==", 1078 + "cpu": [ 1079 + "arm" 1080 + ], 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "optional": true, 1084 + "os": [ 1085 + "linux" 1086 + ] 1087 + }, 1088 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1089 + "version": "4.55.3", 1090 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.3.tgz", 1091 + "integrity": "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==", 1092 + "cpu": [ 1093 + "arm64" 1094 + ], 1095 + "dev": true, 1096 + "license": "MIT", 1097 + "optional": true, 1098 + "os": [ 1099 + "linux" 1100 + ] 1101 + }, 1102 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1103 + "version": "4.55.3", 1104 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.3.tgz", 1105 + "integrity": "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==", 1106 + "cpu": [ 1107 + "arm64" 1108 + ], 1109 + "dev": true, 1110 + "license": "MIT", 1111 + "optional": true, 1112 + "os": [ 1113 + "linux" 1114 + ] 1115 + }, 1116 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 1117 + "version": "4.55.3", 1118 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.3.tgz", 1119 + "integrity": "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==", 1120 + "cpu": [ 1121 + "loong64" 1122 + ], 1123 + "dev": true, 1124 + "license": "MIT", 1125 + "optional": true, 1126 + "os": [ 1127 + "linux" 1128 + ] 1129 + }, 1130 + "node_modules/@rollup/rollup-linux-loong64-musl": { 1131 + "version": "4.55.3", 1132 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.3.tgz", 1133 + "integrity": "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==", 1134 + "cpu": [ 1135 + "loong64" 1136 + ], 1137 + "dev": true, 1138 + "license": "MIT", 1139 + "optional": true, 1140 + "os": [ 1141 + "linux" 1142 + ] 1143 + }, 1144 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 1145 + "version": "4.55.3", 1146 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.3.tgz", 1147 + "integrity": "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==", 1148 + "cpu": [ 1149 + "ppc64" 1150 + ], 1151 + "dev": true, 1152 + "license": "MIT", 1153 + "optional": true, 1154 + "os": [ 1155 + "linux" 1156 + ] 1157 + }, 1158 + "node_modules/@rollup/rollup-linux-ppc64-musl": { 1159 + "version": "4.55.3", 1160 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.3.tgz", 1161 + "integrity": "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==", 1162 + "cpu": [ 1163 + "ppc64" 1164 + ], 1165 + "dev": true, 1166 + "license": "MIT", 1167 + "optional": true, 1168 + "os": [ 1169 + "linux" 1170 + ] 1171 + }, 1172 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1173 + "version": "4.55.3", 1174 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.3.tgz", 1175 + "integrity": "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==", 1176 + "cpu": [ 1177 + "riscv64" 1178 + ], 1179 + "dev": true, 1180 + "license": "MIT", 1181 + "optional": true, 1182 + "os": [ 1183 + "linux" 1184 + ] 1185 + }, 1186 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1187 + "version": "4.55.3", 1188 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.3.tgz", 1189 + "integrity": "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==", 1190 + "cpu": [ 1191 + "riscv64" 1192 + ], 1193 + "dev": true, 1194 + "license": "MIT", 1195 + "optional": true, 1196 + "os": [ 1197 + "linux" 1198 + ] 1199 + }, 1200 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1201 + "version": "4.55.3", 1202 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.3.tgz", 1203 + "integrity": "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==", 1204 + "cpu": [ 1205 + "s390x" 1206 + ], 1207 + "dev": true, 1208 + "license": "MIT", 1209 + "optional": true, 1210 + "os": [ 1211 + "linux" 1212 + ] 1213 + }, 1214 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1215 + "version": "4.55.3", 1216 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.3.tgz", 1217 + "integrity": "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==", 1218 + "cpu": [ 1219 + "x64" 1220 + ], 1221 + "dev": true, 1222 + "license": "MIT", 1223 + "optional": true, 1224 + "os": [ 1225 + "linux" 1226 + ] 1227 + }, 1228 + "node_modules/@rollup/rollup-linux-x64-musl": { 1229 + "version": "4.55.3", 1230 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.3.tgz", 1231 + "integrity": "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==", 1232 + "cpu": [ 1233 + "x64" 1234 + ], 1235 + "dev": true, 1236 + "license": "MIT", 1237 + "optional": true, 1238 + "os": [ 1239 + "linux" 1240 + ] 1241 + }, 1242 + "node_modules/@rollup/rollup-openbsd-x64": { 1243 + "version": "4.55.3", 1244 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.3.tgz", 1245 + "integrity": "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==", 1246 + "cpu": [ 1247 + "x64" 1248 + ], 1249 + "dev": true, 1250 + "license": "MIT", 1251 + "optional": true, 1252 + "os": [ 1253 + "openbsd" 1254 + ] 1255 + }, 1256 + "node_modules/@rollup/rollup-openharmony-arm64": { 1257 + "version": "4.55.3", 1258 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.3.tgz", 1259 + "integrity": "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==", 1260 + "cpu": [ 1261 + "arm64" 1262 + ], 1263 + "dev": true, 1264 + "license": "MIT", 1265 + "optional": true, 1266 + "os": [ 1267 + "openharmony" 1268 + ] 1269 + }, 1270 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1271 + "version": "4.55.3", 1272 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.3.tgz", 1273 + "integrity": "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==", 1274 + "cpu": [ 1275 + "arm64" 1276 + ], 1277 + "dev": true, 1278 + "license": "MIT", 1279 + "optional": true, 1280 + "os": [ 1281 + "win32" 1282 + ] 1283 + }, 1284 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1285 + "version": "4.55.3", 1286 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.3.tgz", 1287 + "integrity": "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==", 1288 + "cpu": [ 1289 + "ia32" 1290 + ], 1291 + "dev": true, 1292 + "license": "MIT", 1293 + "optional": true, 1294 + "os": [ 1295 + "win32" 1296 + ] 1297 + }, 1298 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1299 + "version": "4.55.3", 1300 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.3.tgz", 1301 + "integrity": "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==", 1302 + "cpu": [ 1303 + "x64" 1304 + ], 1305 + "dev": true, 1306 + "license": "MIT", 1307 + "optional": true, 1308 + "os": [ 1309 + "win32" 1310 + ] 1311 + }, 1312 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1313 + "version": "4.55.3", 1314 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.3.tgz", 1315 + "integrity": "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==", 1316 + "cpu": [ 1317 + "x64" 1318 + ], 1319 + "dev": true, 1320 + "license": "MIT", 1321 + "optional": true, 1322 + "os": [ 1323 + "win32" 1324 + ] 1325 + }, 594 1326 "node_modules/@skyware/jetstream": { 595 1327 "version": "0.2.5", 596 1328 "resolved": "https://registry.npmjs.org/@skyware/jetstream/-/jetstream-0.2.5.tgz", ··· 610 1342 "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", 611 1343 "license": "MIT" 612 1344 }, 1345 + "node_modules/@types/bun": { 1346 + "version": "1.3.6", 1347 + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.6.tgz", 1348 + "integrity": "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==", 1349 + "license": "MIT", 1350 + "dependencies": { 1351 + "bun-types": "1.3.6" 1352 + } 1353 + }, 1354 + "node_modules/@types/chai": { 1355 + "version": "5.2.3", 1356 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", 1357 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", 1358 + "dev": true, 1359 + "license": "MIT", 1360 + "dependencies": { 1361 + "@types/deep-eql": "*", 1362 + "assertion-error": "^2.0.1" 1363 + } 1364 + }, 1365 + "node_modules/@types/deep-eql": { 1366 + "version": "4.0.2", 1367 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 1368 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 1369 + "dev": true, 1370 + "license": "MIT" 1371 + }, 1372 + "node_modules/@types/estree": { 1373 + "version": "1.0.8", 1374 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1375 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1376 + "dev": true, 1377 + "license": "MIT" 1378 + }, 613 1379 "node_modules/@types/node": { 614 1380 "version": "24.10.1", 615 1381 "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", 616 1382 "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", 617 - "dev": true, 618 1383 "license": "MIT", 619 1384 "dependencies": { 620 1385 "undici-types": "~7.16.0" ··· 630 1395 "@types/node": "*" 631 1396 } 632 1397 }, 1398 + "node_modules/@vitest/coverage-v8": { 1399 + "version": "4.0.17", 1400 + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz", 1401 + "integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==", 1402 + "dev": true, 1403 + "license": "MIT", 1404 + "dependencies": { 1405 + "@bcoe/v8-coverage": "^1.0.2", 1406 + "@vitest/utils": "4.0.17", 1407 + "ast-v8-to-istanbul": "^0.3.10", 1408 + "istanbul-lib-coverage": "^3.2.2", 1409 + "istanbul-lib-report": "^3.0.1", 1410 + "istanbul-reports": "^3.2.0", 1411 + "magicast": "^0.5.1", 1412 + "obug": "^2.1.1", 1413 + "std-env": "^3.10.0", 1414 + "tinyrainbow": "^3.0.3" 1415 + }, 1416 + "funding": { 1417 + "url": "https://opencollective.com/vitest" 1418 + }, 1419 + "peerDependencies": { 1420 + "@vitest/browser": "4.0.17", 1421 + "vitest": "4.0.17" 1422 + }, 1423 + "peerDependenciesMeta": { 1424 + "@vitest/browser": { 1425 + "optional": true 1426 + } 1427 + } 1428 + }, 1429 + "node_modules/@vitest/coverage-v8/node_modules/magicast": { 1430 + "version": "0.5.1", 1431 + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", 1432 + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", 1433 + "dev": true, 1434 + "license": "MIT", 1435 + "dependencies": { 1436 + "@babel/parser": "^7.28.5", 1437 + "@babel/types": "^7.28.5", 1438 + "source-map-js": "^1.2.1" 1439 + } 1440 + }, 1441 + "node_modules/@vitest/expect": { 1442 + "version": "4.0.17", 1443 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", 1444 + "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", 1445 + "dev": true, 1446 + "license": "MIT", 1447 + "dependencies": { 1448 + "@standard-schema/spec": "^1.0.0", 1449 + "@types/chai": "^5.2.2", 1450 + "@vitest/spy": "4.0.17", 1451 + "@vitest/utils": "4.0.17", 1452 + "chai": "^6.2.1", 1453 + "tinyrainbow": "^3.0.3" 1454 + }, 1455 + "funding": { 1456 + "url": "https://opencollective.com/vitest" 1457 + } 1458 + }, 1459 + "node_modules/@vitest/mocker": { 1460 + "version": "4.0.17", 1461 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", 1462 + "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", 1463 + "dev": true, 1464 + "license": "MIT", 1465 + "dependencies": { 1466 + "@vitest/spy": "4.0.17", 1467 + "estree-walker": "^3.0.3", 1468 + "magic-string": "^0.30.21" 1469 + }, 1470 + "funding": { 1471 + "url": "https://opencollective.com/vitest" 1472 + }, 1473 + "peerDependencies": { 1474 + "msw": "^2.4.9", 1475 + "vite": "^6.0.0 || ^7.0.0-0" 1476 + }, 1477 + "peerDependenciesMeta": { 1478 + "msw": { 1479 + "optional": true 1480 + }, 1481 + "vite": { 1482 + "optional": true 1483 + } 1484 + } 1485 + }, 1486 + "node_modules/@vitest/pretty-format": { 1487 + "version": "4.0.17", 1488 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", 1489 + "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", 1490 + "dev": true, 1491 + "license": "MIT", 1492 + "dependencies": { 1493 + "tinyrainbow": "^3.0.3" 1494 + }, 1495 + "funding": { 1496 + "url": "https://opencollective.com/vitest" 1497 + } 1498 + }, 1499 + "node_modules/@vitest/runner": { 1500 + "version": "4.0.17", 1501 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", 1502 + "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", 1503 + "dev": true, 1504 + "license": "MIT", 1505 + "dependencies": { 1506 + "@vitest/utils": "4.0.17", 1507 + "pathe": "^2.0.3" 1508 + }, 1509 + "funding": { 1510 + "url": "https://opencollective.com/vitest" 1511 + } 1512 + }, 1513 + "node_modules/@vitest/snapshot": { 1514 + "version": "4.0.17", 1515 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", 1516 + "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", 1517 + "dev": true, 1518 + "license": "MIT", 1519 + "dependencies": { 1520 + "@vitest/pretty-format": "4.0.17", 1521 + "magic-string": "^0.30.21", 1522 + "pathe": "^2.0.3" 1523 + }, 1524 + "funding": { 1525 + "url": "https://opencollective.com/vitest" 1526 + } 1527 + }, 1528 + "node_modules/@vitest/spy": { 1529 + "version": "4.0.17", 1530 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz", 1531 + "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==", 1532 + "dev": true, 1533 + "license": "MIT", 1534 + "funding": { 1535 + "url": "https://opencollective.com/vitest" 1536 + } 1537 + }, 1538 + "node_modules/@vitest/utils": { 1539 + "version": "4.0.17", 1540 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz", 1541 + "integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==", 1542 + "dev": true, 1543 + "license": "MIT", 1544 + "dependencies": { 1545 + "@vitest/pretty-format": "4.0.17", 1546 + "tinyrainbow": "^3.0.3" 1547 + }, 1548 + "funding": { 1549 + "url": "https://opencollective.com/vitest" 1550 + } 1551 + }, 1552 + "node_modules/assertion-error": { 1553 + "version": "2.0.1", 1554 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1555 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1556 + "dev": true, 1557 + "license": "MIT", 1558 + "engines": { 1559 + "node": ">=12" 1560 + } 1561 + }, 1562 + "node_modules/ast-v8-to-istanbul": { 1563 + "version": "0.3.10", 1564 + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", 1565 + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", 1566 + "dev": true, 1567 + "license": "MIT", 1568 + "dependencies": { 1569 + "@jridgewell/trace-mapping": "^0.3.31", 1570 + "estree-walker": "^3.0.3", 1571 + "js-tokens": "^9.0.1" 1572 + } 1573 + }, 633 1574 "node_modules/atomic-sleep": { 634 1575 "version": "1.0.0", 635 1576 "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", ··· 639 1580 "node": ">=8.0.0" 640 1581 } 641 1582 }, 1583 + "node_modules/await-lock": { 1584 + "version": "2.2.2", 1585 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 1586 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 1587 + "license": "MIT" 1588 + }, 1589 + "node_modules/bun-types": { 1590 + "version": "1.3.6", 1591 + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.6.tgz", 1592 + "integrity": "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ==", 1593 + "license": "MIT", 1594 + "dependencies": { 1595 + "@types/node": "*" 1596 + } 1597 + }, 642 1598 "node_modules/c12": { 643 1599 "version": "3.1.0", 644 1600 "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", ··· 668 1624 } 669 1625 } 670 1626 }, 1627 + "node_modules/c12/node_modules/dotenv": { 1628 + "version": "16.6.1", 1629 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 1630 + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 1631 + "devOptional": true, 1632 + "license": "BSD-2-Clause", 1633 + "engines": { 1634 + "node": ">=12" 1635 + }, 1636 + "funding": { 1637 + "url": "https://dotenvx.com" 1638 + } 1639 + }, 1640 + "node_modules/chai": { 1641 + "version": "6.2.2", 1642 + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", 1643 + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", 1644 + "dev": true, 1645 + "license": "MIT", 1646 + "engines": { 1647 + "node": ">=18" 1648 + } 1649 + }, 671 1650 "node_modules/chokidar": { 672 1651 "version": "4.0.3", 673 1652 "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", ··· 717 1696 "node": "^14.18.0 || >=16.10.0" 718 1697 } 719 1698 }, 1699 + "node_modules/core-js": { 1700 + "version": "3.48.0", 1701 + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", 1702 + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", 1703 + "hasInstallScript": true, 1704 + "license": "MIT", 1705 + "funding": { 1706 + "type": "opencollective", 1707 + "url": "https://opencollective.com/core-js" 1708 + } 1709 + }, 720 1710 "node_modules/dateformat": { 721 1711 "version": "4.6.3", 722 1712 "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", ··· 751 1741 "license": "MIT" 752 1742 }, 753 1743 "node_modules/dotenv": { 754 - "version": "16.6.1", 755 - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 756 - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 757 - "devOptional": true, 1744 + "version": "17.2.3", 1745 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", 1746 + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", 758 1747 "license": "BSD-2-Clause", 759 1748 "engines": { 760 1749 "node": ">=12" ··· 792 1781 "dependencies": { 793 1782 "once": "^1.4.0" 794 1783 } 1784 + }, 1785 + "node_modules/es-module-lexer": { 1786 + "version": "1.7.0", 1787 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1788 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1789 + "dev": true, 1790 + "license": "MIT" 795 1791 }, 796 1792 "node_modules/esbuild": { 797 1793 "version": "0.27.1", ··· 840 1836 "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 841 1837 "license": "MIT" 842 1838 }, 1839 + "node_modules/estree-walker": { 1840 + "version": "3.0.3", 1841 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1842 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1843 + "dev": true, 1844 + "license": "MIT", 1845 + "dependencies": { 1846 + "@types/estree": "^1.0.0" 1847 + } 1848 + }, 843 1849 "node_modules/event-target-polyfill": { 844 1850 "version": "0.0.4", 845 1851 "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", ··· 852 1858 "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", 853 1859 "license": "MIT" 854 1860 }, 1861 + "node_modules/expect-type": { 1862 + "version": "1.3.0", 1863 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", 1864 + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", 1865 + "dev": true, 1866 + "license": "Apache-2.0", 1867 + "engines": { 1868 + "node": ">=12.0.0" 1869 + } 1870 + }, 855 1871 "node_modules/exsolve": { 856 1872 "version": "1.0.8", 857 1873 "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", ··· 893 1909 "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", 894 1910 "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", 895 1911 "license": "MIT" 1912 + }, 1913 + "node_modules/fdir": { 1914 + "version": "6.5.0", 1915 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 1916 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 1917 + "dev": true, 1918 + "license": "MIT", 1919 + "engines": { 1920 + "node": ">=12.0.0" 1921 + }, 1922 + "peerDependencies": { 1923 + "picomatch": "^3 || ^4" 1924 + }, 1925 + "peerDependenciesMeta": { 1926 + "picomatch": { 1927 + "optional": true 1928 + } 1929 + } 896 1930 }, 897 1931 "node_modules/fsevents": { 898 1932 "version": "2.3.3", ··· 938 1972 "giget": "dist/cli.mjs" 939 1973 } 940 1974 }, 1975 + "node_modules/has-flag": { 1976 + "version": "4.0.0", 1977 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1978 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1979 + "dev": true, 1980 + "license": "MIT", 1981 + "engines": { 1982 + "node": ">=8" 1983 + } 1984 + }, 941 1985 "node_modules/help-me": { 942 1986 "version": "5.0.0", 943 1987 "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", 944 1988 "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", 945 1989 "license": "MIT" 946 1990 }, 1991 + "node_modules/html-escaper": { 1992 + "version": "2.0.2", 1993 + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", 1994 + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", 1995 + "dev": true, 1996 + "license": "MIT" 1997 + }, 1998 + "node_modules/ipaddr.js": { 1999 + "version": "2.3.0", 2000 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", 2001 + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", 2002 + "license": "MIT", 2003 + "engines": { 2004 + "node": ">= 10" 2005 + } 2006 + }, 2007 + "node_modules/iso-datestring-validator": { 2008 + "version": "2.2.2", 2009 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 2010 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 2011 + "license": "MIT" 2012 + }, 2013 + "node_modules/istanbul-lib-coverage": { 2014 + "version": "3.2.2", 2015 + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", 2016 + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", 2017 + "dev": true, 2018 + "license": "BSD-3-Clause", 2019 + "engines": { 2020 + "node": ">=8" 2021 + } 2022 + }, 2023 + "node_modules/istanbul-lib-report": { 2024 + "version": "3.0.1", 2025 + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", 2026 + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", 2027 + "dev": true, 2028 + "license": "BSD-3-Clause", 2029 + "dependencies": { 2030 + "istanbul-lib-coverage": "^3.0.0", 2031 + "make-dir": "^4.0.0", 2032 + "supports-color": "^7.1.0" 2033 + }, 2034 + "engines": { 2035 + "node": ">=10" 2036 + } 2037 + }, 2038 + "node_modules/istanbul-reports": { 2039 + "version": "3.2.0", 2040 + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", 2041 + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", 2042 + "dev": true, 2043 + "license": "BSD-3-Clause", 2044 + "dependencies": { 2045 + "html-escaper": "^2.0.0", 2046 + "istanbul-lib-report": "^3.0.0" 2047 + }, 2048 + "engines": { 2049 + "node": ">=8" 2050 + } 2051 + }, 947 2052 "node_modules/jiti": { 948 2053 "version": "2.6.1", 949 2054 "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", ··· 954 2059 "jiti": "lib/jiti-cli.mjs" 955 2060 } 956 2061 }, 2062 + "node_modules/jose": { 2063 + "version": "5.10.0", 2064 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", 2065 + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", 2066 + "license": "MIT", 2067 + "funding": { 2068 + "url": "https://github.com/sponsors/panva" 2069 + } 2070 + }, 957 2071 "node_modules/joycon": { 958 2072 "version": "3.1.1", 959 2073 "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", ··· 963 2077 "node": ">=10" 964 2078 } 965 2079 }, 2080 + "node_modules/js-tokens": { 2081 + "version": "9.0.1", 2082 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", 2083 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", 2084 + "dev": true, 2085 + "license": "MIT" 2086 + }, 2087 + "node_modules/lru-cache": { 2088 + "version": "10.4.3", 2089 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 2090 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 2091 + "license": "ISC" 2092 + }, 2093 + "node_modules/magic-string": { 2094 + "version": "0.30.21", 2095 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 2096 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 2097 + "dev": true, 2098 + "license": "MIT", 2099 + "dependencies": { 2100 + "@jridgewell/sourcemap-codec": "^1.5.5" 2101 + } 2102 + }, 2103 + "node_modules/magicast": { 2104 + "version": "0.3.5", 2105 + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", 2106 + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", 2107 + "dev": true, 2108 + "license": "MIT", 2109 + "optional": true, 2110 + "peer": true, 2111 + "dependencies": { 2112 + "@babel/parser": "^7.25.4", 2113 + "@babel/types": "^7.25.4", 2114 + "source-map-js": "^1.2.0" 2115 + } 2116 + }, 2117 + "node_modules/make-dir": { 2118 + "version": "4.0.0", 2119 + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", 2120 + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", 2121 + "dev": true, 2122 + "license": "MIT", 2123 + "dependencies": { 2124 + "semver": "^7.5.3" 2125 + }, 2126 + "engines": { 2127 + "node": ">=10" 2128 + }, 2129 + "funding": { 2130 + "url": "https://github.com/sponsors/sindresorhus" 2131 + } 2132 + }, 966 2133 "node_modules/minimist": { 967 2134 "version": "1.2.8", 968 2135 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", ··· 972 2139 "url": "https://github.com/sponsors/ljharb" 973 2140 } 974 2141 }, 2142 + "node_modules/multiformats": { 2143 + "version": "9.9.0", 2144 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 2145 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 2146 + "license": "(Apache-2.0 AND MIT)" 2147 + }, 2148 + "node_modules/nanoid": { 2149 + "version": "3.3.11", 2150 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2151 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2152 + "dev": true, 2153 + "funding": [ 2154 + { 2155 + "type": "github", 2156 + "url": "https://github.com/sponsors/ai" 2157 + } 2158 + ], 2159 + "license": "MIT", 2160 + "bin": { 2161 + "nanoid": "bin/nanoid.cjs" 2162 + }, 2163 + "engines": { 2164 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2165 + } 2166 + }, 975 2167 "node_modules/node-fetch-native": { 976 2168 "version": "1.6.7", 977 2169 "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", ··· 979 2171 "devOptional": true, 980 2172 "license": "MIT" 981 2173 }, 2174 + "node_modules/node-gyp-build": { 2175 + "version": "4.8.4", 2176 + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", 2177 + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", 2178 + "license": "MIT", 2179 + "bin": { 2180 + "node-gyp-build": "bin.js", 2181 + "node-gyp-build-optional": "optional.js", 2182 + "node-gyp-build-test": "build-test.js" 2183 + } 2184 + }, 982 2185 "node_modules/nypm": { 983 2186 "version": "0.6.2", 984 2187 "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", ··· 999 2202 "node": "^14.16.0 || >=16.10.0" 1000 2203 } 1001 2204 }, 2205 + "node_modules/obug": { 2206 + "version": "2.1.1", 2207 + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", 2208 + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", 2209 + "dev": true, 2210 + "funding": [ 2211 + "https://github.com/sponsors/sxzz", 2212 + "https://opencollective.com/debug" 2213 + ], 2214 + "license": "MIT" 2215 + }, 1002 2216 "node_modules/ohash": { 1003 2217 "version": "2.0.11", 1004 2218 "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", ··· 1111 2325 "devOptional": true, 1112 2326 "license": "MIT" 1113 2327 }, 2328 + "node_modules/picocolors": { 2329 + "version": "1.1.1", 2330 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2331 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2332 + "dev": true, 2333 + "license": "ISC" 2334 + }, 2335 + "node_modules/picomatch": { 2336 + "version": "4.0.3", 2337 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 2338 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 2339 + "dev": true, 2340 + "license": "MIT", 2341 + "engines": { 2342 + "node": ">=12" 2343 + }, 2344 + "funding": { 2345 + "url": "https://github.com/sponsors/jonschlinkert" 2346 + } 2347 + }, 1114 2348 "node_modules/pino": { 1115 2349 "version": "10.1.0", 1116 2350 "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", ··· 1191 2425 "confbox": "^0.2.2", 1192 2426 "exsolve": "^1.0.7", 1193 2427 "pathe": "^2.0.3" 2428 + } 2429 + }, 2430 + "node_modules/postcss": { 2431 + "version": "8.5.6", 2432 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 2433 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 2434 + "dev": true, 2435 + "funding": [ 2436 + { 2437 + "type": "opencollective", 2438 + "url": "https://opencollective.com/postcss/" 2439 + }, 2440 + { 2441 + "type": "tidelift", 2442 + "url": "https://tidelift.com/funding/github/npm/postcss" 2443 + }, 2444 + { 2445 + "type": "github", 2446 + "url": "https://github.com/sponsors/ai" 2447 + } 2448 + ], 2449 + "license": "MIT", 2450 + "dependencies": { 2451 + "nanoid": "^3.3.11", 2452 + "picocolors": "^1.1.1", 2453 + "source-map-js": "^1.2.1" 2454 + }, 2455 + "engines": { 2456 + "node": "^10 || ^12 || >=14" 1194 2457 } 1195 2458 }, 1196 2459 "node_modules/prisma": { ··· 1200 2463 "devOptional": true, 1201 2464 "hasInstallScript": true, 1202 2465 "license": "Apache-2.0", 1203 - "peer": true, 1204 2466 "dependencies": { 1205 2467 "@prisma/config": "6.19.0", 1206 2468 "@prisma/engines": "6.19.0" ··· 1312 2574 "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 1313 2575 } 1314 2576 }, 2577 + "node_modules/rollup": { 2578 + "version": "4.55.3", 2579 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz", 2580 + "integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==", 2581 + "dev": true, 2582 + "license": "MIT", 2583 + "dependencies": { 2584 + "@types/estree": "1.0.8" 2585 + }, 2586 + "bin": { 2587 + "rollup": "dist/bin/rollup" 2588 + }, 2589 + "engines": { 2590 + "node": ">=18.0.0", 2591 + "npm": ">=8.0.0" 2592 + }, 2593 + "optionalDependencies": { 2594 + "@rollup/rollup-android-arm-eabi": "4.55.3", 2595 + "@rollup/rollup-android-arm64": "4.55.3", 2596 + "@rollup/rollup-darwin-arm64": "4.55.3", 2597 + "@rollup/rollup-darwin-x64": "4.55.3", 2598 + "@rollup/rollup-freebsd-arm64": "4.55.3", 2599 + "@rollup/rollup-freebsd-x64": "4.55.3", 2600 + "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", 2601 + "@rollup/rollup-linux-arm-musleabihf": "4.55.3", 2602 + "@rollup/rollup-linux-arm64-gnu": "4.55.3", 2603 + "@rollup/rollup-linux-arm64-musl": "4.55.3", 2604 + "@rollup/rollup-linux-loong64-gnu": "4.55.3", 2605 + "@rollup/rollup-linux-loong64-musl": "4.55.3", 2606 + "@rollup/rollup-linux-ppc64-gnu": "4.55.3", 2607 + "@rollup/rollup-linux-ppc64-musl": "4.55.3", 2608 + "@rollup/rollup-linux-riscv64-gnu": "4.55.3", 2609 + "@rollup/rollup-linux-riscv64-musl": "4.55.3", 2610 + "@rollup/rollup-linux-s390x-gnu": "4.55.3", 2611 + "@rollup/rollup-linux-x64-gnu": "4.55.3", 2612 + "@rollup/rollup-linux-x64-musl": "4.55.3", 2613 + "@rollup/rollup-openbsd-x64": "4.55.3", 2614 + "@rollup/rollup-openharmony-arm64": "4.55.3", 2615 + "@rollup/rollup-win32-arm64-msvc": "4.55.3", 2616 + "@rollup/rollup-win32-ia32-msvc": "4.55.3", 2617 + "@rollup/rollup-win32-x64-gnu": "4.55.3", 2618 + "@rollup/rollup-win32-x64-msvc": "4.55.3", 2619 + "fsevents": "~2.3.2" 2620 + } 2621 + }, 1315 2622 "node_modules/safe-stable-stringify": { 1316 2623 "version": "2.5.0", 1317 2624 "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", ··· 1337 2644 ], 1338 2645 "license": "BSD-3-Clause" 1339 2646 }, 2647 + "node_modules/semver": { 2648 + "version": "7.7.3", 2649 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 2650 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 2651 + "dev": true, 2652 + "license": "ISC", 2653 + "bin": { 2654 + "semver": "bin/semver.js" 2655 + }, 2656 + "engines": { 2657 + "node": ">=10" 2658 + } 2659 + }, 2660 + "node_modules/siginfo": { 2661 + "version": "2.0.0", 2662 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2663 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2664 + "dev": true, 2665 + "license": "ISC" 2666 + }, 1340 2667 "node_modules/sonic-boom": { 1341 2668 "version": "4.2.0", 1342 2669 "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", ··· 1346 2673 "atomic-sleep": "^1.0.0" 1347 2674 } 1348 2675 }, 2676 + "node_modules/source-map-js": { 2677 + "version": "1.2.1", 2678 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2679 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2680 + "dev": true, 2681 + "license": "BSD-3-Clause", 2682 + "engines": { 2683 + "node": ">=0.10.0" 2684 + } 2685 + }, 1349 2686 "node_modules/split2": { 1350 2687 "version": "4.2.0", 1351 2688 "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", ··· 1355 2692 "node": ">= 10.x" 1356 2693 } 1357 2694 }, 2695 + "node_modules/stackback": { 2696 + "version": "0.0.2", 2697 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2698 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2699 + "dev": true, 2700 + "license": "MIT" 2701 + }, 2702 + "node_modules/std-env": { 2703 + "version": "3.10.0", 2704 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", 2705 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", 2706 + "dev": true, 2707 + "license": "MIT" 2708 + }, 1358 2709 "node_modules/strip-json-comments": { 1359 2710 "version": "5.0.3", 1360 2711 "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", ··· 1367 2718 "url": "https://github.com/sponsors/sindresorhus" 1368 2719 } 1369 2720 }, 2721 + "node_modules/supports-color": { 2722 + "version": "7.2.0", 2723 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 2724 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 2725 + "dev": true, 2726 + "license": "MIT", 2727 + "dependencies": { 2728 + "has-flag": "^4.0.0" 2729 + }, 2730 + "engines": { 2731 + "node": ">=8" 2732 + } 2733 + }, 1370 2734 "node_modules/thread-stream": { 1371 2735 "version": "3.1.0", 1372 2736 "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", ··· 1382 2746 "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", 1383 2747 "license": "MIT" 1384 2748 }, 2749 + "node_modules/tinybench": { 2750 + "version": "2.9.0", 2751 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2752 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2753 + "dev": true, 2754 + "license": "MIT" 2755 + }, 1385 2756 "node_modules/tinyexec": { 1386 2757 "version": "1.0.2", 1387 2758 "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", ··· 1392 2763 "node": ">=18" 1393 2764 } 1394 2765 }, 2766 + "node_modules/tinyglobby": { 2767 + "version": "0.2.15", 2768 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 2769 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 2770 + "dev": true, 2771 + "license": "MIT", 2772 + "dependencies": { 2773 + "fdir": "^6.5.0", 2774 + "picomatch": "^4.0.3" 2775 + }, 2776 + "engines": { 2777 + "node": ">=12.0.0" 2778 + }, 2779 + "funding": { 2780 + "url": "https://github.com/sponsors/SuperchupuDev" 2781 + } 2782 + }, 2783 + "node_modules/tinyrainbow": { 2784 + "version": "3.0.3", 2785 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", 2786 + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", 2787 + "dev": true, 2788 + "license": "MIT", 2789 + "engines": { 2790 + "node": ">=14.0.0" 2791 + } 2792 + }, 2793 + "node_modules/tlds": { 2794 + "version": "1.261.0", 2795 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", 2796 + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", 2797 + "license": "MIT", 2798 + "bin": { 2799 + "tlds": "bin.js" 2800 + } 2801 + }, 2802 + "node_modules/tslib": { 2803 + "version": "2.8.1", 2804 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2805 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2806 + "license": "0BSD" 2807 + }, 1395 2808 "node_modules/tsx": { 1396 2809 "version": "4.21.0", 1397 2810 "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", ··· 1411 2824 "fsevents": "~2.3.3" 1412 2825 } 1413 2826 }, 2827 + "node_modules/uint8arrays": { 2828 + "version": "3.0.0", 2829 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 2830 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 2831 + "license": "MIT", 2832 + "dependencies": { 2833 + "multiformats": "^9.4.2" 2834 + } 2835 + }, 2836 + "node_modules/undici": { 2837 + "version": "6.23.0", 2838 + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", 2839 + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", 2840 + "license": "MIT", 2841 + "engines": { 2842 + "node": ">=18.17" 2843 + } 2844 + }, 1414 2845 "node_modules/undici-types": { 1415 2846 "version": "7.16.0", 1416 2847 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", 1417 2848 "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", 1418 - "dev": true, 2849 + "license": "MIT" 2850 + }, 2851 + "node_modules/unicode-segmenter": { 2852 + "version": "0.14.5", 2853 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", 2854 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 1419 2855 "license": "MIT" 1420 2856 }, 2857 + "node_modules/vite": { 2858 + "version": "7.3.1", 2859 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", 2860 + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", 2861 + "dev": true, 2862 + "license": "MIT", 2863 + "dependencies": { 2864 + "esbuild": "^0.27.0", 2865 + "fdir": "^6.5.0", 2866 + "picomatch": "^4.0.3", 2867 + "postcss": "^8.5.6", 2868 + "rollup": "^4.43.0", 2869 + "tinyglobby": "^0.2.15" 2870 + }, 2871 + "bin": { 2872 + "vite": "bin/vite.js" 2873 + }, 2874 + "engines": { 2875 + "node": "^20.19.0 || >=22.12.0" 2876 + }, 2877 + "funding": { 2878 + "url": "https://github.com/vitejs/vite?sponsor=1" 2879 + }, 2880 + "optionalDependencies": { 2881 + "fsevents": "~2.3.3" 2882 + }, 2883 + "peerDependencies": { 2884 + "@types/node": "^20.19.0 || >=22.12.0", 2885 + "jiti": ">=1.21.0", 2886 + "less": "^4.0.0", 2887 + "lightningcss": "^1.21.0", 2888 + "sass": "^1.70.0", 2889 + "sass-embedded": "^1.70.0", 2890 + "stylus": ">=0.54.8", 2891 + "sugarss": "^5.0.0", 2892 + "terser": "^5.16.0", 2893 + "tsx": "^4.8.1", 2894 + "yaml": "^2.4.2" 2895 + }, 2896 + "peerDependenciesMeta": { 2897 + "@types/node": { 2898 + "optional": true 2899 + }, 2900 + "jiti": { 2901 + "optional": true 2902 + }, 2903 + "less": { 2904 + "optional": true 2905 + }, 2906 + "lightningcss": { 2907 + "optional": true 2908 + }, 2909 + "sass": { 2910 + "optional": true 2911 + }, 2912 + "sass-embedded": { 2913 + "optional": true 2914 + }, 2915 + "stylus": { 2916 + "optional": true 2917 + }, 2918 + "sugarss": { 2919 + "optional": true 2920 + }, 2921 + "terser": { 2922 + "optional": true 2923 + }, 2924 + "tsx": { 2925 + "optional": true 2926 + }, 2927 + "yaml": { 2928 + "optional": true 2929 + } 2930 + } 2931 + }, 2932 + "node_modules/vitest": { 2933 + "version": "4.0.17", 2934 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz", 2935 + "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", 2936 + "dev": true, 2937 + "license": "MIT", 2938 + "dependencies": { 2939 + "@vitest/expect": "4.0.17", 2940 + "@vitest/mocker": "4.0.17", 2941 + "@vitest/pretty-format": "4.0.17", 2942 + "@vitest/runner": "4.0.17", 2943 + "@vitest/snapshot": "4.0.17", 2944 + "@vitest/spy": "4.0.17", 2945 + "@vitest/utils": "4.0.17", 2946 + "es-module-lexer": "^1.7.0", 2947 + "expect-type": "^1.2.2", 2948 + "magic-string": "^0.30.21", 2949 + "obug": "^2.1.1", 2950 + "pathe": "^2.0.3", 2951 + "picomatch": "^4.0.3", 2952 + "std-env": "^3.10.0", 2953 + "tinybench": "^2.9.0", 2954 + "tinyexec": "^1.0.2", 2955 + "tinyglobby": "^0.2.15", 2956 + "tinyrainbow": "^3.0.3", 2957 + "vite": "^6.0.0 || ^7.0.0", 2958 + "why-is-node-running": "^2.3.0" 2959 + }, 2960 + "bin": { 2961 + "vitest": "vitest.mjs" 2962 + }, 2963 + "engines": { 2964 + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" 2965 + }, 2966 + "funding": { 2967 + "url": "https://opencollective.com/vitest" 2968 + }, 2969 + "peerDependencies": { 2970 + "@edge-runtime/vm": "*", 2971 + "@opentelemetry/api": "^1.9.0", 2972 + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", 2973 + "@vitest/browser-playwright": "4.0.17", 2974 + "@vitest/browser-preview": "4.0.17", 2975 + "@vitest/browser-webdriverio": "4.0.17", 2976 + "@vitest/ui": "4.0.17", 2977 + "happy-dom": "*", 2978 + "jsdom": "*" 2979 + }, 2980 + "peerDependenciesMeta": { 2981 + "@edge-runtime/vm": { 2982 + "optional": true 2983 + }, 2984 + "@opentelemetry/api": { 2985 + "optional": true 2986 + }, 2987 + "@types/node": { 2988 + "optional": true 2989 + }, 2990 + "@vitest/browser-playwright": { 2991 + "optional": true 2992 + }, 2993 + "@vitest/browser-preview": { 2994 + "optional": true 2995 + }, 2996 + "@vitest/browser-webdriverio": { 2997 + "optional": true 2998 + }, 2999 + "@vitest/ui": { 3000 + "optional": true 3001 + }, 3002 + "happy-dom": { 3003 + "optional": true 3004 + }, 3005 + "jsdom": { 3006 + "optional": true 3007 + } 3008 + } 3009 + }, 3010 + "node_modules/why-is-node-running": { 3011 + "version": "2.3.0", 3012 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 3013 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 3014 + "dev": true, 3015 + "license": "MIT", 3016 + "dependencies": { 3017 + "siginfo": "^2.0.0", 3018 + "stackback": "0.0.2" 3019 + }, 3020 + "bin": { 3021 + "why-is-node-running": "cli.js" 3022 + }, 3023 + "engines": { 3024 + "node": ">=8" 3025 + } 3026 + }, 1421 3027 "node_modules/wrappy": { 1422 3028 "version": "1.0.2", 1423 3029 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", ··· 1429 3035 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 1430 3036 "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 1431 3037 "license": "MIT", 1432 - "peer": true, 1433 3038 "engines": { 1434 3039 "node": ">=10.0.0" 1435 3040 }, ··· 1456 3061 }, 1457 3062 "funding": { 1458 3063 "url": "https://github.com/sponsors/sindresorhus" 3064 + } 3065 + }, 3066 + "node_modules/zod": { 3067 + "version": "3.25.76", 3068 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 3069 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 3070 + "license": "MIT", 3071 + "funding": { 3072 + "url": "https://github.com/sponsors/colinhacks" 1459 3073 } 1460 3074 } 1461 3075 }
+7 -2
backend/package.json
··· 11 11 "prisma:generate": "npx prisma generate", 12 12 "docker:build": "docker build -t rito-backend:latest .", 13 13 "docker:run": "docker run -it --rm --name rito-backend --env-file .env rito-backend:latest", 14 - "docker:push": "docker buildx build --platform linux/amd64 -t ghcr.io/usounds/rito-backend:latest . --push" 14 + "docker:push": "docker buildx build --platform linux/amd64 -t ghcr.io/usounds/rito-backend:latest . --push", 15 + "test": "vitest run", 16 + "test:watch": "vitest", 17 + "test:coverage": "vitest run --coverage" 15 18 }, 16 19 "keywords": [], 17 20 "author": "", ··· 37 40 "devDependencies": { 38 41 "@types/node": "^24.10.1", 39 42 "@types/ws": "^8.18.1", 40 - "prisma": "^6.19.0" 43 + "@vitest/coverage-v8": "^4.0.17", 44 + "prisma": "^6.19.0", 45 + "vitest": "^4.0.17" 41 46 }, 42 47 "pnpm": { 43 48 "overrides": {
+46
backend/src/__tests__/config.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 2 + 3 + describe('config', () => { 4 + const originalEnv = process.env; 5 + 6 + beforeEach(() => { 7 + vi.resetModules(); 8 + process.env = { ...originalEnv }; 9 + }); 10 + 11 + afterEach(() => { 12 + process.env = originalEnv; 13 + }); 14 + 15 + it('should export BOOKMARK constant', async () => { 16 + const { BOOKMARK } = await import('../config.js'); 17 + expect(BOOKMARK).toBe('blue.rito.feed.bookmark'); 18 + }); 19 + 20 + it('should export LIKE constant', async () => { 21 + const { LIKE } = await import('../config.js'); 22 + expect(LIKE).toBe('blue.rito.feed.like'); 23 + }); 24 + 25 + it('should export SERVICE constant', async () => { 26 + const { SERVICE } = await import('../config.js'); 27 + expect(SERVICE).toBe('blue.rito.service.schema'); 28 + }); 29 + 30 + it('should export POST_COLLECTION constant', async () => { 31 + const { POST_COLLECTION } = await import('../config.js'); 32 + expect(POST_COLLECTION).toBe('app.bsky.feed.post'); 33 + }); 34 + 35 + it('should use default JETSTREAM_URL when env is not set', async () => { 36 + delete process.env.JETSREAM_URL; 37 + const { JETSREAM_URL } = await import('../config.js'); 38 + expect(JETSREAM_URL).toBe('wss://jetstream2.us-west.bsky.network/subscribe'); 39 + }); 40 + 41 + it('should use default CURSOR_UPDATE_INTERVAL when env is not set', async () => { 42 + delete process.env.CURSOR_UPDATE_INTERVAL; 43 + const { CURSOR_UPDATE_INTERVAL } = await import('../config.js'); 44 + expect(CURSOR_UPDATE_INTERVAL).toBe(60000); 45 + }); 46 + });
+529
backend/src/__tests__/db.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 + 3 + // Mock Prisma and database functions 4 + const mockPrisma = { 5 + jetstreamIndex: { 6 + findUnique: vi.fn(), 7 + upsert: vi.fn(), 8 + }, 9 + userDidHandle: { 10 + upsert: vi.fn(), 11 + }, 12 + bookmark: { 13 + upsert: vi.fn(), 14 + findFirst: vi.fn(), 15 + deleteMany: vi.fn(), 16 + }, 17 + comment: { 18 + upsert: vi.fn(), 19 + deleteMany: vi.fn(), 20 + }, 21 + tag: { 22 + upsert: vi.fn(), 23 + }, 24 + bookmarkTag: { 25 + findMany: vi.fn(), 26 + deleteMany: vi.fn(), 27 + create: vi.fn(), 28 + }, 29 + postToBookmark: { 30 + findUnique: vi.fn(), 31 + }, 32 + resolver: { 33 + upsert: vi.fn(), 34 + deleteMany: vi.fn(), 35 + }, 36 + like: { 37 + upsert: vi.fn(), 38 + deleteMany: vi.fn(), 39 + }, 40 + }; 41 + 42 + // Create testable versions of the database functions 43 + function createDbService(prisma: typeof mockPrisma) { 44 + const dbLimit = async <T>(fn: () => Promise<T>): Promise<T> => fn(); 45 + 46 + return { 47 + async loadCursor(epochUsToDateTime: (cursor: string | number) => string): Promise<string> { 48 + try { 49 + const indexRecord = await prisma.jetstreamIndex.findUnique({ 50 + where: { service: 'rito' } 51 + }); 52 + if (indexRecord && indexRecord.index) { 53 + return indexRecord.index; 54 + } else { 55 + return Date.now().toString(); 56 + } 57 + } catch { 58 + return Date.now().toString(); 59 + } 60 + }, 61 + 62 + async upsertCursor(currentCursor: string): Promise<void> { 63 + await prisma.jetstreamIndex.upsert({ 64 + where: { service: 'rito' }, 65 + update: { index: currentCursor }, 66 + create: { service: 'rito', index: currentCursor }, 67 + }); 68 + }, 69 + 70 + async upsertUserDidHandle(did: string, handle: string): Promise<void> { 71 + await dbLimit(() => 72 + prisma.userDidHandle.upsert({ 73 + where: { did }, 74 + update: { handle }, 75 + create: { did, handle }, 76 + }) 77 + ); 78 + }, 79 + 80 + async upsertBookmarkRecord(data: { 81 + uri: string; 82 + did: string; 83 + subject: string; 84 + ogpTitle?: string; 85 + ogpDescription?: string; 86 + ogpImage?: string; 87 + moderationResult: string | null; 88 + handle: string; 89 + createdAt?: Date; 90 + }): Promise<void> { 91 + await dbLimit(() => 92 + prisma.bookmark.upsert({ 93 + where: { uri: data.uri }, 94 + update: { 95 + subject: data.subject, 96 + ogp_title: data.ogpTitle, 97 + ogp_description: data.ogpDescription, 98 + ogp_image: data.ogpImage, 99 + moderation_result: data.moderationResult, 100 + handle: data.handle, 101 + indexed_at: new Date(), 102 + }, 103 + create: { 104 + uri: data.uri, 105 + did: data.did, 106 + subject: data.subject, 107 + ogp_title: data.ogpTitle, 108 + ogp_description: data.ogpDescription, 109 + ogp_image: data.ogpImage, 110 + moderation_result: data.moderationResult, 111 + handle: data.handle, 112 + created_at: data.createdAt ?? new Date(), 113 + indexed_at: new Date(), 114 + }, 115 + }) 116 + ); 117 + }, 118 + 119 + async upsertComment(data: { 120 + bookmarkUri: string; 121 + lang: string; 122 + title?: string; 123 + comment?: string; 124 + moderationResult: string | null; 125 + }): Promise<void> { 126 + await dbLimit(() => 127 + prisma.comment.upsert({ 128 + where: { 129 + bookmark_uri_lang: { 130 + bookmark_uri: data.bookmarkUri, 131 + lang: data.lang, 132 + }, 133 + }, 134 + update: { 135 + title: data.title, 136 + comment: data.comment, 137 + moderation_result: data.moderationResult, 138 + }, 139 + create: { 140 + bookmark_uri: data.bookmarkUri, 141 + lang: data.lang, 142 + title: data.title, 143 + comment: data.comment, 144 + moderation_result: data.moderationResult, 145 + }, 146 + }) 147 + ); 148 + }, 149 + 150 + async deleteCommentsNotInLangs(bookmarkUri: string, langs: string[]): Promise<void> { 151 + await prisma.comment.deleteMany({ 152 + where: { 153 + bookmark_uri: bookmarkUri, 154 + NOT: { 155 + lang: { in: langs }, 156 + }, 157 + }, 158 + }); 159 + }, 160 + 161 + async safeUpsertTag(name: string): Promise<{ id: number; name: string } | null> { 162 + try { 163 + return await dbLimit(() => 164 + prisma.tag.upsert({ 165 + where: { name }, 166 + update: {}, 167 + create: { name }, 168 + }) 169 + ) as { id: number; name: string }; 170 + } catch { 171 + return null; 172 + } 173 + }, 174 + 175 + async getBookmarkTagIds(bookmarkUri: string): Promise<number[]> { 176 + const tags = await prisma.bookmarkTag.findMany({ 177 + where: { bookmark_uri: bookmarkUri }, 178 + select: { tag_id: true }, 179 + }); 180 + return tags.map((r: { tag_id: number }) => r.tag_id); 181 + }, 182 + 183 + async deleteBookmarkTags(bookmarkUri: string, tagIds: number[]): Promise<void> { 184 + await dbLimit(() => 185 + prisma.bookmarkTag.deleteMany({ 186 + where: { bookmark_uri: bookmarkUri, tag_id: { in: tagIds } }, 187 + }) 188 + ); 189 + }, 190 + 191 + async createBookmarkTag(bookmarkUri: string, tagId: number): Promise<void> { 192 + await dbLimit(() => 193 + prisma.bookmarkTag.create({ data: { bookmark_uri: bookmarkUri, tag_id: tagId } }) 194 + ); 195 + }, 196 + 197 + async findPostToBookmark(sub: string): Promise<{ sub: string; lang?: string } | null> { 198 + return await prisma.postToBookmark.findUnique({ 199 + where: { sub }, 200 + select: { sub: true, lang: true }, 201 + }); 202 + }, 203 + 204 + async findExistingBookmark(did: string, subject: string): Promise<{ uri: string } | null> { 205 + return await prisma.bookmark.findFirst({ 206 + where: { did, subject }, 207 + select: { uri: true }, 208 + }); 209 + }, 210 + 211 + async upsertResolverRecord(nsid: string, did: string, schema: string, verified: boolean): Promise<void> { 212 + await dbLimit(() => 213 + prisma.resolver.upsert({ 214 + where: { nsid_did: { nsid, did } }, 215 + update: { schema, verified, indexed_at: new Date() }, 216 + create: { nsid, did, schema, verified, indexed_at: new Date() }, 217 + }) 218 + ); 219 + }, 220 + 221 + async upsertLikeRecord(aturi: string, subject: string, did: string, createdAt: Date): Promise<void> { 222 + await dbLimit(() => 223 + prisma.like.upsert({ 224 + where: { aturi }, 225 + update: { created_at: createdAt }, 226 + create: { aturi, subject, did, created_at: createdAt }, 227 + }) 228 + ); 229 + }, 230 + 231 + async deleteBookmark(uri: string): Promise<{ count: number }> { 232 + return await dbLimit(() => 233 + prisma.bookmark.deleteMany({ where: { uri } }) 234 + ); 235 + }, 236 + 237 + async deleteLike(aturi: string): Promise<{ count: number }> { 238 + return await dbLimit(() => 239 + prisma.like.deleteMany({ where: { aturi } }) 240 + ); 241 + }, 242 + 243 + async deleteResolver(nsid: string, did: string): Promise<{ count: number }> { 244 + return await dbLimit(() => 245 + prisma.resolver.deleteMany({ where: { nsid, did } }) 246 + ); 247 + }, 248 + }; 249 + } 250 + 251 + describe('Database Service', () => { 252 + let dbService: ReturnType<typeof createDbService>; 253 + 254 + beforeEach(() => { 255 + vi.clearAllMocks(); 256 + dbService = createDbService(mockPrisma); 257 + }); 258 + 259 + describe('loadCursor', () => { 260 + it('should return cursor from database when found', async () => { 261 + const epochUsToDateTime = (c: string | number) => new Date(Number(c) / 1000).toISOString(); 262 + mockPrisma.jetstreamIndex.findUnique.mockResolvedValue({ index: '1704067200000000' }); 263 + 264 + const result = await dbService.loadCursor(epochUsToDateTime); 265 + 266 + expect(result).toBe('1704067200000000'); 267 + expect(mockPrisma.jetstreamIndex.findUnique).toHaveBeenCalledWith({ 268 + where: { service: 'rito' } 269 + }); 270 + }); 271 + 272 + it('should return current time when no cursor found', async () => { 273 + const epochUsToDateTime = (c: string | number) => new Date(Number(c) / 1000).toISOString(); 274 + mockPrisma.jetstreamIndex.findUnique.mockResolvedValue(null); 275 + const before = Date.now(); 276 + 277 + const result = await dbService.loadCursor(epochUsToDateTime); 278 + 279 + expect(Number(result)).toBeGreaterThanOrEqual(before); 280 + }); 281 + 282 + it('should return current time on error', async () => { 283 + const epochUsToDateTime = (c: string | number) => new Date(Number(c) / 1000).toISOString(); 284 + mockPrisma.jetstreamIndex.findUnique.mockRejectedValue(new Error('DB error')); 285 + const before = Date.now(); 286 + 287 + const result = await dbService.loadCursor(epochUsToDateTime); 288 + 289 + expect(Number(result)).toBeGreaterThanOrEqual(before); 290 + }); 291 + }); 292 + 293 + describe('upsertCursor', () => { 294 + it('should call upsert with correct parameters', async () => { 295 + mockPrisma.jetstreamIndex.upsert.mockResolvedValue({}); 296 + 297 + await dbService.upsertCursor('1704067200000000'); 298 + 299 + expect(mockPrisma.jetstreamIndex.upsert).toHaveBeenCalledWith({ 300 + where: { service: 'rito' }, 301 + update: { index: '1704067200000000' }, 302 + create: { service: 'rito', index: '1704067200000000' }, 303 + }); 304 + }); 305 + }); 306 + 307 + describe('upsertUserDidHandle', () => { 308 + it('should upsert user DID handle mapping', async () => { 309 + mockPrisma.userDidHandle.upsert.mockResolvedValue({}); 310 + 311 + await dbService.upsertUserDidHandle('did:plc:abc', 'user.bsky.social'); 312 + 313 + expect(mockPrisma.userDidHandle.upsert).toHaveBeenCalledWith({ 314 + where: { did: 'did:plc:abc' }, 315 + update: { handle: 'user.bsky.social' }, 316 + create: { did: 'did:plc:abc', handle: 'user.bsky.social' }, 317 + }); 318 + }); 319 + }); 320 + 321 + describe('upsertBookmarkRecord', () => { 322 + it('should upsert bookmark with all fields', async () => { 323 + mockPrisma.bookmark.upsert.mockResolvedValue({}); 324 + 325 + await dbService.upsertBookmarkRecord({ 326 + uri: 'at://did:plc:abc/blue.rito.feed.bookmark/123', 327 + did: 'did:plc:abc', 328 + subject: 'https://example.com', 329 + ogpTitle: 'Test Title', 330 + ogpDescription: 'Test Description', 331 + ogpImage: 'https://example.com/og.jpg', 332 + moderationResult: null, 333 + handle: 'user.bsky.social', 334 + }); 335 + 336 + expect(mockPrisma.bookmark.upsert).toHaveBeenCalled(); 337 + const call = mockPrisma.bookmark.upsert.mock.calls[0][0]; 338 + expect(call.where.uri).toBe('at://did:plc:abc/blue.rito.feed.bookmark/123'); 339 + }); 340 + }); 341 + 342 + describe('upsertComment', () => { 343 + it('should upsert comment', async () => { 344 + mockPrisma.comment.upsert.mockResolvedValue({}); 345 + 346 + await dbService.upsertComment({ 347 + bookmarkUri: 'at://did:plc:abc/blue.rito.feed.bookmark/123', 348 + lang: 'ja', 349 + title: 'タイトル', 350 + comment: 'コメント', 351 + moderationResult: null, 352 + }); 353 + 354 + expect(mockPrisma.comment.upsert).toHaveBeenCalled(); 355 + }); 356 + }); 357 + 358 + describe('deleteCommentsNotInLangs', () => { 359 + it('should delete comments not in specified languages', async () => { 360 + mockPrisma.comment.deleteMany.mockResolvedValue({ count: 2 }); 361 + 362 + await dbService.deleteCommentsNotInLangs('at://did:plc:abc/blue.rito.feed.bookmark/123', ['ja', 'en']); 363 + 364 + expect(mockPrisma.comment.deleteMany).toHaveBeenCalledWith({ 365 + where: { 366 + bookmark_uri: 'at://did:plc:abc/blue.rito.feed.bookmark/123', 367 + NOT: { 368 + lang: { in: ['ja', 'en'] }, 369 + }, 370 + }, 371 + }); 372 + }); 373 + }); 374 + 375 + describe('safeUpsertTag', () => { 376 + it('should return tag on success', async () => { 377 + mockPrisma.tag.upsert.mockResolvedValue({ id: 1, name: 'test' }); 378 + 379 + const result = await dbService.safeUpsertTag('test'); 380 + 381 + expect(result).toEqual({ id: 1, name: 'test' }); 382 + }); 383 + 384 + it('should return null on error', async () => { 385 + mockPrisma.tag.upsert.mockRejectedValue(new Error('Unique constraint')); 386 + 387 + const result = await dbService.safeUpsertTag('test'); 388 + 389 + expect(result).toBeNull(); 390 + }); 391 + }); 392 + 393 + describe('getBookmarkTagIds', () => { 394 + it('should return tag IDs', async () => { 395 + mockPrisma.bookmarkTag.findMany.mockResolvedValue([ 396 + { tag_id: 1 }, 397 + { tag_id: 2 }, 398 + { tag_id: 3 }, 399 + ]); 400 + 401 + const result = await dbService.getBookmarkTagIds('at://bookmark'); 402 + 403 + expect(result).toEqual([1, 2, 3]); 404 + }); 405 + 406 + it('should return empty array when no tags', async () => { 407 + mockPrisma.bookmarkTag.findMany.mockResolvedValue([]); 408 + 409 + const result = await dbService.getBookmarkTagIds('at://bookmark'); 410 + 411 + expect(result).toEqual([]); 412 + }); 413 + }); 414 + 415 + describe('deleteBookmarkTags', () => { 416 + it('should delete specified tags', async () => { 417 + mockPrisma.bookmarkTag.deleteMany.mockResolvedValue({ count: 2 }); 418 + 419 + await dbService.deleteBookmarkTags('at://bookmark', [1, 2]); 420 + 421 + expect(mockPrisma.bookmarkTag.deleteMany).toHaveBeenCalledWith({ 422 + where: { bookmark_uri: 'at://bookmark', tag_id: { in: [1, 2] } }, 423 + }); 424 + }); 425 + }); 426 + 427 + describe('createBookmarkTag', () => { 428 + it('should create bookmark tag', async () => { 429 + mockPrisma.bookmarkTag.create.mockResolvedValue({}); 430 + 431 + await dbService.createBookmarkTag('at://bookmark', 5); 432 + 433 + expect(mockPrisma.bookmarkTag.create).toHaveBeenCalledWith({ 434 + data: { bookmark_uri: 'at://bookmark', tag_id: 5 }, 435 + }); 436 + }); 437 + }); 438 + 439 + describe('findPostToBookmark', () => { 440 + it('should find post to bookmark record', async () => { 441 + mockPrisma.postToBookmark.findUnique.mockResolvedValue({ sub: 'did:plc:abc', lang: 'ja' }); 442 + 443 + const result = await dbService.findPostToBookmark('did:plc:abc'); 444 + 445 + expect(result).toEqual({ sub: 'did:plc:abc', lang: 'ja' }); 446 + }); 447 + 448 + it('should return null when not found', async () => { 449 + mockPrisma.postToBookmark.findUnique.mockResolvedValue(null); 450 + 451 + const result = await dbService.findPostToBookmark('did:plc:nonexistent'); 452 + 453 + expect(result).toBeNull(); 454 + }); 455 + }); 456 + 457 + describe('findExistingBookmark', () => { 458 + it('should find existing bookmark', async () => { 459 + mockPrisma.bookmark.findFirst.mockResolvedValue({ uri: 'at://bookmark' }); 460 + 461 + const result = await dbService.findExistingBookmark('did:plc:abc', 'https://example.com'); 462 + 463 + expect(result).toEqual({ uri: 'at://bookmark' }); 464 + }); 465 + 466 + it('should return null when no bookmark exists', async () => { 467 + mockPrisma.bookmark.findFirst.mockResolvedValue(null); 468 + 469 + const result = await dbService.findExistingBookmark('did:plc:abc', 'https://new.com'); 470 + 471 + expect(result).toBeNull(); 472 + }); 473 + }); 474 + 475 + describe('upsertResolverRecord', () => { 476 + it('should upsert resolver', async () => { 477 + mockPrisma.resolver.upsert.mockResolvedValue({}); 478 + 479 + await dbService.upsertResolverRecord('blue.rito.feed.bookmark', 'did:plc:abc', 'https://schema', true); 480 + 481 + expect(mockPrisma.resolver.upsert).toHaveBeenCalled(); 482 + }); 483 + }); 484 + 485 + describe('upsertLikeRecord', () => { 486 + it('should upsert like', async () => { 487 + mockPrisma.like.upsert.mockResolvedValue({}); 488 + const date = new Date(); 489 + 490 + await dbService.upsertLikeRecord('at://like', 'at://subject', 'did:plc:abc', date); 491 + 492 + expect(mockPrisma.like.upsert).toHaveBeenCalledWith({ 493 + where: { aturi: 'at://like' }, 494 + update: { created_at: date }, 495 + create: { aturi: 'at://like', subject: 'at://subject', did: 'did:plc:abc', created_at: date }, 496 + }); 497 + }); 498 + }); 499 + 500 + describe('deleteBookmark', () => { 501 + it('should delete bookmark and return count', async () => { 502 + mockPrisma.bookmark.deleteMany.mockResolvedValue({ count: 1 }); 503 + 504 + const result = await dbService.deleteBookmark('at://bookmark'); 505 + 506 + expect(result).toEqual({ count: 1 }); 507 + }); 508 + }); 509 + 510 + describe('deleteLike', () => { 511 + it('should delete like and return count', async () => { 512 + mockPrisma.like.deleteMany.mockResolvedValue({ count: 1 }); 513 + 514 + const result = await dbService.deleteLike('at://like'); 515 + 516 + expect(result).toEqual({ count: 1 }); 517 + }); 518 + }); 519 + 520 + describe('deleteResolver', () => { 521 + it('should delete resolver and return count', async () => { 522 + mockPrisma.resolver.deleteMany.mockResolvedValue({ count: 1 }); 523 + 524 + const result = await dbService.deleteResolver('blue.rito.feed.bookmark', 'did:plc:abc'); 525 + 526 + expect(result).toEqual({ count: 1 }); 527 + }); 528 + }); 529 + });
+28
backend/src/__tests__/logger.test.ts
··· 1 + import { describe, it, expect, vi } from 'vitest'; 2 + 3 + describe('logger', () => { 4 + it('should export a logger instance', async () => { 5 + const logger = (await import('../logger.js')).default; 6 + expect(logger).toBeDefined(); 7 + expect(typeof logger.info).toBe('function'); 8 + expect(typeof logger.error).toBe('function'); 9 + expect(typeof logger.warn).toBe('function'); 10 + expect(typeof logger.debug).toBe('function'); 11 + }); 12 + 13 + it('should have info method that can be called', async () => { 14 + const logger = (await import('../logger.js')).default; 15 + // Just verify it doesn't throw 16 + expect(() => logger.info('Test log message')).not.toThrow(); 17 + }); 18 + 19 + it('should have error method that can be called', async () => { 20 + const logger = (await import('../logger.js')).default; 21 + expect(() => logger.error('Test error message')).not.toThrow(); 22 + }); 23 + 24 + it('should have warn method that can be called', async () => { 25 + const logger = (await import('../logger.js')).default; 26 + expect(() => logger.warn('Test warning message')).not.toThrow(); 27 + }); 28 + });
+303
backend/src/__tests__/oauth.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 2 + import { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node'; 3 + 4 + // Define store interfaces for testing 5 + interface StateStore { 6 + set(key: string, internalState: NodeSavedState): Promise<void>; 7 + get(key: string): Promise<NodeSavedState | undefined>; 8 + del(key: string): Promise<void>; 9 + } 10 + 11 + interface SessionStore { 12 + set(sub: string, session: NodeSavedSession): Promise<void>; 13 + get(sub: string): Promise<NodeSavedSession | undefined>; 14 + del(sub: string): Promise<void>; 15 + } 16 + 17 + // Mock Prisma client 18 + const mockPrisma = { 19 + nodeOAuthState: { 20 + upsert: vi.fn(), 21 + findUnique: vi.fn(), 22 + delete: vi.fn(), 23 + }, 24 + nodeOAuthSession: { 25 + upsert: vi.fn(), 26 + findUnique: vi.fn(), 27 + delete: vi.fn(), 28 + }, 29 + }; 30 + 31 + // Create test implementations of stores (matching the actual implementation) 32 + function createStateStore(prisma: typeof mockPrisma): StateStore { 33 + return { 34 + async set(key: string, internalState: NodeSavedState): Promise<void> { 35 + await prisma.nodeOAuthState.upsert({ 36 + where: { key }, 37 + update: { state: JSON.stringify(internalState) }, 38 + create: { key, state: JSON.stringify(internalState) }, 39 + }); 40 + }, 41 + 42 + async get(key: string): Promise<NodeSavedState | undefined> { 43 + const record = await prisma.nodeOAuthState.findUnique({ where: { key } }); 44 + if (!record) return undefined; 45 + 46 + try { 47 + return JSON.parse(record.state) as NodeSavedState; 48 + } catch (err) { 49 + console.error('Invalid NodeOAuthState JSON:', err); 50 + return undefined; 51 + } 52 + }, 53 + 54 + async del(key: string): Promise<void> { 55 + await prisma.nodeOAuthState.delete({ where: { key } }).catch(() => { }); 56 + }, 57 + }; 58 + } 59 + 60 + function createSessionStore(prisma: typeof mockPrisma): SessionStore { 61 + return { 62 + async set(sub: string, session: NodeSavedSession): Promise<void> { 63 + await prisma.nodeOAuthSession.upsert({ 64 + where: { key: sub }, 65 + update: { session: JSON.stringify(session) }, 66 + create: { key: sub, session: JSON.stringify(session) }, 67 + }); 68 + }, 69 + 70 + async get(sub: string): Promise<NodeSavedSession | undefined> { 71 + const record = await prisma.nodeOAuthSession.findUnique({ where: { key: sub } }); 72 + if (!record) return undefined; 73 + 74 + try { 75 + return JSON.parse(record.session) as NodeSavedSession; 76 + } catch (err) { 77 + console.error('Invalid NodeOAuthSession JSON:', err); 78 + return undefined; 79 + } 80 + }, 81 + 82 + async del(sub: string): Promise<void> { 83 + await prisma.nodeOAuthSession.delete({ where: { key: sub } }).catch(() => { }); 84 + }, 85 + }; 86 + } 87 + 88 + describe('OAuth State Store', () => { 89 + let stateStore: StateStore; 90 + 91 + beforeEach(() => { 92 + vi.clearAllMocks(); 93 + stateStore = createStateStore(mockPrisma); 94 + }); 95 + 96 + describe('set', () => { 97 + it('should call prisma upsert with correct parameters', async () => { 98 + const mockState = { dpopKey: 'test' } as unknown as NodeSavedState; 99 + mockPrisma.nodeOAuthState.upsert.mockResolvedValue({}); 100 + 101 + await stateStore.set('testKey', mockState); 102 + 103 + expect(mockPrisma.nodeOAuthState.upsert).toHaveBeenCalledWith({ 104 + where: { key: 'testKey' }, 105 + update: { state: JSON.stringify(mockState) }, 106 + create: { key: 'testKey', state: JSON.stringify(mockState) }, 107 + }); 108 + }); 109 + }); 110 + 111 + describe('get', () => { 112 + it('should return state when found', async () => { 113 + const mockState = { dpopKey: 'test' }; 114 + mockPrisma.nodeOAuthState.findUnique.mockResolvedValue({ 115 + key: 'testKey', 116 + state: JSON.stringify(mockState), 117 + }); 118 + 119 + const result = await stateStore.get('testKey'); 120 + 121 + expect(result).toEqual(mockState); 122 + }); 123 + 124 + it('should return undefined when not found', async () => { 125 + mockPrisma.nodeOAuthState.findUnique.mockResolvedValue(null); 126 + 127 + const result = await stateStore.get('nonexistent'); 128 + 129 + expect(result).toBeUndefined(); 130 + }); 131 + 132 + it('should return undefined for invalid JSON', async () => { 133 + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); 134 + mockPrisma.nodeOAuthState.findUnique.mockResolvedValue({ 135 + key: 'testKey', 136 + state: 'invalid json {{{', 137 + }); 138 + 139 + const result = await stateStore.get('testKey'); 140 + 141 + expect(result).toBeUndefined(); 142 + expect(consoleSpy).toHaveBeenCalled(); 143 + consoleSpy.mockRestore(); 144 + }); 145 + }); 146 + 147 + describe('del', () => { 148 + it('should call prisma delete', async () => { 149 + mockPrisma.nodeOAuthState.delete.mockResolvedValue({}); 150 + 151 + await stateStore.del('testKey'); 152 + 153 + expect(mockPrisma.nodeOAuthState.delete).toHaveBeenCalledWith({ 154 + where: { key: 'testKey' }, 155 + }); 156 + }); 157 + 158 + it('should not throw on delete failure', async () => { 159 + mockPrisma.nodeOAuthState.delete.mockRejectedValue(new Error('Not found')); 160 + 161 + await expect(stateStore.del('nonexistent')).resolves.not.toThrow(); 162 + }); 163 + }); 164 + }); 165 + 166 + describe('OAuth Session Store', () => { 167 + let sessionStore: SessionStore; 168 + 169 + beforeEach(() => { 170 + vi.clearAllMocks(); 171 + sessionStore = createSessionStore(mockPrisma); 172 + }); 173 + 174 + describe('set', () => { 175 + it('should call prisma upsert with correct parameters', async () => { 176 + const mockSession = { tokenSet: {} } as unknown as NodeSavedSession; 177 + mockPrisma.nodeOAuthSession.upsert.mockResolvedValue({}); 178 + 179 + await sessionStore.set('did:plc:test', mockSession); 180 + 181 + expect(mockPrisma.nodeOAuthSession.upsert).toHaveBeenCalledWith({ 182 + where: { key: 'did:plc:test' }, 183 + update: { session: JSON.stringify(mockSession) }, 184 + create: { key: 'did:plc:test', session: JSON.stringify(mockSession) }, 185 + }); 186 + }); 187 + }); 188 + 189 + describe('get', () => { 190 + it('should return session when found', async () => { 191 + const mockSession = { tokenSet: { accessToken: 'abc' } }; 192 + mockPrisma.nodeOAuthSession.findUnique.mockResolvedValue({ 193 + key: 'did:plc:test', 194 + session: JSON.stringify(mockSession), 195 + }); 196 + 197 + const result = await sessionStore.get('did:plc:test'); 198 + 199 + expect(result).toEqual(mockSession); 200 + }); 201 + 202 + it('should return undefined when not found', async () => { 203 + mockPrisma.nodeOAuthSession.findUnique.mockResolvedValue(null); 204 + 205 + const result = await sessionStore.get('did:plc:nonexistent'); 206 + 207 + expect(result).toBeUndefined(); 208 + }); 209 + 210 + it('should return undefined for invalid JSON', async () => { 211 + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); 212 + mockPrisma.nodeOAuthSession.findUnique.mockResolvedValue({ 213 + key: 'did:plc:test', 214 + session: 'not valid json', 215 + }); 216 + 217 + const result = await sessionStore.get('did:plc:test'); 218 + 219 + expect(result).toBeUndefined(); 220 + expect(consoleSpy).toHaveBeenCalled(); 221 + consoleSpy.mockRestore(); 222 + }); 223 + }); 224 + 225 + describe('del', () => { 226 + it('should call prisma delete', async () => { 227 + mockPrisma.nodeOAuthSession.delete.mockResolvedValue({}); 228 + 229 + await sessionStore.del('did:plc:test'); 230 + 231 + expect(mockPrisma.nodeOAuthSession.delete).toHaveBeenCalledWith({ 232 + where: { key: 'did:plc:test' }, 233 + }); 234 + }); 235 + 236 + it('should not throw on delete failure', async () => { 237 + mockPrisma.nodeOAuthSession.delete.mockRejectedValue(new Error('Not found')); 238 + 239 + await expect(sessionStore.del('did:plc:nonexistent')).resolves.not.toThrow(); 240 + }); 241 + }); 242 + }); 243 + 244 + describe('OAuth SCOPE', () => { 245 + it('should contain required scopes', () => { 246 + const SCOPE = [ 247 + "atproto", 248 + "include:blue.rito.permissionSet", 249 + "repo:app.bsky.feed.post", 250 + "rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app%23bsky_appview", 251 + "blob:*/*", 252 + ]; 253 + 254 + expect(SCOPE).toContain('atproto'); 255 + expect(SCOPE).toContain('include:blue.rito.permissionSet'); 256 + expect(SCOPE).toContain('repo:app.bsky.feed.post'); 257 + expect(SCOPE).toContain('blob:*/*'); 258 + }); 259 + 260 + it('should have correct scope length', () => { 261 + const SCOPE = [ 262 + "atproto", 263 + "include:blue.rito.permissionSet", 264 + "repo:app.bsky.feed.post", 265 + "rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app%23bsky_appview", 266 + "blob:*/*", 267 + ]; 268 + 269 + expect(SCOPE.length).toBe(5); 270 + }); 271 + }); 272 + 273 + describe('OAuth Client Configuration', () => { 274 + it('should generate correct client metadata structure', () => { 275 + const baseUrl = 'https://rito.blue'; 276 + 277 + const clientMetadata = { 278 + client_id: `${baseUrl}/api/client-metadata.json`, 279 + client_name: 'Rito', 280 + client_uri: baseUrl, 281 + logo_uri: `${baseUrl}/favicon.ico`, 282 + tos_uri: `${baseUrl}/tos`, 283 + policy_uri: `${baseUrl}/privacy`, 284 + redirect_uris: [`${baseUrl}/api/oauth/callback`], 285 + grant_types: ['authorization_code', 'refresh_token'], 286 + scope: 'atproto include:blue.rito.permissionSet repo:app.bsky.feed.post blob:*/*', 287 + response_types: ['code'], 288 + application_type: 'web', 289 + token_endpoint_auth_method: 'private_key_jwt', 290 + token_endpoint_auth_signing_alg: 'RS256', 291 + dpop_bound_access_tokens: true, 292 + jwks_uri: `${baseUrl}/api/jwks.json`, 293 + }; 294 + 295 + expect(clientMetadata.client_id).toBe('https://rito.blue/api/client-metadata.json'); 296 + expect(clientMetadata.client_name).toBe('Rito'); 297 + expect(clientMetadata.redirect_uris).toContain('https://rito.blue/api/oauth/callback'); 298 + expect(clientMetadata.grant_types).toContain('authorization_code'); 299 + expect(clientMetadata.grant_types).toContain('refresh_token'); 300 + expect(clientMetadata.token_endpoint_auth_method).toBe('private_key_jwt'); 301 + expect(clientMetadata.dpop_bound_access_tokens).toBe(true); 302 + }); 303 + });
+319
backend/src/__tests__/types.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + 3 + // Type definitions for testing - mirroring those in index.ts 4 + 5 + interface DidDocument { 6 + alsoKnownAs?: string[]; 7 + } 8 + 9 + interface DomainCheckResult { 10 + result: boolean; 11 + } 12 + 13 + interface OgpResult { 14 + result: { 15 + ogTitle?: string; 16 + ogDescription?: string; 17 + ogImage?: { url: string }[]; 18 + }; 19 + } 20 + 21 + interface DnsAnswer { 22 + data: string; 23 + } 24 + 25 + interface DnsResponse { 26 + Answer?: DnsAnswer[]; 27 + } 28 + 29 + interface PostToBookmarkRecord { 30 + sub: string; 31 + lang?: string; 32 + } 33 + 34 + interface CommentLocale { 35 + lang: string; 36 + title?: string; 37 + comment?: string; 38 + } 39 + 40 + interface BookmarkRecord { 41 + $type: 'blue.rito.feed.bookmark'; 42 + subject: string; 43 + createdAt?: string; 44 + comments?: CommentLocale[]; 45 + ogpTitle?: string; 46 + ogpDescription?: string; 47 + ogpImage?: string; 48 + tags?: string[]; 49 + } 50 + 51 + describe('Type Definitions', () => { 52 + describe('DidDocument', () => { 53 + it('should accept valid DidDocument', () => { 54 + const doc: DidDocument = { 55 + alsoKnownAs: ['at://user.bsky.social'], 56 + }; 57 + expect(doc.alsoKnownAs).toHaveLength(1); 58 + }); 59 + 60 + it('should accept empty DidDocument', () => { 61 + const doc: DidDocument = {}; 62 + expect(doc.alsoKnownAs).toBeUndefined(); 63 + }); 64 + }); 65 + 66 + describe('DomainCheckResult', () => { 67 + it('should accept valid DomainCheckResult', () => { 68 + const result: DomainCheckResult = { result: true }; 69 + expect(result.result).toBe(true); 70 + }); 71 + }); 72 + 73 + describe('OgpResult', () => { 74 + it('should accept valid OgpResult with all fields', () => { 75 + const ogp: OgpResult = { 76 + result: { 77 + ogTitle: 'Test Title', 78 + ogDescription: 'Test Description', 79 + ogImage: [{ url: 'https://example.com/image.jpg' }], 80 + }, 81 + }; 82 + expect(ogp.result.ogTitle).toBe('Test Title'); 83 + expect(ogp.result.ogImage?.[0].url).toBe('https://example.com/image.jpg'); 84 + }); 85 + 86 + it('should accept OgpResult with partial fields', () => { 87 + const ogp: OgpResult = { 88 + result: { 89 + ogTitle: 'Only Title', 90 + }, 91 + }; 92 + expect(ogp.result.ogTitle).toBe('Only Title'); 93 + expect(ogp.result.ogDescription).toBeUndefined(); 94 + }); 95 + }); 96 + 97 + describe('DnsResponse', () => { 98 + it('should accept valid DnsResponse', () => { 99 + const dns: DnsResponse = { 100 + Answer: [{ data: 'did:plc:abc123' }], 101 + }; 102 + expect(dns.Answer).toHaveLength(1); 103 + }); 104 + 105 + it('should handle empty Answer', () => { 106 + const dns: DnsResponse = {}; 107 + expect(dns.Answer).toBeUndefined(); 108 + }); 109 + }); 110 + 111 + describe('PostToBookmarkRecord', () => { 112 + it('should accept valid record with lang', () => { 113 + const record: PostToBookmarkRecord = { 114 + sub: 'did:plc:test', 115 + lang: 'ja', 116 + }; 117 + expect(record.sub).toBe('did:plc:test'); 118 + expect(record.lang).toBe('ja'); 119 + }); 120 + 121 + it('should accept record without lang', () => { 122 + const record: PostToBookmarkRecord = { 123 + sub: 'did:plc:test', 124 + }; 125 + expect(record.lang).toBeUndefined(); 126 + }); 127 + }); 128 + 129 + describe('CommentLocale', () => { 130 + it('should accept full CommentLocale', () => { 131 + const comment: CommentLocale = { 132 + lang: 'ja', 133 + title: 'タイトル', 134 + comment: 'コメント内容', 135 + }; 136 + expect(comment.lang).toBe('ja'); 137 + expect(comment.title).toBe('タイトル'); 138 + }); 139 + 140 + it('should accept CommentLocale with only lang', () => { 141 + const comment: CommentLocale = { lang: 'en' }; 142 + expect(comment.title).toBeUndefined(); 143 + }); 144 + }); 145 + 146 + describe('BookmarkRecord', () => { 147 + it('should accept full BookmarkRecord', () => { 148 + const bookmark: BookmarkRecord = { 149 + $type: 'blue.rito.feed.bookmark', 150 + subject: 'https://example.com/article', 151 + createdAt: '2024-01-01T00:00:00.000Z', 152 + comments: [{ lang: 'ja', title: 'Title', comment: 'Comment' }], 153 + ogpTitle: 'OGP Title', 154 + ogpDescription: 'OGP Description', 155 + ogpImage: 'https://example.com/og.jpg', 156 + tags: ['test', 'bookmark'], 157 + }; 158 + expect(bookmark.$type).toBe('blue.rito.feed.bookmark'); 159 + expect(bookmark.subject).toBe('https://example.com/article'); 160 + expect(bookmark.tags).toHaveLength(2); 161 + }); 162 + 163 + it('should accept minimal BookmarkRecord', () => { 164 + const bookmark: BookmarkRecord = { 165 + $type: 'blue.rito.feed.bookmark', 166 + subject: 'https://example.com', 167 + }; 168 + expect(bookmark.subject).toBe('https://example.com'); 169 + expect(bookmark.comments).toBeUndefined(); 170 + }); 171 + 172 + it('should validate comments array', () => { 173 + const bookmark: BookmarkRecord = { 174 + $type: 'blue.rito.feed.bookmark', 175 + subject: 'https://example.com', 176 + comments: [ 177 + { lang: 'ja', title: '日本語タイトル' }, 178 + { lang: 'en', title: 'English Title' }, 179 + ], 180 + }; 181 + expect(bookmark.comments).toHaveLength(2); 182 + expect(bookmark.comments?.[0].lang).toBe('ja'); 183 + expect(bookmark.comments?.[1].lang).toBe('en'); 184 + }); 185 + }); 186 + }); 187 + 188 + describe('Handle Extraction', () => { 189 + // Test handle extraction logic used throughout the codebase 190 + 191 + it('should extract handle from alsoKnownAs', () => { 192 + const didData: DidDocument = { 193 + alsoKnownAs: ['at://user.bsky.social'], 194 + }; 195 + const handle = didData.alsoKnownAs?.[0]?.replace(/^at:\/\//, '') ?? 'no handle'; 196 + expect(handle).toBe('user.bsky.social'); 197 + }); 198 + 199 + it('should return default when alsoKnownAs is empty', () => { 200 + const didData: DidDocument = { 201 + alsoKnownAs: [], 202 + }; 203 + const handle = didData.alsoKnownAs?.[0]?.replace(/^at:\/\//, '') ?? 'no handle'; 204 + expect(handle).toBe('no handle'); 205 + }); 206 + 207 + it('should return default when alsoKnownAs is undefined', () => { 208 + const didData: DidDocument = {}; 209 + const handle = didData.alsoKnownAs?.[0]?.replace(/^at:\/\//, '') ?? 'no handle'; 210 + expect(handle).toBe('no handle'); 211 + }); 212 + }); 213 + 214 + describe('URL Domain Verification Logic', () => { 215 + // Test the domain verification logic used in upsertBookmark 216 + 217 + function checkVerification(subject: string, handle: string): boolean { 218 + try { 219 + const url = new URL(subject); 220 + const domain = url.hostname; 221 + 222 + if ((url.pathname === '/' || url.pathname === '') && 223 + (domain === handle || domain.endsWith(`.${handle}`))) { 224 + return true; 225 + } 226 + return false; 227 + } catch { 228 + return false; 229 + } 230 + } 231 + 232 + it('should verify exact domain match with root path', () => { 233 + expect(checkVerification('https://user.bsky.social/', 'user.bsky.social')).toBe(true); 234 + }); 235 + 236 + it('should verify exact domain match without path', () => { 237 + expect(checkVerification('https://user.bsky.social', 'user.bsky.social')).toBe(true); 238 + }); 239 + 240 + it('should verify subdomain match', () => { 241 + expect(checkVerification('https://blog.user.bsky.social/', 'user.bsky.social')).toBe(true); 242 + }); 243 + 244 + it('should not verify with non-root path', () => { 245 + expect(checkVerification('https://user.bsky.social/article', 'user.bsky.social')).toBe(false); 246 + }); 247 + 248 + it('should not verify mismatched domain', () => { 249 + expect(checkVerification('https://other.site.com/', 'user.bsky.social')).toBe(false); 250 + }); 251 + 252 + it('should handle invalid URLs', () => { 253 + expect(checkVerification('not-a-url', 'user.bsky.social')).toBe(false); 254 + }); 255 + }); 256 + 257 + describe('DNS TXT Record Parsing', () => { 258 + // Test the DNS TXT record parsing logic 259 + 260 + function parseTxtRecord(txtData: string): string | null { 261 + const didMatch = txtData.match(/did:[\w:.]+/); 262 + return didMatch ? didMatch[0] : null; 263 + } 264 + 265 + it('should extract DID from TXT record', () => { 266 + const txtData = '"did:plc:abc123"'; 267 + expect(parseTxtRecord(txtData)).toBe('did:plc:abc123'); 268 + }); 269 + 270 + it('should extract DID from raw text', () => { 271 + const txtData = 'did:plc:xyz789'; 272 + expect(parseTxtRecord(txtData)).toBe('did:plc:xyz789'); 273 + }); 274 + 275 + it('should handle did:web format', () => { 276 + const txtData = 'did:web:example.com'; 277 + expect(parseTxtRecord(txtData)).toBe('did:web:example.com'); 278 + }); 279 + 280 + it('should return null for no DID', () => { 281 + const txtData = 'some random text'; 282 + expect(parseTxtRecord(txtData)).toBeNull(); 283 + }); 284 + 285 + it('should return null for empty string', () => { 286 + expect(parseTxtRecord('')).toBeNull(); 287 + }); 288 + }); 289 + 290 + describe('NSID Reversal Logic', () => { 291 + // Test the namespace ID reversal logic used in resolver verification 292 + 293 + function reverseHandle(handle: string): string { 294 + return handle.split('.').reverse().join('.'); 295 + } 296 + 297 + it('should reverse handle correctly', () => { 298 + expect(reverseHandle('user.bsky.social')).toBe('social.bsky.user'); 299 + }); 300 + 301 + it('should handle simple domain', () => { 302 + expect(reverseHandle('example.com')).toBe('com.example'); 303 + }); 304 + 305 + it('should handle complex handle', () => { 306 + expect(reverseHandle('rito.blue')).toBe('blue.rito'); 307 + }); 308 + 309 + it('should handle single part', () => { 310 + expect(reverseHandle('localhost')).toBe('localhost'); 311 + }); 312 + 313 + it('should verify nsid starts with reversed handle', () => { 314 + const handle = 'rito.blue'; 315 + const nsid = 'blue.rito.feed.bookmark'; 316 + const reversedHandle = reverseHandle(handle); 317 + expect(nsid.startsWith(reversedHandle)).toBe(true); 318 + }); 319 + });
+423
backend/src/__tests__/utils.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { 3 + epochUsToDateTime, 4 + isValidTangledUrl, 5 + normalizeComment, 6 + extractHandleFromDidDoc, 7 + checkDomainVerification, 8 + parseTxtRecordForDid, 9 + reverseHandleToNsid, 10 + buildAtUri, 11 + normalizeTagsArray, 12 + buildDnsTxtSubdomain, 13 + extractLinksFromPost, 14 + extractTagsFromFacets, 15 + shouldProcessAsRitoPost, 16 + parseDomainFromUrl, 17 + DidDocument, 18 + BookmarkRecord, 19 + CommentLocale, 20 + } from '../utils.js'; 21 + 22 + describe('epochUsToDateTime', () => { 23 + it('should convert epoch microseconds to ISO datetime string', () => { 24 + // 2024-01-01 00:00:00.000 UTC = 1704067200000 ms = 1704067200000000 us 25 + const cursorUs = '1704067200000000'; 26 + const result = epochUsToDateTime(cursorUs); 27 + expect(result).toBe('2024-01-01T00:00:00.000Z'); 28 + }); 29 + 30 + it('should handle string input', () => { 31 + const cursorUs = '1704067200000000'; 32 + const result = epochUsToDateTime(cursorUs); 33 + expect(result).toContain('2024-01-01'); 34 + }); 35 + 36 + it('should handle number input', () => { 37 + const cursorUs = 1704067200000000; 38 + const result = epochUsToDateTime(cursorUs); 39 + expect(result).toContain('2024-01-01'); 40 + }); 41 + 42 + it('should return valid ISO string for current time', () => { 43 + const nowUs = Date.now() * 1000; 44 + const result = epochUsToDateTime(nowUs); 45 + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); 46 + }); 47 + }); 48 + 49 + describe('isValidTangledUrl', () => { 50 + it('should return true for valid tangled.org URL with matching handle', () => { 51 + expect(isValidTangledUrl('https://tangled.org/@rito.blue/skeet.el', 'rito.blue')).toBe(true); 52 + }); 53 + 54 + it('should return true for handle without @ prefix', () => { 55 + expect(isValidTangledUrl('https://tangled.org/rito.blue/skeet.el', 'rito.blue')).toBe(true); 56 + }); 57 + 58 + it('should return false for non-tangled.org domain', () => { 59 + expect(isValidTangledUrl('https://github.com/@rito.blue/skeet.el', 'rito.blue')).toBe(false); 60 + }); 61 + 62 + it('should return false for URL with path less than 2 parts', () => { 63 + expect(isValidTangledUrl('https://tangled.org/@rito.blue', 'rito.blue')).toBe(false); 64 + }); 65 + 66 + it('should return false for mismatched handle', () => { 67 + expect(isValidTangledUrl('https://tangled.org/@other.user/repo', 'rito.blue')).toBe(false); 68 + }); 69 + 70 + it('should return false for invalid URL', () => { 71 + expect(isValidTangledUrl('not-a-valid-url', 'rito.blue')).toBe(false); 72 + }); 73 + 74 + it('should handle empty URL', () => { 75 + expect(isValidTangledUrl('', 'rito.blue')).toBe(false); 76 + }); 77 + 78 + it('should return true with deep path', () => { 79 + expect(isValidTangledUrl('https://tangled.org/@rito.blue/repo/path/to/file', 'rito.blue')).toBe(true); 80 + }); 81 + }); 82 + 83 + describe('normalizeComment', () => { 84 + it('should remove hashtags from text', () => { 85 + const text = 'これは #rito.blue のテストです #test'; 86 + const result = normalizeComment(text); 87 + expect(result).not.toContain('#rito.blue'); 88 + expect(result).not.toContain('#test'); 89 + }); 90 + 91 + it('should remove URLs from text', () => { 92 + const text = 'Check out https://example.com/path and http://test.org'; 93 + const result = normalizeComment(text); 94 + expect(result).not.toContain('https://'); 95 + expect(result).not.toContain('http://'); 96 + }); 97 + 98 + it('should remove domain-like strings from text', () => { 99 + const text = 'Visit example.com/page for more info'; 100 + const result = normalizeComment(text); 101 + expect(result).not.toContain('example.com'); 102 + }); 103 + 104 + it('should compress multiple spaces into one', () => { 105 + const text = 'Hello World'; 106 + const result = normalizeComment(text); 107 + expect(result).toBe('Hello World'); 108 + }); 109 + 110 + it('should handle full-width spaces', () => { 111 + const text = 'こんにちは   世界'; 112 + const result = normalizeComment(text); 113 + expect(result).toBe('こんにちは 世界'); 114 + }); 115 + 116 + it('should trim leading/trailing spaces per line', () => { 117 + const text = ' Hello \n World '; 118 + const result = normalizeComment(text); 119 + expect(result).toBe('Hello\nWorld'); 120 + }); 121 + 122 + it('should preserve newlines', () => { 123 + const text = 'Line1\nLine2\nLine3'; 124 + const result = normalizeComment(text); 125 + expect(result).toBe('Line1\nLine2\nLine3'); 126 + }); 127 + 128 + it('should handle complex mixed content', () => { 129 + const text = '記事を共有 #rito.blue https://example.com これはコメントです'; 130 + const result = normalizeComment(text); 131 + expect(result).toContain('記事を共有'); 132 + expect(result).toContain('これはコメントです'); 133 + }); 134 + 135 + it('should return empty string for URLs only', () => { 136 + const text = 'https://example.com'; 137 + const result = normalizeComment(text); 138 + expect(result.trim()).toBe(''); 139 + }); 140 + 141 + it('should return empty string for tags only', () => { 142 + const text = '#tag1 #tag2 #tag3'; 143 + const result = normalizeComment(text); 144 + expect(result.trim()).toBe(''); 145 + }); 146 + }); 147 + 148 + describe('extractHandleFromDidDoc', () => { 149 + it('should extract handle from alsoKnownAs', () => { 150 + const didData: DidDocument = { alsoKnownAs: ['at://user.bsky.social'] }; 151 + expect(extractHandleFromDidDoc(didData)).toBe('user.bsky.social'); 152 + }); 153 + 154 + it('should return default when alsoKnownAs is empty', () => { 155 + const didData: DidDocument = { alsoKnownAs: [] }; 156 + expect(extractHandleFromDidDoc(didData)).toBe('no handle'); 157 + }); 158 + 159 + it('should return default when alsoKnownAs is undefined', () => { 160 + const didData: DidDocument = {}; 161 + expect(extractHandleFromDidDoc(didData)).toBe('no handle'); 162 + }); 163 + 164 + it('should use custom default value', () => { 165 + const didData: DidDocument = {}; 166 + expect(extractHandleFromDidDoc(didData, 'custom default')).toBe('custom default'); 167 + }); 168 + }); 169 + 170 + describe('checkDomainVerification', () => { 171 + it('should verify exact domain match with root path', () => { 172 + expect(checkDomainVerification('https://user.bsky.social/', 'user.bsky.social')).toBe(true); 173 + }); 174 + 175 + it('should verify exact domain match without path', () => { 176 + expect(checkDomainVerification('https://user.bsky.social', 'user.bsky.social')).toBe(true); 177 + }); 178 + 179 + it('should verify subdomain match', () => { 180 + expect(checkDomainVerification('https://blog.user.bsky.social/', 'user.bsky.social')).toBe(true); 181 + }); 182 + 183 + it('should not verify with non-root path', () => { 184 + expect(checkDomainVerification('https://user.bsky.social/article', 'user.bsky.social')).toBe(false); 185 + }); 186 + 187 + it('should not verify mismatched domain', () => { 188 + expect(checkDomainVerification('https://other.site.com/', 'user.bsky.social')).toBe(false); 189 + }); 190 + 191 + it('should handle invalid URLs', () => { 192 + expect(checkDomainVerification('not-a-url', 'user.bsky.social')).toBe(false); 193 + }); 194 + }); 195 + 196 + describe('parseTxtRecordForDid', () => { 197 + it('should extract DID from quoted TXT record', () => { 198 + expect(parseTxtRecordForDid('"did:plc:abc123"')).toBe('did:plc:abc123'); 199 + }); 200 + 201 + it('should extract DID from raw text', () => { 202 + expect(parseTxtRecordForDid('did:plc:xyz789')).toBe('did:plc:xyz789'); 203 + }); 204 + 205 + it('should handle did:web format', () => { 206 + expect(parseTxtRecordForDid('did:web:example.com')).toBe('did:web:example.com'); 207 + }); 208 + 209 + it('should return null for no DID', () => { 210 + expect(parseTxtRecordForDid('some random text')).toBeNull(); 211 + }); 212 + 213 + it('should return null for empty string', () => { 214 + expect(parseTxtRecordForDid('')).toBeNull(); 215 + }); 216 + }); 217 + 218 + describe('reverseHandleToNsid', () => { 219 + it('should reverse handle correctly', () => { 220 + expect(reverseHandleToNsid('user.bsky.social')).toBe('social.bsky.user'); 221 + }); 222 + 223 + it('should handle simple domain', () => { 224 + expect(reverseHandleToNsid('example.com')).toBe('com.example'); 225 + }); 226 + 227 + it('should handle complex handle', () => { 228 + expect(reverseHandleToNsid('rito.blue')).toBe('blue.rito'); 229 + }); 230 + 231 + it('should handle single part', () => { 232 + expect(reverseHandleToNsid('localhost')).toBe('localhost'); 233 + }); 234 + }); 235 + 236 + describe('buildAtUri', () => { 237 + it('should build valid AT URI', () => { 238 + const result = buildAtUri('did:plc:abc123', 'blue.rito.feed.bookmark', 'rkey123'); 239 + expect(result).toBe('at://did:plc:abc123/blue.rito.feed.bookmark/rkey123'); 240 + }); 241 + 242 + it('should handle different collections', () => { 243 + const result = buildAtUri('did:plc:xyz', 'app.bsky.feed.post', 'post123'); 244 + expect(result).toBe('at://did:plc:xyz/app.bsky.feed.post/post123'); 245 + }); 246 + }); 247 + 248 + describe('normalizeTagsArray', () => { 249 + it('should filter empty strings', () => { 250 + const tags = ['test', '', 'valid', ' ']; 251 + expect(normalizeTagsArray(tags)).toEqual(['test', 'valid']); 252 + }); 253 + 254 + it('should filter out "verified" (case insensitive)', () => { 255 + const tags = ['test', 'Verified', 'VERIFIED', 'valid']; 256 + expect(normalizeTagsArray(tags)).toEqual(['test', 'valid']); 257 + }); 258 + 259 + it('should add Verified when flag is true', () => { 260 + const tags = ['test', 'valid']; 261 + expect(normalizeTagsArray(tags, true)).toEqual(['test', 'valid', 'Verified']); 262 + }); 263 + 264 + it('should handle empty array', () => { 265 + expect(normalizeTagsArray([])).toEqual([]); 266 + }); 267 + }); 268 + 269 + describe('buildDnsTxtSubdomain', () => { 270 + it('should build correct subdomain for NSID', () => { 271 + expect(buildDnsTxtSubdomain('uk.skyblur.post')).toBe('_lexicon.skyblur.uk'); 272 + }); 273 + 274 + it('should handle longer NSID', () => { 275 + expect(buildDnsTxtSubdomain('blue.rito.feed.bookmark')).toBe('_lexicon.feed.rito.blue'); 276 + }); 277 + }); 278 + 279 + describe('extractLinksFromPost', () => { 280 + it('should extract link from external embed', () => { 281 + const record = { 282 + embed: { 283 + $type: 'app.bsky.embed.external', 284 + external: { uri: 'https://example.com/article' } 285 + } 286 + }; 287 + expect(extractLinksFromPost(record)).toEqual(['https://example.com/article']); 288 + }); 289 + 290 + it('should return empty array for no embed', () => { 291 + const record = {}; 292 + expect(extractLinksFromPost(record)).toEqual([]); 293 + }); 294 + 295 + it('should return empty array for non-external embed', () => { 296 + const record = { 297 + embed: { $type: 'app.bsky.embed.images' } 298 + }; 299 + expect(extractLinksFromPost(record)).toEqual([]); 300 + }); 301 + 302 + it('should deduplicate links', () => { 303 + const record = { 304 + embed: { 305 + $type: 'app.bsky.embed.external', 306 + external: { uri: 'https://example.com' } 307 + } 308 + }; 309 + expect(extractLinksFromPost(record)).toEqual(['https://example.com']); 310 + }); 311 + }); 312 + 313 + describe('extractTagsFromFacets', () => { 314 + it('should extract tags from facets', () => { 315 + const facets = [ 316 + { 317 + features: [ 318 + { $type: 'app.bsky.richtext.facet#tag', tag: 'rito.blue' }, 319 + { $type: 'app.bsky.richtext.facet#tag', tag: 'test' } 320 + ] 321 + } 322 + ]; 323 + expect(extractTagsFromFacets(facets)).toEqual(['rito.blue', 'test']); 324 + }); 325 + 326 + it('should handle empty facets', () => { 327 + expect(extractTagsFromFacets([])).toEqual([]); 328 + }); 329 + 330 + it('should handle undefined facets', () => { 331 + expect(extractTagsFromFacets(undefined as any)).toEqual([]); 332 + }); 333 + 334 + it('should ignore non-tag features', () => { 335 + const facets = [ 336 + { 337 + features: [ 338 + { $type: 'app.bsky.richtext.facet#link', uri: 'https://example.com' }, 339 + { $type: 'app.bsky.richtext.facet#tag', tag: 'valid' } 340 + ] 341 + } 342 + ]; 343 + expect(extractTagsFromFacets(facets)).toEqual(['valid']); 344 + }); 345 + }); 346 + 347 + describe('shouldProcessAsRitoPost', () => { 348 + it('should return true when rito.blue tag is present', () => { 349 + expect(shouldProcessAsRitoPost(['rito.blue', 'test'])).toBe(true); 350 + }); 351 + 352 + it('should return false when rito.blue tag is missing', () => { 353 + expect(shouldProcessAsRitoPost(['test', 'other'])).toBe(false); 354 + }); 355 + 356 + it('should return false when via is リト', () => { 357 + expect(shouldProcessAsRitoPost(['rito.blue'], 'リト')).toBe(false); 358 + }); 359 + 360 + it('should return false when via is Rito', () => { 361 + expect(shouldProcessAsRitoPost(['rito.blue'], 'Rito')).toBe(false); 362 + }); 363 + 364 + it('should return true for other via values', () => { 365 + expect(shouldProcessAsRitoPost(['rito.blue'], 'other')).toBe(true); 366 + }); 367 + }); 368 + 369 + describe('parseDomainFromUrl', () => { 370 + it('should extract domain from valid URL', () => { 371 + expect(parseDomainFromUrl('https://example.com/path')).toBe('example.com'); 372 + }); 373 + 374 + it('should handle URL with port', () => { 375 + expect(parseDomainFromUrl('https://example.com:8080/path')).toBe('example.com'); 376 + }); 377 + 378 + it('should return null for invalid URL', () => { 379 + expect(parseDomainFromUrl('not-a-url')).toBeNull(); 380 + }); 381 + 382 + it('should return null for empty string', () => { 383 + expect(parseDomainFromUrl('')).toBeNull(); 384 + }); 385 + }); 386 + 387 + // Type tests 388 + describe('Type Definitions', () => { 389 + describe('BookmarkRecord', () => { 390 + it('should accept full BookmarkRecord', () => { 391 + const bookmark: BookmarkRecord = { 392 + $type: 'blue.rito.feed.bookmark', 393 + subject: 'https://example.com/article', 394 + createdAt: '2024-01-01T00:00:00.000Z', 395 + comments: [{ lang: 'ja', title: 'Title', comment: 'Comment' }], 396 + ogpTitle: 'OGP Title', 397 + ogpDescription: 'OGP Description', 398 + ogpImage: 'https://example.com/og.jpg', 399 + tags: ['test', 'bookmark'], 400 + }; 401 + expect(bookmark.$type).toBe('blue.rito.feed.bookmark'); 402 + }); 403 + 404 + it('should accept minimal BookmarkRecord', () => { 405 + const bookmark: BookmarkRecord = { 406 + $type: 'blue.rito.feed.bookmark', 407 + subject: 'https://example.com', 408 + }; 409 + expect(bookmark.subject).toBe('https://example.com'); 410 + }); 411 + }); 412 + 413 + describe('CommentLocale', () => { 414 + it('should accept full CommentLocale', () => { 415 + const comment: CommentLocale = { 416 + lang: 'ja', 417 + title: 'タイトル', 418 + comment: 'コメント内容', 419 + }; 420 + expect(comment.lang).toBe('ja'); 421 + }); 422 + }); 423 + });
+282
backend/src/db.ts
··· 1 + /** 2 + * Database service module for Rito backend. 3 + * This module wraps Prisma operations for better testability. 4 + */ 5 + 6 + import { PrismaClient } from "@prisma/client"; 7 + import pLimit from "p-limit"; 8 + import logger from './logger.js'; 9 + 10 + // Export the PLimit instance for external use 11 + export const dbLimit = pLimit(5); 12 + 13 + // Create a singleton Prisma client 14 + export const prisma = new PrismaClient(); 15 + 16 + // Set up error handler 17 + (prisma as any).$on('error', (e: any) => { 18 + logger.error(`Prisma error event: ${e?.message || e}`); 19 + process.exit(1); 20 + }); 21 + 22 + /** 23 + * Load cursor from JetstreamIndex 24 + */ 25 + export async function loadCursor(epochUsToDateTime: (cursor: string | number) => string): Promise<string> { 26 + try { 27 + const indexRecord = await prisma.jetstreamIndex.findUnique({ 28 + where: { service: 'rito' } 29 + }); 30 + if (indexRecord && indexRecord.index) { 31 + logger.info(`Cursor from DB: ${indexRecord.index} (${epochUsToDateTime(indexRecord.index)})`); 32 + return indexRecord.index; 33 + } else { 34 + const nowUs = Date.now().toString(); 35 + logger.info(`No DB cursor found, using current time: ${nowUs} (${epochUsToDateTime(nowUs)})`); 36 + return nowUs; 37 + } 38 + } catch (err) { 39 + logger.error(`Failed to load cursor from DB: ${err}`); 40 + return Date.now().toString(); 41 + } 42 + } 43 + 44 + /** 45 + * Upsert cursor to JetstreamIndex 46 + */ 47 + export async function upsertCursor(currentCursor: string): Promise<void> { 48 + await prisma.jetstreamIndex.upsert({ 49 + where: { service: 'rito' }, 50 + update: { index: currentCursor }, 51 + create: { service: 'rito', index: currentCursor }, 52 + }); 53 + } 54 + 55 + /** 56 + * Upsert user DID to handle mapping 57 + */ 58 + export async function upsertUserDidHandle(did: string, handle: string): Promise<void> { 59 + await dbLimit(() => 60 + prisma.userDidHandle.upsert({ 61 + where: { did }, 62 + update: { handle }, 63 + create: { did, handle }, 64 + }) 65 + ); 66 + } 67 + 68 + /** 69 + * Upsert bookmark record 70 + */ 71 + export interface BookmarkUpsertData { 72 + uri: string; 73 + did: string; 74 + subject: string; 75 + ogpTitle?: string; 76 + ogpDescription?: string; 77 + ogpImage?: string; 78 + moderationResult: string | null; 79 + handle: string; 80 + createdAt?: Date; 81 + } 82 + 83 + export async function upsertBookmarkRecord(data: BookmarkUpsertData): Promise<void> { 84 + await dbLimit(() => 85 + prisma.bookmark.upsert({ 86 + where: { uri: data.uri }, 87 + update: { 88 + subject: data.subject, 89 + ogp_title: data.ogpTitle, 90 + ogp_description: data.ogpDescription, 91 + ogp_image: data.ogpImage, 92 + moderation_result: data.moderationResult, 93 + handle: data.handle, 94 + indexed_at: new Date(), 95 + }, 96 + create: { 97 + uri: data.uri, 98 + did: data.did, 99 + subject: data.subject, 100 + ogp_title: data.ogpTitle, 101 + ogp_description: data.ogpDescription, 102 + ogp_image: data.ogpImage, 103 + moderation_result: data.moderationResult, 104 + handle: data.handle, 105 + created_at: data.createdAt ?? new Date(), 106 + indexed_at: new Date(), 107 + }, 108 + }) 109 + ); 110 + } 111 + 112 + /** 113 + * Upsert comment record 114 + */ 115 + export interface CommentUpsertData { 116 + bookmarkUri: string; 117 + lang: string; 118 + title?: string; 119 + comment?: string; 120 + moderationResult: string | null; 121 + } 122 + 123 + export async function upsertComment(data: CommentUpsertData): Promise<void> { 124 + await dbLimit(() => 125 + prisma.comment.upsert({ 126 + where: { 127 + bookmark_uri_lang: { 128 + bookmark_uri: data.bookmarkUri, 129 + lang: data.lang, 130 + }, 131 + }, 132 + update: { 133 + title: data.title, 134 + comment: data.comment, 135 + moderation_result: data.moderationResult, 136 + }, 137 + create: { 138 + bookmark_uri: data.bookmarkUri, 139 + lang: data.lang, 140 + title: data.title, 141 + comment: data.comment, 142 + moderation_result: data.moderationResult, 143 + }, 144 + }) 145 + ); 146 + } 147 + 148 + /** 149 + * Delete comments not in the given language list 150 + */ 151 + export async function deleteCommentsNotInLangs(bookmarkUri: string, langs: string[]): Promise<void> { 152 + await prisma.comment.deleteMany({ 153 + where: { 154 + bookmark_uri: bookmarkUri, 155 + NOT: { 156 + lang: { in: langs }, 157 + }, 158 + }, 159 + }); 160 + } 161 + 162 + /** 163 + * Upsert tag 164 + */ 165 + export async function safeUpsertTag(name: string): Promise<{ id: number; name: string } | null> { 166 + try { 167 + return await dbLimit(() => 168 + prisma.tag.upsert({ 169 + where: { name }, 170 + update: {}, 171 + create: { name }, 172 + }) 173 + ); 174 + } catch (err) { 175 + console.error(`Tag upsert failed for "${name}":`, err); 176 + return null; 177 + } 178 + } 179 + 180 + /** 181 + * Get existing tag IDs for a bookmark 182 + */ 183 + export async function getBookmarkTagIds(bookmarkUri: string): Promise<number[]> { 184 + const tags = await prisma.bookmarkTag.findMany({ 185 + where: { bookmark_uri: bookmarkUri }, 186 + select: { tag_id: true }, 187 + }); 188 + return tags.map(r => r.tag_id); 189 + } 190 + 191 + /** 192 + * Delete bookmark tags by IDs 193 + */ 194 + export async function deleteBookmarkTags(bookmarkUri: string, tagIds: number[]): Promise<void> { 195 + await dbLimit(() => 196 + prisma.bookmarkTag.deleteMany({ 197 + where: { bookmark_uri: bookmarkUri, tag_id: { in: tagIds } }, 198 + }) 199 + ); 200 + } 201 + 202 + /** 203 + * Create bookmark tag 204 + */ 205 + export async function createBookmarkTag(bookmarkUri: string, tagId: number): Promise<void> { 206 + await dbLimit(() => 207 + prisma.bookmarkTag.create({ data: { bookmark_uri: bookmarkUri, tag_id: tagId } }) 208 + ); 209 + } 210 + 211 + /** 212 + * Find postToBookmark record 213 + */ 214 + export async function findPostToBookmark(sub: string): Promise<{ sub: string; lang?: string } | null> { 215 + return await prisma.postToBookmark.findUnique({ 216 + where: { sub }, 217 + select: { sub: true, lang: true }, 218 + }); 219 + } 220 + 221 + /** 222 + * Find existing bookmark by did and subject 223 + */ 224 + export async function findExistingBookmark(did: string, subject: string): Promise<{ uri: string } | null> { 225 + return await prisma.bookmark.findFirst({ 226 + where: { did, subject }, 227 + select: { uri: true }, 228 + }); 229 + } 230 + 231 + /** 232 + * Upsert resolver record 233 + */ 234 + export async function upsertResolverRecord(nsid: string, did: string, schema: string, verified: boolean): Promise<void> { 235 + await dbLimit(() => 236 + prisma.resolver.upsert({ 237 + where: { nsid_did: { nsid, did } }, 238 + update: { schema, verified, indexed_at: new Date() }, 239 + create: { nsid, did, schema, verified, indexed_at: new Date() }, 240 + }) 241 + ); 242 + } 243 + 244 + /** 245 + * Upsert like record 246 + */ 247 + export async function upsertLikeRecord(aturi: string, subject: string, did: string, createdAt: Date): Promise<void> { 248 + await dbLimit(() => 249 + prisma.like.upsert({ 250 + where: { aturi }, 251 + update: { created_at: createdAt }, 252 + create: { aturi, subject, did, created_at: createdAt }, 253 + }) 254 + ); 255 + } 256 + 257 + /** 258 + * Delete bookmark 259 + */ 260 + export async function deleteBookmark(uri: string): Promise<{ count: number }> { 261 + return await dbLimit(() => 262 + prisma.bookmark.deleteMany({ where: { uri } }) 263 + ); 264 + } 265 + 266 + /** 267 + * Delete like 268 + */ 269 + export async function deleteLike(aturi: string): Promise<{ count: number }> { 270 + return await dbLimit(() => 271 + prisma.like.deleteMany({ where: { aturi } }) 272 + ); 273 + } 274 + 275 + /** 276 + * Delete resolver 277 + */ 278 + export async function deleteResolver(nsid: string, did: string): Promise<{ count: number }> { 279 + return await dbLimit(() => 280 + prisma.resolver.deleteMany({ where: { nsid, did } }) 281 + ); 282 + }
+247
backend/src/utils.ts
··· 1 + /** 2 + * Utility functions for the backend that are testable in isolation. 3 + * These are extracted from index.ts for better testability. 4 + */ 5 + 6 + // Type definitions for API responses 7 + export interface DidDocument { 8 + alsoKnownAs?: string[]; 9 + } 10 + 11 + export interface DomainCheckResult { 12 + result: boolean; 13 + } 14 + 15 + export interface OgpResult { 16 + result: { 17 + ogTitle?: string; 18 + ogDescription?: string; 19 + ogImage?: { url: string }[]; 20 + }; 21 + } 22 + 23 + export interface DnsAnswer { 24 + data: string; 25 + } 26 + 27 + export interface DnsResponse { 28 + Answer?: DnsAnswer[]; 29 + } 30 + 31 + export interface PostToBookmarkRecord { 32 + sub: string; 33 + lang?: string; 34 + } 35 + 36 + // Comment locale type 37 + export interface CommentLocale { 38 + lang: string; 39 + title?: string; 40 + comment?: string; 41 + } 42 + 43 + // Bookmark record type (the inner object, not the full record schema) 44 + export interface BookmarkRecord { 45 + $type: 'blue.rito.feed.bookmark'; 46 + subject: string; 47 + createdAt?: string; 48 + comments?: CommentLocale[]; 49 + ogpTitle?: string; 50 + ogpDescription?: string; 51 + ogpImage?: string; 52 + tags?: string[]; 53 + } 54 + 55 + /** 56 + * Convert epoch microseconds to ISO datetime string 57 + */ 58 + export function epochUsToDateTime(cursor: string | number): string { 59 + return new Date(Number(cursor) / 1000).toISOString(); 60 + } 61 + 62 + /** 63 + * Validate if URL is a valid tangled.org URL for the given user handle 64 + */ 65 + export function isValidTangledUrl(url: string, userProfHandle: string): boolean { 66 + try { 67 + const u = new URL(url); 68 + 69 + // ドメインが tangled.org であることを確認 70 + if (u.hostname !== "tangled.org") return false; 71 + 72 + // パスを分解 73 + const parts = u.pathname.split("/").filter(Boolean); 74 + 75 + // 最低でも2要素必要(例: ["@rito.blue", "skeet.el"]) 76 + if (parts.length < 2) return false; 77 + 78 + // 1個目が @handle であることを確認 79 + if (parts[0] !== userProfHandle && parts[0] !== `@${userProfHandle}`) { 80 + return false; 81 + } 82 + 83 + return true; 84 + } catch { 85 + return false; 86 + } 87 + } 88 + 89 + /** 90 + * Normalize comment text by removing hashtags, URLs, and compressing whitespace 91 + */ 92 + export function normalizeComment(text: string): string { 93 + let result = text; 94 + 95 + // #tags を削除 96 + result = result.replace(/#[^\s#]+/g, ''); 97 + 98 + // URL / ドメイン(パス付き含む)を根こそぎ削除 99 + result = result.replace( 100 + /\bhttps?:\/\/[^\s]+|\b[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\/[^\s]*)?/g, 101 + '' 102 + ); 103 + 104 + // 空白を 1 つに圧縮 105 + result = result 106 + // 半角スペース + 全角スペースを 1 つに 107 + .replace(/[  ]+/g, ' ') 108 + // 行頭・行末のスペースだけ除去(改行は残る) 109 + .replace(/^[  ]+|[  ]+$/gm, ''); 110 + 111 + return result; 112 + } 113 + 114 + /** 115 + * Extract handle from DID document's alsoKnownAs field 116 + */ 117 + export function extractHandleFromDidDoc(didData: DidDocument, defaultHandle: string = 'no handle'): string { 118 + return didData.alsoKnownAs?.[0]?.replace(/^at:\/\//, '') ?? defaultHandle; 119 + } 120 + 121 + /** 122 + * Check if URL domain matches user handle for verification 123 + */ 124 + export function checkDomainVerification(subject: string, handle: string): boolean { 125 + try { 126 + const url = new URL(subject); 127 + const domain = url.hostname; 128 + 129 + if ((url.pathname === '/' || url.pathname === '') && 130 + (domain === handle || domain.endsWith(`.${handle}`))) { 131 + return true; 132 + } 133 + return false; 134 + } catch { 135 + return false; 136 + } 137 + } 138 + 139 + /** 140 + * Parse DID from DNS TXT record data 141 + */ 142 + export function parseTxtRecordForDid(txtData: string): string | null { 143 + // Remove quotes and join 144 + const cleaned = txtData.replace(/^"|"$/g, "").replace(/"/g, ""); 145 + const didMatch = cleaned.match(/did:[\w:.]+/); 146 + return didMatch ? didMatch[0] : null; 147 + } 148 + 149 + /** 150 + * Reverse a handle to NSID prefix format 151 + * Example: "rito.blue" -> "blue.rito" 152 + */ 153 + export function reverseHandleToNsid(handle: string): string { 154 + return handle.split('.').reverse().join('.'); 155 + } 156 + 157 + /** 158 + * Build AT URI from components 159 + */ 160 + export function buildAtUri(did: string, collection: string, rkey: string): string { 161 + return `at://${did}/${collection}/${rkey}`; 162 + } 163 + 164 + /** 165 + * Filter and normalize tags array 166 + */ 167 + export function normalizeTagsArray(tags: string[], shouldAddVerified: boolean = false): string[] { 168 + let result = (tags ?? []) 169 + .filter((name: string) => name && name.trim().length > 0) 170 + .filter((name: string) => name.toLowerCase() !== "verified"); 171 + 172 + if (shouldAddVerified) { 173 + result.push("Verified"); 174 + } 175 + 176 + return result; 177 + } 178 + 179 + /** 180 + * Build subdomain for DNS TXT lookup 181 + * Example: "uk.skyblur.post" -> "_lexicon.skyblur.uk" 182 + */ 183 + export function buildDnsTxtSubdomain(nsid: string): string { 184 + const parts = nsid.split('.').reverse(); 185 + return `_lexicon.${parts.slice(1).join('.')}`; 186 + } 187 + 188 + /** 189 + * Extract unique links from post facets and embed 190 + */ 191 + export function extractLinksFromPost(record: any): string[] { 192 + const links: string[] = []; 193 + 194 + if (record.embed?.$type === 'app.bsky.embed.external' && record.embed.external?.uri) { 195 + links.push(record.embed.external.uri); 196 + } 197 + 198 + return Array.from(new Set(links.filter((l): l is string => !!l))); 199 + } 200 + 201 + /** 202 + * Extract hashtags from post facets 203 + */ 204 + export function extractTagsFromFacets(facets: any[]): string[] { 205 + const tags: string[] = []; 206 + 207 + if (facets) { 208 + for (const facet of facets) { 209 + if (facet.features) { 210 + for (const feature of facet.features) { 211 + if (feature.$type === 'app.bsky.richtext.facet#tag' && feature.tag) { 212 + tags.push(feature.tag); 213 + } 214 + } 215 + } 216 + } 217 + } 218 + 219 + return tags; 220 + } 221 + 222 + /** 223 + * Check if post should be processed as rito.blue bookmark 224 + */ 225 + export function shouldProcessAsRitoPost(tags: string[], via?: string): boolean { 226 + if (!tags.includes('rito.blue')) { 227 + return false; 228 + } 229 + 230 + if (via === 'リト' || via === 'Rito') { 231 + return false; 232 + } 233 + 234 + return true; 235 + } 236 + 237 + /** 238 + * Parse domain from URL string 239 + */ 240 + export function parseDomainFromUrl(urlString: string): string | null { 241 + try { 242 + const url = new URL(urlString); 243 + return url.hostname; 244 + } catch { 245 + return null; 246 + } 247 + }
+23
backend/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + globals: true, 6 + environment: 'node', 7 + include: ['src/**/__tests__/**/*.test.ts'], 8 + coverage: { 9 + provider: 'v8', 10 + reporter: ['text', 'json', 'html'], 11 + include: ['src/**/*.ts'], 12 + exclude: [ 13 + 'src/**/__tests__/**', 14 + 'src/lexicons/**', 15 + 'src/index.ts', // Entry point with side effects 16 + 'src/db.ts', // Prisma-dependent module 17 + 'src/lib/HandleOauthClientNode.ts', // OAuth client with top-level await 18 + 'node_modules/**', 19 + ], 20 + }, 21 + testTimeout: 10000, 22 + }, 23 + });