A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
41
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 + });