@vltpkg/vsr 0.0.0-26
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/DEPLOY.md +163 -0
- package/LICENSE +119 -0
- package/README.md +314 -0
- package/config.ts +221 -0
- package/drizzle.config.js +40 -0
- package/info/COMPARISONS.md +37 -0
- package/info/CONFIGURATION.md +143 -0
- package/info/CONTRIBUTING.md +32 -0
- package/info/DATABASE_SETUP.md +108 -0
- package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
- package/info/PROJECT_STRUCTURE.md +291 -0
- package/info/ROADMAP.md +27 -0
- package/info/SUPPORT.md +39 -0
- package/info/TESTING.md +301 -0
- package/info/USER_SUPPORT.md +31 -0
- package/package.json +77 -0
- package/scripts/build-assets.js +31 -0
- package/scripts/build-bin.js +62 -0
- package/scripts/prepack.js +27 -0
- package/src/assets/public/images/bg.png +0 -0
- package/src/assets/public/images/clients/logo-bun.png +0 -0
- package/src/assets/public/images/clients/logo-deno.png +0 -0
- package/src/assets/public/images/clients/logo-npm.png +0 -0
- package/src/assets/public/images/clients/logo-pnpm.png +0 -0
- package/src/assets/public/images/clients/logo-vlt.png +0 -0
- package/src/assets/public/images/clients/logo-yarn.png +0 -0
- package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
- package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
- package/src/assets/public/images/favicon/favicon.ico +0 -0
- package/src/assets/public/images/favicon/favicon.svg +3 -0
- package/src/assets/public/images/favicon/site.webmanifest +21 -0
- package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
- package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
- package/src/assets/public/styles/styles.css +231 -0
- package/src/bin/demo/package.json +6 -0
- package/src/bin/demo/vlt.json +1 -0
- package/src/bin/vsr.ts +484 -0
- package/src/db/client.ts +590 -0
- package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
- package/src/db/migrations/0000_initial.sql +29 -0
- package/src/db/migrations/0001_uuid_validation.sql +35 -0
- package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
- package/src/db/migrations/drop.sql +3 -0
- package/src/db/migrations/meta/0000_snapshot.json +104 -0
- package/src/db/migrations/meta/0001_snapshot.json +155 -0
- package/src/db/migrations/meta/_journal.json +20 -0
- package/src/db/schema.ts +43 -0
- package/src/index.ts +434 -0
- package/src/middleware/config.ts +79 -0
- package/src/middleware/telemetry.ts +43 -0
- package/src/queue/index.ts +97 -0
- package/src/routes/access.ts +852 -0
- package/src/routes/docs.ts +63 -0
- package/src/routes/misc.ts +469 -0
- package/src/routes/packages.ts +2823 -0
- package/src/routes/ping.ts +39 -0
- package/src/routes/search.ts +131 -0
- package/src/routes/static.ts +74 -0
- package/src/routes/tokens.ts +259 -0
- package/src/routes/users.ts +68 -0
- package/src/utils/auth.ts +202 -0
- package/src/utils/cache.ts +587 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/database.ts +69 -0
- package/src/utils/docs.ts +146 -0
- package/src/utils/packages.ts +453 -0
- package/src/utils/response.ts +125 -0
- package/src/utils/routes.ts +64 -0
- package/src/utils/spa.ts +52 -0
- package/src/utils/tracing.ts +52 -0
- package/src/utils/upstream.ts +172 -0
- package/test/access.test.ts +705 -0
- package/test/audit.test.ts +828 -0
- package/test/dashboard.test.ts +693 -0
- package/test/dist-tags.test.ts +678 -0
- package/test/manifest.test.ts +436 -0
- package/test/packument.test.ts +530 -0
- package/test/ping.test.ts +41 -0
- package/test/search.test.ts +472 -0
- package/test/setup.ts +130 -0
- package/test/static.test.ts +646 -0
- package/test/tokens.test.ts +389 -0
- package/test/utils/auth.test.ts +214 -0
- package/test/utils/packages.test.ts +235 -0
- package/test/utils/response.test.ts +184 -0
- package/test/whoami.test.ts +119 -0
- package/tsconfig.json +16 -0
- package/tsconfig.worker.json +3 -0
- package/typedoc.mjs +2 -0
- package/types.ts +598 -0
- package/vitest.config.ts +25 -0
- package/vlt.json.example +56 -0
- package/wrangler.json +65 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { env } from 'cloudflare:test'
|
|
3
|
+
import { app } from '../src/index.ts'
|
|
4
|
+
|
|
5
|
+
describe('Token Management Endpoints', () => {
|
|
6
|
+
describe('Root Registry Token Management', () => {
|
|
7
|
+
describe('GET /-/tokens', () => {
|
|
8
|
+
it('should require authentication for token listing', async () => {
|
|
9
|
+
const res = await app.request('/-/tokens', {}, env)
|
|
10
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
11
|
+
expect(res.headers.get('content-type')).toContain(
|
|
12
|
+
'application/json',
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should handle authenticated token listing', async () => {
|
|
17
|
+
const res = await app.request(
|
|
18
|
+
'/-/tokens',
|
|
19
|
+
{
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: 'Bearer test-admin-token-12345',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
env,
|
|
25
|
+
)
|
|
26
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
27
|
+
expect(res.headers.get('content-type')).toContain(
|
|
28
|
+
'application/json',
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should return proper JSON structure for token list', async () => {
|
|
33
|
+
const res = await app.request('/-/tokens', {}, env)
|
|
34
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
35
|
+
if (res.status === 200) {
|
|
36
|
+
const data = (await res.json()) as any
|
|
37
|
+
expect(data).toBeDefined()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('POST /-/tokens', () => {
|
|
43
|
+
it('should handle token creation requests', async () => {
|
|
44
|
+
const res = await app.request(
|
|
45
|
+
'/-/tokens',
|
|
46
|
+
{
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
password: 'test-password',
|
|
53
|
+
readonly: false,
|
|
54
|
+
cidr_whitelist: [],
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
env,
|
|
58
|
+
)
|
|
59
|
+
expect([200, 400, 401, 500].includes(res.status)).toBe(true)
|
|
60
|
+
expect(res.headers.get('content-type')).toContain(
|
|
61
|
+
'application/json',
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should handle token creation with readonly flag', async () => {
|
|
66
|
+
const res = await app.request(
|
|
67
|
+
'/-/tokens',
|
|
68
|
+
{
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
password: 'test-password',
|
|
75
|
+
readonly: true,
|
|
76
|
+
cidr_whitelist: [],
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
env,
|
|
80
|
+
)
|
|
81
|
+
expect([200, 400, 401, 500].includes(res.status)).toBe(true)
|
|
82
|
+
expect(res.headers.get('content-type')).toContain(
|
|
83
|
+
'application/json',
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should validate required fields for token creation', async () => {
|
|
88
|
+
const res = await app.request(
|
|
89
|
+
'/-/tokens',
|
|
90
|
+
{
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({}),
|
|
96
|
+
},
|
|
97
|
+
env,
|
|
98
|
+
)
|
|
99
|
+
expect([400, 401, 500].includes(res.status)).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('PUT /-/tokens', () => {
|
|
104
|
+
it('should handle token update requests', async () => {
|
|
105
|
+
const res = await app.request(
|
|
106
|
+
'/-/tokens',
|
|
107
|
+
{
|
|
108
|
+
method: 'PUT',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
token: 'existing-token',
|
|
114
|
+
readonly: true,
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
env,
|
|
118
|
+
)
|
|
119
|
+
expect([200, 400, 401, 404, 500].includes(res.status)).toBe(
|
|
120
|
+
true,
|
|
121
|
+
)
|
|
122
|
+
expect(res.headers.get('content-type')).toContain(
|
|
123
|
+
'application/json',
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should handle token scope updates', async () => {
|
|
128
|
+
const res = await app.request(
|
|
129
|
+
'/-/tokens',
|
|
130
|
+
{
|
|
131
|
+
method: 'PUT',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
token: 'existing-token',
|
|
137
|
+
cidr_whitelist: ['192.168.1.0/24'],
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
env,
|
|
141
|
+
)
|
|
142
|
+
expect([200, 400, 401, 404, 500].includes(res.status)).toBe(
|
|
143
|
+
true,
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('DELETE /-/tokens/{token}', () => {
|
|
149
|
+
it('should handle token deletion requests', async () => {
|
|
150
|
+
const res = await app.request(
|
|
151
|
+
'/-/tokens/test-token-to-delete',
|
|
152
|
+
{
|
|
153
|
+
method: 'DELETE',
|
|
154
|
+
},
|
|
155
|
+
env,
|
|
156
|
+
)
|
|
157
|
+
expect([200, 401, 404, 500].includes(res.status)).toBe(true)
|
|
158
|
+
if (res.status !== 404) {
|
|
159
|
+
expect(res.headers.get('content-type')).toContain(
|
|
160
|
+
'application/json',
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should require valid token for deletion', async () => {
|
|
166
|
+
const res = await app.request(
|
|
167
|
+
'/-/tokens/invalid-token',
|
|
168
|
+
{
|
|
169
|
+
method: 'DELETE',
|
|
170
|
+
},
|
|
171
|
+
env,
|
|
172
|
+
)
|
|
173
|
+
expect([401, 404, 500].includes(res.status)).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should handle authenticated token deletion', async () => {
|
|
177
|
+
const res = await app.request(
|
|
178
|
+
'/-/tokens/test-token-to-delete',
|
|
179
|
+
{
|
|
180
|
+
method: 'DELETE',
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: 'Bearer test-admin-token-12345',
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
env,
|
|
186
|
+
)
|
|
187
|
+
expect([200, 404, 500].includes(res.status)).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('Upstream Registry Token Management', () => {
|
|
193
|
+
describe('Upstream Token Endpoints', () => {
|
|
194
|
+
it('should handle upstream token listing', async () => {
|
|
195
|
+
const res = await app.request('/npm/-/tokens', {}, env)
|
|
196
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
197
|
+
expect(res.headers.get('content-type')).toContain(
|
|
198
|
+
'application/json',
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should handle upstream token creation', async () => {
|
|
203
|
+
const res = await app.request(
|
|
204
|
+
'/npm/-/tokens',
|
|
205
|
+
{
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: {
|
|
208
|
+
'Content-Type': 'application/json',
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
password: 'test-password',
|
|
212
|
+
readonly: false,
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
env,
|
|
216
|
+
)
|
|
217
|
+
expect([200, 400, 401, 500].includes(res.status)).toBe(true)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should handle upstream token updates', async () => {
|
|
221
|
+
const res = await app.request(
|
|
222
|
+
'/npm/-/tokens',
|
|
223
|
+
{
|
|
224
|
+
method: 'PUT',
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'application/json',
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
token: 'existing-token',
|
|
230
|
+
readonly: true,
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
env,
|
|
234
|
+
)
|
|
235
|
+
expect([200, 400, 401, 404, 500].includes(res.status)).toBe(
|
|
236
|
+
true,
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should handle upstream token deletion', async () => {
|
|
241
|
+
const res = await app.request(
|
|
242
|
+
'/npm/-/tokens/test-token',
|
|
243
|
+
{
|
|
244
|
+
method: 'DELETE',
|
|
245
|
+
},
|
|
246
|
+
env,
|
|
247
|
+
)
|
|
248
|
+
expect([200, 401, 404, 500].includes(res.status)).toBe(true)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('Different Upstream Registries', () => {
|
|
253
|
+
it('should handle JSR token management', async () => {
|
|
254
|
+
const res = await app.request('/jsr/-/tokens', {}, env)
|
|
255
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should handle custom upstream token management', async () => {
|
|
259
|
+
const res = await app.request('/custom/-/tokens', {}, env)
|
|
260
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should handle local upstream token management', async () => {
|
|
264
|
+
const res = await app.request('/local/-/tokens', {}, env)
|
|
265
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('Token Response Structure', () => {
|
|
271
|
+
describe('Token List Response', () => {
|
|
272
|
+
it('should return proper structure for token listing', async () => {
|
|
273
|
+
const res = await app.request('/-/tokens', {}, env)
|
|
274
|
+
expect([200, 400, 401].includes(res.status)).toBe(true)
|
|
275
|
+
if (res.status === 200) {
|
|
276
|
+
const data = (await res.json()) as any
|
|
277
|
+
expect(data).toBeDefined()
|
|
278
|
+
// Token list structure validation would depend on actual implementation
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('Token Creation Response', () => {
|
|
284
|
+
it('should return token details on successful creation', async () => {
|
|
285
|
+
const res = await app.request(
|
|
286
|
+
'/-/tokens',
|
|
287
|
+
{
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: {
|
|
290
|
+
'Content-Type': 'application/json',
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify({
|
|
293
|
+
password: 'test-password',
|
|
294
|
+
readonly: false,
|
|
295
|
+
}),
|
|
296
|
+
},
|
|
297
|
+
env,
|
|
298
|
+
)
|
|
299
|
+
if (res.status === 200) {
|
|
300
|
+
const data = (await res.json()) as any
|
|
301
|
+
expect(data).toBeDefined()
|
|
302
|
+
// Would validate token structure based on implementation
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe('Error Handling', () => {
|
|
309
|
+
describe('Invalid Requests', () => {
|
|
310
|
+
it('should handle malformed JSON in token creation', async () => {
|
|
311
|
+
const res = await app.request(
|
|
312
|
+
'/-/tokens',
|
|
313
|
+
{
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: {
|
|
316
|
+
'Content-Type': 'application/json',
|
|
317
|
+
},
|
|
318
|
+
body: 'invalid-json',
|
|
319
|
+
},
|
|
320
|
+
env,
|
|
321
|
+
)
|
|
322
|
+
expect([400, 500].includes(res.status)).toBe(true)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should handle missing content-type header', async () => {
|
|
326
|
+
const res = await app.request(
|
|
327
|
+
'/-/tokens',
|
|
328
|
+
{
|
|
329
|
+
method: 'POST',
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
password: 'test-password',
|
|
332
|
+
}),
|
|
333
|
+
},
|
|
334
|
+
env,
|
|
335
|
+
)
|
|
336
|
+
expect([400, 415, 500].includes(res.status)).toBe(true)
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('Authentication Errors', () => {
|
|
341
|
+
it('should handle invalid authentication tokens', async () => {
|
|
342
|
+
const res = await app.request(
|
|
343
|
+
'/-/tokens/some-token',
|
|
344
|
+
{
|
|
345
|
+
method: 'DELETE',
|
|
346
|
+
headers: {
|
|
347
|
+
Authorization: 'Bearer invalid-token',
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
env,
|
|
351
|
+
)
|
|
352
|
+
expect([401, 404, 500].includes(res.status)).toBe(true)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should handle malformed authorization headers', async () => {
|
|
356
|
+
const res = await app.request(
|
|
357
|
+
'/-/tokens/some-token',
|
|
358
|
+
{
|
|
359
|
+
method: 'DELETE',
|
|
360
|
+
headers: {
|
|
361
|
+
Authorization: 'InvalidFormat',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
env,
|
|
365
|
+
)
|
|
366
|
+
expect([401, 404, 500].includes(res.status)).toBe(true)
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('Response Headers and Caching', () => {
|
|
372
|
+
describe('Content-Type Headers', () => {
|
|
373
|
+
it('should set appropriate content-type for token responses', async () => {
|
|
374
|
+
const res = await app.request('/-/tokens', {}, env)
|
|
375
|
+
expect(res.headers.get('content-type')).toContain(
|
|
376
|
+
'application/json',
|
|
377
|
+
)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('Security Headers', () => {
|
|
382
|
+
it('should include security headers in token responses', async () => {
|
|
383
|
+
const res = await app.request('/-/tokens', {}, env)
|
|
384
|
+
// Security headers would be validated based on implementation
|
|
385
|
+
expect(res.status).toBeDefined()
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
})
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getTokenFromHeader,
|
|
4
|
+
parseTokenAccess,
|
|
5
|
+
} from '../../src/utils/auth.ts'
|
|
6
|
+
import type { HonoContext, TokenScope } from '../../types.ts'
|
|
7
|
+
|
|
8
|
+
describe('Auth Utils', () => {
|
|
9
|
+
describe('getTokenFromHeader', () => {
|
|
10
|
+
it('should extract token from Bearer authorization header', () => {
|
|
11
|
+
const mockContext = {
|
|
12
|
+
req: {
|
|
13
|
+
header: vi.fn().mockReturnValue('Bearer test-token-12345'),
|
|
14
|
+
},
|
|
15
|
+
} as unknown as HonoContext
|
|
16
|
+
|
|
17
|
+
const token = getTokenFromHeader(mockContext)
|
|
18
|
+
expect(token).toBe('test-token-12345')
|
|
19
|
+
expect(mockContext.req.header).toHaveBeenCalledWith(
|
|
20
|
+
'Authorization',
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should handle Bearer token with extra whitespace', () => {
|
|
25
|
+
const mockContext = {
|
|
26
|
+
req: {
|
|
27
|
+
header: vi
|
|
28
|
+
.fn()
|
|
29
|
+
.mockReturnValue('Bearer test-token-with-spaces '),
|
|
30
|
+
},
|
|
31
|
+
} as unknown as HonoContext
|
|
32
|
+
|
|
33
|
+
const token = getTokenFromHeader(mockContext)
|
|
34
|
+
expect(token).toBe('test-token-with-spaces')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should return null for missing authorization header', () => {
|
|
38
|
+
const mockContext = {
|
|
39
|
+
req: {
|
|
40
|
+
header: vi.fn().mockReturnValue(undefined),
|
|
41
|
+
},
|
|
42
|
+
} as unknown as HonoContext
|
|
43
|
+
|
|
44
|
+
const token = getTokenFromHeader(mockContext)
|
|
45
|
+
expect(token).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should return null for non-Bearer authorization header', () => {
|
|
49
|
+
const mockContext = {
|
|
50
|
+
req: {
|
|
51
|
+
header: vi.fn().mockReturnValue('Basic dXNlcjpwYXNz'),
|
|
52
|
+
},
|
|
53
|
+
} as unknown as HonoContext
|
|
54
|
+
|
|
55
|
+
const token = getTokenFromHeader(mockContext)
|
|
56
|
+
expect(token).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should return empty string for malformed Bearer header', () => {
|
|
60
|
+
const mockContext = {
|
|
61
|
+
req: {
|
|
62
|
+
header: vi.fn().mockReturnValue('Bearer '),
|
|
63
|
+
},
|
|
64
|
+
} as unknown as HonoContext
|
|
65
|
+
|
|
66
|
+
const token = getTokenFromHeader(mockContext)
|
|
67
|
+
expect(token).toBe('')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('parseTokenAccess', () => {
|
|
72
|
+
it('should parse read-only access for any package', () => {
|
|
73
|
+
const scope: TokenScope[] = [
|
|
74
|
+
{
|
|
75
|
+
types: { pkg: { read: true, write: false } },
|
|
76
|
+
values: ['*'],
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const access = parseTokenAccess({
|
|
81
|
+
scope,
|
|
82
|
+
uuid: 'test-user-uuid',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(access.anyPackage).toBe(true)
|
|
86
|
+
expect(access.specificPackage).toBe(false)
|
|
87
|
+
expect(access.readAccess).toBe(true)
|
|
88
|
+
expect(access.writeAccess).toBe(false)
|
|
89
|
+
expect(access.methods).toEqual(['get'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should parse write access for specific package', () => {
|
|
93
|
+
const scope: TokenScope[] = [
|
|
94
|
+
{
|
|
95
|
+
types: { pkg: { read: false, write: true } },
|
|
96
|
+
values: ['my-package'],
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const access = parseTokenAccess({
|
|
101
|
+
scope,
|
|
102
|
+
pkg: 'my-package',
|
|
103
|
+
uuid: 'test-user-uuid',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect(access.anyPackage).toBe(false)
|
|
107
|
+
expect(access.specificPackage).toBe(true)
|
|
108
|
+
expect(access.readAccess).toBe(false)
|
|
109
|
+
expect(access.writeAccess).toBe(true)
|
|
110
|
+
expect(access.methods).toEqual(['put', 'post', 'delete'])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should parse user-specific access', () => {
|
|
114
|
+
const scope: TokenScope[] = [
|
|
115
|
+
{
|
|
116
|
+
types: { user: { read: true, write: true } },
|
|
117
|
+
values: ['~test-user-uuid'],
|
|
118
|
+
},
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
const access = parseTokenAccess({
|
|
122
|
+
scope,
|
|
123
|
+
uuid: 'test-user-uuid',
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(access.anyUser).toBe(false)
|
|
127
|
+
expect(access.specificUser).toBe(true)
|
|
128
|
+
expect(access.readAccess).toBe(true)
|
|
129
|
+
expect(access.writeAccess).toBe(true)
|
|
130
|
+
expect(access.methods).toEqual(['get', 'put', 'post', 'delete'])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should parse wildcard user access', () => {
|
|
134
|
+
const scope: TokenScope[] = [
|
|
135
|
+
{
|
|
136
|
+
types: { user: { read: true, write: false } },
|
|
137
|
+
values: ['*'],
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
const access = parseTokenAccess({
|
|
142
|
+
scope,
|
|
143
|
+
uuid: 'any-user-uuid',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(access.anyUser).toBe(true)
|
|
147
|
+
expect(access.specificUser).toBe(false)
|
|
148
|
+
expect(access.readAccess).toBe(true)
|
|
149
|
+
expect(access.writeAccess).toBe(false)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should handle multiple scopes with combined permissions', () => {
|
|
153
|
+
const scope: TokenScope[] = [
|
|
154
|
+
{
|
|
155
|
+
types: { pkg: { read: true, write: false } },
|
|
156
|
+
values: ['package-1'],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
types: { pkg: { read: false, write: true } },
|
|
160
|
+
values: ['package-1'],
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
const access = parseTokenAccess({
|
|
165
|
+
scope,
|
|
166
|
+
pkg: 'package-1',
|
|
167
|
+
uuid: 'test-user-uuid',
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
expect(access.specificPackage).toBe(true)
|
|
171
|
+
expect(access.readAccess).toBe(true)
|
|
172
|
+
expect(access.writeAccess).toBe(true)
|
|
173
|
+
expect(access.methods).toEqual(['get', 'put', 'post', 'delete'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should handle empty scope array', () => {
|
|
177
|
+
const scope: TokenScope[] = []
|
|
178
|
+
|
|
179
|
+
const access = parseTokenAccess({
|
|
180
|
+
scope,
|
|
181
|
+
uuid: 'test-user-uuid',
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(access.anyUser).toBe(false)
|
|
185
|
+
expect(access.specificUser).toBe(false)
|
|
186
|
+
expect(access.anyPackage).toBe(false)
|
|
187
|
+
expect(access.specificPackage).toBe(false)
|
|
188
|
+
expect(access.readAccess).toBe(false)
|
|
189
|
+
expect(access.writeAccess).toBe(false)
|
|
190
|
+
expect(access.methods).toEqual([])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle scope with no matching package', () => {
|
|
194
|
+
const scope: TokenScope[] = [
|
|
195
|
+
{
|
|
196
|
+
types: { pkg: true, user: false },
|
|
197
|
+
values: ['other-package'],
|
|
198
|
+
methods: ['get', 'put'],
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
const access = parseTokenAccess({
|
|
203
|
+
scope,
|
|
204
|
+
pkg: 'my-package',
|
|
205
|
+
uuid: 'test-user-uuid',
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect(access.specificPackage).toBe(false)
|
|
209
|
+
expect(access.readAccess).toBe(false)
|
|
210
|
+
expect(access.writeAccess).toBe(false)
|
|
211
|
+
expect(access.methods).toEqual([])
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|