@clipr/worker 0.0.5 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +432 -0
- package/coverage/coverage-final.json +16 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +161 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/crypto.ts.html +340 -0
- package/coverage/src/index.html +176 -0
- package/coverage/src/index.ts.html +262 -0
- package/coverage/src/kv-backend.ts.html +295 -0
- package/coverage/src/kv.ts.html +577 -0
- package/coverage/src/middleware/auth.ts.html +250 -0
- package/coverage/src/middleware/index.html +116 -0
- package/coverage/src/routes/health.ts.html +100 -0
- package/coverage/src/routes/import-export.ts.html +232 -0
- package/coverage/src/routes/index.html +221 -0
- package/coverage/src/routes/links.ts.html +379 -0
- package/coverage/src/routes/password.ts.html +436 -0
- package/coverage/src/routes/qr.ts.html +202 -0
- package/coverage/src/routes/redirect.ts.html +346 -0
- package/coverage/src/routes/shorten.ts.html +340 -0
- package/coverage/src/routes/stats.ts.html +145 -0
- package/coverage/src/test-utils.ts.html +184 -0
- package/coverage/src/utils/index.html +116 -0
- package/coverage/src/utils/qr.ts.html +190 -0
- package/package.json +2 -2
- package/src/crypto.test.ts +40 -0
- package/src/index.ts +3 -2
- package/src/middleware/auth.test.ts +83 -0
- package/src/middleware/auth.ts +5 -10
- package/src/middleware/rate-limit.ts +33 -0
- package/src/routes/api.test.ts +469 -0
- package/src/test-utils.ts +6 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<title>Code coverage report for src/test-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> / <a href="index.html">src</a> test-utils.ts</h1>
|
|
23
|
+
<div class='clearfix'>
|
|
24
|
+
|
|
25
|
+
<div class='fl pad1y space-right2'>
|
|
26
|
+
<span class="strong">93.33% </span>
|
|
27
|
+
<span class="quiet">Statements</span>
|
|
28
|
+
<span class='fraction'>14/15</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">87.5% </span>
|
|
41
|
+
<span class="quiet">Functions</span>
|
|
42
|
+
<span class='fraction'>7/8</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<div class='fl pad1y space-right2'>
|
|
47
|
+
<span class="strong">92.3% </span>
|
|
48
|
+
<span class="quiet">Lines</span>
|
|
49
|
+
<span class='fraction'>12/13</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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
|
100
|
+
<span class="cline-any cline-neutral"> </span>
|
|
101
|
+
<span class="cline-any cline-yes">54x</span>
|
|
102
|
+
<span class="cline-any cline-neutral"> </span>
|
|
103
|
+
<span class="cline-any cline-yes">54x</span>
|
|
104
|
+
<span class="cline-any cline-neutral"> </span>
|
|
105
|
+
<span class="cline-any cline-yes">111x</span>
|
|
106
|
+
<span class="cline-any cline-neutral"> </span>
|
|
107
|
+
<span class="cline-any cline-neutral"> </span>
|
|
108
|
+
<span class="cline-any cline-yes">90x</span>
|
|
109
|
+
<span class="cline-any cline-yes">90x</span>
|
|
110
|
+
<span class="cline-any cline-neutral"> </span>
|
|
111
|
+
<span class="cline-any cline-neutral"> </span>
|
|
112
|
+
<span class="cline-any cline-yes">2x</span>
|
|
113
|
+
<span class="cline-any cline-yes">2x</span>
|
|
114
|
+
<span class="cline-any cline-neutral"> </span>
|
|
115
|
+
<span class="cline-any cline-neutral"> </span>
|
|
116
|
+
<span class="cline-any cline-yes">8x</span>
|
|
117
|
+
<span class="cline-any cline-yes">8x</span>
|
|
118
|
+
<span class="cline-any cline-yes">40x</span>
|
|
119
|
+
<span class="cline-any cline-neutral"> </span>
|
|
120
|
+
<span class="cline-any cline-yes">8x</span>
|
|
121
|
+
<span class="cline-any cline-yes">8x</span>
|
|
122
|
+
<span class="cline-any cline-neutral"> </span>
|
|
123
|
+
<span class="cline-any cline-neutral"> </span>
|
|
124
|
+
<span class="cline-any cline-neutral"> </span>
|
|
125
|
+
<span class="cline-any cline-neutral"> </span>
|
|
126
|
+
<span class="cline-any cline-neutral"> </span>
|
|
127
|
+
<span class="cline-any cline-neutral"> </span>
|
|
128
|
+
<span class="cline-any cline-no"> </span>
|
|
129
|
+
<span class="cline-any cline-neutral"> </span>
|
|
130
|
+
<span class="cline-any cline-neutral"> </span>
|
|
131
|
+
<span class="cline-any cline-neutral"> </span>
|
|
132
|
+
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">/** In-memory KVNamespace mock for testing. */
|
|
133
|
+
export function createMockKV(): KVNamespace {
|
|
134
|
+
const store = new Map<string, string>();
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
get(key: string, _opts?: unknown): Promise<string | null> {
|
|
138
|
+
return Promise.resolve(store.get(key) ?? null);
|
|
139
|
+
},
|
|
140
|
+
put(key: string, value: string): Promise<void> {
|
|
141
|
+
store.set(key, value);
|
|
142
|
+
return Promise.resolve();
|
|
143
|
+
},
|
|
144
|
+
delete(key: string): Promise<void> {
|
|
145
|
+
store.delete(key);
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
},
|
|
148
|
+
list(opts?: { prefix?: string }): Promise<KVNamespaceListResult<unknown, string>> {
|
|
149
|
+
let storeKeys = [...store.keys()];
|
|
150
|
+
<span class="missing-if-branch" title="else path not taken" >E</span>if (opts?.prefix) {
|
|
151
|
+
storeKeys = storeKeys.filter((k) => k.startsWith(opts.prefix!));
|
|
152
|
+
}
|
|
153
|
+
const keys = storeKeys.map((name) => ({ name }));
|
|
154
|
+
return Promise.resolve({
|
|
155
|
+
keys,
|
|
156
|
+
list_complete: true,
|
|
157
|
+
cacheStatus: null,
|
|
158
|
+
} as KVNamespaceListResult<unknown, string>);
|
|
159
|
+
},
|
|
160
|
+
<span class="fstat-no" title="function not covered" > getWithMetadata(): Promise<KVNamespaceGetWithMetadataResult<string, unknown>> {</span>
|
|
161
|
+
<span class="cstat-no" title="statement not covered" > return Promise.resolve({ value: null, metadata: null, cacheStatus: null });</span>
|
|
162
|
+
},
|
|
163
|
+
} as unknown as KVNamespace;
|
|
164
|
+
}
|
|
165
|
+
</pre></td></tr></table></pre>
|
|
166
|
+
|
|
167
|
+
<div class='push'></div><!-- for sticky footer -->
|
|
168
|
+
</div><!-- /wrapper -->
|
|
169
|
+
<div class='footer quiet pad2 space-top1 center small'>
|
|
170
|
+
Code coverage generated by
|
|
171
|
+
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
172
|
+
at 2026-03-31T14:33:55.154Z
|
|
173
|
+
</div>
|
|
174
|
+
<script src="../prettify.js"></script>
|
|
175
|
+
<script>
|
|
176
|
+
window.onload = function () {
|
|
177
|
+
prettyPrint();
|
|
178
|
+
};
|
|
179
|
+
</script>
|
|
180
|
+
<script src="../sorter.js"></script>
|
|
181
|
+
<script src="../block-navigation.js"></script>
|
|
182
|
+
</body>
|
|
183
|
+
</html>
|
|
184
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<title>Code coverage report for src/utils</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> src/utils</h1>
|
|
23
|
+
<div class='clearfix'>
|
|
24
|
+
|
|
25
|
+
<div class='fl pad1y space-right2'>
|
|
26
|
+
<span class="strong">0% </span>
|
|
27
|
+
<span class="quiet">Statements</span>
|
|
28
|
+
<span class='fraction'>0/8</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<div class='fl pad1y space-right2'>
|
|
33
|
+
<span class="strong">0% </span>
|
|
34
|
+
<span class="quiet">Branches</span>
|
|
35
|
+
<span class='fraction'>0/4</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
<div class='fl pad1y space-right2'>
|
|
40
|
+
<span class="strong">0% </span>
|
|
41
|
+
<span class="quiet">Functions</span>
|
|
42
|
+
<span class='fraction'>0/2</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<div class='fl pad1y space-right2'>
|
|
47
|
+
<span class="strong">0% </span>
|
|
48
|
+
<span class="quiet">Lines</span>
|
|
49
|
+
<span class='fraction'>0/7</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 low'></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 low" data-value="qr.ts"><a href="qr.ts.html">qr.ts</a></td>
|
|
83
|
+
<td data-value="0" class="pic low">
|
|
84
|
+
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
|
|
85
|
+
</td>
|
|
86
|
+
<td data-value="0" class="pct low">0%</td>
|
|
87
|
+
<td data-value="8" class="abs low">0/8</td>
|
|
88
|
+
<td data-value="0" class="pct low">0%</td>
|
|
89
|
+
<td data-value="4" class="abs low">0/4</td>
|
|
90
|
+
<td data-value="0" class="pct low">0%</td>
|
|
91
|
+
<td data-value="2" class="abs low">0/2</td>
|
|
92
|
+
<td data-value="0" class="pct low">0%</td>
|
|
93
|
+
<td data-value="7" class="abs low">0/7</td>
|
|
94
|
+
</tr>
|
|
95
|
+
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
</div>
|
|
99
|
+
<div class='push'></div><!-- for sticky footer -->
|
|
100
|
+
</div><!-- /wrapper -->
|
|
101
|
+
<div class='footer quiet pad2 space-top1 center small'>
|
|
102
|
+
Code coverage generated by
|
|
103
|
+
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
104
|
+
at 2026-03-31T14:33:55.154Z
|
|
105
|
+
</div>
|
|
106
|
+
<script src="../../prettify.js"></script>
|
|
107
|
+
<script>
|
|
108
|
+
window.onload = function () {
|
|
109
|
+
prettyPrint();
|
|
110
|
+
};
|
|
111
|
+
</script>
|
|
112
|
+
<script src="../../sorter.js"></script>
|
|
113
|
+
<script src="../../block-navigation.js"></script>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<title>Code coverage report for src/utils/qr.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> / <a href="index.html">src/utils</a> qr.ts</h1>
|
|
23
|
+
<div class='clearfix'>
|
|
24
|
+
|
|
25
|
+
<div class='fl pad1y space-right2'>
|
|
26
|
+
<span class="strong">0% </span>
|
|
27
|
+
<span class="quiet">Statements</span>
|
|
28
|
+
<span class='fraction'>0/8</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<div class='fl pad1y space-right2'>
|
|
33
|
+
<span class="strong">0% </span>
|
|
34
|
+
<span class="quiet">Branches</span>
|
|
35
|
+
<span class='fraction'>0/4</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
<div class='fl pad1y space-right2'>
|
|
40
|
+
<span class="strong">0% </span>
|
|
41
|
+
<span class="quiet">Functions</span>
|
|
42
|
+
<span class='fraction'>0/2</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<div class='fl pad1y space-right2'>
|
|
47
|
+
<span class="strong">0% </span>
|
|
48
|
+
<span class="quiet">Lines</span>
|
|
49
|
+
<span class='fraction'>0/7</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 low'></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></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
|
102
|
+
<span class="cline-any cline-neutral"> </span>
|
|
103
|
+
<span class="cline-any cline-neutral"> </span>
|
|
104
|
+
<span class="cline-any cline-neutral"> </span>
|
|
105
|
+
<span class="cline-any cline-neutral"> </span>
|
|
106
|
+
<span class="cline-any cline-neutral"> </span>
|
|
107
|
+
<span class="cline-any cline-neutral"> </span>
|
|
108
|
+
<span class="cline-any cline-neutral"> </span>
|
|
109
|
+
<span class="cline-any cline-neutral"> </span>
|
|
110
|
+
<span class="cline-any cline-neutral"> </span>
|
|
111
|
+
<span class="cline-any cline-neutral"> </span>
|
|
112
|
+
<span class="cline-any cline-neutral"> </span>
|
|
113
|
+
<span class="cline-any cline-neutral"> </span>
|
|
114
|
+
<span class="cline-any cline-neutral"> </span>
|
|
115
|
+
<span class="cline-any cline-neutral"> </span>
|
|
116
|
+
<span class="cline-any cline-neutral"> </span>
|
|
117
|
+
<span class="cline-any cline-no"> </span>
|
|
118
|
+
<span class="cline-any cline-no"> </span>
|
|
119
|
+
<span class="cline-any cline-neutral"> </span>
|
|
120
|
+
<span class="cline-any cline-neutral"> </span>
|
|
121
|
+
<span class="cline-any cline-neutral"> </span>
|
|
122
|
+
<span class="cline-any cline-neutral"> </span>
|
|
123
|
+
<span class="cline-any cline-no"> </span>
|
|
124
|
+
<span class="cline-any cline-neutral"> </span>
|
|
125
|
+
<span class="cline-any cline-neutral"> </span>
|
|
126
|
+
<span class="cline-any cline-neutral"> </span>
|
|
127
|
+
<span class="cline-any cline-no"> </span>
|
|
128
|
+
<span class="cline-any cline-neutral"> </span>
|
|
129
|
+
<span class="cline-any cline-neutral"> </span>
|
|
130
|
+
<span class="cline-any cline-neutral"> </span>
|
|
131
|
+
<span class="cline-any cline-neutral"> </span>
|
|
132
|
+
<span class="cline-any cline-no"> </span>
|
|
133
|
+
<span class="cline-any cline-no"> </span>
|
|
134
|
+
<span class="cline-any cline-no"> </span>
|
|
135
|
+
<span class="cline-any cline-neutral"> </span>
|
|
136
|
+
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import QRCode from 'qrcode';
|
|
137
|
+
|
|
138
|
+
export type QrFormat = 'svg' | 'png';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate a QR code for the given text.
|
|
142
|
+
* @param text - The content to encode (typically a URL).
|
|
143
|
+
* @param format - Output format: 'svg' or 'png'.
|
|
144
|
+
* @param size - Width/height in pixels (for PNG) or module count hint.
|
|
145
|
+
* @returns The QR code as a string (SVG) or Buffer (PNG).
|
|
146
|
+
*/
|
|
147
|
+
export async function <span class="fstat-no" title="function not covered" >generateQr(</span>
|
|
148
|
+
text: string,
|
|
149
|
+
format: QrFormat = <span class="branch-0 cbranch-no" title="branch not covered" >'svg',</span>
|
|
150
|
+
size: number = <span class="branch-0 cbranch-no" title="branch not covered" >256,</span>
|
|
151
|
+
): Promise<{ data: string | Buffer; contentType: string }> {
|
|
152
|
+
<span class="cstat-no" title="statement not covered" > if (format === 'svg') {</span>
|
|
153
|
+
const svg = <span class="cstat-no" title="statement not covered" >await QRCode.toString(text, {</span>
|
|
154
|
+
type: 'svg',
|
|
155
|
+
width: size,
|
|
156
|
+
margin: 2,
|
|
157
|
+
});
|
|
158
|
+
<span class="cstat-no" title="statement not covered" > return { data: svg, contentType: 'image/svg+xml' };</span>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// PNG format
|
|
162
|
+
const dataUrl = <span class="cstat-no" title="statement not covered" >await QRCode.toDataURL(text, {</span>
|
|
163
|
+
width: size,
|
|
164
|
+
margin: 2,
|
|
165
|
+
});
|
|
166
|
+
// Strip the data:image/png;base64, prefix and decode
|
|
167
|
+
const base64 = <span class="cstat-no" title="statement not covered" >dataUrl.replace(/^data:image\/png;base64,/, '');</span>
|
|
168
|
+
const buffer = <span class="cstat-no" title="statement not covered" >Uint8Array.from(atob(base64), <span class="fstat-no" title="function not covered" >(c</span>h) => <span class="cstat-no" title="statement not covered" >ch.charCodeAt(0))</span>;</span>
|
|
169
|
+
<span class="cstat-no" title="statement not covered" > return { data: buffer as unknown as Buffer, contentType: 'image/png' };</span>
|
|
170
|
+
}
|
|
171
|
+
</pre></td></tr></table></pre>
|
|
172
|
+
|
|
173
|
+
<div class='push'></div><!-- for sticky footer -->
|
|
174
|
+
</div><!-- /wrapper -->
|
|
175
|
+
<div class='footer quiet pad2 space-top1 center small'>
|
|
176
|
+
Code coverage generated by
|
|
177
|
+
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
178
|
+
at 2026-03-31T14:33:55.154Z
|
|
179
|
+
</div>
|
|
180
|
+
<script src="../../prettify.js"></script>
|
|
181
|
+
<script>
|
|
182
|
+
window.onload = function () {
|
|
183
|
+
prettyPrint();
|
|
184
|
+
};
|
|
185
|
+
</script>
|
|
186
|
+
<script src="../../sorter.js"></script>
|
|
187
|
+
<script src="../../block-navigation.js"></script>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipr/worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Cloudflare Worker for clipr URL redirects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"hono": "^4.7.0",
|
|
9
9
|
"qrcode": "^1.5.4",
|
|
10
|
-
"@clipr/core": "0.0.
|
|
10
|
+
"@clipr/core": "0.0.9"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@cloudflare/workers-types": "^4.20250327.0",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { hashPassword, verifyPassword } from './crypto.js';
|
|
3
|
+
|
|
4
|
+
describe('hashPassword', () => {
|
|
5
|
+
it('returns a string in salt:hash format', async () => {
|
|
6
|
+
const result = await hashPassword('mypassword');
|
|
7
|
+
const parts = result.split(':');
|
|
8
|
+
expect(parts).toHaveLength(2);
|
|
9
|
+
// Salt is 16 bytes = 32 hex chars
|
|
10
|
+
expect(parts[0]).toMatch(/^[0-9a-f]{32}$/);
|
|
11
|
+
// Hash is 256 bits = 32 bytes = 64 hex chars
|
|
12
|
+
expect(parts[1]).toMatch(/^[0-9a-f]{64}$/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('produces different hashes for the same password (random salt)', async () => {
|
|
16
|
+
const a = await hashPassword('same');
|
|
17
|
+
const b = await hashPassword('same');
|
|
18
|
+
expect(a).not.toBe(b);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('verifyPassword', () => {
|
|
23
|
+
it('returns true for correct password', async () => {
|
|
24
|
+
const stored = await hashPassword('correct');
|
|
25
|
+
expect(await verifyPassword('correct', stored)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns false for wrong password', async () => {
|
|
29
|
+
const stored = await hashPassword('correct');
|
|
30
|
+
expect(await verifyPassword('wrong', stored)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns false for malformed stored hash (no colon)', async () => {
|
|
34
|
+
expect(await verifyPassword('anything', 'nocolonhere')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns false for malformed stored hash (empty parts)', async () => {
|
|
38
|
+
expect(await verifyPassword('anything', ':')).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { cors } from 'hono/cors';
|
|
3
3
|
import { authMiddleware } from './middleware/auth.js';
|
|
4
|
+
import { rateLimitMiddleware } from './middleware/rate-limit.js';
|
|
4
5
|
import { handleHealth } from './routes/health.js';
|
|
5
6
|
import { handleExport, handleImport } from './routes/import-export.js';
|
|
6
7
|
import {
|
|
@@ -37,8 +38,8 @@ app.get('/health', handleHealth);
|
|
|
37
38
|
app.get('/password/:code', handlePasswordPage);
|
|
38
39
|
app.post('/password/:code', handlePasswordVerify);
|
|
39
40
|
|
|
40
|
-
// --- API
|
|
41
|
-
app.post('/api/shorten', handleShorten);
|
|
41
|
+
// --- Public API (rate-limited, no auth) ---
|
|
42
|
+
app.post('/api/shorten', rateLimitMiddleware, handleShorten);
|
|
42
43
|
|
|
43
44
|
app.get('/api/links', handleListLinks);
|
|
44
45
|
app.get('/api/links/:code', handleGetLink);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import app from '../index.js';
|
|
3
|
+
import { createMockKV } from '../test-utils.js';
|
|
4
|
+
|
|
5
|
+
const ENV = (kv: KVNamespace) => ({
|
|
6
|
+
URLS: kv,
|
|
7
|
+
API_TOKEN: 'test-token',
|
|
8
|
+
BASE_URL: 'https://test.sh',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('auth middleware', () => {
|
|
12
|
+
// --- Public routes pass without token ---
|
|
13
|
+
it('allows GET /health without token', async () => {
|
|
14
|
+
const kv = createMockKV();
|
|
15
|
+
const res = await app.request('/health', {}, ENV(kv));
|
|
16
|
+
expect(res.status).toBe(200);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('allows GET /:slug without token (redirect/404)', async () => {
|
|
20
|
+
const kv = createMockKV();
|
|
21
|
+
const res = await app.request('/some-slug', {}, ENV(kv));
|
|
22
|
+
// No entry seeded, so it returns 404 — but NOT 401
|
|
23
|
+
expect(res.status).toBe(404);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// --- API routes require token ---
|
|
27
|
+
it('returns 401 for API route without Authorization header', async () => {
|
|
28
|
+
const kv = createMockKV();
|
|
29
|
+
const res = await app.request('/api/links', {}, ENV(kv));
|
|
30
|
+
expect(res.status).toBe(401);
|
|
31
|
+
const body = await res.json();
|
|
32
|
+
expect(body.error).toMatch(/Missing Authorization/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns 401 for API route with wrong token', async () => {
|
|
36
|
+
const kv = createMockKV();
|
|
37
|
+
const res = await app.request(
|
|
38
|
+
'/api/links',
|
|
39
|
+
{ headers: { Authorization: 'Bearer wrong-token' } },
|
|
40
|
+
ENV(kv),
|
|
41
|
+
);
|
|
42
|
+
expect(res.status).toBe(401);
|
|
43
|
+
const body = await res.json();
|
|
44
|
+
expect(body.error).toMatch(/Invalid API token/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns 401 for malformed Authorization header', async () => {
|
|
48
|
+
const kv = createMockKV();
|
|
49
|
+
const res = await app.request(
|
|
50
|
+
'/api/links',
|
|
51
|
+
{ headers: { Authorization: 'Basic abc123' } },
|
|
52
|
+
ENV(kv),
|
|
53
|
+
);
|
|
54
|
+
expect(res.status).toBe(401);
|
|
55
|
+
const body = await res.json();
|
|
56
|
+
expect(body.error).toMatch(/Invalid Authorization format/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('allows API route with correct Bearer token', async () => {
|
|
60
|
+
const kv = createMockKV();
|
|
61
|
+
const res = await app.request(
|
|
62
|
+
'/api/links',
|
|
63
|
+
{ headers: { Authorization: 'Bearer test-token' } },
|
|
64
|
+
ENV(kv),
|
|
65
|
+
);
|
|
66
|
+
expect(res.status).toBe(200);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('allows POST /password/:code without token', async () => {
|
|
70
|
+
const kv = createMockKV();
|
|
71
|
+
const res = await app.request(
|
|
72
|
+
'/password/some-code',
|
|
73
|
+
{
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
76
|
+
body: 'password=test',
|
|
77
|
+
},
|
|
78
|
+
ENV(kv),
|
|
79
|
+
);
|
|
80
|
+
// The route may return 404 if link not found, but NOT 401
|
|
81
|
+
expect(res.status).not.toBe(401);
|
|
82
|
+
});
|
|
83
|
+
});
|