six-degrees 0.2.1 → 0.3.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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +10 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +39 -11
- package/.next/standalone/.next/required-server-files.json +4 -1
- package/.next/standalone/.next/routes-manifest.json +60 -0
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +3 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +3 -2
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +3 -2
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +3 -2
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +9 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/company-scores/route.js +3 -2
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +3 -2
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/export/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/export/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/export/route.js +7 -0
- package/.next/standalone/.next/server/app/api/data/export/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/export/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/import/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/import/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/import/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/import/route.js +8 -0
- package/.next/standalone/.next/server/app/api/data/import/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/import/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/restart/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/restart/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/restart/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/restart/route.js +8 -0
- package/.next/standalone/.next/server/app/api/data/restart/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/restart/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route.js +7 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/route.js +7 -0
- package/.next/standalone/.next/server/app/api/data/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +3 -2
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +2 -2
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +3 -2
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +3 -2
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +3 -2
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +3 -2
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/settings/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/settings/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/settings/route.js +9 -0
- package/.next/standalone/.next/server/app/api/settings/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/settings/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +8 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +8 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +3 -2
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +3 -2
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +3 -2
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +3 -2
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js +2 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js +2 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +12 -12
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +12 -12
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +12 -12
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +12 -12
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +12 -12
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/settings/page.js +13 -0
- package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/settings.html +1 -0
- package/.next/standalone/.next/server/app/settings.meta +26 -0
- package/.next/standalone/.next/server/app/settings.rsc +24 -0
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +12 -12
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app-paths-manifest.json +10 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0i5hdu4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +3 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +21 -3
- package/.next/standalone/.next/server/chunks/_1fn30_l._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +2 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_company-scores_legacy_route_actions_1ncspu_.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_export_route_actions_1lkimty.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_import_route_actions_1eji0kd.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_restart_route_actions_00_-u6o.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_reveal_route_actions_0-moqtn.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_route_actions_0j0w5ep.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_route_actions_1bpm9ac.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_sector-preview_route_actions_0w79454.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_sector-suggestions_route_actions_1eoqugp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +155 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_0mc73lk.js → lib_rpc_00tx2sm.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_02-qa4d.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_02ndsg2.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_08q5vp6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -2
- package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +158 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0mocm6d._.js → [root-of-the-server]__0an6blh._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bipr5s._.js → [root-of-the-server]__1z1j3ek._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0ggabjc._.js → _0942v8n._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1djpk8t._.js → _0hl_t7r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +132 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_11x4dq5.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1f8t_rp._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1nkgb45._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_01xbj_9._.js → node_modules_1wax83z._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_00nap8h._.js → node_modules_next_dist_1bunoni._.js} +2 -2
- package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0pco7a6._.js +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +15 -7
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/prefetch-hints.json +14 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +1 -0
- package/.next/standalone/.next/static/chunks/{34at5d2ifl62i.js → 0ngx9s6-zgshe.js} +5 -5
- package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +2 -0
- package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +1 -0
- package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +44 -0
- package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +1 -0
- package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +1 -0
- package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +1 -0
- package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +1 -0
- package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +16 -0
- package/.next/standalone/.next/static/chunks/2jz311xl1pp_8.js +1 -0
- package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +2 -0
- package/.next/standalone/.next/static/qqqcxvzBUizkECrT4swyK/_clientMiddlewareManifest.js +14 -0
- package/.next/standalone/CHANGELOG.md +298 -0
- package/.next/standalone/CONTRIBUTING.md +5 -2
- package/.next/standalone/README.md +138 -30
- package/.next/standalone/SECURITY.md +117 -10
- package/.next/standalone/app/api/company-scores/legacy/route.js +32 -0
- package/.next/standalone/app/api/company-scores/route.js +20 -11
- package/.next/standalone/app/api/data/export/route.js +49 -0
- package/.next/standalone/app/api/data/import/route.js +83 -0
- package/.next/standalone/app/api/data/restart/route.js +37 -0
- package/.next/standalone/app/api/data/reveal/route.js +14 -0
- package/.next/standalone/app/api/data/route.js +39 -0
- package/.next/standalone/app/api/ingest/route.js +15 -47
- package/.next/standalone/app/api/scraper/route.js +264 -125
- package/.next/standalone/app/api/settings/route.js +58 -0
- package/.next/standalone/app/api/settings/sector-preview/route.js +38 -0
- package/.next/standalone/app/api/settings/sector-suggestions/route.js +20 -0
- package/.next/standalone/app/api/update/route.js +188 -23
- package/.next/standalone/app/avatars/[file]/route.js +4 -2
- package/.next/standalone/app/components/CompanyScores.js +28 -7
- package/.next/standalone/app/components/EmptyState.js +8 -0
- package/.next/standalone/app/components/ForceGraph.js +39 -35
- package/.next/standalone/app/components/LegacyScoresCard.js +129 -0
- package/.next/standalone/app/components/Sidebar.js +11 -10
- package/.next/standalone/app/components/UpdatePanel.js +306 -119
- package/.next/standalone/app/components/UserProvider.js +4 -3
- package/.next/standalone/app/components/settings/DataSection.js +437 -0
- package/.next/standalone/app/components/settings/SectorSection.js +445 -0
- package/.next/standalone/app/components/ui.js +62 -0
- package/.next/standalone/app/launch/page.js +30 -20
- package/.next/standalone/app/page.js +13 -0
- package/.next/standalone/app/paths/page.js +12 -4
- package/.next/standalone/app/profile/page.js +41 -8
- package/.next/standalone/app/queue/page.js +8 -2
- package/.next/standalone/app/settings/page.js +96 -0
- package/.next/standalone/app/setup/page.js +32 -16
- package/.next/standalone/bin/args.mjs +44 -0
- package/.next/standalone/bin/six-degrees.mjs +9 -15
- package/.next/standalone/lib/companies.js +44 -119
- package/.next/standalone/lib/csv.js +8 -0
- package/.next/standalone/lib/data-export.js +170 -0
- package/.next/standalone/lib/data-folder.js +238 -0
- package/.next/standalone/lib/data-import.js +878 -0
- package/.next/standalone/lib/db-client.js +48 -3
- package/.next/standalone/lib/durable.js +73 -0
- package/.next/standalone/lib/gate.js +90 -1
- package/.next/standalone/lib/ingest.js +40 -0
- package/.next/standalone/lib/legacy-offer.js +185 -0
- package/.next/standalone/lib/legacy-scores.js +77 -0
- package/.next/standalone/lib/linkedin-limits.js +124 -1
- package/.next/standalone/lib/rpc.js +121 -13
- package/.next/standalone/lib/sample-stats.js +16 -0
- package/.next/standalone/lib/scan-progress.js +13 -0
- package/.next/standalone/lib/scan-state.js +53 -0
- package/.next/standalone/lib/scanner-python.js +685 -0
- package/.next/standalone/lib/scanner-setup.js +104 -0
- package/.next/standalone/lib/scoring.js +747 -138
- package/.next/standalone/lib/scraper-client.js +9 -5
- package/.next/standalone/lib/sector-directory.js +1532 -0
- package/.next/standalone/lib/sector-focus.js +163 -0
- package/.next/standalone/lib/sector-labels.js +81 -0
- package/.next/standalone/lib/settings-effects.js +59 -0
- package/.next/standalone/lib/settings.js +83 -0
- package/.next/standalone/lib/updater-job.js +838 -0
- package/.next/standalone/lib/updater.js +494 -0
- package/.next/standalone/middleware.js +25 -28
- package/.next/standalone/next.config.mjs +19 -3
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -2
- package/.next/standalone/scripts/apply-update.sh +482 -0
- package/.next/standalone/scripts/build-app.mjs +251 -9
- package/.next/standalone/scripts/requirements.txt +106 -4
- package/.next/standalone/scripts/test-release-server.mjs +143 -0
- package/.next/standalone/server.js +1 -1
- package/.next/static/chunks/06x3jiiz96m5t.js +1 -0
- package/.next/static/chunks/{34at5d2ifl62i.js → 0ngx9s6-zgshe.js} +5 -5
- package/.next/static/chunks/0q-_khndfxubv.js +2 -0
- package/.next/static/chunks/1c79veqedvubt.js +1 -0
- package/.next/static/chunks/1chp3nx45meg5.js +44 -0
- package/.next/static/chunks/1irqj0lf-fu3d.js +1 -0
- package/.next/static/chunks/1uadqylrod5_v.js +1 -0
- package/.next/static/chunks/23u0zclgwpc4d.js +1 -0
- package/.next/static/chunks/289ktj97di6zg.js +1 -0
- package/.next/static/chunks/2b3lmf8e6uj8m.js +16 -0
- package/.next/static/chunks/2jz311xl1pp_8.js +1 -0
- package/.next/static/chunks/3npq5yd7c4dmn.js +2 -0
- package/.next/static/qqqcxvzBUizkECrT4swyK/_clientMiddlewareManifest.js +14 -0
- package/CHANGELOG.md +298 -0
- package/README.md +138 -30
- package/SECURITY.md +117 -10
- package/bin/args.mjs +44 -0
- package/bin/six-degrees.mjs +9 -15
- package/lib/companies.js +44 -119
- package/lib/csv.js +8 -0
- package/lib/data-export.js +170 -0
- package/lib/data-folder.js +238 -0
- package/lib/data-import.js +878 -0
- package/lib/db-client.js +48 -3
- package/lib/durable.js +73 -0
- package/lib/gate.js +90 -1
- package/lib/ingest.js +40 -0
- package/lib/legacy-offer.js +185 -0
- package/lib/legacy-scores.js +77 -0
- package/lib/linkedin-limits.js +124 -1
- package/lib/rpc.js +121 -13
- package/lib/sample-stats.js +16 -0
- package/lib/scan-progress.js +13 -0
- package/lib/scan-state.js +53 -0
- package/lib/scanner-python.js +685 -0
- package/lib/scanner-setup.js +104 -0
- package/lib/scoring.js +747 -138
- package/lib/scraper-client.js +9 -5
- package/lib/sector-directory.js +1532 -0
- package/lib/sector-focus.js +163 -0
- package/lib/sector-labels.js +81 -0
- package/lib/settings-effects.js +59 -0
- package/lib/settings.js +83 -0
- package/lib/updater-job.js +838 -0
- package/lib/updater.js +494 -0
- package/package.json +1 -2
- package/scripts/requirements.txt +106 -4
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +0 -150
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +0 -151
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +0 -150
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +0 -155
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zor0ys._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0_rp-01._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0hzdk79._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0ow7u5r._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t0zc2d._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0wa631d._.js +0 -3
- package/.next/standalone/.next/server/chunks/_13ccnao._.js +0 -3
- package/.next/standalone/.next/server/chunks/_13es063._.js +0 -3
- package/.next/standalone/.next/server/chunks/_142872j._.js +0 -3
- package/.next/standalone/.next/server/chunks/_18wsb_a._.js +0 -8
- package/.next/standalone/.next/server/chunks/_1e-4h5h._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1gvvd7n._.js +0 -158
- package/.next/standalone/.next/server/chunks/_1moda66._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1p6ghzv._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1xmmi_v._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1z-yrlf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +0 -4
- package/.next/standalone/.next/static/ZGTiDrZ5NHGETgGdlhssu/_clientMiddlewareManifest.js +0 -6
- package/.next/standalone/.next/static/chunks/0jk64vo17avsb.js +0 -1
- package/.next/standalone/.next/static/chunks/0s65dsrthylo6.js +0 -2
- package/.next/standalone/.next/static/chunks/0uyfdhi8dek85.js +0 -16
- package/.next/standalone/.next/static/chunks/220-ne82aus0c.js +0 -44
- package/.next/standalone/.next/static/chunks/229haorf0f19b.js +0 -2
- package/.next/standalone/.next/static/chunks/2g54uafnwuakc.js +0 -1
- package/.next/standalone/.next/static/chunks/2spwqs6b6dx13.js +0 -1
- package/.next/standalone/.next/static/chunks/38dlepuubiox0.js +0 -1
- package/.next/standalone/scripts/score-connections.sql +0 -80
- package/.next/standalone/scripts/score_new_connections.sql +0 -77
- package/.next/standalone/site/app.js +0 -84
- package/.next/standalone/site/index.html +0 -221
- package/.next/standalone/site/style.css +0 -155
- package/.next/standalone/site/video/install-mac-poster.jpg +0 -0
- package/.next/standalone/site/video/install-mac.mp4 +0 -0
- package/.next/standalone/site/video/linux-poster.jpg +0 -0
- package/.next/standalone/site/video/linux.mp4 +0 -0
- package/.next/standalone/site/video/scan-poster.jpg +0 -0
- package/.next/standalone/site/video/scan.mp4 +0 -0
- package/.next/static/ZGTiDrZ5NHGETgGdlhssu/_clientMiddlewareManifest.js +0 -6
- package/.next/static/chunks/0jk64vo17avsb.js +0 -1
- package/.next/static/chunks/0s65dsrthylo6.js +0 -2
- package/.next/static/chunks/0uyfdhi8dek85.js +0 -16
- package/.next/static/chunks/220-ne82aus0c.js +0 -44
- package/.next/static/chunks/229haorf0f19b.js +0 -2
- package/.next/static/chunks/2g54uafnwuakc.js +0 -1
- package/.next/static/chunks/2spwqs6b6dx13.js +0 -1
- package/.next/static/chunks/38dlepuubiox0.js +0 -1
- package/scripts/score_new_connections.sql +0 -77
- /package/.next/standalone/.next/static/{ZGTiDrZ5NHGETgGdlhssu → qqqcxvzBUizkECrT4swyK}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZGTiDrZ5NHGETgGdlhssu → qqqcxvzBUizkECrT4swyK}/_ssgManifest.js +0 -0
- /package/.next/static/{ZGTiDrZ5NHGETgGdlhssu → qqqcxvzBUizkECrT4swyK}/_buildManifest.js +0 -0
- /package/.next/static/{ZGTiDrZ5NHGETgGdlhssu → qqqcxvzBUizkECrT4swyK}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
// One-click updates for the Mac app: the work. The decisions are in
|
|
2
|
+
// lib/updater.js, and the swap after the app has quit is scripts/apply-update.sh.
|
|
3
|
+
//
|
|
4
|
+
// A click on "Install and restart" (Settings → Updates) starts one background
|
|
5
|
+
// job, which the page polls:
|
|
6
|
+
//
|
|
7
|
+
// checking read releases/latest again. The version is never taken from
|
|
8
|
+
// the page, and a pre-release is never installed.
|
|
9
|
+
// downloading SHA256SUMS, then the disk image for this Mac's chip
|
|
10
|
+
// verifying the image's SHA-256 against SHA256SUMS
|
|
11
|
+
// preparing mount it read-only and check the app inside: its code signature,
|
|
12
|
+
// bundle id, version, macOS minimum and chip, and that no link
|
|
13
|
+
// points outside it. Copy it beside the running app, check the
|
|
14
|
+
// copy the same way, and unmount.
|
|
15
|
+
// restarting start the helper, then end the server so the app quits
|
|
16
|
+
//
|
|
17
|
+
// Everything before "restarting" happens while the old version keeps running.
|
|
18
|
+
// If any of it fails, or the user cancels, the app stays exactly as it was.
|
|
19
|
+
//
|
|
20
|
+
// The system tools are called by absolute path, never through PATH, and never
|
|
21
|
+
// the developer-tools stubs (lipo, git) that ask to install Xcode's tools on a
|
|
22
|
+
// Mac that doesn't have them.
|
|
23
|
+
|
|
24
|
+
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
25
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import fsp from 'node:fs/promises';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import {
|
|
31
|
+
APP_BUNDLE_NAME, BUNDLE_ID, CHIP_LABEL, GITHUB, UPDATE_HANDOFF_EXIT_CODE,
|
|
32
|
+
chipOf, digestsMatch, foreignAppProcesses, helperArgs, leftoversBeside, machoArchs, parseSha256Sums,
|
|
33
|
+
planFromRelease, relaunchArgs, stagingPath, versionAtLeast,
|
|
34
|
+
} from './updater.js';
|
|
35
|
+
import { runningNow } from './scan-state.js';
|
|
36
|
+
|
|
37
|
+
const TOOL = {
|
|
38
|
+
hdiutil: '/usr/bin/hdiutil',
|
|
39
|
+
codesign: '/usr/bin/codesign',
|
|
40
|
+
ditto: '/usr/bin/ditto',
|
|
41
|
+
plutil: '/usr/bin/plutil',
|
|
42
|
+
xattr: '/usr/bin/xattr',
|
|
43
|
+
du: '/usr/bin/du',
|
|
44
|
+
swVers: '/usr/bin/sw_vers',
|
|
45
|
+
sysctl: '/usr/sbin/sysctl',
|
|
46
|
+
ps: '/bin/ps',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const USER_AGENT = 'six-degrees-update';
|
|
50
|
+
const MB = 1024 * 1024;
|
|
51
|
+
const SPACE_MARGIN = 64 * MB;
|
|
52
|
+
const ACTIVE = new Set(['checking', 'downloading', 'verifying', 'preparing', 'restarting']);
|
|
53
|
+
const CANCELLABLE = new Set(['checking', 'downloading', 'verifying', 'preparing']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A failure with a message fit to show on the Settings page as it is, and a
|
|
57
|
+
* code when the page does something particular about it ('stale-check': offer
|
|
58
|
+
* a new check rather than "Try again").
|
|
59
|
+
*/
|
|
60
|
+
export class UpdateError extends Error {
|
|
61
|
+
constructor(message, code = null) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.code = code;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const CANCELLED = 'cancelled';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Where the updater keeps things, none of them in the data folder (DESKTOP.md
|
|
71
|
+
* rule 3) and none in the log the app empties at every start:
|
|
72
|
+
* cacheDir ~/Library/Caches/Six Degrees: the previous version, zipped (one copy)
|
|
73
|
+
* statusFile …/last-update.json: how the last update went, for the next start
|
|
74
|
+
* confirmFile …/update-confirmed.json: the new version saying it has started,
|
|
75
|
+
* which the helper waits for before it lets go of the old one
|
|
76
|
+
* logFile $TMPDIR/six-degrees-update.log: the helper's own log
|
|
77
|
+
*/
|
|
78
|
+
export function updatePaths({ home = os.homedir(), tmp = os.tmpdir() } = {}) {
|
|
79
|
+
const cacheDir = path.join(home, 'Library', 'Caches', 'Six Degrees');
|
|
80
|
+
return {
|
|
81
|
+
cacheDir,
|
|
82
|
+
statusFile: path.join(cacheDir, 'last-update.json'),
|
|
83
|
+
confirmFile: path.join(cacheDir, 'update-confirmed.json'),
|
|
84
|
+
logFile: path.join(tmp, 'six-degrees-update.log'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The status file, or null when there is none or it can't be read. */
|
|
89
|
+
export function readStatusFile(file) {
|
|
90
|
+
try {
|
|
91
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
92
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeStatusFile(file, value) {
|
|
99
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
100
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
101
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value)}\n`);
|
|
102
|
+
fs.renameSync(tmp, file);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Say, for a helper that is waiting on it, that this version has started
|
|
107
|
+
* (scripts/apply-update.sh keeps the previous version, whole, until it hears
|
|
108
|
+
* this, and puts it back if the new one closes first). Called from GET
|
|
109
|
+
* /api/update, which every page makes (StaleServerBanner), so the first page
|
|
110
|
+
* this version shows is the proof. Only while the status file says an update
|
|
111
|
+
* to exactly this version was handed over and hasn't finished. Returns whether
|
|
112
|
+
* it wrote.
|
|
113
|
+
*/
|
|
114
|
+
export function confirmStarted({ statusFile, confirmFile }, version) {
|
|
115
|
+
const status = readStatusFile(statusFile);
|
|
116
|
+
if (!status || status.outcome !== 'started' || status.to !== version) return false;
|
|
117
|
+
if (readStatusFile(confirmFile)?.version === version) return false;
|
|
118
|
+
try {
|
|
119
|
+
writeStatusFile(confirmFile, { version, pid: process.pid, at: new Date().toISOString() });
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
return false; // the helper then keeps waiting, and keeps the new version: nothing is lost
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** This Mac's chip and macOS version, asked of the system (lib/updater.js chipOf). */
|
|
127
|
+
export function macFacts() {
|
|
128
|
+
const ask = (file, args) => {
|
|
129
|
+
try { return execFileSync(file, args, { timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); }
|
|
130
|
+
catch { return ''; }
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
chip: chipOf({ sysctlArm64: ask(TOOL.sysctl, ['-n', 'hw.optional.arm64']), processArch: process.arch }),
|
|
134
|
+
macosVersion: ask(TOOL.swVers, ['-productVersion']),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** What lib/updater.js bundleRefusal needs to know about the running bundle. */
|
|
139
|
+
export function bundleFacts(bundle) {
|
|
140
|
+
if (!bundle) return { exists: false };
|
|
141
|
+
try {
|
|
142
|
+
const st = fs.statSync(bundle);
|
|
143
|
+
let parentWritable = true;
|
|
144
|
+
try { fs.accessSync(path.dirname(bundle), fs.constants.W_OK); } catch { parentWritable = false; }
|
|
145
|
+
return {
|
|
146
|
+
exists: st.isDirectory(),
|
|
147
|
+
parentWritable,
|
|
148
|
+
ownerUid: st.uid,
|
|
149
|
+
uid: typeof process.getuid === 'function' ? process.getuid() : undefined,
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return { exists: false };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── leftovers ────────────────────────────────────────────────────────────────
|
|
157
|
+
// An update cut off half-way (the server or the helper killed, the Mac shut
|
|
158
|
+
// down) can leave things behind that nothing will ever remove. The next check
|
|
159
|
+
// or install removes them: only this app's own names, only once nothing can
|
|
160
|
+
// still be using them, and never the previous version a failed update
|
|
161
|
+
// couldn't put back, which is the user's to rename.
|
|
162
|
+
|
|
163
|
+
const HOUR = 3600000;
|
|
164
|
+
|
|
165
|
+
function pidAlive(pid) {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return err.code === 'EPERM'; // someone else's: alive
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Remove what interrupted updates left: beside the app, the old version moved
|
|
176
|
+
* aside and a new version taken out again, once their helper has ended and ten
|
|
177
|
+
* minutes have passed, and a staged copy after an hour; in the cache, the
|
|
178
|
+
* helper's half-made zips; in TMPDIR, a job's download folder after an hour
|
|
179
|
+
* (unmounting its image first). Returns what it removed. The caller makes sure
|
|
180
|
+
* no update is running in this server.
|
|
181
|
+
*/
|
|
182
|
+
export function cleanLeftovers({
|
|
183
|
+
bundle, cacheDir, tmpRoot, statusFile, now = Date.now(), isAlive = pidAlive, asideAgeMs = 10 * 60000,
|
|
184
|
+
} = {}) {
|
|
185
|
+
const removed = [];
|
|
186
|
+
const age = (st) => now - Math.max(st.mtimeMs, st.ctimeMs);
|
|
187
|
+
const stat = (p) => { try { return fs.lstatSync(p); } catch { return null; } };
|
|
188
|
+
const list = (dir) => { try { return fs.readdirSync(dir); } catch { return []; } };
|
|
189
|
+
const remove = (p) => {
|
|
190
|
+
try {
|
|
191
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
192
|
+
removed.push(p);
|
|
193
|
+
} catch { /* next time */ }
|
|
194
|
+
};
|
|
195
|
+
const status = statusFile ? readStatusFile(statusFile) : null;
|
|
196
|
+
const theOnlyCopy = status?.outcome === 'failed' && typeof status.previous === 'string' ? path.resolve(status.previous) : null;
|
|
197
|
+
|
|
198
|
+
if (bundle) {
|
|
199
|
+
const dir = path.dirname(bundle);
|
|
200
|
+
for (const { name, kind, pid } of leftoversBeside(list(dir), path.basename(bundle))) {
|
|
201
|
+
const p = path.join(dir, name);
|
|
202
|
+
const st = stat(p);
|
|
203
|
+
if (!st || p === theOnlyCopy) continue;
|
|
204
|
+
if (kind === 'incoming') {
|
|
205
|
+
if (age(st) > HOUR) remove(p);
|
|
206
|
+
} else if (!isAlive(pid) && age(st) > asideAgeMs) {
|
|
207
|
+
remove(p);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (cacheDir) {
|
|
212
|
+
for (const name of list(cacheDir)) {
|
|
213
|
+
if (!/^\.previous-[A-Za-z0-9]{6}$/.test(name) && !/\.zip\.part$/.test(name)
|
|
214
|
+
&& !/^(last-update|update-confirmed)\.json\.\d+\.tmp$/.test(name)) continue;
|
|
215
|
+
const p = path.join(cacheDir, name);
|
|
216
|
+
const st = stat(p);
|
|
217
|
+
if (st && age(st) > HOUR) remove(p);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (tmpRoot) {
|
|
221
|
+
for (const name of list(tmpRoot)) {
|
|
222
|
+
if (!/^six-degrees-update-[A-Za-z0-9]{6}$/.test(name)) continue;
|
|
223
|
+
const p = path.join(tmpRoot, name);
|
|
224
|
+
const st = stat(p);
|
|
225
|
+
if (!st || !st.isDirectory() || age(st) <= HOUR) continue;
|
|
226
|
+
const point = path.join(p, 'mnt');
|
|
227
|
+
if (mounted(point)) {
|
|
228
|
+
// A system tool on a mount point found at run time: nothing for the build to trace.
|
|
229
|
+
try { execFileSync(/*turbopackIgnore: true*/ TOOL.hdiutil, ['detach', point, '-force'], { stdio: 'ignore', timeout: 30000 }); } catch { /* checked below */ }
|
|
230
|
+
if (mounted(point)) continue; // never remove into a mounted image
|
|
231
|
+
}
|
|
232
|
+
remove(p);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return removed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── the job ──────────────────────────────────────────────────────────────────
|
|
239
|
+
// One per server process, kept on globalThis so that every route bundle (and a
|
|
240
|
+
// reloaded module in development) sees the same one.
|
|
241
|
+
|
|
242
|
+
const STORE = Symbol.for('six-degrees.update-job');
|
|
243
|
+
function store() {
|
|
244
|
+
if (!globalThis[STORE]) globalThis[STORE] = { job: null };
|
|
245
|
+
return globalThis[STORE];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** What the page sees of a job. */
|
|
249
|
+
function view(job) {
|
|
250
|
+
if (!job) return null;
|
|
251
|
+
const { id, phase, version, notes, received, total, error, code, startedAt, endedAt } = job;
|
|
252
|
+
return { id, phase, version, notes, received, total, error, code, startedAt, endedAt };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The current (or last) job, for update-status and the Settings page. */
|
|
256
|
+
export function currentUpdate() {
|
|
257
|
+
return view(store().job);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Is a job running (up to and including "restarting")? */
|
|
261
|
+
export function updateActive() {
|
|
262
|
+
const job = store().job;
|
|
263
|
+
return Boolean(job && ACTIVE.has(job.phase));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The version the last "Check for updates" offered, on this server. Install
|
|
268
|
+
* installs exactly that one: if a newer release has come out since, it stops
|
|
269
|
+
* and asks for a new check rather than install something nobody was shown.
|
|
270
|
+
* Kept here, never taken from the page.
|
|
271
|
+
*/
|
|
272
|
+
export function rememberCheck(version) {
|
|
273
|
+
store().checked = version ? String(version) : null;
|
|
274
|
+
}
|
|
275
|
+
export function checkedVersion() {
|
|
276
|
+
return store().checked || null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Start installing the newest release. Returns { job } at once; the work runs
|
|
281
|
+
* in the background. `done` settles when the job stops (tests wait for it).
|
|
282
|
+
* Refused with { error } while another job is running.
|
|
283
|
+
*/
|
|
284
|
+
export function startUpdate(ctx) {
|
|
285
|
+
const s = store();
|
|
286
|
+
if (s.job && ACTIVE.has(s.job.phase)) {
|
|
287
|
+
return { error: 'An update is already in progress.', job: view(s.job) };
|
|
288
|
+
}
|
|
289
|
+
const job = {
|
|
290
|
+
id: randomUUID(),
|
|
291
|
+
phase: 'checking',
|
|
292
|
+
version: null,
|
|
293
|
+
notes: null,
|
|
294
|
+
received: 0,
|
|
295
|
+
total: null,
|
|
296
|
+
error: null,
|
|
297
|
+
code: null,
|
|
298
|
+
startedAt: new Date().toISOString(),
|
|
299
|
+
endedAt: null,
|
|
300
|
+
ctrl: new AbortController(),
|
|
301
|
+
};
|
|
302
|
+
s.job = job;
|
|
303
|
+
const done = runUpdate(job, ctx);
|
|
304
|
+
return { job: view(job), done };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Stop a job that hasn't reached "restarting". Nothing is left behind. */
|
|
308
|
+
export function cancelUpdate() {
|
|
309
|
+
const job = store().job;
|
|
310
|
+
if (job && CANCELLABLE.has(job.phase)) job.ctrl.abort(new UpdateError(CANCELLED));
|
|
311
|
+
return view(job);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Forget the job and the last check (tests only). */
|
|
315
|
+
export function _resetUpdateForTesting() {
|
|
316
|
+
store().job = null;
|
|
317
|
+
store().checked = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function run(file, args, { signal, timeoutMs = 120000 } = {}) {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
execFile(file, args, { signal, timeout: timeoutMs, maxBuffer: 8 * MB }, (err, stdout, stderr) => {
|
|
323
|
+
if (err) {
|
|
324
|
+
const said = String(stderr || '').trim().split('\n').filter(Boolean).slice(-2).join(' ');
|
|
325
|
+
err.detail = said || err.message;
|
|
326
|
+
reject(err);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
resolve(String(stdout));
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const mb = (bytes) => Math.max(1, Math.round(bytes / MB));
|
|
335
|
+
|
|
336
|
+
function ensureSpace(dir, needed, where) {
|
|
337
|
+
let free;
|
|
338
|
+
try {
|
|
339
|
+
const s = fs.statfsSync(dir);
|
|
340
|
+
free = s.bavail * s.bsize;
|
|
341
|
+
} catch {
|
|
342
|
+
return; // can't tell; the copy itself will fail plainly if the disk is full
|
|
343
|
+
}
|
|
344
|
+
if (free < needed) {
|
|
345
|
+
throw new UpdateError(`There isn't enough free space ${where}: the update needs about ${mb(needed)} MB, and ${mb(free)} MB is free.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function fetchWithin(url, c, signal, ms, accept) {
|
|
350
|
+
return c.fetchImpl(url, {
|
|
351
|
+
headers: { Accept: accept, 'User-Agent': USER_AGENT },
|
|
352
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(ms)]),
|
|
353
|
+
cache: 'no-store',
|
|
354
|
+
redirect: 'follow',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Download to `file`, hashing as it arrives. It gives up when nothing has
|
|
360
|
+
* arrived for `idleMs`, not after a fixed total: the image is about 220 MB, and
|
|
361
|
+
* a slow connection that is still moving is fine.
|
|
362
|
+
*/
|
|
363
|
+
async function download(url, file, { job, c, signal, expectedSize }) {
|
|
364
|
+
const idle = new AbortController();
|
|
365
|
+
const both = AbortSignal.any([signal, idle.signal]);
|
|
366
|
+
let timer = null;
|
|
367
|
+
const arm = () => {
|
|
368
|
+
clearTimeout(timer);
|
|
369
|
+
timer = setTimeout(() => idle.abort(new UpdateError(
|
|
370
|
+
`The download stopped for ${Math.round(c.idleMs / 1000)} seconds. Check your connection and try again.`,
|
|
371
|
+
)), c.idleMs);
|
|
372
|
+
};
|
|
373
|
+
arm();
|
|
374
|
+
const fh = await fsp.open(file, 'w');
|
|
375
|
+
const hash = createHash('sha256');
|
|
376
|
+
let received = 0;
|
|
377
|
+
try {
|
|
378
|
+
const res = await c.fetchImpl(url, {
|
|
379
|
+
headers: { Accept: 'application/octet-stream', 'User-Agent': USER_AGENT },
|
|
380
|
+
signal: both,
|
|
381
|
+
cache: 'no-store',
|
|
382
|
+
redirect: 'follow',
|
|
383
|
+
});
|
|
384
|
+
if (!res.ok || !res.body) throw new UpdateError(`GitHub answered ${res.status} to the download. Try again later.`);
|
|
385
|
+
const header = Number(res.headers.get('content-length')) || null;
|
|
386
|
+
if (expectedSize && header && header !== expectedSize) {
|
|
387
|
+
throw new UpdateError('The download is not the size GitHub listed for it, so it was stopped.');
|
|
388
|
+
}
|
|
389
|
+
const total = header || expectedSize || null;
|
|
390
|
+
job.total = total;
|
|
391
|
+
const limit = total || c.maxBytes;
|
|
392
|
+
for await (const chunk of res.body) {
|
|
393
|
+
arm();
|
|
394
|
+
received += chunk.byteLength;
|
|
395
|
+
if (received > limit) throw new UpdateError('The download is bigger than GitHub said it would be, so it was stopped.');
|
|
396
|
+
hash.update(chunk);
|
|
397
|
+
let offset = 0;
|
|
398
|
+
while (offset < chunk.byteLength) {
|
|
399
|
+
const { bytesWritten } = await fh.write(chunk, offset, chunk.byteLength - offset);
|
|
400
|
+
offset += bytesWritten;
|
|
401
|
+
}
|
|
402
|
+
job.received = received;
|
|
403
|
+
}
|
|
404
|
+
if (total && received !== total) throw new UpdateError('The download ended early. Try again.');
|
|
405
|
+
return hash.digest('hex');
|
|
406
|
+
} catch (err) {
|
|
407
|
+
// Say why it stopped (cancelled, or nothing arriving), not "aborted".
|
|
408
|
+
if (both.aborted && both.reason instanceof Error) throw both.reason;
|
|
409
|
+
throw err;
|
|
410
|
+
} finally {
|
|
411
|
+
clearTimeout(timer);
|
|
412
|
+
await fh.close();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Mount read-only, out of sight, at `mount.point`; notes the device for detach(). */
|
|
417
|
+
async function attach(image, mount, signal) {
|
|
418
|
+
await fsp.mkdir(mount.point, { recursive: true });
|
|
419
|
+
let out;
|
|
420
|
+
try {
|
|
421
|
+
out = await run(TOOL.hdiutil, ['attach', image, '-nobrowse', '-readonly', '-noautoopen', '-mountpoint', mount.point, '-plist'],
|
|
422
|
+
{ signal, timeoutMs: 180000 });
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (signal.aborted) throw err;
|
|
425
|
+
throw new UpdateError(`The disk image wouldn't open (${err.detail}).`);
|
|
426
|
+
}
|
|
427
|
+
const devices = [...out.matchAll(/<key>dev-entry<\/key>\s*<string>([^<]+)<\/string>/g)].map((m) => m[1]);
|
|
428
|
+
mount.device = devices.find((d) => /^\/dev\/disk\d+$/.test(d)) || null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function mounted(point) {
|
|
432
|
+
try {
|
|
433
|
+
return fs.statSync(point).dev !== fs.statSync(path.dirname(point)).dev;
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Unmount, patiently. Right after use, Spotlight or Finder can hold a volume
|
|
441
|
+
* for a moment and hdiutil says "Resource busy"; and once the volume is gone,
|
|
442
|
+
* only the disk device may be left to eject (TRAPS §28, build-app.mjs detach).
|
|
443
|
+
* Safe to call when nothing was mounted: an attach that was cancelled half-way
|
|
444
|
+
* may or may not have mounted the image, so this looks rather than assumes.
|
|
445
|
+
*/
|
|
446
|
+
async function detach({ point, device }, sleep) {
|
|
447
|
+
const tries = [[], [], ['-force'], ['-force'], ['-force']];
|
|
448
|
+
let last = null;
|
|
449
|
+
for (let i = 0; i < tries.length; i++) {
|
|
450
|
+
const volume = mounted(point);
|
|
451
|
+
if (!volume && (!device || !fs.existsSync(device))) return;
|
|
452
|
+
try {
|
|
453
|
+
await run(TOOL.hdiutil, ['detach', volume ? point : device, ...tries[i]], { timeoutMs: 60000 });
|
|
454
|
+
return;
|
|
455
|
+
} catch (err) {
|
|
456
|
+
last = err;
|
|
457
|
+
await sleep(1000 * (i + 1));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!mounted(point) && (!device || !fs.existsSync(device))) return;
|
|
461
|
+
throw last;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** The first link inside `root` that points outside it, or null (TRAPS §37). */
|
|
465
|
+
async function linkOutside(root) {
|
|
466
|
+
const entries = await fsp.readdir(root, { recursive: true, withFileTypes: true });
|
|
467
|
+
for (const e of entries) {
|
|
468
|
+
if (!e.isSymbolicLink()) continue;
|
|
469
|
+
const at = path.join(e.parentPath ?? e.path, e.name);
|
|
470
|
+
const to = await fsp.readlink(at);
|
|
471
|
+
const resolved = path.resolve(path.dirname(at), to);
|
|
472
|
+
if (path.isAbsolute(to) || (resolved !== root && !resolved.startsWith(root + path.sep))) {
|
|
473
|
+
return `${path.relative(root, at)} → ${to}`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function readHead(file, bytes = 4096) {
|
|
480
|
+
const fh = await fsp.open(file, 'r');
|
|
481
|
+
try {
|
|
482
|
+
const buf = Buffer.alloc(bytes);
|
|
483
|
+
const { bytesRead } = await fh.read(buf, 0, bytes, 0);
|
|
484
|
+
return buf.subarray(0, bytesRead);
|
|
485
|
+
} finally {
|
|
486
|
+
await fh.close();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Is this the app it should be? Its signature verifies strictly (what CI
|
|
492
|
+
* checks, release.yml), it is Six Degrees at exactly the release's version, it
|
|
493
|
+
* runs on this macOS and this chip, and every link stays inside it.
|
|
494
|
+
*/
|
|
495
|
+
async function checkApp(app, { plan, c, signal, where }) {
|
|
496
|
+
const st = await fsp.lstat(app).catch(() => null);
|
|
497
|
+
if (!st || !st.isDirectory()) throw new UpdateError(`The disk image doesn't contain ${APP_BUNDLE_NAME}.`);
|
|
498
|
+
try {
|
|
499
|
+
await run(TOOL.codesign, ['--verify', '--deep', '--strict', app], { signal, timeoutMs: 300000 });
|
|
500
|
+
} catch (err) {
|
|
501
|
+
if (signal.aborted) throw err;
|
|
502
|
+
throw new UpdateError(`The new version's signature doesn't check out ${where} (${err.detail}), so it wasn't used.`);
|
|
503
|
+
}
|
|
504
|
+
const plist = path.join(app, 'Contents', 'Info.plist');
|
|
505
|
+
const read = (key) => run(TOOL.plutil, ['-extract', key, 'raw', '-o', '-', plist], { signal, timeoutMs: 15000 })
|
|
506
|
+
.then((s) => s.trim()).catch(() => '');
|
|
507
|
+
const [id, version, minimum, executable] = await Promise.all([
|
|
508
|
+
read('CFBundleIdentifier'), read('CFBundleShortVersionString'), read('LSMinimumSystemVersion'), read('CFBundleExecutable'),
|
|
509
|
+
]);
|
|
510
|
+
if (id !== c.bundleId) {
|
|
511
|
+
throw new UpdateError(`The download isn't Six Degrees (it calls itself ${id || 'nothing'}), so it wasn't used.`);
|
|
512
|
+
}
|
|
513
|
+
if (version !== plan.version) {
|
|
514
|
+
throw new UpdateError(`The download says it is version ${version || '(none)'}, not ${plan.version}, so it wasn't used.`);
|
|
515
|
+
}
|
|
516
|
+
if (minimum && c.macosVersion && !versionAtLeast(c.macosVersion, minimum)) {
|
|
517
|
+
throw new UpdateError(`Version ${plan.version} needs macOS ${minimum} or later, and this Mac has ${c.macosVersion}.`);
|
|
518
|
+
}
|
|
519
|
+
// The chip, from the executable itself. The classic app's executable is a
|
|
520
|
+
// bash script, so for it the bundled Node binary is the one that tells.
|
|
521
|
+
const candidates = [
|
|
522
|
+
executable && !executable.includes('/') ? path.join(app, 'Contents', 'MacOS', executable) : null,
|
|
523
|
+
path.join(app, 'Contents', 'Resources', 'node'),
|
|
524
|
+
].filter(Boolean);
|
|
525
|
+
for (const file of candidates) {
|
|
526
|
+
const archs = machoArchs(await readHead(file).catch(() => null));
|
|
527
|
+
if (!archs.size) continue;
|
|
528
|
+
if (!archs.has(c.chip)) {
|
|
529
|
+
throw new UpdateError(`The download is built for ${[...archs].map((a) => CHIP_LABEL[a] || a).join(' and ')} Macs, not this one, so it wasn't used.`);
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
const escaping = await linkOutside(app);
|
|
534
|
+
if (escaping) throw new UpdateError(`The new version has a link that points outside it (${escaping}), so it wasn't used.`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The helper's whole environment: nothing of this process's (test switches
|
|
539
|
+
* included) but the system's own tools, the user, and a UTF-8 locale. Without
|
|
540
|
+
* the locale, ps and lsof write a path that isn't plain ASCII as escapes, and
|
|
541
|
+
* the helper can't find the app's processes in, say, "Programmes Été" (TRAPS
|
|
542
|
+
* §40). tests/apply-update.test.mjs runs the helper with exactly this.
|
|
543
|
+
*/
|
|
544
|
+
export function helperEnv() {
|
|
545
|
+
return {
|
|
546
|
+
PATH: '/usr/bin:/bin:/usr/sbin:/sbin',
|
|
547
|
+
HOME: os.homedir(),
|
|
548
|
+
TMPDIR: os.tmpdir(),
|
|
549
|
+
LANG: 'en_US.UTF-8',
|
|
550
|
+
LC_ALL: 'en_US.UTF-8',
|
|
551
|
+
...(process.env.USER ? { USER: process.env.USER, LOGNAME: process.env.USER } : {}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Other users' processes running the app at `bundle` (lib/updater.js
|
|
557
|
+
* foreignAppProcesses). An app someone else has open can't be stopped by this
|
|
558
|
+
* user, so it is never replaced from under them.
|
|
559
|
+
*/
|
|
560
|
+
export function othersRunningApp(bundle) {
|
|
561
|
+
if (!bundle || typeof process.getuid !== 'function') return [];
|
|
562
|
+
let out = '';
|
|
563
|
+
try {
|
|
564
|
+
out = execFileSync(TOOL.ps, ['-axo', 'uid=,pid=,comm='], {
|
|
565
|
+
timeout: 10000, maxBuffer: 16 * MB, stdio: ['ignore', 'pipe', 'ignore'], env: helperEnv(),
|
|
566
|
+
}).toString();
|
|
567
|
+
} catch {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
let real = bundle;
|
|
571
|
+
try { real = fs.realpathSync(bundle); } catch { /* the path as given */ }
|
|
572
|
+
return foreignAppProcesses(out, { bundles: [bundle, real], uid: process.getuid() });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Start the helper on its own, outside the app: its own session (so it
|
|
577
|
+
* outlives the app), its working folder in TMPDIR (so it is never inside the
|
|
578
|
+
* bundle it replaces), a clean environment (helperEnv), and its output in the
|
|
579
|
+
* update log. Resolves with the running child once it has started.
|
|
580
|
+
*/
|
|
581
|
+
export function launchHelper({ helper, args, cwd, logFile }) {
|
|
582
|
+
const env = helperEnv();
|
|
583
|
+
const out = fs.openSync(logFile, 'w');
|
|
584
|
+
try {
|
|
585
|
+
const child = spawn('/bin/bash', [helper, ...args], { cwd, env, detached: true, stdio: ['ignore', out, out] });
|
|
586
|
+
return new Promise((resolve, reject) => {
|
|
587
|
+
child.once('error', reject);
|
|
588
|
+
child.once('spawn', () => { child.unref(); resolve(child); });
|
|
589
|
+
});
|
|
590
|
+
} finally {
|
|
591
|
+
fs.closeSync(out);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* End this server with the code the Mac app reads as "quit quietly"
|
|
597
|
+
* (desktop/lib.mjs serverExitAction). Not a SIGTERM to itself: Next catches
|
|
598
|
+
* that and exits with 143, which says "stopped from outside" (TRAPS §39).
|
|
599
|
+
* The pause lets the page's next poll see "restarting". The helper is already
|
|
600
|
+
* running, and waits for the app to be gone before it touches anything.
|
|
601
|
+
*/
|
|
602
|
+
function exitForUpdate() {
|
|
603
|
+
setTimeout(() => process.exit(UPDATE_HANDOFF_EXIT_CODE), 1200);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Next wraps the global fetch to cache what pages fetch. This download is no
|
|
607
|
+
// page's business, outlives the click that started it, and is about 220 MB, so use
|
|
608
|
+
// the fetch underneath when Next says which one that is.
|
|
609
|
+
const plainFetch = (...args) => (globalThis.fetch?._nextOriginalFetch || globalThis.fetch)(...args);
|
|
610
|
+
|
|
611
|
+
const DEFAULTS = {
|
|
612
|
+
bundleId: BUNDLE_ID,
|
|
613
|
+
// GitHub, unless the route passes lib/updater.js releaseSource()'s loopback
|
|
614
|
+
// test address. Tests point both at a server on 127.0.0.1 the same way.
|
|
615
|
+
apiBase: GITHUB.apiBase,
|
|
616
|
+
downloadBase: GITHUB.downloadBase,
|
|
617
|
+
idleMs: 30000,
|
|
618
|
+
maxBytes: 1024 * MB,
|
|
619
|
+
waitSeconds: 30,
|
|
620
|
+
// How long the helper waits for the new version to say it has started before
|
|
621
|
+
// it lets go of the old one anyway (it only waits that long if the new
|
|
622
|
+
// version keeps running without a page loading).
|
|
623
|
+
confirmWaitSeconds: 600,
|
|
624
|
+
// How long the helper must stay up before the app quits for it. It spends
|
|
625
|
+
// its first seconds waiting for the app to go, so one that has already ended
|
|
626
|
+
// was given something it refused, and the app stays open to say so.
|
|
627
|
+
settleMs: 400,
|
|
628
|
+
// What the scanner's route is running (lib/scan-state.js scannerJob), or null.
|
|
629
|
+
scanRunning: () => null,
|
|
630
|
+
othersRunning: () => [],
|
|
631
|
+
launch: launchHelper,
|
|
632
|
+
exit: exitForUpdate,
|
|
633
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Another user of this Mac has the app open: this user can't stop their copy,
|
|
637
|
+
// and swapping the app from under it would break it. Asked before the download
|
|
638
|
+
// and again just before the hand-over.
|
|
639
|
+
function othersCheck(c) {
|
|
640
|
+
if (c.othersRunning().length) {
|
|
641
|
+
throw new UpdateError('Another user of this Mac has Six Degrees open, so it can\'t be replaced now. Try again once they have quit it.');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function plain(err) {
|
|
646
|
+
if (err instanceof UpdateError) return err.message;
|
|
647
|
+
if (err?.name === 'TimeoutError') return "GitHub didn't answer in time. Try again in a moment.";
|
|
648
|
+
if (err instanceof TypeError && /fetch failed/i.test(err.message)) {
|
|
649
|
+
return `Could not reach GitHub (${err.cause?.code || err.cause?.message || 'no connection'}). Check your connection and try again.`;
|
|
650
|
+
}
|
|
651
|
+
return `The update stopped: ${err?.detail || err?.message || err}.`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** The last few lines the helper wrote, to say why it stopped. */
|
|
655
|
+
function logTail(file) {
|
|
656
|
+
try {
|
|
657
|
+
return fs.readFileSync(file, 'utf8').trim().split('\n').slice(-2).join(' ').replace(/^\S+ \S+ {2}/, '');
|
|
658
|
+
} catch {
|
|
659
|
+
return '';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* The job itself. `c` (the context) comes from the route, which works out
|
|
665
|
+
* every value on the server:
|
|
666
|
+
* slug, currentVersion, chip, macosVersion this copy and this Mac
|
|
667
|
+
* bundle the running app (lib/updater.js runningBundle)
|
|
668
|
+
* dataDir to reopen it on the same data
|
|
669
|
+
* helper scripts/apply-update.sh inside the running app
|
|
670
|
+
* tmpRoot, cacheDir, statusFile, confirmFile, lib/updater-job.js updatePaths
|
|
671
|
+
* logFile
|
|
672
|
+
* apiBase, downloadBase lib/updater.js releaseSource
|
|
673
|
+
* pids the app's processes the helper waits for
|
|
674
|
+
* expectVersion the version the last check offered
|
|
675
|
+
* othersRunning other users' processes running the app
|
|
676
|
+
* fetchImpl, scanRunning, launch, exit, … see DEFAULTS
|
|
677
|
+
*/
|
|
678
|
+
async function runUpdate(job, ctx) {
|
|
679
|
+
const c = { fetchImpl: plainFetch, ...DEFAULTS, ...ctx };
|
|
680
|
+
const { signal } = job.ctrl;
|
|
681
|
+
const set = (patch) => Object.assign(job, patch);
|
|
682
|
+
let work = null;
|
|
683
|
+
let mount = null;
|
|
684
|
+
let staged = null;
|
|
685
|
+
let handedOver = false;
|
|
686
|
+
// Quitting the app mid-update ends this process mid-job (Next exits on the
|
|
687
|
+
// SIGTERM the app sends it). Leave nothing behind then either: not a mounted
|
|
688
|
+
// image, and not a half-copied app hidden beside the real one. Synchronous,
|
|
689
|
+
// because nothing asynchronous runs once a process is exiting.
|
|
690
|
+
const onExit = () => {
|
|
691
|
+
if (handedOver) return;
|
|
692
|
+
if (mount && mounted(mount.point)) {
|
|
693
|
+
// A system tool on a mount point made at run time: nothing for the build to trace.
|
|
694
|
+
try { execFileSync(/*turbopackIgnore: true*/ TOOL.hdiutil, ['detach', mount.point, '-force'], { stdio: 'ignore', timeout: 15000 }); } catch { /* best effort */ }
|
|
695
|
+
}
|
|
696
|
+
for (const dir of [staged, work]) {
|
|
697
|
+
if (dir) try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
process.once('exit', onExit);
|
|
701
|
+
try {
|
|
702
|
+
// ── checking ──
|
|
703
|
+
const res = await fetchWithin(`${c.apiBase}/repos/${c.slug}/releases/latest`, c, signal, 15000, 'application/vnd.github+json');
|
|
704
|
+
if (res.status === 404) throw new UpdateError('No release has been published yet.');
|
|
705
|
+
if (!res.ok) throw new UpdateError(`GitHub answered ${res.status}. Try again in a moment.`);
|
|
706
|
+
const { plan, refusal } = planFromRelease(await res.json(), {
|
|
707
|
+
currentVersion: c.currentVersion, chip: c.chip, slug: c.slug, downloadBase: c.downloadBase,
|
|
708
|
+
});
|
|
709
|
+
if (refusal) throw new UpdateError(refusal.message);
|
|
710
|
+
// Exactly the version the page offered, or nothing: a release newer than
|
|
711
|
+
// that is something nobody has seen yet, not what they clicked to install.
|
|
712
|
+
if (c.expectVersion && plan.version !== c.expectVersion) {
|
|
713
|
+
throw new UpdateError(
|
|
714
|
+
`Version ${plan.version} has come out since you checked, and you were offered ${c.expectVersion}. Check again, then install.`,
|
|
715
|
+
'stale-check',
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
set({ version: plan.version, notes: plan.notes, total: plan.dmg.size });
|
|
719
|
+
othersCheck(c);
|
|
720
|
+
|
|
721
|
+
work = await fsp.mkdtemp(path.join(c.tmpRoot, 'six-degrees-update-'));
|
|
722
|
+
// GitHub always lists the size; the fallback is well above any release (about
|
|
723
|
+
// 220 MB with the scanner's Python inside), so a missing one never lets a
|
|
724
|
+
// download start on a disk that can't hold it.
|
|
725
|
+
ensureSpace(c.tmpRoot, (plan.dmg.size || 512 * MB) + SPACE_MARGIN, 'for the download');
|
|
726
|
+
|
|
727
|
+
// ── downloading ──
|
|
728
|
+
set({ phase: 'downloading' });
|
|
729
|
+
const sumsRes = await fetchWithin(plan.sums.url, c, signal, 30000, 'text/plain, application/octet-stream');
|
|
730
|
+
if (!sumsRes.ok) throw new UpdateError(`GitHub answered ${sumsRes.status} for the checksum file. Try again later.`);
|
|
731
|
+
const tooBig = () => new UpdateError('The checksum file is not what was expected, so nothing was downloaded.');
|
|
732
|
+
if (Number(sumsRes.headers.get('content-length')) > 64 * 1024) throw tooBig();
|
|
733
|
+
const sumsText = await sumsRes.text();
|
|
734
|
+
if (sumsText.length > 64 * 1024) throw tooBig();
|
|
735
|
+
const want = parseSha256Sums(sumsText).get(plan.dmg.name);
|
|
736
|
+
if (!want) throw new UpdateError(`The checksum file doesn't list ${plan.dmg.name}, so its download can't be checked.`);
|
|
737
|
+
// A file made at run time, never one of the app's: nothing for the build to trace.
|
|
738
|
+
const image = path.join(/*turbopackIgnore: true*/ work, plan.dmg.name);
|
|
739
|
+
const got = await download(plan.dmg.url, `${image}.part`, { job, c, signal, expectedSize: plan.dmg.size });
|
|
740
|
+
|
|
741
|
+
// ── verifying ──
|
|
742
|
+
set({ phase: 'verifying' });
|
|
743
|
+
if (!digestsMatch(want, got)) {
|
|
744
|
+
throw new UpdateError("The download doesn't match the checksum published with it, so it wasn't used. Try again later.");
|
|
745
|
+
}
|
|
746
|
+
await fsp.rename(`${image}.part`, image);
|
|
747
|
+
|
|
748
|
+
// ── preparing ──
|
|
749
|
+
set({ phase: 'preparing' });
|
|
750
|
+
// Noted before attaching, so a cancel in the middle still unmounts it.
|
|
751
|
+
mount = { point: path.join(work, 'mnt'), device: null };
|
|
752
|
+
await attach(image, mount, signal);
|
|
753
|
+
const inImage = path.join(mount.point, APP_BUNDLE_NAME);
|
|
754
|
+
await checkApp(inImage, { plan, c, signal, where: 'in the disk image' });
|
|
755
|
+
const kb = Number.parseInt(await run(TOOL.du, ['-sk', inImage], { signal, timeoutMs: 120000 }), 10) || 0;
|
|
756
|
+
ensureSpace(path.dirname(c.bundle), kb * 1024 + SPACE_MARGIN, `in ${path.dirname(c.bundle)}`);
|
|
757
|
+
staged = stagingPath(c.bundle);
|
|
758
|
+
await fsp.rm(staged, { recursive: true, force: true }); // a copy left by an update that was interrupted
|
|
759
|
+
try {
|
|
760
|
+
// ditto, never cpSync, which rewrites Electron's links into absolute paths (TRAPS §37).
|
|
761
|
+
await run(TOOL.ditto, [inImage, staged], { signal, timeoutMs: 900000 });
|
|
762
|
+
} catch (err) {
|
|
763
|
+
if (signal.aborted) throw err;
|
|
764
|
+
throw new UpdateError(`Could not copy the new version into ${path.dirname(c.bundle)} (${err.detail}).`);
|
|
765
|
+
}
|
|
766
|
+
await checkApp(staged, { plan, c, signal, where: 'after copying' });
|
|
767
|
+
await run(TOOL.xattr, ['-dr', 'com.apple.quarantine', staged], { signal }).catch(() => {});
|
|
768
|
+
await detach(mount, c.sleep);
|
|
769
|
+
mount = null;
|
|
770
|
+
await fsp.rm(image, { force: true });
|
|
771
|
+
|
|
772
|
+
// ── restarting ──
|
|
773
|
+
// A last look: a scan, or Install on the Scan page, may have started while
|
|
774
|
+
// this ran. Quitting now would stop it half-way, so leave the app alone and
|
|
775
|
+
// say which one it was.
|
|
776
|
+
if (signal.aborted) throw signal.reason;
|
|
777
|
+
const busy = await c.scanRunning();
|
|
778
|
+
if (busy) {
|
|
779
|
+
throw new UpdateError(`${runningNow(busy)}, so the update was not applied. Try again when it has finished.`);
|
|
780
|
+
}
|
|
781
|
+
othersCheck(c);
|
|
782
|
+
if (signal.aborted) throw signal.reason;
|
|
783
|
+
set({ phase: 'restarting' }); // from here it can't be cancelled
|
|
784
|
+
|
|
785
|
+
const helper = path.join(work, 'apply-update.sh');
|
|
786
|
+
await fsp.copyFile(c.helper, helper); // run a copy: the original is inside the app it replaces
|
|
787
|
+
const args = helperArgs({
|
|
788
|
+
target: c.bundle,
|
|
789
|
+
staged,
|
|
790
|
+
keepDir: c.cacheDir,
|
|
791
|
+
statusFile: c.statusFile,
|
|
792
|
+
from: c.currentVersion,
|
|
793
|
+
to: plan.version,
|
|
794
|
+
pids: c.pids || [process.ppid, process.pid],
|
|
795
|
+
waitSeconds: c.waitSeconds,
|
|
796
|
+
logFile: c.logFile,
|
|
797
|
+
work,
|
|
798
|
+
confirmFile: c.confirmFile,
|
|
799
|
+
confirmWaitSeconds: c.confirmWaitSeconds,
|
|
800
|
+
relaunch: relaunchArgs({ dataDir: c.dataDir }),
|
|
801
|
+
});
|
|
802
|
+
// Written first: the helper replaces it with the outcome, and if it never
|
|
803
|
+
// gets that far, the next start says so instead of saying nothing.
|
|
804
|
+
const started = { outcome: 'started', from: c.currentVersion, to: plan.version, at: new Date().toISOString(), log: c.logFile };
|
|
805
|
+
try { writeStatusFile(c.statusFile, started); } catch { /* a courtesy; the update doesn't depend on it */ }
|
|
806
|
+
let child;
|
|
807
|
+
try {
|
|
808
|
+
child = await c.launch({ helper, args, cwd: c.tmpRoot, logFile: c.logFile });
|
|
809
|
+
await c.sleep(c.settleMs);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
fs.rmSync(c.statusFile, { force: true });
|
|
812
|
+
throw new UpdateError(`The update helper wouldn't start (${err.message}).`);
|
|
813
|
+
}
|
|
814
|
+
if (child && (child.exitCode !== null || child.signalCode !== null)) {
|
|
815
|
+
fs.rmSync(c.statusFile, { force: true });
|
|
816
|
+
const said = logTail(c.logFile);
|
|
817
|
+
throw new UpdateError(`The update helper stopped before the app closed${said ? ` (${said})` : ''}.`);
|
|
818
|
+
}
|
|
819
|
+
// From here the helper waits for this app to go, so the app must quit.
|
|
820
|
+
handedOver = true;
|
|
821
|
+
c.exit();
|
|
822
|
+
} catch (err) {
|
|
823
|
+
const cancelled = signal.aborted && signal.reason instanceof UpdateError && signal.reason.message === CANCELLED;
|
|
824
|
+
set({
|
|
825
|
+
phase: cancelled ? 'cancelled' : 'failed',
|
|
826
|
+
error: cancelled ? null : plain(err),
|
|
827
|
+
code: !cancelled && err instanceof UpdateError ? err.code : null,
|
|
828
|
+
});
|
|
829
|
+
} finally {
|
|
830
|
+
if (mount) await detach(mount, c.sleep).catch(() => {});
|
|
831
|
+
if (!handedOver) {
|
|
832
|
+
if (staged) await fsp.rm(staged, { recursive: true, force: true }).catch(() => {});
|
|
833
|
+
if (work) await fsp.rm(work, { recursive: true, force: true }).catch(() => {});
|
|
834
|
+
}
|
|
835
|
+
process.removeListener('exit', onExit);
|
|
836
|
+
set({ endedAt: new Date().toISOString() });
|
|
837
|
+
}
|
|
838
|
+
}
|