six-degrees 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1729) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -0
  2. package/.next/standalone/.next/app-path-routes-manifest.json +31 -0
  3. package/.next/standalone/.next/build-manifest.json +24 -0
  4. package/.next/standalone/.next/package.json +1 -0
  5. package/.next/standalone/.next/prerender-manifest.json +297 -0
  6. package/.next/standalone/.next/required-server-files.json +371 -0
  7. package/.next/standalone/.next/routes-manifest.json +222 -0
  8. package/.next/standalone/.next/server/app/_global-error/page/app-paths-manifest.json +3 -0
  9. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +20 -0
  10. package/.next/standalone/.next/server/app/_global-error/page/next-font-manifest.json +6 -0
  11. package/.next/standalone/.next/server/app/_global-error/page/react-loadable-manifest.json +1 -0
  12. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +4 -0
  13. package/.next/standalone/.next/server/app/_global-error/page.js +9 -0
  14. package/.next/standalone/.next/server/app/_global-error/page.js.map +5 -0
  15. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -0
  16. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +3 -0
  17. package/.next/standalone/.next/server/app/_global-error.html +1 -0
  18. package/.next/standalone/.next/server/app/_global-error.meta +22 -0
  19. package/.next/standalone/.next/server/app/_global-error.rsc +15 -0
  20. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +20 -0
  21. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +15 -0
  22. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
  23. package/.next/standalone/.next/server/app/_not-found/page/app-paths-manifest.json +3 -0
  24. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +20 -0
  25. package/.next/standalone/.next/server/app/_not-found/page/next-font-manifest.json +11 -0
  26. package/.next/standalone/.next/server/app/_not-found/page/react-loadable-manifest.json +1 -0
  27. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +4 -0
  28. package/.next/standalone/.next/server/app/_not-found/page.js +12 -0
  29. package/.next/standalone/.next/server/app/_not-found/page.js.map +5 -0
  30. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -0
  31. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +3 -0
  32. package/.next/standalone/.next/server/app/_not-found.html +1 -0
  33. package/.next/standalone/.next/server/app/_not-found.meta +27 -0
  34. package/.next/standalone/.next/server/app/_not-found.rsc +18 -0
  35. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +18 -0
  36. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -0
  37. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -0
  38. package/.next/standalone/.next/server/app/api/admin-delete/route/app-paths-manifest.json +3 -0
  39. package/.next/standalone/.next/server/app/api/admin-delete/route/build-manifest.json +12 -0
  40. package/.next/standalone/.next/server/app/api/admin-delete/route/server-reference-manifest.json +4 -0
  41. package/.next/standalone/.next/server/app/api/admin-delete/route.js +7 -0
  42. package/.next/standalone/.next/server/app/api/admin-delete/route.js.map +5 -0
  43. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -0
  44. package/.next/standalone/.next/server/app/api/admin-delete/route_client-reference-manifest.js +3 -0
  45. package/.next/standalone/.next/server/app/api/admin-update/route/app-paths-manifest.json +3 -0
  46. package/.next/standalone/.next/server/app/api/admin-update/route/build-manifest.json +12 -0
  47. package/.next/standalone/.next/server/app/api/admin-update/route/server-reference-manifest.json +4 -0
  48. package/.next/standalone/.next/server/app/api/admin-update/route.js +7 -0
  49. package/.next/standalone/.next/server/app/api/admin-update/route.js.map +5 -0
  50. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -0
  51. package/.next/standalone/.next/server/app/api/admin-update/route_client-reference-manifest.js +3 -0
  52. package/.next/standalone/.next/server/app/api/bridges/route/app-paths-manifest.json +3 -0
  53. package/.next/standalone/.next/server/app/api/bridges/route/build-manifest.json +12 -0
  54. package/.next/standalone/.next/server/app/api/bridges/route/server-reference-manifest.json +4 -0
  55. package/.next/standalone/.next/server/app/api/bridges/route.js +7 -0
  56. package/.next/standalone/.next/server/app/api/bridges/route.js.map +5 -0
  57. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -0
  58. package/.next/standalone/.next/server/app/api/bridges/route_client-reference-manifest.js +3 -0
  59. package/.next/standalone/.next/server/app/api/bulk-import/route/app-paths-manifest.json +3 -0
  60. package/.next/standalone/.next/server/app/api/bulk-import/route/build-manifest.json +12 -0
  61. package/.next/standalone/.next/server/app/api/bulk-import/route/server-reference-manifest.json +4 -0
  62. package/.next/standalone/.next/server/app/api/bulk-import/route.js +7 -0
  63. package/.next/standalone/.next/server/app/api/bulk-import/route.js.map +5 -0
  64. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -0
  65. package/.next/standalone/.next/server/app/api/bulk-import/route_client-reference-manifest.js +3 -0
  66. package/.next/standalone/.next/server/app/api/company-scores/route/app-paths-manifest.json +3 -0
  67. package/.next/standalone/.next/server/app/api/company-scores/route/build-manifest.json +12 -0
  68. package/.next/standalone/.next/server/app/api/company-scores/route/server-reference-manifest.json +4 -0
  69. package/.next/standalone/.next/server/app/api/company-scores/route.js +7 -0
  70. package/.next/standalone/.next/server/app/api/company-scores/route.js.map +5 -0
  71. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -0
  72. package/.next/standalone/.next/server/app/api/company-scores/route_client-reference-manifest.js +3 -0
  73. package/.next/standalone/.next/server/app/api/connections/route/app-paths-manifest.json +3 -0
  74. package/.next/standalone/.next/server/app/api/connections/route/build-manifest.json +12 -0
  75. package/.next/standalone/.next/server/app/api/connections/route/server-reference-manifest.json +4 -0
  76. package/.next/standalone/.next/server/app/api/connections/route.js +7 -0
  77. package/.next/standalone/.next/server/app/api/connections/route.js.map +5 -0
  78. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -0
  79. package/.next/standalone/.next/server/app/api/connections/route_client-reference-manifest.js +3 -0
  80. package/.next/standalone/.next/server/app/api/delete-cluster/route/app-paths-manifest.json +3 -0
  81. package/.next/standalone/.next/server/app/api/delete-cluster/route/build-manifest.json +12 -0
  82. package/.next/standalone/.next/server/app/api/delete-cluster/route/server-reference-manifest.json +4 -0
  83. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +7 -0
  84. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.map +5 -0
  85. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -0
  86. package/.next/standalone/.next/server/app/api/delete-cluster/route_client-reference-manifest.js +3 -0
  87. package/.next/standalone/.next/server/app/api/ingest/route/app-paths-manifest.json +3 -0
  88. package/.next/standalone/.next/server/app/api/ingest/route/build-manifest.json +12 -0
  89. package/.next/standalone/.next/server/app/api/ingest/route/server-reference-manifest.json +4 -0
  90. package/.next/standalone/.next/server/app/api/ingest/route.js +8 -0
  91. package/.next/standalone/.next/server/app/api/ingest/route.js.map +5 -0
  92. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -0
  93. package/.next/standalone/.next/server/app/api/ingest/route_client-reference-manifest.js +3 -0
  94. package/.next/standalone/.next/server/app/api/network/route/app-paths-manifest.json +3 -0
  95. package/.next/standalone/.next/server/app/api/network/route/build-manifest.json +12 -0
  96. package/.next/standalone/.next/server/app/api/network/route/server-reference-manifest.json +4 -0
  97. package/.next/standalone/.next/server/app/api/network/route.js +7 -0
  98. package/.next/standalone/.next/server/app/api/network/route.js.map +5 -0
  99. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -0
  100. package/.next/standalone/.next/server/app/api/network/route_client-reference-manifest.js +3 -0
  101. package/.next/standalone/.next/server/app/api/notifications/route/app-paths-manifest.json +3 -0
  102. package/.next/standalone/.next/server/app/api/notifications/route/build-manifest.json +12 -0
  103. package/.next/standalone/.next/server/app/api/notifications/route/server-reference-manifest.json +4 -0
  104. package/.next/standalone/.next/server/app/api/notifications/route.js +7 -0
  105. package/.next/standalone/.next/server/app/api/notifications/route.js.map +5 -0
  106. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -0
  107. package/.next/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +3 -0
  108. package/.next/standalone/.next/server/app/api/outreach/route/app-paths-manifest.json +3 -0
  109. package/.next/standalone/.next/server/app/api/outreach/route/build-manifest.json +12 -0
  110. package/.next/standalone/.next/server/app/api/outreach/route/server-reference-manifest.json +4 -0
  111. package/.next/standalone/.next/server/app/api/outreach/route.js +7 -0
  112. package/.next/standalone/.next/server/app/api/outreach/route.js.map +5 -0
  113. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -0
  114. package/.next/standalone/.next/server/app/api/outreach/route_client-reference-manifest.js +3 -0
  115. package/.next/standalone/.next/server/app/api/queue/route/app-paths-manifest.json +3 -0
  116. package/.next/standalone/.next/server/app/api/queue/route/build-manifest.json +12 -0
  117. package/.next/standalone/.next/server/app/api/queue/route/server-reference-manifest.json +4 -0
  118. package/.next/standalone/.next/server/app/api/queue/route.js +7 -0
  119. package/.next/standalone/.next/server/app/api/queue/route.js.map +5 -0
  120. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -0
  121. package/.next/standalone/.next/server/app/api/queue/route_client-reference-manifest.js +3 -0
  122. package/.next/standalone/.next/server/app/api/scraper/route/app-paths-manifest.json +3 -0
  123. package/.next/standalone/.next/server/app/api/scraper/route/build-manifest.json +12 -0
  124. package/.next/standalone/.next/server/app/api/scraper/route/server-reference-manifest.json +4 -0
  125. package/.next/standalone/.next/server/app/api/scraper/route.js +7 -0
  126. package/.next/standalone/.next/server/app/api/scraper/route.js.map +5 -0
  127. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -0
  128. package/.next/standalone/.next/server/app/api/scraper/route_client-reference-manifest.js +3 -0
  129. package/.next/standalone/.next/server/app/api/setup-profile/route/app-paths-manifest.json +3 -0
  130. package/.next/standalone/.next/server/app/api/setup-profile/route/build-manifest.json +12 -0
  131. package/.next/standalone/.next/server/app/api/setup-profile/route/server-reference-manifest.json +4 -0
  132. package/.next/standalone/.next/server/app/api/setup-profile/route.js +7 -0
  133. package/.next/standalone/.next/server/app/api/setup-profile/route.js.map +5 -0
  134. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -0
  135. package/.next/standalone/.next/server/app/api/setup-profile/route_client-reference-manifest.js +3 -0
  136. package/.next/standalone/.next/server/app/api/unlock/route/app-paths-manifest.json +3 -0
  137. package/.next/standalone/.next/server/app/api/unlock/route/build-manifest.json +12 -0
  138. package/.next/standalone/.next/server/app/api/unlock/route/server-reference-manifest.json +4 -0
  139. package/.next/standalone/.next/server/app/api/unlock/route.js +7 -0
  140. package/.next/standalone/.next/server/app/api/unlock/route.js.map +5 -0
  141. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -0
  142. package/.next/standalone/.next/server/app/api/unlock/route_client-reference-manifest.js +3 -0
  143. package/.next/standalone/.next/server/app/api/update/route/app-paths-manifest.json +3 -0
  144. package/.next/standalone/.next/server/app/api/update/route/build-manifest.json +12 -0
  145. package/.next/standalone/.next/server/app/api/update/route/server-reference-manifest.json +4 -0
  146. package/.next/standalone/.next/server/app/api/update/route.js +7 -0
  147. package/.next/standalone/.next/server/app/api/update/route.js.map +5 -0
  148. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -0
  149. package/.next/standalone/.next/server/app/api/update/route_client-reference-manifest.js +3 -0
  150. package/.next/standalone/.next/server/app/api/update-images/route/app-paths-manifest.json +3 -0
  151. package/.next/standalone/.next/server/app/api/update-images/route/build-manifest.json +12 -0
  152. package/.next/standalone/.next/server/app/api/update-images/route/server-reference-manifest.json +4 -0
  153. package/.next/standalone/.next/server/app/api/update-images/route.js +7 -0
  154. package/.next/standalone/.next/server/app/api/update-images/route.js.map +5 -0
  155. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -0
  156. package/.next/standalone/.next/server/app/api/update-images/route_client-reference-manifest.js +3 -0
  157. package/.next/standalone/.next/server/app/api/users/route/app-paths-manifest.json +3 -0
  158. package/.next/standalone/.next/server/app/api/users/route/build-manifest.json +12 -0
  159. package/.next/standalone/.next/server/app/api/users/route/server-reference-manifest.json +4 -0
  160. package/.next/standalone/.next/server/app/api/users/route.js +7 -0
  161. package/.next/standalone/.next/server/app/api/users/route.js.map +5 -0
  162. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -0
  163. package/.next/standalone/.next/server/app/api/users/route_client-reference-manifest.js +3 -0
  164. package/.next/standalone/.next/server/app/avatars/[file]/route/app-paths-manifest.json +3 -0
  165. package/.next/standalone/.next/server/app/avatars/[file]/route/build-manifest.json +12 -0
  166. package/.next/standalone/.next/server/app/avatars/[file]/route/server-reference-manifest.json +4 -0
  167. package/.next/standalone/.next/server/app/avatars/[file]/route.js +6 -0
  168. package/.next/standalone/.next/server/app/avatars/[file]/route.js.map +5 -0
  169. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -0
  170. package/.next/standalone/.next/server/app/avatars/[file]/route_client-reference-manifest.js +3 -0
  171. package/.next/standalone/.next/server/app/favicon.ico/route/app-paths-manifest.json +3 -0
  172. package/.next/standalone/.next/server/app/favicon.ico/route/build-manifest.json +12 -0
  173. package/.next/standalone/.next/server/app/favicon.ico/route.js +7 -0
  174. package/.next/standalone/.next/server/app/favicon.ico/route.js.map +5 -0
  175. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -0
  176. package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
  177. package/.next/standalone/.next/server/app/favicon.ico.meta +1 -0
  178. package/.next/standalone/.next/server/app/import/page/app-paths-manifest.json +3 -0
  179. package/.next/standalone/.next/server/app/import/page/build-manifest.json +20 -0
  180. package/.next/standalone/.next/server/app/import/page/next-font-manifest.json +11 -0
  181. package/.next/standalone/.next/server/app/import/page/react-loadable-manifest.json +1 -0
  182. package/.next/standalone/.next/server/app/import/page/server-reference-manifest.json +4 -0
  183. package/.next/standalone/.next/server/app/import/page.js +13 -0
  184. package/.next/standalone/.next/server/app/import/page.js.map +5 -0
  185. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -0
  186. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +3 -0
  187. package/.next/standalone/.next/server/app/import.html +1 -0
  188. package/.next/standalone/.next/server/app/import.meta +26 -0
  189. package/.next/standalone/.next/server/app/import.rsc +24 -0
  190. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +24 -0
  191. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +4 -0
  192. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +28 -0
  193. package/.next/standalone/.next/server/app/index.html +1 -0
  194. package/.next/standalone/.next/server/app/index.meta +21 -0
  195. package/.next/standalone/.next/server/app/index.rsc +22 -0
  196. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -0
  197. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +22 -0
  198. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +4 -0
  199. package/.next/standalone/.next/server/app/launch/page/app-paths-manifest.json +3 -0
  200. package/.next/standalone/.next/server/app/launch/page/build-manifest.json +20 -0
  201. package/.next/standalone/.next/server/app/launch/page/next-font-manifest.json +11 -0
  202. package/.next/standalone/.next/server/app/launch/page/react-loadable-manifest.json +1 -0
  203. package/.next/standalone/.next/server/app/launch/page/server-reference-manifest.json +4 -0
  204. package/.next/standalone/.next/server/app/launch/page.js +13 -0
  205. package/.next/standalone/.next/server/app/launch/page.js.map +5 -0
  206. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -0
  207. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +3 -0
  208. package/.next/standalone/.next/server/app/launch.html +44 -0
  209. package/.next/standalone/.next/server/app/launch.meta +26 -0
  210. package/.next/standalone/.next/server/app/launch.rsc +24 -0
  211. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +24 -0
  212. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +4 -0
  213. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +28 -0
  214. package/.next/standalone/.next/server/app/page/app-paths-manifest.json +3 -0
  215. package/.next/standalone/.next/server/app/page/build-manifest.json +20 -0
  216. package/.next/standalone/.next/server/app/page/next-font-manifest.json +11 -0
  217. package/.next/standalone/.next/server/app/page/react-loadable-manifest.json +1 -0
  218. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +4 -0
  219. package/.next/standalone/.next/server/app/page.js +13 -0
  220. package/.next/standalone/.next/server/app/page.js.map +5 -0
  221. package/.next/standalone/.next/server/app/page.js.nft.json +1 -0
  222. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +3 -0
  223. package/.next/standalone/.next/server/app/paths/page/app-paths-manifest.json +3 -0
  224. package/.next/standalone/.next/server/app/paths/page/build-manifest.json +20 -0
  225. package/.next/standalone/.next/server/app/paths/page/next-font-manifest.json +11 -0
  226. package/.next/standalone/.next/server/app/paths/page/react-loadable-manifest.json +1 -0
  227. package/.next/standalone/.next/server/app/paths/page/server-reference-manifest.json +4 -0
  228. package/.next/standalone/.next/server/app/paths/page.js +13 -0
  229. package/.next/standalone/.next/server/app/paths/page.js.map +5 -0
  230. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -0
  231. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +3 -0
  232. package/.next/standalone/.next/server/app/paths.html +1 -0
  233. package/.next/standalone/.next/server/app/paths.meta +26 -0
  234. package/.next/standalone/.next/server/app/paths.rsc +24 -0
  235. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +24 -0
  236. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +4 -0
  237. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +28 -0
  238. package/.next/standalone/.next/server/app/profile/page/app-paths-manifest.json +3 -0
  239. package/.next/standalone/.next/server/app/profile/page/build-manifest.json +20 -0
  240. package/.next/standalone/.next/server/app/profile/page/next-font-manifest.json +11 -0
  241. package/.next/standalone/.next/server/app/profile/page/react-loadable-manifest.json +1 -0
  242. package/.next/standalone/.next/server/app/profile/page/server-reference-manifest.json +4 -0
  243. package/.next/standalone/.next/server/app/profile/page.js +13 -0
  244. package/.next/standalone/.next/server/app/profile/page.js.map +5 -0
  245. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -0
  246. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +3 -0
  247. package/.next/standalone/.next/server/app/profile.html +1 -0
  248. package/.next/standalone/.next/server/app/profile.meta +26 -0
  249. package/.next/standalone/.next/server/app/profile.rsc +24 -0
  250. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +24 -0
  251. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +4 -0
  252. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +28 -0
  253. package/.next/standalone/.next/server/app/queue/page/app-paths-manifest.json +3 -0
  254. package/.next/standalone/.next/server/app/queue/page/build-manifest.json +20 -0
  255. package/.next/standalone/.next/server/app/queue/page/next-font-manifest.json +11 -0
  256. package/.next/standalone/.next/server/app/queue/page/react-loadable-manifest.json +1 -0
  257. package/.next/standalone/.next/server/app/queue/page/server-reference-manifest.json +4 -0
  258. package/.next/standalone/.next/server/app/queue/page.js +13 -0
  259. package/.next/standalone/.next/server/app/queue/page.js.map +5 -0
  260. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -0
  261. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +3 -0
  262. package/.next/standalone/.next/server/app/queue.html +1 -0
  263. package/.next/standalone/.next/server/app/queue.meta +26 -0
  264. package/.next/standalone/.next/server/app/queue.rsc +24 -0
  265. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +24 -0
  266. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +4 -0
  267. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +28 -0
  268. package/.next/standalone/.next/server/app/setup/page/app-paths-manifest.json +3 -0
  269. package/.next/standalone/.next/server/app/setup/page/build-manifest.json +20 -0
  270. package/.next/standalone/.next/server/app/setup/page/next-font-manifest.json +11 -0
  271. package/.next/standalone/.next/server/app/setup/page/react-loadable-manifest.json +1 -0
  272. package/.next/standalone/.next/server/app/setup/page/server-reference-manifest.json +4 -0
  273. package/.next/standalone/.next/server/app/setup/page.js +13 -0
  274. package/.next/standalone/.next/server/app/setup/page.js.map +5 -0
  275. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -0
  276. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +3 -0
  277. package/.next/standalone/.next/server/app/setup.html +1 -0
  278. package/.next/standalone/.next/server/app/setup.meta +26 -0
  279. package/.next/standalone/.next/server/app/setup.rsc +24 -0
  280. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +24 -0
  281. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +4 -0
  282. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +28 -0
  283. package/.next/standalone/.next/server/app-paths-manifest.json +31 -0
  284. package/.next/standalone/.next/server/chunks/[externals]__1pd1lps._.js +3 -0
  285. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +150 -0
  286. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +151 -0
  287. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15bjnu5._.js +5 -0
  288. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +150 -0
  289. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +155 -0
  290. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zor0ys._.js +3 -0
  291. package/.next/standalone/.next/server/chunks/[turbopack]_runtime.js +807 -0
  292. package/.next/standalone/.next/server/chunks/_0_rp-01._.js +3 -0
  293. package/.next/standalone/.next/server/chunks/_0hzdk79._.js +3 -0
  294. package/.next/standalone/.next/server/chunks/_0ow7u5r._.js +3 -0
  295. package/.next/standalone/.next/server/chunks/_0t0zc2d._.js +3 -0
  296. package/.next/standalone/.next/server/chunks/_0wa631d._.js +3 -0
  297. package/.next/standalone/.next/server/chunks/_13ccnao._.js +3 -0
  298. package/.next/standalone/.next/server/chunks/_13es063._.js +3 -0
  299. package/.next/standalone/.next/server/chunks/_142872j._.js +3 -0
  300. package/.next/standalone/.next/server/chunks/_18wsb_a._.js +8 -0
  301. package/.next/standalone/.next/server/chunks/_1_fynia._.js +5 -0
  302. package/.next/standalone/.next/server/chunks/_1e-4h5h._.js +3 -0
  303. package/.next/standalone/.next/server/chunks/_1gvvd7n._.js +158 -0
  304. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +3 -0
  305. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +3 -0
  306. package/.next/standalone/.next/server/chunks/_1moda66._.js +3 -0
  307. package/.next/standalone/.next/server/chunks/_1p6ghzv._.js +3 -0
  308. package/.next/standalone/.next/server/chunks/_1xmmi_v._.js +3 -0
  309. package/.next/standalone/.next/server/chunks/_1z-yrlf._.js +3 -0
  310. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_admin-delete_route_actions_1p93wag.js +3 -0
  311. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_admin-update_route_actions_1sf7bsv.js +3 -0
  312. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_bridges_route_actions_1n3pety.js +3 -0
  313. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_bulk-import_route_actions_10b3t1k.js +3 -0
  314. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_company-scores_route_actions_0qanhj-.js +3 -0
  315. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_connections_route_actions_20wls3v.js +3 -0
  316. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_delete-cluster_route_actions_0v9iazl.js +3 -0
  317. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_ingest_route_actions_1oz_9xc.js +3 -0
  318. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_network_route_actions_1r570mb.js +3 -0
  319. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_notifications_route_actions_1272ib8.js +3 -0
  320. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_outreach_route_actions_1ze1hrq.js +3 -0
  321. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_queue_route_actions_099d1-o.js +3 -0
  322. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_scraper_route_actions_0d98z8s.js +3 -0
  323. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_setup-profile_route_actions_0nk95pf.js +3 -0
  324. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_unlock_route_actions_0yzwsc4.js +3 -0
  325. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_update-images_route_actions_1rk6h3z.js +3 -0
  326. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_update_route_actions_0tvsuhe.js +3 -0
  327. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_users_route_actions_0mvbx1f.js +3 -0
  328. package/.next/standalone/.next/server/chunks/_next-internal_server_app_avatars_[file]_route_actions_0177eip.js +3 -0
  329. package/.next/standalone/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_0g2jjls.js +3 -0
  330. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -0
  331. package/.next/standalone/.next/server/chunks/lib_rpc_0mc73lk.js +3 -0
  332. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +3 -0
  333. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +3 -0
  334. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +3 -0
  335. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bipr5s._.js +3 -0
  336. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +3 -0
  337. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +33 -0
  338. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +3 -0
  339. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0j26pto._.js +14 -0
  340. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mocm6d._.js +33 -0
  341. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +3 -0
  342. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1678-w0._.js +3 -0
  343. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +3 -0
  344. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +3 -0
  345. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +3 -0
  346. package/.next/standalone/.next/server/chunks/ssr/[turbopack]_runtime.js +807 -0
  347. package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +3 -0
  348. package/.next/standalone/.next/server/chunks/ssr/_0ggabjc._.js +3 -0
  349. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +3 -0
  350. package/.next/standalone/.next/server/chunks/ssr/_1djpk8t._.js +3 -0
  351. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0zi5s8-.js +3 -0
  352. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0pt47yr.js +3 -0
  353. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_import_page_actions_0bljgrx.js +3 -0
  354. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_launch_page_actions_1qmf5nl.js +3 -0
  355. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_page_actions_0hhsz1j.js +3 -0
  356. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_paths_page_actions_09jbzbd.js +3 -0
  357. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_profile_page_actions_1b7qq3l.js +3 -0
  358. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_queue_page_actions_1s499-2.js +3 -0
  359. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_setup_page_actions_0u1sm0s.js +3 -0
  360. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +3 -0
  361. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +46 -0
  362. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +18 -0
  363. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  364. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +3 -0
  365. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +35 -0
  366. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -0
  367. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +3 -0
  368. package/.next/standalone/.next/server/chunks/ssr/node_modules_01xbj_9._.js +33 -0
  369. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +3 -0
  370. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +3 -0
  371. package/.next/standalone/.next/server/chunks/ssr/node_modules_1nkgb45._.js +23 -0
  372. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_00nap8h._.js +3 -0
  373. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +6 -0
  374. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_0wpq8j3._.js +3 -0
  375. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0symwr9.js +3 -0
  376. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892.js +3 -0
  377. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0l_sp0x.js +3 -0
  378. package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0pco7a6._.js +11 -0
  379. package/.next/standalone/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_1nw6_jx.js +3 -0
  380. package/.next/standalone/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0n2dz80.js +3 -0
  381. package/.next/standalone/.next/server/functions-config-manifest.json +4 -0
  382. package/.next/standalone/.next/server/middleware-build-manifest.js +24 -0
  383. package/.next/standalone/.next/server/middleware-manifest.json +34 -0
  384. package/.next/standalone/.next/server/next-font-manifest.js +1 -0
  385. package/.next/standalone/.next/server/next-font-manifest.json +39 -0
  386. package/.next/standalone/.next/server/pages/404.html +1 -0
  387. package/.next/standalone/.next/server/pages/500.html +1 -0
  388. package/.next/standalone/.next/server/pages-manifest.json +4 -0
  389. package/.next/standalone/.next/server/prefetch-hints.json +118 -0
  390. package/.next/standalone/.next/server/server-reference-manifest.js +1 -0
  391. package/.next/standalone/.next/server/server-reference-manifest.json +5 -0
  392. package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_buildManifest.js +11 -0
  393. package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +6 -0
  394. package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_ssgManifest.js +1 -0
  395. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +1 -0
  396. package/.next/standalone/.next/static/chunks/0bma92pht_c97.js +1 -0
  397. package/.next/standalone/.next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  398. package/.next/standalone/.next/static/chunks/19mx3mg6lkumu.js +1 -0
  399. package/.next/standalone/.next/static/chunks/1odow92u6wxz0.js +1 -0
  400. package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +1 -0
  401. package/.next/standalone/.next/static/chunks/1zmpcmngg5c7-.js +2 -0
  402. package/.next/standalone/.next/static/chunks/2-rtuqsgzmno4.js +22 -0
  403. package/.next/standalone/.next/static/chunks/220-ne82aus0c.js +44 -0
  404. package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +2 -0
  405. package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +16 -0
  406. package/.next/standalone/.next/static/chunks/310vm2bl3xxpt.js +31 -0
  407. package/.next/standalone/.next/static/chunks/34at5d2ifl62i.js +33 -0
  408. package/.next/standalone/.next/static/chunks/3fntmmi971322.js +1 -0
  409. package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +1 -0
  410. package/.next/standalone/.next/static/chunks/3pfyuezyqd9ob.js +1 -0
  411. package/.next/standalone/.next/static/chunks/3ts-_4i6bfkh2.js +1 -0
  412. package/.next/standalone/.next/static/chunks/40nvbgfn7-072.css +3 -0
  413. package/.next/standalone/.next/static/chunks/turbopack-43vu1xwipchft.js +1 -0
  414. package/.next/standalone/.next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
  415. package/.next/standalone/.next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  416. package/.next/standalone/.next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
  417. package/.next/standalone/.next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
  418. package/.next/standalone/.next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  419. package/.next/standalone/.next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
  420. package/.next/standalone/.next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
  421. package/.next/standalone/.next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  422. package/.next/standalone/.next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
  423. package/.next/standalone/.next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  424. package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  425. package/.next/standalone/.next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  426. package/.next/standalone/AGENTS.md +8 -0
  427. package/.next/standalone/CHANGELOG.md +553 -0
  428. package/.next/standalone/CLAUDE.md +29 -0
  429. package/.next/standalone/CODE_OF_CONDUCT.md +39 -0
  430. package/.next/standalone/CONTRIBUTING.md +99 -0
  431. package/.next/standalone/LICENSE +21 -0
  432. package/.next/standalone/README.md +265 -0
  433. package/.next/standalone/SECURITY.md +46 -0
  434. package/.next/standalone/app/api/admin-delete/route.js +18 -0
  435. package/.next/standalone/app/api/admin-update/route.js +23 -0
  436. package/.next/standalone/app/api/bridges/route.js +43 -0
  437. package/.next/standalone/app/api/bulk-import/route.js +90 -0
  438. package/.next/standalone/app/api/company-scores/route.js +62 -0
  439. package/.next/standalone/app/api/connections/route.js +38 -0
  440. package/.next/standalone/app/api/delete-cluster/route.js +25 -0
  441. package/.next/standalone/app/api/ingest/route.js +329 -0
  442. package/.next/standalone/app/api/network/route.js +49 -0
  443. package/.next/standalone/app/api/notifications/route.js +50 -0
  444. package/.next/standalone/app/api/outreach/route.js +81 -0
  445. package/.next/standalone/app/api/queue/route.js +81 -0
  446. package/.next/standalone/app/api/scraper/route.js +489 -0
  447. package/.next/standalone/app/api/setup-profile/route.js +48 -0
  448. package/.next/standalone/app/api/unlock/route.js +29 -0
  449. package/.next/standalone/app/api/update/route.js +221 -0
  450. package/.next/standalone/app/api/update-images/route.js +39 -0
  451. package/.next/standalone/app/api/users/route.js +73 -0
  452. package/.next/standalone/app/avatars/[file]/route.js +33 -0
  453. package/.next/standalone/app/components/Avatar.js +35 -0
  454. package/.next/standalone/app/components/BridgeRing.js +753 -0
  455. package/.next/standalone/app/components/ChainView.js +631 -0
  456. package/.next/standalone/app/components/CompanyScores.js +144 -0
  457. package/.next/standalone/app/components/EmptyState.js +110 -0
  458. package/.next/standalone/app/components/FilterPanel.js +184 -0
  459. package/.next/standalone/app/components/ForceGraph.js +1043 -0
  460. package/.next/standalone/app/components/GridView.js +166 -0
  461. package/.next/standalone/app/components/LinkedInLimits.js +143 -0
  462. package/.next/standalone/app/components/ListView.js +148 -0
  463. package/.next/standalone/app/components/MappingProgress.js +134 -0
  464. package/.next/standalone/app/components/OnboardingGate.js +37 -0
  465. package/.next/standalone/app/components/OrbitGraph.js +630 -0
  466. package/.next/standalone/app/components/OutlinkQuest.js +212 -0
  467. package/.next/standalone/app/components/PathsAnalyzer.js +520 -0
  468. package/.next/standalone/app/components/RingsView.js +277 -0
  469. package/.next/standalone/app/components/SeparationView.js +564 -0
  470. package/.next/standalone/app/components/Sidebar.js +1398 -0
  471. package/.next/standalone/app/components/StaleServerBanner.js +53 -0
  472. package/.next/standalone/app/components/UpdatePanel.js +246 -0
  473. package/.next/standalone/app/components/UserProvider.js +116 -0
  474. package/.next/standalone/app/components/views.js +59 -0
  475. package/.next/standalone/app/favicon.ico +0 -0
  476. package/.next/standalone/app/globals.css +33 -0
  477. package/.next/standalone/app/import/page.js +189 -0
  478. package/.next/standalone/app/launch/page.js +834 -0
  479. package/.next/standalone/app/layout.js +37 -0
  480. package/.next/standalone/app/page.js +490 -0
  481. package/.next/standalone/app/paths/page.js +561 -0
  482. package/.next/standalone/app/profile/page.js +572 -0
  483. package/.next/standalone/app/queue/page.js +685 -0
  484. package/.next/standalone/app/setup/page.js +562 -0
  485. package/.next/standalone/bin/six-degrees.mjs +149 -0
  486. package/.next/standalone/db/schema.js +160 -0
  487. package/.next/standalone/eslint.config.mjs +34 -0
  488. package/.next/standalone/install.sh +192 -0
  489. package/.next/standalone/jsconfig.json +7 -0
  490. package/.next/standalone/lib/app-version.js +7 -0
  491. package/.next/standalone/lib/companies.js +241 -0
  492. package/.next/standalone/lib/csv.js +172 -0
  493. package/.next/standalone/lib/db-client.js +152 -0
  494. package/.next/standalone/lib/db.js +288 -0
  495. package/.next/standalone/lib/demo.js +34 -0
  496. package/.next/standalone/lib/gate.js +87 -0
  497. package/.next/standalone/lib/ingest.js +62 -0
  498. package/.next/standalone/lib/linkedin-limits.js +118 -0
  499. package/.next/standalone/lib/network.js +13 -0
  500. package/.next/standalone/lib/paths.js +31 -0
  501. package/.next/standalone/lib/paused.js +75 -0
  502. package/.next/standalone/lib/profile.js +55 -0
  503. package/.next/standalone/lib/promote.js +139 -0
  504. package/.next/standalone/lib/quest.js +119 -0
  505. package/.next/standalone/lib/release.js +50 -0
  506. package/.next/standalone/lib/rpc.js +80 -0
  507. package/.next/standalone/lib/scan-progress.js +63 -0
  508. package/.next/standalone/lib/scoring.js +410 -0
  509. package/.next/standalone/lib/scraper-client.js +65 -0
  510. package/.next/standalone/lib/separation.js +326 -0
  511. package/.next/standalone/lib/tiers.js +50 -0
  512. package/.next/standalone/lib/user.js +32 -0
  513. package/.next/standalone/middleware.js +48 -0
  514. package/.next/standalone/next.config.mjs +35 -0
  515. package/.next/standalone/node_modules/@img/colour/color.cjs +1596 -0
  516. package/.next/standalone/node_modules/@img/colour/index.cjs +1 -0
  517. package/.next/standalone/node_modules/@img/colour/package.json +58 -0
  518. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +46 -0
  519. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +219 -0
  520. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +1 -0
  521. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.18.6 +0 -0
  522. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +43 -0
  523. package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/versions.json +30 -0
  524. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +46 -0
  525. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +219 -0
  526. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
  527. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.18.6 +0 -0
  528. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +43 -0
  529. package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
  530. package/.next/standalone/node_modules/@img/sharp-linux-x64/index.cjs +2 -0
  531. package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64-0.35.4.node +0 -0
  532. package/.next/standalone/node_modules/@img/sharp-linux-x64/package.json +47 -0
  533. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/index.cjs +2 -0
  534. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64-0.35.4.node +0 -0
  535. package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +47 -0
  536. package/.next/standalone/node_modules/@next/env/dist/index.js +1 -0
  537. package/.next/standalone/node_modules/@next/env/package.json +36 -0
  538. package/.next/standalone/node_modules/@swc/helpers/cjs/_interop_require_default.cjs +6 -0
  539. package/.next/standalone/node_modules/@swc/helpers/cjs/_interop_require_wildcard.cjs +38 -0
  540. package/.next/standalone/node_modules/@swc/helpers/esm/_interop_require_default.js +4 -0
  541. package/.next/standalone/node_modules/@swc/helpers/esm/_interop_require_wildcard.js +36 -0
  542. package/.next/standalone/node_modules/@swc/helpers/package.json +705 -0
  543. package/.next/standalone/node_modules/client-only/index.js +0 -0
  544. package/.next/standalone/node_modules/client-only/package.json +19 -0
  545. package/.next/standalone/node_modules/detect-libc/lib/detect-libc.js +313 -0
  546. package/.next/standalone/node_modules/detect-libc/lib/elf.js +39 -0
  547. package/.next/standalone/node_modules/detect-libc/lib/filesystem.js +51 -0
  548. package/.next/standalone/node_modules/detect-libc/lib/process.js +24 -0
  549. package/.next/standalone/node_modules/detect-libc/package.json +44 -0
  550. package/.next/standalone/node_modules/next/dist/build/adapter/setup-node-env.external.js +15 -0
  551. package/.next/standalone/node_modules/next/dist/build/define-env.js +263 -0
  552. package/.next/standalone/node_modules/next/dist/build/duration-to-string.js +98 -0
  553. package/.next/standalone/node_modules/next/dist/build/get-supported-browsers.js +38 -0
  554. package/.next/standalone/node_modules/next/dist/build/next-config-ts/require-hook.js +85 -0
  555. package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +140 -0
  556. package/.next/standalone/node_modules/next/dist/build/output/format.js +84 -0
  557. package/.next/standalone/node_modules/next/dist/build/output/log.js +135 -0
  558. package/.next/standalone/node_modules/next/dist/build/segment-config/app/app-segment-config.js +178 -0
  559. package/.next/standalone/node_modules/next/dist/build/segment-config/app/app-segments.js +147 -0
  560. package/.next/standalone/node_modules/next/dist/build/segment-config/app/collect-root-param-keys.js +52 -0
  561. package/.next/standalone/node_modules/next/dist/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.js +137 -0
  562. package/.next/standalone/node_modules/next/dist/build/static-paths/app.js +783 -0
  563. package/.next/standalone/node_modules/next/dist/build/static-paths/pages.js +169 -0
  564. package/.next/standalone/node_modules/next/dist/build/static-paths/utils.js +119 -0
  565. package/.next/standalone/node_modules/next/dist/build/swc/helpers.js +15 -0
  566. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1409 -0
  567. package/.next/standalone/node_modules/next/dist/build/swc/install-bindings.js +30 -0
  568. package/.next/standalone/node_modules/next/dist/build/swc/jest-transformer.js +77 -0
  569. package/.next/standalone/node_modules/next/dist/build/swc/loaderWorkerPool.js +40 -0
  570. package/.next/standalone/node_modules/next/dist/build/swc/options.js +411 -0
  571. package/.next/standalone/node_modules/next/dist/build/swc/types.js +6 -0
  572. package/.next/standalone/node_modules/next/dist/build/utils.js +1258 -0
  573. package/.next/standalone/node_modules/next/dist/cli/next-test.js +171 -0
  574. package/.next/standalone/node_modules/next/dist/client/add-base-path.js +24 -0
  575. package/.next/standalone/node_modules/next/dist/client/app-call-server.js +34 -0
  576. package/.next/standalone/node_modules/next/dist/client/app-find-source-map-url.js +39 -0
  577. package/.next/standalone/node_modules/next/dist/client/assign-location.js +29 -0
  578. package/.next/standalone/node_modules/next/dist/client/components/app-router-announcer.js +78 -0
  579. package/.next/standalone/node_modules/next/dist/client/components/app-router-headers.js +128 -0
  580. package/.next/standalone/node_modules/next/dist/client/components/app-router-instance.js +407 -0
  581. package/.next/standalone/node_modules/next/dist/client/components/app-router-utils.js +62 -0
  582. package/.next/standalone/node_modules/next/dist/client/components/app-router.js +522 -0
  583. package/.next/standalone/node_modules/next/dist/client/components/builtin/default.js +35 -0
  584. package/.next/standalone/node_modules/next/dist/client/components/builtin/error-styles.js +165 -0
  585. package/.next/standalone/node_modules/next/dist/client/components/builtin/global-error.js +100 -0
  586. package/.next/standalone/node_modules/next/dist/client/components/dev-root-http-access-fallback-boundary.js +52 -0
  587. package/.next/standalone/node_modules/next/dist/client/components/error-boundary.js +148 -0
  588. package/.next/standalone/node_modules/next/dist/client/components/errors/graceful-degrade-boundary.js +86 -0
  589. package/.next/standalone/node_modules/next/dist/client/components/errors/root-error-boundary.js +41 -0
  590. package/.next/standalone/node_modules/next/dist/client/components/forbidden.js +48 -0
  591. package/.next/standalone/node_modules/next/dist/client/components/handle-isr-error.js +30 -0
  592. package/.next/standalone/node_modules/next/dist/client/components/hooks-server-context.js +42 -0
  593. package/.next/standalone/node_modules/next/dist/client/components/http-access-fallback/error-boundary.js +125 -0
  594. package/.next/standalone/node_modules/next/dist/client/components/http-access-fallback/http-access-fallback.js +72 -0
  595. package/.next/standalone/node_modules/next/dist/client/components/instant-samples.js +134 -0
  596. package/.next/standalone/node_modules/next/dist/client/components/is-next-router-error.js +23 -0
  597. package/.next/standalone/node_modules/next/dist/client/components/links.js +316 -0
  598. package/.next/standalone/node_modules/next/dist/client/components/match-segments.js +32 -0
  599. package/.next/standalone/node_modules/next/dist/client/components/nav-failure-handler.js +61 -0
  600. package/.next/standalone/node_modules/next/dist/client/components/navigation-devtools.js +126 -0
  601. package/.next/standalone/node_modules/next/dist/client/components/navigation-dynamic-rendering.js +38 -0
  602. package/.next/standalone/node_modules/next/dist/client/components/navigation-untracked.js +69 -0
  603. package/.next/standalone/node_modules/next/dist/client/components/navigation.js +246 -0
  604. package/.next/standalone/node_modules/next/dist/client/components/navigation.react-server.js +75 -0
  605. package/.next/standalone/node_modules/next/dist/client/components/not-found.js +42 -0
  606. package/.next/standalone/node_modules/next/dist/client/components/offline.js +172 -0
  607. package/.next/standalone/node_modules/next/dist/client/components/readonly-url-search-params.js +46 -0
  608. package/.next/standalone/node_modules/next/dist/client/components/redirect-boundary.js +107 -0
  609. package/.next/standalone/node_modules/next/dist/client/components/redirect-error.js +43 -0
  610. package/.next/standalone/node_modules/next/dist/client/components/redirect-status-code.js +24 -0
  611. package/.next/standalone/node_modules/next/dist/client/components/redirect.js +91 -0
  612. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/compute-changed-path.js +205 -0
  613. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/create-href-from-url.js +21 -0
  614. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/create-initial-router-state.js +161 -0
  615. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/create-router-cache-key.js +32 -0
  616. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/fetch-server-response.js +603 -0
  617. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/is-navigating-to-new-root-layout.js +70 -0
  618. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/ppr-navigations.js +1474 -0
  619. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/committed-state.js +49 -0
  620. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/find-head-in-cache.js +63 -0
  621. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/has-interception-route-in-current-tree.js +38 -0
  622. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/hmr-refresh-reducer.js +29 -0
  623. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/navigate-reducer.js +56 -0
  624. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/refresh-reducer.js +90 -0
  625. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/restore-reducer.js +75 -0
  626. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/server-action-reducer.js +349 -0
  627. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/server-patch-reducer.js +67 -0
  628. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/router-reducer-types.js +70 -0
  629. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/router-reducer.js +74 -0
  630. package/.next/standalone/node_modules/next/dist/client/components/router-reducer/set-cache-busting-search-param.js +66 -0
  631. package/.next/standalone/node_modules/next/dist/client/components/router-transition.js +135 -0
  632. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/bfcache.js +130 -0
  633. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache-key.js +56 -0
  634. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache-map.js +326 -0
  635. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache.js +2684 -0
  636. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/fetch.js +37 -0
  637. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/lru.js +152 -0
  638. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/navigation-testing-lock.js +415 -0
  639. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/navigation.js +773 -0
  640. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/optimistic-routes.js +648 -0
  641. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/prefetch.js +32 -0
  642. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/scheduler.js +1770 -0
  643. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/types.js +84 -0
  644. package/.next/standalone/node_modules/next/dist/client/components/segment-cache/vary-path.js +326 -0
  645. package/.next/standalone/node_modules/next/dist/client/components/server-async-storage.js +43 -0
  646. package/.next/standalone/node_modules/next/dist/client/components/static-generation-bailout.js +42 -0
  647. package/.next/standalone/node_modules/next/dist/client/components/unauthorized.js +49 -0
  648. package/.next/standalone/node_modules/next/dist/client/components/unrecognized-action-error.js +39 -0
  649. package/.next/standalone/node_modules/next/dist/client/components/unresolved-thenable.js +23 -0
  650. package/.next/standalone/node_modules/next/dist/client/components/unstable-rethrow.js +32 -0
  651. package/.next/standalone/node_modules/next/dist/client/components/use-action-queue.js +148 -0
  652. package/.next/standalone/node_modules/next/dist/client/components/use-offline.js +68 -0
  653. package/.next/standalone/node_modules/next/dist/client/dev/debug-channel.js +428 -0
  654. package/.next/standalone/node_modules/next/dist/client/flight-data-helpers.js +235 -0
  655. package/.next/standalone/node_modules/next/dist/client/has-base-path.js +23 -0
  656. package/.next/standalone/node_modules/next/dist/client/lib/console.js +177 -0
  657. package/.next/standalone/node_modules/next/dist/client/lib/javascript-url.js +32 -0
  658. package/.next/standalone/node_modules/next/dist/client/navigation-build-id.js +53 -0
  659. package/.next/standalone/node_modules/next/dist/client/normalize-trailing-slash.js +37 -0
  660. package/.next/standalone/node_modules/next/dist/client/remove-base-path.js +32 -0
  661. package/.next/standalone/node_modules/next/dist/client/route-params.js +249 -0
  662. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/cookies/index.js +339 -0
  663. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/cookies/package.json +1 -0
  664. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/ponyfill/index.js +46 -0
  665. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/ponyfill/package.json +1 -0
  666. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/abort-controller.js.text.js +1 -0
  667. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/console.js.text.js +1 -0
  668. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/events.js.text.js +1 -0
  669. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/index.js +5 -0
  670. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/load.js +18713 -0
  671. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/package.json +1 -0
  672. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/stream.js +44 -0
  673. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/timers.js.text.js +1 -0
  674. package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/url.js.text.js +1 -0
  675. package/.next/standalone/node_modules/next/dist/compiled/@hapi/accept/index.js +1 -0
  676. package/.next/standalone/node_modules/next/dist/compiled/@hapi/accept/package.json +1 -0
  677. package/.next/standalone/node_modules/next/dist/compiled/@mswjs/interceptors/ClientRequest/index.js +1 -0
  678. package/.next/standalone/node_modules/next/dist/compiled/@mswjs/interceptors/ClientRequest/package.json +1 -0
  679. package/.next/standalone/node_modules/next/dist/compiled/@napi-rs/triples/index.js +1 -0
  680. package/.next/standalone/node_modules/next/dist/compiled/@napi-rs/triples/package.json +1 -0
  681. package/.next/standalone/node_modules/next/dist/compiled/@opentelemetry/api/index.js +1 -0
  682. package/.next/standalone/node_modules/next/dist/compiled/@opentelemetry/api/package.json +1 -0
  683. package/.next/standalone/node_modules/next/dist/compiled/@vercel/detect-agent/index.js +1 -0
  684. package/.next/standalone/node_modules/next/dist/compiled/@vercel/detect-agent/package.json +1 -0
  685. package/.next/standalone/node_modules/next/dist/compiled/async-retry/index.js +1 -0
  686. package/.next/standalone/node_modules/next/dist/compiled/async-retry/package.json +1 -0
  687. package/.next/standalone/node_modules/next/dist/compiled/async-sema/index.js +1 -0
  688. package/.next/standalone/node_modules/next/dist/compiled/async-sema/package.json +1 -0
  689. package/.next/standalone/node_modules/next/dist/compiled/busboy/index.js +1 -0
  690. package/.next/standalone/node_modules/next/dist/compiled/busboy/package.json +1 -0
  691. package/.next/standalone/node_modules/next/dist/compiled/bytes/index.js +8 -0
  692. package/.next/standalone/node_modules/next/dist/compiled/bytes/package.json +1 -0
  693. package/.next/standalone/node_modules/next/dist/compiled/ci-info/index.js +1 -0
  694. package/.next/standalone/node_modules/next/dist/compiled/ci-info/package.json +1 -0
  695. package/.next/standalone/node_modules/next/dist/compiled/commander/index.js +1 -0
  696. package/.next/standalone/node_modules/next/dist/compiled/commander/package.json +1 -0
  697. package/.next/standalone/node_modules/next/dist/compiled/comment-json/index.js +7 -0
  698. package/.next/standalone/node_modules/next/dist/compiled/comment-json/package.json +1 -0
  699. package/.next/standalone/node_modules/next/dist/compiled/compression/index.js +58 -0
  700. package/.next/standalone/node_modules/next/dist/compiled/compression/package.json +1 -0
  701. package/.next/standalone/node_modules/next/dist/compiled/conf/index.js +3 -0
  702. package/.next/standalone/node_modules/next/dist/compiled/conf/package.json +1 -0
  703. package/.next/standalone/node_modules/next/dist/compiled/content-disposition/index.js +6 -0
  704. package/.next/standalone/node_modules/next/dist/compiled/content-disposition/package.json +1 -0
  705. package/.next/standalone/node_modules/next/dist/compiled/cookie/index.js +7 -0
  706. package/.next/standalone/node_modules/next/dist/compiled/cookie/package.json +1 -0
  707. package/.next/standalone/node_modules/next/dist/compiled/cross-spawn/index.js +1 -0
  708. package/.next/standalone/node_modules/next/dist/compiled/cross-spawn/package.json +1 -0
  709. package/.next/standalone/node_modules/next/dist/compiled/debug/index.js +1 -0
  710. package/.next/standalone/node_modules/next/dist/compiled/debug/package.json +1 -0
  711. package/.next/standalone/node_modules/next/dist/compiled/edge-runtime/index.js +1 -0
  712. package/.next/standalone/node_modules/next/dist/compiled/edge-runtime/package.json +1 -0
  713. package/.next/standalone/node_modules/next/dist/compiled/find-up/index.js +1 -0
  714. package/.next/standalone/node_modules/next/dist/compiled/find-up/package.json +1 -0
  715. package/.next/standalone/node_modules/next/dist/compiled/fresh/index.js +8 -0
  716. package/.next/standalone/node_modules/next/dist/compiled/fresh/package.json +1 -0
  717. package/.next/standalone/node_modules/next/dist/compiled/httpxy/index.js +1 -0
  718. package/.next/standalone/node_modules/next/dist/compiled/httpxy/package.json +1 -0
  719. package/.next/standalone/node_modules/next/dist/compiled/image-detector/detector.js +1 -0
  720. package/.next/standalone/node_modules/next/dist/compiled/image-size/index.js +1 -0
  721. package/.next/standalone/node_modules/next/dist/compiled/image-size/package.json +1 -0
  722. package/.next/standalone/node_modules/next/dist/compiled/ipaddr.js/ipaddr.js +1 -0
  723. package/.next/standalone/node_modules/next/dist/compiled/ipaddr.js/package.json +1 -0
  724. package/.next/standalone/node_modules/next/dist/compiled/is-animated/index.js +1 -0
  725. package/.next/standalone/node_modules/next/dist/compiled/is-animated/package.json +1 -0
  726. package/.next/standalone/node_modules/next/dist/compiled/is-docker/index.js +1 -0
  727. package/.next/standalone/node_modules/next/dist/compiled/is-docker/package.json +1 -0
  728. package/.next/standalone/node_modules/next/dist/compiled/is-wsl/index.js +1 -0
  729. package/.next/standalone/node_modules/next/dist/compiled/is-wsl/package.json +1 -0
  730. package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/index.js +11 -0
  731. package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/package.json +1 -0
  732. package/.next/standalone/node_modules/next/dist/compiled/nanoid/index.cjs +1 -0
  733. package/.next/standalone/node_modules/next/dist/compiled/nanoid/package.json +1 -0
  734. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +183 -0
  735. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +183 -0
  736. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +6 -0
  737. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +20 -0
  738. package/.next/standalone/node_modules/next/dist/compiled/p-limit/index.js +1 -0
  739. package/.next/standalone/node_modules/next/dist/compiled/p-limit/package.json +1 -0
  740. package/.next/standalone/node_modules/next/dist/compiled/p-queue/index.js +1 -0
  741. package/.next/standalone/node_modules/next/dist/compiled/p-queue/package.json +1 -0
  742. package/.next/standalone/node_modules/next/dist/compiled/path-browserify/index.js +1 -0
  743. package/.next/standalone/node_modules/next/dist/compiled/path-browserify/package.json +1 -0
  744. package/.next/standalone/node_modules/next/dist/compiled/path-to-regexp/index.js +1 -0
  745. package/.next/standalone/node_modules/next/dist/compiled/path-to-regexp/package.json +1 -0
  746. package/.next/standalone/node_modules/next/dist/compiled/picomatch/index.js +1 -0
  747. package/.next/standalone/node_modules/next/dist/compiled/picomatch/package.json +1 -0
  748. package/.next/standalone/node_modules/next/dist/compiled/react-is/cjs/react-is.development.js +133 -0
  749. package/.next/standalone/node_modules/next/dist/compiled/react-is/cjs/react-is.production.js +130 -0
  750. package/.next/standalone/node_modules/next/dist/compiled/react-is/index.js +7 -0
  751. package/.next/standalone/node_modules/next/dist/compiled/react-is/package.json +26 -0
  752. package/.next/standalone/node_modules/next/dist/compiled/regenerator-runtime/package.json +19 -0
  753. package/.next/standalone/node_modules/next/dist/compiled/regenerator-runtime/runtime.js +729 -0
  754. package/.next/standalone/node_modules/next/dist/compiled/semver/index.js +1 -0
  755. package/.next/standalone/node_modules/next/dist/compiled/semver/package.json +1 -0
  756. package/.next/standalone/node_modules/next/dist/compiled/send/index.js +70 -0
  757. package/.next/standalone/node_modules/next/dist/compiled/send/package.json +1 -0
  758. package/.next/standalone/node_modules/next/dist/compiled/source-map/package.json +1 -0
  759. package/.next/standalone/node_modules/next/dist/compiled/source-map/source-map.js +1 -0
  760. package/.next/standalone/node_modules/next/dist/compiled/stacktrace-parser/package.json +1 -0
  761. package/.next/standalone/node_modules/next/dist/compiled/stacktrace-parser/stack-trace-parser.cjs.js +1 -0
  762. package/.next/standalone/node_modules/next/dist/compiled/string-hash/index.js +1 -0
  763. package/.next/standalone/node_modules/next/dist/compiled/string-hash/package.json +1 -0
  764. package/.next/standalone/node_modules/next/dist/compiled/strip-ansi/index.js +1 -0
  765. package/.next/standalone/node_modules/next/dist/compiled/strip-ansi/package.json +1 -0
  766. package/.next/standalone/node_modules/next/dist/compiled/superstruct/index.cjs +1 -0
  767. package/.next/standalone/node_modules/next/dist/compiled/superstruct/package.json +1 -0
  768. package/.next/standalone/node_modules/next/dist/compiled/tar/index.min.js +1 -0
  769. package/.next/standalone/node_modules/next/dist/compiled/tar/package.json +1 -0
  770. package/.next/standalone/node_modules/next/dist/compiled/text-table/index.js +1 -0
  771. package/.next/standalone/node_modules/next/dist/compiled/text-table/package.json +1 -0
  772. package/.next/standalone/node_modules/next/dist/compiled/watchpack/package.json +1 -0
  773. package/.next/standalone/node_modules/next/dist/compiled/watchpack/watchpack.js +1 -0
  774. package/.next/standalone/node_modules/next/dist/compiled/write-file-atomic/index.js +1 -0
  775. package/.next/standalone/node_modules/next/dist/compiled/write-file-atomic/package.json +1 -0
  776. package/.next/standalone/node_modules/next/dist/compiled/ws/index.js +1 -0
  777. package/.next/standalone/node_modules/next/dist/compiled/ws/package.json +1 -0
  778. package/.next/standalone/node_modules/next/dist/compiled/zod/index.cjs +1 -0
  779. package/.next/standalone/node_modules/next/dist/compiled/zod/package.json +1 -0
  780. package/.next/standalone/node_modules/next/dist/compiled/zod-validation-error/index.js +1 -0
  781. package/.next/standalone/node_modules/next/dist/compiled/zod-validation-error/package.json +1 -0
  782. package/.next/standalone/node_modules/next/dist/experimental/testmode/context.js +57 -0
  783. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +158 -0
  784. package/.next/standalone/node_modules/next/dist/experimental/testmode/httpget.js +33 -0
  785. package/.next/standalone/node_modules/next/dist/experimental/testmode/server-edge.js +32 -0
  786. package/.next/standalone/node_modules/next/dist/experimental/testmode/server.js +73 -0
  787. package/.next/standalone/node_modules/next/dist/export/helpers/create-incremental-cache.js +61 -0
  788. package/.next/standalone/node_modules/next/dist/lib/batcher.js +65 -0
  789. package/.next/standalone/node_modules/next/dist/lib/build-custom-route.js +46 -0
  790. package/.next/standalone/node_modules/next/dist/lib/bundler.js +168 -0
  791. package/.next/standalone/node_modules/next/dist/lib/client-and-server-references.js +54 -0
  792. package/.next/standalone/node_modules/next/dist/lib/coalesced-function.js +39 -0
  793. package/.next/standalone/node_modules/next/dist/lib/compile-error.js +14 -0
  794. package/.next/standalone/node_modules/next/dist/lib/constants.js +420 -0
  795. package/.next/standalone/node_modules/next/dist/lib/create-client-router-filter.js +83 -0
  796. package/.next/standalone/node_modules/next/dist/lib/default-transpiled-packages.json +1 -0
  797. package/.next/standalone/node_modules/next/dist/lib/detached-promise.js +32 -0
  798. package/.next/standalone/node_modules/next/dist/lib/detect-typo.js +51 -0
  799. package/.next/standalone/node_modules/next/dist/lib/download-swc.js +183 -0
  800. package/.next/standalone/node_modules/next/dist/lib/error-telemetry-utils.js +42 -0
  801. package/.next/standalone/node_modules/next/dist/lib/fallback.js +100 -0
  802. package/.next/standalone/node_modules/next/dist/lib/file-exists.js +53 -0
  803. package/.next/standalone/node_modules/next/dist/lib/find-config.js +102 -0
  804. package/.next/standalone/node_modules/next/dist/lib/find-pages-dir.js +65 -0
  805. package/.next/standalone/node_modules/next/dist/lib/find-root.js +280 -0
  806. package/.next/standalone/node_modules/next/dist/lib/format-cli-help-output.js +84 -0
  807. package/.next/standalone/node_modules/next/dist/lib/format-dynamic-import-path.js +43 -0
  808. package/.next/standalone/node_modules/next/dist/lib/format-server-error.js +75 -0
  809. package/.next/standalone/node_modules/next/dist/lib/framework/boundary-components.js +62 -0
  810. package/.next/standalone/node_modules/next/dist/lib/framework/boundary-constants.js +36 -0
  811. package/.next/standalone/node_modules/next/dist/lib/fs/rename.js +87 -0
  812. package/.next/standalone/node_modules/next/dist/lib/fs/write-atomic.js +28 -0
  813. package/.next/standalone/node_modules/next/dist/lib/generate-interception-routes-rewrites.js +52 -0
  814. package/.next/standalone/node_modules/next/dist/lib/get-files-in-dir.js +33 -0
  815. package/.next/standalone/node_modules/next/dist/lib/get-network-host.js +44 -0
  816. package/.next/standalone/node_modules/next/dist/lib/get-package-version.js +118 -0
  817. package/.next/standalone/node_modules/next/dist/lib/get-project-dir.js +51 -0
  818. package/.next/standalone/node_modules/next/dist/lib/git-worktree.js +74 -0
  819. package/.next/standalone/node_modules/next/dist/lib/has-necessary-dependencies.js +49 -0
  820. package/.next/standalone/node_modules/next/dist/lib/helpers/get-cache-directory.js +66 -0
  821. package/.next/standalone/node_modules/next/dist/lib/helpers/get-npx-command.js +29 -0
  822. package/.next/standalone/node_modules/next/dist/lib/helpers/get-online.js +50 -0
  823. package/.next/standalone/node_modules/next/dist/lib/helpers/get-pkg-manager.js +65 -0
  824. package/.next/standalone/node_modules/next/dist/lib/helpers/get-registry.js +45 -0
  825. package/.next/standalone/node_modules/next/dist/lib/helpers/get-reserved-port.js +116 -0
  826. package/.next/standalone/node_modules/next/dist/lib/helpers/git.js +59 -0
  827. package/.next/standalone/node_modules/next/dist/lib/helpers/install.js +83 -0
  828. package/.next/standalone/node_modules/next/dist/lib/import-next-warning.js +50 -0
  829. package/.next/standalone/node_modules/next/dist/lib/inline-static-env.js +112 -0
  830. package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +40 -0
  831. package/.next/standalone/node_modules/next/dist/lib/interop-default.js +16 -0
  832. package/.next/standalone/node_modules/next/dist/lib/is-api-route.js +15 -0
  833. package/.next/standalone/node_modules/next/dist/lib/is-app-page-route.js +15 -0
  834. package/.next/standalone/node_modules/next/dist/lib/is-app-route-route.js +15 -0
  835. package/.next/standalone/node_modules/next/dist/lib/is-edge-runtime.js +16 -0
  836. package/.next/standalone/node_modules/next/dist/lib/is-error.js +78 -0
  837. package/.next/standalone/node_modules/next/dist/lib/is-interception-route-rewrite.js +18 -0
  838. package/.next/standalone/node_modules/next/dist/lib/is-internal-component.js +36 -0
  839. package/.next/standalone/node_modules/next/dist/lib/is-serializable-props.js +111 -0
  840. package/.next/standalone/node_modules/next/dist/lib/known-edge-safe-packages.json +1 -0
  841. package/.next/standalone/node_modules/next/dist/lib/load-custom-routes.js +594 -0
  842. package/.next/standalone/node_modules/next/dist/lib/memory/gc-observer.js +53 -0
  843. package/.next/standalone/node_modules/next/dist/lib/memory/shutdown.js +29 -0
  844. package/.next/standalone/node_modules/next/dist/lib/memory/startup.js +47 -0
  845. package/.next/standalone/node_modules/next/dist/lib/memory/trace.js +109 -0
  846. package/.next/standalone/node_modules/next/dist/lib/metadata/constants.js +40 -0
  847. package/.next/standalone/node_modules/next/dist/lib/metadata/default-metadata.js +82 -0
  848. package/.next/standalone/node_modules/next/dist/lib/metadata/generate/icon-mark.js +22 -0
  849. package/.next/standalone/node_modules/next/dist/lib/metadata/generate/utils.js +52 -0
  850. package/.next/standalone/node_modules/next/dist/lib/metadata/get-metadata-route.js +162 -0
  851. package/.next/standalone/node_modules/next/dist/lib/metadata/is-metadata-route.js +221 -0
  852. package/.next/standalone/node_modules/next/dist/lib/metadata/metadata-context.js +18 -0
  853. package/.next/standalone/node_modules/next/dist/lib/metadata/metadata.js +1660 -0
  854. package/.next/standalone/node_modules/next/dist/lib/metadata/resolve-metadata.js +884 -0
  855. package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-basics.js +232 -0
  856. package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-icons.js +56 -0
  857. package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-opengraph.js +199 -0
  858. package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-title.js +40 -0
  859. package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-url.js +135 -0
  860. package/.next/standalone/node_modules/next/dist/lib/metadata/types/alternative-urls-types.js +7 -0
  861. package/.next/standalone/node_modules/next/dist/lib/metadata/types/extra-types.js +9 -0
  862. package/.next/standalone/node_modules/next/dist/lib/metadata/types/icons.js +6 -0
  863. package/.next/standalone/node_modules/next/dist/lib/metadata/types/manifest-types.js +6 -0
  864. package/.next/standalone/node_modules/next/dist/lib/metadata/types/metadata-interface.js +18 -0
  865. package/.next/standalone/node_modules/next/dist/lib/metadata/types/metadata-types.js +10 -0
  866. package/.next/standalone/node_modules/next/dist/lib/metadata/types/opengraph-types.js +6 -0
  867. package/.next/standalone/node_modules/next/dist/lib/metadata/types/resolvers.js +6 -0
  868. package/.next/standalone/node_modules/next/dist/lib/metadata/types/twitter-types.js +7 -0
  869. package/.next/standalone/node_modules/next/dist/lib/mime-type.js +20 -0
  870. package/.next/standalone/node_modules/next/dist/lib/mkcert.js +204 -0
  871. package/.next/standalone/node_modules/next/dist/lib/multi-file-writer.js +75 -0
  872. package/.next/standalone/node_modules/next/dist/lib/needs-experimental-react.js +16 -0
  873. package/.next/standalone/node_modules/next/dist/lib/non-nullable.js +15 -0
  874. package/.next/standalone/node_modules/next/dist/lib/normalize-path.js +21 -0
  875. package/.next/standalone/node_modules/next/dist/lib/oxford-comma-list.js +15 -0
  876. package/.next/standalone/node_modules/next/dist/lib/page-types.js +18 -0
  877. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +185 -0
  878. package/.next/standalone/node_modules/next/dist/lib/pick.js +19 -0
  879. package/.next/standalone/node_modules/next/dist/lib/picocolors.js +177 -0
  880. package/.next/standalone/node_modules/next/dist/lib/pretty-bytes.js +74 -0
  881. package/.next/standalone/node_modules/next/dist/lib/profiles-dir.js +45 -0
  882. package/.next/standalone/node_modules/next/dist/lib/realpath.js +20 -0
  883. package/.next/standalone/node_modules/next/dist/lib/recursive-copy.js +76 -0
  884. package/.next/standalone/node_modules/next/dist/lib/recursive-delete.js +137 -0
  885. package/.next/standalone/node_modules/next/dist/lib/recursive-readdir.js +124 -0
  886. package/.next/standalone/node_modules/next/dist/lib/redirect-status.js +46 -0
  887. package/.next/standalone/node_modules/next/dist/lib/require-instrumentation-client.js +32 -0
  888. package/.next/standalone/node_modules/next/dist/lib/resolve-build-paths.js +182 -0
  889. package/.next/standalone/node_modules/next/dist/lib/resolve-from.js +65 -0
  890. package/.next/standalone/node_modules/next/dist/lib/route-pattern-normalizer.js +107 -0
  891. package/.next/standalone/node_modules/next/dist/lib/scheduler.js +64 -0
  892. package/.next/standalone/node_modules/next/dist/lib/semver-noop.js +18 -0
  893. package/.next/standalone/node_modules/next/dist/lib/server-external-packages.jsonc +103 -0
  894. package/.next/standalone/node_modules/next/dist/lib/setup-exception-listeners.js +11 -0
  895. package/.next/standalone/node_modules/next/dist/lib/static-env.js +92 -0
  896. package/.next/standalone/node_modules/next/dist/lib/try-to-parse-path.js +67 -0
  897. package/.next/standalone/node_modules/next/dist/lib/turbopack-cache-seed.js +171 -0
  898. package/.next/standalone/node_modules/next/dist/lib/turbopack-warning.js +185 -0
  899. package/.next/standalone/node_modules/next/dist/lib/typescript/diagnosticFormatter.js +243 -0
  900. package/.next/standalone/node_modules/next/dist/lib/typescript/getTypeScriptConfiguration.js +73 -0
  901. package/.next/standalone/node_modules/next/dist/lib/typescript/getTypeScriptIntent.js +52 -0
  902. package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
  903. package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +26 -0
  904. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheck.js +149 -0
  905. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
  906. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
  907. package/.next/standalone/node_modules/next/dist/lib/typescript/type-paths.js +51 -0
  908. package/.next/standalone/node_modules/next/dist/lib/typescript/writeAppTypeDeclarations.js +77 -0
  909. package/.next/standalone/node_modules/next/dist/lib/typescript/writeConfigurationDefaults.js +395 -0
  910. package/.next/standalone/node_modules/next/dist/lib/url.js +75 -0
  911. package/.next/standalone/node_modules/next/dist/lib/verify-partytown-setup.js +104 -0
  912. package/.next/standalone/node_modules/next/dist/lib/verify-root-layout.js +165 -0
  913. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +271 -0
  914. package/.next/standalone/node_modules/next/dist/lib/wait.js +19 -0
  915. package/.next/standalone/node_modules/next/dist/lib/with-promise-cache.js +24 -0
  916. package/.next/standalone/node_modules/next/dist/lib/worker.js +234 -0
  917. package/.next/standalone/node_modules/next/dist/server/ReactDOMServerPages.js +16 -0
  918. package/.next/standalone/node_modules/next/dist/server/accept-header.js +132 -0
  919. package/.next/standalone/node_modules/next/dist/server/after/after-context.js +178 -0
  920. package/.next/standalone/node_modules/next/dist/server/after/after.js +28 -0
  921. package/.next/standalone/node_modules/next/dist/server/after/awaiter.js +74 -0
  922. package/.next/standalone/node_modules/next/dist/server/after/builtin-request-context.js +38 -0
  923. package/.next/standalone/node_modules/next/dist/server/after/index.js +21 -0
  924. package/.next/standalone/node_modules/next/dist/server/after/run-with-after.js +35 -0
  925. package/.next/standalone/node_modules/next/dist/server/api-utils/get-cookie-parser.js +22 -0
  926. package/.next/standalone/node_modules/next/dist/server/api-utils/index.js +217 -0
  927. package/.next/standalone/node_modules/next/dist/server/api-utils/node/api-resolver.js +395 -0
  928. package/.next/standalone/node_modules/next/dist/server/api-utils/node/parse-body.js +79 -0
  929. package/.next/standalone/node_modules/next/dist/server/api-utils/node/try-get-preview-data.js +89 -0
  930. package/.next/standalone/node_modules/next/dist/server/api-utils/web.js +17 -0
  931. package/.next/standalone/node_modules/next/dist/server/app-render/action-async-storage-instance.js +14 -0
  932. package/.next/standalone/node_modules/next/dist/server/app-render/action-async-storage.external.js +13 -0
  933. package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +1125 -0
  934. package/.next/standalone/node_modules/next/dist/server/app-render/after-task-async-storage-instance.js +14 -0
  935. package/.next/standalone/node_modules/next/dist/server/app-render/after-task-async-storage.external.js +13 -0
  936. package/.next/standalone/node_modules/next/dist/server/app-render/app-render-prerender-utils.js +332 -0
  937. package/.next/standalone/node_modules/next/dist/server/app-render/app-render-render-utils.js +76 -0
  938. package/.next/standalone/node_modules/next/dist/server/app-render/app-render-scheduling.js +188 -0
  939. package/.next/standalone/node_modules/next/dist/server/app-render/app-render.js +6496 -0
  940. package/.next/standalone/node_modules/next/dist/server/app-render/async-local-storage.js +76 -0
  941. package/.next/standalone/node_modules/next/dist/server/app-render/blocking-route-messages.js +181 -0
  942. package/.next/standalone/node_modules/next/dist/server/app-render/cache-signal.js +181 -0
  943. package/.next/standalone/node_modules/next/dist/server/app-render/collect-segment-data.js +937 -0
  944. package/.next/standalone/node_modules/next/dist/server/app-render/console-async-storage-instance.js +14 -0
  945. package/.next/standalone/node_modules/next/dist/server/app-render/console-async-storage.external.js +13 -0
  946. package/.next/standalone/node_modules/next/dist/server/app-render/create-component-styles-and-scripts.js +39 -0
  947. package/.next/standalone/node_modules/next/dist/server/app-render/create-component-tree.js +806 -0
  948. package/.next/standalone/node_modules/next/dist/server/app-render/create-error-handler.js +190 -0
  949. package/.next/standalone/node_modules/next/dist/server/app-render/create-flight-router-state-from-loader-tree.js +154 -0
  950. package/.next/standalone/node_modules/next/dist/server/app-render/csrf-protection.js +86 -0
  951. package/.next/standalone/node_modules/next/dist/server/app-render/debug-channel-server.js +47 -0
  952. package/.next/standalone/node_modules/next/dist/server/app-render/debug-channel-server.node.js +60 -0
  953. package/.next/standalone/node_modules/next/dist/server/app-render/debug-channel-server.web.js +60 -0
  954. package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-error-delivery.js +37 -0
  955. package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-events.js +26 -0
  956. package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-scheduler.js +87 -0
  957. package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-worker-globals.js +41 -0
  958. package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-worker-snapshot.js +79 -0
  959. package/.next/standalone/node_modules/next/dist/server/app-render/dynamic-access-async-storage-instance.js +14 -0
  960. package/.next/standalone/node_modules/next/dist/server/app-render/dynamic-access-async-storage.external.js +13 -0
  961. package/.next/standalone/node_modules/next/dist/server/app-render/dynamic-rendering.js +1201 -0
  962. package/.next/standalone/node_modules/next/dist/server/app-render/encryption-utils-server.js +114 -0
  963. package/.next/standalone/node_modules/next/dist/server/app-render/encryption-utils.js +93 -0
  964. package/.next/standalone/node_modules/next/dist/server/app-render/encryption.js +261 -0
  965. package/.next/standalone/node_modules/next/dist/server/app-render/entry-base.js +265 -0
  966. package/.next/standalone/node_modules/next/dist/server/app-render/flight-render-result.js +28 -0
  967. package/.next/standalone/node_modules/next/dist/server/app-render/get-asset-query-string.js +29 -0
  968. package/.next/standalone/node_modules/next/dist/server/app-render/get-css-inlined-link-tags.js +45 -0
  969. package/.next/standalone/node_modules/next/dist/server/app-render/get-layer-assets.js +69 -0
  970. package/.next/standalone/node_modules/next/dist/server/app-render/get-preloadable-fonts.js +39 -0
  971. package/.next/standalone/node_modules/next/dist/server/app-render/get-script-nonce-from-header.js +32 -0
  972. package/.next/standalone/node_modules/next/dist/server/app-render/get-short-dynamic-param-type.js +25 -0
  973. package/.next/standalone/node_modules/next/dist/server/app-render/has-loading-component-in-tree.js +19 -0
  974. package/.next/standalone/node_modules/next/dist/server/app-render/instant-test-bootstrap.js +20 -0
  975. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/boundary-constants.js +31 -0
  976. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/boundary-impl.js +127 -0
  977. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.js +38 -0
  978. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-config.js +241 -0
  979. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-samples.js +432 -0
  980. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.js +33 -0
  981. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-validation.js +934 -0
  982. package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/stream-utils.js +96 -0
  983. package/.next/standalone/node_modules/next/dist/server/app-render/interop-default.js +17 -0
  984. package/.next/standalone/node_modules/next/dist/server/app-render/make-get-server-inserted-html.js +91 -0
  985. package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +327 -0
  986. package/.next/standalone/node_modules/next/dist/server/app-render/metadata-insertion/create-server-inserted-metadata.js +29 -0
  987. package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/instrument-module-getter.js +32 -0
  988. package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/track-dynamic-import.js +57 -0
  989. package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/track-module-loading.external.js +32 -0
  990. package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/track-module-loading.instance.js +62 -0
  991. package/.next/standalone/node_modules/next/dist/server/app-render/parse-and-validate-flight-router-state.js +49 -0
  992. package/.next/standalone/node_modules/next/dist/server/app-render/postponed-state.js +230 -0
  993. package/.next/standalone/node_modules/next/dist/server/app-render/prospective-render-utils.js +74 -0
  994. package/.next/standalone/node_modules/next/dist/server/app-render/react-large-shell-error.js +17 -0
  995. package/.next/standalone/node_modules/next/dist/server/app-render/react-server.node.js +39 -0
  996. package/.next/standalone/node_modules/next/dist/server/app-render/render-css-resource.js +57 -0
  997. package/.next/standalone/node_modules/next/dist/server/app-render/required-scripts.js +76 -0
  998. package/.next/standalone/node_modules/next/dist/server/app-render/rsc/postpone.js +18 -0
  999. package/.next/standalone/node_modules/next/dist/server/app-render/rsc/preloads.js +74 -0
  1000. package/.next/standalone/node_modules/next/dist/server/app-render/rsc/taint.js +79 -0
  1001. package/.next/standalone/node_modules/next/dist/server/app-render/segment-explorer-path.js +128 -0
  1002. package/.next/standalone/node_modules/next/dist/server/app-render/server-inserted-html.js +77 -0
  1003. package/.next/standalone/node_modules/next/dist/server/app-render/staged-rendering.js +324 -0
  1004. package/.next/standalone/node_modules/next/dist/server/app-render/stale-time.js +101 -0
  1005. package/.next/standalone/node_modules/next/dist/server/app-render/stream-ops.js +155 -0
  1006. package/.next/standalone/node_modules/next/dist/server/app-render/stream-ops.node.js +842 -0
  1007. package/.next/standalone/node_modules/next/dist/server/app-render/stream-ops.web.js +249 -0
  1008. package/.next/standalone/node_modules/next/dist/server/app-render/strip-flight-headers.js +18 -0
  1009. package/.next/standalone/node_modules/next/dist/server/app-render/sync-io-messages.js +67 -0
  1010. package/.next/standalone/node_modules/next/dist/server/app-render/types.js +65 -0
  1011. package/.next/standalone/node_modules/next/dist/server/app-render/use-flight-response.js +217 -0
  1012. package/.next/standalone/node_modules/next/dist/server/app-render/vary-params.js +261 -0
  1013. package/.next/standalone/node_modules/next/dist/server/app-render/wait-for-response.js +36 -0
  1014. package/.next/standalone/node_modules/next/dist/server/app-render/walk-tree-with-flight-router-state.js +237 -0
  1015. package/.next/standalone/node_modules/next/dist/server/app-render/work-async-storage-instance.js +14 -0
  1016. package/.next/standalone/node_modules/next/dist/server/app-render/work-async-storage.external.js +13 -0
  1017. package/.next/standalone/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.js +14 -0
  1018. package/.next/standalone/node_modules/next/dist/server/app-render/work-unit-async-storage.external.js +250 -0
  1019. package/.next/standalone/node_modules/next/dist/server/async-storage/draft-mode-provider.js +62 -0
  1020. package/.next/standalone/node_modules/next/dist/server/async-storage/request-store.js +201 -0
  1021. package/.next/standalone/node_modules/next/dist/server/async-storage/with-store.js +6 -0
  1022. package/.next/standalone/node_modules/next/dist/server/async-storage/work-store.js +98 -0
  1023. package/.next/standalone/node_modules/next/dist/server/base-http/helpers.js +36 -0
  1024. package/.next/standalone/node_modules/next/dist/server/base-http/index.js +54 -0
  1025. package/.next/standalone/node_modules/next/dist/server/base-http/node.js +147 -0
  1026. package/.next/standalone/node_modules/next/dist/server/base-http/web.js +124 -0
  1027. package/.next/standalone/node_modules/next/dist/server/base-server.js +1957 -0
  1028. package/.next/standalone/node_modules/next/dist/server/body-streams.js +122 -0
  1029. package/.next/standalone/node_modules/next/dist/server/cache-dir.js +26 -0
  1030. package/.next/standalone/node_modules/next/dist/server/capsize-font-metrics.json +181516 -0
  1031. package/.next/standalone/node_modules/next/dist/server/ci-info.js +41 -0
  1032. package/.next/standalone/node_modules/next/dist/server/client-component-renderer-logger.js +70 -0
  1033. package/.next/standalone/node_modules/next/dist/server/config-schema.js +800 -0
  1034. package/.next/standalone/node_modules/next/dist/server/config-shared.js +395 -0
  1035. package/.next/standalone/node_modules/next/dist/server/config-utils.js +185 -0
  1036. package/.next/standalone/node_modules/next/dist/server/config.js +1836 -0
  1037. package/.next/standalone/node_modules/next/dist/server/create-deduped-by-callsite-server-error-logger.js +96 -0
  1038. package/.next/standalone/node_modules/next/dist/server/crypto-utils.js +70 -0
  1039. package/.next/standalone/node_modules/next/dist/server/dev/browser-logs/file-logger.js +186 -0
  1040. package/.next/standalone/node_modules/next/dist/server/dev/browser-logs/receive-logs.js +610 -0
  1041. package/.next/standalone/node_modules/next/dist/server/dev/browser-logs/source-map.js +222 -0
  1042. package/.next/standalone/node_modules/next/dist/server/dev/debug-channel.js +112 -0
  1043. package/.next/standalone/node_modules/next/dist/server/dev/dev-indicator-server-state.js +15 -0
  1044. package/.next/standalone/node_modules/next/dist/server/dev/dev-validation-worker-pool.js +232 -0
  1045. package/.next/standalone/node_modules/next/dist/server/dev/dev-validation-worker.js +308 -0
  1046. package/.next/standalone/node_modules/next/dist/server/dev/get-source-map-from-file.js +100 -0
  1047. package/.next/standalone/node_modules/next/dist/server/dev/hot-middleware.js +276 -0
  1048. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-rspack.js +171 -0
  1049. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-shared-utils.js +56 -0
  1050. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +1756 -0
  1051. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-types.js +60 -0
  1052. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1380 -0
  1053. package/.next/standalone/node_modules/next/dist/server/dev/log-requests.js +152 -0
  1054. package/.next/standalone/node_modules/next/dist/server/dev/messages.js +74 -0
  1055. package/.next/standalone/node_modules/next/dist/server/dev/middleware-turbopack.js +422 -0
  1056. package/.next/standalone/node_modules/next/dist/server/dev/middleware-webpack.js +495 -0
  1057. package/.next/standalone/node_modules/next/dist/server/dev/node-stack-frames.js +81 -0
  1058. package/.next/standalone/node_modules/next/dist/server/dev/on-demand-entry-handler.js +773 -0
  1059. package/.next/standalone/node_modules/next/dist/server/dev/parse-version-info.js +108 -0
  1060. package/.next/standalone/node_modules/next/dist/server/dev/require-cache.js +109 -0
  1061. package/.next/standalone/node_modules/next/dist/server/dev/serialized-errors.js +67 -0
  1062. package/.next/standalone/node_modules/next/dist/server/dev/server-action-logger.js +37 -0
  1063. package/.next/standalone/node_modules/next/dist/server/dev/static-paths-worker.js +106 -0
  1064. package/.next/standalone/node_modules/next/dist/server/dev/turbopack-utils.js +727 -0
  1065. package/.next/standalone/node_modules/next/dist/server/dev/use-cache-probe-pool.js +177 -0
  1066. package/.next/standalone/node_modules/next/dist/server/dev/use-cache-probe-worker.js +180 -0
  1067. package/.next/standalone/node_modules/next/dist/server/dynamic-rendering-utils.js +285 -0
  1068. package/.next/standalone/node_modules/next/dist/server/font-utils.js +43 -0
  1069. package/.next/standalone/node_modules/next/dist/server/get-app-route-from-entrypoint.js +30 -0
  1070. package/.next/standalone/node_modules/next/dist/server/get-page-files.js +40 -0
  1071. package/.next/standalone/node_modules/next/dist/server/get-route-from-entrypoint.js +35 -0
  1072. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +1262 -0
  1073. package/.next/standalone/node_modules/next/dist/server/instrumentation/types.js +6 -0
  1074. package/.next/standalone/node_modules/next/dist/server/instrumentation/utils.js +21 -0
  1075. package/.next/standalone/node_modules/next/dist/server/internal-utils.js +39 -0
  1076. package/.next/standalone/node_modules/next/dist/server/is-private-ip.js +36 -0
  1077. package/.next/standalone/node_modules/next/dist/server/lib/app-dir-module.js +59 -0
  1078. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +135 -0
  1079. package/.next/standalone/node_modules/next/dist/server/lib/async-callback-set.js +29 -0
  1080. package/.next/standalone/node_modules/next/dist/server/lib/cache-control.js +22 -0
  1081. package/.next/standalone/node_modules/next/dist/server/lib/cache-handlers/default.external.js +18 -0
  1082. package/.next/standalone/node_modules/next/dist/server/lib/cache-handlers/default.js +196 -0
  1083. package/.next/standalone/node_modules/next/dist/server/lib/cache-handlers/types.js +8 -0
  1084. package/.next/standalone/node_modules/next/dist/server/lib/chrome-devtools-workspace.js +116 -0
  1085. package/.next/standalone/node_modules/next/dist/server/lib/clone-response.js +77 -0
  1086. package/.next/standalone/node_modules/next/dist/server/lib/cpu-profile.js +56 -0
  1087. package/.next/standalone/node_modules/next/dist/server/lib/decode-query-path-parameter.js +26 -0
  1088. package/.next/standalone/node_modules/next/dist/server/lib/dedupe-fetch.js +163 -0
  1089. package/.next/standalone/node_modules/next/dist/server/lib/dev-bundler-service.js +110 -0
  1090. package/.next/standalone/node_modules/next/dist/server/lib/disk-lru-cache.external.js +63 -0
  1091. package/.next/standalone/node_modules/next/dist/server/lib/encode-header-safe.js +44 -0
  1092. package/.next/standalone/node_modules/next/dist/server/lib/etag.js +56 -0
  1093. package/.next/standalone/node_modules/next/dist/server/lib/experimental/create-env-definitions.js +48 -0
  1094. package/.next/standalone/node_modules/next/dist/server/lib/experimental/ppr.js +47 -0
  1095. package/.next/standalone/node_modules/next/dist/server/lib/find-page-file.js +147 -0
  1096. package/.next/standalone/node_modules/next/dist/server/lib/fix-mojibake.js +25 -0
  1097. package/.next/standalone/node_modules/next/dist/server/lib/format-hostname.js +16 -0
  1098. package/.next/standalone/node_modules/next/dist/server/lib/generate-agent-files.js +180 -0
  1099. package/.next/standalone/node_modules/next/dist/server/lib/i18n-provider.js +129 -0
  1100. package/.next/standalone/node_modules/next/dist/server/lib/implicit-tags.js +87 -0
  1101. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +348 -0
  1102. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +547 -0
  1103. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/memory-cache.external.js +59 -0
  1104. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +73 -0
  1105. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/tags-manifest.external.js +55 -0
  1106. package/.next/standalone/node_modules/next/dist/server/lib/install-code-frame.js +22 -0
  1107. package/.next/standalone/node_modules/next/dist/server/lib/is-ipv6.js +41 -0
  1108. package/.next/standalone/node_modules/next/dist/server/lib/is-non-html-sec-fetch-dest.js +38 -0
  1109. package/.next/standalone/node_modules/next/dist/server/lib/is-rsc-request.js +18 -0
  1110. package/.next/standalone/node_modules/next/dist/server/lib/lazy-module.js +80 -0
  1111. package/.next/standalone/node_modules/next/dist/server/lib/lazy-result.js +46 -0
  1112. package/.next/standalone/node_modules/next/dist/server/lib/lru-cache.js +191 -0
  1113. package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +18 -0
  1114. package/.next/standalone/node_modules/next/dist/server/lib/mock-request.js +441 -0
  1115. package/.next/standalone/node_modules/next/dist/server/lib/module-loader/module-loader.js +8 -0
  1116. package/.next/standalone/node_modules/next/dist/server/lib/module-loader/node-module-loader.js +25 -0
  1117. package/.next/standalone/node_modules/next/dist/server/lib/module-loader/route-module-loader.js +26 -0
  1118. package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +28 -0
  1119. package/.next/standalone/node_modules/next/dist/server/lib/params-utils.js +70 -0
  1120. package/.next/standalone/node_modules/next/dist/server/lib/parse-stack.js +46 -0
  1121. package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +1027 -0
  1122. package/.next/standalone/node_modules/next/dist/server/lib/patch-set-header.js +45 -0
  1123. package/.next/standalone/node_modules/next/dist/server/lib/postponed-request-body.js +64 -0
  1124. package/.next/standalone/node_modules/next/dist/server/lib/release-compression-stream.js +24 -0
  1125. package/.next/standalone/node_modules/next/dist/server/lib/render-server.js +157 -0
  1126. package/.next/standalone/node_modules/next/dist/server/lib/router-server.js +756 -0
  1127. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +110 -0
  1128. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/build-data-route.js +48 -0
  1129. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.js +50 -0
  1130. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/cache-life-type-utils.js +212 -0
  1131. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/decode-path-params.js +42 -0
  1132. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/filesystem.js +612 -0
  1133. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js +89 -0
  1134. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/instrumentation-node-extensions.js +114 -0
  1135. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/is-postpone.js +16 -0
  1136. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +128 -0
  1137. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/resolve-routes.js +646 -0
  1138. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/root-params-type-utils.js +61 -0
  1139. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/route-types-utils.js +326 -0
  1140. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/router-server-context.js +26 -0
  1141. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/typegen.js +892 -0
  1142. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/types.js +6 -0
  1143. package/.next/standalone/node_modules/next/dist/server/lib/server-action-request-meta.js +53 -0
  1144. package/.next/standalone/node_modules/next/dist/server/lib/server-ipc/utils.js +83 -0
  1145. package/.next/standalone/node_modules/next/dist/server/lib/source-maps.js +241 -0
  1146. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +540 -0
  1147. package/.next/standalone/node_modules/next/dist/server/lib/streaming-metadata.js +27 -0
  1148. package/.next/standalone/node_modules/next/dist/server/lib/to-route.js +37 -0
  1149. package/.next/standalone/node_modules/next/dist/server/lib/trace/constants.js +208 -0
  1150. package/.next/standalone/node_modules/next/dist/server/lib/trace/local-span-recorder.js +385 -0
  1151. package/.next/standalone/node_modules/next/dist/server/lib/trace/request-insights-identity.js +39 -0
  1152. package/.next/standalone/node_modules/next/dist/server/lib/trace/request-insights.js +308 -0
  1153. package/.next/standalone/node_modules/next/dist/server/lib/trace/span-store.js +69 -0
  1154. package/.next/standalone/node_modules/next/dist/server/lib/trace/tracer.js +392 -0
  1155. package/.next/standalone/node_modules/next/dist/server/lib/trace/utils.js +16 -0
  1156. package/.next/standalone/node_modules/next/dist/server/lib/types.js +6 -0
  1157. package/.next/standalone/node_modules/next/dist/server/lib/utils.js +292 -0
  1158. package/.next/standalone/node_modules/next/dist/server/lib/worker-utils.js +36 -0
  1159. package/.next/standalone/node_modules/next/dist/server/load-components.js +185 -0
  1160. package/.next/standalone/node_modules/next/dist/server/load-default-error-components.js +41 -0
  1161. package/.next/standalone/node_modules/next/dist/server/load-manifest.external.js +117 -0
  1162. package/.next/standalone/node_modules/next/dist/server/match-bundle.js +25 -0
  1163. package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +49 -0
  1164. package/.next/standalone/node_modules/next/dist/server/mcp/get-or-create-mcp-server.js +51 -0
  1165. package/.next/standalone/node_modules/next/dist/server/mcp/mcp-telemetry-tracker.js +82 -0
  1166. package/.next/standalone/node_modules/next/dist/server/mcp/tools/compile-route.js +86 -0
  1167. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-compilation-issues.js +68 -0
  1168. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-errors.js +114 -0
  1169. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-logs.js +67 -0
  1170. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-page-metadata.js +188 -0
  1171. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-project-metadata.js +59 -0
  1172. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-request-insights.js +57 -0
  1173. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-routes.js +136 -0
  1174. package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-server-action-by-id.js +121 -0
  1175. package/.next/standalone/node_modules/next/dist/server/mcp/tools/next-instance-error-state.js +31 -0
  1176. package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/browser-communication.js +90 -0
  1177. package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/format-compilation-issues.js +72 -0
  1178. package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/format-errors.js +137 -0
  1179. package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/resolve-path-to-route.js +49 -0
  1180. package/.next/standalone/node_modules/next/dist/server/next-server.js +1443 -0
  1181. package/.next/standalone/node_modules/next/dist/server/next-typescript.js +13 -0
  1182. package/.next/standalone/node_modules/next/dist/server/next.js +485 -0
  1183. package/.next/standalone/node_modules/next/dist/server/node-environment-baseline.js +25 -0
  1184. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/console-dim.external.js +301 -0
  1185. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/console-exit.js +49 -0
  1186. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/console-file.js +56 -0
  1187. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/date.js +52 -0
  1188. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/error-inspect.js +8 -0
  1189. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.js +577 -0
  1190. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/global-behaviors.js +33 -0
  1191. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/io-utils.js +102 -0
  1192. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/node-crypto.js +103 -0
  1193. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/process-error-handlers.js +170 -0
  1194. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/random.js +28 -0
  1195. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.js +518 -0
  1196. package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/web-crypto.js +45 -0
  1197. package/.next/standalone/node_modules/next/dist/server/node-environment.js +19 -0
  1198. package/.next/standalone/node_modules/next/dist/server/node-polyfill-crypto.js +21 -0
  1199. package/.next/standalone/node_modules/next/dist/server/normalizers/absolute-filename-normalizer.js +33 -0
  1200. package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-bundle-path-normalizer.js +54 -0
  1201. package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-filename-normalizer.js +22 -0
  1202. package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-page-normalizer.js +40 -0
  1203. package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-pathname-normalizer.js +55 -0
  1204. package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/index.js +42 -0
  1205. package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/index.js +44 -0
  1206. package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-bundle-path-normalizer.js +55 -0
  1207. package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-filename-normalizer.js +22 -0
  1208. package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-page-normalizer.js +19 -0
  1209. package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-pathname-normalizer.js +19 -0
  1210. package/.next/standalone/node_modules/next/dist/server/normalizers/locale-route-normalizer.js +26 -0
  1211. package/.next/standalone/node_modules/next/dist/server/normalizers/normalizer.js +6 -0
  1212. package/.next/standalone/node_modules/next/dist/server/normalizers/normalizers.js +23 -0
  1213. package/.next/standalone/node_modules/next/dist/server/normalizers/prefixing-normalizer.js +26 -0
  1214. package/.next/standalone/node_modules/next/dist/server/normalizers/request/base-path.js +25 -0
  1215. package/.next/standalone/node_modules/next/dist/server/normalizers/request/next-data.js +38 -0
  1216. package/.next/standalone/node_modules/next/dist/server/normalizers/request/pathname-normalizer.js +6 -0
  1217. package/.next/standalone/node_modules/next/dist/server/normalizers/request/prefix.js +39 -0
  1218. package/.next/standalone/node_modules/next/dist/server/normalizers/request/rsc.js +19 -0
  1219. package/.next/standalone/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.js +33 -0
  1220. package/.next/standalone/node_modules/next/dist/server/normalizers/request/suffix.js +27 -0
  1221. package/.next/standalone/node_modules/next/dist/server/normalizers/underscore-normalizer.js +17 -0
  1222. package/.next/standalone/node_modules/next/dist/server/normalizers/wrap-normalizer-fn.js +17 -0
  1223. package/.next/standalone/node_modules/next/dist/server/og/cache-image-response.js +341 -0
  1224. package/.next/standalone/node_modules/next/dist/server/og/image-response.js +72 -0
  1225. package/.next/standalone/node_modules/next/dist/server/patch-error-inspect.js +507 -0
  1226. package/.next/standalone/node_modules/next/dist/server/pipe-readable.js +212 -0
  1227. package/.next/standalone/node_modules/next/dist/server/post-process.js +39 -0
  1228. package/.next/standalone/node_modules/next/dist/server/render-result.js +246 -0
  1229. package/.next/standalone/node_modules/next/dist/server/render.js +1150 -0
  1230. package/.next/standalone/node_modules/next/dist/server/request/connection.js +135 -0
  1231. package/.next/standalone/node_modules/next/dist/server/request/cookies.js +239 -0
  1232. package/.next/standalone/node_modules/next/dist/server/request/draft-mode.js +238 -0
  1233. package/.next/standalone/node_modules/next/dist/server/request/fallback-params.js +95 -0
  1234. package/.next/standalone/node_modules/next/dist/server/request/headers.js +235 -0
  1235. package/.next/standalone/node_modules/next/dist/server/request/io.js +96 -0
  1236. package/.next/standalone/node_modules/next/dist/server/request/params.js +634 -0
  1237. package/.next/standalone/node_modules/next/dist/server/request/pathname.js +141 -0
  1238. package/.next/standalone/node_modules/next/dist/server/request/root-params.js +202 -0
  1239. package/.next/standalone/node_modules/next/dist/server/request/search-params.js +582 -0
  1240. package/.next/standalone/node_modules/next/dist/server/request/utils.js +84 -0
  1241. package/.next/standalone/node_modules/next/dist/server/request-meta.js +55 -0
  1242. package/.next/standalone/node_modules/next/dist/server/require-hook.js +82 -0
  1243. package/.next/standalone/node_modules/next/dist/server/require.js +109 -0
  1244. package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +323 -0
  1245. package/.next/standalone/node_modules/next/dist/server/response-cache/types.js +41 -0
  1246. package/.next/standalone/node_modules/next/dist/server/response-cache/utils.js +104 -0
  1247. package/.next/standalone/node_modules/next/dist/server/response-cache/web.js +101 -0
  1248. package/.next/standalone/node_modules/next/dist/server/resume-data-cache/cache-store.js +101 -0
  1249. package/.next/standalone/node_modules/next/dist/server/resume-data-cache/resume-data-cache.js +142 -0
  1250. package/.next/standalone/node_modules/next/dist/server/revalidation-utils.js +156 -0
  1251. package/.next/standalone/node_modules/next/dist/server/route-definitions/app-page-route-definition.js +16 -0
  1252. package/.next/standalone/node_modules/next/dist/server/route-definitions/app-route-route-definition.js +6 -0
  1253. package/.next/standalone/node_modules/next/dist/server/route-definitions/locale-route-definition.js +6 -0
  1254. package/.next/standalone/node_modules/next/dist/server/route-definitions/pages-api-route-definition.js +6 -0
  1255. package/.next/standalone/node_modules/next/dist/server/route-definitions/pages-route-definition.js +6 -0
  1256. package/.next/standalone/node_modules/next/dist/server/route-definitions/route-definition.js +6 -0
  1257. package/.next/standalone/node_modules/next/dist/server/route-kind.js +32 -0
  1258. package/.next/standalone/node_modules/next/dist/server/route-matcher-managers/default-route-matcher-manager.js +239 -0
  1259. package/.next/standalone/node_modules/next/dist/server/route-matcher-managers/dev-route-matcher-manager.js +120 -0
  1260. package/.next/standalone/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.js +6 -0
  1261. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +55 -0
  1262. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-route-route-matcher-provider.js +43 -0
  1263. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-app-page-route-matcher-provider.js +91 -0
  1264. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-app-route-route-matcher-provider.js +112 -0
  1265. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-pages-api-route-matcher-provider.js +77 -0
  1266. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-pages-route-matcher-provider.js +77 -0
  1267. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/file-cache-route-matcher-provider.js +28 -0
  1268. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/helpers/file-reader/batched-file-reader.js +106 -0
  1269. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/helpers/file-reader/default-file-reader.js +42 -0
  1270. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/helpers/file-reader/file-reader.js +6 -0
  1271. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/cached-route-matcher-provider.js +30 -0
  1272. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/manifest-loaders/manifest-loader.js +6 -0
  1273. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.js +34 -0
  1274. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/manifest-loaders/server-manifest-loader.js +20 -0
  1275. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/manifest-route-matcher-provider.js +21 -0
  1276. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/pages-api-route-matcher-provider.js +55 -0
  1277. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/pages-route-matcher-provider.js +64 -0
  1278. package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.js +6 -0
  1279. package/.next/standalone/node_modules/next/dist/server/route-matchers/app-page-route-matcher.js +18 -0
  1280. package/.next/standalone/node_modules/next/dist/server/route-matchers/app-route-route-matcher.js +15 -0
  1281. package/.next/standalone/node_modules/next/dist/server/route-matchers/locale-route-matcher.js +68 -0
  1282. package/.next/standalone/node_modules/next/dist/server/route-matchers/pages-api-route-matcher.js +30 -0
  1283. package/.next/standalone/node_modules/next/dist/server/route-matchers/pages-route-matcher.js +30 -0
  1284. package/.next/standalone/node_modules/next/dist/server/route-matchers/route-matcher.js +54 -0
  1285. package/.next/standalone/node_modules/next/dist/server/route-matches/app-page-route-match.js +6 -0
  1286. package/.next/standalone/node_modules/next/dist/server/route-matches/app-route-route-match.js +6 -0
  1287. package/.next/standalone/node_modules/next/dist/server/route-matches/locale-route-match.js +6 -0
  1288. package/.next/standalone/node_modules/next/dist/server/route-matches/pages-api-route-match.js +16 -0
  1289. package/.next/standalone/node_modules/next/dist/server/route-matches/pages-route-match.js +6 -0
  1290. package/.next/standalone/node_modules/next/dist/server/route-matches/route-match.js +6 -0
  1291. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.js +51 -0
  1292. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +36 -0
  1293. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/module.js +176 -0
  1294. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/module.render.js +24 -0
  1295. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/normalize-request-url.js +62 -0
  1296. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/app-router-context.js +4 -0
  1297. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.js +87 -0
  1298. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/head-manager-context.js +4 -0
  1299. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/hooks-client-context.js +4 -0
  1300. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/image-config-context.js +4 -0
  1301. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/router-context.js +4 -0
  1302. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/server-inserted-html.js +4 -0
  1303. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.js +140 -0
  1304. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-compiler-runtime.js +4 -0
  1305. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-dom.js +4 -0
  1306. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.js +4 -0
  1307. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime.js +4 -0
  1308. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-turbopack-server.js +4 -0
  1309. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-turbopack-static.js +4 -0
  1310. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-server.js +4 -0
  1311. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-static.js +4 -0
  1312. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react.js +4 -0
  1313. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.js +125 -0
  1314. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-compiler-runtime.js +4 -0
  1315. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-dom-server.js +4 -0
  1316. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-dom.js +4 -0
  1317. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.js +4 -0
  1318. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-runtime.js +4 -0
  1319. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-turbopack-client.js +4 -0
  1320. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-webpack-client.js +4 -0
  1321. package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react.js +4 -0
  1322. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/auto-implement-methods.js +85 -0
  1323. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/clean-url.js +24 -0
  1324. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/get-pathname-from-absolute-path.js +29 -0
  1325. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/is-static-gen-enabled.js +15 -0
  1326. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/parsed-url-query-to-params.js +20 -0
  1327. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/module.compiled.js +36 -0
  1328. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/module.js +948 -0
  1329. package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/shared-modules.js +56 -0
  1330. package/.next/standalone/node_modules/next/dist/server/route-modules/checks.js +45 -0
  1331. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/builtin/_error.js +100 -0
  1332. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +20 -0
  1333. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/module.js +92 -0
  1334. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/module.render.js +24 -0
  1335. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +567 -0
  1336. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/app-router-context.js +4 -0
  1337. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.js +102 -0
  1338. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/head-manager-context.js +4 -0
  1339. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/hooks-client-context.js +4 -0
  1340. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/html-context.js +4 -0
  1341. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/image-config-context.js +4 -0
  1342. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/loadable-context.js +4 -0
  1343. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/loadable.js +4 -0
  1344. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/router-context.js +4 -0
  1345. package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/server-inserted-html.js +4 -0
  1346. package/.next/standalone/node_modules/next/dist/server/route-modules/pages-api/module.compiled.js +20 -0
  1347. package/.next/standalone/node_modules/next/dist/server/route-modules/pages-api/module.js +58 -0
  1348. package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +697 -0
  1349. package/.next/standalone/node_modules/next/dist/server/runtime-reacts.external.js +46 -0
  1350. package/.next/standalone/node_modules/next/dist/server/send-payload.js +88 -0
  1351. package/.next/standalone/node_modules/next/dist/server/send-response.js +70 -0
  1352. package/.next/standalone/node_modules/next/dist/server/serve-static.js +66 -0
  1353. package/.next/standalone/node_modules/next/dist/server/server-route-utils.js +30 -0
  1354. package/.next/standalone/node_modules/next/dist/server/server-utils.js +340 -0
  1355. package/.next/standalone/node_modules/next/dist/server/setup-http-agent-env.js +31 -0
  1356. package/.next/standalone/node_modules/next/dist/server/stream-utils/encoded-tags.js +122 -0
  1357. package/.next/standalone/node_modules/next/dist/server/stream-utils/node-buffered-transform-stream.js +51 -0
  1358. package/.next/standalone/node_modules/next/dist/server/stream-utils/node-web-streams-helper.js +939 -0
  1359. package/.next/standalone/node_modules/next/dist/server/stream-utils/uint8array-helpers.js +74 -0
  1360. package/.next/standalone/node_modules/next/dist/server/typescript/constant.js +98 -0
  1361. package/.next/standalone/node_modules/next/dist/server/typescript/index.js +199 -0
  1362. package/.next/standalone/node_modules/next/dist/server/typescript/rules/client-boundary.js +100 -0
  1363. package/.next/standalone/node_modules/next/dist/server/typescript/rules/config.js +534 -0
  1364. package/.next/standalone/node_modules/next/dist/server/typescript/rules/entry.js +149 -0
  1365. package/.next/standalone/node_modules/next/dist/server/typescript/rules/error.js +38 -0
  1366. package/.next/standalone/node_modules/next/dist/server/typescript/rules/metadata.js +234 -0
  1367. package/.next/standalone/node_modules/next/dist/server/typescript/rules/server-boundary.js +115 -0
  1368. package/.next/standalone/node_modules/next/dist/server/typescript/rules/server.js +79 -0
  1369. package/.next/standalone/node_modules/next/dist/server/typescript/utils.js +212 -0
  1370. package/.next/standalone/node_modules/next/dist/server/use-cache/cache-life-profile.js +84 -0
  1371. package/.next/standalone/node_modules/next/dist/server/use-cache/cache-life.js +102 -0
  1372. package/.next/standalone/node_modules/next/dist/server/use-cache/cache-tag.js +52 -0
  1373. package/.next/standalone/node_modules/next/dist/server/use-cache/clone-cache-entry.js +28 -0
  1374. package/.next/standalone/node_modules/next/dist/server/use-cache/constants.js +44 -0
  1375. package/.next/standalone/node_modules/next/dist/server/use-cache/handlers.js +274 -0
  1376. package/.next/standalone/node_modules/next/dist/server/use-cache/tiered-cache-handler.js +145 -0
  1377. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-errors.js +59 -0
  1378. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-probe-globals.js +39 -0
  1379. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-probe-scheduler.js +117 -0
  1380. package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +2610 -0
  1381. package/.next/standalone/node_modules/next/dist/server/utils.js +73 -0
  1382. package/.next/standalone/node_modules/next/dist/server/web/adapter.js +428 -0
  1383. package/.next/standalone/node_modules/next/dist/server/web/edge-route-module-wrapper.js +136 -0
  1384. package/.next/standalone/node_modules/next/dist/server/web/error.js +69 -0
  1385. package/.next/standalone/node_modules/next/dist/server/web/exports/index.js +56 -0
  1386. package/.next/standalone/node_modules/next/dist/server/web/get-edge-preview-props.js +23 -0
  1387. package/.next/standalone/node_modules/next/dist/server/web/globals.js +128 -0
  1388. package/.next/standalone/node_modules/next/dist/server/web/http.js +39 -0
  1389. package/.next/standalone/node_modules/next/dist/server/web/internal-edge-wait-until.js +64 -0
  1390. package/.next/standalone/node_modules/next/dist/server/web/next-url.js +199 -0
  1391. package/.next/standalone/node_modules/next/dist/server/web/sandbox/context.js +468 -0
  1392. package/.next/standalone/node_modules/next/dist/server/web/sandbox/fetch-inline-assets.js +35 -0
  1393. package/.next/standalone/node_modules/next/dist/server/web/sandbox/index.js +28 -0
  1394. package/.next/standalone/node_modules/next/dist/server/web/sandbox/resource-managers.js +123 -0
  1395. package/.next/standalone/node_modules/next/dist/server/web/sandbox/sandbox.js +137 -0
  1396. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/headers.js +293 -0
  1397. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/next-request.js +142 -0
  1398. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/reflect.js +30 -0
  1399. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.js +226 -0
  1400. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/cookies.js +29 -0
  1401. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/fetch-event.js +98 -0
  1402. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/image-response.js +22 -0
  1403. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/request.js +99 -0
  1404. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/response.js +136 -0
  1405. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/revalidate.js +226 -0
  1406. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +307 -0
  1407. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-no-store.js +51 -0
  1408. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/url-pattern.js +13 -0
  1409. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/user-agent.js +46 -0
  1410. package/.next/standalone/node_modules/next/dist/server/web/types.js +6 -0
  1411. package/.next/standalone/node_modules/next/dist/server/web/utils.js +151 -0
  1412. package/.next/standalone/node_modules/next/dist/server/web/web-on-close.js +87 -0
  1413. package/.next/standalone/node_modules/next/dist/shared/lib/action-revalidation-kind.js +31 -0
  1414. package/.next/standalone/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.js +50 -0
  1415. package/.next/standalone/node_modules/next/dist/shared/lib/app-router-types.js +159 -0
  1416. package/.next/standalone/node_modules/next/dist/shared/lib/constants.js +453 -0
  1417. package/.next/standalone/node_modules/next/dist/shared/lib/deep-freeze.js +30 -0
  1418. package/.next/standalone/node_modules/next/dist/shared/lib/deployment-id.js +63 -0
  1419. package/.next/standalone/node_modules/next/dist/shared/lib/dset.js +34 -0
  1420. package/.next/standalone/node_modules/next/dist/shared/lib/encode-uri-path.js +15 -0
  1421. package/.next/standalone/node_modules/next/dist/shared/lib/entry-constants.js +36 -0
  1422. package/.next/standalone/node_modules/next/dist/shared/lib/error-source.js +36 -0
  1423. package/.next/standalone/node_modules/next/dist/shared/lib/errors/code-frame.js +20 -0
  1424. package/.next/standalone/node_modules/next/dist/shared/lib/errors/constants.js +19 -0
  1425. package/.next/standalone/node_modules/next/dist/shared/lib/errors/empty-generate-static-params-error.js +31 -0
  1426. package/.next/standalone/node_modules/next/dist/shared/lib/errors/hard-deprecated-config-error.js +25 -0
  1427. package/.next/standalone/node_modules/next/dist/shared/lib/escape-regexp.js +22 -0
  1428. package/.next/standalone/node_modules/next/dist/shared/lib/get-hostname.js +23 -0
  1429. package/.next/standalone/node_modules/next/dist/shared/lib/hash.js +42 -0
  1430. package/.next/standalone/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.js +18 -0
  1431. package/.next/standalone/node_modules/next/dist/shared/lib/head.js +164 -0
  1432. package/.next/standalone/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.js +60 -0
  1433. package/.next/standalone/node_modules/next/dist/shared/lib/html-context.shared-runtime.js +40 -0
  1434. package/.next/standalone/node_modules/next/dist/shared/lib/htmlescape.js +52 -0
  1435. package/.next/standalone/node_modules/next/dist/shared/lib/i18n/detect-domain-locale.js +25 -0
  1436. package/.next/standalone/node_modules/next/dist/shared/lib/i18n/get-locale-redirect.js +59 -0
  1437. package/.next/standalone/node_modules/next/dist/shared/lib/i18n/normalize-locale-path.js +54 -0
  1438. package/.next/standalone/node_modules/next/dist/shared/lib/image-blur-svg.js +22 -0
  1439. package/.next/standalone/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.js +19 -0
  1440. package/.next/standalone/node_modules/next/dist/shared/lib/image-config.js +77 -0
  1441. package/.next/standalone/node_modules/next/dist/shared/lib/instant-messages.js +43 -0
  1442. package/.next/standalone/node_modules/next/dist/shared/lib/invariant-error.js +23 -0
  1443. package/.next/standalone/node_modules/next/dist/shared/lib/is-plain-object.js +42 -0
  1444. package/.next/standalone/node_modules/next/dist/shared/lib/is-thenable.js +20 -0
  1445. package/.next/standalone/node_modules/next/dist/shared/lib/isomorphic/path.js +14 -0
  1446. package/.next/standalone/node_modules/next/dist/shared/lib/lazy-dynamic/bailout-to-csr.js +37 -0
  1447. package/.next/standalone/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.js +19 -0
  1448. package/.next/standalone/node_modules/next/dist/shared/lib/loadable.shared-runtime.js +246 -0
  1449. package/.next/standalone/node_modules/next/dist/shared/lib/match-local-pattern.js +46 -0
  1450. package/.next/standalone/node_modules/next/dist/shared/lib/match-remote-pattern.js +63 -0
  1451. package/.next/standalone/node_modules/next/dist/shared/lib/modern-browserslist-target.js +15 -0
  1452. package/.next/standalone/node_modules/next/dist/shared/lib/no-fallback-error.external.js +18 -0
  1453. package/.next/standalone/node_modules/next/dist/shared/lib/normalized-asset-prefix.js +28 -0
  1454. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/absolute-path-to-page.js +26 -0
  1455. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/denormalize-page-path.js +18 -0
  1456. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/ensure-leading-slash.js +18 -0
  1457. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/get-page-paths.js +40 -0
  1458. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/normalize-data-path.js +23 -0
  1459. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/normalize-page-path.js +26 -0
  1460. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/normalize-path-sep.js +19 -0
  1461. package/.next/standalone/node_modules/next/dist/shared/lib/page-path/remove-page-path-tail.js +20 -0
  1462. package/.next/standalone/node_modules/next/dist/shared/lib/promise-with-resolvers.js +26 -0
  1463. package/.next/standalone/node_modules/next/dist/shared/lib/request-insights.js +30 -0
  1464. package/.next/standalone/node_modules/next/dist/shared/lib/router/adapters.js +139 -0
  1465. package/.next/standalone/node_modules/next/dist/shared/lib/router/routes/app.js +158 -0
  1466. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/add-locale.js +28 -0
  1467. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/add-path-prefix.js +20 -0
  1468. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/add-path-suffix.js +20 -0
  1469. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +62 -0
  1470. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/as-path-to-search-params.js +17 -0
  1471. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/cache-busting-search-param.js +70 -0
  1472. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +16 -0
  1473. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/format-next-pathname-info.js +27 -0
  1474. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/format-url.js +111 -0
  1475. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-dynamic-param.js +174 -0
  1476. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-next-pathname-info.js +52 -0
  1477. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-route-from-asset-path.js +25 -0
  1478. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-segment-param.js +90 -0
  1479. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/html-bots.js +17 -0
  1480. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/index.js +30 -0
  1481. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/interception-prefix-from-param-type.js +33 -0
  1482. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/interception-routes.js +108 -0
  1483. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/is-bot.js +57 -0
  1484. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/is-dynamic.js +26 -0
  1485. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.js +31 -0
  1486. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-loader-tree.js +30 -0
  1487. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-path.js +33 -0
  1488. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.js +43 -0
  1489. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-url.js +38 -0
  1490. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/path-has-prefix.js +20 -0
  1491. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/path-match.js +49 -0
  1492. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +293 -0
  1493. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/querystring.js +79 -0
  1494. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/relativize-url.js +40 -0
  1495. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/remove-path-prefix.js +39 -0
  1496. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/remove-trailing-slash.js +22 -0
  1497. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/resolve-param-value.js +116 -0
  1498. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/route-match-utils.js +113 -0
  1499. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/route-matcher.js +45 -0
  1500. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/route-regex.js +256 -0
  1501. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/sorted-routes.js +244 -0
  1502. package/.next/standalone/node_modules/next/dist/shared/lib/router-context.shared-runtime.js +18 -0
  1503. package/.next/standalone/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.js +99 -0
  1504. package/.next/standalone/node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.js +66 -0
  1505. package/.next/standalone/node_modules/next/dist/shared/lib/segment.js +102 -0
  1506. package/.next/standalone/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.js +35 -0
  1507. package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +68 -0
  1508. package/.next/standalone/node_modules/next/dist/shared/lib/side-effect.js +63 -0
  1509. package/.next/standalone/node_modules/next/dist/shared/lib/size-limit.js +40 -0
  1510. package/.next/standalone/node_modules/next/dist/shared/lib/turbopack/internal-error.js +53 -0
  1511. package/.next/standalone/node_modules/next/dist/shared/lib/utils/reflect-utils.js +69 -0
  1512. package/.next/standalone/node_modules/next/dist/shared/lib/utils/warn-once.js +22 -0
  1513. package/.next/standalone/node_modules/next/dist/shared/lib/utils.js +216 -0
  1514. package/.next/standalone/node_modules/next/dist/shared/lib/zod.js +101 -0
  1515. package/.next/standalone/node_modules/next/dist/telemetry/agent-name.js +20 -0
  1516. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +91 -0
  1517. package/.next/standalone/node_modules/next/dist/telemetry/detached-flush.js +56 -0
  1518. package/.next/standalone/node_modules/next/dist/telemetry/events/build.js +187 -0
  1519. package/.next/standalone/node_modules/next/dist/telemetry/events/index.js +41 -0
  1520. package/.next/standalone/node_modules/next/dist/telemetry/events/plugins.js +51 -0
  1521. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +55 -0
  1522. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +68 -0
  1523. package/.next/standalone/node_modules/next/dist/telemetry/flush-telemetry.js +19 -0
  1524. package/.next/standalone/node_modules/next/dist/telemetry/post-telemetry-payload.js +48 -0
  1525. package/.next/standalone/node_modules/next/dist/telemetry/project-id.js +48 -0
  1526. package/.next/standalone/node_modules/next/dist/telemetry/storage.js +305 -0
  1527. package/.next/standalone/node_modules/next/dist/trace/index.js +58 -0
  1528. package/.next/standalone/node_modules/next/dist/trace/report/index.js +37 -0
  1529. package/.next/standalone/node_modules/next/dist/trace/report/to-json-build.js +40 -0
  1530. package/.next/standalone/node_modules/next/dist/trace/report/to-json.js +171 -0
  1531. package/.next/standalone/node_modules/next/dist/trace/report/to-telemetry.js +36 -0
  1532. package/.next/standalone/node_modules/next/dist/trace/shared.js +39 -0
  1533. package/.next/standalone/node_modules/next/dist/trace/trace.js +211 -0
  1534. package/.next/standalone/node_modules/next/package.json +139 -0
  1535. package/.next/standalone/node_modules/react/cjs/react-compiler-runtime.production.js +16 -0
  1536. package/.next/standalone/node_modules/react/cjs/react-jsx-dev-runtime.production.js +14 -0
  1537. package/.next/standalone/node_modules/react/cjs/react-jsx-runtime.production.js +34 -0
  1538. package/.next/standalone/node_modules/react/cjs/react.development.js +1284 -0
  1539. package/.next/standalone/node_modules/react/cjs/react.production.js +542 -0
  1540. package/.next/standalone/node_modules/react/compiler-runtime.js +14 -0
  1541. package/.next/standalone/node_modules/react/index.js +7 -0
  1542. package/.next/standalone/node_modules/react/jsx-dev-runtime.js +7 -0
  1543. package/.next/standalone/node_modules/react/jsx-runtime.js +7 -0
  1544. package/.next/standalone/node_modules/react/package.json +51 -0
  1545. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +6603 -0
  1546. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +6692 -0
  1547. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +7410 -0
  1548. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +7512 -0
  1549. package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +7707 -0
  1550. package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +210 -0
  1551. package/.next/standalone/node_modules/react-dom/index.js +38 -0
  1552. package/.next/standalone/node_modules/react-dom/package.json +117 -0
  1553. package/.next/standalone/node_modules/react-dom/server.browser.js +16 -0
  1554. package/.next/standalone/node_modules/react-dom/server.edge.js +17 -0
  1555. package/.next/standalone/node_modules/react-dom/server.node.js +18 -0
  1556. package/.next/standalone/node_modules/react-dom/static.node.js +14 -0
  1557. package/.next/standalone/node_modules/semver/package.json +38 -0
  1558. package/.next/standalone/node_modules/sharp/dist/channel.cjs +177 -0
  1559. package/.next/standalone/node_modules/sharp/dist/channel.mjs +177 -0
  1560. package/.next/standalone/node_modules/sharp/dist/colour.cjs +199 -0
  1561. package/.next/standalone/node_modules/sharp/dist/colour.mjs +199 -0
  1562. package/.next/standalone/node_modules/sharp/dist/composite.cjs +213 -0
  1563. package/.next/standalone/node_modules/sharp/dist/composite.mjs +213 -0
  1564. package/.next/standalone/node_modules/sharp/dist/constructor.cjs +511 -0
  1565. package/.next/standalone/node_modules/sharp/dist/constructor.mjs +511 -0
  1566. package/.next/standalone/node_modules/sharp/dist/index.cjs +25 -0
  1567. package/.next/standalone/node_modules/sharp/dist/index.d.cts +1999 -0
  1568. package/.next/standalone/node_modules/sharp/dist/index.d.mts +2046 -0
  1569. package/.next/standalone/node_modules/sharp/dist/index.mjs +25 -0
  1570. package/.next/standalone/node_modules/sharp/dist/input.cjs +819 -0
  1571. package/.next/standalone/node_modules/sharp/dist/input.mjs +819 -0
  1572. package/.next/standalone/node_modules/sharp/dist/is.cjs +143 -0
  1573. package/.next/standalone/node_modules/sharp/dist/is.mjs +143 -0
  1574. package/.next/standalone/node_modules/sharp/dist/libvips.cjs +212 -0
  1575. package/.next/standalone/node_modules/sharp/dist/libvips.mjs +212 -0
  1576. package/.next/standalone/node_modules/sharp/dist/operation.cjs +1002 -0
  1577. package/.next/standalone/node_modules/sharp/dist/operation.mjs +1002 -0
  1578. package/.next/standalone/node_modules/sharp/dist/output.cjs +1785 -0
  1579. package/.next/standalone/node_modules/sharp/dist/output.mjs +1785 -0
  1580. package/.next/standalone/node_modules/sharp/dist/resize.cjs +617 -0
  1581. package/.next/standalone/node_modules/sharp/dist/resize.mjs +617 -0
  1582. package/.next/standalone/node_modules/sharp/dist/sharp.cjs +174 -0
  1583. package/.next/standalone/node_modules/sharp/dist/sharp.mjs +174 -0
  1584. package/.next/standalone/node_modules/sharp/dist/utility.cjs +301 -0
  1585. package/.next/standalone/node_modules/sharp/dist/utility.mjs +301 -0
  1586. package/.next/standalone/node_modules/sharp/node_modules/semver/classes/comparator.js +143 -0
  1587. package/.next/standalone/node_modules/sharp/node_modules/semver/classes/range.js +577 -0
  1588. package/.next/standalone/node_modules/sharp/node_modules/semver/classes/semver.js +350 -0
  1589. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/clean.js +8 -0
  1590. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/cmp.js +54 -0
  1591. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/coerce.js +62 -0
  1592. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare-build.js +9 -0
  1593. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare-loose.js +5 -0
  1594. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare.js +7 -0
  1595. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/diff.js +60 -0
  1596. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/eq.js +5 -0
  1597. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/gt.js +5 -0
  1598. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/gte.js +5 -0
  1599. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/inc.js +21 -0
  1600. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/lt.js +5 -0
  1601. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/lte.js +5 -0
  1602. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/major.js +5 -0
  1603. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/minor.js +5 -0
  1604. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/neq.js +5 -0
  1605. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/parse.js +18 -0
  1606. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/patch.js +5 -0
  1607. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/prerelease.js +8 -0
  1608. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/rcompare.js +5 -0
  1609. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/rsort.js +5 -0
  1610. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/satisfies.js +12 -0
  1611. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/sort.js +5 -0
  1612. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/truncate.js +48 -0
  1613. package/.next/standalone/node_modules/sharp/node_modules/semver/functions/valid.js +8 -0
  1614. package/.next/standalone/node_modules/sharp/node_modules/semver/index.js +93 -0
  1615. package/.next/standalone/node_modules/sharp/node_modules/semver/internal/constants.js +37 -0
  1616. package/.next/standalone/node_modules/sharp/node_modules/semver/internal/debug.js +11 -0
  1617. package/.next/standalone/node_modules/sharp/node_modules/semver/internal/identifiers.js +29 -0
  1618. package/.next/standalone/node_modules/sharp/node_modules/semver/internal/lrucache.js +42 -0
  1619. package/.next/standalone/node_modules/sharp/node_modules/semver/internal/parse-options.js +17 -0
  1620. package/.next/standalone/node_modules/sharp/node_modules/semver/internal/re.js +223 -0
  1621. package/.next/standalone/node_modules/sharp/node_modules/semver/package.json +78 -0
  1622. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/gtr.js +6 -0
  1623. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/intersects.js +9 -0
  1624. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/ltr.js +6 -0
  1625. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/max-satisfying.js +27 -0
  1626. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/min-satisfying.js +26 -0
  1627. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/min-version.js +63 -0
  1628. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/outside.js +82 -0
  1629. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/simplify.js +49 -0
  1630. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/subset.js +249 -0
  1631. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/to-comparators.js +10 -0
  1632. package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/valid.js +13 -0
  1633. package/.next/standalone/node_modules/sharp/package.json +227 -0
  1634. package/.next/standalone/node_modules/styled-jsx/dist/index/index.js +499 -0
  1635. package/.next/standalone/node_modules/styled-jsx/index.js +1 -0
  1636. package/.next/standalone/node_modules/styled-jsx/package.json +132 -0
  1637. package/.next/standalone/node_modules/styled-jsx/style.js +1 -0
  1638. package/.next/standalone/package-lock.json +7160 -0
  1639. package/.next/standalone/package.json +88 -0
  1640. package/.next/standalone/public/demo-data.json +1 -0
  1641. package/.next/standalone/scripts/audit-avatars.mjs +132 -0
  1642. package/.next/standalone/scripts/build-app.mjs +536 -0
  1643. package/.next/standalone/scripts/check-node.mjs +28 -0
  1644. package/.next/standalone/scripts/check-stale-server.mjs +57 -0
  1645. package/.next/standalone/scripts/gen-synthetic.mjs +213 -0
  1646. package/.next/standalone/scripts/image_store.py +125 -0
  1647. package/.next/standalone/scripts/make-dmg-background.mjs +77 -0
  1648. package/.next/standalone/scripts/prepare-standalone.mjs +42 -0
  1649. package/.next/standalone/scripts/requirements.txt +4 -0
  1650. package/.next/standalone/scripts/sandbox.mjs +59 -0
  1651. package/.next/standalone/scripts/score-connections.sql +80 -0
  1652. package/.next/standalone/scripts/score_new_connections.sql +77 -0
  1653. package/.next/standalone/scripts/scrape.py +3574 -0
  1654. package/.next/standalone/scripts/screenshots.py +116 -0
  1655. package/.next/standalone/scripts/update.mjs +84 -0
  1656. package/.next/standalone/server.js +38 -0
  1657. package/.next/static/N9wokeIiC9rUIGmzBhj6T/_buildManifest.js +11 -0
  1658. package/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +6 -0
  1659. package/.next/static/N9wokeIiC9rUIGmzBhj6T/_ssgManifest.js +1 -0
  1660. package/.next/static/chunks/08k1crid-05dt.js +1 -0
  1661. package/.next/static/chunks/0bma92pht_c97.js +1 -0
  1662. package/.next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  1663. package/.next/static/chunks/19mx3mg6lkumu.js +1 -0
  1664. package/.next/static/chunks/1odow92u6wxz0.js +1 -0
  1665. package/.next/static/chunks/1wb_is4wsdm-u.js +1 -0
  1666. package/.next/static/chunks/1zmpcmngg5c7-.js +2 -0
  1667. package/.next/static/chunks/2-rtuqsgzmno4.js +22 -0
  1668. package/.next/static/chunks/220-ne82aus0c.js +44 -0
  1669. package/.next/static/chunks/2rrf5kzx2fhin.js +2 -0
  1670. package/.next/static/chunks/2s_k9x4d0gi9h.js +16 -0
  1671. package/.next/static/chunks/310vm2bl3xxpt.js +31 -0
  1672. package/.next/static/chunks/34at5d2ifl62i.js +33 -0
  1673. package/.next/static/chunks/3fntmmi971322.js +1 -0
  1674. package/.next/static/chunks/3hsi90cejq2dg.js +1 -0
  1675. package/.next/static/chunks/3pfyuezyqd9ob.js +1 -0
  1676. package/.next/static/chunks/3ts-_4i6bfkh2.js +1 -0
  1677. package/.next/static/chunks/40nvbgfn7-072.css +3 -0
  1678. package/.next/static/chunks/turbopack-43vu1xwipchft.js +1 -0
  1679. package/.next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
  1680. package/.next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  1681. package/.next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
  1682. package/.next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
  1683. package/.next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  1684. package/.next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
  1685. package/.next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
  1686. package/.next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  1687. package/.next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
  1688. package/.next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  1689. package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  1690. package/.next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  1691. package/CHANGELOG.md +553 -0
  1692. package/LICENSE +21 -0
  1693. package/README.md +265 -0
  1694. package/SECURITY.md +46 -0
  1695. package/bin/six-degrees.mjs +149 -0
  1696. package/db/schema.js +160 -0
  1697. package/lib/app-version.js +7 -0
  1698. package/lib/companies.js +241 -0
  1699. package/lib/csv.js +172 -0
  1700. package/lib/db-client.js +152 -0
  1701. package/lib/db.js +288 -0
  1702. package/lib/demo.js +34 -0
  1703. package/lib/gate.js +87 -0
  1704. package/lib/ingest.js +62 -0
  1705. package/lib/linkedin-limits.js +118 -0
  1706. package/lib/network.js +13 -0
  1707. package/lib/paths.js +31 -0
  1708. package/lib/paused.js +75 -0
  1709. package/lib/profile.js +55 -0
  1710. package/lib/promote.js +139 -0
  1711. package/lib/quest.js +119 -0
  1712. package/lib/release.js +50 -0
  1713. package/lib/rpc.js +80 -0
  1714. package/lib/scan-progress.js +63 -0
  1715. package/lib/scoring.js +410 -0
  1716. package/lib/scraper-client.js +65 -0
  1717. package/lib/separation.js +326 -0
  1718. package/lib/tiers.js +50 -0
  1719. package/lib/user.js +32 -0
  1720. package/package.json +88 -0
  1721. package/public/demo-data.json +1 -0
  1722. package/scripts/audit-avatars.mjs +132 -0
  1723. package/scripts/check-node.mjs +28 -0
  1724. package/scripts/check-stale-server.mjs +57 -0
  1725. package/scripts/image_store.py +125 -0
  1726. package/scripts/requirements.txt +4 -0
  1727. package/scripts/score_new_connections.sql +77 -0
  1728. package/scripts/scrape.py +3574 -0
  1729. package/scripts/update.mjs +84 -0
