A minimal AT Protocol Personal Data Server written in JavaScript.
0
fork

Configure Feed

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

migrate to vitest

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+1649 -561
+1
.gitignore
··· 4 4 .env 5 5 .dev.vars 6 6 .backup/ 7 + coverage/
+1211 -2
package-lock.json
··· 1 1 { 2 2 "name": "pds.js", 3 - "version": "0.1.0", 3 + "version": "0.6.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "pds.js", 9 - "version": "0.1.0", 9 + "version": "0.6.0", 10 10 "devDependencies": { 11 11 "@biomejs/biome": "^2.3.11", 12 12 "@cloudflare/workers-types": "^4.20260103.0", 13 + "@vitest/coverage-v8": "^4.0.16", 13 14 "typescript": "^5.9.3", 15 + "vitest": "^4.0.16", 14 16 "wrangler": "^4.54.0" 17 + } 18 + }, 19 + "node_modules/@babel/helper-string-parser": { 20 + "version": "7.27.1", 21 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 22 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 23 + "dev": true, 24 + "license": "MIT", 25 + "engines": { 26 + "node": ">=6.9.0" 27 + } 28 + }, 29 + "node_modules/@babel/helper-validator-identifier": { 30 + "version": "7.28.5", 31 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", 32 + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", 33 + "dev": true, 34 + "license": "MIT", 35 + "engines": { 36 + "node": ">=6.9.0" 37 + } 38 + }, 39 + "node_modules/@babel/parser": { 40 + "version": "7.28.5", 41 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", 42 + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", 43 + "dev": true, 44 + "license": "MIT", 45 + "dependencies": { 46 + "@babel/types": "^7.28.5" 47 + }, 48 + "bin": { 49 + "parser": "bin/babel-parser.js" 50 + }, 51 + "engines": { 52 + "node": ">=6.0.0" 53 + } 54 + }, 55 + "node_modules/@babel/types": { 56 + "version": "7.28.5", 57 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", 58 + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", 59 + "dev": true, 60 + "license": "MIT", 61 + "dependencies": { 62 + "@babel/helper-string-parser": "^7.27.1", 63 + "@babel/helper-validator-identifier": "^7.28.5" 64 + }, 65 + "engines": { 66 + "node": ">=6.9.0" 67 + } 68 + }, 69 + "node_modules/@bcoe/v8-coverage": { 70 + "version": "1.0.2", 71 + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", 72 + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", 73 + "dev": true, 74 + "license": "MIT", 75 + "engines": { 76 + "node": ">=18" 15 77 } 16 78 }, 17 79 "node_modules/@biomejs/biome": { ··· 1201 1263 "dev": true, 1202 1264 "license": "MIT" 1203 1265 }, 1266 + "node_modules/@rollup/rollup-android-arm-eabi": { 1267 + "version": "4.55.1", 1268 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", 1269 + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", 1270 + "cpu": [ 1271 + "arm" 1272 + ], 1273 + "dev": true, 1274 + "license": "MIT", 1275 + "optional": true, 1276 + "os": [ 1277 + "android" 1278 + ] 1279 + }, 1280 + "node_modules/@rollup/rollup-android-arm64": { 1281 + "version": "4.55.1", 1282 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", 1283 + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", 1284 + "cpu": [ 1285 + "arm64" 1286 + ], 1287 + "dev": true, 1288 + "license": "MIT", 1289 + "optional": true, 1290 + "os": [ 1291 + "android" 1292 + ] 1293 + }, 1294 + "node_modules/@rollup/rollup-darwin-arm64": { 1295 + "version": "4.55.1", 1296 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", 1297 + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", 1298 + "cpu": [ 1299 + "arm64" 1300 + ], 1301 + "dev": true, 1302 + "license": "MIT", 1303 + "optional": true, 1304 + "os": [ 1305 + "darwin" 1306 + ] 1307 + }, 1308 + "node_modules/@rollup/rollup-darwin-x64": { 1309 + "version": "4.55.1", 1310 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", 1311 + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", 1312 + "cpu": [ 1313 + "x64" 1314 + ], 1315 + "dev": true, 1316 + "license": "MIT", 1317 + "optional": true, 1318 + "os": [ 1319 + "darwin" 1320 + ] 1321 + }, 1322 + "node_modules/@rollup/rollup-freebsd-arm64": { 1323 + "version": "4.55.1", 1324 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", 1325 + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", 1326 + "cpu": [ 1327 + "arm64" 1328 + ], 1329 + "dev": true, 1330 + "license": "MIT", 1331 + "optional": true, 1332 + "os": [ 1333 + "freebsd" 1334 + ] 1335 + }, 1336 + "node_modules/@rollup/rollup-freebsd-x64": { 1337 + "version": "4.55.1", 1338 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", 1339 + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", 1340 + "cpu": [ 1341 + "x64" 1342 + ], 1343 + "dev": true, 1344 + "license": "MIT", 1345 + "optional": true, 1346 + "os": [ 1347 + "freebsd" 1348 + ] 1349 + }, 1350 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1351 + "version": "4.55.1", 1352 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", 1353 + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", 1354 + "cpu": [ 1355 + "arm" 1356 + ], 1357 + "dev": true, 1358 + "license": "MIT", 1359 + "optional": true, 1360 + "os": [ 1361 + "linux" 1362 + ] 1363 + }, 1364 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1365 + "version": "4.55.1", 1366 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", 1367 + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", 1368 + "cpu": [ 1369 + "arm" 1370 + ], 1371 + "dev": true, 1372 + "license": "MIT", 1373 + "optional": true, 1374 + "os": [ 1375 + "linux" 1376 + ] 1377 + }, 1378 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1379 + "version": "4.55.1", 1380 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", 1381 + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", 1382 + "cpu": [ 1383 + "arm64" 1384 + ], 1385 + "dev": true, 1386 + "license": "MIT", 1387 + "optional": true, 1388 + "os": [ 1389 + "linux" 1390 + ] 1391 + }, 1392 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1393 + "version": "4.55.1", 1394 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", 1395 + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", 1396 + "cpu": [ 1397 + "arm64" 1398 + ], 1399 + "dev": true, 1400 + "license": "MIT", 1401 + "optional": true, 1402 + "os": [ 1403 + "linux" 1404 + ] 1405 + }, 1406 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 1407 + "version": "4.55.1", 1408 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", 1409 + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", 1410 + "cpu": [ 1411 + "loong64" 1412 + ], 1413 + "dev": true, 1414 + "license": "MIT", 1415 + "optional": true, 1416 + "os": [ 1417 + "linux" 1418 + ] 1419 + }, 1420 + "node_modules/@rollup/rollup-linux-loong64-musl": { 1421 + "version": "4.55.1", 1422 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", 1423 + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", 1424 + "cpu": [ 1425 + "loong64" 1426 + ], 1427 + "dev": true, 1428 + "license": "MIT", 1429 + "optional": true, 1430 + "os": [ 1431 + "linux" 1432 + ] 1433 + }, 1434 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 1435 + "version": "4.55.1", 1436 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", 1437 + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", 1438 + "cpu": [ 1439 + "ppc64" 1440 + ], 1441 + "dev": true, 1442 + "license": "MIT", 1443 + "optional": true, 1444 + "os": [ 1445 + "linux" 1446 + ] 1447 + }, 1448 + "node_modules/@rollup/rollup-linux-ppc64-musl": { 1449 + "version": "4.55.1", 1450 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", 1451 + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", 1452 + "cpu": [ 1453 + "ppc64" 1454 + ], 1455 + "dev": true, 1456 + "license": "MIT", 1457 + "optional": true, 1458 + "os": [ 1459 + "linux" 1460 + ] 1461 + }, 1462 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1463 + "version": "4.55.1", 1464 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", 1465 + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", 1466 + "cpu": [ 1467 + "riscv64" 1468 + ], 1469 + "dev": true, 1470 + "license": "MIT", 1471 + "optional": true, 1472 + "os": [ 1473 + "linux" 1474 + ] 1475 + }, 1476 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1477 + "version": "4.55.1", 1478 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", 1479 + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", 1480 + "cpu": [ 1481 + "riscv64" 1482 + ], 1483 + "dev": true, 1484 + "license": "MIT", 1485 + "optional": true, 1486 + "os": [ 1487 + "linux" 1488 + ] 1489 + }, 1490 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1491 + "version": "4.55.1", 1492 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", 1493 + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", 1494 + "cpu": [ 1495 + "s390x" 1496 + ], 1497 + "dev": true, 1498 + "license": "MIT", 1499 + "optional": true, 1500 + "os": [ 1501 + "linux" 1502 + ] 1503 + }, 1504 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1505 + "version": "4.55.1", 1506 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", 1507 + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", 1508 + "cpu": [ 1509 + "x64" 1510 + ], 1511 + "dev": true, 1512 + "license": "MIT", 1513 + "optional": true, 1514 + "os": [ 1515 + "linux" 1516 + ] 1517 + }, 1518 + "node_modules/@rollup/rollup-linux-x64-musl": { 1519 + "version": "4.55.1", 1520 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", 1521 + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", 1522 + "cpu": [ 1523 + "x64" 1524 + ], 1525 + "dev": true, 1526 + "license": "MIT", 1527 + "optional": true, 1528 + "os": [ 1529 + "linux" 1530 + ] 1531 + }, 1532 + "node_modules/@rollup/rollup-openbsd-x64": { 1533 + "version": "4.55.1", 1534 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", 1535 + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", 1536 + "cpu": [ 1537 + "x64" 1538 + ], 1539 + "dev": true, 1540 + "license": "MIT", 1541 + "optional": true, 1542 + "os": [ 1543 + "openbsd" 1544 + ] 1545 + }, 1546 + "node_modules/@rollup/rollup-openharmony-arm64": { 1547 + "version": "4.55.1", 1548 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", 1549 + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", 1550 + "cpu": [ 1551 + "arm64" 1552 + ], 1553 + "dev": true, 1554 + "license": "MIT", 1555 + "optional": true, 1556 + "os": [ 1557 + "openharmony" 1558 + ] 1559 + }, 1560 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1561 + "version": "4.55.1", 1562 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", 1563 + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", 1564 + "cpu": [ 1565 + "arm64" 1566 + ], 1567 + "dev": true, 1568 + "license": "MIT", 1569 + "optional": true, 1570 + "os": [ 1571 + "win32" 1572 + ] 1573 + }, 1574 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1575 + "version": "4.55.1", 1576 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", 1577 + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", 1578 + "cpu": [ 1579 + "ia32" 1580 + ], 1581 + "dev": true, 1582 + "license": "MIT", 1583 + "optional": true, 1584 + "os": [ 1585 + "win32" 1586 + ] 1587 + }, 1588 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1589 + "version": "4.55.1", 1590 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", 1591 + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", 1592 + "cpu": [ 1593 + "x64" 1594 + ], 1595 + "dev": true, 1596 + "license": "MIT", 1597 + "optional": true, 1598 + "os": [ 1599 + "win32" 1600 + ] 1601 + }, 1602 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1603 + "version": "4.55.1", 1604 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", 1605 + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", 1606 + "cpu": [ 1607 + "x64" 1608 + ], 1609 + "dev": true, 1610 + "license": "MIT", 1611 + "optional": true, 1612 + "os": [ 1613 + "win32" 1614 + ] 1615 + }, 1204 1616 "node_modules/@sindresorhus/is": { 1205 1617 "version": "7.2.0", 1206 1618 "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", ··· 1221 1633 "dev": true, 1222 1634 "license": "CC0-1.0" 1223 1635 }, 1636 + "node_modules/@standard-schema/spec": { 1637 + "version": "1.1.0", 1638 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", 1639 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", 1640 + "dev": true, 1641 + "license": "MIT" 1642 + }, 1643 + "node_modules/@types/chai": { 1644 + "version": "5.2.3", 1645 + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", 1646 + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", 1647 + "dev": true, 1648 + "license": "MIT", 1649 + "dependencies": { 1650 + "@types/deep-eql": "*", 1651 + "assertion-error": "^2.0.1" 1652 + } 1653 + }, 1654 + "node_modules/@types/deep-eql": { 1655 + "version": "4.0.2", 1656 + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", 1657 + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", 1658 + "dev": true, 1659 + "license": "MIT" 1660 + }, 1661 + "node_modules/@types/estree": { 1662 + "version": "1.0.8", 1663 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1664 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1665 + "dev": true, 1666 + "license": "MIT" 1667 + }, 1668 + "node_modules/@vitest/coverage-v8": { 1669 + "version": "4.0.16", 1670 + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz", 1671 + "integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==", 1672 + "dev": true, 1673 + "license": "MIT", 1674 + "dependencies": { 1675 + "@bcoe/v8-coverage": "^1.0.2", 1676 + "@vitest/utils": "4.0.16", 1677 + "ast-v8-to-istanbul": "^0.3.8", 1678 + "istanbul-lib-coverage": "^3.2.2", 1679 + "istanbul-lib-report": "^3.0.1", 1680 + "istanbul-lib-source-maps": "^5.0.6", 1681 + "istanbul-reports": "^3.2.0", 1682 + "magicast": "^0.5.1", 1683 + "obug": "^2.1.1", 1684 + "std-env": "^3.10.0", 1685 + "tinyrainbow": "^3.0.3" 1686 + }, 1687 + "funding": { 1688 + "url": "https://opencollective.com/vitest" 1689 + }, 1690 + "peerDependencies": { 1691 + "@vitest/browser": "4.0.16", 1692 + "vitest": "4.0.16" 1693 + }, 1694 + "peerDependenciesMeta": { 1695 + "@vitest/browser": { 1696 + "optional": true 1697 + } 1698 + } 1699 + }, 1700 + "node_modules/@vitest/expect": { 1701 + "version": "4.0.16", 1702 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", 1703 + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", 1704 + "dev": true, 1705 + "license": "MIT", 1706 + "dependencies": { 1707 + "@standard-schema/spec": "^1.0.0", 1708 + "@types/chai": "^5.2.2", 1709 + "@vitest/spy": "4.0.16", 1710 + "@vitest/utils": "4.0.16", 1711 + "chai": "^6.2.1", 1712 + "tinyrainbow": "^3.0.3" 1713 + }, 1714 + "funding": { 1715 + "url": "https://opencollective.com/vitest" 1716 + } 1717 + }, 1718 + "node_modules/@vitest/mocker": { 1719 + "version": "4.0.16", 1720 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", 1721 + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", 1722 + "dev": true, 1723 + "license": "MIT", 1724 + "dependencies": { 1725 + "@vitest/spy": "4.0.16", 1726 + "estree-walker": "^3.0.3", 1727 + "magic-string": "^0.30.21" 1728 + }, 1729 + "funding": { 1730 + "url": "https://opencollective.com/vitest" 1731 + }, 1732 + "peerDependencies": { 1733 + "msw": "^2.4.9", 1734 + "vite": "^6.0.0 || ^7.0.0-0" 1735 + }, 1736 + "peerDependenciesMeta": { 1737 + "msw": { 1738 + "optional": true 1739 + }, 1740 + "vite": { 1741 + "optional": true 1742 + } 1743 + } 1744 + }, 1745 + "node_modules/@vitest/pretty-format": { 1746 + "version": "4.0.16", 1747 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", 1748 + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", 1749 + "dev": true, 1750 + "license": "MIT", 1751 + "dependencies": { 1752 + "tinyrainbow": "^3.0.3" 1753 + }, 1754 + "funding": { 1755 + "url": "https://opencollective.com/vitest" 1756 + } 1757 + }, 1758 + "node_modules/@vitest/runner": { 1759 + "version": "4.0.16", 1760 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", 1761 + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", 1762 + "dev": true, 1763 + "license": "MIT", 1764 + "dependencies": { 1765 + "@vitest/utils": "4.0.16", 1766 + "pathe": "^2.0.3" 1767 + }, 1768 + "funding": { 1769 + "url": "https://opencollective.com/vitest" 1770 + } 1771 + }, 1772 + "node_modules/@vitest/snapshot": { 1773 + "version": "4.0.16", 1774 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", 1775 + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", 1776 + "dev": true, 1777 + "license": "MIT", 1778 + "dependencies": { 1779 + "@vitest/pretty-format": "4.0.16", 1780 + "magic-string": "^0.30.21", 1781 + "pathe": "^2.0.3" 1782 + }, 1783 + "funding": { 1784 + "url": "https://opencollective.com/vitest" 1785 + } 1786 + }, 1787 + "node_modules/@vitest/spy": { 1788 + "version": "4.0.16", 1789 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", 1790 + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", 1791 + "dev": true, 1792 + "license": "MIT", 1793 + "funding": { 1794 + "url": "https://opencollective.com/vitest" 1795 + } 1796 + }, 1797 + "node_modules/@vitest/utils": { 1798 + "version": "4.0.16", 1799 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", 1800 + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", 1801 + "dev": true, 1802 + "license": "MIT", 1803 + "dependencies": { 1804 + "@vitest/pretty-format": "4.0.16", 1805 + "tinyrainbow": "^3.0.3" 1806 + }, 1807 + "funding": { 1808 + "url": "https://opencollective.com/vitest" 1809 + } 1810 + }, 1224 1811 "node_modules/acorn": { 1225 1812 "version": "8.14.0", 1226 1813 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", ··· 1244 1831 "node": ">=0.4.0" 1245 1832 } 1246 1833 }, 1834 + "node_modules/assertion-error": { 1835 + "version": "2.0.1", 1836 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1837 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1838 + "dev": true, 1839 + "license": "MIT", 1840 + "engines": { 1841 + "node": ">=12" 1842 + } 1843 + }, 1844 + "node_modules/ast-v8-to-istanbul": { 1845 + "version": "0.3.10", 1846 + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", 1847 + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", 1848 + "dev": true, 1849 + "license": "MIT", 1850 + "dependencies": { 1851 + "@jridgewell/trace-mapping": "^0.3.31", 1852 + "estree-walker": "^3.0.3", 1853 + "js-tokens": "^9.0.1" 1854 + } 1855 + }, 1856 + "node_modules/ast-v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { 1857 + "version": "0.3.31", 1858 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 1859 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 1860 + "dev": true, 1861 + "license": "MIT", 1862 + "dependencies": { 1863 + "@jridgewell/resolve-uri": "^3.1.0", 1864 + "@jridgewell/sourcemap-codec": "^1.4.14" 1865 + } 1866 + }, 1247 1867 "node_modules/blake3-wasm": { 1248 1868 "version": "2.1.5", 1249 1869 "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 1250 1870 "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 1251 1871 "dev": true, 1252 1872 "license": "MIT" 1873 + }, 1874 + "node_modules/chai": { 1875 + "version": "6.2.2", 1876 + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", 1877 + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", 1878 + "dev": true, 1879 + "license": "MIT", 1880 + "engines": { 1881 + "node": ">=18" 1882 + } 1253 1883 }, 1254 1884 "node_modules/color": { 1255 1885 "version": "4.2.3", ··· 1310 1940 "url": "https://opencollective.com/express" 1311 1941 } 1312 1942 }, 1943 + "node_modules/debug": { 1944 + "version": "4.4.3", 1945 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1946 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1947 + "dev": true, 1948 + "license": "MIT", 1949 + "dependencies": { 1950 + "ms": "^2.1.3" 1951 + }, 1952 + "engines": { 1953 + "node": ">=6.0" 1954 + }, 1955 + "peerDependenciesMeta": { 1956 + "supports-color": { 1957 + "optional": true 1958 + } 1959 + } 1960 + }, 1313 1961 "node_modules/detect-libc": { 1314 1962 "version": "2.1.2", 1315 1963 "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", ··· 1329 1977 "funding": { 1330 1978 "url": "https://github.com/sponsors/antfu" 1331 1979 } 1980 + }, 1981 + "node_modules/es-module-lexer": { 1982 + "version": "1.7.0", 1983 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1984 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1985 + "dev": true, 1986 + "license": "MIT" 1332 1987 }, 1333 1988 "node_modules/esbuild": { 1334 1989 "version": "0.27.0", ··· 1372 2027 "@esbuild/win32-x64": "0.27.0" 1373 2028 } 1374 2029 }, 2030 + "node_modules/estree-walker": { 2031 + "version": "3.0.3", 2032 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 2033 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 2034 + "dev": true, 2035 + "license": "MIT", 2036 + "dependencies": { 2037 + "@types/estree": "^1.0.0" 2038 + } 2039 + }, 1375 2040 "node_modules/exit-hook": { 1376 2041 "version": "2.2.1", 1377 2042 "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", ··· 1385 2050 "url": "https://github.com/sponsors/sindresorhus" 1386 2051 } 1387 2052 }, 2053 + "node_modules/expect-type": { 2054 + "version": "1.3.0", 2055 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", 2056 + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", 2057 + "dev": true, 2058 + "license": "Apache-2.0", 2059 + "engines": { 2060 + "node": ">=12.0.0" 2061 + } 2062 + }, 2063 + "node_modules/fdir": { 2064 + "version": "6.5.0", 2065 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 2066 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 2067 + "dev": true, 2068 + "license": "MIT", 2069 + "engines": { 2070 + "node": ">=12.0.0" 2071 + }, 2072 + "peerDependencies": { 2073 + "picomatch": "^3 || ^4" 2074 + }, 2075 + "peerDependenciesMeta": { 2076 + "picomatch": { 2077 + "optional": true 2078 + } 2079 + } 2080 + }, 1388 2081 "node_modules/fsevents": { 1389 2082 "version": "2.3.3", 1390 2083 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 1407 2100 "dev": true, 1408 2101 "license": "BSD-2-Clause" 1409 2102 }, 2103 + "node_modules/has-flag": { 2104 + "version": "4.0.0", 2105 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2106 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2107 + "dev": true, 2108 + "license": "MIT", 2109 + "engines": { 2110 + "node": ">=8" 2111 + } 2112 + }, 2113 + "node_modules/html-escaper": { 2114 + "version": "2.0.2", 2115 + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", 2116 + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", 2117 + "dev": true, 2118 + "license": "MIT" 2119 + }, 1410 2120 "node_modules/is-arrayish": { 1411 2121 "version": "0.3.4", 1412 2122 "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", ··· 1414 2124 "dev": true, 1415 2125 "license": "MIT" 1416 2126 }, 2127 + "node_modules/istanbul-lib-coverage": { 2128 + "version": "3.2.2", 2129 + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", 2130 + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", 2131 + "dev": true, 2132 + "license": "BSD-3-Clause", 2133 + "engines": { 2134 + "node": ">=8" 2135 + } 2136 + }, 2137 + "node_modules/istanbul-lib-report": { 2138 + "version": "3.0.1", 2139 + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", 2140 + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", 2141 + "dev": true, 2142 + "license": "BSD-3-Clause", 2143 + "dependencies": { 2144 + "istanbul-lib-coverage": "^3.0.0", 2145 + "make-dir": "^4.0.0", 2146 + "supports-color": "^7.1.0" 2147 + }, 2148 + "engines": { 2149 + "node": ">=10" 2150 + } 2151 + }, 2152 + "node_modules/istanbul-lib-report/node_modules/supports-color": { 2153 + "version": "7.2.0", 2154 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 2155 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 2156 + "dev": true, 2157 + "license": "MIT", 2158 + "dependencies": { 2159 + "has-flag": "^4.0.0" 2160 + }, 2161 + "engines": { 2162 + "node": ">=8" 2163 + } 2164 + }, 2165 + "node_modules/istanbul-lib-source-maps": { 2166 + "version": "5.0.6", 2167 + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", 2168 + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", 2169 + "dev": true, 2170 + "license": "BSD-3-Clause", 2171 + "dependencies": { 2172 + "@jridgewell/trace-mapping": "^0.3.23", 2173 + "debug": "^4.1.1", 2174 + "istanbul-lib-coverage": "^3.0.0" 2175 + }, 2176 + "engines": { 2177 + "node": ">=10" 2178 + } 2179 + }, 2180 + "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { 2181 + "version": "0.3.31", 2182 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 2183 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 2184 + "dev": true, 2185 + "license": "MIT", 2186 + "dependencies": { 2187 + "@jridgewell/resolve-uri": "^3.1.0", 2188 + "@jridgewell/sourcemap-codec": "^1.4.14" 2189 + } 2190 + }, 2191 + "node_modules/istanbul-reports": { 2192 + "version": "3.2.0", 2193 + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", 2194 + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", 2195 + "dev": true, 2196 + "license": "BSD-3-Clause", 2197 + "dependencies": { 2198 + "html-escaper": "^2.0.0", 2199 + "istanbul-lib-report": "^3.0.0" 2200 + }, 2201 + "engines": { 2202 + "node": ">=8" 2203 + } 2204 + }, 2205 + "node_modules/js-tokens": { 2206 + "version": "9.0.1", 2207 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", 2208 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", 2209 + "dev": true, 2210 + "license": "MIT" 2211 + }, 1417 2212 "node_modules/kleur": { 1418 2213 "version": "4.1.5", 1419 2214 "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", ··· 1424 2219 "node": ">=6" 1425 2220 } 1426 2221 }, 2222 + "node_modules/magic-string": { 2223 + "version": "0.30.21", 2224 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 2225 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 2226 + "dev": true, 2227 + "license": "MIT", 2228 + "dependencies": { 2229 + "@jridgewell/sourcemap-codec": "^1.5.5" 2230 + } 2231 + }, 2232 + "node_modules/magicast": { 2233 + "version": "0.5.1", 2234 + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", 2235 + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", 2236 + "dev": true, 2237 + "license": "MIT", 2238 + "dependencies": { 2239 + "@babel/parser": "^7.28.5", 2240 + "@babel/types": "^7.28.5", 2241 + "source-map-js": "^1.2.1" 2242 + } 2243 + }, 2244 + "node_modules/make-dir": { 2245 + "version": "4.0.0", 2246 + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", 2247 + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", 2248 + "dev": true, 2249 + "license": "MIT", 2250 + "dependencies": { 2251 + "semver": "^7.5.3" 2252 + }, 2253 + "engines": { 2254 + "node": ">=10" 2255 + }, 2256 + "funding": { 2257 + "url": "https://github.com/sponsors/sindresorhus" 2258 + } 2259 + }, 1427 2260 "node_modules/mime": { 1428 2261 "version": "3.0.0", 1429 2262 "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", ··· 1464 2297 "node": ">=18.0.0" 1465 2298 } 1466 2299 }, 2300 + "node_modules/ms": { 2301 + "version": "2.1.3", 2302 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 2303 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 2304 + "dev": true, 2305 + "license": "MIT" 2306 + }, 2307 + "node_modules/nanoid": { 2308 + "version": "3.3.11", 2309 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2310 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2311 + "dev": true, 2312 + "funding": [ 2313 + { 2314 + "type": "github", 2315 + "url": "https://github.com/sponsors/ai" 2316 + } 2317 + ], 2318 + "license": "MIT", 2319 + "bin": { 2320 + "nanoid": "bin/nanoid.cjs" 2321 + }, 2322 + "engines": { 2323 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2324 + } 2325 + }, 2326 + "node_modules/obug": { 2327 + "version": "2.1.1", 2328 + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", 2329 + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", 2330 + "dev": true, 2331 + "funding": [ 2332 + "https://github.com/sponsors/sxzz", 2333 + "https://opencollective.com/debug" 2334 + ], 2335 + "license": "MIT" 2336 + }, 1467 2337 "node_modules/path-to-regexp": { 1468 2338 "version": "6.3.0", 1469 2339 "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", ··· 1478 2348 "dev": true, 1479 2349 "license": "MIT" 1480 2350 }, 2351 + "node_modules/picocolors": { 2352 + "version": "1.1.1", 2353 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2354 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2355 + "dev": true, 2356 + "license": "ISC" 2357 + }, 2358 + "node_modules/picomatch": { 2359 + "version": "4.0.3", 2360 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 2361 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 2362 + "dev": true, 2363 + "license": "MIT", 2364 + "engines": { 2365 + "node": ">=12" 2366 + }, 2367 + "funding": { 2368 + "url": "https://github.com/sponsors/jonschlinkert" 2369 + } 2370 + }, 2371 + "node_modules/postcss": { 2372 + "version": "8.5.6", 2373 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 2374 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 2375 + "dev": true, 2376 + "funding": [ 2377 + { 2378 + "type": "opencollective", 2379 + "url": "https://opencollective.com/postcss/" 2380 + }, 2381 + { 2382 + "type": "tidelift", 2383 + "url": "https://tidelift.com/funding/github/npm/postcss" 2384 + }, 2385 + { 2386 + "type": "github", 2387 + "url": "https://github.com/sponsors/ai" 2388 + } 2389 + ], 2390 + "license": "MIT", 2391 + "dependencies": { 2392 + "nanoid": "^3.3.11", 2393 + "picocolors": "^1.1.1", 2394 + "source-map-js": "^1.2.1" 2395 + }, 2396 + "engines": { 2397 + "node": "^10 || ^12 || >=14" 2398 + } 2399 + }, 2400 + "node_modules/rollup": { 2401 + "version": "4.55.1", 2402 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", 2403 + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", 2404 + "dev": true, 2405 + "license": "MIT", 2406 + "dependencies": { 2407 + "@types/estree": "1.0.8" 2408 + }, 2409 + "bin": { 2410 + "rollup": "dist/bin/rollup" 2411 + }, 2412 + "engines": { 2413 + "node": ">=18.0.0", 2414 + "npm": ">=8.0.0" 2415 + }, 2416 + "optionalDependencies": { 2417 + "@rollup/rollup-android-arm-eabi": "4.55.1", 2418 + "@rollup/rollup-android-arm64": "4.55.1", 2419 + "@rollup/rollup-darwin-arm64": "4.55.1", 2420 + "@rollup/rollup-darwin-x64": "4.55.1", 2421 + "@rollup/rollup-freebsd-arm64": "4.55.1", 2422 + "@rollup/rollup-freebsd-x64": "4.55.1", 2423 + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", 2424 + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", 2425 + "@rollup/rollup-linux-arm64-gnu": "4.55.1", 2426 + "@rollup/rollup-linux-arm64-musl": "4.55.1", 2427 + "@rollup/rollup-linux-loong64-gnu": "4.55.1", 2428 + "@rollup/rollup-linux-loong64-musl": "4.55.1", 2429 + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", 2430 + "@rollup/rollup-linux-ppc64-musl": "4.55.1", 2431 + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", 2432 + "@rollup/rollup-linux-riscv64-musl": "4.55.1", 2433 + "@rollup/rollup-linux-s390x-gnu": "4.55.1", 2434 + "@rollup/rollup-linux-x64-gnu": "4.55.1", 2435 + "@rollup/rollup-linux-x64-musl": "4.55.1", 2436 + "@rollup/rollup-openbsd-x64": "4.55.1", 2437 + "@rollup/rollup-openharmony-arm64": "4.55.1", 2438 + "@rollup/rollup-win32-arm64-msvc": "4.55.1", 2439 + "@rollup/rollup-win32-ia32-msvc": "4.55.1", 2440 + "@rollup/rollup-win32-x64-gnu": "4.55.1", 2441 + "@rollup/rollup-win32-x64-msvc": "4.55.1", 2442 + "fsevents": "~2.3.2" 2443 + } 2444 + }, 1481 2445 "node_modules/semver": { 1482 2446 "version": "7.7.3", 1483 2447 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", ··· 1531 2495 "@img/sharp-win32-x64": "0.33.5" 1532 2496 } 1533 2497 }, 2498 + "node_modules/siginfo": { 2499 + "version": "2.0.0", 2500 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2501 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2502 + "dev": true, 2503 + "license": "ISC" 2504 + }, 1534 2505 "node_modules/simple-swizzle": { 1535 2506 "version": "0.2.4", 1536 2507 "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", ··· 1541 2512 "is-arrayish": "^0.3.1" 1542 2513 } 1543 2514 }, 2515 + "node_modules/source-map-js": { 2516 + "version": "1.2.1", 2517 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2518 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2519 + "dev": true, 2520 + "license": "BSD-3-Clause", 2521 + "engines": { 2522 + "node": ">=0.10.0" 2523 + } 2524 + }, 2525 + "node_modules/stackback": { 2526 + "version": "0.0.2", 2527 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2528 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2529 + "dev": true, 2530 + "license": "MIT" 2531 + }, 2532 + "node_modules/std-env": { 2533 + "version": "3.10.0", 2534 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", 2535 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", 2536 + "dev": true, 2537 + "license": "MIT" 2538 + }, 1544 2539 "node_modules/stoppable": { 1545 2540 "version": "1.1.0", 1546 2541 "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", ··· 1565 2560 "url": "https://github.com/chalk/supports-color?sponsor=1" 1566 2561 } 1567 2562 }, 2563 + "node_modules/tinybench": { 2564 + "version": "2.9.0", 2565 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2566 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2567 + "dev": true, 2568 + "license": "MIT" 2569 + }, 2570 + "node_modules/tinyexec": { 2571 + "version": "1.0.2", 2572 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", 2573 + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", 2574 + "dev": true, 2575 + "license": "MIT", 2576 + "engines": { 2577 + "node": ">=18" 2578 + } 2579 + }, 2580 + "node_modules/tinyglobby": { 2581 + "version": "0.2.15", 2582 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 2583 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 2584 + "dev": true, 2585 + "license": "MIT", 2586 + "dependencies": { 2587 + "fdir": "^6.5.0", 2588 + "picomatch": "^4.0.3" 2589 + }, 2590 + "engines": { 2591 + "node": ">=12.0.0" 2592 + }, 2593 + "funding": { 2594 + "url": "https://github.com/sponsors/SuperchupuDev" 2595 + } 2596 + }, 2597 + "node_modules/tinyrainbow": { 2598 + "version": "3.0.3", 2599 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", 2600 + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", 2601 + "dev": true, 2602 + "license": "MIT", 2603 + "engines": { 2604 + "node": ">=14.0.0" 2605 + } 2606 + }, 1568 2607 "node_modules/tslib": { 1569 2608 "version": "2.8.1", 1570 2609 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", ··· 1605 2644 "license": "MIT", 1606 2645 "dependencies": { 1607 2646 "pathe": "^2.0.3" 2647 + } 2648 + }, 2649 + "node_modules/vite": { 2650 + "version": "7.3.1", 2651 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", 2652 + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", 2653 + "dev": true, 2654 + "license": "MIT", 2655 + "dependencies": { 2656 + "esbuild": "^0.27.0", 2657 + "fdir": "^6.5.0", 2658 + "picomatch": "^4.0.3", 2659 + "postcss": "^8.5.6", 2660 + "rollup": "^4.43.0", 2661 + "tinyglobby": "^0.2.15" 2662 + }, 2663 + "bin": { 2664 + "vite": "bin/vite.js" 2665 + }, 2666 + "engines": { 2667 + "node": "^20.19.0 || >=22.12.0" 2668 + }, 2669 + "funding": { 2670 + "url": "https://github.com/vitejs/vite?sponsor=1" 2671 + }, 2672 + "optionalDependencies": { 2673 + "fsevents": "~2.3.3" 2674 + }, 2675 + "peerDependencies": { 2676 + "@types/node": "^20.19.0 || >=22.12.0", 2677 + "jiti": ">=1.21.0", 2678 + "less": "^4.0.0", 2679 + "lightningcss": "^1.21.0", 2680 + "sass": "^1.70.0", 2681 + "sass-embedded": "^1.70.0", 2682 + "stylus": ">=0.54.8", 2683 + "sugarss": "^5.0.0", 2684 + "terser": "^5.16.0", 2685 + "tsx": "^4.8.1", 2686 + "yaml": "^2.4.2" 2687 + }, 2688 + "peerDependenciesMeta": { 2689 + "@types/node": { 2690 + "optional": true 2691 + }, 2692 + "jiti": { 2693 + "optional": true 2694 + }, 2695 + "less": { 2696 + "optional": true 2697 + }, 2698 + "lightningcss": { 2699 + "optional": true 2700 + }, 2701 + "sass": { 2702 + "optional": true 2703 + }, 2704 + "sass-embedded": { 2705 + "optional": true 2706 + }, 2707 + "stylus": { 2708 + "optional": true 2709 + }, 2710 + "sugarss": { 2711 + "optional": true 2712 + }, 2713 + "terser": { 2714 + "optional": true 2715 + }, 2716 + "tsx": { 2717 + "optional": true 2718 + }, 2719 + "yaml": { 2720 + "optional": true 2721 + } 2722 + } 2723 + }, 2724 + "node_modules/vitest": { 2725 + "version": "4.0.16", 2726 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", 2727 + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", 2728 + "dev": true, 2729 + "license": "MIT", 2730 + "dependencies": { 2731 + "@vitest/expect": "4.0.16", 2732 + "@vitest/mocker": "4.0.16", 2733 + "@vitest/pretty-format": "4.0.16", 2734 + "@vitest/runner": "4.0.16", 2735 + "@vitest/snapshot": "4.0.16", 2736 + "@vitest/spy": "4.0.16", 2737 + "@vitest/utils": "4.0.16", 2738 + "es-module-lexer": "^1.7.0", 2739 + "expect-type": "^1.2.2", 2740 + "magic-string": "^0.30.21", 2741 + "obug": "^2.1.1", 2742 + "pathe": "^2.0.3", 2743 + "picomatch": "^4.0.3", 2744 + "std-env": "^3.10.0", 2745 + "tinybench": "^2.9.0", 2746 + "tinyexec": "^1.0.2", 2747 + "tinyglobby": "^0.2.15", 2748 + "tinyrainbow": "^3.0.3", 2749 + "vite": "^6.0.0 || ^7.0.0", 2750 + "why-is-node-running": "^2.3.0" 2751 + }, 2752 + "bin": { 2753 + "vitest": "vitest.mjs" 2754 + }, 2755 + "engines": { 2756 + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" 2757 + }, 2758 + "funding": { 2759 + "url": "https://opencollective.com/vitest" 2760 + }, 2761 + "peerDependencies": { 2762 + "@edge-runtime/vm": "*", 2763 + "@opentelemetry/api": "^1.9.0", 2764 + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", 2765 + "@vitest/browser-playwright": "4.0.16", 2766 + "@vitest/browser-preview": "4.0.16", 2767 + "@vitest/browser-webdriverio": "4.0.16", 2768 + "@vitest/ui": "4.0.16", 2769 + "happy-dom": "*", 2770 + "jsdom": "*" 2771 + }, 2772 + "peerDependenciesMeta": { 2773 + "@edge-runtime/vm": { 2774 + "optional": true 2775 + }, 2776 + "@opentelemetry/api": { 2777 + "optional": true 2778 + }, 2779 + "@types/node": { 2780 + "optional": true 2781 + }, 2782 + "@vitest/browser-playwright": { 2783 + "optional": true 2784 + }, 2785 + "@vitest/browser-preview": { 2786 + "optional": true 2787 + }, 2788 + "@vitest/browser-webdriverio": { 2789 + "optional": true 2790 + }, 2791 + "@vitest/ui": { 2792 + "optional": true 2793 + }, 2794 + "happy-dom": { 2795 + "optional": true 2796 + }, 2797 + "jsdom": { 2798 + "optional": true 2799 + } 2800 + } 2801 + }, 2802 + "node_modules/why-is-node-running": { 2803 + "version": "2.3.0", 2804 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 2805 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 2806 + "dev": true, 2807 + "license": "MIT", 2808 + "dependencies": { 2809 + "siginfo": "^2.0.0", 2810 + "stackback": "0.0.2" 2811 + }, 2812 + "bin": { 2813 + "why-is-node-running": "cli.js" 2814 + }, 2815 + "engines": { 2816 + "node": ">=8" 1608 2817 } 1609 2818 }, 1610 2819 "node_modules/workerd": {
+5 -1
package.json
··· 7 7 "dev": "wrangler dev --persist-to .wrangler/state", 8 8 "dev:remote": "wrangler dev --remote", 9 9 "deploy": "wrangler deploy", 10 - "test": "node --test test/pds.test.js", 10 + "test": "vitest run", 11 + "test:watch": "vitest", 12 + "test:coverage": "vitest run --coverage", 11 13 "test:e2e": "node --test test/e2e.test.js", 12 14 "setup": "node scripts/setup.js", 13 15 "format": "biome format --write .", ··· 18 20 "devDependencies": { 19 21 "@biomejs/biome": "^2.3.11", 20 22 "@cloudflare/workers-types": "^4.20260103.0", 23 + "@vitest/coverage-v8": "^4.0.16", 21 24 "typescript": "^5.9.3", 25 + "vitest": "^4.0.16", 22 26 "wrangler": "^4.54.0" 23 27 } 24 28 }
+179 -258
test/e2e.test.js
··· 3 3 * Uses Node's built-in test runner and fetch 4 4 */ 5 5 6 - import assert from 'node:assert'; 7 6 import { spawn } from 'node:child_process'; 8 7 import { randomBytes } from 'node:crypto'; 9 - import { after, before, describe, it } from 'node:test'; 8 + import { afterAll, beforeAll, describe, it, expect } from 'vitest'; 10 9 import { DpopClient } from './helpers/dpop.js'; 11 10 import { getOAuthTokenWithScope } from './helpers/oauth.js'; 12 11 ··· 88 87 } 89 88 90 89 describe('E2E Tests', () => { 91 - before(async () => { 90 + beforeAll(async () => { 92 91 // Start wrangler 93 92 wrangler = spawn( 94 93 'npx', ··· 112 111 handle: 'test.local', 113 112 }), 114 113 }); 115 - assert.ok(res.ok, 'PDS initialization failed'); 114 + expect(res.ok).toBeTruthy(); 116 115 }); 117 116 118 - after(() => { 117 + afterAll(() => { 119 118 if (wrangler) { 120 119 wrangler.kill(); 121 120 } ··· 125 124 it('root returns ASCII art', async () => { 126 125 const res = await fetch(`${BASE}/`); 127 126 const text = await res.text(); 128 - assert.ok(text.includes('PDS'), 'Root should contain PDS'); 127 + expect(text.includes('PDS')).toBeTruthy(); // Root should contain PDS; 129 128 }); 130 129 131 130 it('describeServer returns DID', async () => { 132 131 const res = await fetch(`${BASE}/xrpc/com.atproto.server.describeServer`); 133 132 const data = await res.json(); 134 - assert.ok(data.did, 'describeServer should return did'); 133 + expect(data.did).toBeTruthy(); 135 134 }); 136 135 137 136 it('resolveHandle returns DID', async () => { ··· 139 138 `${BASE}/xrpc/com.atproto.identity.resolveHandle?handle=test.local`, 140 139 ); 141 140 const data = await res.json(); 142 - assert.ok(data.did, 'resolveHandle should return did'); 141 + expect(data.did).toBeTruthy(); 143 142 }); 144 143 }); 145 144 ··· 152 151 password: PASSWORD, 153 152 }, 154 153 ); 155 - assert.strictEqual(status, 200); 156 - assert.ok(data.accessJwt, 'Should return accessJwt'); 157 - assert.ok(data.refreshJwt, 'Should return refreshJwt'); 154 + expect(status).toBe(200); 155 + expect(data.accessJwt).toBeTruthy(); 156 + expect(data.refreshJwt).toBeTruthy(); 158 157 token = data.accessJwt; 159 158 refreshToken = data.refreshJwt; 160 159 }); ··· 164 163 headers: { Authorization: `Bearer ${token}` }, 165 164 }); 166 165 const data = await res.json(); 167 - assert.ok(data.did, 'getSession should return did'); 166 + expect(data.did).toBeTruthy(); 168 167 }); 169 168 170 169 it('refreshSession returns new tokens', async () => { ··· 176 175 }, 177 176 ); 178 177 const data = await res.json(); 179 - assert.ok(data.accessJwt, 'Should return new accessJwt'); 180 - assert.ok(data.refreshJwt, 'Should return new refreshJwt'); 178 + expect(data.accessJwt).toBeTruthy(); 179 + expect(data.refreshJwt).toBeTruthy(); 181 180 token = data.accessJwt; // Use new token 182 181 }); 183 182 ··· 189 188 headers: { Authorization: `Bearer ${token}` }, 190 189 }, 191 190 ); 192 - assert.strictEqual(res.status, 400); 191 + expect(res.status).toBe(400); 193 192 }); 194 193 195 194 it('refreshSession rejects missing auth', async () => { ··· 199 198 method: 'POST', 200 199 }, 201 200 ); 202 - assert.strictEqual(res.status, 401); 201 + expect(res.status).toBe(401); 203 202 }); 204 203 205 204 it('createRecord rejects without auth', async () => { ··· 208 207 collection: 'x', 209 208 record: {}, 210 209 }); 211 - assert.strictEqual(status, 401); 210 + expect(status).toBe(401); 212 211 }); 213 212 214 213 it('getPreferences works', async () => { ··· 216 215 headers: { Authorization: `Bearer ${token}` }, 217 216 }); 218 217 const data = await res.json(); 219 - assert.ok(data.preferences, 'Should return preferences'); 218 + expect(data.preferences).toBeTruthy(); 220 219 }); 221 220 222 221 it('putPreferences works', async () => { ··· 225 224 { preferences: [{ $type: 'app.bsky.actor.defs#savedFeedsPrefV2' }] }, 226 225 { Authorization: `Bearer ${token}` }, 227 226 ); 228 - assert.strictEqual(status, 200); 227 + expect(status).toBe(200); 229 228 }); 230 229 }); 231 230 ··· 240 239 }, 241 240 { Authorization: `Bearer ${token}` }, 242 241 ); 243 - assert.strictEqual(status, 200); 244 - assert.ok(data.uri, 'Should return uri'); 242 + expect(status).toBe(200); 243 + expect(data.uri).toBeTruthy(); 245 244 testRkey = data.uri.split('/').pop(); 246 245 }); 247 246 ··· 250 249 `${BASE}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=app.bsky.feed.post&rkey=${testRkey}`, 251 250 ); 252 251 const data = await res.json(); 253 - assert.ok(data.value?.text, 'Should return record value'); 252 + expect(data.value?.text).toBeTruthy(); 254 253 }); 255 254 256 255 it('putRecord updates record', async () => { ··· 264 263 }, 265 264 { Authorization: `Bearer ${token}` }, 266 265 ); 267 - assert.strictEqual(status, 200); 268 - assert.ok(data.uri); 266 + expect(status).toBe(200); 267 + expect(data.uri).toBeTruthy(); 269 268 }); 270 269 271 270 it('listRecords returns records', async () => { ··· 273 272 `${BASE}/xrpc/com.atproto.repo.listRecords?repo=${DID}&collection=app.bsky.feed.post`, 274 273 ); 275 274 const data = await res.json(); 276 - assert.ok(data.records?.length > 0, 'Should return records'); 275 + expect(data.records?.length > 0).toBeTruthy(); 277 276 }); 278 277 279 278 it('describeRepo returns did', async () => { ··· 281 280 `${BASE}/xrpc/com.atproto.repo.describeRepo?repo=${DID}`, 282 281 ); 283 282 const data = await res.json(); 284 - assert.ok(data.did); 283 + expect(data.did).toBeTruthy(); 285 284 }); 286 285 287 286 it('applyWrites create', async () => { ··· 300 299 }, 301 300 { Authorization: `Bearer ${token}` }, 302 301 ); 303 - assert.strictEqual(status, 200); 304 - assert.ok(data.results); 302 + expect(status).toBe(200); 303 + expect(data.results).toBeTruthy(); 305 304 }); 306 305 307 306 it('applyWrites delete', async () => { ··· 319 318 }, 320 319 { Authorization: `Bearer ${token}` }, 321 320 ); 322 - assert.strictEqual(status, 200); 323 - assert.ok(data.results); 321 + expect(status).toBe(200); 322 + expect(data.results).toBeTruthy(); 324 323 }); 325 324 }); 326 325 ··· 330 329 `${BASE}/xrpc/com.atproto.sync.getLatestCommit?did=${DID}`, 331 330 ); 332 331 const data = await res.json(); 333 - assert.ok(data.cid); 332 + expect(data.cid).toBeTruthy(); 334 333 }); 335 334 336 335 it('getRepoStatus returns did', async () => { ··· 338 337 `${BASE}/xrpc/com.atproto.sync.getRepoStatus?did=${DID}`, 339 338 ); 340 339 const data = await res.json(); 341 - assert.ok(data.did); 340 + expect(data.did).toBeTruthy(); 342 341 }); 343 342 344 343 it('getRepo returns CAR', async () => { ··· 346 345 `${BASE}/xrpc/com.atproto.sync.getRepo?did=${DID}`, 347 346 ); 348 347 const data = await res.arrayBuffer(); 349 - assert.ok(data.byteLength > 100, 'Should return CAR data'); 348 + expect(data.byteLength > 100).toBeTruthy(); 350 349 }); 351 350 352 351 it('getRecord returns record CAR', async () => { ··· 354 353 `${BASE}/xrpc/com.atproto.sync.getRecord?did=${DID}&collection=app.bsky.feed.post&rkey=${testRkey}`, 355 354 ); 356 355 const data = await res.arrayBuffer(); 357 - assert.ok(data.byteLength > 50); 356 + expect(data.byteLength > 50).toBeTruthy(); 358 357 }); 359 358 360 359 it('listRepos returns repos', async () => { 361 360 const res = await fetch(`${BASE}/xrpc/com.atproto.sync.listRepos`); 362 361 const data = await res.json(); 363 - assert.ok(data.repos?.length > 0); 362 + expect(data.repos?.length > 0).toBeTruthy(); 364 363 }); 365 364 }); 366 365 ··· 373 372 password: 'wrong-password', 374 373 }, 375 374 ); 376 - assert.strictEqual(status, 401); 375 + expect(status).toBe(401); 377 376 }); 378 377 379 378 it('wrong repo rejected (403)', async () => { ··· 386 385 }, 387 386 { Authorization: `Bearer ${token}` }, 388 387 ); 389 - assert.strictEqual(status, 403); 388 + expect(status).toBe(403); 390 389 }); 391 390 392 391 it('non-existent record errors', async () => { 393 392 const res = await fetch( 394 393 `${BASE}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=app.bsky.feed.post&rkey=nonexistent`, 395 394 ); 396 - assert.ok([400, 404].includes(res.status)); 395 + expect([400, 404].includes(res.status)).toBeTruthy(); 397 396 }); 398 397 }); 399 398 ··· 419 418 headers: { 'Content-Type': 'image/png' }, 420 419 body: pngBytes, 421 420 }); 422 - assert.strictEqual(res.status, 401); 421 + expect(res.status).toBe(401); 423 422 }); 424 423 425 424 it('uploadBlob returns CID', async () => { ··· 432 431 body: pngBytes, 433 432 }); 434 433 const data = await res.json(); 435 - assert.ok(data.blob?.ref?.$link); 436 - assert.strictEqual(data.blob?.mimeType, 'image/png'); 434 + expect(data.blob?.ref?.$link).toBeTruthy(); 435 + expect(data.blob?.mimeType).toBe('image/png'); 437 436 blobCid = data.blob.ref.$link; 438 437 }); 439 438 ··· 442 441 `${BASE}/xrpc/com.atproto.sync.listBlobs?did=${DID}`, 443 442 ); 444 443 const data = await res.json(); 445 - assert.ok(data.cids?.includes(blobCid)); 444 + expect(data.cids?.includes(blobCid)).toBeTruthy();; 446 445 }); 447 446 448 447 it('getBlob retrieves data', async () => { 449 448 const res = await fetch( 450 449 `${BASE}/xrpc/com.atproto.sync.getBlob?did=${DID}&cid=${blobCid}`, 451 450 ); 452 - assert.ok(res.ok); 453 - assert.strictEqual(res.headers.get('content-type'), 'image/png'); 454 - assert.strictEqual(res.headers.get('x-content-type-options'), 'nosniff'); 451 + expect(res.ok).toBeTruthy(); 452 + expect(res.headers.get('content-type')).toBe('image/png'); 453 + expect(res.headers.get('x-content-type-options')).toBe('nosniff'); 455 454 }); 456 455 457 456 it('getBlob rejects wrong DID', async () => { 458 457 const res = await fetch( 459 458 `${BASE}/xrpc/com.atproto.sync.getBlob?did=did:plc:wrongdid&cid=${blobCid}`, 460 459 ); 461 - assert.strictEqual(res.status, 400); 460 + expect(res.status).toBe(400); 462 461 }); 463 462 464 463 it('getBlob rejects invalid CID', async () => { 465 464 const res = await fetch( 466 465 `${BASE}/xrpc/com.atproto.sync.getBlob?did=${DID}&cid=invalid`, 467 466 ); 468 - assert.strictEqual(res.status, 400); 467 + expect(res.status).toBe(400); 469 468 }); 470 469 471 470 it('getBlob 404 for missing blob', async () => { 472 471 const res = await fetch( 473 472 `${BASE}/xrpc/com.atproto.sync.getBlob?did=${DID}&cid=bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`, 474 473 ); 475 - assert.strictEqual(res.status, 404); 474 + expect(res.status).toBe(404); 476 475 }); 477 476 478 477 it('createRecord with blob ref', async () => { ··· 502 501 }, 503 502 { Authorization: `Bearer ${token}` }, 504 503 ); 505 - assert.strictEqual(status, 200); 504 + expect(status).toBe(200); 506 505 blobPostRkey = data.uri.split('/').pop(); 507 506 }); 508 507 ··· 511 510 `${BASE}/xrpc/com.atproto.sync.listBlobs?did=${DID}`, 512 511 ); 513 512 const data = await res.json(); 514 - assert.ok(data.cids?.includes(blobCid)); 513 + expect(data.cids?.includes(blobCid)).toBeTruthy();; 515 514 }); 516 515 517 516 it('deleteRecord with blob cleans up', async () => { ··· 520 519 { repo: DID, collection: 'app.bsky.feed.post', rkey: blobPostRkey }, 521 520 { Authorization: `Bearer ${token}` }, 522 521 ); 523 - assert.strictEqual(status, 200); 522 + expect(status).toBe(200); 524 523 525 524 const res = await fetch( 526 525 `${BASE}/xrpc/com.atproto.sync.listBlobs?did=${DID}`, 527 526 ); 528 527 const data = await res.json(); 529 - assert.strictEqual( 530 - data.cids?.length, 531 - 0, 532 - 'Orphaned blob should be cleaned up', 533 - ); 528 + expect( 529 + data.cids?.length).toBe(0); 534 530 }); 535 531 }); 536 532 ··· 538 534 it('AS metadata', async () => { 539 535 const res = await fetch(`${BASE}/.well-known/oauth-authorization-server`); 540 536 const data = await res.json(); 541 - assert.strictEqual(data.issuer, BASE); 542 - assert.strictEqual( 543 - data.authorization_endpoint, 544 - `${BASE}/oauth/authorize`, 545 - ); 546 - assert.strictEqual(data.token_endpoint, `${BASE}/oauth/token`); 547 - assert.strictEqual( 548 - data.pushed_authorization_request_endpoint, 549 - `${BASE}/oauth/par`, 550 - ); 551 - assert.strictEqual(data.revocation_endpoint, `${BASE}/oauth/revoke`); 552 - assert.strictEqual(data.jwks_uri, `${BASE}/oauth/jwks`); 553 - assert.deepStrictEqual(data.scopes_supported, ['atproto']); 554 - assert.deepStrictEqual(data.dpop_signing_alg_values_supported, ['ES256']); 555 - assert.strictEqual(data.require_pushed_authorization_requests, false); 556 - assert.strictEqual(data.client_id_metadata_document_supported, true); 557 - assert.deepStrictEqual(data.protected_resources, [BASE]); 537 + expect(data.issuer).toBe(BASE); 538 + expect(data.authorization_endpoint).toBe(`${BASE}/oauth/authorize`); 539 + expect(data.token_endpoint).toBe(`${BASE}/oauth/token`); 540 + expect(data.pushed_authorization_request_endpoint).toBe(`${BASE}/oauth/par`); 541 + expect(data.revocation_endpoint).toBe(`${BASE}/oauth/revoke`); 542 + expect(data.jwks_uri).toBe(`${BASE}/oauth/jwks`); 543 + expect(data.scopes_supported).toEqual(['atproto']); 544 + expect(data.dpop_signing_alg_values_supported).toEqual(['ES256']); 545 + expect(data.require_pushed_authorization_requests).toBe(false); 546 + expect(data.client_id_metadata_document_supported).toBe(true); 547 + expect(data.protected_resources).toEqual([BASE]); 558 548 }); 559 549 560 550 it('PR metadata', async () => { 561 551 const res = await fetch(`${BASE}/.well-known/oauth-protected-resource`); 562 552 const data = await res.json(); 563 - assert.strictEqual(data.resource, BASE); 564 - assert.deepStrictEqual(data.authorization_servers, [BASE]); 553 + expect(data.resource).toBe(BASE); 554 + expect(data.authorization_servers).toEqual([BASE]); 565 555 }); 566 556 567 557 it('JWKS endpoint', async () => { 568 558 const res = await fetch(`${BASE}/oauth/jwks`); 569 559 const data = await res.json(); 570 - assert.ok(data.keys?.length > 0); 560 + expect(data.keys?.length > 0).toBeTruthy(); 571 561 const key = data.keys[0]; 572 - assert.strictEqual(key.kty, 'EC'); 573 - assert.strictEqual(key.crv, 'P-256'); 574 - assert.strictEqual(key.alg, 'ES256'); 575 - assert.strictEqual(key.use, 'sig'); 576 - assert.ok(key.x && key.y, 'Should have x,y coords'); 577 - assert.ok(!key.d, 'Should not expose private key'); 562 + expect(key.kty).toBe('EC'); 563 + expect(key.crv).toBe('P-256'); 564 + expect(key.alg).toBe('ES256'); 565 + expect(key.use).toBe('sig'); 566 + expect(key.x && key.y).toBeTruthy(); 567 + expect(!key.d).toBeTruthy(); 578 568 }); 579 569 580 570 it('PAR rejects missing DPoP', async () => { ··· 586 576 code_challenge: 'test', 587 577 code_challenge_method: 'S256', 588 578 }); 589 - assert.strictEqual(status, 400); 590 - assert.strictEqual(data.error, 'invalid_dpop_proof'); 579 + expect(status).toBe(400); 580 + expect(data.error).toBe('invalid_dpop_proof'); 591 581 }); 592 582 593 583 it('token rejects missing DPoP', async () => { ··· 596 586 code: 'fake', 597 587 client_id: 'http://localhost:3000', 598 588 }); 599 - assert.strictEqual(status, 400); 600 - assert.strictEqual(data.error, 'invalid_dpop_proof'); 589 + expect(status).toBe(400); 590 + expect(data.error).toBe('invalid_dpop_proof'); 601 591 }); 602 592 603 593 it('revoke returns 200 for invalid token', async () => { ··· 605 595 token: 'nonexistent', 606 596 client_id: 'http://localhost:3000', 607 597 }); 608 - assert.strictEqual(status, 200); 598 + expect(status).toBe(200); 609 599 }); 610 600 }); 611 601 ··· 643 633 }).toString(), 644 634 }); 645 635 646 - assert.strictEqual(parRes.status, 200, 'PAR should succeed'); 636 + expect(parRes.status).toBe(200); 647 637 const parData = await parRes.json(); 648 - assert.ok(parData.request_uri, 'PAR should return request_uri'); 649 - assert.ok(parData.expires_in > 0, 'PAR should return expires_in'); 638 + expect(parData.request_uri).toBeTruthy(); 639 + expect(parData.expires_in > 0).toBeTruthy(); 650 640 651 641 // Step 2: Authorization (simulate user consent by POSTing to authorize) 652 642 const authRes = await fetch(`${BASE}/oauth/authorize`, { ··· 660 650 redirect: 'manual', 661 651 }); 662 652 663 - assert.strictEqual(authRes.status, 302, 'Authorize should redirect'); 653 + expect(authRes.status).toBe(302); 664 654 const location = authRes.headers.get('location'); 665 - assert.ok(location, 'Should have Location header'); 655 + expect(location).toBeTruthy(); 666 656 667 657 const redirectUrl = new URL(location); 668 658 const authCode = redirectUrl.searchParams.get('code'); 669 - assert.ok(authCode, 'Redirect should have code'); 670 - assert.strictEqual(redirectUrl.searchParams.get('state'), 'test-state'); 671 - assert.strictEqual(redirectUrl.searchParams.get('iss'), BASE); 659 + expect(authCode).toBeTruthy(); 660 + expect(redirectUrl.searchParams.get('state')).toBe('test-state'); 661 + expect(redirectUrl.searchParams.get('iss')).toBe(BASE); 672 662 673 663 // Step 3: Token exchange 674 664 const tokenProof = await dpop.createProof('POST', `${BASE}/oauth/token`); ··· 687 677 }).toString(), 688 678 }); 689 679 690 - assert.strictEqual(tokenRes.status, 200, 'Token exchange should succeed'); 680 + expect(tokenRes.status).toBe(200); 691 681 const tokenData = await tokenRes.json(); 692 - assert.ok(tokenData.access_token, 'Should return access_token'); 693 - assert.ok(tokenData.refresh_token, 'Should return refresh_token'); 694 - assert.strictEqual(tokenData.token_type, 'DPoP'); 695 - assert.strictEqual(tokenData.scope, 'atproto'); 696 - assert.ok(tokenData.sub, 'Should return sub'); 682 + expect(tokenData.access_token).toBeTruthy(); 683 + expect(tokenData.refresh_token).toBeTruthy(); 684 + expect(tokenData.token_type).toBe('DPoP'); 685 + expect(tokenData.scope).toBe('atproto'); 686 + expect(tokenData.sub).toBeTruthy(); 697 687 698 688 // Step 4: Use access token with DPoP for protected endpoint 699 689 const resourceProof = await dpop.createProof( ··· 711 701 }, 712 702 ); 713 703 714 - assert.strictEqual( 715 - sessionRes.status, 716 - 200, 717 - 'Protected endpoint should work with DPoP token', 718 - ); 704 + expect( 705 + sessionRes.status).toBe(200); 719 706 const sessionData = await sessionRes.json(); 720 - assert.ok(sessionData.did, 'Should return session data'); 707 + expect(sessionData.did).toBeTruthy(); 721 708 722 709 // Step 5: Refresh token 723 710 const refreshProof = await dpop.createProof( ··· 737 724 }).toString(), 738 725 }); 739 726 740 - assert.strictEqual(refreshRes.status, 200, 'Refresh should succeed'); 727 + expect(refreshRes.status).toBe(200); 741 728 const refreshData = await refreshRes.json(); 742 - assert.ok(refreshData.access_token, 'Should return new access_token'); 743 - assert.ok(refreshData.refresh_token, 'Should return new refresh_token'); 729 + expect(refreshData.access_token).toBeTruthy(); 730 + expect(refreshData.refresh_token).toBeTruthy(); 744 731 745 732 // Step 6: Revoke token 746 733 const revokeRes = await fetch(`${BASE}/oauth/revoke`, { ··· 751 738 client_id: clientId, 752 739 }).toString(), 753 740 }); 754 - assert.strictEqual(revokeRes.status, 200); 741 + expect(revokeRes.status).toBe(200); 755 742 }); 756 743 757 744 it('DPoP key mismatch rejected', async () => { ··· 817 804 }).toString(), 818 805 }); 819 806 820 - assert.strictEqual(tokenRes.status, 400); 807 + expect(tokenRes.status).toBe(400); 821 808 const tokenData = await tokenRes.json(); 822 - assert.strictEqual(tokenData.error, 'invalid_dpop_proof'); 809 + expect(tokenData.error).toBe('invalid_dpop_proof'); 823 810 }); 824 811 825 812 it('fragment response_mode returns code in fragment', async () => { ··· 853 840 }).toString(), 854 841 }); 855 842 const parData = await parRes.json(); 856 - assert.ok(parData.request_uri); 843 + expect(parData.request_uri).toBeTruthy(); 857 844 858 845 // Authorize 859 846 const authRes = await fetch(`${BASE}/oauth/authorize`, { ··· 867 854 redirect: 'manual', 868 855 }); 869 856 870 - assert.strictEqual(authRes.status, 302); 857 + expect(authRes.status).toBe(302); 871 858 const location = authRes.headers.get('location'); 872 - assert.ok(location); 859 + expect(location).toBeTruthy(); 873 860 // For fragment mode, code should be in hash fragment 874 - assert.ok(location.includes('#'), 'Should use fragment'); 861 + expect(location.includes('#')).toBeTruthy(); // Should use fragment; 875 862 const url = new URL(location); 876 863 const fragment = new URLSearchParams(url.hash.slice(1)); 877 - assert.ok(fragment.get('code'), 'Code should be in fragment'); 878 - assert.ok(fragment.get('iss'), 'Issuer should be in fragment'); 864 + expect(fragment.get('code')).toBeTruthy(); // Code should be in fragment; 865 + expect(fragment.get('iss')).toBeTruthy(); // Issuer should be in fragment; 879 866 }); 880 867 881 868 it('PKCE failure - wrong code_verifier rejected', async () => { ··· 941 928 }).toString(), 942 929 }); 943 930 944 - assert.strictEqual(tokenRes.status, 400); 931 + expect(tokenRes.status).toBe(400); 945 932 const tokenData = await tokenRes.json(); 946 - assert.strictEqual(tokenData.error, 'invalid_grant'); 947 - assert.ok(tokenData.message?.includes('code_verifier')); 933 + expect(tokenData.error).toBe('invalid_grant'); 934 + expect(tokenData.message?.includes('code_verifier')).toBeTruthy();; 948 935 }); 949 936 950 937 it('redirect_uri mismatch rejected', async () => { ··· 976 963 }).toString(), 977 964 }); 978 965 979 - assert.strictEqual(parRes.status, 400); 966 + expect(parRes.status).toBe(400); 980 967 const parData = await parRes.json(); 981 - assert.strictEqual(parData.error, 'invalid_request'); 982 - assert.ok(parData.message?.includes('redirect_uri')); 968 + expect(parData.error).toBe('invalid_request'); 969 + expect(parData.message?.includes('redirect_uri')).toBeTruthy();; 983 970 }); 984 971 985 972 it('DPoP jti replay rejected', async () => { ··· 1013 1000 login_hint: DID, 1014 1001 }).toString(), 1015 1002 }); 1016 - assert.strictEqual(parRes1.status, 200); 1003 + expect(parRes1.status).toBe(200); 1017 1004 1018 1005 // Second request with SAME proof should be rejected 1019 1006 const parRes2 = await fetch(`${BASE}/oauth/par`, { ··· 1033 1020 }).toString(), 1034 1021 }); 1035 1022 1036 - assert.strictEqual(parRes2.status, 400); 1023 + expect(parRes2.status).toBe(400); 1037 1024 const data = await parRes2.json(); 1038 - assert.strictEqual(data.error, 'invalid_dpop_proof'); 1039 - assert.ok(data.message?.includes('replay')); 1025 + expect(data.error).toBe('invalid_dpop_proof'); 1026 + expect(data.message?.includes('replay')).toBeTruthy();; 1040 1027 }); 1041 1028 }); 1042 1029 ··· 1069 1056 }), 1070 1057 }); 1071 1058 1072 - assert.strictEqual(res.status, 403, 'Should reject with 403'); 1059 + expect(res.status).toBe(403); 1073 1060 const body = await res.json(); 1074 - assert.ok( 1075 - body.message?.includes('Missing required scope'), 1076 - 'Error should mention missing scope', 1077 - ); 1061 + expect(body.message?.includes('Missing required scope')).toBeTruthy(); // Error should mention missing scope 1078 1062 }); 1079 1063 1080 1064 it('createRecord allowed with matching scope', async () => { ··· 1105 1089 }), 1106 1090 }); 1107 1091 1108 - assert.strictEqual(res.status, 200, 'Should allow with correct scope'); 1092 + expect(res.status).toBe(200); 1109 1093 const body = await res.json(); 1110 - assert.ok(body.uri, 'Should return uri'); 1094 + expect(body.uri).toBeTruthy(); 1111 1095 1112 1096 // Note: We don't clean up here because our token only has create scope 1113 1097 // The record will be cleaned up by subsequent tests with full-access tokens ··· 1144 1128 }), 1145 1129 }); 1146 1130 1147 - assert.strictEqual( 1148 - res.status, 1149 - 200, 1150 - 'Wildcard scope should allow any collection', 1151 - ); 1131 + expect( 1132 + res.status).toBe(200); 1152 1133 }); 1153 1134 1154 1135 it('deleteRecord denied without delete scope', async () => { ··· 1179 1160 }), 1180 1161 }); 1181 1162 1182 - assert.strictEqual( 1183 - res.status, 1184 - 403, 1185 - 'Should reject delete without delete scope', 1186 - ); 1163 + expect( 1164 + res.status).toBe(403); 1187 1165 }); 1188 1166 1189 1167 it('uploadBlob denied with mismatched MIME scope', async () => { ··· 1211 1189 body: new Uint8Array([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]), // Fake MP4 header 1212 1190 }); 1213 1191 1214 - assert.strictEqual( 1215 - res.status, 1216 - 403, 1217 - 'Should reject video upload with image-only scope', 1218 - ); 1192 + expect( 1193 + res.status).toBe(403); 1219 1194 const body = await res.json(); 1220 - assert.ok( 1221 - body.message?.includes('Missing required scope'), 1222 - 'Error should mention missing scope', 1223 - ); 1195 + expect(body.message?.includes('Missing required scope')).toBeTruthy(); // Error should mention missing scope 1224 1196 }); 1225 1197 1226 1198 it('uploadBlob allowed with matching MIME scope', async () => { ··· 1257 1229 body: pngBytes, 1258 1230 }); 1259 1231 1260 - assert.strictEqual( 1261 - res.status, 1262 - 200, 1263 - 'Should allow image upload with image scope', 1264 - ); 1232 + expect( 1233 + res.status).toBe(200); 1265 1234 }); 1266 1235 1267 1236 it('transition:generic grants full access', async () => { ··· 1295 1264 }), 1296 1265 }); 1297 1266 1298 - assert.strictEqual( 1299 - res.status, 1300 - 200, 1301 - 'transition:generic should grant full access', 1302 - ); 1267 + expect( 1268 + res.status).toBe(200); 1303 1269 }); 1304 1270 }); 1305 1271 ··· 1337 1303 }).toString(), 1338 1304 }); 1339 1305 1340 - assert.strictEqual(parRes.status, 200, 'PAR should succeed'); 1306 + expect(parRes.status).toBe(200); 1341 1307 const { request_uri } = await parRes.json(); 1342 1308 1343 1309 // GET the authorize page ··· 1348 1314 const html = await authorizeRes.text(); 1349 1315 1350 1316 // Verify permissions table is rendered 1351 - assert.ok( 1352 - html.includes('Repository permissions:'), 1353 - 'Should show repo permissions section', 1354 - ); 1355 - assert.ok( 1356 - html.includes('app.bsky.feed.post'), 1357 - 'Should show collection name', 1358 - ); 1359 - assert.ok( 1360 - html.includes('Upload permissions:'), 1361 - 'Should show upload permissions section', 1362 - ); 1363 - assert.ok(html.includes('image/*'), 'Should show blob MIME type'); 1317 + expect(html.includes('Repository permissions:')).toBeTruthy(); // Should show repo permissions section 1318 + expect(html.includes('app.bsky.feed.post')).toBeTruthy(); // Should show collection name 1319 + expect(html.includes('Upload permissions:')).toBeTruthy(); // Should show upload permissions section 1320 + expect(html.includes('image/*')).toBeTruthy(); // Should show blob MIME type; 1364 1321 }); 1365 1322 1366 1323 it('consent page shows identity message for atproto-only scope', async () => { ··· 1395 1352 }).toString(), 1396 1353 }); 1397 1354 1398 - assert.strictEqual(parRes.status, 200, 'PAR should succeed'); 1355 + expect(parRes.status).toBe(200); 1399 1356 const { request_uri } = await parRes.json(); 1400 1357 1401 1358 // GET the authorize page ··· 1406 1363 const html = await authorizeRes.text(); 1407 1364 1408 1365 // Verify identity-only message 1409 - assert.ok( 1410 - html.includes('wants to uniquely identify you'), 1411 - 'Should show identity-only message', 1412 - ); 1413 - assert.ok( 1414 - !html.includes('Repository permissions:'), 1415 - 'Should NOT show permissions table', 1416 - ); 1366 + expect(html.includes('wants to uniquely identify you')).toBeTruthy(); // Should show identity-only message 1367 + expect(!html.includes('Repository permissions:')).toBeTruthy(); // Should NOT show permissions table 1417 1368 }); 1418 1369 1419 1370 it('consent page shows warning for transition:generic scope', async () => { ··· 1448 1399 }).toString(), 1449 1400 }); 1450 1401 1451 - assert.strictEqual(parRes.status, 200, 'PAR should succeed'); 1402 + expect(parRes.status).toBe(200); 1452 1403 const { request_uri } = await parRes.json(); 1453 1404 1454 1405 // GET the authorize page ··· 1459 1410 const html = await authorizeRes.text(); 1460 1411 1461 1412 // Verify warning banner 1462 - assert.ok( 1463 - html.includes('Full repository access requested'), 1464 - 'Should show full access warning', 1465 - ); 1413 + expect(html.includes('Full repository access requested')).toBeTruthy(); // Should show full access warning 1466 1414 }); 1467 1415 1468 1416 it('supports direct authorization without PAR', async () => { ··· 1488 1436 authorizeUrl.searchParams.set('login_hint', DID); 1489 1437 1490 1438 const getRes = await fetch(authorizeUrl.toString()); 1491 - assert.strictEqual( 1492 - getRes.status, 1493 - 200, 1494 - 'Direct authorize GET should succeed', 1495 - ); 1439 + expect( 1440 + getRes.status).toBe(200); 1496 1441 1497 1442 const html = await getRes.text(); 1498 - assert.ok(html.includes('Authorize'), 'Should show consent page'); 1499 - assert.ok( 1500 - html.includes('request_uri'), 1501 - 'Should include request_uri in form', 1502 - ); 1443 + expect(html.includes('Authorize')).toBeTruthy(); // Should show consent page; 1444 + expect(html.includes('request_uri')).toBeTruthy(); // Should include request_uri in form 1503 1445 }); 1504 1446 1505 1447 it('completes full direct authorization flow', async () => { ··· 1525 1467 authorizeUrl.searchParams.set('login_hint', DID); 1526 1468 1527 1469 const getRes = await fetch(authorizeUrl.toString()); 1528 - assert.strictEqual(getRes.status, 200); 1470 + expect(getRes.status).toBe(200); 1529 1471 const html = await getRes.text(); 1530 1472 1531 1473 // Extract request_uri from the form 1532 1474 const requestUriMatch = html.match(/name="request_uri" value="([^"]+)"/); 1533 - assert.ok(requestUriMatch, 'Should have request_uri in form'); 1475 + expect(requestUriMatch).toBeTruthy(); 1534 1476 const requestUri = requestUriMatch[1]; 1535 1477 1536 1478 // Step 2: POST to authorize (user approval) ··· 1545 1487 redirect: 'manual', 1546 1488 }); 1547 1489 1548 - assert.strictEqual(authRes.status, 302, 'Should redirect after approval'); 1490 + expect(authRes.status).toBe(302); 1549 1491 const location = authRes.headers.get('location'); 1550 - assert.ok(location, 'Should have Location header'); 1492 + expect(location).toBeTruthy(); 1551 1493 const locationUrl = new URL(location); 1552 1494 const code = locationUrl.searchParams.get('code'); 1553 - assert.ok(code, 'Should have authorization code'); 1554 - assert.strictEqual(locationUrl.searchParams.get('state'), state); 1495 + expect(code).toBeTruthy(); 1496 + expect(locationUrl.searchParams.get('state')).toBe(state); 1555 1497 1556 1498 // Step 3: Exchange code for tokens 1557 1499 const dpop = await DpopClient.create(); ··· 1572 1514 }).toString(), 1573 1515 }); 1574 1516 1575 - assert.strictEqual(tokenRes.status, 200, 'Token exchange should succeed'); 1517 + expect(tokenRes.status).toBe(200); 1576 1518 const tokenData = await tokenRes.json(); 1577 - assert.ok(tokenData.access_token, 'Should have access_token'); 1578 - assert.strictEqual(tokenData.token_type, 'DPoP'); 1519 + expect(tokenData.access_token).toBeTruthy(); 1520 + expect(tokenData.token_type).toBe('DPoP'); 1579 1521 }); 1580 1522 1581 1523 it('consent page shows profile card when login_hint is provided', async () => { ··· 1601 1543 const res = await fetch(authorizeUrl.toString()); 1602 1544 const html = await res.text(); 1603 1545 1604 - assert.ok( 1605 - html.includes('profile-card'), 1606 - 'Should include profile card element', 1607 - ); 1608 - assert.ok( 1609 - html.includes('@test.handle.example'), 1610 - 'Should show handle with @ prefix', 1611 - ); 1612 - assert.ok( 1613 - html.includes('app.bsky.actor.getProfile'), 1614 - 'Should include profile fetch script', 1615 - ); 1546 + expect(html.includes('profile-card')).toBeTruthy(); // Should include profile card element 1547 + expect(html.includes('@test.handle.example')).toBeTruthy(); // Should show handle with @ prefix 1548 + expect(html.includes('app.bsky.actor.getProfile')).toBeTruthy(); // Should include profile fetch script 1616 1549 }); 1617 1550 1618 1551 it('consent page does not show profile card when login_hint is omitted', async () => { ··· 1639 1572 const html = await res.text(); 1640 1573 1641 1574 // Check for the actual element (id="profile-card"), not the CSS class selector 1642 - assert.ok( 1643 - !html.includes('id="profile-card"'), 1644 - 'Should NOT include profile card element', 1645 - ); 1646 - assert.ok( 1647 - !html.includes('app.bsky.actor.getProfile'), 1648 - 'Should NOT include profile fetch script', 1649 - ); 1575 + expect(!html.includes('id="profile-card"')).toBeTruthy(); // Should NOT include profile card element 1576 + expect(!html.includes('app.bsky.actor.getProfile')).toBeTruthy(); // Should NOT include profile fetch script 1650 1577 }); 1651 1578 1652 1579 it('consent page escapes dangerous characters in login_hint', async () => { ··· 1677 1604 1678 1605 // JSON.stringify escapes double quotes, so the payload should be escaped 1679 1606 // The raw ");alert(" should NOT appear - it should be escaped as \");alert(\" 1680 - assert.ok( 1681 - !html.includes('");alert("'), 1607 + expect( 1608 + !html.includes('").toBeTruthy();alert("'), 1682 1609 'Should escape double quotes to prevent XSS breakout', 1683 1610 ); 1684 1611 // Verify the escaped version is present (backslash before the quote) 1685 - assert.ok( 1686 - html.includes('\\"'), 1687 - 'Should contain escaped characters from JSON.stringify', 1688 - ); 1612 + expect(html.includes('\\"')).toBeTruthy(); // Should contain escaped characters from JSON.stringify 1689 1613 }); 1690 1614 }); 1691 1615 ··· 1703 1627 }, 1704 1628 ); 1705 1629 // AppView returns 200 (found) or 400 (RecordNotFound), not 404 or 502 1706 - assert.ok( 1630 + expect( 1707 1631 res.status === 200 || res.status === 400, 1708 1632 `Expected 200 or 400 from AppView, got ${res.status}`, 1709 - ); 1633 + ).toBeTruthy(); 1710 1634 // Verify we got a JSON response (not an error page) 1711 1635 const contentType = res.headers.get('content-type'); 1712 - assert.ok( 1713 - contentType?.includes('application/json'), 1714 - 'Should return JSON', 1715 - ); 1636 + expect(contentType?.includes('application/json')).toBeTruthy(); // Should return JSON 1716 1637 }); 1717 1638 1718 1639 it('handles foreign repo locally without header (returns not found)', async () => { ··· 1722 1643 `${BASE}/xrpc/com.atproto.repo.getRecord?repo=did:plc:z72i7hdynmk6r22z27h6tvur&collection=app.bsky.feed.post&rkey=3juzlwllznd24`, 1723 1644 ); 1724 1645 // Local PDS returns 404 for non-existent record/DID 1725 - assert.strictEqual(res.status, 404); 1646 + expect(res.status).toBe(404); 1726 1647 }); 1727 1648 1728 1649 it('returns error for unknown proxy service', async () => { ··· 1734 1655 }, 1735 1656 }, 1736 1657 ); 1737 - assert.strictEqual(res.status, 400); 1658 + expect(res.status).toBe(400); 1738 1659 const data = await res.json(); 1739 - assert.ok(data.message.includes('Unknown proxy service')); 1660 + expect(data.message.includes('Unknown proxy service')).toBeTruthy();; 1740 1661 }); 1741 1662 1742 1663 it('returns error for malformed atproto-proxy header', async () => { ··· 1749 1670 }, 1750 1671 }, 1751 1672 ); 1752 - assert.strictEqual(res1.status, 400); 1673 + expect(res1.status).toBe(400); 1753 1674 const data1 = await res1.json(); 1754 - assert.ok(data1.message.includes('Malformed atproto-proxy header')); 1675 + expect(data1.message.includes('Malformed atproto-proxy header')).toBeTruthy();; 1755 1676 1756 1677 // Header with only fragment 1757 1678 const res2 = await fetch( ··· 1762 1683 }, 1763 1684 }, 1764 1685 ); 1765 - assert.strictEqual(res2.status, 400); 1686 + expect(res2.status).toBe(400); 1766 1687 const data2 = await res2.json(); 1767 - assert.ok(data2.message.includes('Malformed atproto-proxy header')); 1688 + expect(data2.message.includes('Malformed atproto-proxy header')).toBeTruthy();; 1768 1689 }); 1769 1690 1770 1691 it('returns local record for local DID without proxy header', async () => { ··· 1788 1709 const res = await fetch( 1789 1710 `${BASE}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=app.bsky.feed.post&rkey=${rkey}`, 1790 1711 ); 1791 - assert.strictEqual(res.status, 200); 1712 + expect(res.status).toBe(200); 1792 1713 const data = await res.json(); 1793 - assert.ok(data.value.text.includes('Test post for local DID test')); 1714 + expect(data.value.text.includes('Test post for local DID test')).toBeTruthy();; 1794 1715 1795 1716 // Cleanup - verify success to ensure test isolation 1796 1717 const { status: cleanupStatus } = await jsonPost( ··· 1798 1719 { repo: DID, collection: 'app.bsky.feed.post', rkey }, 1799 1720 { Authorization: `Bearer ${token}` }, 1800 1721 ); 1801 - assert.strictEqual(cleanupStatus, 200, 'Cleanup should succeed'); 1722 + expect(cleanupStatus).toBe(200); 1802 1723 }); 1803 1724 1804 1725 it('describeRepo handles foreign DID locally', async () => { ··· 1807 1728 `${BASE}/xrpc/com.atproto.repo.describeRepo?repo=did:plc:z72i7hdynmk6r22z27h6tvur`, 1808 1729 ); 1809 1730 // Local PDS returns 404 for non-existent DID 1810 - assert.strictEqual(res.status, 404); 1731 + expect(res.status).toBe(404); 1811 1732 }); 1812 1733 1813 1734 it('listRecords handles foreign DID locally', async () => { ··· 1817 1738 `${BASE}/xrpc/com.atproto.repo.listRecords?repo=did:plc:z72i7hdynmk6r22z27h6tvur&collection=app.bsky.feed.post&limit=1`, 1818 1739 ); 1819 1740 // Local PDS returns 200 with empty records (or 404 for completely unknown DID) 1820 - assert.ok( 1741 + expect( 1821 1742 res.status === 200 || res.status === 404, 1822 1743 `Expected 200 or 404, got ${res.status}`, 1823 - ); 1744 + ).toBeTruthy(); 1824 1745 }); 1825 1746 }); 1826 1747 ··· 1831 1752 { repo: DID, collection: 'app.bsky.feed.post', rkey: testRkey }, 1832 1753 { Authorization: `Bearer ${token}` }, 1833 1754 ); 1834 - assert.strictEqual(status, 200); 1755 + expect(status).toBe(200); 1835 1756 }); 1836 1757 }); 1837 1758 });
+239 -300
test/pds.test.js
··· 1 - import assert from 'node:assert'; 2 - import { describe, test } from 'node:test'; 1 + import { describe, test, expect } from 'vitest'; 3 2 import { 4 3 base32Decode, 5 4 base32Encode, ··· 49 48 0xa2, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c, 50 49 0x64, 0x63, 0x6e, 0x75, 0x6d, 0x18, 0x2a, 51 50 ]); 52 - assert.deepStrictEqual(encoded, expected); 51 + expect(encoded).toEqual(expected); 53 52 }); 54 53 55 54 test('encodes null', () => { 56 55 const encoded = cborEncode(null); 57 - assert.deepStrictEqual(encoded, new Uint8Array([0xf6])); 56 + expect(encoded).toEqual(new Uint8Array([0xf6])); 58 57 }); 59 58 60 59 test('encodes booleans', () => { 61 - assert.deepStrictEqual(cborEncode(true), new Uint8Array([0xf5])); 62 - assert.deepStrictEqual(cborEncode(false), new Uint8Array([0xf4])); 60 + expect(cborEncode(true)).toEqual(new Uint8Array([0xf5])); 61 + expect(cborEncode(false)).toEqual(new Uint8Array([0xf4])); 63 62 }); 64 63 65 64 test('encodes small integers', () => { 66 - assert.deepStrictEqual(cborEncode(0), new Uint8Array([0x00])); 67 - assert.deepStrictEqual(cborEncode(1), new Uint8Array([0x01])); 68 - assert.deepStrictEqual(cborEncode(23), new Uint8Array([0x17])); 65 + expect(cborEncode(0)).toEqual(new Uint8Array([0x00])); 66 + expect(cborEncode(1)).toEqual(new Uint8Array([0x01])); 67 + expect(cborEncode(23)).toEqual(new Uint8Array([0x17])); 69 68 }); 70 69 71 70 test('encodes integers >= 24', () => { 72 - assert.deepStrictEqual(cborEncode(24), new Uint8Array([0x18, 0x18])); 73 - assert.deepStrictEqual(cborEncode(255), new Uint8Array([0x18, 0xff])); 71 + expect(cborEncode(24)).toEqual(new Uint8Array([0x18, 0x18])); 72 + expect(cborEncode(255)).toEqual(new Uint8Array([0x18, 0xff])); 74 73 }); 75 74 76 75 test('encodes negative integers', () => { 77 - assert.deepStrictEqual(cborEncode(-1), new Uint8Array([0x20])); 78 - assert.deepStrictEqual(cborEncode(-10), new Uint8Array([0x29])); 76 + expect(cborEncode(-1)).toEqual(new Uint8Array([0x20])); 77 + expect(cborEncode(-10)).toEqual(new Uint8Array([0x29])); 79 78 }); 80 79 81 80 test('encodes strings', () => { 82 81 const encoded = cborEncode('hello'); 83 82 // 0x65 = text string of length 5 84 - assert.deepStrictEqual( 85 - encoded, 86 - new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), 87 - ); 83 + expect(encoded).toEqual(new Uint8Array([0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f])); 88 84 }); 89 85 90 86 test('encodes byte strings', () => { 91 87 const bytes = new Uint8Array([1, 2, 3]); 92 88 const encoded = cborEncode(bytes); 93 89 // 0x43 = byte string of length 3 94 - assert.deepStrictEqual(encoded, new Uint8Array([0x43, 1, 2, 3])); 90 + expect(encoded).toEqual(new Uint8Array([0x43, 1, 2, 3])); 95 91 }); 96 92 97 93 test('encodes arrays', () => { 98 94 const encoded = cborEncode([1, 2, 3]); 99 95 // 0x83 = array of length 3 100 - assert.deepStrictEqual(encoded, new Uint8Array([0x83, 0x01, 0x02, 0x03])); 96 + expect(encoded).toEqual(new Uint8Array([0x83, 0x01, 0x02, 0x03])); 101 97 }); 102 98 103 99 test('sorts map keys deterministically', () => { 104 100 const encoded1 = cborEncode({ z: 1, a: 2 }); 105 101 const encoded2 = cborEncode({ a: 2, z: 1 }); 106 - assert.deepStrictEqual(encoded1, encoded2); 102 + expect(encoded1).toEqual(encoded2); 107 103 // First key should be 'a' (0x61) 108 - assert.strictEqual(encoded1[1], 0x61); 104 + expect(encoded1[1]).toBe(0x61); 109 105 }); 110 106 111 107 test('encodes large integers >= 2^31 without overflow', () => { ··· 113 109 const twoTo31 = 2147483648; 114 110 const encoded = cborEncode(twoTo31); 115 111 const decoded = cborDecode(encoded); 116 - assert.strictEqual(decoded, twoTo31); 112 + expect(decoded).toBe(twoTo31); 117 113 118 114 // 2^32 - 1 (max unsigned 32-bit) 119 115 const maxU32 = 4294967295; 120 116 const encoded2 = cborEncode(maxU32); 121 117 const decoded2 = cborDecode(encoded2); 122 - assert.strictEqual(decoded2, maxU32); 118 + expect(decoded2).toBe(maxU32); 123 119 }); 124 120 125 121 test('encodes 2^31 with correct byte format', () => { 126 122 // 2147483648 = 0x80000000 127 123 // CBOR: major type 0 (unsigned int), additional info 26 (4-byte follows) 128 124 const encoded = cborEncode(2147483648); 129 - assert.strictEqual(encoded[0], 0x1a); // type 0 | info 26 130 - assert.strictEqual(encoded[1], 0x80); 131 - assert.strictEqual(encoded[2], 0x00); 132 - assert.strictEqual(encoded[3], 0x00); 133 - assert.strictEqual(encoded[4], 0x00); 125 + expect(encoded[0]).toBe(0x1a); // type 0 | info 26 126 + expect(encoded[1]).toBe(0x80); 127 + expect(encoded[2]).toBe(0x00); 128 + expect(encoded[3]).toBe(0x00); 129 + expect(encoded[4]).toBe(0x00); 134 130 }); 135 131 }); 136 132 ··· 138 134 test('encodes bytes to base32lower', () => { 139 135 const bytes = new Uint8Array([0x01, 0x71, 0x12, 0x20]); 140 136 const encoded = base32Encode(bytes); 141 - assert.strictEqual(typeof encoded, 'string'); 142 - assert.match(encoded, /^[a-z2-7]+$/); 137 + expect(typeof encoded).toBe('string'); 138 + expect(encoded).toMatch(/^[a-z2-7]+$/); 143 139 }); 144 140 }); 145 141 ··· 148 144 const data = cborEncode({ test: 'data' }); 149 145 const cid = await createCid(data); 150 146 151 - assert.strictEqual(cid.length, 36); // 2 prefix + 2 multihash header + 32 hash 152 - assert.strictEqual(cid[0], 0x01); // CIDv1 153 - assert.strictEqual(cid[1], 0x71); // dag-cbor 154 - assert.strictEqual(cid[2], 0x12); // sha-256 155 - assert.strictEqual(cid[3], 0x20); // 32 bytes 147 + expect(cid.length).toBe(36); // 2 prefix + 2 multihash header + 32 hash 148 + expect(cid[0]).toBe(0x01); // CIDv1 149 + expect(cid[1]).toBe(0x71); // dag-cbor 150 + expect(cid[2]).toBe(0x12); // sha-256 151 + expect(cid[3]).toBe(0x20); // 32 bytes 156 152 }); 157 153 158 154 test('createBlobCid uses raw codec', async () => { 159 155 const data = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic bytes 160 156 const cid = await createBlobCid(data); 161 157 162 - assert.strictEqual(cid.length, 36); 163 - assert.strictEqual(cid[0], 0x01); // CIDv1 164 - assert.strictEqual(cid[1], 0x55); // raw codec 165 - assert.strictEqual(cid[2], 0x12); // sha-256 166 - assert.strictEqual(cid[3], 0x20); // 32 bytes 158 + expect(cid.length).toBe(36); 159 + expect(cid[0]).toBe(0x01); // CIDv1 160 + expect(cid[1]).toBe(0x55); // raw codec 161 + expect(cid[2]).toBe(0x12); // sha-256 162 + expect(cid[3]).toBe(0x20); // 32 bytes 167 163 }); 168 164 169 165 test('same bytes produce different CIDs with different codecs', async () => { ··· 171 167 const dagCborCid = cidToString(await createCid(data)); 172 168 const rawCid = cidToString(await createBlobCid(data)); 173 169 174 - assert.notStrictEqual(dagCborCid, rawCid); 170 + expect(dagCborCid).not.toBe(rawCid); 175 171 }); 176 172 177 173 test('cidToString returns base32lower with b prefix', async () => { ··· 179 175 const cid = await createCid(data); 180 176 const cidStr = cidToString(cid); 181 177 182 - assert.strictEqual(cidStr[0], 'b'); 183 - assert.match(cidStr, /^b[a-z2-7]+$/); 178 + expect(cidStr[0]).toBe('b'); 179 + expect(cidStr).toMatch(/^b[a-z2-7]+$/); 184 180 }); 185 181 186 182 test('same input produces same CID', async () => { ··· 189 185 const cid1 = cidToString(await createCid(data1)); 190 186 const cid2 = cidToString(await createCid(data2)); 191 187 192 - assert.strictEqual(cid1, cid2); 188 + expect(cid1).toBe(cid2); 193 189 }); 194 190 195 191 test('different input produces different CID', async () => { 196 192 const cid1 = cidToString(await createCid(cborEncode({ a: 1 }))); 197 193 const cid2 = cidToString(await createCid(cborEncode({ a: 2 }))); 198 194 199 - assert.notStrictEqual(cid1, cid2); 195 + expect(cid1).not.toBe(cid2); 200 196 }); 201 197 }); 202 198 203 199 describe('TID Generation', () => { 204 200 test('creates 13-character TIDs', () => { 205 201 const tid = createTid(); 206 - assert.strictEqual(tid.length, 13); 202 + expect(tid.length).toBe(13); 207 203 }); 208 204 209 205 test('uses valid base32-sort characters', () => { 210 206 const tid = createTid(); 211 - assert.match(tid, /^[234567abcdefghijklmnopqrstuvwxyz]+$/); 207 + expect(tid).toMatch(/^[234567abcdefghijklmnopqrstuvwxyz]+$/); 212 208 }); 213 209 214 210 test('generates monotonically increasing TIDs', () => { ··· 216 212 const tid2 = createTid(); 217 213 const tid3 = createTid(); 218 214 219 - assert.ok(tid1 < tid2, `${tid1} should be less than ${tid2}`); 220 - assert.ok(tid2 < tid3, `${tid2} should be less than ${tid3}`); 215 + expect(tid1 < tid2).toBe(true); 216 + expect(tid2 < tid3).toBe(true); 221 217 }); 222 218 223 219 test('generates unique TIDs', () => { ··· 225 221 for (let i = 0; i < 100; i++) { 226 222 tids.add(createTid()); 227 223 } 228 - assert.strictEqual(tids.size, 100); 224 + expect(tids.size).toBe(100); 229 225 }); 230 226 }); 231 227 ··· 233 229 test('generates key pair with correct sizes', async () => { 234 230 const kp = await generateKeyPair(); 235 231 236 - assert.strictEqual(kp.privateKey.length, 32); 237 - assert.strictEqual(kp.publicKey.length, 33); // compressed 238 - assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03); 232 + expect(kp.privateKey.length).toBe(32); 233 + expect(kp.publicKey.length).toBe(33); // compressed 234 + expect(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03).toBe(true); 239 235 }); 240 236 241 237 test('can sign data with generated key', async () => { ··· 244 240 const data = new TextEncoder().encode('test message'); 245 241 const sig = await sign(key, data); 246 242 247 - assert.strictEqual(sig.length, 64); // r (32) + s (32) 243 + expect(sig.length).toBe(64); // r (32) + s (32) 248 244 }); 249 245 250 246 test('different messages produce different signatures', async () => { ··· 254 250 const sig1 = await sign(key, new TextEncoder().encode('message 1')); 255 251 const sig2 = await sign(key, new TextEncoder().encode('message 2')); 256 252 257 - assert.notDeepStrictEqual(sig1, sig2); 253 + expect(sig1).not.toEqual(sig2); 258 254 }); 259 255 260 256 test('bytesToHex and hexToBytes roundtrip', () => { ··· 262 258 const hex = bytesToHex(original); 263 259 const back = hexToBytes(hex); 264 260 265 - assert.strictEqual(hex, '000ff0ffabcd'); 266 - assert.deepStrictEqual(back, original); 261 + expect(hex).toBe('000ff0ffabcd'); 262 + expect(back).toEqual(original); 267 263 }); 268 264 269 265 test('importPrivateKey rejects invalid key lengths', async () => { 270 266 // Too short 271 - await assert.rejects( 272 - () => importPrivateKey(new Uint8Array(31)), 267 + await expect(() => importPrivateKey(new Uint8Array(31))).rejects.toThrow( 273 268 /expected 32 bytes, got 31/, 274 269 ); 275 270 276 271 // Too long 277 - await assert.rejects( 278 - () => importPrivateKey(new Uint8Array(33)), 272 + await expect(() => importPrivateKey(new Uint8Array(33))).rejects.toThrow( 279 273 /expected 32 bytes, got 33/, 280 274 ); 281 275 282 276 // Empty 283 - await assert.rejects( 284 - () => importPrivateKey(new Uint8Array(0)), 277 + await expect(() => importPrivateKey(new Uint8Array(0))).rejects.toThrow( 285 278 /expected 32 bytes, got 0/, 286 279 ); 287 280 }); 288 281 289 282 test('importPrivateKey rejects non-Uint8Array input', async () => { 290 283 // Arrays have .length but aren't Uint8Array 291 - await assert.rejects( 292 - () => importPrivateKey([1, 2, 3]), 284 + await expect(() => importPrivateKey([1, 2, 3])).rejects.toThrow( 293 285 /Invalid private key/, 294 286 ); 295 287 296 288 // Strings don't work either 297 - await assert.rejects( 298 - () => importPrivateKey('not bytes'), 289 + await expect(() => importPrivateKey('not bytes')).rejects.toThrow( 299 290 /Invalid private key/, 300 291 ); 301 292 302 293 // null/undefined 303 - await assert.rejects(() => importPrivateKey(null), /Invalid private key/); 294 + await expect(() => importPrivateKey(null)).rejects.toThrow(/Invalid private key/); 304 295 }); 305 296 }); 306 297 307 298 describe('MST Key Depth', () => { 308 299 test('returns a non-negative integer', async () => { 309 300 const depth = await getKeyDepth('app.bsky.feed.post/abc123'); 310 - assert.strictEqual(typeof depth, 'number'); 311 - assert.ok(depth >= 0); 301 + expect(typeof depth).toBe('number'); 302 + expect(depth >= 0).toBe(true); 312 303 }); 313 304 314 305 test('is deterministic for same key', async () => { 315 306 const key = 'app.bsky.feed.post/test123'; 316 307 const depth1 = await getKeyDepth(key); 317 308 const depth2 = await getKeyDepth(key); 318 - assert.strictEqual(depth1, depth2); 309 + expect(depth1).toBe(depth2); 319 310 }); 320 311 321 312 test('different keys can have different depths', async () => { ··· 325 316 depths.add(await getKeyDepth(`collection/key${i}`)); 326 317 } 327 318 // Should have at least 1 unique depth (realistically more) 328 - assert.ok(depths.size >= 1); 319 + expect(depths.size >= 1).toBe(true); 329 320 }); 330 321 331 322 test('handles empty string', async () => { 332 323 const depth = await getKeyDepth(''); 333 - assert.strictEqual(typeof depth, 'number'); 334 - assert.ok(depth >= 0); 324 + expect(typeof depth).toBe('number'); 325 + expect(depth >= 0).toBe(true); 335 326 }); 336 327 337 328 test('handles unicode strings', async () => { 338 329 const depth = await getKeyDepth('app.bsky.feed.post/émoji🎉'); 339 - assert.strictEqual(typeof depth, 'number'); 340 - assert.ok(depth >= 0); 330 + expect(typeof depth).toBe('number'); 331 + expect(depth >= 0).toBe(true); 341 332 }); 342 333 }); 343 334 ··· 346 337 const original = { hello: 'world', num: 42 }; 347 338 const encoded = cborEncode(original); 348 339 const decoded = cborDecode(encoded); 349 - assert.deepStrictEqual(decoded, original); 340 + expect(decoded).toEqual(original); 350 341 }); 351 342 352 343 test('decodes null', () => { 353 344 const encoded = cborEncode(null); 354 345 const decoded = cborDecode(encoded); 355 - assert.strictEqual(decoded, null); 346 + expect(decoded).toBe(null); 356 347 }); 357 348 358 349 test('decodes booleans', () => { 359 - assert.strictEqual(cborDecode(cborEncode(true)), true); 360 - assert.strictEqual(cborDecode(cborEncode(false)), false); 350 + expect(cborDecode(cborEncode(true))).toBe(true); 351 + expect(cborDecode(cborEncode(false))).toBe(false); 361 352 }); 362 353 363 354 test('decodes integers', () => { 364 - assert.strictEqual(cborDecode(cborEncode(0)), 0); 365 - assert.strictEqual(cborDecode(cborEncode(42)), 42); 366 - assert.strictEqual(cborDecode(cborEncode(255)), 255); 367 - assert.strictEqual(cborDecode(cborEncode(-1)), -1); 368 - assert.strictEqual(cborDecode(cborEncode(-10)), -10); 355 + expect(cborDecode(cborEncode(0))).toBe(0); 356 + expect(cborDecode(cborEncode(42))).toBe(42); 357 + expect(cborDecode(cborEncode(255))).toBe(255); 358 + expect(cborDecode(cborEncode(-1))).toBe(-1); 359 + expect(cborDecode(cborEncode(-10))).toBe(-10); 369 360 }); 370 361 371 362 test('decodes strings', () => { 372 - assert.strictEqual(cborDecode(cborEncode('hello')), 'hello'); 373 - assert.strictEqual(cborDecode(cborEncode('')), ''); 363 + expect(cborDecode(cborEncode('hello'))).toBe('hello'); 364 + expect(cborDecode(cborEncode(''))).toBe(''); 374 365 }); 375 366 376 367 test('decodes arrays', () => { 377 - assert.deepStrictEqual(cborDecode(cborEncode([1, 2, 3])), [1, 2, 3]); 378 - assert.deepStrictEqual(cborDecode(cborEncode([])), []); 368 + expect(cborDecode(cborEncode([1, 2, 3]))).toEqual([1, 2, 3]); 369 + expect(cborDecode(cborEncode([]))).toEqual([]); 379 370 }); 380 371 381 372 test('decodes nested structures', () => { 382 373 const original = { arr: [1, { nested: true }], str: 'test' }; 383 374 const decoded = cborDecode(cborEncode(original)); 384 - assert.deepStrictEqual(decoded, original); 375 + expect(decoded).toEqual(original); 385 376 }); 386 377 }); 387 378 388 379 describe('CAR File Builder', () => { 389 380 test('varint encodes small numbers', () => { 390 - assert.deepStrictEqual(varint(0), new Uint8Array([0])); 391 - assert.deepStrictEqual(varint(1), new Uint8Array([1])); 392 - assert.deepStrictEqual(varint(127), new Uint8Array([127])); 381 + expect(varint(0)).toEqual(new Uint8Array([0])); 382 + expect(varint(1)).toEqual(new Uint8Array([1])); 383 + expect(varint(127)).toEqual(new Uint8Array([127])); 393 384 }); 394 385 395 386 test('varint encodes multi-byte numbers', () => { 396 387 // 128 = 0x80 -> [0x80 | 0x00, 0x01] = [0x80, 0x01] 397 - assert.deepStrictEqual(varint(128), new Uint8Array([0x80, 0x01])); 388 + expect(varint(128)).toEqual(new Uint8Array([0x80, 0x01])); 398 389 // 300 = 0x12c -> [0xac, 0x02] 399 - assert.deepStrictEqual(varint(300), new Uint8Array([0xac, 0x02])); 390 + expect(varint(300)).toEqual(new Uint8Array([0xac, 0x02])); 400 391 }); 401 392 402 393 test('base32 encode/decode roundtrip', () => { 403 394 const original = new Uint8Array([0x01, 0x71, 0x12, 0x20, 0xab, 0xcd]); 404 395 const encoded = base32Encode(original); 405 396 const decoded = base32Decode(encoded); 406 - assert.deepStrictEqual(decoded, original); 397 + expect(decoded).toEqual(original); 407 398 }); 408 399 409 400 test('buildCarFile produces valid structure', async () => { ··· 413 404 414 405 const car = buildCarFile(cidStr, [{ cid: cidStr, data }]); 415 406 416 - assert.ok(car instanceof Uint8Array); 417 - assert.ok(car.length > 0); 407 + expect(car instanceof Uint8Array).toBe(true); 408 + expect(car.length > 0).toBe(true); 418 409 // First byte should be varint of header length 419 - assert.ok(car[0] > 0); 410 + expect(car[0] > 0).toBe(true); 420 411 }); 421 412 }); 422 413 ··· 424 415 test('base64UrlEncode encodes bytes correctly', () => { 425 416 const input = new TextEncoder().encode('hello world'); 426 417 const encoded = base64UrlEncode(input); 427 - assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ'); 428 - assert.ok(!encoded.includes('+')); 429 - assert.ok(!encoded.includes('/')); 430 - assert.ok(!encoded.includes('=')); 418 + expect(encoded).toBe('aGVsbG8gd29ybGQ'); 419 + expect(encoded.includes('+')).toBe(false); 420 + expect(encoded.includes('/')).toBe(false); 421 + expect(encoded.includes('=')).toBe(false); 431 422 }); 432 423 433 424 test('base64UrlDecode decodes string correctly', () => { 434 425 const decoded = base64UrlDecode('aGVsbG8gd29ybGQ'); 435 426 const str = new TextDecoder().decode(decoded); 436 - assert.strictEqual(str, 'hello world'); 427 + expect(str).toBe('hello world'); 437 428 }); 438 429 439 430 test('base64url roundtrip', () => { 440 431 const original = new Uint8Array([0, 1, 2, 255, 254, 253]); 441 432 const encoded = base64UrlEncode(original); 442 433 const decoded = base64UrlDecode(encoded); 443 - assert.deepStrictEqual(decoded, original); 434 + expect(decoded).toEqual(original); 444 435 }); 445 436 }); 446 437 ··· 451 442 const jwt = await createAccessJwt(did, secret); 452 443 453 444 const parts = jwt.split('.'); 454 - assert.strictEqual(parts.length, 3); 445 + expect(parts.length).toBe(3); 455 446 456 447 // Decode header 457 448 const header = JSON.parse( 458 449 new TextDecoder().decode(base64UrlDecode(parts[0])), 459 450 ); 460 - assert.strictEqual(header.typ, 'at+jwt'); 461 - assert.strictEqual(header.alg, 'HS256'); 451 + expect(header.typ).toBe('at+jwt'); 452 + expect(header.alg).toBe('HS256'); 462 453 463 454 // Decode payload 464 455 const payload = JSON.parse( 465 456 new TextDecoder().decode(base64UrlDecode(parts[1])), 466 457 ); 467 - assert.strictEqual(payload.scope, 'com.atproto.access'); 468 - assert.strictEqual(payload.sub, did); 469 - assert.strictEqual(payload.aud, did); 470 - assert.ok(payload.iat > 0); 471 - assert.ok(payload.exp > payload.iat); 458 + expect(payload.scope).toBe('com.atproto.access'); 459 + expect(payload.sub).toBe(did); 460 + expect(payload.aud).toBe(did); 461 + expect(payload.iat > 0).toBe(true); 462 + expect(payload.exp > payload.iat).toBe(true); 472 463 }); 473 464 474 465 test('createRefreshJwt creates valid JWT with jti', async () => { ··· 480 471 const header = JSON.parse( 481 472 new TextDecoder().decode(base64UrlDecode(parts[0])), 482 473 ); 483 - assert.strictEqual(header.typ, 'refresh+jwt'); 474 + expect(header.typ).toBe('refresh+jwt'); 484 475 485 476 const payload = JSON.parse( 486 477 new TextDecoder().decode(base64UrlDecode(parts[1])), 487 478 ); 488 - assert.strictEqual(payload.scope, 'com.atproto.refresh'); 489 - assert.ok(payload.jti); // has unique token ID 479 + expect(payload.scope).toBe('com.atproto.refresh'); 480 + expect(payload.jti).toBeTruthy(); // has unique token ID 490 481 }); 491 482 }); 492 483 ··· 497 488 const jwt = await createAccessJwt(did, secret); 498 489 499 490 const payload = await verifyAccessJwt(jwt, secret); 500 - assert.strictEqual(payload.sub, did); 501 - assert.strictEqual(payload.scope, 'com.atproto.access'); 491 + expect(payload.sub).toBe(did); 492 + expect(payload.scope).toBe('com.atproto.access'); 502 493 }); 503 494 504 495 test('verifyAccessJwt throws for wrong secret', async () => { 505 496 const did = 'did:web:test.example'; 506 497 const jwt = await createAccessJwt(did, 'correct-secret'); 507 498 508 - await assert.rejects( 509 - () => verifyAccessJwt(jwt, 'wrong-secret'), 499 + await expect(() => verifyAccessJwt(jwt, 'wrong-secret')).rejects.toThrow( 510 500 /invalid signature/i, 511 501 ); 512 502 }); ··· 517 507 // Create token that expired 1 second ago 518 508 const jwt = await createAccessJwt(did, secret, -1); 519 509 520 - await assert.rejects(() => verifyAccessJwt(jwt, secret), /expired/i); 510 + await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow(/expired/i); 521 511 }); 522 512 523 513 test('verifyAccessJwt throws for refresh token', async () => { ··· 525 515 const secret = 'test-secret-key'; 526 516 const jwt = await createRefreshJwt(did, secret); 527 517 528 - await assert.rejects( 529 - () => verifyAccessJwt(jwt, secret), 518 + await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow( 530 519 /invalid token type/i, 531 520 ); 532 521 }); ··· 537 526 const jwt = await createRefreshJwt(did, secret); 538 527 539 528 const payload = await verifyRefreshJwt(jwt, secret); 540 - assert.strictEqual(payload.sub, did); 541 - assert.strictEqual(payload.scope, 'com.atproto.refresh'); 542 - assert.ok(payload.jti); // has token ID 529 + expect(payload.sub).toBe(did); 530 + expect(payload.scope).toBe('com.atproto.refresh'); 531 + expect(payload.jti).toBeTruthy(); // has token ID 543 532 }); 544 533 545 534 test('verifyRefreshJwt throws for wrong secret', async () => { 546 535 const did = 'did:web:test.example'; 547 536 const jwt = await createRefreshJwt(did, 'correct-secret'); 548 537 549 - await assert.rejects( 550 - () => verifyRefreshJwt(jwt, 'wrong-secret'), 538 + await expect(() => verifyRefreshJwt(jwt, 'wrong-secret')).rejects.toThrow( 551 539 /invalid signature/i, 552 540 ); 553 541 }); ··· 558 546 // Create token that expired 1 second ago 559 547 const jwt = await createRefreshJwt(did, secret, -1); 560 548 561 - await assert.rejects(() => verifyRefreshJwt(jwt, secret), /expired/i); 549 + await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow(/expired/i); 562 550 }); 563 551 564 552 test('verifyRefreshJwt throws for access token', async () => { ··· 566 554 const secret = 'test-secret-key'; 567 555 const jwt = await createAccessJwt(did, secret); 568 556 569 - await assert.rejects( 570 - () => verifyRefreshJwt(jwt, secret), 557 + await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow( 571 558 /invalid token type/i, 572 559 ); 573 560 }); ··· 576 563 const secret = 'test-secret-key'; 577 564 578 565 // Not a JWT at all 579 - await assert.rejects( 580 - () => verifyAccessJwt('not-a-jwt', secret), 566 + await expect(() => verifyAccessJwt('not-a-jwt', secret)).rejects.toThrow( 581 567 /Invalid JWT format/i, 582 568 ); 583 569 584 570 // Only two parts 585 - await assert.rejects( 586 - () => verifyAccessJwt('two.parts', secret), 571 + await expect(() => verifyAccessJwt('two.parts', secret)).rejects.toThrow( 587 572 /Invalid JWT format/i, 588 573 ); 589 574 590 575 // Four parts 591 - await assert.rejects( 592 - () => verifyAccessJwt('one.two.three.four', secret), 576 + await expect(() => verifyAccessJwt('one.two.three.four', secret)).rejects.toThrow( 593 577 /Invalid JWT format/i, 594 578 ); 595 579 }); ··· 597 581 test('verifyRefreshJwt throws for malformed JWT', async () => { 598 582 const secret = 'test-secret-key'; 599 583 600 - await assert.rejects( 601 - () => verifyRefreshJwt('not-a-jwt', secret), 584 + await expect(() => verifyRefreshJwt('not-a-jwt', secret)).rejects.toThrow( 602 585 /Invalid JWT format/i, 603 586 ); 604 587 605 - await assert.rejects( 606 - () => verifyRefreshJwt('two.parts', secret), 588 + await expect(() => verifyRefreshJwt('two.parts', secret)).rejects.toThrow( 607 589 /Invalid JWT format/i, 608 590 ); 609 591 }); ··· 612 594 describe('MIME Type Sniffing', () => { 613 595 test('detects JPEG', () => { 614 596 const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]); 615 - assert.strictEqual(sniffMimeType(bytes), 'image/jpeg'); 597 + expect(sniffMimeType(bytes)).toBe('image/jpeg'); 616 598 }); 617 599 618 600 test('detects PNG', () => { 619 601 const bytes = new Uint8Array([ 620 602 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 621 603 ]); 622 - assert.strictEqual(sniffMimeType(bytes), 'image/png'); 604 + expect(sniffMimeType(bytes)).toBe('image/png'); 623 605 }); 624 606 625 607 test('detects GIF', () => { 626 608 const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); 627 - assert.strictEqual(sniffMimeType(bytes), 'image/gif'); 609 + expect(sniffMimeType(bytes)).toBe('image/gif'); 628 610 }); 629 611 630 612 test('detects WebP', () => { ··· 642 624 0x42, 643 625 0x50, // WEBP 644 626 ]); 645 - assert.strictEqual(sniffMimeType(bytes), 'image/webp'); 627 + expect(sniffMimeType(bytes)).toBe('image/webp'); 646 628 }); 647 629 648 630 test('detects MP4', () => { ··· 660 642 0x6f, 661 643 0x6d, // isom brand 662 644 ]); 663 - assert.strictEqual(sniffMimeType(bytes), 'video/mp4'); 645 + expect(sniffMimeType(bytes)).toBe('video/mp4'); 664 646 }); 665 647 666 648 test('detects AVIF', () => { ··· 678 660 0x69, 679 661 0x66, // avif brand 680 662 ]); 681 - assert.strictEqual(sniffMimeType(bytes), 'image/avif'); 663 + expect(sniffMimeType(bytes)).toBe('image/avif'); 682 664 }); 683 665 684 666 test('detects HEIC', () => { ··· 696 678 0x69, 697 679 0x63, // heic brand 698 680 ]); 699 - assert.strictEqual(sniffMimeType(bytes), 'image/heic'); 681 + expect(sniffMimeType(bytes)).toBe('image/heic'); 700 682 }); 701 683 702 684 test('returns null for unknown', () => { 703 685 const bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]); 704 - assert.strictEqual(sniffMimeType(bytes), null); 686 + expect(sniffMimeType(bytes)).toBe(null); 705 687 }); 706 688 }); 707 689 ··· 726 708 }, 727 709 }; 728 710 const refs = findBlobRefs(record); 729 - assert.deepStrictEqual(refs, ['bafkreiabc123']); 711 + expect(refs).toEqual(['bafkreiabc123']); 730 712 }); 731 713 732 714 test('finds multiple blob refs', () => { ··· 751 733 ], 752 734 }; 753 735 const refs = findBlobRefs(record); 754 - assert.deepStrictEqual(refs, ['cid1', 'cid2']); 736 + expect(refs).toEqual(['cid1', 'cid2']); 755 737 }); 756 738 757 739 test('returns empty array when no blobs', () => { 758 740 const record = { text: 'Hello world', count: 42 }; 759 741 const refs = findBlobRefs(record); 760 - assert.deepStrictEqual(refs, []); 742 + expect(refs).toEqual([]); 761 743 }); 762 744 763 745 test('handles null and primitives', () => { 764 - assert.deepStrictEqual(findBlobRefs(null), []); 765 - assert.deepStrictEqual(findBlobRefs('string'), []); 766 - assert.deepStrictEqual(findBlobRefs(42), []); 746 + expect(findBlobRefs(null)).toEqual([]); 747 + expect(findBlobRefs('string')).toEqual([]); 748 + expect(findBlobRefs(42)).toEqual([]); 767 749 }); 768 750 }); 769 751 ··· 781 763 const jkt2 = await computeJwkThumbprint(jwk); 782 764 783 765 // Thumbprint must be deterministic 784 - assert.strictEqual(jkt1, jkt2); 766 + expect(jkt1).toBe(jkt2); 785 767 // Must be base64url-encoded SHA-256 (43 chars) 786 - assert.strictEqual(jkt1.length, 43); 768 + expect(jkt1.length).toBe(43); 787 769 // Must only contain base64url characters 788 - assert.match(jkt1, /^[A-Za-z0-9_-]+$/); 770 + expect(jkt1).toMatch(/^[A-Za-z0-9_-]+$/); 789 771 }); 790 772 791 773 test('produces different thumbprints for different keys', async () => { ··· 805 787 const jkt1 = await computeJwkThumbprint(jwk1); 806 788 const jkt2 = await computeJwkThumbprint(jwk2); 807 789 808 - assert.notStrictEqual(jkt1, jkt2); 790 + expect(jkt1).not.toBe(jkt2); 809 791 }); 810 792 }); 811 793 812 794 describe('Client Metadata', () => { 813 795 test('isLoopbackClient detects localhost', () => { 814 - assert.strictEqual(isLoopbackClient('http://localhost:8080'), true); 815 - assert.strictEqual(isLoopbackClient('http://127.0.0.1:3000'), true); 816 - assert.strictEqual(isLoopbackClient('https://example.com'), false); 796 + expect(isLoopbackClient('http://localhost:8080')).toBe(true); 797 + expect(isLoopbackClient('http://127.0.0.1:3000')).toBe(true); 798 + expect(isLoopbackClient('https://example.com')).toBe(false); 817 799 }); 818 800 819 801 test('getLoopbackClientMetadata returns permissive defaults', () => { 820 802 const metadata = getLoopbackClientMetadata('http://localhost:8080'); 821 - assert.strictEqual(metadata.client_id, 'http://localhost:8080'); 822 - assert.ok(metadata.grant_types.includes('authorization_code')); 823 - assert.strictEqual(metadata.dpop_bound_access_tokens, true); 803 + expect(metadata.client_id).toBe('http://localhost:8080'); 804 + expect(metadata.grant_types.includes('authorization_code')).toBe(true); 805 + expect(metadata.dpop_bound_access_tokens).toBe(true); 824 806 }); 825 807 826 808 test('validateClientMetadata rejects mismatched client_id', () => { ··· 830 812 grant_types: ['authorization_code'], 831 813 response_types: ['code'], 832 814 }; 833 - assert.throws( 834 - () => 835 - validateClientMetadata(metadata, 'https://example.com/metadata.json'), 836 - /client_id mismatch/, 837 - ); 815 + expect(() => 816 + validateClientMetadata(metadata, 'https://example.com/metadata.json'), 817 + ).toThrow(/client_id mismatch/); 838 818 }); 839 819 }); 840 820 ··· 844 824 const result = parseAtprotoProxyHeader( 845 825 'did:web:api.bsky.app#bsky_appview', 846 826 ); 847 - assert.deepStrictEqual(result, { 827 + expect(result).toEqual({ 848 828 did: 'did:web:api.bsky.app', 849 829 serviceId: 'bsky_appview', 850 830 }); ··· 854 834 const result = parseAtprotoProxyHeader( 855 835 'did:plc:z72i7hdynmk6r22z27h6tvur#atproto_labeler', 856 836 ); 857 - assert.deepStrictEqual(result, { 837 + expect(result).toEqual({ 858 838 did: 'did:plc:z72i7hdynmk6r22z27h6tvur', 859 839 serviceId: 'atproto_labeler', 860 840 }); 861 841 }); 862 842 863 843 test('returns null for null/undefined', () => { 864 - assert.strictEqual(parseAtprotoProxyHeader(null), null); 865 - assert.strictEqual(parseAtprotoProxyHeader(undefined), null); 866 - assert.strictEqual(parseAtprotoProxyHeader(''), null); 844 + expect(parseAtprotoProxyHeader(null)).toBe(null); 845 + expect(parseAtprotoProxyHeader(undefined)).toBe(null); 846 + expect(parseAtprotoProxyHeader('')).toBe(null); 867 847 }); 868 848 869 849 test('returns null for header without fragment', () => { 870 - assert.strictEqual(parseAtprotoProxyHeader('did:web:api.bsky.app'), null); 850 + expect(parseAtprotoProxyHeader('did:web:api.bsky.app')).toBe(null); 871 851 }); 872 852 873 853 test('returns null for header with only fragment', () => { 874 - assert.strictEqual(parseAtprotoProxyHeader('#bsky_appview'), null); 854 + expect(parseAtprotoProxyHeader('#bsky_appview')).toBe(null); 875 855 }); 876 856 877 857 test('returns null for header with trailing fragment', () => { 878 - assert.strictEqual( 879 - parseAtprotoProxyHeader('did:web:api.bsky.app#'), 880 - null, 881 - ); 858 + expect(parseAtprotoProxyHeader('did:web:api.bsky.app#')).toBe(null); 882 859 }); 883 860 }); 884 861 885 862 describe('getKnownServiceUrl', () => { 886 863 test('returns URL for known Bluesky AppView', () => { 887 864 const result = getKnownServiceUrl('did:web:api.bsky.app', 'bsky_appview'); 888 - assert.strictEqual(result, BSKY_APPVIEW_URL); 865 + expect(result).toBe(BSKY_APPVIEW_URL); 889 866 }); 890 867 891 868 test('returns null for unknown service DID', () => { ··· 893 870 'did:web:unknown.service', 894 871 'bsky_appview', 895 872 ); 896 - assert.strictEqual(result, null); 873 + expect(result).toBe(null); 897 874 }); 898 875 899 876 test('returns null for unknown service ID', () => { ··· 901 878 'did:web:api.bsky.app', 902 879 'unknown_service', 903 880 ); 904 - assert.strictEqual(result, null); 881 + expect(result).toBe(null); 905 882 }); 906 883 907 884 test('returns null for both unknown', () => { 908 885 const result = getKnownServiceUrl('did:web:unknown', 'unknown'); 909 - assert.strictEqual(result, null); 886 + expect(result).toBe(null); 910 887 }); 911 888 }); 912 889 }); ··· 915 892 describe('parseRepoScope', () => { 916 893 test('parses repo scope with query parameter action', () => { 917 894 const result = parseRepoScope('repo:app.bsky.feed.post?action=create'); 918 - assert.deepStrictEqual(result, { 895 + expect(result).toEqual({ 919 896 collection: 'app.bsky.feed.post', 920 897 actions: ['create'], 921 898 }); ··· 925 902 const result = parseRepoScope( 926 903 'repo:app.bsky.feed.post?action=create&action=update', 927 904 ); 928 - assert.deepStrictEqual(result, { 905 + expect(result).toEqual({ 929 906 collection: 'app.bsky.feed.post', 930 907 actions: ['create', 'update'], 931 908 }); ··· 933 910 934 911 test('parses repo scope without actions as all actions', () => { 935 912 const result = parseRepoScope('repo:app.bsky.feed.post'); 936 - assert.deepStrictEqual(result, { 913 + expect(result).toEqual({ 937 914 collection: 'app.bsky.feed.post', 938 915 actions: ['create', 'update', 'delete'], 939 916 }); ··· 941 918 942 919 test('parses wildcard collection with action', () => { 943 920 const result = parseRepoScope('repo:*?action=create'); 944 - assert.deepStrictEqual(result, { 921 + expect(result).toEqual({ 945 922 collection: '*', 946 923 actions: ['create'], 947 924 }); ··· 951 928 const result = parseRepoScope( 952 929 'repo?collection=app.bsky.feed.post&action=create', 953 930 ); 954 - assert.deepStrictEqual(result, { 931 + expect(result).toEqual({ 955 932 collection: 'app.bsky.feed.post', 956 933 actions: ['create'], 957 934 }); ··· 961 938 const result = parseRepoScope( 962 939 'repo:app.bsky.feed.post?action=create&action=create&action=update', 963 940 ); 964 - assert.deepStrictEqual(result, { 941 + expect(result).toEqual({ 965 942 collection: 'app.bsky.feed.post', 966 943 actions: ['create', 'update'], 967 944 }); 968 945 }); 969 946 970 947 test('returns null for non-repo scope', () => { 971 - assert.strictEqual(parseRepoScope('atproto'), null); 972 - assert.strictEqual(parseRepoScope('blob:image/*'), null); 973 - assert.strictEqual(parseRepoScope('transition:generic'), null); 948 + expect(parseRepoScope('atproto')).toBe(null); 949 + expect(parseRepoScope('blob:image/*')).toBe(null); 950 + expect(parseRepoScope('transition:generic')).toBe(null); 974 951 }); 975 952 976 953 test('returns null for invalid repo scope', () => { 977 - assert.strictEqual(parseRepoScope('repo:'), null); 978 - assert.strictEqual(parseRepoScope('repo?'), null); 954 + expect(parseRepoScope('repo:')).toBe(null); 955 + expect(parseRepoScope('repo?')).toBe(null); 979 956 }); 980 957 }); 981 958 982 959 describe('parseBlobScope', () => { 983 960 test('parses wildcard MIME', () => { 984 961 const result = parseBlobScope('blob:*/*'); 985 - assert.deepStrictEqual(result, { accept: ['*/*'] }); 962 + expect(result).toEqual({ accept: ['*/*'] }); 986 963 }); 987 964 988 965 test('parses type wildcard', () => { 989 966 const result = parseBlobScope('blob:image/*'); 990 - assert.deepStrictEqual(result, { accept: ['image/*'] }); 967 + expect(result).toEqual({ accept: ['image/*'] }); 991 968 }); 992 969 993 970 test('parses specific MIME', () => { 994 971 const result = parseBlobScope('blob:image/png'); 995 - assert.deepStrictEqual(result, { accept: ['image/png'] }); 972 + expect(result).toEqual({ accept: ['image/png'] }); 996 973 }); 997 974 998 975 test('parses multiple MIMEs', () => { 999 976 const result = parseBlobScope('blob:image/png,image/jpeg'); 1000 - assert.deepStrictEqual(result, { accept: ['image/png', 'image/jpeg'] }); 977 + expect(result).toEqual({ accept: ['image/png', 'image/jpeg'] }); 1001 978 }); 1002 979 1003 980 test('returns null for non-blob scope', () => { 1004 - assert.strictEqual(parseBlobScope('atproto'), null); 1005 - assert.strictEqual(parseBlobScope('repo:*:create'), null); 981 + expect(parseBlobScope('atproto')).toBe(null); 982 + expect(parseBlobScope('repo:*:create')).toBe(null); 1006 983 }); 1007 984 }); 1008 985 1009 986 describe('matchesMime', () => { 1010 987 test('wildcard matches everything', () => { 1011 - assert.strictEqual(matchesMime('*/*', 'image/png'), true); 1012 - assert.strictEqual(matchesMime('*/*', 'video/mp4'), true); 988 + expect(matchesMime('*/*', 'image/png')).toBe(true); 989 + expect(matchesMime('*/*', 'video/mp4')).toBe(true); 1013 990 }); 1014 991 1015 992 test('type wildcard matches same type', () => { 1016 - assert.strictEqual(matchesMime('image/*', 'image/png'), true); 1017 - assert.strictEqual(matchesMime('image/*', 'image/jpeg'), true); 1018 - assert.strictEqual(matchesMime('image/*', 'video/mp4'), false); 993 + expect(matchesMime('image/*', 'image/png')).toBe(true); 994 + expect(matchesMime('image/*', 'image/jpeg')).toBe(true); 995 + expect(matchesMime('image/*', 'video/mp4')).toBe(false); 1019 996 }); 1020 997 1021 998 test('exact match', () => { 1022 - assert.strictEqual(matchesMime('image/png', 'image/png'), true); 1023 - assert.strictEqual(matchesMime('image/png', 'image/jpeg'), false); 999 + expect(matchesMime('image/png', 'image/png')).toBe(true); 1000 + expect(matchesMime('image/png', 'image/jpeg')).toBe(false); 1024 1001 }); 1025 1002 1026 1003 test('case insensitive', () => { 1027 - assert.strictEqual(matchesMime('image/PNG', 'image/png'), true); 1028 - assert.strictEqual(matchesMime('IMAGE/*', 'image/png'), true); 1004 + expect(matchesMime('image/PNG', 'image/png')).toBe(true); 1005 + expect(matchesMime('IMAGE/*', 'image/png')).toBe(true); 1029 1006 }); 1030 1007 }); 1031 1008 }); ··· 1034 1011 describe('static scopes', () => { 1035 1012 test('atproto grants full access', () => { 1036 1013 const perms = new ScopePermissions('atproto'); 1037 - assert.strictEqual( 1038 - perms.allowsRepo('app.bsky.feed.post', 'create'), 1039 - true, 1040 - ); 1041 - assert.strictEqual(perms.allowsRepo('any.collection', 'delete'), true); 1042 - assert.strictEqual(perms.allowsBlob('image/png'), true); 1043 - assert.strictEqual(perms.allowsBlob('video/mp4'), true); 1014 + expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); 1015 + expect(perms.allowsRepo('any.collection', 'delete')).toBe(true); 1016 + expect(perms.allowsBlob('image/png')).toBe(true); 1017 + expect(perms.allowsBlob('video/mp4')).toBe(true); 1044 1018 }); 1045 1019 1046 1020 test('transition:generic grants full repo/blob access', () => { 1047 1021 const perms = new ScopePermissions('transition:generic'); 1048 - assert.strictEqual( 1049 - perms.allowsRepo('app.bsky.feed.post', 'create'), 1050 - true, 1051 - ); 1052 - assert.strictEqual(perms.allowsRepo('any.collection', 'delete'), true); 1053 - assert.strictEqual(perms.allowsBlob('image/png'), true); 1022 + expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); 1023 + expect(perms.allowsRepo('any.collection', 'delete')).toBe(true); 1024 + expect(perms.allowsBlob('image/png')).toBe(true); 1054 1025 }); 1055 1026 }); 1056 1027 1057 1028 describe('repo scopes', () => { 1058 1029 test('wildcard collection allows any collection', () => { 1059 1030 const perms = new ScopePermissions('repo:*?action=create'); 1060 - assert.strictEqual( 1061 - perms.allowsRepo('app.bsky.feed.post', 'create'), 1062 - true, 1063 - ); 1064 - assert.strictEqual( 1065 - perms.allowsRepo('app.bsky.feed.like', 'create'), 1066 - true, 1067 - ); 1068 - assert.strictEqual( 1069 - perms.allowsRepo('app.bsky.feed.post', 'delete'), 1070 - false, 1071 - ); 1031 + expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); 1032 + expect(perms.allowsRepo('app.bsky.feed.like', 'create')).toBe(true); 1033 + expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(false); 1072 1034 }); 1073 1035 1074 1036 test('specific collection restricts to that collection', () => { 1075 1037 const perms = new ScopePermissions( 1076 1038 'repo:app.bsky.feed.post?action=create', 1077 1039 ); 1078 - assert.strictEqual( 1079 - perms.allowsRepo('app.bsky.feed.post', 'create'), 1080 - true, 1081 - ); 1082 - assert.strictEqual( 1083 - perms.allowsRepo('app.bsky.feed.like', 'create'), 1084 - false, 1085 - ); 1040 + expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); 1041 + expect(perms.allowsRepo('app.bsky.feed.like', 'create')).toBe(false); 1086 1042 }); 1087 1043 1088 1044 test('multiple actions', () => { 1089 1045 const perms = new ScopePermissions('repo:*?action=create&action=update'); 1090 - assert.strictEqual(perms.allowsRepo('x', 'create'), true); 1091 - assert.strictEqual(perms.allowsRepo('x', 'update'), true); 1092 - assert.strictEqual(perms.allowsRepo('x', 'delete'), false); 1046 + expect(perms.allowsRepo('x', 'create')).toBe(true); 1047 + expect(perms.allowsRepo('x', 'update')).toBe(true); 1048 + expect(perms.allowsRepo('x', 'delete')).toBe(false); 1093 1049 }); 1094 1050 1095 1051 test('multiple scopes combine', () => { 1096 1052 const perms = new ScopePermissions( 1097 1053 'repo:app.bsky.feed.post?action=create repo:app.bsky.feed.like?action=delete', 1098 1054 ); 1099 - assert.strictEqual( 1100 - perms.allowsRepo('app.bsky.feed.post', 'create'), 1101 - true, 1102 - ); 1103 - assert.strictEqual( 1104 - perms.allowsRepo('app.bsky.feed.like', 'delete'), 1105 - true, 1106 - ); 1107 - assert.strictEqual( 1108 - perms.allowsRepo('app.bsky.feed.post', 'delete'), 1109 - false, 1110 - ); 1055 + expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); 1056 + expect(perms.allowsRepo('app.bsky.feed.like', 'delete')).toBe(true); 1057 + expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(false); 1111 1058 }); 1112 1059 1113 1060 test('allowsRepo with query param format scopes', () => { 1114 1061 const perms = new ScopePermissions( 1115 1062 'atproto repo:app.bsky.feed.post?action=create', 1116 1063 ); 1117 - assert.strictEqual( 1118 - perms.allowsRepo('app.bsky.feed.post', 'create'), 1119 - true, 1120 - ); 1121 - assert.strictEqual( 1122 - perms.allowsRepo('app.bsky.feed.post', 'delete'), 1123 - true, 1124 - ); // atproto grants full access 1064 + expect(perms.allowsRepo('app.bsky.feed.post', 'create')).toBe(true); 1065 + expect(perms.allowsRepo('app.bsky.feed.post', 'delete')).toBe(true); // atproto grants full access 1125 1066 }); 1126 1067 }); 1127 1068 1128 1069 describe('blob scopes', () => { 1129 1070 test('wildcard allows any MIME', () => { 1130 1071 const perms = new ScopePermissions('blob:*/*'); 1131 - assert.strictEqual(perms.allowsBlob('image/png'), true); 1132 - assert.strictEqual(perms.allowsBlob('video/mp4'), true); 1072 + expect(perms.allowsBlob('image/png')).toBe(true); 1073 + expect(perms.allowsBlob('video/mp4')).toBe(true); 1133 1074 }); 1134 1075 1135 1076 test('type wildcard restricts to type', () => { 1136 1077 const perms = new ScopePermissions('blob:image/*'); 1137 - assert.strictEqual(perms.allowsBlob('image/png'), true); 1138 - assert.strictEqual(perms.allowsBlob('image/jpeg'), true); 1139 - assert.strictEqual(perms.allowsBlob('video/mp4'), false); 1078 + expect(perms.allowsBlob('image/png')).toBe(true); 1079 + expect(perms.allowsBlob('image/jpeg')).toBe(true); 1080 + expect(perms.allowsBlob('video/mp4')).toBe(false); 1140 1081 }); 1141 1082 1142 1083 test('specific MIME restricts exactly', () => { 1143 1084 const perms = new ScopePermissions('blob:image/png'); 1144 - assert.strictEqual(perms.allowsBlob('image/png'), true); 1145 - assert.strictEqual(perms.allowsBlob('image/jpeg'), false); 1085 + expect(perms.allowsBlob('image/png')).toBe(true); 1086 + expect(perms.allowsBlob('image/jpeg')).toBe(false); 1146 1087 }); 1147 1088 }); 1148 1089 1149 1090 describe('empty/no scope', () => { 1150 1091 test('no scope denies everything', () => { 1151 1092 const perms = new ScopePermissions(''); 1152 - assert.strictEqual(perms.allowsRepo('x', 'create'), false); 1153 - assert.strictEqual(perms.allowsBlob('image/png'), false); 1093 + expect(perms.allowsRepo('x', 'create')).toBe(false); 1094 + expect(perms.allowsBlob('image/png')).toBe(false); 1154 1095 }); 1155 1096 1156 1097 test('undefined scope denies everything', () => { 1157 1098 const perms = new ScopePermissions(undefined); 1158 - assert.strictEqual(perms.allowsRepo('x', 'create'), false); 1099 + expect(perms.allowsRepo('x', 'create')).toBe(false); 1159 1100 }); 1160 1101 }); 1161 1102 ··· 1164 1105 const perms = new ScopePermissions( 1165 1106 'repo:app.bsky.feed.post?action=create', 1166 1107 ); 1167 - assert.throws(() => perms.assertRepo('app.bsky.feed.like', 'create'), { 1168 - message: /Missing required scope/, 1169 - }); 1108 + expect(() => perms.assertRepo('app.bsky.feed.like', 'create')).toThrow( 1109 + /Missing required scope/, 1110 + ); 1170 1111 }); 1171 1112 1172 1113 test('does not throw when allowed', () => { 1173 1114 const perms = new ScopePermissions( 1174 1115 'repo:app.bsky.feed.post?action=create', 1175 1116 ); 1176 - assert.doesNotThrow(() => 1177 - perms.assertRepo('app.bsky.feed.post', 'create'), 1178 - ); 1117 + expect(() => perms.assertRepo('app.bsky.feed.post', 'create')).not.toThrow(); 1179 1118 }); 1180 1119 }); 1181 1120 1182 1121 describe('assertBlob', () => { 1183 1122 test('throws ScopeMissingError when denied', () => { 1184 1123 const perms = new ScopePermissions('blob:image/*'); 1185 - assert.throws(() => perms.assertBlob('video/mp4'), { 1186 - message: /Missing required scope/, 1187 - }); 1124 + expect(() => perms.assertBlob('video/mp4')).toThrow( 1125 + /Missing required scope/, 1126 + ); 1188 1127 }); 1189 1128 1190 1129 test('does not throw when allowed', () => { 1191 1130 const perms = new ScopePermissions('blob:image/*'); 1192 - assert.doesNotThrow(() => perms.assertBlob('image/png')); 1131 + expect(() => perms.assertBlob('image/png')).not.toThrow(); 1193 1132 }); 1194 1133 }); 1195 1134 }); ··· 1197 1136 describe('parseScopesForDisplay', () => { 1198 1137 test('parses identity-only scope', () => { 1199 1138 const result = parseScopesForDisplay('atproto'); 1200 - assert.strictEqual(result.hasAtproto, true); 1201 - assert.strictEqual(result.hasTransitionGeneric, false); 1202 - assert.strictEqual(result.repoPermissions.size, 0); 1203 - assert.deepStrictEqual(result.blobPermissions, []); 1139 + expect(result.hasAtproto).toBe(true); 1140 + expect(result.hasTransitionGeneric).toBe(false); 1141 + expect(result.repoPermissions.size).toBe(0); 1142 + expect(result.blobPermissions).toEqual([]); 1204 1143 }); 1205 1144 1206 1145 test('parses granular repo scopes', () => { 1207 1146 const result = parseScopesForDisplay( 1208 1147 'atproto repo:app.bsky.feed.post?action=create&action=update', 1209 1148 ); 1210 - assert.strictEqual(result.repoPermissions.size, 1); 1149 + expect(result.repoPermissions.size).toBe(1); 1211 1150 const postPerms = result.repoPermissions.get('app.bsky.feed.post'); 1212 - assert.deepStrictEqual(postPerms, { 1151 + expect(postPerms).toEqual({ 1213 1152 create: true, 1214 1153 update: true, 1215 1154 delete: false, ··· 1221 1160 'atproto repo:app.bsky.feed.post?action=create repo:app.bsky.feed.post?action=delete', 1222 1161 ); 1223 1162 const postPerms = result.repoPermissions.get('app.bsky.feed.post'); 1224 - assert.deepStrictEqual(postPerms, { 1163 + expect(postPerms).toEqual({ 1225 1164 create: true, 1226 1165 update: false, 1227 1166 delete: true, ··· 1230 1169 1231 1170 test('parses blob scopes', () => { 1232 1171 const result = parseScopesForDisplay('atproto blob:image/*'); 1233 - assert.deepStrictEqual(result.blobPermissions, ['image/*']); 1172 + expect(result.blobPermissions).toEqual(['image/*']); 1234 1173 }); 1235 1174 1236 1175 test('detects transition:generic', () => { 1237 1176 const result = parseScopesForDisplay('atproto transition:generic'); 1238 - assert.strictEqual(result.hasTransitionGeneric, true); 1177 + expect(result.hasTransitionGeneric).toBe(true); 1239 1178 }); 1240 1179 1241 1180 test('handles empty scope string', () => { 1242 1181 const result = parseScopesForDisplay(''); 1243 - assert.strictEqual(result.hasAtproto, false); 1244 - assert.strictEqual(result.hasTransitionGeneric, false); 1245 - assert.strictEqual(result.repoPermissions.size, 0); 1246 - assert.deepStrictEqual(result.blobPermissions, []); 1182 + expect(result.hasAtproto).toBe(false); 1183 + expect(result.hasTransitionGeneric).toBe(false); 1184 + expect(result.repoPermissions.size).toBe(0); 1185 + expect(result.blobPermissions).toEqual([]); 1247 1186 }); 1248 1187 });
+14
vitest.config.js
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['test/**/*.test.js'], 6 + testTimeout: 30000, 7 + hookTimeout: 60000, 8 + coverage: { 9 + provider: 'v8', 10 + reporter: ['text', 'html'], 11 + include: ['src/**/*.js'], 12 + }, 13 + }, 14 + });