mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

feat: create tangled repo on enrolment via repo.create with source url

+719 -7
+3
package.json
··· 34 34 "test:browser:update": "docker run --rm --network host -v $(pwd):/work/ -v /tmp/playwright-node-modules:/work/node_modules -w /work/ -it mcr.microsoft.com/playwright:v1.59.1-noble bash -c 'corepack enable && pnpm i && pnpm playwright test test/browser --update-snapshots'" 35 35 }, 36 36 "dependencies": { 37 + "@atcute/tid": "^1.1.2", 37 38 "@atproto/api": "^0.19.11", 38 39 "@atproto/jwk-jose": "^0.1.11", 39 40 "@atproto/oauth-client-node": "^0.3.17", ··· 43 44 "@nuxt/image": "^2.0.0", 44 45 "@nuxt/scripts": "^1.0.6", 45 46 "@nuxtjs/html-validator": "^2.1.0", 47 + "@octokit/app": "^16.1.2", 48 + "@octokit/auth-app": "^8.2.0", 46 49 "@octokit/webhooks-methods": "^6.0.0", 47 50 "drizzle-orm": "^0.45.2", 48 51 "nuxt": "^4.4.4",
+260
pnpm-lock.yaml
··· 12 12 13 13 .: 14 14 dependencies: 15 + '@atcute/tid': 16 + specifier: ^1.1.2 17 + version: 1.1.2 15 18 '@atproto/api': 16 19 specifier: ^0.19.11 17 20 version: 0.19.11 ··· 39 42 '@nuxtjs/html-validator': 40 43 specifier: ^2.1.0 41 44 version: 2.1.0(@voidzero-dev/vite-plus-test@0.1.20)(magicast@0.5.2) 45 + '@octokit/app': 46 + specifier: ^16.1.2 47 + version: 16.1.2 48 + '@octokit/auth-app': 49 + specifier: ^8.2.0 50 + version: 8.2.0 42 51 '@octokit/webhooks-methods': 43 52 specifier: ^6.0.0 44 53 version: 6.0.0 ··· 111 120 version: 3.2.7(typescript@6.0.3) 112 121 113 122 packages: 123 + 124 + '@atcute/tid@1.1.2': 125 + resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} 126 + 127 + '@atcute/time-ms@1.3.2': 128 + resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==} 114 129 115 130 '@atproto-labs/did-resolver@0.2.6': 116 131 resolution: {integrity: sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==} ··· 1427 1442 '@nuxtjs/html-validator@2.1.0': 1428 1443 resolution: {integrity: sha512-ldo8ioSsH3OEumtgwDMokTxlhjgO9FxjJWViAxisq5l/wjvaVX8SYTQ02wjtQcQQPSvS6BwgypAp400RlyFHng==} 1429 1444 1445 + '@octokit/app@16.1.2': 1446 + resolution: {integrity: sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==} 1447 + engines: {node: '>= 20'} 1448 + 1449 + '@octokit/auth-app@8.2.0': 1450 + resolution: {integrity: sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==} 1451 + engines: {node: '>= 20'} 1452 + 1453 + '@octokit/auth-oauth-app@9.0.3': 1454 + resolution: {integrity: sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==} 1455 + engines: {node: '>= 20'} 1456 + 1457 + '@octokit/auth-oauth-device@8.0.3': 1458 + resolution: {integrity: sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==} 1459 + engines: {node: '>= 20'} 1460 + 1461 + '@octokit/auth-oauth-user@6.0.2': 1462 + resolution: {integrity: sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==} 1463 + engines: {node: '>= 20'} 1464 + 1465 + '@octokit/auth-token@6.0.0': 1466 + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} 1467 + engines: {node: '>= 20'} 1468 + 1469 + '@octokit/auth-unauthenticated@7.0.3': 1470 + resolution: {integrity: sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g==} 1471 + engines: {node: '>= 20'} 1472 + 1473 + '@octokit/core@7.0.6': 1474 + resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} 1475 + engines: {node: '>= 20'} 1476 + 1477 + '@octokit/endpoint@11.0.3': 1478 + resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} 1479 + engines: {node: '>= 20'} 1480 + 1481 + '@octokit/graphql@9.0.3': 1482 + resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} 1483 + engines: {node: '>= 20'} 1484 + 1485 + '@octokit/oauth-app@8.0.3': 1486 + resolution: {integrity: sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==} 1487 + engines: {node: '>= 20'} 1488 + 1489 + '@octokit/oauth-authorization-url@8.0.0': 1490 + resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} 1491 + engines: {node: '>= 20'} 1492 + 1493 + '@octokit/oauth-methods@6.0.2': 1494 + resolution: {integrity: sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==} 1495 + engines: {node: '>= 20'} 1496 + 1497 + '@octokit/openapi-types@27.0.0': 1498 + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} 1499 + 1500 + '@octokit/openapi-webhooks-types@12.1.0': 1501 + resolution: {integrity: sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==} 1502 + 1503 + '@octokit/plugin-paginate-rest@14.0.0': 1504 + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} 1505 + engines: {node: '>= 20'} 1506 + peerDependencies: 1507 + '@octokit/core': '>=6' 1508 + 1509 + '@octokit/request-error@7.1.0': 1510 + resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} 1511 + engines: {node: '>= 20'} 1512 + 1513 + '@octokit/request@10.0.8': 1514 + resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==} 1515 + engines: {node: '>= 20'} 1516 + 1517 + '@octokit/types@16.0.0': 1518 + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} 1519 + 1430 1520 '@octokit/webhooks-methods@6.0.0': 1431 1521 resolution: {integrity: sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==} 1432 1522 engines: {node: '>= 20'} 1433 1523 1434 1524 '@octokit/webhooks-types@7.6.1': 1435 1525 resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==} 1526 + 1527 + '@octokit/webhooks@14.2.0': 1528 + resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==} 1529 + engines: {node: '>= 20'} 1436 1530 1437 1531 '@one-ini/wasm@0.1.1': 1438 1532 resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} ··· 2647 2741 '@tybys/wasm-util@0.10.2': 2648 2742 resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} 2649 2743 2744 + '@types/aws-lambda@8.10.161': 2745 + resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==} 2746 + 2650 2747 '@types/chai@5.2.3': 2651 2748 resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 2652 2749 ··· 3152 3249 engines: {node: '>=6.0.0'} 3153 3250 hasBin: true 3154 3251 3252 + before-after-hook@4.0.0: 3253 + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} 3254 + 3155 3255 bindings@1.5.0: 3156 3256 resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} 3157 3257 ··· 3752 3852 resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} 3753 3853 engines: {node: '>=18'} 3754 3854 3855 + fast-content-type-parse@3.0.0: 3856 + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} 3857 + 3755 3858 fast-deep-equal@3.1.3: 3756 3859 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 3757 3860 ··· 4189 4292 4190 4293 json-stable-stringify-without-jsonify@1.0.1: 4191 4294 resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 4295 + 4296 + json-with-bigint@3.5.8: 4297 + resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} 4192 4298 4193 4299 json5@2.2.3: 4194 4300 resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} ··· 5314 5420 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 5315 5421 engines: {node: '>=8.0'} 5316 5422 5423 + toad-cache@3.7.0: 5424 + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} 5425 + engines: {node: '>=12'} 5426 + 5317 5427 toidentifier@1.0.1: 5318 5428 resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 5319 5429 engines: {node: '>=0.6'} ··· 5400 5510 oxc-parser: 5401 5511 optional: true 5402 5512 5513 + universal-github-app-jwt@2.2.2: 5514 + resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} 5515 + 5516 + universal-user-agent@7.0.3: 5517 + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} 5518 + 5403 5519 unplugin-utils@0.3.1: 5404 5520 resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} 5405 5521 engines: {node: '>=20.19.0'} ··· 5774 5890 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 5775 5891 5776 5892 snapshots: 5893 + 5894 + '@atcute/tid@1.1.2': 5895 + dependencies: 5896 + '@atcute/time-ms': 1.3.2 5897 + 5898 + '@atcute/time-ms@1.3.2': {} 5777 5899 5778 5900 '@atproto-labs/did-resolver@0.2.6': 5779 5901 dependencies: ··· 7215 7337 - magicast 7216 7338 - vitest 7217 7339 7340 + '@octokit/app@16.1.2': 7341 + dependencies: 7342 + '@octokit/auth-app': 8.2.0 7343 + '@octokit/auth-unauthenticated': 7.0.3 7344 + '@octokit/core': 7.0.6 7345 + '@octokit/oauth-app': 8.0.3 7346 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) 7347 + '@octokit/types': 16.0.0 7348 + '@octokit/webhooks': 14.2.0 7349 + 7350 + '@octokit/auth-app@8.2.0': 7351 + dependencies: 7352 + '@octokit/auth-oauth-app': 9.0.3 7353 + '@octokit/auth-oauth-user': 6.0.2 7354 + '@octokit/request': 10.0.8 7355 + '@octokit/request-error': 7.1.0 7356 + '@octokit/types': 16.0.0 7357 + toad-cache: 3.7.0 7358 + universal-github-app-jwt: 2.2.2 7359 + universal-user-agent: 7.0.3 7360 + 7361 + '@octokit/auth-oauth-app@9.0.3': 7362 + dependencies: 7363 + '@octokit/auth-oauth-device': 8.0.3 7364 + '@octokit/auth-oauth-user': 6.0.2 7365 + '@octokit/request': 10.0.8 7366 + '@octokit/types': 16.0.0 7367 + universal-user-agent: 7.0.3 7368 + 7369 + '@octokit/auth-oauth-device@8.0.3': 7370 + dependencies: 7371 + '@octokit/oauth-methods': 6.0.2 7372 + '@octokit/request': 10.0.8 7373 + '@octokit/types': 16.0.0 7374 + universal-user-agent: 7.0.3 7375 + 7376 + '@octokit/auth-oauth-user@6.0.2': 7377 + dependencies: 7378 + '@octokit/auth-oauth-device': 8.0.3 7379 + '@octokit/oauth-methods': 6.0.2 7380 + '@octokit/request': 10.0.8 7381 + '@octokit/types': 16.0.0 7382 + universal-user-agent: 7.0.3 7383 + 7384 + '@octokit/auth-token@6.0.0': {} 7385 + 7386 + '@octokit/auth-unauthenticated@7.0.3': 7387 + dependencies: 7388 + '@octokit/request-error': 7.1.0 7389 + '@octokit/types': 16.0.0 7390 + 7391 + '@octokit/core@7.0.6': 7392 + dependencies: 7393 + '@octokit/auth-token': 6.0.0 7394 + '@octokit/graphql': 9.0.3 7395 + '@octokit/request': 10.0.8 7396 + '@octokit/request-error': 7.1.0 7397 + '@octokit/types': 16.0.0 7398 + before-after-hook: 4.0.0 7399 + universal-user-agent: 7.0.3 7400 + 7401 + '@octokit/endpoint@11.0.3': 7402 + dependencies: 7403 + '@octokit/types': 16.0.0 7404 + universal-user-agent: 7.0.3 7405 + 7406 + '@octokit/graphql@9.0.3': 7407 + dependencies: 7408 + '@octokit/request': 10.0.8 7409 + '@octokit/types': 16.0.0 7410 + universal-user-agent: 7.0.3 7411 + 7412 + '@octokit/oauth-app@8.0.3': 7413 + dependencies: 7414 + '@octokit/auth-oauth-app': 9.0.3 7415 + '@octokit/auth-oauth-user': 6.0.2 7416 + '@octokit/auth-unauthenticated': 7.0.3 7417 + '@octokit/core': 7.0.6 7418 + '@octokit/oauth-authorization-url': 8.0.0 7419 + '@octokit/oauth-methods': 6.0.2 7420 + '@types/aws-lambda': 8.10.161 7421 + universal-user-agent: 7.0.3 7422 + 7423 + '@octokit/oauth-authorization-url@8.0.0': {} 7424 + 7425 + '@octokit/oauth-methods@6.0.2': 7426 + dependencies: 7427 + '@octokit/oauth-authorization-url': 8.0.0 7428 + '@octokit/request': 10.0.8 7429 + '@octokit/request-error': 7.1.0 7430 + '@octokit/types': 16.0.0 7431 + 7432 + '@octokit/openapi-types@27.0.0': {} 7433 + 7434 + '@octokit/openapi-webhooks-types@12.1.0': {} 7435 + 7436 + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': 7437 + dependencies: 7438 + '@octokit/core': 7.0.6 7439 + '@octokit/types': 16.0.0 7440 + 7441 + '@octokit/request-error@7.1.0': 7442 + dependencies: 7443 + '@octokit/types': 16.0.0 7444 + 7445 + '@octokit/request@10.0.8': 7446 + dependencies: 7447 + '@octokit/endpoint': 11.0.3 7448 + '@octokit/request-error': 7.1.0 7449 + '@octokit/types': 16.0.0 7450 + fast-content-type-parse: 3.0.0 7451 + json-with-bigint: 3.5.8 7452 + universal-user-agent: 7.0.3 7453 + 7454 + '@octokit/types@16.0.0': 7455 + dependencies: 7456 + '@octokit/openapi-types': 27.0.0 7457 + 7218 7458 '@octokit/webhooks-methods@6.0.0': {} 7219 7459 7220 7460 '@octokit/webhooks-types@7.6.1': {} 7221 7461 7462 + '@octokit/webhooks@14.2.0': 7463 + dependencies: 7464 + '@octokit/openapi-webhooks-types': 12.1.0 7465 + '@octokit/request-error': 7.1.0 7466 + '@octokit/webhooks-methods': 6.0.0 7467 + 7222 7468 '@one-ini/wasm@0.1.1': {} 7223 7469 7224 7470 '@oxc-minify/binding-android-arm-eabi@0.128.0': ··· 7916 8162 dependencies: 7917 8163 tslib: 2.8.1 7918 8164 optional: true 8165 + 8166 + '@types/aws-lambda@8.10.161': {} 7919 8167 7920 8168 '@types/chai@5.2.3': 7921 8169 dependencies: ··· 8434 8682 8435 8683 baseline-browser-mapping@2.10.27: {} 8436 8684 8685 + before-after-hook@4.0.0: {} 8686 + 8437 8687 bindings@1.5.0: 8438 8688 dependencies: 8439 8689 file-uri-to-path: 1.0.0 ··· 9025 9275 exsolve@1.0.8: {} 9026 9276 9027 9277 fake-indexeddb@6.2.5: {} 9278 + 9279 + fast-content-type-parse@3.0.0: {} 9028 9280 9029 9281 fast-deep-equal@3.1.3: {} 9030 9282 ··· 9493 9745 9494 9746 json-stable-stringify-without-jsonify@1.0.1: {} 9495 9747 9748 + json-with-bigint@3.5.8: {} 9749 + 9496 9750 json5@2.2.3: {} 9497 9751 9498 9752 keyv@4.5.4: ··· 10971 11225 dependencies: 10972 11226 is-number: 7.0.0 10973 11227 11228 + toad-cache@3.7.0: {} 11229 + 10974 11230 toidentifier@1.0.1: {} 10975 11231 10976 11232 totalist@3.0.1: {} ··· 11057 11313 unplugin-utils: 0.3.1 11058 11314 optionalDependencies: 11059 11315 oxc-parser: 0.128.0 11316 + 11317 + universal-github-app-jwt@2.2.2: {} 11318 + 11319 + universal-user-agent@7.0.3: {} 11060 11320 11061 11321 unplugin-utils@0.3.1: 11062 11322 dependencies:
+8
server/api/atproto/callback.get.ts
··· 1 1 import { userIdentity } from '~~/server/db/schema' 2 + import { enqueue } from '~~/server/utils/queue' 2 3 import { generateAndPublishKey } from '~~/server/utils/tangled-pubkey' 3 4 4 5 export default defineEventHandler(async event => { ··· 32 33 oauthSession: session, 33 34 installationId, 34 35 }) 36 + 37 + // Backfill: enqueue a single job that walks the installation's repo list 38 + // and fans out per-repo enrolment. Doing this in the worker (rather than 39 + // inline here) keeps the OAuth callback fast regardless of repo count, and 40 + // gives us proper retry semantics if pagination doesn't finish in one 41 + // worker tick. 42 + await enqueue('tangled.backfill-installation', { installationId, page: 1 }) 35 43 36 44 await sendRedirect(event, '/dashboard', 302) 37 45 })
+31
server/utils/github-app.ts
··· 1 + import { App } from '@octokit/app' 2 + 3 + let cachedApp: App | undefined 4 + 5 + function useApp(): App { 6 + if (cachedApp) return cachedApp 7 + const appId = process.env.NUXT_GITHUB_APP_ID 8 + const privateKey = process.env.NUXT_GITHUB_APP_PRIVATE_KEY 9 + if (!appId || !privateKey) { 10 + throw new Error('NUXT_GITHUB_APP_ID and NUXT_GITHUB_APP_PRIVATE_KEY must be set') 11 + } 12 + cachedApp = new App({ 13 + appId, 14 + // Vercel env vars escape newlines; restore them so PEM parsing works. 15 + privateKey: privateKey.replaceAll('\\n', '\n'), 16 + }) 17 + return cachedApp 18 + } 19 + 20 + export type InstallationOctokit = Awaited<ReturnType<App['getInstallationOctokit']>> 21 + 22 + /** Get an Octokit pre-authed for a specific GitHub App installation. */ 23 + export async function installationOctokit(installationId: number): Promise<InstallationOctokit> { 24 + const app = useApp() 25 + return app.getInstallationOctokit(installationId) 26 + } 27 + 28 + /** Test hook. */ 29 + export function clearGitHubAppCache() { 30 + cachedApp = undefined 31 + }
+92 -7
server/utils/job-handlers.ts
··· 1 + import { sql } from 'drizzle-orm' 2 + import { userIdentity } from '../db/schema' 3 + import { useOAuthClient } from './atproto-oauth' 4 + import { useDb } from './db' 5 + import { installationOctokit } from './github-app' 1 6 import type { JobEnvelope } from './queue' 2 - import { useOAuthClient } from './atproto-oauth' 7 + import { enqueue } from './queue' 3 8 import { generateAndPublishKey } from './tangled-pubkey' 9 + import { enrollRepo } from './tangled-repo' 4 10 5 11 /** 6 - * Map of job kind → handler. Handlers are filled in by later commits: 7 - * - 'github.push' → commit 12 (sync push events) 8 - * - 'github.create' / 'github.delete' → commit 13 (branch/tag ref ops) 9 - * - 'github.repository' → commit 14/15 (description, lifecycle) 10 - * - 'tangled.create-repo' → commit 10 (initial enrolment) 11 - * - 'atproto.publish-pubkey' → this commit (key rotation) 12 + * Map of job kind → handler. Each commit fills in its slice: 13 + * - 'github.push' → commit 12 (sync push events) 14 + * - 'github.create' / 'github.delete' → commit 13 (branch/tag ref ops) 15 + * - 'github.repository' → commit 14/15 (description, lifecycle) 16 + * - 'github.installation_repositories' → this commit (fan-out enrolment) 17 + * - 'tangled.backfill-installation' → this commit (paginate + fan-out) 18 + * - 'tangled.create-repo' → this commit (per-repo enrolment) 19 + * - 'atproto.publish-pubkey' → commit 9 12 20 * 13 21 * Unknown kinds throw so they surface as job failures rather than silent 14 22 * acknowledgement. ··· 19 27 'github.delete', 20 28 'github.repository', 21 29 'github.installation_repositories', 30 + 'tangled.backfill-installation', 22 31 'tangled.create-repo', 23 32 'atproto.publish-pubkey', 24 33 ]) 34 + 35 + const BACKFILL_PAGE_SIZE = 100 25 36 26 37 interface PublishPubkeyPayload { 27 38 did: string 28 39 installationId: number 29 40 } 30 41 42 + interface CreateRepoPayload { 43 + installationId: number 44 + githubRepoId: number 45 + } 46 + 47 + interface InstallationRepositoriesPayload { 48 + installationId: number 49 + action: 'added' | 'removed' 50 + addedRepoIds: number[] 51 + removedRepoIds: number[] 52 + } 53 + 54 + interface BackfillInstallationPayload { 55 + installationId: number 56 + page: number 57 + } 58 + 31 59 export async function dispatch(envelope: JobEnvelope): Promise<void> { 32 60 if (!KNOWN_KINDS.has(envelope.kind)) { 33 61 throw new Error(`unknown job kind: ${envelope.kind}`) ··· 38 66 const client = await useOAuthClient() 39 67 const session = await client.restore(did) 40 68 await generateAndPublishKey({ oauthSession: session, installationId }) 69 + return 70 + } 71 + 72 + if (envelope.kind === 'tangled.create-repo') { 73 + const { installationId, githubRepoId } = envelope.payload as CreateRepoPayload 74 + 75 + // Find the user identity bound to this install. If OAuth hasn't completed 76 + // yet, drop this job silently \u2014 OAuth callback re-enqueues for all 77 + // accessible repos at completion time, so we'll get a fresh trigger. 78 + const db = useDb() 79 + const identity = await db.select({ did: userIdentity.did }) 80 + .from(userIdentity) 81 + .where(sql`${userIdentity.installationId} = ${installationId}`) 82 + if (identity.length === 0) return 83 + 84 + const client = await useOAuthClient() 85 + const session = await client.restore(identity[0]!.did) 86 + await enrollRepo({ oauthSession: session, installationId, githubRepoId }) 87 + return 88 + } 89 + 90 + if (envelope.kind === 'tangled.backfill-installation') { 91 + const { installationId, page } = envelope.payload as BackfillInstallationPayload 92 + const octokit = await installationOctokit(installationId) 93 + const { data } = await octokit.request('GET /installation/repositories', { 94 + per_page: BACKFILL_PAGE_SIZE, 95 + page, 96 + }) 97 + 98 + // Fan out one tangled.create-repo job per repo on this page. 99 + for (const repo of data.repositories) { 100 + // eslint-disable-next-line no-await-in-loop -- enqueue is sequential by design 101 + await enqueue('tangled.create-repo', { installationId, githubRepoId: repo.id }) 102 + } 103 + 104 + // If there are more pages, re-queue ourselves for the next one. This 105 + // keeps each tick small and bounded; an install with thousands of repos 106 + // walks through over many minutes rather than blocking one worker. 107 + const seenSoFar = (page - 1) * BACKFILL_PAGE_SIZE + data.repositories.length 108 + if (seenSoFar < data.total_count && data.repositories.length > 0) { 109 + await enqueue('tangled.backfill-installation', { installationId, page: page + 1 }) 110 + } 111 + return 112 + } 113 + 114 + if (envelope.kind === 'github.installation_repositories') { 115 + const { installationId, action, addedRepoIds } = envelope.payload as InstallationRepositoriesPayload 116 + if (action !== 'added') return 117 + 118 + // Fan out one tangled.create-repo job per added repo. The fan-out keeps 119 + // each unit small enough to fit comfortably in the per-job lease, lets 120 + // failures retry independently, and runs the OAuth precondition check 121 + // per repo (an install can outlive a tangled identity disconnection). 122 + for (const id of addedRepoIds) { 123 + // eslint-disable-next-line no-await-in-loop -- fan-out enqueue is sequential by design 124 + await enqueue('tangled.create-repo', { installationId, githubRepoId: id }) 125 + } 41 126 return 42 127 } 43 128
+132
server/utils/tangled-repo.ts
··· 1 + import { Agent } from '@atproto/api' 2 + import type { OAuthSession } from '@atproto/oauth-client-node' 3 + import { now as tidNow } from '@atcute/tid' 4 + import { sql } from 'drizzle-orm' 5 + import { repoMapping } from '../db/schema' 6 + import { useDb } from './db' 7 + import { installationOctokit } from './github-app' 8 + 9 + const REPO_LEXICON = 'sh.tangled.repo' 10 + const REPO_CREATE_NSID = 'sh.tangled.repo.create' 11 + 12 + /** 13 + * Default knot for users with no `sh.tangled.knot` records. PLAN.md "Open 14 + * questions" #1: confirm with the tangled team that this is the right 15 + * appview-hosted default. 16 + */ 17 + const DEFAULT_KNOT = 'knot1.tangled.sh' 18 + 19 + export interface EnrolResult { 20 + status: 'enrolled' | 'already' | 'skipped' 21 + reason?: 'private' | 'fork' | 'no-identity' 22 + } 23 + 24 + /** 25 + * Enroll a single GitHub repo on tangled. 26 + * 27 + * Flow: 28 + * 1. Skip if a `repo_mapping` row already exists. 29 + * 2. Fetch GitHub repo metadata via the install token. Skip private/fork. 30 + * 3. Pick a knot (user default → `DEFAULT_KNOT`). 31 + * 4. Get a service-auth JWT for `(aud=did:web:<knot>, lxm=sh.tangled.repo.create)`. 32 + * 5. POST to `https://<knot>/xrpc/sh.tangled.repo.create` with 33 + * `{ rkey, name, source, defaultBranch }`. The knot clones the repo from 34 + * `source` and mints a `repoDid`. 35 + * 6. Write a `sh.tangled.repo` record on the user's PDS. 36 + * 7. Insert the `repo_mapping` row. 37 + */ 38 + export async function enrollRepo(opts: { 39 + oauthSession: OAuthSession 40 + installationId: number 41 + githubRepoId: number 42 + }): Promise<EnrolResult> { 43 + const db = useDb() 44 + 45 + const existing = await db.select({ id: repoMapping.id }) 46 + .from(repoMapping) 47 + .where(sql`${repoMapping.installationId} = ${opts.installationId} AND ${repoMapping.githubRepoId} = ${opts.githubRepoId}`) 48 + if (existing.length > 0) { 49 + return { status: 'already' } 50 + } 51 + 52 + // 1. GitHub repo metadata. 53 + const octokit = await installationOctokit(opts.installationId) 54 + const { data: repo } = await octokit.request('GET /repositories/{repository_id}', { 55 + repository_id: opts.githubRepoId, 56 + }) 57 + 58 + if (repo.private) return { status: 'skipped', reason: 'private' } 59 + if (repo.fork) return { status: 'skipped', reason: 'fork' } 60 + 61 + const [owner, name] = repo.full_name.split('/') 62 + if (!owner || !name) { 63 + throw new Error(`unexpected github full_name shape: ${repo.full_name}`) 64 + } 65 + 66 + // 2. Pick a knot. Users *can* configure additional knots; v1 always uses 67 + // the default. Wiring user choice through is dashboard work. 68 + const knot = DEFAULT_KNOT 69 + 70 + // 3. Service-auth JWT for the knot procedure. 71 + const agent = new Agent(opts.oauthSession) 72 + const aud = `did:web:${knot}` 73 + const exp = Math.floor(Date.now() / 1000) + 60 74 + const { data: { token } } = await agent.com.atproto.server.getServiceAuth({ 75 + aud, 76 + lxm: REPO_CREATE_NSID, 77 + exp, 78 + }) 79 + 80 + // 4. Knot procedure call. Tangled mints a repoDid here and starts cloning 81 + // from `source`. 82 + const rkey = tidNow() 83 + const sourceUrl = `https://github.com/${owner}/${name}` 84 + const knotResponse = await fetch(`https://${knot}/xrpc/${REPO_CREATE_NSID}`, { 85 + method: 'POST', 86 + headers: { 87 + 'authorization': `Bearer ${token}`, 88 + 'content-type': 'application/json', 89 + }, 90 + body: JSON.stringify({ 91 + rkey, 92 + name, 93 + source: sourceUrl, 94 + defaultBranch: repo.default_branch, 95 + }), 96 + }) 97 + if (!knotResponse.ok) { 98 + const body = await knotResponse.text() 99 + throw new Error(`knot ${knot} returned ${knotResponse.status}: ${body}`) 100 + } 101 + const { repoDid } = await knotResponse.json() as { repoDid?: string } 102 + if (!repoDid) { 103 + throw new Error(`knot ${knot} returned no repoDid`) 104 + } 105 + 106 + // 5. PDS record so the appview firehose discovers the repo. 107 + await agent.com.atproto.repo.putRecord({ 108 + repo: opts.oauthSession.did, 109 + collection: REPO_LEXICON, 110 + rkey, 111 + record: { 112 + $type: REPO_LEXICON, 113 + name, 114 + knot, 115 + repoDid, 116 + createdAt: new Date().toISOString(), 117 + }, 118 + }) 119 + 120 + // 6. Persist mapping. 121 + await db.insert(repoMapping).values({ 122 + installationId: opts.installationId, 123 + githubRepoId: opts.githubRepoId, 124 + githubFullName: repo.full_name, 125 + tangledRepoDid: repoDid, 126 + tangledFullName: `${opts.oauthSession.did}/${name}`, 127 + knot, 128 + status: 'active', 129 + }) 130 + 131 + return { status: 'enrolled' } 132 + }
+193
test/unit/tangled-repo.spec.ts
··· 1 + import crypto from 'node:crypto' 2 + import { sql } from 'drizzle-orm' 3 + import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 4 + import { installation, repoMapping } from '../../server/db/schema' 5 + import { clearDb, setDb, useDb } from '../../server/utils/db' 6 + import { clearEncryptionKeyCache } from '../../server/utils/encryption' 7 + import { enrollRepo } from '../../server/utils/tangled-repo' 8 + import { createTestDb } from '../utils/db' 9 + 10 + const ORIGINAL_ENC_KEY = process.env.NUXT_ENCRYPTION_KEY 11 + 12 + interface GithubRepoLike { 13 + id: number 14 + full_name: string 15 + private: boolean 16 + fork: boolean 17 + default_branch: string 18 + } 19 + 20 + const githubGet = vi.fn<(input: { repository_id: number }) => Promise<{ data: GithubRepoLike }>>() 21 + const getServiceAuthMock = vi.fn<(input: { aud: string, lxm: string, exp: number }) => Promise<{ data: { token: string } }>>() 22 + const putRecordMock = vi.fn<(input: { repo: string, collection: string, rkey: string, record: Record<string, unknown> }) => Promise<unknown>>() 23 + 24 + vi.mock('@atproto/api', () => ({ 25 + Agent: class { 26 + com = { 27 + atproto: { 28 + server: { getServiceAuth: getServiceAuthMock }, 29 + repo: { putRecord: putRecordMock }, 30 + }, 31 + } 32 + }, 33 + })) 34 + 35 + vi.mock('../../server/utils/github-app', () => ({ 36 + installationOctokit: async () => ({ 37 + request: githubGet, 38 + }), 39 + clearGitHubAppCache: () => {}, 40 + })) 41 + 42 + const fakeFetch = vi.fn<(url: string, init: RequestInit) => Promise<Response>>() 43 + const ORIGINAL_FETCH = globalThis.fetch 44 + 45 + describe('enrollRepo', () => { 46 + beforeEach(async () => { 47 + process.env.NUXT_ENCRYPTION_KEY = crypto.randomBytes(32).toString('base64') 48 + clearEncryptionKeyCache() 49 + 50 + setDb(await createTestDb()) 51 + await useDb().insert(installation).values({ 52 + id: 1, accountLogin: 'alice', accountId: 100, accountType: 'User', 53 + }) 54 + 55 + githubGet.mockReset() 56 + getServiceAuthMock.mockReset() 57 + putRecordMock.mockReset() 58 + fakeFetch.mockReset() 59 + globalThis.fetch = fakeFetch as unknown as typeof fetch 60 + 61 + getServiceAuthMock.mockResolvedValue({ data: { token: 'service-auth-jwt' } }) 62 + putRecordMock.mockResolvedValue({ data: { uri: 'at://did:plc:abc/sh.tangled.repo/whatever', cid: 'bafy' } }) 63 + }) 64 + 65 + afterEach(() => { 66 + globalThis.fetch = ORIGINAL_FETCH 67 + if (ORIGINAL_ENC_KEY === undefined) delete process.env.NUXT_ENCRYPTION_KEY 68 + else process.env.NUXT_ENCRYPTION_KEY = ORIGINAL_ENC_KEY 69 + clearEncryptionKeyCache() 70 + clearDb() 71 + }) 72 + 73 + function fakeOauthSession(did: string) { 74 + return { did } as never 75 + } 76 + 77 + function ghRepo(over: Partial<GithubRepoLike> = {}): GithubRepoLike { 78 + return { 79 + id: 9001, 80 + full_name: 'alice/my-project', 81 + private: false, 82 + fork: false, 83 + default_branch: 'main', 84 + ...over, 85 + } 86 + } 87 + 88 + it('enrolls a public, non-fork repo end to end', async () => { 89 + githubGet.mockResolvedValue({ data: ghRepo() }) 90 + fakeFetch.mockResolvedValue(new Response( 91 + JSON.stringify({ repoDid: 'did:plc:repo-xyz' }), 92 + { status: 200 }, 93 + )) 94 + 95 + const result = await enrollRepo({ 96 + oauthSession: fakeOauthSession('did:plc:abc'), 97 + installationId: 1, 98 + githubRepoId: 9001, 99 + }) 100 + expect(result.status).toBe('enrolled') 101 + 102 + // Service auth requested with the right shape. 103 + expect(getServiceAuthMock).toHaveBeenCalledTimes(1) 104 + const sa = getServiceAuthMock.mock.calls[0]?.[0] 105 + expect(sa?.aud).toBe('did:web:knot1.tangled.sh') 106 + expect(sa?.lxm).toBe('sh.tangled.repo.create') 107 + 108 + // Knot procedure invoked with source URL and rkey. 109 + expect(fakeFetch).toHaveBeenCalledTimes(1) 110 + const fetchCall = fakeFetch.mock.calls[0] 111 + const url = fetchCall?.[0] 112 + const init = fetchCall?.[1] 113 + expect(url).toBe('https://knot1.tangled.sh/xrpc/sh.tangled.repo.create') 114 + expect((init!.headers as Record<string, string>).authorization).toBe('Bearer service-auth-jwt') 115 + const body = JSON.parse(init!.body as string) as Record<string, unknown> 116 + expect(body.name).toBe('my-project') 117 + expect(body.source).toBe('https://github.com/alice/my-project') 118 + expect(body.defaultBranch).toBe('main') 119 + expect(typeof body.rkey).toBe('string') 120 + 121 + // PDS record written with the same rkey. 122 + expect(putRecordMock).toHaveBeenCalledTimes(1) 123 + const put = putRecordMock.mock.calls[0]?.[0] 124 + expect(put?.rkey).toBe(body.rkey) 125 + expect(put?.record.repoDid).toBe('did:plc:repo-xyz') 126 + expect(put?.record.knot).toBe('knot1.tangled.sh') 127 + 128 + // Mapping persisted. 129 + const rows = await useDb().select().from(repoMapping) 130 + .where(sql`${repoMapping.installationId} = 1`) 131 + expect(rows).toHaveLength(1) 132 + expect(rows[0]!.tangledRepoDid).toBe('did:plc:repo-xyz') 133 + expect(rows[0]!.knot).toBe('knot1.tangled.sh') 134 + expect(rows[0]!.status).toBe('active') 135 + }) 136 + 137 + it('skips private repos', async () => { 138 + githubGet.mockResolvedValue({ data: ghRepo({ private: true }) }) 139 + 140 + const result = await enrollRepo({ 141 + oauthSession: fakeOauthSession('did:plc:abc'), 142 + installationId: 1, 143 + githubRepoId: 9001, 144 + }) 145 + expect(result).toEqual({ status: 'skipped', reason: 'private' }) 146 + expect(fakeFetch).not.toHaveBeenCalled() 147 + expect(putRecordMock).not.toHaveBeenCalled() 148 + expect(await useDb().select().from(repoMapping)).toHaveLength(0) 149 + }) 150 + 151 + it('skips forks', async () => { 152 + githubGet.mockResolvedValue({ data: ghRepo({ fork: true }) }) 153 + 154 + const result = await enrollRepo({ 155 + oauthSession: fakeOauthSession('did:plc:abc'), 156 + installationId: 1, 157 + githubRepoId: 9001, 158 + }) 159 + expect(result).toEqual({ status: 'skipped', reason: 'fork' }) 160 + expect(fakeFetch).not.toHaveBeenCalled() 161 + }) 162 + 163 + it('no-ops if a mapping already exists', async () => { 164 + await useDb().insert(repoMapping).values({ 165 + installationId: 1, 166 + githubRepoId: 9001, 167 + githubFullName: 'alice/my-project', 168 + status: 'active', 169 + }) 170 + 171 + const result = await enrollRepo({ 172 + oauthSession: fakeOauthSession('did:plc:abc'), 173 + installationId: 1, 174 + githubRepoId: 9001, 175 + }) 176 + expect(result).toEqual({ status: 'already' }) 177 + expect(githubGet).not.toHaveBeenCalled() 178 + }) 179 + 180 + it('throws and writes nothing if the knot rejects the procedure', async () => { 181 + githubGet.mockResolvedValue({ data: ghRepo() }) 182 + fakeFetch.mockResolvedValue(new Response('nope', { status: 500 })) 183 + 184 + await expect(enrollRepo({ 185 + oauthSession: fakeOauthSession('did:plc:abc'), 186 + installationId: 1, 187 + githubRepoId: 9001, 188 + })).rejects.toThrow(/knot1\.tangled\.sh returned 500/) 189 + 190 + expect(putRecordMock).not.toHaveBeenCalled() 191 + expect(await useDb().select().from(repoMapping)).toHaveLength(0) 192 + }) 193 + })