@@ -0,0 +1,3574 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 6 Degrees LinkedIn Scraper
4
+
5
+ Drives Chrome with a persistent profile. You log into LinkedIn by hand once;
6
+ the session is reused on every later run.
7
+ Scrapes your connections into the app running on your own machine.
8
+
9
+ Usage:
10
+ pip3 install playwright requests
11
+ playwright install chromium
12
+
13
+ python3 scripts/scrape.py # First run: full scrape. Later: new only
14
+ python3 scripts/scrape.py --full # Force a full re-walk of every page
15
+ python3 scripts/scrape.py --refresh # Only look for newly added connections
16
+ python3 scripts/scrape.py --bridge "Jane Doe" # Scrape one bridge's connections
17
+ python3 scripts/scrape.py --company "Acme" # Scan one company
18
+ python3 scripts/scrape.py --auto-bridge # Map every bridge in turn
19
+ python3 scripts/scrape.py --auto-bridge --tiers=S,A --max-bridges=10
20
+ python3 scripts/scrape.py --rescrape "Name" # Delete + re-scrape a bridge
21
+ python3 scripts/scrape.py --auto-bridge --deeper # ...and finish lists read only partly
22
+
23
+ IMPORTANT: Only scrape ONE bridge at a time. Do NOT batch multiple bridges
24
+ in one session — LinkedIn detects automation and flags your account.
25
+ """
26
+
27
+ import argparse
28
+ import json
29
+ import os
30
+ import signal
31
+ import sys
32
+ import time
33
+ import requests
34
+ from datetime import datetime
35
+ from pathlib import Path
36
+ from urllib.parse import parse_qs, urlparse
37
+
38
+ # Local helper: download + compress avatars into permanent WebP files so they
39
+ # don't break every ~3 weeks when LinkedIn's signed CDN URLs expire.
40
+ from image_store import localize_images
41
+
42
+ # --- Config ---
43
+ def _load_env_local():
44
+ """Fallback: read NEXT_PUBLIC_* values from the app's gitignored .env.local."""
45
+ env = {}
46
+ env_file = Path(__file__).resolve().parent.parent / ".env.local"
47
+ if env_file.exists():
48
+ for line in env_file.read_text().splitlines():
49
+ if "=" in line and not line.strip().startswith("#"):
50
+ k, v = line.split("=", 1)
51
+ env[k.strip()] = v.strip()
52
+ return env
53
+
54
+ _ENV_LOCAL = _load_env_local()
55
+
56
+ # The scraper pushes into the app running on THIS machine. It must never
57
+ # default to somebody else's deployment — that would send a user's network to
58
+ # a server they don't control.
59
+ APP_URL = os.getenv("APP_URL", "http://localhost:3000")
60
+
61
+ CONNECTIONS_URL = "https://www.linkedin.com/mynetwork/invite-connect/connections/"
62
+
63
+ # Token for the deployed app's destructive API routes (see middleware.js). Sent
64
+ # as a bearer so --rescrape/delete-cluster still work against the deployment.
65
+ # Not needed for ordinary ingest, which is open.
66
+ ADMIN_TOKEN = os.getenv("ADMIN_TOKEN") or _ENV_LOCAL.get("ADMIN_TOKEN", "")
67
+
68
+
69
+ def _assert_local_target():
70
+ """Refuse to push to a remote host unless explicitly allowed.
71
+
72
+ A misconfigured APP_URL would upload someone's private network to a third
73
+ party, so this fails loudly rather than silently doing it.
74
+ """
75
+ from urllib.parse import urlparse
76
+ host = (urlparse(APP_URL).hostname or "").lower()
77
+ if host in ("localhost", "127.0.0.1", "::1", "0.0.0.0"):
78
+ return
79
+ if os.getenv("ALLOW_REMOTE_PUSH") == "1":
80
+ print(f"WARNING: pushing scraped data to remote host {host}")
81
+ return
82
+ raise SystemExit(
83
+ f"Refusing to push scraped data to '{APP_URL}'.\n"
84
+ "The scraper writes to the app on your own machine. Start it with "
85
+ "`npm run dev` and leave APP_URL unset, or set ALLOW_REMOTE_PUSH=1 if "
86
+ "you really mean to send your network to a remote server."
87
+ )
88
+
89
+
90
+ def app_headers(json_body=True):
91
+ """Headers for calls to the app's API routes, incl. admin-route auth."""
92
+ headers = {"Content-Type": "application/json"} if json_body else {}
93
+ if ADMIN_TOKEN:
94
+ headers["Authorization"] = f"Bearer {ADMIN_TOKEN}"
95
+ return headers
96
+
97
+
98
+ # Active user ID — set by the server when a user triggers a scrape
99
+ _active_user_id = None
100
+
101
+ # --- Scraper JS (same logic as our proven browser scripts) ---
102
+ CONNECTIONS_SCRAPER_JS = """
103
+ async () => {
104
+ // Load all connections
105
+ let attempts = 0;
106
+ while (attempts < 120) {
107
+ window.scrollTo(0, document.body.scrollHeight);
108
+ await new Promise(r => setTimeout(r, 500));
109
+ const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Load more'));
110
+ if (btn) { btn.scrollIntoView(); btn.click(); attempts++; await new Promise(r => setTimeout(r, 1000)); }
111
+ else { if (document.querySelectorAll('a[href*="/in/"]').length / 2 > 100 || attempts > 5) break; attempts++; await new Promise(r => setTimeout(r, 1000)); }
112
+ }
113
+
114
+ // Extract
115
+ const connections = [];
116
+ const seen = new Set();
117
+ const imgMap = {};
118
+
119
+ document.querySelectorAll('a[href*="/in/"]').forEach(link => {
120
+ const href = link.getAttribute('href');
121
+ const profileUrl = href.startsWith('http') ? href.split('?')[0] : 'https://www.linkedin.com' + href.split('?')[0];
122
+ const img = link.querySelector('img');
123
+
124
+ if (img && img.src && img.src.includes('media.licdn.com')) {
125
+ imgMap[profileUrl] = img.src;
126
+ return;
127
+ }
128
+ if (img) return;
129
+
130
+ const wrapper = link.children[0];
131
+ if (!wrapper || wrapper.children.length < 2) return;
132
+ const name = wrapper.children[0].textContent.trim();
133
+ const headline = wrapper.children[1].textContent.trim();
134
+ if (!name || seen.has(profileUrl)) return;
135
+ seen.add(profileUrl);
136
+
137
+ let connectedDate = '';
138
+ let el = link.nextElementSibling;
139
+ for (let i = 0; i < 5 && el; i++) {
140
+ const t = el.textContent.trim();
141
+ if (t.startsWith('Connected on')) { connectedDate = t.replace('Connected on ', ''); break; }
142
+ el = el.nextElementSibling;
143
+ }
144
+
145
+ connections.push({ name, headline, profileUrl, connectedDate, imageUrl: imgMap[profileUrl] || '' });
146
+ });
147
+
148
+ // Merge image URLs
149
+ connections.forEach(c => { if (!c.imageUrl && imgMap[c.profileUrl]) c.imageUrl = imgMap[c.profileUrl]; });
150
+
151
+ return connections;
152
+ }
153
+ """
154
+
155
+ SEARCH_SCRAPER_JS = """
156
+ async (maxPages) => {
157
+ const results = [];
158
+ for (let page = 1; page <= maxPages; page++) {
159
+ await new Promise(r => setTimeout(r, 2000));
160
+
161
+ const urlMap = {};
162
+ document.querySelectorAll('a').forEach(a => {
163
+ const h = a.getAttribute('href') || '';
164
+ if (!h.includes('/in/')) return;
165
+ const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
166
+ const text = a.textContent.trim();
167
+ if (!text || text.length > 60 || text.includes('mutual') || text.includes('Connect') || text.includes('Invite') || text.includes('•')) return;
168
+ if (!urlMap[text]) urlMap[text] = url;
169
+ });
170
+
171
+ const main = document.querySelector('[role="main"], main');
172
+ const pageText = main ? main.innerText : '';
173
+ const lines = pageText.split('\\n').map(l => l.trim());
174
+
175
+ let i = 0;
176
+ while (i < lines.length) {
177
+ if (urlMap[lines[i]]) {
178
+ const name = lines[i];
179
+ const url = urlMap[name];
180
+ let headline = '';
181
+ let j = i + 1;
182
+ while (j < lines.length && j < i + 8) {
183
+ const l = lines[j].trim();
184
+ if (!l || l.match(/^\\u2022\\s*(1st|2nd|3rd)/) || l === name) { j++; continue; }
185
+ if (l === 'Connect' || l === 'Follow' || l === 'Message' || l.includes('mutual connection') || l.match(/followers$/)) break;
186
+ if (!headline) { headline = l; j++; continue; }
187
+ j++;
188
+ }
189
+ results.push({ name, headline, profileUrl: url, imageUrl: '' });
190
+ }
191
+ i++;
192
+ }
193
+
194
+ if (page < maxPages) {
195
+ const nextBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Next');
196
+ if (!nextBtn) break;
197
+ nextBtn.click();
198
+ await new Promise(r => setTimeout(r, 2500));
199
+ }
200
+ }
201
+ return results;
202
+ }
203
+ """
204
+
205
+
206
+ FEED_URL = "https://www.linkedin.com/feed/"
207
+ # A first sign-in means email, password, and often a 2FA code from another
208
+ # device. Five minutes was not enough, and timing out used to CLOSE the window
209
+ # mid-login and throw the attempt away. Wait for as long as the window is open:
210
+ # closing it is the honest "I am done" signal, and any timer here is a guess.
211
+ LOGIN_WAIT_SECONDS = int(os.getenv("SIX_DEGREES_LOGIN_WAIT", "1800"))
212
+ MAX_SCROLL_ROUNDS = 400 # ~10 people load per round
213
+ SCROLL_PAUSE_SECONDS = 0.9
214
+ SCROLL_STALL_LIMIT = 10 # rounds with no new names before stopping
215
+ LOGIN_POLL_SECONDS = 2
216
+
217
+
218
+ def _has_session_cookie(target):
219
+ """The one signal LinkedIn cannot render away.
220
+
221
+ `li_at` is the session cookie. It is set only after a real sign-in and it
222
+ survives redesigns, which the DOM does not: the `nav.global-nav` element an
223
+ earlier version of this check looked for no longer exists on any LinkedIn
224
+ page, so that check silently never matched.
225
+
226
+ Reads the browser CONTEXT, not a page, on purpose. Signing in can open a
227
+ second window or replace the tab, and a check bound to one Page object then
228
+ watches something the user has already navigated away from — it waits
229
+ forever while the session it is waiting for sits right there in the jar.
230
+ """
231
+ ctx = getattr(target, "context", target)
232
+ try:
233
+ for c in ctx.cookies("https://www.linkedin.com"):
234
+ if c.get("name") == "li_at" and c.get("value"):
235
+ return True
236
+ except Exception:
237
+ pass
238
+ return False
239
+
240
+
241
+ def _looks_logged_out(page):
242
+ """LinkedIn shows several different logged-out pages.
243
+
244
+ Matching on the URL alone is not enough: the signed-out landing page is
245
+ often just linkedin.com/ with a sign-in splash, which slips past a
246
+ login/authwall check and leaves the scraper collecting nothing.
247
+ """
248
+ url = (page.url or "").lower()
249
+ if any(marker in url for marker in ("/login", "/authwall", "/signup", "/checkpoint", "/uas/")):
250
+ return True
251
+ if _has_session_cookie(page):
252
+ return False
253
+ try:
254
+ if page.query_selector("a[href*='/login'], button[data-tracking-control-name*='sign-in']"):
255
+ return True
256
+ except Exception:
257
+ pass
258
+ return "/feed" not in url and "/mynetwork" not in url
259
+
260
+
261
+ def _page_wall(pg):
262
+ """"checkpoint" if this page is LinkedIn's security check, "signin" if it is a
263
+ sign-in page or wall, else None. Only LinkedIn pages count."""
264
+ try:
265
+ if pg.is_closed():
266
+ return None
267
+ u = urlparse(pg.url or "")
268
+ if not (u.hostname or "").endswith("linkedin.com"):
269
+ return None
270
+ path = u.path.lower()
271
+ if path.startswith("/checkpoint"):
272
+ return "checkpoint"
273
+ if path.startswith(("/authwall", "/uas/", "/login")):
274
+ return "signin"
275
+ except Exception:
276
+ pass
277
+ return None
278
+
279
+
280
+ def _off_the_wall(context):
281
+ """At least one open LinkedIn page is past any sign-in or security check.
282
+
283
+ Sign-in can finish in another tab or window and leave a stale /login page
284
+ behind, so one page on a wall doesn't mean you aren't signed in.
285
+ """
286
+ try:
287
+ pages = [pg for pg in context.pages if not pg.is_closed()]
288
+ linkedin = [pg for pg in pages
289
+ if (urlparse(pg.url or "").hostname or "").endswith("linkedin.com")]
290
+ walls = [_page_wall(pg) for pg in linkedin]
291
+ # A security check still open anywhere isn't finished, whatever else
292
+ # is open; a stale sign-in page in another tab is.
293
+ if "checkpoint" in walls:
294
+ return False
295
+ return any(w is None for w in walls)
296
+ except Exception:
297
+ return False
298
+
299
+
300
+ def ensure_logged_in(page, timeout_s=LOGIN_WAIT_SECONDS, log_fn=None, stop_on_checkpoint=False):
301
+ """Wait for a human to finish logging in, rather than scraping an empty page.
302
+
303
+ The browser uses a persistent profile, so this is a once-per-machine step —
304
+ every later run finds the session already there and returns immediately.
305
+
306
+ The session cookie survives LinkedIn's security check, so the cookie alone
307
+ used to count as signed in and a scan carried on straight past the check
308
+ (TRAPS §35). So: were you signed in when this started? If you were, and
309
+ LinkedIn now shows a security check or a sign-in wall, that is LinkedIn
310
+ pushing back — with stop_on_checkpoint (2nd-degree reads) the run ends
311
+ there, since carrying on after one is what LinkedIn says turns a warning
312
+ into a restriction. If you weren't, it is an ordinary sign-in, and LinkedIn's
313
+ own sign-in and two-factor steps live under /checkpoint too: wait through
314
+ them.
315
+ """
316
+ context = page.context
317
+ had_session = _has_session_cookie(context)
318
+
319
+ try:
320
+ page.goto(FEED_URL, wait_until="domcontentloaded")
321
+ except Exception:
322
+ pass
323
+ time.sleep(2)
324
+
325
+ wall = _page_wall(page)
326
+ signed_out = had_session and (wall or not _has_session_cookie(context) or _looks_logged_out(page))
327
+ if stop_on_checkpoint and signed_out:
328
+ reason = "a security check" if wall == "checkpoint" else "being signed out"
329
+ _keep_pushback_evidence(page, reason)
330
+ set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
331
+ raise LinkedInPushedBack(0, reason)
332
+
333
+ if _has_session_cookie(context) and not _looks_logged_out(page) and not wall:
334
+ return True
335
+
336
+ say = log_fn or (lambda m: None)
337
+ if had_session and wall == "checkpoint":
338
+ print()
339
+ print(" LinkedIn wants a security check. Finish it in the browser window;")
340
+ print(" this closes by itself once it's done. Then leave scanning for a day.")
341
+ print()
342
+ say("Waiting for you to finish LinkedIn's security check...")
343
+ else:
344
+ _print_sign_in_banner(say)
345
+ return _wait_for_sign_in(page, context, timeout_s, say)
346
+
347
+
348
+ def _print_sign_in_banner(say):
349
+ say("Waiting for you to sign into LinkedIn in the browser window...")
350
+ print()
351
+ print(" ==================================================================")
352
+ print(" Sign into LinkedIn in the browser window that just opened.")
353
+ print()
354
+ print(" Use your email and password. \"Continue with Google\" and \"Sign in")
355
+ print(" with Apple\" do not work here — Google blocks its sign-in flow")
356
+ print(" inside automated browsers, which is why that window comes up")
357
+ print(" greyed out and does nothing.")
358
+ print()
359
+ print(" Nothing is scraped until you are signed in, and the scrape starts")
360
+ print(" by itself the moment you are. Take as long as you need.")
361
+ print(" Close the browser window to cancel.")
362
+ print(" ==================================================================")
363
+ print()
364
+
365
+
366
+ def _wait_for_sign_in(page, context, timeout_s, say):
367
+ waited = 0
368
+ while waited < timeout_s:
369
+ time.sleep(LOGIN_POLL_SECONDS)
370
+ waited += LOGIN_POLL_SECONDS
371
+
372
+ # Closing every window is how you say "not now". One page closing is
373
+ # not: signing in legitimately opens and closes tabs.
374
+ try:
375
+ if not [pg for pg in context.pages if not pg.is_closed()]:
376
+ print("\n Browser closed — nothing was scraped.")
377
+ say("Browser closed before sign-in.")
378
+ return False
379
+ except Exception:
380
+ print("\n Browser closed — nothing was scraped.")
381
+ return False
382
+
383
+ # The cookie belongs to the whole browser, so it is found no matter which
384
+ # tab or window the sign-in finished in — but it only counts once a
385
+ # LinkedIn page is past the security check or sign-in wall.
386
+ if _has_session_cookie(context) and _off_the_wall(context):
387
+ print()
388
+ print(" Signed in — LinkedIn is clear.")
389
+ print()
390
+ say("Signed in.")
391
+ return True
392
+
393
+ if waited % 15 == 0:
394
+ mins, secs = divmod(waited, 60)
395
+ note = f"{mins}m{secs:02d}s" if mins else f"{secs}s"
396
+ print(f" waiting for sign-in... ({note})", flush=True)
397
+ if waited % 60 == 0:
398
+ say(f"Still waiting for sign-in... ({note})")
399
+
400
+ print("\n Gave up waiting for sign-in. Whatever you completed is saved in the")
401
+ print(" browser profile — run this again and it will pick up from there.")
402
+ say("Timed out waiting for sign-in.")
403
+ return False
404
+
405
+
406
+ def open_login_window():
407
+ """Just sign in, confirm it worked, and exit.
408
+
409
+ Separated out because a sign-in that has to happen inside a run you are also
410
+ trying to watch is two things going wrong at once. Do this first, see it
411
+ succeed, and every later scrape starts immediately.
412
+ """
413
+ from playwright.sync_api import sync_playwright
414
+
415
+ with sync_playwright() as p:
416
+ browser = p.chromium.launch_persistent_context(
417
+ user_data_dir=get_scraper_profile_path(),
418
+ headless=False,
419
+ channel="chrome",
420
+ args=["--disable-blink-features=AutomationControlled"],
421
+ timeout=120000,
422
+ )
423
+ page = browser.pages[0] if browser.pages else browser.new_page()
424
+ page.set_default_timeout(120000)
425
+
426
+ # The cookie alone isn't enough: it survives a security check, and this is
427
+ # the window you open to finish one (TRAPS §35). ensure_logged_in loads
428
+ # the feed, returns at once if all is well, and otherwise waits for you.
429
+ ok = ensure_logged_in(page)
430
+ if ok:
431
+ print(" LinkedIn is signed in on this machine and clear to use.\n")
432
+ browser.close()
433
+ return ok
434
+
435
+
436
+ def get_scraper_profile_path():
437
+ """The Chrome profile the scraper drives.
438
+
439
+ Separate from your everyday Chrome profile so the two never fight over the
440
+ same lock. It holds a real logged-in LinkedIn session, so it lives inside
441
+ the app's data directory (SIX_DEGREES_HOME, default ~/.six-degrees) and is
442
+ removed along with everything else when you delete that folder. Treat it
443
+ like a password: it can be several hundred megabytes and it is the one
444
+ artifact here that grants access to your account.
445
+ """
446
+ base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
447
+ profile_dir = base / "chrome-profile"
448
+ profile_dir.mkdir(parents=True, exist_ok=True)
449
+ return str(profile_dir)
450
+
451
+
452
+ def resolve_active_user():
453
+ """Attach this scrape to the profile the app is showing.
454
+
455
+ The app filters every view by user id, so rows written with no owner land in
456
+ the database and then never appear on screen — the scrape looks like it
457
+ silently did nothing. The app decides which profile is "you" (the one that
458
+ owns the network — lib/profile.js) and this adopts the same answer, so the
459
+ two can never disagree. It used to refuse outright when a stray empty
460
+ profile existed, which stopped every scan with a reason nobody saw.
461
+
462
+ Order: SIX_DEGREES_USER_ID (set by the app when it runs this), then
463
+ SIX_DEGREES_USER by name (for the command line), then ask the app.
464
+ """
465
+ global _active_user_id
466
+ if _active_user_id:
467
+ return _active_user_id
468
+
469
+ given_id = os.getenv("SIX_DEGREES_USER_ID", "").strip()
470
+ if given_id:
471
+ _active_user_id = given_id
472
+ return _active_user_id
473
+
474
+ wanted = os.getenv("SIX_DEGREES_USER", "").strip().lower()
475
+ try:
476
+ if wanted:
477
+ resp = requests.get(f"{APP_URL}/api/users", headers=app_headers(json_body=False), timeout=15)
478
+ users = resp.json().get("users", []) if resp.status_code == 200 else []
479
+ for u in users:
480
+ if (u.get("name") or "").strip().lower() == wanted:
481
+ _active_user_id = u["id"]
482
+ print(f" Scraping into profile: {u['name']}")
483
+ return _active_user_id
484
+ raise SystemExit(f"No profile named '{os.getenv('SIX_DEGREES_USER')}' in the app.")
485
+
486
+ resp = requests.get(f"{APP_URL}/api/users?me=1", headers=app_headers(json_body=False), timeout=15)
487
+ me = resp.json().get("user") if resp.status_code == 200 else None
488
+ except SystemExit:
489
+ raise
490
+ except Exception:
491
+ print(f"\n Could not reach the app at {APP_URL}.")
492
+ print(" Start it with `npm run dev` in another terminal, then run this again.\n")
493
+ raise SystemExit(1)
494
+
495
+ if not me or not me.get("id"):
496
+ raise SystemExit(f"The app at {APP_URL} did not say which profile to use.")
497
+
498
+ _active_user_id = me["id"]
499
+ print(f" Scraping into profile: {me.get('name')}")
500
+ return _active_user_id
501
+
502
+
503
+ def read_bridged_ids():
504
+ """The ids of connections whose circle has already been mapped."""
505
+ params = {}
506
+ if _active_user_id:
507
+ params["userId"] = _active_user_id
508
+ try:
509
+ resp = requests.get(f"{APP_URL}/api/bridges", params=params,
510
+ headers=app_headers(json_body=False), timeout=30)
511
+ if resp.status_code != 200:
512
+ print(" (could not read mapped bridges; treating none as mapped)")
513
+ return set()
514
+ return set(resp.json().get("bridgeIds", []))
515
+ except Exception:
516
+ print(" (could not read mapped bridges; treating none as mapped)")
517
+ return set()
518
+
519
+
520
+ def read_connections(endpoint="", params=None):
521
+ """Read connections from the local app.
522
+
523
+ The hosted build queried the cloud database directly. Locally the app owns
524
+ the SQLite file, so the scraper asks it instead of opening the database a
525
+ second time. PostgREST-style values ("eq.1") are unwrapped here so the many
526
+ existing call sites did not have to change.
527
+ """
528
+ query = {}
529
+ for key, value in (params or {}).items():
530
+ if key in ("select", "order", "limit"):
531
+ if key != "select":
532
+ query[key] = value
533
+ continue
534
+ query[key] = value.split(".", 1)[1] if isinstance(value, str) and value.startswith("eq.") else value
535
+ try:
536
+ resp = requests.get(f"{APP_URL}/api/connections", params=query, headers=app_headers(json_body=False), timeout=30)
537
+ if resp.status_code != 200:
538
+ return []
539
+ return resp.json().get("connections", [])
540
+ except Exception:
541
+ return []
542
+
543
+
544
+ def delete_bridge_cluster(bridge_name):
545
+ """Delete all degree-2 connections for a bridge so it can be re-scraped clean."""
546
+ # Find the bridge (read-only)
547
+ bridges = read_connections(params={"name": f"eq.{bridge_name}", "degree": "eq.1", "limit": "1"})
548
+ if not bridges:
549
+ print(f"Bridge '{bridge_name}' not found.")
550
+ return None
551
+
552
+ bridge_id = bridges[0]["id"]
553
+ existing = read_connections(params={"source_connection_id": f"eq.{bridge_id}", "degree": "eq.2", "select": "id"})
554
+ count = len(existing)
555
+
556
+ if count == 0:
557
+ print(f" No existing cluster data for {bridge_name}.")
558
+ return bridge_id
559
+
560
+ # Delete via Vercel API route (has service_role key)
561
+ resp = requests.post(f"{APP_URL}/api/delete-cluster", json={"bridgeId": bridge_id}, headers=app_headers())
562
+ if resp.status_code == 200:
563
+ print(f" Deleted {count} old records for {bridge_name}.")
564
+ else:
565
+ print(f" Delete failed: {resp.status_code} {resp.text[:100]}")
566
+
567
+ return bridge_id
568
+
569
+
570
+ class SaveFailed(Exception):
571
+ """The app refused what was scraped.
572
+
573
+ It used to print a one-line "Push error" and carry on — "Done! 0 new
574
+ degree-2 connections added" — to the next person, spending more LinkedIn
575
+ views on people who could not be saved either. TRAPS §32.
576
+ """
577
+
578
+
579
+ class LinkedInPushedBack(Exception):
580
+ """A results page would not open after END_CHECKS tries.
581
+
582
+ That is what LinkedIn blocking the account's search looked like on
583
+ 2026-09-24: page 28 of a list never loaded. The batch used to note it for
584
+ that person and carry on with the next one after a cooldown — straight
585
+ into the same block. Raised after saving, so `found` is what was read.
586
+ """
587
+
588
+ def __init__(self, found=0, reason="a page that would not open"):
589
+ super().__init__(reason)
590
+ self.found = found
591
+ self.reason = reason
592
+
593
+
594
+ class BudgetReached(Exception):
595
+ """Today's or this month's search budget is used. Raised after saving."""
596
+
597
+ def __init__(self, found=0, kind="daily"):
598
+ super().__init__(f"the {kind} search budget")
599
+ self.found = found
600
+ self.kind = kind
601
+
602
+
603
+ class CoolingDown(Exception):
604
+ """A cooldown lock is active: nothing may search LinkedIn until it lifts."""
605
+
606
+ def __init__(self, cd):
607
+ super().__init__(cooldown_message(cd))
608
+ self.cd = cd
609
+
610
+
611
+ class NotSignedIn(Exception):
612
+ """LinkedIn wasn't signed in, or the window was closed while waiting for it.
613
+
614
+ Not a fact about the person being read. It used to come back as "no
615
+ connections", and that person was noted as hidden and skipped for good — a
616
+ stop pressed while the browser was opening did exactly that. TRAPS §34.
617
+ """
618
+
619
+
620
+ class SearchLimitReached(Exception):
621
+ """LinkedIn says this account has used up its searches for the month.
622
+
623
+ Every page of someone's connections is a search, and a free account has a
624
+ monthly allowance that reading whole lists uses up fast. Raised after what
625
+ was read has been saved, so `found` is how many that was; the record of how
626
+ far the read got means the next run carries on from the same page.
627
+ """
628
+
629
+ def __init__(self, found=0):
630
+ super().__init__("LinkedIn's monthly search limit")
631
+ self.found = found
632
+
633
+
634
+ def push_connections(connections, degree=1, bridge_id=None, user_id=None, on_saved=None):
635
+ """Push connections via Vercel API route (which has write access).
636
+ No local keys needed — the server handles auth.
637
+
638
+ on_saved runs as soon as the app has the rows, before the photos: those can
639
+ take a while, and a stop that lands during them must not lose the note of
640
+ how far a read got.
641
+ """
642
+
643
+ # Send to Vercel API route — it writes with the service_role key
644
+ payload = {
645
+ "connections": connections,
646
+ "type": "degree2" if degree == 2 else "degree1",
647
+ "bridgeId": bridge_id,
648
+ "userId": user_id or _active_user_id,
649
+ }
650
+
651
+ resp = requests.post(
652
+ f"{APP_URL}/api/ingest",
653
+ headers=app_headers(),
654
+ json=payload,
655
+ )
656
+
657
+ if resp.status_code == 200:
658
+ result = resp.json()
659
+ # "saved" is what was actually new; an older app only sends "processed",
660
+ # which counted everything sent and made nothing look like something.
661
+ inserted = result.get("saved", result.get("processed", 0))
662
+ promoted = result.get("promoted", 0)
663
+ notes = []
664
+ if result.get("alreadyKnown"):
665
+ notes.append(f"{result['alreadyKnown']} already on file")
666
+ if result.get("alreadyConnected"):
667
+ notes.append(f"{result['alreadyConnected']} already your connections")
668
+ if result.get("duplicates"):
669
+ notes.append(f"{result['duplicates']} repeats")
670
+ print(f" Sent {len(connections)} → {inserted} new" + (f" ({', '.join(notes)})" if notes else ""))
671
+ if promoted:
672
+ # Someone you were introduced to has accepted. The bridge that
673
+ # produced them is kept on their row, so the path stays visible.
674
+ # Worded carefully: this read as "N new 2nd-degree people added" when it
675
+ # means the opposite — people already in a bridge's circle who are also
676
+ # your connections, now counted once, with who introduced you kept.
677
+ print(f" {promoted} were already in a bridge's circle — merged into your connections, keeping who introduced you")
678
+ else:
679
+ print(f" Push error: {resp.status_code} {resp.text[:200]}")
680
+ raise SaveFailed(f"the app answered {resp.status_code}: {resp.text[:160]}")
681
+
682
+ if on_saved:
683
+ on_saved()
684
+
685
+ # Batch update profile images separately (faster than inline)
686
+ images_to_update = [
687
+ {"profileUrl": c["profileUrl"], "imageUrl": c["imageUrl"]}
688
+ for c in connections
689
+ if c.get("imageUrl") and c.get("profileUrl")
690
+ ]
691
+ # Capture avatars permanently: download + compress each while its signed URL is
692
+ # still fresh, then store the local /avatars/*.webp path instead of the licdn URL.
693
+ images_to_update = localize_images(images_to_update)
694
+ if images_to_update:
695
+ print(f" Updating {len(images_to_update)} profile images...")
696
+ # Send in batches of 100 to avoid timeout
697
+ for i in range(0, len(images_to_update), 100):
698
+ batch = images_to_update[i:i+100]
699
+ r = requests.post(
700
+ f"{APP_URL}/api/update-images",
701
+ headers=app_headers(),
702
+ json={"images": batch},
703
+ )
704
+ if r.status_code == 200:
705
+ result = r.json()
706
+ print(f" Images {i+1}-{min(i+100, len(images_to_update))}: {result.get('updated', 0)} updated")
707
+
708
+ # Check for unlocked paths (read-only lookup)
709
+ if degree == 1:
710
+ print(" Checking for unlocked paths...")
711
+ pending = read_connections(params={"unlock_status": "eq.pending", "degree": "eq.2", "select": "id,profile_url,name"})
712
+ d1_urls = {c.get("profileUrl", "").strip() for c in connections if c.get("profileUrl")}
713
+ for p in pending:
714
+ if p["profile_url"] in d1_urls:
715
+ # Unlock via API route
716
+ requests.post(f"{APP_URL}/api/unlock", json={"connectionId": p["id"]}, headers=app_headers())
717
+ print(f" *** PATH UNLOCKED: {p['name']}! Run --bridge \"{p['name']}\" to scrape their network ***")
718
+
719
+ return inserted
720
+
721
+
722
+ def push_company(people, company_name):
723
+ """Push company-scraped people to database as degree 3 (company scan).
724
+ Uses the same Vercel API route but with type='company'."""
725
+ connections = []
726
+ for p in people:
727
+ connections.append({
728
+ "name": p.get("name", "").strip(),
729
+ "headline": p.get("headline", "").strip(),
730
+ "profileUrl": p.get("profileUrl", "").strip(),
731
+ "imageUrl": p.get("imageUrl", ""),
732
+ })
733
+
734
+ connections = [c for c in connections if c.get("name") and c.get("profileUrl")]
735
+ if not connections:
736
+ print(f" No valid people to push for {company_name}")
737
+ return 0
738
+
739
+ payload = {
740
+ "connections": connections,
741
+ "type": "company",
742
+ "companyName": company_name,
743
+ "userId": _active_user_id,
744
+ }
745
+
746
+ resp = requests.post(
747
+ f"{APP_URL}/api/ingest",
748
+ headers=app_headers(),
749
+ json=payload,
750
+ )
751
+
752
+ if resp.status_code == 200:
753
+ result = resp.json()
754
+ inserted = result.get("processed", 0)
755
+ print(f" Pushed {len(connections)} → {inserted} saved to database")
756
+ else:
757
+ print(f" Push error: {resp.status_code} {resp.text[:200]}")
758
+ raise SaveFailed(f"the app answered {resp.status_code}: {resp.text[:160]}")
759
+
760
+ # Update profile images
761
+ images_to_update = [
762
+ {"profileUrl": c["profileUrl"], "imageUrl": c["imageUrl"]}
763
+ for c in connections
764
+ if c.get("imageUrl") and c.get("profileUrl")
765
+ ]
766
+ # Capture avatars permanently (download + compress while the signed URL is fresh).
767
+ images_to_update = localize_images(images_to_update)
768
+ if images_to_update:
769
+ for i in range(0, len(images_to_update), 100):
770
+ batch = images_to_update[i:i+100]
771
+ r = requests.post(
772
+ f"{APP_URL}/api/update-images",
773
+ headers=app_headers(),
774
+ json={"images": batch},
775
+ )
776
+ if r.status_code == 200:
777
+ print(f" Images {i+1}-{min(i+100, len(images_to_update))}: {r.json().get('updated', 0)} updated")
778
+
779
+ return inserted
780
+
781
+
782
+ def scrape_full(headless=False):
783
+ """Full first-time scrape: uses search page to get ALL connections with images.
784
+ Paginates through /search/results/people/?network=["F"] (10 results per page).
785
+ This is the setup method — captures everything including profile photos."""
786
+ from playwright.sync_api import sync_playwright
787
+
788
+ cd = read_cooldown()
789
+ if cd:
790
+ raise CoolingDown(cd)
791
+ if searches_left()[0] <= 0:
792
+ raise BudgetReached(0, searches_left()[1])
793
+
794
+ print("\n=== Full Account Setup Scrape ===\n")
795
+ print("This scrapes ALL connections via the search page (not the connections page).")
796
+ print("Takes 3-5 minutes for ~500 connections.\n")
797
+
798
+ with sync_playwright() as p:
799
+ browser = p.chromium.launch_persistent_context(
800
+ user_data_dir=get_scraper_profile_path(),
801
+ headless=headless,
802
+ channel="chrome",
803
+ args=["--disable-blink-features=AutomationControlled"],
804
+ timeout=120000,
805
+ )
806
+
807
+ page = browser.pages[0] if browser.pages else browser.new_page()
808
+ page.set_default_timeout(120000)
809
+ page.set_default_navigation_timeout(120000)
810
+
811
+ # Check login
812
+ try:
813
+ page.goto("https://www.linkedin.com/", wait_until="domcontentloaded")
814
+ except:
815
+ pass
816
+ if not ensure_logged_in(page):
817
+ browser.close()
818
+ return
819
+
820
+ # Navigate to 1st-degree search
821
+ search_url = "https://www.linkedin.com/search/results/people/?network=%5B%22F%22%5D&origin=FACETED_SEARCH"
822
+ print("Opening 1st-degree connections search...")
823
+ try:
824
+ charge_linkedin("searches")
825
+ page.goto(search_url, wait_until="commit")
826
+ except:
827
+ pass
828
+ time.sleep(8)
829
+
830
+ all_connections = []
831
+ all_images = []
832
+ page_num = 1
833
+
834
+ while page_num <= 100:
835
+ print(f" Page {page_num}... ", end="", flush=True)
836
+ time.sleep(3)
837
+
838
+ # Extract connections + images from current page
839
+ page_data = page.evaluate("""
840
+ () => {
841
+ const urlMap = {};
842
+ const imgMap = {};
843
+ document.querySelectorAll('a').forEach(a => {
844
+ const h = a.getAttribute('href') || '';
845
+ if (!h.includes('/in/')) return;
846
+ const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
847
+ const img = a.querySelector('img');
848
+ if (img && img.src && img.src.includes('media.licdn.com')) {
849
+ imgMap[url] = img.src;
850
+ return;
851
+ }
852
+ const text = a.textContent.trim();
853
+ if (!text || text.length > 60 || text.includes('mutual') || text.includes('Connect')) return;
854
+ if (!urlMap[text]) urlMap[text] = url;
855
+ });
856
+ const main = document.querySelector('[role="main"], main');
857
+ const pageText = main ? main.innerText : '';
858
+ const lines = pageText.split('\\n').map(l => l.trim());
859
+ const results = [];
860
+ let i = 0;
861
+ while (i < lines.length) {
862
+ if (urlMap[lines[i]]) {
863
+ const name = lines[i], url = urlMap[name];
864
+ let headline = '';
865
+ let j = i + 1;
866
+ while (j < lines.length && j < i + 8) {
867
+ const l = lines[j].trim();
868
+ if (!l || l.match(/^\\u2022\\s*(1st|2nd|3rd)/) || l === name) { j++; continue; }
869
+ if (l === 'Connect' || l === 'Follow' || l === 'Message' || l.includes('mutual connection')) break;
870
+ if (!headline) { headline = l; j++; continue; }
871
+ j++;
872
+ }
873
+ results.push({ name, headline, profileUrl: url, imageUrl: imgMap[url] || '' });
874
+ }
875
+ i++;
876
+ }
877
+ return results;
878
+ }
879
+ """)
880
+
881
+ new_count = len(page_data)
882
+ all_connections.extend(page_data)
883
+ img_count = sum(1 for c in page_data if c.get("imageUrl"))
884
+ print(f"{new_count} found ({img_count} with images)")
885
+
886
+ # Click Next
887
+ try:
888
+ next_btn = page.locator('button:has-text("Next")').first
889
+ if next_btn.is_visible(timeout=3000):
890
+ if searches_left()[0] <= 0:
891
+ print(" " + budget_message(searches_left()[1]))
892
+ break
893
+ charge_linkedin("searches")
894
+ next_btn.click()
895
+ page_num += 1
896
+ time.sleep(3)
897
+ else:
898
+ print(" No more pages.")
899
+ break
900
+ except:
901
+ print(" No more pages.")
902
+ break
903
+
904
+ print(f"\nTotal: {len(all_connections)} connections, {sum(1 for c in all_connections if c.get('imageUrl'))} with images")
905
+ browser.close()
906
+
907
+ # Push connections
908
+ print(f"\nPushing {len(all_connections)} connections to the app...")
909
+ inserted = push_connections(all_connections, degree=1)
910
+ print(f"Done! {inserted} new.")
911
+
912
+ return all_connections
913
+
914
+
915
+ # The connections page keeps its list inside <main>, which is its own scroll
916
+ # container: the window itself never scrolls, so window.scrollTo() — what this
917
+ # scraper used to call — is a no-op there. Nothing new ever loaded and the run
918
+ # ended with the first ten people. Scroll the real container instead, and send a
919
+ # genuine wheel event too, because the list loads on an intersection observer.
920
+ SCROLL_CONTAINER_JS = """
921
+ () => {
922
+ let best = null;
923
+ document.querySelectorAll('main, [role="main"], div').forEach(el => {
924
+ const cs = getComputedStyle(el);
925
+ if (!/(auto|scroll)/.test(cs.overflowY)) return;
926
+ if (el.scrollHeight <= el.clientHeight + 20) return;
927
+ if (!best || el.scrollHeight > best.scrollHeight) best = el;
928
+ });
929
+ const target = best || document.scrollingElement || document.body;
930
+ target.scrollTop = target.scrollHeight;
931
+ window.scrollTo(0, document.body.scrollHeight);
932
+ return target.scrollTop;
933
+ }
934
+ """
935
+
936
+ # Anchor everything on the profile link, never on class names (LinkedIn's are
937
+ # hashed per deploy) and never on guessing which text line goes with which URL.
938
+ # Each person renders two <a> tags pointing at the same profile: one wrapping
939
+ # the photo, one wrapping the name and headline. Group by href and merge.
940
+ CONNECTIONS_EXTRACT_JS = r"""
941
+ () => {
942
+ const root = document.querySelector('main') || document.querySelector('[role="main"]') || document.body;
943
+ const clean = s => (s || '').replace(/[’']s profile picture$/i, '').trim();
944
+ const byHref = new Map();
945
+ root.querySelectorAll('a[href*="/in/"]').forEach(a => {
946
+ if (a.closest('nav, header, footer')) return;
947
+ const raw = a.getAttribute('href') || '';
948
+ if (!raw.includes('/in/')) return;
949
+ let url = raw.startsWith('http') ? raw : 'https://www.linkedin.com' + raw;
950
+ url = url.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/';
951
+ let rec = byHref.get(url);
952
+ if (!rec) { rec = { profileUrl: url, name: '', headline: '', imageUrl: '', _named: false }; byHref.set(url, rec); }
953
+ const img = a.querySelector('img');
954
+ if (img) {
955
+ if (!rec.imageUrl && img.src && img.src.includes('licdn.com') && !/ghost|^data:/.test(img.src)) rec.imageUrl = img.src;
956
+ // Fallback only: the alt text reads "Jane Doe's profile picture".
957
+ if (!rec.name && img.alt) rec.name = clean(img.alt);
958
+ }
959
+ const lines = (a.innerText || '').split('\n').map(s => s.trim()).filter(Boolean);
960
+ if (lines.length) {
961
+ // The text anchor is authoritative; it always wins over the image alt.
962
+ if (!rec._named) { rec.name = clean(lines[0]); rec._named = true; }
963
+ if (!rec.headline && lines[1] && !/^Connected on/i.test(lines[1])) rec.headline = lines[1];
964
+ }
965
+ // "Connected on September 22, 2026" sits in the card, outside the link. It is
966
+ // what "newest first" goes by, and it went missing when this extractor was
967
+ // rewritten. Climb to the nearest block holding exactly ONE such line: more
968
+ // than one means we have reached the list itself and would take a neighbour's.
969
+ if (!rec.connectedDate) {
970
+ let el = a;
971
+ for (let k = 0; k < 8 && el.parentElement; k++) {
972
+ el = el.parentElement;
973
+ const hits = (el.innerText || '').match(/Connected on [A-Za-z]+\.? \d{1,2}, \d{4}/g);
974
+ if (!hits) continue;
975
+ if (hits.length === 1) rec.connectedDate = hits[0].replace(/^Connected on /, '');
976
+ break;
977
+ }
978
+ }
979
+ });
980
+ return [...byHref.values()].filter(r => r.name && r.name.length >= 2).map(({ _named, ...r }) => r);
981
+ }
982
+ """
983
+
984
+
985
+ def _reported_connection_count(page):
986
+ """The "754 connections" line the page prints above the list."""
987
+ try:
988
+ return page.evaluate(
989
+ """() => { const m = document.body.innerText.match(/([\\d,]+)\\s+connections?/i);
990
+ return m ? parseInt(m[1].replace(/,/g, '')) : null; }"""
991
+ )
992
+ except Exception:
993
+ return None
994
+
995
+
996
+ def scrape_connections(headless=False, full_walk=False, log_fn=None):
997
+ """Read your connections from the connections page.
998
+
999
+ full_walk=False is the incremental refresh: it walks from the most recently
1000
+ added and stops once it has seen a long run of people already on file.
1001
+ full_walk=True keeps going to the bottom of the list, which is the way to
1002
+ collect an entire network in one pass.
1003
+ """
1004
+ from playwright.sync_api import sync_playwright
1005
+
1006
+ say = log_fn or (lambda m: None)
1007
+ # Not a search, so no budget — but after LinkedIn pushed back (a security
1008
+ # check, above all) nothing automated should open it until the cooldown lifts.
1009
+ cd = read_cooldown()
1010
+ if cd:
1011
+ raise CoolingDown(cd)
1012
+ mode = "Full Connection Scrape" if full_walk else "Smart Connection Refresh"
1013
+ print(f"\n=== {mode} ===\n")
1014
+
1015
+ existing_urls = set()
1016
+ if not full_walk:
1017
+ try:
1018
+ params = {"degree": "eq.1", "select": "profile_url", "limit": "5000"}
1019
+ if _active_user_id:
1020
+ params["user_id"] = f"eq.{_active_user_id}"
1021
+ existing_urls = {e["profile_url"] for e in read_connections(params=params)}
1022
+ print(f" {len(existing_urls)} existing connections on file")
1023
+ except Exception:
1024
+ pass
1025
+
1026
+ collected = {}
1027
+ reported = None
1028
+
1029
+ with sync_playwright() as p:
1030
+ browser = p.chromium.launch_persistent_context(
1031
+ user_data_dir=get_scraper_profile_path(),
1032
+ headless=headless,
1033
+ channel="chrome",
1034
+ args=["--disable-blink-features=AutomationControlled"],
1035
+ timeout=120000,
1036
+ )
1037
+
1038
+ page = browser.pages[0] if browser.pages else browser.new_page()
1039
+ page.set_default_timeout(120000)
1040
+ page.set_default_navigation_timeout(120000)
1041
+
1042
+ if not ensure_logged_in(page, log_fn=log_fn):
1043
+ browser.close()
1044
+ return []
1045
+
1046
+ print("Opening connections page...")
1047
+ say("Opening your connections page...")
1048
+ try:
1049
+ page.goto(CONNECTIONS_URL, wait_until="domcontentloaded")
1050
+ except Exception:
1051
+ pass
1052
+ try:
1053
+ # Wait for the list itself, not a fixed sleep: a slow render used to
1054
+ # look identical to an empty network.
1055
+ page.wait_for_selector('a[href*="/in/"]', timeout=60000)
1056
+ except Exception:
1057
+ print(" No connections rendered on the page.")
1058
+ say("No connections rendered — is the page loading?")
1059
+ browser.close()
1060
+ return []
1061
+ time.sleep(3)
1062
+
1063
+ reported = _reported_connection_count(page)
1064
+ if reported:
1065
+ print(f" LinkedIn reports {reported} connections")
1066
+ say(f"LinkedIn reports {reported} connections")
1067
+
1068
+ known_streak = 0
1069
+ stalls = 0
1070
+ extract_errors = 0
1071
+
1072
+ for round_num in range(MAX_SCROLL_ROUNDS):
1073
+ if stop_requested():
1074
+ print(" Stopped — keeping what was collected so far.")
1075
+ break
1076
+ try:
1077
+ page.evaluate(SCROLL_CONTAINER_JS)
1078
+ except Exception:
1079
+ pass
1080
+ try:
1081
+ page.mouse.move(700, 500)
1082
+ page.mouse.wheel(0, 3000)
1083
+ except Exception:
1084
+ pass
1085
+ time.sleep(SCROLL_PAUSE_SECONDS)
1086
+
1087
+ before = len(collected)
1088
+ try:
1089
+ rows = page.evaluate(CONNECTIONS_EXTRACT_JS)
1090
+ extract_errors = 0
1091
+ except Exception as exc:
1092
+ # Never let this look like "you have no connections": say it out
1093
+ # loud and give up rather than reporting a confident zero.
1094
+ rows = []
1095
+ extract_errors += 1
1096
+ if extract_errors == 1:
1097
+ print(f" Could not read the page: {exc}")
1098
+ say("Could not read the connections page.")
1099
+ if extract_errors >= 3:
1100
+ print(" Aborting — the page could not be read three times running.")
1101
+ browser.close()
1102
+ return []
1103
+ for row in rows:
1104
+ url = row.get("profileUrl")
1105
+ if not url or url in collected:
1106
+ continue
1107
+ collected[url] = row
1108
+ if url in existing_urls:
1109
+ known_streak += 1
1110
+ else:
1111
+ known_streak = 0
1112
+ gained = len(collected) - before
1113
+
1114
+ if gained:
1115
+ stalls = 0
1116
+ else:
1117
+ stalls += 1
1118
+ # Older layouts paginate with a button instead of scrolling.
1119
+ try:
1120
+ more = page.locator('button:has-text("Load more"), button:has-text("Show more")').first
1121
+ if more.count() and more.is_visible(timeout=500):
1122
+ more.click()
1123
+ time.sleep(1.5)
1124
+ stalls = 0
1125
+ except Exception:
1126
+ pass
1127
+
1128
+ if gained and len(collected) % 50 < gained:
1129
+ msg = f" {len(collected)}" + (f" / {reported}" if reported else "") + " collected"
1130
+ print(msg, flush=True)
1131
+ say(msg.strip())
1132
+
1133
+ if stalls >= SCROLL_STALL_LIMIT:
1134
+ print(f" Reached the end of the list ({len(collected)} collected).")
1135
+ break
1136
+ if reported and len(collected) >= reported:
1137
+ print(f" Collected all {len(collected)} connections.")
1138
+ break
1139
+ if not full_walk and known_streak >= 20:
1140
+ print(f" Caught up — {known_streak} already on file in a row.")
1141
+ break
1142
+
1143
+ browser.close()
1144
+
1145
+ found = list(collected.values())
1146
+ new_rows = [r for r in found if r["profileUrl"] not in existing_urls] if existing_urls else found
1147
+ with_photos = sum(1 for r in found if r.get("imageUrl"))
1148
+ if existing_urls:
1149
+ print(f"\nCollected {len(found)} connections ({with_photos} with photos); {len(new_rows)} new.")
1150
+ say(f"Collected {len(found)} connections, {len(new_rows)} new")
1151
+ else:
1152
+ # A full walk does not load what is already saved, so it cannot say what is
1153
+ # new — it used to call all 814 "new", then the app said "0 new". The app's
1154
+ # "Sent … → N new" line below is the one that knows.
1155
+ print(f"\nCollected {len(found)} connections ({with_photos} with photos).")
1156
+ say(f"Collected {len(found)} connections")
1157
+
1158
+ # Only meaningful on a full walk: a refresh stops early on purpose.
1159
+ if full_walk and reported and len(found) < reported * 0.5:
1160
+ print(f" WARNING: LinkedIn reported {reported} but only {len(found)} were read.")
1161
+ print(" Re-run, and if it repeats the page layout has probably changed again.")
1162
+ say(f"Warning: only read {len(found)} of {reported}")
1163
+
1164
+ if new_rows:
1165
+ print(f"\nPushing {len(new_rows)} connections to the app...")
1166
+ push_connections(new_rows, degree=1)
1167
+ else:
1168
+ print("\nNothing new to push.")
1169
+
1170
+ print(f"\nDone. Visit {APP_URL} to see your network.")
1171
+ return new_rows
1172
+
1173
+
1174
+ BRIDGE_COOLDOWN = 120 # after a real scrape: many page views, be polite
1175
+ BRIDGE_LOAD_ATTEMPTS = 6 # how many times to look for the connections URN
1176
+ BRIDGE_LOAD_STEP = 5 # seconds between looks
1177
+ LINKEDIN_MAX_PAGES = 100 # LinkedIn's people search never goes past page 100
1178
+ END_CHECKS = 3 # looks for another page before calling a list finished
1179
+ SAVE_EVERY_PAGES = 10 # save as a long read goes, so a stop loses little
1180
+ # Pace between searches. 0.1.6 read a page every ~6 s — 27 searches in three and
1181
+ # a half minutes — and LinkedIn blocked the account's search on the 28th. These
1182
+ # are a stopgap until the pacing is set from research (TRAPS §35): a rest before
1183
+ # every search, and a longer one after every SAVE_EVERY_PAGES. Fixed, not
1184
+ # randomised: the point is fewer searches an hour, not looking like a person.
1185
+ PAGE_PAUSE = 20 # seconds before each next page of results
1186
+ CHUNK_COOLDOWN = 60 # extra seconds after every SAVE_EVERY_PAGES pages
1187
+ LEGACY_PAGES_READ = 10 # how far every read before 0.1.6 went, at most
1188
+
1189
+
1190
+ # Someone whose connections are hidden can never produce a 2nd-degree row. The
1191
+ # "who still needs bridging" query is "everyone with no 2nd-degree rows", so
1192
+ # without a record of the attempt they come back in the list on every run — and
1193
+ # because the list is sorted by tier, the same person is retried first, forever.
1194
+ # That is what made auto-bridge look stuck rather than slow.
1195
+ def _skips_path():
1196
+ base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
1197
+ base.mkdir(parents=True, exist_ok=True)
1198
+ return base / "bridge-skips.json"
1199
+
1200
+
1201
+ def load_bridge_skips():
1202
+ try:
1203
+ return json.loads(_skips_path().read_text())
1204
+ except Exception:
1205
+ return {}
1206
+
1207
+
1208
+ def record_bridge_skip(profile_url, name, reason):
1209
+ skips = load_bridge_skips()
1210
+ skips[profile_url] = {
1211
+ "name": name,
1212
+ "reason": reason,
1213
+ "at": datetime.now().isoformat(timespec="seconds"),
1214
+ }
1215
+ try:
1216
+ _skips_path().write_text(json.dumps(skips, indent=2))
1217
+ except Exception as exc:
1218
+ print(f" (could not record the skip: {exc})")
1219
+
1220
+
1221
+ # People whose reads came back unclear. Nothing about them is concluded, but
1222
+ # without a count, two of them side by side would stop every batch at the same
1223
+ # place, forever. After UNCLEAR_LIMIT unclear reads they go to the back of the
1224
+ # queue — still tried, just not first. A clear read wipes the count.
1225
+ UNCLEAR_LIMIT = 2
1226
+
1227
+
1228
+ def _unclear_path():
1229
+ base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
1230
+ base.mkdir(parents=True, exist_ok=True)
1231
+ return base / "bridge-unclear.json"
1232
+
1233
+
1234
+ def load_unclear():
1235
+ try:
1236
+ data = json.loads(_unclear_path().read_text())
1237
+ return data if isinstance(data, dict) else {}
1238
+ except Exception:
1239
+ return {}
1240
+
1241
+
1242
+ def note_unclear(profile_url, clear=False):
1243
+ data = load_unclear()
1244
+ if clear:
1245
+ if profile_url not in data:
1246
+ return
1247
+ data.pop(profile_url, None)
1248
+ else:
1249
+ n = int((data.get(profile_url) or {}).get("n") or 0) + 1
1250
+ data[profile_url] = {"n": n, "at": datetime.now().isoformat(timespec="seconds")}
1251
+ try:
1252
+ _unclear_path().write_text(json.dumps(data, indent=2))
1253
+ except Exception:
1254
+ pass
1255
+
1256
+
1257
+ # ---------------------------------------------------------------------------
1258
+ # How much this machine has asked of LinkedIn, and whether it may ask now.
1259
+ #
1260
+ # LinkedIn's limit on people search for free accounts is monthly: it resets at
1261
+ # midnight Pacific on the 1st, with no published number (Help Center a564226;
1262
+ # reports put it around 250–350 a month). Every page of someone's connections
1263
+ # is one search. 0.1.6 spent a night's worth in minutes and search was blocked
1264
+ # (TRAPS §35). So every search and profile view is written down here, and a run
1265
+ # stops — saving what it read, carrying on later from the same page — when the
1266
+ # day's or the month's budget is used. A cooldown lock is set when LinkedIn
1267
+ # pushes back, and nothing searches until it lifts.
1268
+ #
1269
+ # These belong to the LinkedIn account (the one Chrome profile), not to a
1270
+ # profile in the app: three app profiles must not triple the budget. The app's
1271
+ # Scan page reads and edits the same files (lib/linkedin-limits.js).
1272
+ #
1273
+ # linkedin-activity.json {"searches": [epoch s, ...], "profiles": [...]}
1274
+ # scan-limits.json {"daily": 50, "monthly": 250} 0 = no cap
1275
+ # linkedin-cooldown.json {"until": epoch s, "reason": "...", "set_at": ...}
1276
+ # ---------------------------------------------------------------------------
1277
+ DEFAULT_DAILY_SEARCHES = 50
1278
+ DEFAULT_MONTHLY_SEARCHES = 250
1279
+ DAY_SECONDS = 24 * 3600
1280
+ PACIFIC = "America/Los_Angeles"
1281
+
1282
+
1283
+ def _home():
1284
+ base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
1285
+ base.mkdir(parents=True, exist_ok=True)
1286
+ return base
1287
+
1288
+
1289
+ def _write_json_atomic(path, data):
1290
+ import tempfile
1291
+ fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=path.name + ".", suffix=".tmp")
1292
+ try:
1293
+ with os.fdopen(fd, "w") as f:
1294
+ json.dump(data, f, indent=2)
1295
+ f.flush()
1296
+ os.fsync(f.fileno())
1297
+ os.replace(tmp, path)
1298
+ except Exception:
1299
+ try:
1300
+ os.unlink(tmp)
1301
+ except OSError:
1302
+ pass
1303
+ raise
1304
+
1305
+
1306
+ class _Locked:
1307
+ """An exclusive lock around a read-modify-write, so two runs can't lose counts."""
1308
+
1309
+ def __init__(self, name):
1310
+ self.path = _home() / f".{name}.lock"
1311
+ self.f = None
1312
+
1313
+ def __enter__(self):
1314
+ self.f = open(self.path, "a")
1315
+ try:
1316
+ import fcntl # macOS and Linux; Windows has none
1317
+ fcntl.flock(self.f, fcntl.LOCK_EX)
1318
+ except ImportError:
1319
+ pass
1320
+ return self
1321
+
1322
+ def __exit__(self, *exc):
1323
+ try:
1324
+ import fcntl
1325
+ fcntl.flock(self.f, fcntl.LOCK_UN)
1326
+ except ImportError:
1327
+ pass
1328
+ finally:
1329
+ self.f.close()
1330
+
1331
+
1332
+ def month_start_pacific(now=None):
1333
+ """Epoch seconds of midnight Pacific on the 1st of this month: when LinkedIn resets."""
1334
+ from zoneinfo import ZoneInfo
1335
+ tz = ZoneInfo(PACIFIC)
1336
+ t = datetime.fromtimestamp(now if now is not None else time.time(), tz)
1337
+ return t.replace(day=1, hour=0, minute=0, second=0, microsecond=0).timestamp()
1338
+
1339
+
1340
+ def next_month_start_pacific(now=None):
1341
+ from zoneinfo import ZoneInfo
1342
+ tz = ZoneInfo(PACIFIC)
1343
+ t = datetime.fromtimestamp(now if now is not None else time.time(), tz)
1344
+ y, m = (t.year + 1, 1) if t.month == 12 else (t.year, t.month + 1)
1345
+ return datetime(y, m, 1, tzinfo=tz).timestamp()
1346
+
1347
+
1348
+ def _read_activity():
1349
+ """The record, or — if it can't be read — a record that says the day is used.
1350
+
1351
+ Failing open would read a damaged file as "nothing used yet". The damaged
1352
+ copy is kept beside it.
1353
+ """
1354
+ path = _home() / "linkedin-activity.json"
1355
+ try:
1356
+ data = json.loads(path.read_text())
1357
+ if not isinstance(data, dict):
1358
+ raise ValueError("not an object")
1359
+ return {"searches": [float(x) for x in data.get("searches", [])],
1360
+ "profiles": [float(x) for x in data.get("profiles", [])]}
1361
+ except FileNotFoundError:
1362
+ return {"searches": [], "profiles": []}
1363
+ except Exception:
1364
+ # Kept aside, and replaced by a record that says today's budget is used —
1365
+ # written, so it holds for the whole day rather than one check. (Failing
1366
+ # open would read a damaged file as nothing searched.)
1367
+ now = time.time()
1368
+ try:
1369
+ path.rename(path.with_name(f"{path.name}.corrupt-{int(now)}"))
1370
+ except OSError:
1371
+ pass
1372
+ print(" (the record of LinkedIn searches couldn't be read; counting today as used)")
1373
+ fresh = {"searches": [now] * max(1, search_limits()["daily"] or 1), "profiles": []}
1374
+ try:
1375
+ _write_json_atomic(path, fresh)
1376
+ except Exception:
1377
+ pass
1378
+ return fresh
1379
+
1380
+
1381
+ def charge_linkedin(kind="searches", n=1):
1382
+ """Write down n searches (or profile views) made just now."""
1383
+ now = time.time()
1384
+ keep_from = month_start_pacific(now) - 32 * DAY_SECONDS
1385
+ with _Locked("linkedin-activity"):
1386
+ data = _read_activity()
1387
+ data[kind] = [t for t in data.get(kind, []) if t >= keep_from] + [now] * n
1388
+ other = "profiles" if kind == "searches" else "searches"
1389
+ data[other] = [t for t in data.get(other, []) if t >= keep_from]
1390
+ try:
1391
+ _write_json_atomic(_home() / "linkedin-activity.json", data)
1392
+ except Exception as exc:
1393
+ print(f" (could not write down LinkedIn activity: {exc})")
1394
+
1395
+
1396
+ def search_limits():
1397
+ try:
1398
+ data = json.loads((_home() / "scan-limits.json").read_text())
1399
+ except Exception:
1400
+ data = {}
1401
+ def num(v, default):
1402
+ try:
1403
+ v = int(v)
1404
+ return v if v >= 0 else default
1405
+ except (TypeError, ValueError):
1406
+ return default
1407
+ return {"daily": num(data.get("daily"), DEFAULT_DAILY_SEARCHES),
1408
+ "monthly": num(data.get("monthly"), DEFAULT_MONTHLY_SEARCHES)}
1409
+
1410
+
1411
+ def linkedin_usage(now=None):
1412
+ now = now if now is not None else time.time()
1413
+ data = _read_activity()
1414
+ month0 = month_start_pacific(now)
1415
+ return {
1416
+ "searches_today": sum(1 for t in data["searches"] if t > now - DAY_SECONDS),
1417
+ "searches_month": sum(1 for t in data["searches"] if t >= month0),
1418
+ "profiles_today": sum(1 for t in data["profiles"] if t > now - DAY_SECONDS),
1419
+ }
1420
+
1421
+
1422
+ def searches_left(now=None):
1423
+ """(how many searches may still be made, "daily" | "monthly" — whichever runs out first)."""
1424
+ use, lim = linkedin_usage(now), search_limits()
1425
+ left_day = max(0, lim["daily"] - use["searches_today"]) if lim["daily"] else 10 ** 9
1426
+ left_month = max(0, lim["monthly"] - use["searches_month"]) if lim["monthly"] else 10 ** 9
1427
+ return (left_day, "daily") if left_day <= left_month else (left_month, "monthly")
1428
+
1429
+
1430
+ def _when(ts):
1431
+ t = datetime.fromtimestamp(ts)
1432
+ hour = t.hour % 12 or 12
1433
+ return f"{t.strftime('%a %b')} {t.day}, {hour}:{t.minute:02d} {'AM' if t.hour < 12 else 'PM'}"
1434
+
1435
+
1436
+ def _day(ts):
1437
+ t = datetime.fromtimestamp(ts)
1438
+ return f"{t.strftime('%b')} {t.day}"
1439
+
1440
+
1441
+ def budget_message(kind):
1442
+ lim = search_limits()
1443
+ if kind == "monthly":
1444
+ when = _day(next_month_start_pacific())
1445
+ return (f"This month's search budget ({lim['monthly']}) is used. It starts again on {when} "
1446
+ f"(LinkedIn's month); the next run carries on from the same page.")
1447
+ return (f"Today's search budget ({lim['daily']}) is used — it counts the last 24 hours. "
1448
+ f"The next run carries on from the same page.")
1449
+
1450
+
1451
+ def read_cooldown(now=None):
1452
+ """The active cooldown, or None."""
1453
+ now = now if now is not None else time.time()
1454
+ try:
1455
+ data = json.loads((_home() / "linkedin-cooldown.json").read_text())
1456
+ if float(data.get("until", 0)) > now:
1457
+ return data
1458
+ except Exception:
1459
+ pass
1460
+ return None
1461
+
1462
+
1463
+ def set_cooldown(seconds=None, until=None, reason=""):
1464
+ """Nothing searches LinkedIn until this lifts. Never shortens an existing one."""
1465
+ now = time.time()
1466
+ until = until if until is not None else now + seconds
1467
+ current = read_cooldown(now)
1468
+ if current and float(current["until"]) >= until:
1469
+ return
1470
+ try:
1471
+ _write_json_atomic(_home() / "linkedin-cooldown.json",
1472
+ {"until": until, "reason": reason, "set_at": now})
1473
+ print(f" Scanning paused until {_when(until)}"
1474
+ f" ({reason}). The Scan page can lift it.")
1475
+ except Exception as exc:
1476
+ print(f" (could not set the cooldown: {exc})")
1477
+
1478
+
1479
+ def cooldown_message(cd):
1480
+ until = _when(float(cd["until"]))
1481
+ return (f"Scanning is paused until {until} — {cd.get('reason') or 'LinkedIn pushed back'}. "
1482
+ f"Lift it on the Scan page if you're sure LinkedIn is fine.")
1483
+
1484
+
1485
+ def clear_bridge_skips():
1486
+ try:
1487
+ _skips_path().unlink()
1488
+ print("Cleared the skip list — every bridge will be tried again.")
1489
+ except FileNotFoundError:
1490
+ print("No skips recorded.")
1491
+
1492
+
1493
+ # How far each person's connections have been read.
1494
+ #
1495
+ # Reading someone's whole list can take a hundred pages and a quarter of an hour.
1496
+ # Without a note of where a read got to, a stop, LinkedIn's monthly search limit
1497
+ # or a crash threw all of it away and the next run started again at page 1 — and
1498
+ # before 0.1.6 nobody's list was read past page 10 at all, with no way to go
1499
+ # back for the rest. Every save now notes the last page read and whether
1500
+ # LinkedIn had more, and the next run carries on from the page after. Kept per
1501
+ # profile in the app, beside the skip list. TRAPS §34.
1502
+ #
1503
+ # {"<profile id>": {"<their profile url>": {
1504
+ # "name": ..., "pages": 25, # read through page 25
1505
+ # "more": true, # LinkedIn had a page 26
1506
+ # "urn": ..., # the id their connections are searched by
1507
+ # "hidden": true, # only when their list had gone private
1508
+ # "at": ...}}}
1509
+ def _progress_path():
1510
+ base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
1511
+ base.mkdir(parents=True, exist_ok=True)
1512
+ return base / "bridge-progress.json"
1513
+
1514
+
1515
+ def _progress_owner():
1516
+ return str(_active_user_id or "default")
1517
+
1518
+
1519
+ def _read_progress_file():
1520
+ try:
1521
+ data = json.loads(_progress_path().read_text())
1522
+ return data if isinstance(data, dict) else {}
1523
+ except Exception:
1524
+ return {}
1525
+
1526
+
1527
+ def load_bridge_progress():
1528
+ """How far each of this profile's people has been read, by profile URL."""
1529
+ mine = _read_progress_file().get(_progress_owner())
1530
+ return mine if isinstance(mine, dict) else {}
1531
+
1532
+
1533
+ def _change_progress(change):
1534
+ data = _read_progress_file()
1535
+ mine = data.get(_progress_owner())
1536
+ if not isinstance(mine, dict):
1537
+ mine = {}
1538
+ change(mine)
1539
+ data[_progress_owner()] = mine
1540
+ try:
1541
+ path = _progress_path()
1542
+ tmp = path.with_name(path.name + ".tmp")
1543
+ tmp.write_text(json.dumps(data, indent=2))
1544
+ tmp.replace(path) # never a half-written file
1545
+ except Exception as exc:
1546
+ print(f" (could not note how far this read got: {exc})")
1547
+
1548
+
1549
+ def record_bridge_progress(profile_url, name, pages, more, urn=None):
1550
+ """Note that someone's connections have been read through page `pages`.
1551
+
1552
+ It never winds the note back. A read from page 1 that stops short of an
1553
+ earlier, deeper one leaves the deeper one standing: those pages are still
1554
+ saved. forget_bridge_progress() is for when they are not.
1555
+ """
1556
+ def change(mine):
1557
+ old = mine.get(profile_url) or {}
1558
+ read, left = int(pages), bool(more)
1559
+ if int(old.get("pages") or 0) > read:
1560
+ read, left = int(old["pages"]), bool(old.get("more"))
1561
+ entry = {"name": name, "pages": read, "more": left,
1562
+ "at": datetime.now().isoformat(timespec="seconds")}
1563
+ if urn or old.get("urn"):
1564
+ entry["urn"] = urn or old.get("urn")
1565
+ mine[profile_url] = entry
1566
+ _change_progress(change)
1567
+
1568
+
1569
+ def mark_bridge_hidden(profile_url, name):
1570
+ """Their list was readable once and is not now. What is mapped stays."""
1571
+ def change(mine):
1572
+ old = mine.get(profile_url) or {"pages": LEGACY_PAGES_READ, "more": True}
1573
+ mine[profile_url] = {**old, "name": name, "hidden": True,
1574
+ "at": datetime.now().isoformat(timespec="seconds")}
1575
+ _change_progress(change)
1576
+
1577
+
1578
+ def forget_bridge_progress(profile_url):
1579
+ _change_progress(lambda mine: mine.pop(profile_url, None))
1580
+
1581
+
1582
+ def next_page_to_read(entry, retry_hidden=False):
1583
+ """The page to carry on from for someone already mapped, or None if there is nothing left.
1584
+
1585
+ No note at all means they were mapped before 0.1.6 kept one, and every read
1586
+ then stopped at page 10.
1587
+ """
1588
+ if entry is None:
1589
+ return LEGACY_PAGES_READ + 1
1590
+ if entry.get("hidden") and not retry_hidden:
1591
+ return None
1592
+ if not entry.get("more"):
1593
+ return None
1594
+ nxt = int(entry.get("pages") or 0) + 1
1595
+ return nxt if nxt <= LINKEDIN_MAX_PAGES else None
1596
+
1597
+
1598
+ # Stopping cleanly.
1599
+ #
1600
+ # A long auto-bridge run is the one thing here you will want to interrupt, and
1601
+ # killing the process outright leaves a Chrome window with a live LinkedIn
1602
+ # session orphaned behind it. So SIGTERM sets a flag instead: the loop notices
1603
+ # between people, and while counting down, and exits with a summary.
1604
+ _stop_requested = False
1605
+
1606
+
1607
+ def _request_stop(signum, _frame):
1608
+ global _stop_requested
1609
+ if not _stop_requested:
1610
+ _stop_requested = True
1611
+ print("\n Stopping after the current step — closing the browser cleanly...", flush=True)
1612
+
1613
+
1614
+ def install_stop_handler():
1615
+ for sig in (signal.SIGTERM, signal.SIGINT):
1616
+ try:
1617
+ signal.signal(sig, _request_stop)
1618
+ except (ValueError, OSError):
1619
+ pass # not the main thread (server mode) — nothing to install
1620
+
1621
+
1622
+ def stop_requested():
1623
+ return _stop_requested
1624
+
1625
+
1626
+ def interruptible_sleep(seconds, on_tick=None, step=5):
1627
+ """Sleep, but notice a stop request. Returns False if we were interrupted."""
1628
+ waited = 0
1629
+ while waited < seconds:
1630
+ if stop_requested():
1631
+ return False
1632
+ chunk = min(step, seconds - waited)
1633
+ time.sleep(chunk)
1634
+ waited += chunk
1635
+ if on_tick and waited < seconds:
1636
+ on_tick(seconds - waited)
1637
+ return not stop_requested()
1638
+
1639
+
1640
+
1641
+
1642
+ def _profile_unavailable(page):
1643
+ """LinkedIn's explicit "you cannot see this" pages.
1644
+
1645
+ Worth checking separately: these never grow a connections link, so without
1646
+ this the scraper spends its whole budget waiting for something that is not
1647
+ coming.
1648
+ """
1649
+ try:
1650
+ return page.evaluate("""
1651
+ () => {
1652
+ const t = (document.body.innerText || '').toLowerCase();
1653
+ return t.includes('this profile is not available')
1654
+ || t.includes('profile unavailable')
1655
+ || t.includes("this page doesn't exist")
1656
+ || t.includes('page not found');
1657
+ }
1658
+ """)
1659
+ except Exception:
1660
+ return False
1661
+
1662
+
1663
+ def _find_connection_urn(page):
1664
+ """The id LinkedIn uses to search someone's connections, read from an href."""
1665
+ try:
1666
+ return page.evaluate("""
1667
+ () => {
1668
+ for (const link of document.querySelectorAll('a')) {
1669
+ const m = (link.href || '').match(/connectionOf[=%5B%22"]*([A-Za-z0-9_:-]+)/);
1670
+ if (m) return m[1];
1671
+ }
1672
+ return null;
1673
+ }
1674
+ """)
1675
+ except Exception:
1676
+ return None
1677
+
1678
+
1679
+ # What one page of someone's connections holds.
1680
+ #
1681
+ # A raw string (r"""), like every snippet of JavaScript in this file must be. In
1682
+ # a plain string Python turns the "\n" in split('\n') into a real line break
1683
+ # before the browser sees it, and the whole function is a syntax error —
1684
+ # "Invalid or unexpected token". That broke every 2nd-degree scan on page 1 from
1685
+ # 2026-09-09 until it was noticed. tests/scraper-js.test.mjs now parses every
1686
+ # snippet on every PR. TRAPS §31.
1687
+ BRIDGE_RESULTS_JS = r"""
1688
+ () => {
1689
+ // Build URL map from profile links
1690
+ // Anchor on the profile link, never on the anchor's text.
1691
+ //
1692
+ // This used to build a name -> URL map keyed by the link text, first
1693
+ // one wins. Any two results sharing display text collapsed onto a
1694
+ // single URL — and "LinkedIn Member" is the text for every
1695
+ // out-of-network person in a 2nd-degree search, so one person's photo
1696
+ // was handed to everybody who happened to be unnamed. That is where
1697
+ // the duplicate faces came from.
1698
+ const clean = (t) => (t || '').replace(/[\u2019']s profile picture$/i, '').trim();
1699
+ const byHref = new Map();
1700
+ const root = document.querySelector('[role="main"], main') || document.body;
1701
+
1702
+ root.querySelectorAll('a[href*="/in/"]').forEach((a) => {
1703
+ if (a.closest('nav, header, footer')) return;
1704
+ const raw = a.getAttribute('href') || '';
1705
+ if (!raw.includes('/in/')) return;
1706
+ let url = raw.startsWith('http') ? raw : 'https://www.linkedin.com' + raw;
1707
+ url = url.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/';
1708
+
1709
+ let rec = byHref.get(url);
1710
+ if (!rec) { rec = { profileUrl: url, name: '', headline: '', imageUrl: '', named: false }; byHref.set(url, rec); }
1711
+
1712
+ const img = a.querySelector('img');
1713
+ if (img && img.src && img.src.includes('media.licdn.com') && !/ghost/.test(img.src)) {
1714
+ if (!rec.imageUrl) rec.imageUrl = img.src;
1715
+ if (!rec.name && img.alt) rec.name = clean(img.alt);
1716
+ }
1717
+
1718
+ const lines = (a.innerText || '').split('\n').map((l) => l.trim()).filter(Boolean);
1719
+ if (lines.length && !rec.named) {
1720
+ const first = clean(lines[0]);
1721
+ if (first && first.length <= 60 && !/^(Connect|Follow|Message|Invite)$/i.test(first)) {
1722
+ rec.name = first;
1723
+ rec.named = true;
1724
+ }
1725
+ }
1726
+
1727
+ // Headline lives beside the name in the result card, not inside the
1728
+ // anchor — walk up to the card and take the first real line after
1729
+ // the name. Not simply the next line: LinkedIn puts a screen-reader
1730
+ // line ("View Jane Doe's profile") straight after the name, and taking
1731
+ // that saved it as everybody's headline. Skip lines that are chrome.
1732
+ const chrome = (l) => /^View\b.*profile$/i.test(l)
1733
+ || /^(Connect|Follow|Message|Invite|Pending)$/i.test(l)
1734
+ || /mutual connection/i.test(l)
1735
+ || /followers$/i.test(l)
1736
+ || /degree connection/i.test(l)
1737
+ || /^(1st|2nd|3rd\+?)$/i.test(l)
1738
+ || /^\u2022/.test(l);
1739
+ if (!rec.headline) {
1740
+ let card = a;
1741
+ for (let k = 0; k < 6 && card.parentElement; k++) {
1742
+ card = card.parentElement;
1743
+ const cardLines = (card.innerText || '').split('\n').map((l) => l.trim()).filter(Boolean);
1744
+ const idx = cardLines.findIndex((l) => clean(l) === rec.name);
1745
+ if (idx >= 0) {
1746
+ const cand = cardLines.slice(idx + 1, idx + 5).find((l) => !chrome(l) && clean(l) !== rec.name);
1747
+ if (cand) rec.headline = cand;
1748
+ break;
1749
+ }
1750
+ }
1751
+ }
1752
+ });
1753
+
1754
+ const results = [...byHref.values()]
1755
+ .filter((r) => r.name && r.name.length >= 2)
1756
+ .map(({ named, ...r }) => r);
1757
+ return results;
1758
+ }
1759
+ """
1760
+
1761
+ # The profile links on the page, as a fingerprint: how a click on Next is known
1762
+ # to have brought up a different page rather than the same one again.
1763
+ RESULT_LINKS_JS = r"""
1764
+ () => {
1765
+ const root = document.querySelector('[role="main"], main') || document.body;
1766
+ const out = new Set();
1767
+ root.querySelectorAll('a[href*="/in/"]').forEach((a) => {
1768
+ if (a.closest('nav, header, footer')) return;
1769
+ out.add((a.getAttribute('href') || '').split('?')[0]);
1770
+ });
1771
+ return [...out].sort();
1772
+ }
1773
+ """
1774
+
1775
+ # "About 1,340 results" above the list. Only for the log: it is approximate, and
1776
+ # the end of a list is decided by the pages themselves.
1777
+ RESULT_COUNT_JS = r"""
1778
+ () => {
1779
+ const root = document.querySelector('[role="main"], main') || document.body;
1780
+ const text = (root.innerText || '').slice(0, 3000);
1781
+ const m = text.match(/(?:About\s+)?(\d[\d,.]*)\s*([KkMm])?\+?\s+results?\b/);
1782
+ if (!m) return null;
1783
+ let n = parseFloat(m[1].replace(/,/g, ''));
1784
+ if (m[2]) n *= /k/i.test(m[2]) ? 1000 : 1000000;
1785
+ return Number.isFinite(n) && n > 0 ? Math.round(n) : null;
1786
+ }
1787
+ """
1788
+
1789
+ # LinkedIn's wording when a free account has used its searches for the month.
1790
+ SEARCH_LIMIT_JS = r"""
1791
+ () => {
1792
+ const t = (document.body.innerText || '').toLowerCase();
1793
+ // Reached, not approaching: "approaching the commercial use limit" is a
1794
+ // warning (PUSHBACK_JS), and reporting it as the limit sent people off to
1795
+ // wait for next month.
1796
+ const approaching = t.includes('approaching the commercial use limit');
1797
+ return (t.includes('commercial use limit') && !approaching)
1798
+ || t.includes('reached the monthly limit')
1799
+ || t.includes('reached your monthly limit');
1800
+ }
1801
+ """
1802
+
1803
+ # LinkedIn pushing back, in its own words (Help Center articles a1393432,
1804
+ # a1340567, a1339220) and by where it sends the page. Any of these ends the
1805
+ # batch: carrying on after an "unusual activity" warning or a security check is
1806
+ # what LinkedIn says turns a warning into a restriction. TRAPS §35.
1807
+ PUSHBACK_JS = r"""
1808
+ () => {
1809
+ const path = location.pathname.toLowerCase();
1810
+ if (path.startsWith('/checkpoint')) return 'a security check';
1811
+ if (path.startsWith('/authwall') || path.startsWith('/uas/') || path.startsWith('/login')) return 'being signed out';
1812
+ const t = (document.body.innerText || '').toLowerCase();
1813
+ if (t.includes('unusual activity from your account')) return 'an unusual-activity warning';
1814
+ if (t.includes('ability to view profiles has been temporarily restricted')
1815
+ || t.includes('profile viewing temporarily restricted')) return 'profile viewing being restricted';
1816
+ if (t.includes('restricted your account') || t.includes('account has been temporarily restricted')) return 'the account being restricted';
1817
+ if (t.includes('approaching the commercial use limit')) return 'the monthly search limit coming up';
1818
+ return null;
1819
+ }
1820
+ """
1821
+
1822
+ # Did their profile page really render? Their first name in the tab title or the
1823
+ # page heading means LinkedIn served the profile — so a missing connections link
1824
+ # is them hiding their list, not a page that failed to load (TRAPS §35).
1825
+ PROFILE_SHOWN_JS = r"""
1826
+ (name) => {
1827
+ // Whole words only, and never against " | LinkedIn": every tab is titled
1828
+ // LinkedIn, and "li", "lin" or "ed" are inside it — a blank page counted as
1829
+ // Li Wei's profile.
1830
+ const norm = (s) => String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();
1831
+ const first = norm(name).split(' ')[0];
1832
+ if (!first || first.length < 2) return false;
1833
+ const esc = first.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1834
+ const word = new RegExp('(^|[^\\p{L}])' + esc + '([^\\p{L}]|$)', 'u');
1835
+ const title = norm(document.title).replace(/^\(\d+\)\s*/, '').replace(/\s*\|\s*linkedin\s*$/, '');
1836
+ const h1 = document.querySelector('main h1, h1');
1837
+ const heading = norm(h1 && h1.innerText);
1838
+ return word.test(heading) || (title !== 'linkedin' && word.test(title));
1839
+ }
1840
+ """
1841
+
1842
+ # LinkedIn's empty-search message: positive evidence that a list has ended, as
1843
+ # opposed to a page that simply didn't load.
1844
+ NO_RESULTS_JS = r"""
1845
+ () => {
1846
+ const root = document.querySelector('[role="main"], main') || document.body;
1847
+ return /no results found/i.test(root.innerText || '');
1848
+ }
1849
+ """
1850
+
1851
+
1852
+ def _window_closed(page):
1853
+ """Closing the browser window is how you say "stop". Treated as a stop."""
1854
+ global _stop_requested
1855
+ try:
1856
+ closed = page.is_closed()
1857
+ except Exception:
1858
+ closed = True
1859
+ if closed and not stop_requested():
1860
+ _stop_requested = True
1861
+ print("\n The browser window was closed — stopping, and keeping what was read.")
1862
+ return closed
1863
+
1864
+
1865
+ def _pushback(page):
1866
+ """The reason LinkedIn is pushing back, or None."""
1867
+ try:
1868
+ return page.evaluate(PUSHBACK_JS)
1869
+ except Exception:
1870
+ return None
1871
+
1872
+
1873
+ def _keep_pushback_evidence(page, reason):
1874
+ """Keep what the page showed, so the wording can be recognised next time.
1875
+
1876
+ The real notices have never been captured (PHASES.md). Local only, beside
1877
+ the database; nothing here leaves the machine.
1878
+ """
1879
+ try:
1880
+ text = page.evaluate("() => (document.body.innerText || '').slice(0, 2000)")
1881
+ except Exception:
1882
+ text = ""
1883
+ try:
1884
+ base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees")) / "pushback"
1885
+ base.mkdir(parents=True, exist_ok=True)
1886
+ stamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
1887
+ (base / f"{stamp}.txt").write_text(f"reason: {reason}\nurl: {getattr(page, 'url', '')}\n\n{text}\n")
1888
+ print(f" (What the page showed is kept in {base}/{stamp}.txt)")
1889
+ except Exception:
1890
+ pass
1891
+
1892
+
1893
+ def _pushback_advice(reason):
1894
+ """What to do next, by what LinkedIn did."""
1895
+ reason = reason or ""
1896
+ if "security check" in reason:
1897
+ if os.environ.get("SIX_DEGREES_FROM_APP"):
1898
+ return ('Finish the check by hand: on the Scan page, step 2, press "Open LinkedIn". '
1899
+ "Then leave scanning for a day.")
1900
+ return "Finish the check by hand: run with --login. Then leave scanning for a day."
1901
+ if "signed out" in reason:
1902
+ return "Sign in again (Open LinkedIn on the Scan page, or --login), then leave it a day."
1903
+ return "Leave it at least a day before scanning again."
1904
+
1905
+
1906
+ def _pushed_back(page, reach, reason):
1907
+ """Note push-back on this read: the rest is left for later, never marked finished."""
1908
+ reach["pushed_back"] = True
1909
+ reach["pushback_reason"] = reason
1910
+ reach["more"] = True
1911
+ print(f" LinkedIn is pushing back ({reason}). Stopping here.")
1912
+ _keep_pushback_evidence(page, reason)
1913
+
1914
+
1915
+ # Which page the pagination bar says is showing, when it says.
1916
+ CURRENT_PAGE_JS = r"""
1917
+ () => {
1918
+ for (const el of document.querySelectorAll('[aria-current="true"], [aria-current="page"]')) {
1919
+ const t = (el.innerText || el.textContent || '').trim();
1920
+ if (/^\d{1,3}$/.test(t)) return parseInt(t, 10);
1921
+ }
1922
+ return null;
1923
+ }
1924
+ """
1925
+
1926
+
1927
+ def _url_page(url):
1928
+ """The page number in a LinkedIn search URL; page 1 when there is none."""
1929
+ try:
1930
+ return int(parse_qs(urlparse(url).query).get("page", ["1"])[0])
1931
+ except (ValueError, TypeError):
1932
+ return 1
1933
+
1934
+
1935
+ def _result_links(page):
1936
+ try:
1937
+ return page.evaluate(RESULT_LINKS_JS)
1938
+ except Exception:
1939
+ return []
1940
+
1941
+
1942
+ def _scroll_to_foot(page):
1943
+ try:
1944
+ page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
1945
+ except Exception:
1946
+ pass
1947
+
1948
+
1949
+ def _read_results_page(page):
1950
+ """The people on the current page, looked for END_CHECKS times if it seems empty."""
1951
+ for attempt in range(1, END_CHECKS + 1):
1952
+ found = page.evaluate(BRIDGE_RESULTS_JS)
1953
+ if found:
1954
+ return found
1955
+ if attempt < END_CHECKS:
1956
+ _scroll_to_foot(page)
1957
+ time.sleep(2 * attempt)
1958
+ return []
1959
+
1960
+
1961
+ def _find_next(page):
1962
+ """LinkedIn's Next button when there is another page, else None.
1963
+
1964
+ Looked for END_CHECKS times, scrolling to the foot of the list and waiting a
1965
+ little longer each time: the pagination bar renders late, and calling a list
1966
+ finished because the button was slow would stop that person short for good.
1967
+ A Next that is there but disabled is LinkedIn saying this is the last page.
1968
+ """
1969
+ for attempt in range(1, END_CHECKS + 1):
1970
+ try:
1971
+ btn = page.locator('button:has-text("Next")').first
1972
+ if btn.is_visible() and btn.is_enabled():
1973
+ return btn
1974
+ except Exception:
1975
+ pass
1976
+ if attempt < END_CHECKS:
1977
+ print(f" No Next button yet; looking again ({attempt + 1} of {END_CHECKS})...")
1978
+ _scroll_to_foot(page)
1979
+ time.sleep(2 * attempt)
1980
+ return None
1981
+
1982
+
1983
+ def _wait_for_new_results(page, before, seconds):
1984
+ """True once the profile links differ from `before` — the page really changed."""
1985
+ waited = 0
1986
+ while waited < seconds:
1987
+ time.sleep(1)
1988
+ waited += 1
1989
+ now = _result_links(page)
1990
+ if now and now != before:
1991
+ return True
1992
+ return False
1993
+
1994
+
1995
+ def _advance(page, pg):
1996
+ """Go from results page pg to pg + 1. Returns "moved", "end" or "stuck".
1997
+
1998
+ "end" only once _find_next has looked END_CHECKS times. A click that leaves
1999
+ the same results showing is tried again, up to END_CHECKS times; after that
2000
+ the read stops as "stuck", which leaves the rest to the next run rather than
2001
+ calling the list finished.
2002
+ """
2003
+ before = _result_links(page)
2004
+ btn = _find_next(page)
2005
+ if btn is None:
2006
+ # The end of a list is only believed from a live page that was showing
2007
+ # results. A closed window or a blank page proves nothing, and a wrong
2008
+ # "end" marks a list finished for good.
2009
+ if _window_closed(page) or not before:
2010
+ return "stuck"
2011
+ return "end"
2012
+ for attempt in range(1, END_CHECKS + 1):
2013
+ charge_linkedin("searches") # every click that can load a page counts
2014
+ try:
2015
+ btn.click(timeout=15000)
2016
+ except Exception:
2017
+ pass
2018
+ if _wait_for_new_results(page, before, 10):
2019
+ return "moved"
2020
+ if _url_page(page.url) == pg + 1:
2021
+ # The click landed and the page is just slow. Clicking again could
2022
+ # skip a page, so give it longer instead.
2023
+ return "moved" if _wait_for_new_results(page, before, 15) else "stuck"
2024
+ print(f" Page {pg + 1} did not open; clicking Next again ({attempt + 1} of {END_CHECKS})...")
2025
+ btn = _find_next(page)
2026
+ if btn is None:
2027
+ return "stuck"
2028
+ return "stuck"
2029
+
2030
+
2031
+ def _dedupe(connections):
2032
+ """Keep each profile once: the app refused a whole batch that named anyone twice (TRAPS §32)."""
2033
+ seen, unique = set(), []
2034
+ for c in connections:
2035
+ url = c.get("profileUrl")
2036
+ if url and url not in seen:
2037
+ seen.add(url)
2038
+ unique.append(c)
2039
+ return unique
2040
+
2041
+
2042
+ def _page_limit_hint(next_page):
2043
+ if os.environ.get("SIX_DEGREES_FROM_APP"):
2044
+ return (f'To read the rest, set "Read up to" to every page on the Scan page and keep '
2045
+ f'"Also finish people already mapped" ticked: the next run carries on from page {next_page}.')
2046
+ return f"Run again with --deeper, and no --max-pages, to carry on from page {next_page}."
2047
+
2048
+
2049
+ def _scrape_one_bridge(page, bridge_name, bridge_id, profile_url, max_pages=LINKEDIN_MAX_PAGES,
2050
+ start_page=1, urn=None, on_save=None):
2051
+ """
2052
+ Read one person's connections, from start_page until their list ends or
2053
+ max_pages. Takes an already-open Playwright page.
2054
+
2055
+ Returns (connections, status, reach). `connections` is what has not already
2056
+ been handed to on_save(connections, last_page, more, urn), which is called
2057
+ every SAVE_EVERY_PAGES pages of a long read. `reach` says how far it got:
2058
+ last the last page read (start_page - 1 if none)
2059
+ more LinkedIn has pages after `last` that were not read
2060
+ urn the id LinkedIn searches their connections by
2061
+ found how many people were read in all
2062
+ limited LinkedIn's monthly search limit ended the read
2063
+
2064
+ Timing is calibrated from successful runs:
2065
+ - 30s profile render wait (LinkedIn is slow)
2066
+ - 10s after search URL navigation
2067
+ - 3s on each page before reading it
2068
+ - Playwright locator click for Next (not JS — more reliable)
2069
+ """
2070
+ slug = profile_url.split("/in/")[1].rstrip("/")
2071
+ reach = {"last": start_page - 1, "more": False, "urn": urn, "found": 0, "limited": False,
2072
+ "pushed_back": False}
2073
+
2074
+ if urn:
2075
+ # Known from an earlier read, so their profile needn't be opened again —
2076
+ # one fewer profile view, which is what LinkedIn counts most.
2077
+ print(f" Going straight to {bridge_name}'s connections (their search id is known).")
2078
+ else:
2079
+ # Step 1: Navigate to profile (wait_until="commit" — don't wait for full load)
2080
+ print(f" Opening {bridge_name}'s profile...")
2081
+ charge_linkedin("profiles")
2082
+ try:
2083
+ page.goto(profile_url, wait_until="commit")
2084
+ except Exception as e:
2085
+ print(f" Navigation slow: {str(e)[:50]}... continuing anyway")
2086
+
2087
+ # Steps 2 and 3, together. The URN is what we are actually after, and it is
2088
+ # read from a link's href — so look for it on every pass instead of waiting
2089
+ # for a "500+ connections" link first and only then extracting. A public
2090
+ # profile now finishes as soon as the link exists, and a private one is not
2091
+ # charged an extra scroll-and-wait on the way to the same answer.
2092
+ for attempt in range(BRIDGE_LOAD_ATTEMPTS):
2093
+ time.sleep(BRIDGE_LOAD_STEP)
2094
+
2095
+ why = _pushback(page)
2096
+ if why:
2097
+ _pushed_back(page, reach, why)
2098
+ return [], "pushback", reach
2099
+
2100
+ if _profile_unavailable(page):
2101
+ print(" Profile is not viewable — skipping.")
2102
+ return [], "private", reach
2103
+
2104
+ urn = _find_connection_urn(page)
2105
+ if urn:
2106
+ print(f" Profile loaded ({(attempt + 1) * BRIDGE_LOAD_STEP}s)")
2107
+ break
2108
+
2109
+ # The connections block lazy-loads; nudge it once part-way through.
2110
+ if attempt == BRIDGE_LOAD_ATTEMPTS // 2:
2111
+ try:
2112
+ page.evaluate("window.scrollTo(0, 600)")
2113
+ except Exception:
2114
+ pass
2115
+ elif attempt < BRIDGE_LOAD_ATTEMPTS - 1:
2116
+ print(f" Still loading... ({(attempt + 1) * BRIDGE_LOAD_STEP}s)")
2117
+
2118
+ if not urn:
2119
+ why = _pushback(page)
2120
+ if why:
2121
+ _pushed_back(page, reach, why)
2122
+ return [], "pushback", reach
2123
+ shown = False
2124
+ try:
2125
+ shown = bool(page.evaluate(PROFILE_SHOWN_JS, bridge_name))
2126
+ except Exception:
2127
+ pass
2128
+ if stop_requested() or _window_closed(page):
2129
+ return [], "stopped", reach
2130
+ if not shown:
2131
+ # Their profile never rendered, so this says nothing about them.
2132
+ # Marking them hidden here is what made a block skip-list people
2133
+ # for good; this counts towards the batch's circuit breaker.
2134
+ print(" Their profile didn't load properly, so they're not marked hidden.")
2135
+ return [], "unclear", reach
2136
+ print(f" Could not find URN — connections are private.")
2137
+ return [], "private", reach
2138
+
2139
+ print(f" Found URN: {urn[:30]}...")
2140
+ reach["urn"] = urn
2141
+
2142
+ # Step 4: search everyone they are connected to. network=["F","S","O"] is
2143
+ # 1st, 2nd and 3rd+ — all of them; the app drops your own connections when
2144
+ # it saves. It used to be announced as a "3rd+ filter", which it never was.
2145
+ search_url = f"https://www.linkedin.com/search/results/people/?network=%5B%22F%22%2C%22S%22%2C%22O%22%5D&connectionOf=%5B%22{urn}%22%5D"
2146
+ left, kind = searches_left()
2147
+ if left <= 0:
2148
+ reach["budget"] = kind
2149
+ reach["more"] = True
2150
+ print(" " + budget_message(kind))
2151
+ return [], "budget", reach
2152
+ if start_page > 1:
2153
+ search_url += f"&page={start_page}"
2154
+ print(f" Opening their connections at page {start_page}...")
2155
+ else:
2156
+ print(" Opening their connections...")
2157
+ charge_linkedin("searches")
2158
+ try:
2159
+ page.goto(search_url, wait_until="commit")
2160
+ except:
2161
+ pass
2162
+
2163
+ # Step 5: Wait for search results — 10s initial + up to 30s retry
2164
+ # Successful runs: results appear within 10-15s
2165
+ time.sleep(10)
2166
+ results_loaded = False
2167
+ for attempt in range(6):
2168
+ try:
2169
+ page.wait_for_selector('a[href*="/in/"]', timeout=5000)
2170
+ print(" Search results loaded!")
2171
+ results_loaded = True
2172
+ break
2173
+ except:
2174
+ print(f" Still loading... ({10 + (attempt+1)*5}s)")
2175
+ time.sleep(5)
2176
+
2177
+ try:
2178
+ at_limit = bool(page.evaluate(SEARCH_LIMIT_JS))
2179
+ except Exception:
2180
+ at_limit = False
2181
+ why = _pushback(page)
2182
+ if why:
2183
+ _pushed_back(page, reach, why)
2184
+ return [], "pushback", reach
2185
+ if at_limit:
2186
+ print(" LinkedIn's monthly search limit.")
2187
+ reach["limited"] = reach["more"] = True
2188
+ _keep_pushback_evidence(page, "the monthly search limit")
2189
+ return [], "limited", reach
2190
+
2191
+ if not results_loaded:
2192
+ if stop_requested() or _window_closed(page):
2193
+ reach["more"] = True
2194
+ return [], "stopped", reach
2195
+ try:
2196
+ says_none = bool(page.evaluate(NO_RESULTS_JS))
2197
+ except Exception:
2198
+ says_none = False
2199
+ if says_none:
2200
+ # LinkedIn's own empty state: a real answer, not push-back.
2201
+ print(" LinkedIn says there are no results here.")
2202
+ if start_page > 1:
2203
+ return [], "success", reach # carrying on: their list ended
2204
+ return [], "empty", reach
2205
+ # Their connections link was there, so their list is visible: a search
2206
+ # that won't open is LinkedIn failing us, not them hiding anything.
2207
+ _pushed_back(page, reach, "a search that would not open")
2208
+ return [], "pushback", reach
2209
+
2210
+ if start_page > 1:
2211
+ # Carrying on only works if LinkedIn really opened that page. If it
2212
+ # quietly showed page 1 instead, reading on would note pages as read
2213
+ # that never were.
2214
+ shown = None
2215
+ try:
2216
+ shown = page.evaluate(CURRENT_PAGE_JS)
2217
+ except Exception:
2218
+ pass
2219
+ at = _url_page(page.url)
2220
+ if at != start_page or (shown is not None and shown != start_page):
2221
+ print(f" LinkedIn opened page {shown or at}, not page {start_page}. "
2222
+ "Stopping so no page is noted as read that wasn't.")
2223
+ reach["more"] = True
2224
+ return [], "error", reach
2225
+
2226
+ try:
2227
+ total = page.evaluate(RESULT_COUNT_JS)
2228
+ except Exception:
2229
+ total = None
2230
+ if total:
2231
+ pages = -(-total // 10)
2232
+ print(f" About {total:,} results in their list: {pages} pages"
2233
+ + (f", and LinkedIn shows the first {LINKEDIN_MAX_PAGES}." if pages > LINKEDIN_MAX_PAGES else "."))
2234
+ until = "until their list ends" if max_pages >= LINKEDIN_MAX_PAGES else f"up to page {max_pages}"
2235
+ print(f" Reading from page {start_page} {until}, saving every {SAVE_EVERY_PAGES} pages.")
2236
+
2237
+ # Step 6: read each page, then move to the next, until the list ends. Every
2238
+ # page is a search on your account.
2239
+ chunk, chunk_pages, pg = [], 0, start_page
2240
+ while True:
2241
+ if stop_requested() or _window_closed(page):
2242
+ reach["more"] = True
2243
+ print(f" Stopped at page {pg}; the next run carries on from there.")
2244
+ break
2245
+ print(f" Page {pg}... ", end="", flush=True)
2246
+ try:
2247
+ time.sleep(3)
2248
+ if page.evaluate(SEARCH_LIMIT_JS):
2249
+ print("LinkedIn's monthly search limit.")
2250
+ reach["limited"] = reach["more"] = True
2251
+ _keep_pushback_evidence(page, "the monthly search limit")
2252
+ break
2253
+ why = _pushback(page)
2254
+ if why:
2255
+ print()
2256
+ _pushed_back(page, reach, why)
2257
+ break
2258
+ page_results = _read_results_page(page)
2259
+ except Exception:
2260
+ if stop_requested() or _window_closed(page): # the browser went down with the stop
2261
+ reach["more"] = True
2262
+ print("stopped.")
2263
+ break
2264
+ raise
2265
+ if not page_results:
2266
+ why = _pushback(page)
2267
+ if why:
2268
+ print()
2269
+ _pushed_back(page, reach, why)
2270
+ break
2271
+ ended = False
2272
+ try:
2273
+ ended = bool(page.evaluate(NO_RESULTS_JS))
2274
+ except Exception:
2275
+ pass
2276
+ if pg == start_page and start_page > 1 and not ended:
2277
+ # Carrying on, and the very first page is blank without saying
2278
+ # "No results found". That is how a search LinkedIn is limiting
2279
+ # can look, so it is not taken as the end: a wrong "finished"
2280
+ # would drop the rest of their list for good. TRAPS §35.
2281
+ reach["more"] = True
2282
+ reach["unsure"] = True
2283
+ print(f"blank. That can mean their list ended, or that LinkedIn is limiting "
2284
+ f"searches, so they are left for the next run to check.")
2285
+ break
2286
+ print(f"nothing on it after {END_CHECKS} looks — that's the end of their list.")
2287
+ break
2288
+
2289
+ page_results = [c for c in page_results if slug not in c.get("profileUrl", "")]
2290
+ chunk.extend(page_results)
2291
+ chunk_pages += 1
2292
+ reach["last"] = pg
2293
+ reach["found"] += len(page_results)
2294
+ print(f"{len(page_results)} found (total: {reach['found']})")
2295
+
2296
+ if pg >= LINKEDIN_MAX_PAGES:
2297
+ print(f" That was page {pg}, as far as LinkedIn's search goes.")
2298
+ break
2299
+ if pg >= max_pages:
2300
+ reach["more"] = _find_next(page) is not None or _window_closed(page)
2301
+ if reach["more"]:
2302
+ print(f" Stopped at the {max_pages}-page limit, and LinkedIn has more. "
2303
+ + _page_limit_hint(pg + 1))
2304
+ else:
2305
+ print(" No more pages — that's all of their list.")
2306
+ break
2307
+
2308
+ # The budget is checked before every search, and it ends the read on its
2309
+ # own path: through the page-limit branch, a missing Next button would
2310
+ # have marked the list finished.
2311
+ left, kind = searches_left()
2312
+ if left <= 0:
2313
+ # Looking for Next costs nothing. No Next on a live page of results
2314
+ # means the list is finished — not paused at the next page forever.
2315
+ if _find_next(page) is None and not _window_closed(page) and _result_links(page):
2316
+ print(f" No Next button after {END_CHECKS} looks — that's all of their list.")
2317
+ break
2318
+ reach["budget"] = kind
2319
+ reach["more"] = True
2320
+ print(" " + budget_message(kind))
2321
+ break
2322
+
2323
+ # Rest before the next search, and longer after every SAVE_EVERY_PAGES.
2324
+ rest = PAGE_PAUSE
2325
+ if (pg - start_page + 1) % SAVE_EVERY_PAGES == 0:
2326
+ rest += CHUNK_COOLDOWN
2327
+ print(f" {SAVE_EVERY_PAGES} pages read; resting {rest}s before the next search.")
2328
+ if not interruptible_sleep(rest) or _window_closed(page):
2329
+ reach["more"] = True
2330
+ print(f" Stopped before page {pg + 1}; the next run carries on from there.")
2331
+ break
2332
+
2333
+ try:
2334
+ step = _advance(page, pg)
2335
+ except Exception:
2336
+ if not stop_requested():
2337
+ raise
2338
+ step = "stuck"
2339
+ if step == "end":
2340
+ print(f" No Next button after {END_CHECKS} looks — that's all of their list.")
2341
+ break
2342
+ if step == "stuck":
2343
+ reach["more"] = True
2344
+ if not stop_requested() and not _window_closed(page):
2345
+ print(f" Page {pg + 1} would not open after {END_CHECKS} tries. LinkedIn may be "
2346
+ f"limiting searches; the next run carries on from there.")
2347
+ _pushed_back(page, reach, _pushback(page) or "a page that would not open")
2348
+ break
2349
+ pg += 1
2350
+
2351
+ # Save as it goes, so a stop, a crash or the search limit costs at most
2352
+ # the last few pages rather than the whole list.
2353
+ if on_save and chunk_pages >= SAVE_EVERY_PAGES:
2354
+ on_save(_dedupe(chunk), reach["last"], True, reach["urn"])
2355
+ chunk, chunk_pages = [], 0
2356
+
2357
+ # Filter out the bridge themselves (already done page by page) and keep each
2358
+ # profile once: the same person can turn up on more than one page, and a
2359
+ # mutual connection repeats under many results.
2360
+ chunk = _dedupe(chunk)
2361
+ print(f" Read {reach['found']} in all, pages {start_page}–{reach['last']}"
2362
+ if reach["last"] >= start_page else " Nothing read.")
2363
+
2364
+ return chunk, "success", reach
2365
+
2366
+
2367
+ class BridgeRead(list):
2368
+ """The connections read, plus how the read went (`status`).
2369
+
2370
+ A list, so everything that only wants the people — the CLI, server mode —
2371
+ keeps working. auto_bridge_all reads `status` to tell "their list is hidden"
2372
+ (LinkedIn served their profile, with no connections link) from "unclear"
2373
+ (the profile never rendered), which must never be recorded as hidden.
2374
+ """
2375
+ status = "success"
2376
+
2377
+
2378
+ def _read(connections, status):
2379
+ out = BridgeRead(connections or [])
2380
+ out.status = status
2381
+ return out
2382
+
2383
+
2384
+ def scrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES, deeper=False, fresh=False,
2385
+ profile_url=None):
2386
+ """Read one person's connections into the app (opens its own browser).
2387
+
2388
+ deeper: carry on from the page the last read of them stopped at, instead of
2389
+ starting again at page 1.
2390
+ fresh: their mapped circle was just deleted, so forget how far it was read.
2391
+
2392
+ Returns every connection read ([] when none; None when they could not be
2393
+ looked up). Raises SaveFailed if the app refuses a save, NotSignedIn if
2394
+ LinkedIn isn't signed in, and, after saving, SearchLimitReached if
2395
+ LinkedIn's monthly limit ends the read or LinkedInPushedBack if a page
2396
+ would not open.
2397
+ """
2398
+ from playwright.sync_api import sync_playwright
2399
+
2400
+ # Nothing searches while a cooldown is on, or with no budget left. Checked
2401
+ # before a browser opens: even the sign-in check is a LinkedIn page view.
2402
+ cd = read_cooldown()
2403
+ if cd:
2404
+ raise CoolingDown(cd)
2405
+ left, kind = searches_left()
2406
+ if left <= 0:
2407
+ print(budget_message(kind))
2408
+ raise BudgetReached(0, kind)
2409
+
2410
+ # Find the bridge (read-only, filtered by active user). By profile URL when
2411
+ # given — Resume uses it, since two connections can share a name — else by name.
2412
+ params = ({"profile_url": f"eq.{profile_url}", "degree": "eq.1", "limit": "1"} if profile_url
2413
+ else {"name": f"eq.{bridge_name}", "degree": "eq.1", "limit": "1"})
2414
+ if _active_user_id:
2415
+ params["user_id"] = f"eq.{_active_user_id}"
2416
+ bridges = read_connections(params=params)
2417
+ if not bridges:
2418
+ print(f"Bridge '{bridge_name or profile_url}' not found in database.")
2419
+ return
2420
+
2421
+ bridge = bridges[0]
2422
+ bridge_id = bridge["id"]
2423
+ profile_url = bridge["profile_url"]
2424
+ bridge_name = bridge.get("name") or bridge_name
2425
+
2426
+ if fresh:
2427
+ forget_bridge_progress(profile_url)
2428
+ start_page, urn = 1, None
2429
+ if deeper:
2430
+ entry = load_bridge_progress().get(profile_url)
2431
+ nxt = next_page_to_read(entry, retry_hidden=True)
2432
+ if nxt is None:
2433
+ print(f"Every page of {bridge_name}'s connections has been read already.")
2434
+ return _read([], "finished")
2435
+ if nxt > max_pages:
2436
+ print(f"Pages 1–{nxt - 1} of {bridge_name}'s connections are read already. "
2437
+ f"Choose more than {max_pages} pages to read further.")
2438
+ return _read([], "finished")
2439
+ start_page, urn = nxt, (entry or {}).get("urn")
2440
+
2441
+ print(f"\n=== {'Carrying on with' if start_page > 1 else 'Scraping'} connections of {bridge_name} ===\n")
2442
+
2443
+ read = []
2444
+
2445
+ def save(connections, last, more, urn_now):
2446
+ if not connections:
2447
+ record_bridge_progress(profile_url, bridge_name, last, more, urn_now)
2448
+ return
2449
+ print(f" Saving {len(connections)} (through page {last})...")
2450
+ push_connections(connections, degree=2, bridge_id=bridge_id,
2451
+ on_saved=lambda: record_bridge_progress(profile_url, bridge_name, last, more, urn_now))
2452
+ read.extend(connections)
2453
+
2454
+ outcome = None
2455
+ try:
2456
+ with sync_playwright() as p:
2457
+ browser = p.chromium.launch_persistent_context(
2458
+ user_data_dir=get_scraper_profile_path(),
2459
+ headless=headless,
2460
+ channel="chrome",
2461
+ args=["--disable-blink-features=AutomationControlled"],
2462
+ timeout=120000,
2463
+ )
2464
+ try:
2465
+ page = browser.pages[0] if browser.pages else browser.new_page()
2466
+ page.set_default_timeout(120000)
2467
+ page.set_default_navigation_timeout(120000)
2468
+
2469
+ # No page load before this: ensure_logged_in has to see whether
2470
+ # the session cookie was there *before* LinkedIn answered, or a
2471
+ # sign-out between people looks like a first sign-in.
2472
+ if not ensure_logged_in(page, stop_on_checkpoint=True):
2473
+ raise NotSignedIn()
2474
+
2475
+ outcome = _scrape_one_bridge(page, bridge_name, bridge_id, profile_url,
2476
+ max_pages=max_pages, start_page=start_page, urn=urn,
2477
+ on_save=save)
2478
+ finally:
2479
+ try:
2480
+ browser.close()
2481
+ except Exception:
2482
+ pass
2483
+ except (SaveFailed, NotSignedIn, LinkedInPushedBack):
2484
+ if stop_requested():
2485
+ return _read([], "stopped")
2486
+ raise
2487
+ except Exception:
2488
+ # A stop takes the browser down with it, and closing it can then fail.
2489
+ # If the read itself got to the end, what it found is still saved below.
2490
+ if outcome is None:
2491
+ if stop_requested():
2492
+ return _read([], "stopped")
2493
+ raise
2494
+
2495
+ connections, status, reach = outcome
2496
+
2497
+ if connections:
2498
+ print(f"\nSaving {len(connections)} connections to the app...")
2499
+ save(connections, reach["last"], reach["more"], reach["urn"])
2500
+ elif status == "success" and (start_page > 1 or reach["last"] >= 1):
2501
+ # Nothing unsaved, but where the read ended is still worth noting — for a
2502
+ # read carried on from page 11 that found nothing, it means "finished".
2503
+ record_bridge_progress(profile_url, bridge_name, reach["last"], reach["more"], reach["urn"])
2504
+ elif status == "private" and start_page > 1:
2505
+ # Only reached when their profile clearly rendered without a connections
2506
+ # link; a profile that didn't render is "unclear" and marks nothing.
2507
+ mark_bridge_hidden(profile_url, bridge_name)
2508
+ print(" Their connections are not visible any more — keeping what is mapped.")
2509
+
2510
+ if status == "stopped" or stop_requested():
2511
+ return _read(read, "stopped")
2512
+ if reach["limited"]:
2513
+ set_cooldown(until=next_month_start_pacific(), reason="LinkedIn's monthly search limit")
2514
+ raise SearchLimitReached(len(read))
2515
+ if reach.get("pushed_back"):
2516
+ why = reach.get("pushback_reason") or "a page that would not open"
2517
+ set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {why}")
2518
+ raise LinkedInPushedBack(len(read), why)
2519
+ if reach.get("budget"):
2520
+ raise BudgetReached(len(read), reach["budget"])
2521
+
2522
+ if not read:
2523
+ print(f"\nDone! No connections found ({status}).")
2524
+ return _read([], status)
2525
+
2526
+ left = (f" LinkedIn has more; the next run carries on from page {reach['last'] + 1}."
2527
+ if reach["more"] else " That's all of their list.")
2528
+ print(f"\nDone! {len(read)} of {bridge_name}'s connections read and saved.{left}")
2529
+ return _read(read, status)
2530
+
2531
+
2532
+ def scrape_company(company_name, headless=False, log_fn=None):
2533
+ """Scrape all people at a specific company from LinkedIn search.
2534
+ Returns list of {name, headline, profileUrl, imageUrl}."""
2535
+ from playwright.sync_api import sync_playwright
2536
+
2537
+ def log(msg):
2538
+ print(msg)
2539
+ if log_fn:
2540
+ log_fn(msg)
2541
+
2542
+ log(f"Scraping employees at: {company_name}")
2543
+
2544
+ # A company scan is people searches too: the same cooldown and budget.
2545
+ cd = read_cooldown()
2546
+ if cd:
2547
+ log(cooldown_message(cd))
2548
+ return []
2549
+ if searches_left()[0] <= 0:
2550
+ log(budget_message(searches_left()[1]))
2551
+ return []
2552
+
2553
+ with sync_playwright() as p:
2554
+ browser = p.chromium.launch_persistent_context(
2555
+ user_data_dir=get_scraper_profile_path(),
2556
+ headless=headless,
2557
+ channel="chrome",
2558
+ args=["--disable-blink-features=AutomationControlled"],
2559
+ timeout=120000,
2560
+ )
2561
+
2562
+ page = browser.pages[0] if browser.pages else browser.new_page()
2563
+ page.set_default_timeout(120000)
2564
+ page.set_default_navigation_timeout(120000)
2565
+
2566
+ # Check login
2567
+ try:
2568
+ page.goto("https://www.linkedin.com/", wait_until="domcontentloaded")
2569
+ except:
2570
+ pass
2571
+ if not ensure_logged_in(page):
2572
+ log("Not signed into LinkedIn — sign in and run this again.")
2573
+ browser.close()
2574
+ return []
2575
+
2576
+ # === STEP 1: Find the people search page for this company ===
2577
+ # Strategy: try company page first, fall back to search feed
2578
+ import urllib.parse
2579
+ import re
2580
+ people_url = None
2581
+ company_id = None
2582
+
2583
+ # --- Approach A: Direct company page ---
2584
+ company_slug = company_name.lower().replace(' ', '-').replace('.', '').replace(',', '')
2585
+ company_page_url = f"https://www.linkedin.com/company/{urllib.parse.quote(company_slug)}/"
2586
+ log(f" Trying company page: {company_slug}")
2587
+ try:
2588
+ page.goto(company_page_url, wait_until="commit")
2589
+ except:
2590
+ pass
2591
+ time.sleep(6)
2592
+
2593
+ # Check if the company page loaded (not a 404 or unavailable)
2594
+ page_ok = page.evaluate("""
2595
+ () => {
2596
+ const title = document.title || '';
2597
+ const body = document.body?.innerText || '';
2598
+ if (title.includes('Page not found') || title.includes('404')) return false;
2599
+ if (body.includes('page doesn') || body.includes('not available') || body.includes('doesn\\'t exist')) return false;
2600
+ // Check if we see a company name heading
2601
+ const h1 = document.querySelector('h1');
2602
+ return h1 && h1.textContent.trim().length > 0;
2603
+ }
2604
+ """)
2605
+
2606
+ if page_ok:
2607
+ log(f" Company page loaded ✓")
2608
+ # Extract people URL from company page ("See all X employees" link)
2609
+ people_url = page.evaluate("""
2610
+ () => {
2611
+ const links = document.querySelectorAll('a');
2612
+ for (const link of links) {
2613
+ const h = link.href || '';
2614
+ if (h.includes('currentCompany') && h.includes('/search/results/people/')) {
2615
+ return h;
2616
+ }
2617
+ }
2618
+ // Try extracting numeric company ID from page source
2619
+ const scripts = document.querySelectorAll('script, code');
2620
+ for (const s of scripts) {
2621
+ const text = s.textContent || '';
2622
+ const match = text.match(/"companyId":(\\d+)/);
2623
+ if (match) {
2624
+ return 'https://www.linkedin.com/search/results/people/?currentCompany=%5B%22' + match[1] + '%22%5D&origin=FACETED_SEARCH';
2625
+ }
2626
+ }
2627
+ return null;
2628
+ }
2629
+ """)
2630
+ if people_url:
2631
+ log(f" Got people URL from company page")
2632
+ else:
2633
+ log(f" Company page unavailable, using search fallback...")
2634
+
2635
+ # --- Approach B: Search feed fallback ---
2636
+ if not people_url:
2637
+ log(f" Searching LinkedIn feed for: {company_name}")
2638
+ search_url = f"https://www.linkedin.com/search/results/all/?keywords={urllib.parse.quote(company_name)}"
2639
+ try:
2640
+ charge_linkedin("searches")
2641
+ page.goto(search_url, wait_until="commit")
2642
+ except:
2643
+ pass
2644
+ time.sleep(8)
2645
+
2646
+ # Look for People section links with currentCompany
2647
+ people_url = page.evaluate("""
2648
+ () => {
2649
+ const links = document.querySelectorAll('a');
2650
+ for (const link of links) {
2651
+ const h = link.href || '';
2652
+ if (h.includes('currentCompany') && h.includes('/search/results/people/')) {
2653
+ return h;
2654
+ }
2655
+ }
2656
+ for (const link of links) {
2657
+ const text = (link.textContent || '').trim().toLowerCase();
2658
+ const h = link.href || '';
2659
+ if ((text.includes('see all') && text.includes('people')) && h.includes('/search/results/people/')) {
2660
+ return h;
2661
+ }
2662
+ }
2663
+ return null;
2664
+ }
2665
+ """)
2666
+
2667
+ if not people_url:
2668
+ # Scroll down — People section may be below fold
2669
+ log(f" Scrolling to find People section...")
2670
+ page.evaluate("window.scrollTo(0, document.body.scrollHeight / 2)")
2671
+ time.sleep(3)
2672
+ people_url = page.evaluate("""
2673
+ () => {
2674
+ const links = document.querySelectorAll('a');
2675
+ for (const link of links) {
2676
+ const h = link.href || '';
2677
+ if (h.includes('currentCompany') && h.includes('/search/results/people/')) {
2678
+ return h;
2679
+ }
2680
+ }
2681
+ return null;
2682
+ }
2683
+ """)
2684
+
2685
+ if not people_url:
2686
+ # Try clicking the "People" tab on the search page
2687
+ log(f" Trying People tab...")
2688
+ try:
2689
+ people_btn = page.locator('button:has-text("People")').first
2690
+ if people_btn.is_visible(timeout=3000):
2691
+ charge_linkedin("searches")
2692
+ people_btn.click()
2693
+ time.sleep(5)
2694
+ current = page.url
2695
+ if 'currentCompany' in current:
2696
+ people_url = current
2697
+ except:
2698
+ pass
2699
+
2700
+ if people_url:
2701
+ log(f" Got people URL from search feed")
2702
+
2703
+ if not people_url:
2704
+ log(f"Could not find people for '{company_name}' via company page or search")
2705
+ browser.close()
2706
+ return []
2707
+
2708
+ # === STEP 2: Extract company ID and navigate to people search ===
2709
+ id_match = re.search(r'currentCompany=%5B%22(\d+)%22%5D', people_url)
2710
+ if not id_match:
2711
+ id_match = re.search(r'currentCompany=\[%22(\d+)%22\]', people_url)
2712
+ if id_match:
2713
+ company_id = id_match.group(1)
2714
+ log(f" Company ID: {company_id}")
2715
+
2716
+ # Build a clean base URL — strip any existing network filter
2717
+ # We'll select all degree tabs on the page instead
2718
+ base_url = re.sub(r'[&?]network=[^&]*', '', people_url)
2719
+ # Clean up any double && or trailing ?&
2720
+ base_url = base_url.replace('&&', '&').replace('?&', '?').rstrip('?').rstrip('&')
2721
+
2722
+ log(f" Opening people search...")
2723
+ try:
2724
+ charge_linkedin("searches")
2725
+ page.goto(base_url, wait_until="commit")
2726
+ except:
2727
+ pass
2728
+ time.sleep(8)
2729
+
2730
+ # === STEP 3: Select ALL degree tabs (1st + 2nd + 3rd) ===
2731
+ # LinkedIn shows degree filter buttons at the top of search results.
2732
+ # We need to click each one that isn't already selected.
2733
+ log(f" Selecting all degree filters...")
2734
+
2735
+ # Click each degree button: "1st", "2nd", "3rd+"
2736
+ for degree_label in ['1st', '2nd', '3rd\\+', '3rd']:
2737
+ try:
2738
+ btn = page.locator(f'button:has-text("{degree_label.replace(chr(92), "")}")').first
2739
+ if btn.is_visible(timeout=2000):
2740
+ # Check if already active/selected
2741
+ is_active = btn.evaluate("""
2742
+ el => {
2743
+ const cl = el.className || '';
2744
+ const aria = el.getAttribute('aria-pressed') || el.getAttribute('aria-checked') || '';
2745
+ return cl.includes('active') || cl.includes('selected') || aria === 'true';
2746
+ }
2747
+ """)
2748
+ if not is_active:
2749
+ charge_linkedin("searches")
2750
+ btn.click()
2751
+ log(f" Clicked {degree_label.replace(chr(92), '')} ✓")
2752
+ time.sleep(2)
2753
+ else:
2754
+ log(f" {degree_label.replace(chr(92), '')} already selected ✓")
2755
+ except:
2756
+ pass
2757
+
2758
+ # Give LinkedIn time to reload results with all filters
2759
+ time.sleep(5)
2760
+
2761
+ # Verify the URL now has the network filter — if not, the buttons
2762
+ # might be pills/links instead. Try clicking connection degree links.
2763
+ current_url = page.url
2764
+ if 'network' not in current_url:
2765
+ log(f" Buttons didn't set filter, trying degree links...")
2766
+ # Some LinkedIn layouts use links instead of buttons for degree filters
2767
+ degree_links = page.evaluate("""
2768
+ () => {
2769
+ const results = [];
2770
+ document.querySelectorAll('a, button').forEach(el => {
2771
+ const text = (el.textContent || '').trim();
2772
+ if (/^(1st|2nd|3rd\\+?)$/.test(text)) {
2773
+ results.push(text);
2774
+ el.click();
2775
+ }
2776
+ });
2777
+ return results;
2778
+ }
2779
+ """)
2780
+ if degree_links:
2781
+ charge_linkedin("searches", len(degree_links)) # each click reloads the results
2782
+ log(f" Clicked degree links: {degree_links}")
2783
+ time.sleep(5)
2784
+
2785
+ # If STILL no network filter and we have the company ID, build the URL manually
2786
+ current_url = page.url
2787
+ if 'network' not in current_url and company_id:
2788
+ log(f" Degree buttons not found — using URL with all degrees")
2789
+ all_degrees_url = f"https://www.linkedin.com/search/results/people/?currentCompany=%5B%22{company_id}%22%5D&network=%5B%22F%22%2C%22S%22%2C%22O%22%5D&origin=FACETED_SEARCH"
2790
+ try:
2791
+ charge_linkedin("searches")
2792
+ page.goto(all_degrees_url, wait_until="commit")
2793
+ except:
2794
+ pass
2795
+ time.sleep(8)
2796
+
2797
+ # === STEP 4: Paginate and scrape all results ===
2798
+ all_people = []
2799
+ seen_urls = set()
2800
+ page_num = 1
2801
+
2802
+ while page_num <= 20:
2803
+ pass
2804
+ log(f" Page {page_num}...")
2805
+ time.sleep(3)
2806
+
2807
+ page_data = page.evaluate("""
2808
+ () => {
2809
+ const urlMap = {};
2810
+ const imgMap = {};
2811
+ document.querySelectorAll('img').forEach(img => {
2812
+ if (!img.src || !img.src.includes('media.licdn.com')) return;
2813
+ const link = img.closest('a');
2814
+ if (link) {
2815
+ const h = link.getAttribute('href') || '';
2816
+ if (h.includes('/in/')) {
2817
+ const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
2818
+ imgMap[url] = img.src;
2819
+ }
2820
+ }
2821
+ });
2822
+ document.querySelectorAll('a').forEach(a => {
2823
+ const h = a.getAttribute('href') || '';
2824
+ if (!h.includes('/in/')) return;
2825
+ const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
2826
+ const img = a.querySelector('img');
2827
+ if (img && img.src && img.src.includes('media.licdn.com')) { imgMap[url] = img.src; return; }
2828
+ const text = a.textContent.trim();
2829
+ if (!text || text.length > 60 || text.includes('mutual') || text.includes('Connect')) return;
2830
+ if (!urlMap[text]) urlMap[text] = url;
2831
+ });
2832
+ const main = document.querySelector('[role="main"], main');
2833
+ const pageText = main ? main.innerText : '';
2834
+ const lines = pageText.split('\\n').map(l => l.trim());
2835
+ const results = [];
2836
+ let i = 0;
2837
+ while (i < lines.length) {
2838
+ if (urlMap[lines[i]]) {
2839
+ const name = lines[i], url = urlMap[name];
2840
+ let headline = '';
2841
+ let j = i + 1;
2842
+ while (j < lines.length && j < i + 8) {
2843
+ const l = lines[j].trim();
2844
+ if (!l || l.match(/^\\u2022\\s*(1st|2nd|3rd)/) || l === name) { j++; continue; }
2845
+ if (l === 'Connect' || l === 'Follow' || l === 'Message' || l.includes('mutual connection')) break;
2846
+ if (!headline) { headline = l; j++; continue; }
2847
+ j++;
2848
+ }
2849
+ results.push({ name, headline, profileUrl: url, imageUrl: imgMap[url] || '' });
2850
+ }
2851
+ i++;
2852
+ }
2853
+ return results;
2854
+ }
2855
+ """)
2856
+
2857
+ new_on_page = 0
2858
+ for p in page_data:
2859
+ url = p.get('profileUrl', '')
2860
+ if url and url not in seen_urls:
2861
+ seen_urls.add(url)
2862
+ all_people.append(p)
2863
+ new_on_page += 1
2864
+
2865
+ img_count = sum(1 for p in page_data if p.get('imageUrl'))
2866
+ log(f" {len(page_data)} on page, {new_on_page} new ({img_count} photos) — total: {len(all_people)}")
2867
+
2868
+ if len(page_data) == 0:
2869
+ break
2870
+
2871
+ # Click Next
2872
+ try:
2873
+ next_btn = page.locator('button:has-text("Next")').first
2874
+ if next_btn.is_visible(timeout=3000):
2875
+ if searches_left()[0] <= 0: # before paying for a page, not after
2876
+ log(budget_message(searches_left()[1]))
2877
+ break
2878
+ charge_linkedin("searches")
2879
+ next_btn.click()
2880
+ page_num += 1
2881
+ time.sleep(3)
2882
+ else:
2883
+ break
2884
+ except:
2885
+ break
2886
+
2887
+ log(f"Total: {len(all_people)} people found at {company_name}")
2888
+ browser.close()
2889
+
2890
+ return all_people
2891
+
2892
+
2893
+ def auto_bridge_all(headless=False, log_fn=None, retry_private=False, max_bridges=0, tiers=None,
2894
+ order="newest", max_pages=LINKEDIN_MAX_PAGES, deeper=False, only_unfinished=False):
2895
+ """Map everyone whose circle is not mapped yet, in the order chosen.
2896
+
2897
+ Picks up from where it left off. With deeper, it also finishes people
2898
+ already mapped whose list goes on, from the page their last read stopped at.
2899
+ """
2900
+
2901
+ def log(msg):
2902
+ print(msg)
2903
+ if log_fn:
2904
+ log_fn(msg)
2905
+
2906
+ cd = read_cooldown()
2907
+ if cd:
2908
+ log(cooldown_message(cd))
2909
+ return []
2910
+ left, kind = searches_left()
2911
+ if left <= 0:
2912
+ log(budget_message(kind))
2913
+ return []
2914
+ use, lim = linkedin_usage(), search_limits()
2915
+ log(f"Search budget: {use['searches_today']} of {lim['daily'] or 'no limit'} used today, "
2916
+ f"{use['searches_month']} of {lim['monthly'] or 'no limit'} this month")
2917
+
2918
+ # Get all degree-1 connections (filtered by active user)
2919
+ D1_LIMIT = 20000
2920
+ newest = order != "score"
2921
+ # "added" is the order rows were saved in: each scan saves LinkedIn's list
2922
+ # top to bottom, and that list is "recently added" first.
2923
+ d1_params = {"degree": "eq.1", "select": "id,name,tier,power_score,profile_url,created_at,connected_date",
2924
+ "order": "added" if newest else "power_score.desc", "limit": str(D1_LIMIT)}
2925
+ if _active_user_id:
2926
+ d1_params["user_id"] = f"eq.{_active_user_id}"
2927
+ all_d1 = read_connections(params=d1_params)
2928
+ if len(all_d1) >= D1_LIMIT:
2929
+ log(f"WARNING: only the first {D1_LIMIT} connections were read; some will be missed.")
2930
+
2931
+ # Who already has a mapped circle. Asked as one row per bridge rather than
2932
+ # by pulling every 2nd-degree row and de-duplicating — that carried a limit
2933
+ # of 2000, so past that many the answer was quietly wrong and a resumed run
2934
+ # re-scraped people it had already done.
2935
+ bridged_ids = read_bridged_ids()
2936
+
2937
+ # Someone counts as mapped once any of their list has been read, even if
2938
+ # everyone on it was already one of your connections and nothing was saved —
2939
+ # otherwise they would be read again from page 1 on every run.
2940
+ progress = load_bridge_progress()
2941
+ mapped = set(bridged_ids) | {c["id"] for c in all_d1 if c.get("profile_url") in progress}
2942
+
2943
+ # Filter unbridged, sort by tier priority (S first, then by score)
2944
+ tier_order = {"S": 0, "A": 1, "B": 2, "C": 3, "D": 4}
2945
+ unbridged = [c for c in all_d1 if c["id"] not in mapped]
2946
+
2947
+ # People already found to be private are not tried again. Without this they
2948
+ # reappear every run, in the same place, and the run never gets past them.
2949
+ skips = {} if retry_private else load_bridge_skips()
2950
+ skipped_now = [c for c in unbridged if c.get("profile_url") in skips]
2951
+ unbridged = [c for c in unbridged if c.get("profile_url") not in skips]
2952
+
2953
+ # People already mapped whose list goes on past where it was read.
2954
+ unfinished = []
2955
+ for c in all_d1:
2956
+ if c["id"] not in mapped:
2957
+ continue
2958
+ nxt = next_page_to_read(progress.get(c.get("profile_url")), retry_hidden=retry_private)
2959
+ if nxt is not None and nxt <= max_pages:
2960
+ unfinished.append({**c, "_from_page": nxt})
2961
+ if only_unfinished:
2962
+ # "Resume all": only people whose read was cut short, nobody new.
2963
+ unbridged = list(unfinished)
2964
+ deeper = True
2965
+ elif deeper:
2966
+ unbridged = unbridged + unfinished
2967
+
2968
+ # Choosing tiers rather than "everything" or "only what is new".
2969
+ #
2970
+ # New connections need no special mode: the list of who still needs bridging
2971
+ # is recomputed from the data every run, so anyone added since simply
2972
+ # appears in it, sorted into their tier. What is worth choosing is how far
2973
+ # down the list to go — most people never want to map their D-tier circles,
2974
+ # and that choice is the one that costs rate limit.
2975
+ # Newest-first goes strictly by the date you connected, across every tier —
2976
+ # that is what "my most recent connection" means. Tiers choose who is included
2977
+ # in highest-tier-first order.
2978
+ if tiers and newest:
2979
+ log("Going by the date you connected, so every tier is included")
2980
+ if tiers and not newest:
2981
+ wanted = {t.strip().upper() for t in tiers if t and t.strip()}
2982
+ before = len(unbridged)
2983
+ unbridged = [c for c in unbridged if (c.get("tier") or "D").upper() in wanted]
2984
+ log(f"Limiting to {'/'.join(sorted(wanted, key=lambda t: tier_order.get(t, 9)))}-tier "
2985
+ f"({len(unbridged)} of {before} outstanding)")
2986
+
2987
+ if newest:
2988
+ # Your newest connections first, by LinkedIn's "Connected on" date. People
2989
+ # without one yet (saved before dates were captured; one full scan fills
2990
+ # them in) come after, in the order they were saved — LinkedIn's own
2991
+ # "recently added" order within a scan. sort() is stable, so reverse=True
2992
+ # keeps that order among equal keys.
2993
+ unbridged.sort(key=lambda c: (1 if c.get("connected_date") else 0,
2994
+ c.get("connected_date") or c.get("created_at") or ""),
2995
+ reverse=True)
2996
+ undated = sum(1 for c in unbridged if not c.get("connected_date"))
2997
+ log("Order: newest connections first, by the date you connected")
2998
+ if undated:
2999
+ log(f" {undated} have no connection date yet — run \"Scan my whole network\" once to fill them in")
3000
+ else:
3001
+ unbridged.sort(key=lambda c: (tier_order.get(c.get("tier", "D"), 4), -(float(c.get("power_score", 0)))))
3002
+ log("Order: highest tier first")
3003
+ # Always said, so a read that stops at a page limit is never a mystery.
3004
+ if max_pages >= LINKEDIN_MAX_PAGES:
3005
+ log(f"Reading every page of each person's connections until their list ends "
3006
+ f"(LinkedIn shows {LINKEDIN_MAX_PAGES} pages at most), saving every {SAVE_EVERY_PAGES}")
3007
+ else:
3008
+ log(f"Reading up to {max_pages} pages per person (about {max_pages * 10} of their connections)")
3009
+
3010
+ unclear = load_unclear()
3011
+ parked = [c for c in unbridged if int((unclear.get(c.get("profile_url")) or {}).get("n") or 0) >= UNCLEAR_LIMIT]
3012
+ if parked:
3013
+ parked_ids = {c["id"] for c in parked}
3014
+ unbridged = [c for c in unbridged if c["id"] not in parked_ids] + parked
3015
+ log(f"{len(parked)} came back unclear {UNCLEAR_LIMIT}+ times, so they go to the back of the queue")
3016
+
3017
+ finishing = sum(1 for c in unbridged if c.get("_from_page", 1) > 1)
3018
+ log(f"Found {len(unbridged) - finishing} unbridged connections")
3019
+ log(f"Already bridged: {len(mapped)}")
3020
+ if deeper:
3021
+ log(f"Finishing {finishing} already mapped whose list goes on, from the page each stopped at")
3022
+ elif unfinished:
3023
+ log(f"({len(unfinished)} already mapped have more pages to read. "
3024
+ f"Tick \"Also finish people already mapped\" to include them.)")
3025
+ if skipped_now:
3026
+ log(f"Skipping {len(skipped_now)} whose connections are hidden (--retry-private to try again)")
3027
+
3028
+ if not unbridged:
3029
+ log("Nothing left to bridge.")
3030
+ return []
3031
+
3032
+ # Show plan
3033
+ tier_counts = {}
3034
+ for c in unbridged:
3035
+ t = c.get("tier", "D")
3036
+ tier_counts[t] = tier_counts.get(t, 0) + 1
3037
+ for t in ["S", "A", "B", "C", "D"]:
3038
+ if tier_counts.get(t, 0) > 0:
3039
+ log(f" {t}-tier: {tier_counts[t]} to bridge")
3040
+
3041
+ if max_bridges and max_bridges > 0:
3042
+ unbridged = unbridged[:max_bridges]
3043
+ log(f"Stopping after {len(unbridged)} this run.")
3044
+
3045
+ # Two people in a row whose reads were unclear — a profile that never
3046
+ # rendered, a failure — is how LinkedIn limiting us looks from here, so two
3047
+ # in a row end the batch. Only unclear reads count. A profile that rendered
3048
+ # with no connections link is them hiding their list: that is recorded and
3049
+ # counts as LinkedIn answering normally. Nothing unclear is ever recorded,
3050
+ # so there is nothing to take back (an earlier version recorded, then
3051
+ # un-recorded, and two hidden people side by side stalled every batch).
3052
+ # TRAPS §35.
3053
+ BREAKER = 2
3054
+ streak = 0
3055
+ stopped_early = None # why the batch ended before its list did
3056
+
3057
+ results = []
3058
+ for i, person in enumerate(unbridged):
3059
+ if stop_requested():
3060
+ log("Stopped.")
3061
+ break
3062
+ left, kind = searches_left()
3063
+ if left <= 0:
3064
+ log(budget_message(kind))
3065
+ stopped_early = f"the {kind} search budget"
3066
+ break
3067
+
3068
+ name = person["name"]
3069
+ tier = person.get("tier", "?")
3070
+ score = person.get("power_score", "?")
3071
+ url = person.get("profile_url", "")
3072
+ from_page = person.get("_from_page", 1)
3073
+
3074
+ log(f"[{i+1}/{len(unbridged)}] {name} ({tier}-tier, score {score})"
3075
+ + (f", carrying on from page {from_page}" if from_page > 1 else ""))
3076
+
3077
+ healthy = False # LinkedIn answered normally for this person
3078
+ try:
3079
+ result = scrape_bridge(name, headless=headless, max_pages=max_pages, deeper=from_page > 1,
3080
+ profile_url=url or None)
3081
+ count = len(result) if result else 0
3082
+ status = getattr(result, "status", None)
3083
+ if count > 0:
3084
+ healthy = True
3085
+ results.append({"name": name, "tier": tier, "found": count, "status": "done"})
3086
+ log(f" {count} connections read")
3087
+ elif stop_requested() or status == "stopped":
3088
+ pass # stopped part-way: nothing to conclude about them
3089
+ elif result is None:
3090
+ results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
3091
+ log(" Could not find them in the app to read their list")
3092
+ elif status == "private":
3093
+ # Their profile rendered, with no connections link: a real answer.
3094
+ healthy = True
3095
+ results.append({"name": name, "tier": tier, "found": 0, "status": "private"})
3096
+ if from_page > 1:
3097
+ log(" Their connections are hidden now — keeping what's mapped")
3098
+ elif url:
3099
+ record_bridge_skip(url, name, "no visible connections")
3100
+ log(" Connections are hidden — noted, and skipped from now on")
3101
+ elif status == "empty":
3102
+ healthy = True
3103
+ results.append({"name": name, "tier": tier, "found": 0, "status": "private"})
3104
+ if url:
3105
+ record_bridge_skip(url, name, "LinkedIn showed no one in their list")
3106
+ log(" LinkedIn shows no one in their list — noted, and skipped from now on")
3107
+ elif status == "finished":
3108
+ healthy = True
3109
+ results.append({"name": name, "tier": tier, "found": 0, "status": "finished"})
3110
+ log(f" Nothing past page {from_page - 1}: that was all of their list")
3111
+ elif status == "success" and from_page > 1:
3112
+ entry = load_bridge_progress().get(url) or {}
3113
+ if entry and not entry.get("more"):
3114
+ healthy = True
3115
+ results.append({"name": name, "tier": tier, "found": 0, "status": "finished"})
3116
+ log(f" Nothing past page {from_page - 1}: that was all of their list")
3117
+ else:
3118
+ results.append({"name": name, "tier": tier, "found": 0, "status": "unclear"})
3119
+ log(" Couldn't tell whether their list goes on; the next run checks again")
3120
+ else:
3121
+ # "unclear", or a search that opened but showed nobody: nothing
3122
+ # is recorded, and it counts towards the breaker.
3123
+ results.append({"name": name, "tier": tier, "found": 0, "status": "unclear"})
3124
+ log(" Nothing came back, and it wasn't clear why — not marked hidden; tried again next time")
3125
+ except KeyboardInterrupt:
3126
+ log("Stopped.")
3127
+ raise
3128
+ except BudgetReached as exc:
3129
+ results.append({"name": name, "tier": tier, "found": exc.found, "status": "done" if exc.found else "unclear"})
3130
+ log(" " + budget_message(exc.kind))
3131
+ stopped_early = f"the {exc.kind} search budget"
3132
+ break
3133
+ except CoolingDown as exc:
3134
+ log(" " + str(exc))
3135
+ stopped_early = "a cooldown"
3136
+ break
3137
+ except NotSignedIn:
3138
+ results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
3139
+ log(" LinkedIn isn't signed in, so nothing was read. Stopping the batch: sign in")
3140
+ log(" on the Scan page, then run it again.")
3141
+ stopped_early = "not signed in"
3142
+ break
3143
+ except LinkedInPushedBack as exc:
3144
+ results.append({"name": name, "tier": tier, "found": exc.found, "status": "done" if exc.found else "error"})
3145
+ log(f" LinkedIn pushed back: {exc.reason}.")
3146
+ log(" Stopping the batch rather than carrying on into it. Everything read is saved, and")
3147
+ log(" the next run carries on from the same page.")
3148
+ log(" " + _pushback_advice(exc.reason))
3149
+ stopped_early = "LinkedIn pushed back"
3150
+ break
3151
+ except SearchLimitReached as exc:
3152
+ results.append({"name": name, "tier": tier, "found": exc.found, "status": "done" if exc.found else "error"})
3153
+ log(" LinkedIn says this account has reached its monthly search limit.")
3154
+ log(" Everything read so far is saved. Once the limit resets (the start of next month")
3155
+ log(" for a free account) the next run carries on from the same page.")
3156
+ stopped_early = "the monthly search limit"
3157
+ break
3158
+ except SaveFailed as exc:
3159
+ results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
3160
+ log(f" The app could not save these connections: {str(exc)[:120]}")
3161
+ log(" Stopping the batch: anyone after this would cost LinkedIn views and not be saved either.")
3162
+ stopped_early = "a failed save"
3163
+ break
3164
+ except BaseException as exc:
3165
+ # One bad profile must never end the run. BaseException rather than
3166
+ # Exception on purpose: a SystemExit raised deep in a helper would
3167
+ # otherwise take the whole loop down with it.
3168
+ results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
3169
+ log(f" Failed: {str(exc)[:80]}")
3170
+
3171
+ if stop_requested():
3172
+ continue # the loop's own check says "Stopped." and ends it
3173
+ if url and results and results[-1]["name"] == name:
3174
+ if healthy:
3175
+ note_unclear(url, clear=True)
3176
+ elif results[-1]["status"] == "unclear":
3177
+ note_unclear(url)
3178
+ if healthy:
3179
+ streak = 0
3180
+ else:
3181
+ streak += 1
3182
+ if streak >= BREAKER:
3183
+ log(f" {streak} people in a row with no clear answer (a profile that didn't load, or")
3184
+ log(" a failure), which is how LinkedIn limiting us looks. Stopping the batch;")
3185
+ log(" nobody was marked hidden for it. Leave it at least a day.")
3186
+ stopped_early = "unclear reads in a row"
3187
+ set_cooldown(seconds=6 * 3600, reason="two people in a row with no clear answer")
3188
+ break
3189
+
3190
+ # Cooldown between bridges (skip on the last one). The same full pause
3191
+ # after a person who came back with nothing: it used to be 15 s, which
3192
+ # made the loop go faster exactly when LinkedIn was pushing back.
3193
+ if i < len(unbridged) - 1:
3194
+ cooldown = BRIDGE_COOLDOWN
3195
+ log(f" Waiting {cooldown}s before the next one...")
3196
+ if not interruptible_sleep(cooldown, on_tick=lambda left: log(f" {left}s to go"), step=15):
3197
+ log("Stopped.")
3198
+ break
3199
+
3200
+ # Summary
3201
+ success = sum(1 for r in results if r["status"] == "done")
3202
+ private = sum(1 for r in results if r["status"] == "private")
3203
+ errors = sum(1 for r in results if r["status"] in ("error", "unclear"))
3204
+ finished = sum(1 for r in results if r["status"] == "finished")
3205
+ head = ("Stopped" if stop_requested()
3206
+ else f"Stopped early ({stopped_early})" if stopped_early
3207
+ else "Complete")
3208
+ log(f"{head}: {success} bridged / {private} hidden / {errors} failed or unclear"
3209
+ + (f" / {finished} had nothing more" if finished else ""))
3210
+ if any(r["status"] == "private" for r in results):
3211
+ log("Hidden profiles are remembered and will be skipped next time.")
3212
+
3213
+ return results
3214
+
3215
+
3216
+ def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
3217
+ """Delete all existing cluster data for a bridge and re-scrape from scratch."""
3218
+ print(f"\n=== Re-scraping {bridge_name} (delete + fresh scrape) ===\n")
3219
+
3220
+ # Check before deleting anything: a cooldown or an empty budget would leave
3221
+ # their circle deleted and not read again.
3222
+ cd = read_cooldown()
3223
+ if cd:
3224
+ raise CoolingDown(cd)
3225
+ if searches_left()[0] <= 0:
3226
+ raise BudgetReached(0, searches_left()[1])
3227
+
3228
+ print(f"Deleting old cluster data...")
3229
+ bridge_id = delete_bridge_cluster(bridge_name)
3230
+ if bridge_id is None:
3231
+ return
3232
+
3233
+ scrape_bridge(bridge_name, headless=headless, max_pages=max_pages, fresh=True)
3234
+
3235
+
3236
+ def run_server(port=5555):
3237
+ """
3238
+ Run a local HTTP server that the website's Setup page can talk to.
3239
+ Start once: python3 scripts/scrape.py --server
3240
+ Then use the buttons on the Setup page — no more terminal commands.
3241
+ """
3242
+ from http.server import HTTPServer, BaseHTTPRequestHandler
3243
+ import threading
3244
+
3245
+ # Track current job
3246
+ state = {"running": False, "log": [], "result": None}
3247
+
3248
+ # Only the app served from this machine may drive the scraper. This used to
3249
+ # answer Access-Control-Allow-Origin: * , which let any website the operator
3250
+ # happened to visit start a scrape of their LinkedIn account and read the
3251
+ # results back — including the batched auto-bridge run this file's own
3252
+ # docstring warns gets accounts flagged.
3253
+ ALLOWED_ORIGINS = {
3254
+ "http://localhost:3000", "http://127.0.0.1:3000",
3255
+ "http://localhost:3210", "http://127.0.0.1:3210",
3256
+ }
3257
+
3258
+ class ScrapeHandler(BaseHTTPRequestHandler):
3259
+ def _cors(self):
3260
+ origin = self.headers.get("Origin")
3261
+ if origin in ALLOWED_ORIGINS:
3262
+ self.send_header("Access-Control-Allow-Origin", origin)
3263
+ self.send_header("Vary", "Origin")
3264
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
3265
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
3266
+
3267
+ def _reject_cross_site(self):
3268
+ """True (and already responded) if this request came from another site.
3269
+
3270
+ A cross-origin POST with a simple content type reaches this server
3271
+ with no preflight, so refusing the response is not enough — the
3272
+ scrape would already have started.
3273
+ """
3274
+ origin = self.headers.get("Origin")
3275
+ if origin and origin not in ALLOWED_ORIGINS:
3276
+ self.send_response(403)
3277
+ self.send_header("Content-Type", "application/json")
3278
+ self.end_headers()
3279
+ self.wfile.write(json.dumps({"error": "cross-site request refused"}).encode())
3280
+ return True
3281
+ return False
3282
+
3283
+ def do_OPTIONS(self):
3284
+ self.send_response(200)
3285
+ self._cors()
3286
+ self.end_headers()
3287
+
3288
+ def do_GET(self):
3289
+ if self._reject_cross_site():
3290
+ return
3291
+ if self.path == "/status":
3292
+ self.send_response(200)
3293
+ self._cors()
3294
+ self.send_header("Content-Type", "application/json")
3295
+ self.end_headers()
3296
+ self.wfile.write(json.dumps({
3297
+ "running": state["running"],
3298
+ "log": state["log"][-20:],
3299
+ "result": state["result"],
3300
+ }).encode())
3301
+ return
3302
+
3303
+ if self.path == "/ping":
3304
+ self.send_response(200)
3305
+ self._cors()
3306
+ self.send_header("Content-Type", "application/json")
3307
+ self.end_headers()
3308
+ self.wfile.write(b'{"ok":true}')
3309
+ return
3310
+
3311
+ self.send_response(404)
3312
+ self.end_headers()
3313
+
3314
+ def do_POST(self):
3315
+ if self._reject_cross_site():
3316
+ return
3317
+ content_len = int(self.headers.get("Content-Length", 0))
3318
+ body = json.loads(self.rfile.read(content_len)) if content_len else {}
3319
+
3320
+ if self.path == "/scrape":
3321
+ if state["running"]:
3322
+ self.send_response(409)
3323
+ self._cors()
3324
+ self.send_header("Content-Type", "application/json")
3325
+ self.end_headers()
3326
+ self.wfile.write(b'{"error":"Scrape already running"}')
3327
+ return
3328
+
3329
+ action = body.get("action", "connections")
3330
+ bridge_name = body.get("bridge", "")
3331
+ user_id = body.get("userId", "")
3332
+
3333
+ # Set active user for this scrape session
3334
+ global _active_user_id
3335
+ _active_user_id = user_id or None
3336
+
3337
+ state["running"] = True
3338
+ state["log"] = [f"Starting {action}" + (f": {bridge_name}" if bridge_name else "")]
3339
+ state["result"] = None
3340
+
3341
+ def run_scrape():
3342
+ global _stop_requested
3343
+ _stop_requested = False # a window closed in an earlier job stops only that job
3344
+ try:
3345
+ if action == "full-scrape":
3346
+ state["log"].append("Full account setup — walking your whole connections list...")
3347
+ result = scrape_connections(
3348
+ full_walk=True,
3349
+ log_fn=lambda msg: state["log"].append(msg))
3350
+ count = len(result) if result else 0
3351
+ state["result"] = {"status": "done", "action": "full-scrape", "found": count}
3352
+ state["log"].append(f"Done! {count} connections with images captured")
3353
+ elif action == "company":
3354
+ company = body.get("company", "")
3355
+ state["log"].append(f"Scanning company: {company}")
3356
+ people = scrape_company(company, log_fn=lambda msg: state["log"].append(msg))
3357
+ # Push scraped people to database so they persist
3358
+ if people:
3359
+ state["log"].append(f"Saving {len(people)} people to database...")
3360
+ push_company(people, company)
3361
+ state["log"].append(f"Saved to database ✓")
3362
+ state["result"] = {"status": "done", "action": "company", "found": len(people), "people": people}
3363
+ state["log"].append(f"Done! {len(people)} people found at {company}")
3364
+ elif action == "auto-bridge":
3365
+ state["log"].append("Auto-bridging all connections (S-tier first)...")
3366
+ state["log"].append("2 min cooldown between each bridge")
3367
+ results = auto_bridge_all(log_fn=lambda msg: state["log"].append(msg))
3368
+ success = sum(1 for r in results if r.get("status") == "done")
3369
+ state["result"] = {"status": "done", "action": "auto-bridge", "found": success, "total": len(results)}
3370
+ state["log"].append(f"Done! {success}/{len(results)} bridges mapped")
3371
+ elif action == "bridge" and bridge_name:
3372
+ state["log"].append(f"Scraping bridge: {bridge_name}")
3373
+ result = scrape_bridge(bridge_name)
3374
+ count = len(result) if result else 0
3375
+ state["result"] = {"status": "done", "action": "bridge", "bridge": bridge_name, "found": count}
3376
+ state["log"].append(f"Done! {count} connections found")
3377
+ elif action == "rescrape" and bridge_name:
3378
+ state["log"].append(f"Re-scraping: {bridge_name}")
3379
+ rescrape_bridge(bridge_name)
3380
+ state["result"] = {"status": "done", "action": "rescrape", "bridge": bridge_name}
3381
+ state["log"].append("Done! Re-scrape complete")
3382
+ else:
3383
+ # Refresh connections only — no auto-bridge
3384
+ state["log"].append("Checking for new connections...")
3385
+ result = scrape_connections(
3386
+ log_fn=lambda msg: state["log"].append(msg))
3387
+ new_count = len(result) if result else 0
3388
+ state["log"].append(f" {new_count} connections scanned")
3389
+ state["result"] = {"status": "done", "action": "refresh", "found": new_count}
3390
+ state["log"].append("Refresh complete!")
3391
+ except Exception as e:
3392
+ state["result"] = {"status": "error", "error": str(e)}
3393
+ state["log"].append(f"Error: {str(e)[:100]}")
3394
+ finally:
3395
+ state["running"] = False
3396
+
3397
+ threading.Thread(target=run_scrape, daemon=True).start()
3398
+
3399
+ self.send_response(200)
3400
+ self._cors()
3401
+ self.send_header("Content-Type", "application/json")
3402
+ self.end_headers()
3403
+ self.wfile.write(b'{"started":true}')
3404
+ return
3405
+
3406
+ self.send_response(404)
3407
+ self.end_headers()
3408
+
3409
+ def log_message(self, format, *args):
3410
+ pass # Suppress default HTTP logs
3411
+
3412
+ import socket
3413
+ # Kill any existing server on this port
3414
+ try:
3415
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3416
+ s.bind(("127.0.0.1", port))
3417
+ s.close()
3418
+ except OSError:
3419
+ print(f" Port {port} in use — killing old server...")
3420
+ import subprocess
3421
+ subprocess.run(f"lsof -ti:{port} | xargs kill -9", shell=True, capture_output=True)
3422
+ time.sleep(1)
3423
+
3424
+ server = HTTPServer(("127.0.0.1", port), ScrapeHandler)
3425
+ print(f"\n{'='*50}")
3426
+ print(f" 6 Degrees Scraper Server")
3427
+ print(f" Running on http://localhost:{port}")
3428
+ print(f"{'='*50}")
3429
+ print(f"\n Open your visualization and use the Setup page buttons.")
3430
+ print(f" Press Ctrl+C to stop.\n")
3431
+
3432
+ try:
3433
+ server.serve_forever()
3434
+ except KeyboardInterrupt:
3435
+ print("\nServer stopped.")
3436
+ server.server_close()
3437
+
3438
+
3439
+ if __name__ == "__main__":
3440
+ parser = argparse.ArgumentParser(
3441
+ description="6 Degrees LinkedIn Scraper",
3442
+ formatter_class=argparse.RawDescriptionHelpFormatter,
3443
+ epilog="""
3444
+ Examples:
3445
+ python3 scripts/scrape.py --server # Start local server (use Setup page buttons)
3446
+ python3 scripts/scrape.py # Scrape your degree-1 connections
3447
+ python3 scripts/scrape.py --login # Sign in once, then exit
3448
+ python3 scripts/scrape.py --full # Walk the whole connections list
3449
+ python3 scripts/scrape.py --refresh # Only what is new since last run
3450
+ python3 scripts/scrape.py --bridge "Jane Doe" # Scrape one bridge's connections
3451
+ python3 scripts/scrape.py --rescrape "Name" # Delete + re-scrape a bridge
3452
+ """,
3453
+ )
3454
+ parser.add_argument("--full", action="store_true",
3455
+ help="Walk your whole connections list top to bottom (first-time scrape)")
3456
+ parser.add_argument("--refresh", action="store_true",
3457
+ help="Only look for connections added since the last run")
3458
+ parser.add_argument("--search", action="store_true",
3459
+ help="Legacy: collect via the people-search pages instead of the connections page")
3460
+ parser.add_argument("--login", action="store_true",
3461
+ help="Just sign into LinkedIn and save the session, then exit")
3462
+ parser.add_argument("--server", action="store_true", help="Start local scraper server (use website buttons)")
3463
+ parser.add_argument("--bridge", type=str, help="Name of one bridge person to scrape")
3464
+ parser.add_argument("--rescrape", type=str, help="Delete + re-scrape a bridge's cluster from scratch")
3465
+ parser.add_argument("--company", type=str, help="Scan everyone the app can see at one company")
3466
+ parser.add_argument("--auto-bridge", action="store_true",
3467
+ help="Map every bridge in turn, highest tier first")
3468
+ parser.add_argument("--retry-private", action="store_true",
3469
+ help="With --auto-bridge: try people previously found to be hidden")
3470
+ parser.add_argument("--clear-skips", action="store_true",
3471
+ help="Forget every hidden-profile skip and start clean")
3472
+ parser.add_argument("--max-bridges", type=int, default=0,
3473
+ help="With --auto-bridge: stop after this many people (0 = no limit)")
3474
+ parser.add_argument("--tiers", type=str, default="",
3475
+ help="With --auto-bridge: only these tiers, e.g. --tiers=S,A")
3476
+ parser.add_argument("--order", choices=("newest", "score"), default="newest",
3477
+ help="With --auto-bridge: newest connections first (default), or highest tier first")
3478
+ parser.add_argument("--max-pages", type=int, default=LINKEDIN_MAX_PAGES,
3479
+ help="Result pages to read per person. By default every page, until their "
3480
+ "list ends (LinkedIn shows 100 at most). Every page is a search on your account.")
3481
+ parser.add_argument("--deeper", action="store_true",
3482
+ help="Carry on with people already mapped, from the page their last read "
3483
+ "stopped at. With --auto-bridge: alongside new people. With --bridge: that person.")
3484
+ parser.add_argument("--bridge-url", type=str,
3485
+ help="Carry on with one person, found by their LinkedIn profile URL (implies --deeper)")
3486
+ parser.add_argument("--only-unfinished", action="store_true",
3487
+ help="With --auto-bridge: only people whose read was cut short (Resume all)")
3488
+ parser.add_argument("--headless", action="store_true", help="Run browser in headless mode")
3489
+ args = parser.parse_args()
3490
+ args.max_pages = max(1, min(LINKEDIN_MAX_PAGES, args.max_pages))
3491
+
3492
+ # A failed save ends the run with its reason, not a traceback (TRAPS §32).
3493
+ def _say_why(exc_type, exc, tb):
3494
+ if issubclass(exc_type, SaveFailed):
3495
+ print(f"\n The app could not save what was scraped: {exc}\n", file=sys.stderr)
3496
+ elif issubclass(exc_type, NotSignedIn):
3497
+ print("\n LinkedIn isn't signed in, so nothing was read. Sign in with --login, then run it again.\n",
3498
+ file=sys.stderr)
3499
+ elif issubclass(exc_type, (BudgetReached, CoolingDown)):
3500
+ msg = budget_message(exc.kind) if issubclass(exc_type, BudgetReached) else str(exc)
3501
+ print(f"\n {msg}\n", file=sys.stderr)
3502
+ elif issubclass(exc_type, LinkedInPushedBack):
3503
+ print(f"\n LinkedIn pushed back: {getattr(exc, 'reason', exc)}. What was read is saved. "
3504
+ f"{_pushback_advice(getattr(exc, 'reason', ''))}\n", file=sys.stderr)
3505
+ elif issubclass(exc_type, SearchLimitReached):
3506
+ print("\n LinkedIn says this account has reached its monthly search limit. What was read "
3507
+ "is saved; run again with --deeper once it resets.\n", file=sys.stderr)
3508
+ else:
3509
+ sys.__excepthook__(exc_type, exc, tb)
3510
+ sys.excepthook = _say_why
3511
+
3512
+ install_stop_handler()
3513
+ _assert_local_target()
3514
+
3515
+ if args.clear_skips:
3516
+ clear_bridge_skips()
3517
+ raise SystemExit(0)
3518
+
3519
+ if args.login:
3520
+ raise SystemExit(0 if open_login_window() else 1)
3521
+
3522
+ if not args.server:
3523
+ resolve_active_user()
3524
+
3525
+ # A budget or a cooldown ending a run is the design working, not a failure:
3526
+ # say why and exit 0, so the Scan page doesn't show it as a red error.
3527
+ try:
3528
+ if args.company:
3529
+ # The server mode scraped and pushed as one step; the CLI has to do the
3530
+ # same or the scan appears to work and saves nothing.
3531
+ people = scrape_company(args.company, headless=args.headless)
3532
+ if people:
3533
+ print(f"\nSaving {len(people)} people from {args.company}...")
3534
+ push_company(people, args.company)
3535
+ print(f"Done. {len(people) if people else 0} found at {args.company}.")
3536
+ elif args.auto_bridge:
3537
+ results = auto_bridge_all(headless=args.headless, retry_private=args.retry_private,
3538
+ max_bridges=args.max_bridges,
3539
+ tiers=args.tiers.split(",") if args.tiers else None,
3540
+ order=args.order, max_pages=args.max_pages, deeper=args.deeper,
3541
+ only_unfinished=args.only_unfinished)
3542
+ done = sum(1 for r in results if r.get("status") == "done")
3543
+ print(f"\nDone. {done}/{len(results)} bridges mapped.")
3544
+ elif args.search:
3545
+ scrape_full(headless=args.headless)
3546
+ elif args.full:
3547
+ scrape_connections(headless=args.headless, full_walk=True)
3548
+ elif args.refresh:
3549
+ scrape_connections(headless=args.headless)
3550
+ elif args.server:
3551
+ run_server()
3552
+ elif args.rescrape:
3553
+ rescrape_bridge(args.rescrape, headless=args.headless, max_pages=args.max_pages)
3554
+ elif args.bridge_url:
3555
+ scrape_bridge(None, headless=args.headless, max_pages=args.max_pages, deeper=True,
3556
+ profile_url=args.bridge_url)
3557
+ elif args.bridge:
3558
+ scrape_bridge(args.bridge, headless=args.headless, max_pages=args.max_pages, deeper=args.deeper)
3559
+ else:
3560
+ # Nothing collected yet means this is a first run, and the full scrape
3561
+ # is the one that walks every search page and captures photos. Running
3562
+ # the incremental refresh here is what made a fresh install look broken.
3563
+ existing = read_connections(params={"degree": "eq.1", "limit": "1"})
3564
+ if existing:
3565
+ print("\nExisting connections found — checking for new ones only.")
3566
+ print("Use --full to re-walk everything.\n")
3567
+ scrape_connections(headless=args.headless)
3568
+ else:
3569
+ print("\nNo connections yet — walking your whole connections list.")
3570
+ print("This takes a couple of minutes and captures photos.\n")
3571
+ scrape_connections(headless=args.headless, full_walk=True)
3572
+ except (BudgetReached, CoolingDown) as exc:
3573
+ print("\n " + (budget_message(exc.kind) if isinstance(exc, BudgetReached) else str(exc)))
3574
+ raise SystemExit(0)