six-degrees 0.8.0 → 1.0.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/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- 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 +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- 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.nft.json +1 -1
- 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.nft.json +1 -1
- 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.js.nft.json +1 -1
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/csv/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restore/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/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 +16 -16
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +16 -16
- 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 +15 -15
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/insights.html +1 -1
- package/.next/standalone/.next/server/app/insights.rsc +16 -16
- package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +15 -15
- 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 +16 -16
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +16 -16
- 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 +15 -15
- 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 +16 -16
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +16 -16
- 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 +15 -15
- 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 +16 -16
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +16 -16
- 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 +15 -15
- 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 +16 -16
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +16 -16
- 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 +15 -15
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +16 -16
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +16 -16
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +15 -15
- 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 +17 -17
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +17 -17
- 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 +15 -15
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +16 -16
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +15 -15
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_02iz_9q._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0t552z6.js → lib_rpc_1hjjjr4.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- 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/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0hggugbh84m4t.js +21 -0
- package/.next/standalone/.next/static/chunks/1ods5h86raivz.js +1 -0
- package/.next/standalone/CHANGELOG.md +23 -0
- package/.next/standalone/README.md +15 -13
- package/.next/standalone/SECURITY.md +9 -4
- package/.next/standalone/app/api/scraper/route.js +9 -1
- package/.next/standalone/app/setup/page.js +55 -4
- package/.next/standalone/install.sh +7 -6
- package/.next/standalone/lib/scanner-setup.js +82 -3
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/build-app.mjs +167 -11
- package/.next/standalone/scripts/build-desktop.mjs +3 -2
- package/.next/standalone/scripts/ci-mac-signing.sh +128 -0
- package/.next/standalone/scripts/entitlements/electron.plist +23 -0
- package/.next/standalone/scripts/entitlements/node.plist +27 -0
- package/.next/standalone/scripts/mac-sign.mjs +563 -0
- package/.next/standalone/scripts/make-dmg-background.mjs +2 -1
- package/.next/standalone/scripts/set-signing-secrets.sh +96 -0
- package/.next/static/chunks/0hggugbh84m4t.js +21 -0
- package/.next/static/chunks/1ods5h86raivz.js +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +15 -13
- package/SECURITY.md +9 -4
- package/lib/scanner-setup.js +82 -3
- package/lib/settings.js +3 -0
- package/package.json +1 -1
- package/.next/standalone/.next/static/chunks/0838_s6w-i_ik.js +0 -1
- package/.next/standalone/.next/static/chunks/112mv-com-mqa.js +0 -21
- package/.next/static/chunks/0838_s6w-i_ik.js +0 -1
- package/.next/static/chunks/112mv-com-mqa.js +0 -21
- /package/.next/standalone/.next/static/{3NCyJyAPDCEYsYTwF1Ieu → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{3NCyJyAPDCEYsYTwF1Ieu → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{3NCyJyAPDCEYsYTwF1Ieu → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
- /package/.next/static/{3NCyJyAPDCEYsYTwF1Ieu → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
- /package/.next/static/{3NCyJyAPDCEYsYTwF1Ieu → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{3NCyJyAPDCEYsYTwF1Ieu → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { release as osRelease } from 'node:os';
|
|
4
5
|
import { projectRoot, dataDir } from '../../../lib/paths';
|
|
5
6
|
import { resolveProfile, networkCounts } from '../../../lib/profile';
|
|
6
7
|
import { scanProgress, mappingNow } from '../../../lib/scan-progress';
|
|
@@ -15,7 +16,8 @@ import { waitingPhotoCount } from '../../../lib/photos';
|
|
|
15
16
|
import { readSettings } from '../../../lib/settings';
|
|
16
17
|
import { riskAccepted, touchesLinkedIn, RISK_REFUSAL } from '../../../lib/scan-risk';
|
|
17
18
|
import { reachIndex, circleState } from '../../../lib/reach';
|
|
18
|
-
import { chromeInstalled, signedInFrom, SIGNED_IN_FILE, CHROME_REFUSAL } from '../../../lib/scanner-setup';
|
|
19
|
+
import { chromeInstalled, signedInFrom, macosVersion, SIGNED_IN_FILE, CHROME_REFUSAL } from '../../../lib/scanner-setup';
|
|
20
|
+
import { installKind } from '../../../lib/release';
|
|
19
21
|
import { AUTO_REFUSAL, connectResultIn, connectMarks, inviteRefusal, personRefusal } from '../../../lib/auto-connect';
|
|
20
22
|
import { markSentRows, awardXP, whose, requestStanding } from '../../../lib/requests';
|
|
21
23
|
import {
|
|
@@ -385,6 +387,12 @@ async function status() {
|
|
|
385
387
|
signedIn: signedIn(),
|
|
386
388
|
// The one-time "I understand" before the first scan (lib/scan-risk.js).
|
|
387
389
|
riskAccepted: scanRisk(),
|
|
390
|
+
// Step 1's App Management item (lib/scanner-setup.js appManagementStep):
|
|
391
|
+
// which macOS, so its button opens the pane where that version keeps it,
|
|
392
|
+
// and whether this is the Mac app, the name macOS asks about. null off a Mac.
|
|
393
|
+
mac: process.platform === 'darwin'
|
|
394
|
+
? { version: macosVersion(process.platform, osRelease()), app: installKind(process.env, projectRoot()) === 'mac-app' }
|
|
395
|
+
: null,
|
|
388
396
|
},
|
|
389
397
|
network,
|
|
390
398
|
photosWaiting,
|
|
@@ -5,7 +5,8 @@ import { useSearchParams } from 'next/navigation';
|
|
|
5
5
|
import OnboardingGate from '../components/OnboardingGate';
|
|
6
6
|
import Link from 'next/link';
|
|
7
7
|
import { stopScrape, pickedPerson, scanRequest, HIDE_CHROME_KEY } from '../../lib/scraper-client';
|
|
8
|
-
import { setupStep, askForField } from '../../lib/scanner-setup';
|
|
8
|
+
import { setupStep, askForField, appManagementStep } from '../../lib/scanner-setup';
|
|
9
|
+
import { saveSettings } from '../../lib/settings-client';
|
|
9
10
|
import { RISK_POINTS } from '../../lib/scan-risk';
|
|
10
11
|
import { IS_DEMO } from '../../lib/demo';
|
|
11
12
|
import { circleScanCost } from '../../lib/reach';
|
|
@@ -82,6 +83,8 @@ function SetupInner() {
|
|
|
82
83
|
// loads, null when it couldn't be read. And the answer given here, if any.
|
|
83
84
|
const [settings, setSettings] = useState(undefined);
|
|
84
85
|
const [field, setField] = useState(null);
|
|
86
|
+
// Step 1's App Management item, answered here ('done' or 'skipped'), if it was.
|
|
87
|
+
const [appAnswer, setAppAnswer] = useState(null);
|
|
85
88
|
// Someone sent here to have their circle scanned (Bridge Chains, the Degrees
|
|
86
89
|
// panel's Ready to scan): /setup?scan=<id>. Nothing starts until it's confirmed.
|
|
87
90
|
// The router's search params rather than window.location: every way here is a
|
|
@@ -143,6 +146,15 @@ function SetupInner() {
|
|
|
143
146
|
window.scrollTo(0, 0);
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
// Done or Skip on the App Management item: it goes at once, and the answer is
|
|
150
|
+
// kept so it isn't offered again. If that can't be saved it still goes for
|
|
151
|
+
// now and comes back next time, which is all it costs (as Skip for now does,
|
|
152
|
+
// app/components/FieldStep.js).
|
|
153
|
+
async function answerAppManagement(answer) {
|
|
154
|
+
setAppAnswer(answer);
|
|
155
|
+
await saveSettings({ appManagement: answer });
|
|
156
|
+
}
|
|
157
|
+
|
|
146
158
|
// "I understand": when, kept with your settings so it travels with a copy.
|
|
147
159
|
async function acceptRisk() {
|
|
148
160
|
setError(null);
|
|
@@ -212,6 +224,10 @@ function SetupInner() {
|
|
|
212
224
|
// Your field, asked once when your connections are in, before who they know
|
|
213
225
|
// (lib/scanner-setup.js askForField); left out while that isn't known (null).
|
|
214
226
|
const askField = IS_DEMO || field ? false : askForField(s, settings);
|
|
227
|
+
// On a Mac, macOS may ask about App Management when the scanner starts Chrome
|
|
228
|
+
// (lib/scanner-setup.js appManagementStep): offered once, under step 1, and
|
|
229
|
+
// never part of whether step 1 is done.
|
|
230
|
+
const appMgmt = IS_DEMO || appAnswer ? false : appManagementStep(s, settings);
|
|
215
231
|
// Step 4, honestly: the first circle shows in minutes, the rest takes days.
|
|
216
232
|
const pace = li?.limits?.pace;
|
|
217
233
|
const firstCircle = durationText(firstCircleSeconds(pace));
|
|
@@ -375,6 +391,10 @@ function SetupInner() {
|
|
|
375
391
|
more={s && step1.chrome && (
|
|
376
392
|
<Launch href={step1.chrome.href} quiet={Boolean(step1.button)}>{step1.chrome.label}</Launch>
|
|
377
393
|
)} />
|
|
394
|
+
{/* Blake, 2026-10-04: "shouldnt we have in the onboarding for scan … a button where the
|
|
395
|
+
user is basically brought to the app management and enables it like Flow does".
|
|
396
|
+
Optional, so it sits under the step and never holds it up. */}
|
|
397
|
+
{appMgmt && <AppManagementItem item={appMgmt} onAnswer={answerAppManagement} />}
|
|
378
398
|
</StepRow>
|
|
379
399
|
<StepRow n={2} label="Sign in" state={stateOf(2, !!c.signedIn)}>
|
|
380
400
|
<Mission title="Sign in to LinkedIn, once" time="You do this part" done={!!c.signedIn}
|
|
@@ -799,15 +819,46 @@ function Mission({ title, time, body, launch, more, done = false, children }) {
|
|
|
799
819
|
);
|
|
800
820
|
}
|
|
801
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Step 1's optional item on a Mac (lib/scanner-setup.js appManagementStep):
|
|
824
|
+
* why macOS may ask about App Management while scanning, the button that opens
|
|
825
|
+
* that pane, and Done or Skip, either of which puts it away for good.
|
|
826
|
+
*/
|
|
827
|
+
function AppManagementItem({ item, onAnswer }) {
|
|
828
|
+
return (
|
|
829
|
+
<section aria-labelledby="app-management-title" data-app-management style={{
|
|
830
|
+
marginTop: 10, padding: '14px 18px', borderRadius: 14,
|
|
831
|
+
background: 'rgba(var(--sd-ink, 255, 255, 255), 0.025)',
|
|
832
|
+
border: '1px dashed rgba(var(--sd-ink, 255, 255, 255), 0.16)',
|
|
833
|
+
}}>
|
|
834
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
|
|
835
|
+
<span id="app-management-title" style={{ fontSize: 16, fontWeight: 800 }}>{item.title}</span>
|
|
836
|
+
<span style={{ fontSize: 12, color: 'var(--sd-fg-2, #8fd9b6)', marginLeft: 'auto' }}>Optional, once</span>
|
|
837
|
+
</div>
|
|
838
|
+
<div style={{ fontSize: 13.5, color: 'var(--sd-fg-3, #9fb0bb)', lineHeight: 1.6, margin: '6px 0 12px', maxWidth: 600 }}>{item.text}</div>
|
|
839
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
840
|
+
<Launch href={item.href} quiet>Open App Management</Launch>
|
|
841
|
+
<Btn onClick={() => onAnswer('done')}>Done</Btn>
|
|
842
|
+
<Btn onClick={() => onAnswer('skipped')}>Skip</Btn>
|
|
843
|
+
</div>
|
|
844
|
+
<div style={{ fontSize: 12, color: 'var(--sd-fg-4, #778)', marginTop: 10 }}>It’s in {item.where}.</div>
|
|
845
|
+
</section>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
802
849
|
/**
|
|
803
850
|
* The big go button. `quiet` once that step is done, so the next thing to do
|
|
804
|
-
* stands out. With `href`, a link that looks the same
|
|
805
|
-
*
|
|
851
|
+
* stands out. With `href`, a link that looks the same: a web address (Chrome's
|
|
852
|
+
* download) opens outside the app, which the desktop app sends to the browser.
|
|
853
|
+
* A System Settings address (App Management) opens in place: a browser then
|
|
854
|
+
* asks to open System Settings without leaving an empty tab behind, and the
|
|
855
|
+
* desktop app opens System Settings itself (desktop/main.mjs).
|
|
806
856
|
*/
|
|
807
857
|
function Launch({ children, onClick, disabled, quiet = false, href }) {
|
|
808
858
|
if (href) {
|
|
859
|
+
const web = /^https?:/i.test(href);
|
|
809
860
|
return (
|
|
810
|
-
<a className="launch-go" href={href} target
|
|
861
|
+
<a className="launch-go" href={href} {...(web ? { target: '_blank', rel: 'noreferrer' } : {})} style={{
|
|
811
862
|
...launchLook(false, quiet), textDecoration: 'none',
|
|
812
863
|
}}>
|
|
813
864
|
<span aria-hidden="true" style={{ fontSize: quiet ? 11 : 13 }}>▶</span>{children}
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
# 3. copies "Six Degrees.app" into /Applications (or ~/Applications)
|
|
10
10
|
# 4. opens it
|
|
11
11
|
#
|
|
12
|
-
# Why a script
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
12
|
+
# Why a script as well as the .dmg: one line that picks the right build for this
|
|
13
|
+
# Mac, checks it against the published checksums, installs it and opens it, and
|
|
14
|
+
# running it again updates. Until 0.8.0 the app wasn't signed, and this was also
|
|
15
|
+
# the way around macOS's first-launch block (a file fetched with curl isn't
|
|
16
|
+
# marked as downloaded from the internet). Since then the app is signed by Blake
|
|
17
|
+
# Burford and notarized by Apple (docs/brain/DESKTOP.md D4), so the .dmg opens
|
|
18
|
+
# like any Mac app too.
|
|
18
19
|
#
|
|
19
20
|
# Your data is never touched: it lives in ~/.six-degrees, not in the app, so
|
|
20
21
|
# running this again later is also how you update.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// What the Scan page's first step says and offers, from the checks GET
|
|
2
2
|
// /api/scraper reports (app/api/scraper/route.js, lib/scanner-python.js),
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
3
|
+
// its optional item on a Mac about macOS's App Management question
|
|
4
|
+
// (appManagementStep), whether it asks for your field once your connections
|
|
5
|
+
// are in (askForField), and two answers the server gives that step: is Google
|
|
6
|
+
// Chrome here (chromeInstalled), and are you signed in to LinkedIn (signedInFrom).
|
|
6
7
|
// Plain data in, plain data out: the page draws it, and
|
|
7
8
|
// tests/scanner-setup.test.mjs checks every state without a browser. No Node
|
|
8
9
|
// imports here; the page runs in the browser.
|
|
@@ -178,6 +179,84 @@ function stepOnly(status) {
|
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// ── macOS's App Management question ─────────────────────────────────────────
|
|
183
|
+
// The scanner starts Google Chrome, and Chrome's own updater may then try to
|
|
184
|
+
// update /Applications/Google Chrome.app. macOS puts that down to the app that
|
|
185
|
+
// started Chrome and asks whether Six Degrees may manage apps (System Settings
|
|
186
|
+
// → Privacy & Security → App Management). Saying no is harmless: Chrome updates
|
|
187
|
+
// itself the next time you open it, and scanning works either way. Now that
|
|
188
|
+
// the Mac app is signed, a yes lasts across updates (unsigned copies lost it
|
|
189
|
+
// every update). "shouldnt we have in the onboarding for scan … a button where
|
|
190
|
+
// the user is basically brought to the app management and enables it like
|
|
191
|
+
// Flow does" (Blake, 2026-10-04). So step 1 offers the pane, once, and never
|
|
192
|
+
// waits for it: the app can't read whether it was allowed, and it's optional.
|
|
193
|
+
|
|
194
|
+
// The pane's address, as macOS 26 has it (checked on 26.5.1; macOS 15 knows it
|
|
195
|
+
// too), and as macOS 13 and 14 have it. desktop/lib.mjs SETTINGS_URLS lets
|
|
196
|
+
// exactly these two out of the Mac app; tests/desktop.test.mjs keeps them equal.
|
|
197
|
+
export const APP_MANAGEMENT_URL = 'x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AppBundles';
|
|
198
|
+
export const APP_MANAGEMENT_URL_OLDER = 'x-apple.systempreferences:com.apple.preference.security?Privacy_AppBundles';
|
|
199
|
+
export const APP_MANAGEMENT_URLS = Object.freeze([APP_MANAGEMENT_URL, APP_MANAGEMENT_URL_OLDER]);
|
|
200
|
+
export const APP_MANAGEMENT_TITLE = 'Let Six Degrees open Chrome without macOS asking';
|
|
201
|
+
export const APP_MANAGEMENT_WHERE = 'System Settings → Privacy & Security → App Management';
|
|
202
|
+
|
|
203
|
+
/** The Scan page's App Management item, once answered: 'done' or 'skipped' (null: not yet). */
|
|
204
|
+
export const APP_MANAGEMENT_SETTING = {
|
|
205
|
+
default: null,
|
|
206
|
+
parse(value) {
|
|
207
|
+
if (value === null || value === 'done' || value === 'skipped') return value;
|
|
208
|
+
throw new Error('The App Management answer is done, skipped or null.');
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* macOS's major version from the Darwin kernel's (os.release() on the
|
|
214
|
+
* server): Darwin 25 is macOS 26, when the numbers jumped to the year, and 20
|
|
215
|
+
* to 24 are macOS 11 to 15. null off a Mac, or when it can't be read.
|
|
216
|
+
*/
|
|
217
|
+
export function macosVersion(platform, release) {
|
|
218
|
+
if (platform !== 'darwin') return null;
|
|
219
|
+
const darwin = Number.parseInt(String(release ?? ''), 10);
|
|
220
|
+
if (!Number.isInteger(darwin) || darwin < 1) return null;
|
|
221
|
+
if (darwin >= 25) return darwin + 1;
|
|
222
|
+
if (darwin >= 20) return darwin - 9;
|
|
223
|
+
return 10; // Catalina and before were all 10.x
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Step 1's optional item about App Management: { title, text, href, where }
|
|
228
|
+
* to show it, false not to, null while that isn't known yet (the page leaves
|
|
229
|
+
* it out until it is, as with askForField).
|
|
230
|
+
*
|
|
231
|
+
* Only on a Mac (`status.checks.mac`: { version, app }, null elsewhere and
|
|
232
|
+
* from an older server), and from macOS 13, which brought App Management.
|
|
233
|
+
* Never once answered, Done or Skip (`settings.appManagement`), and not when
|
|
234
|
+
* settings couldn't be read, since the answer couldn't be kept either. The
|
|
235
|
+
* button opens the pane for that macOS (an unknown version gets the newest
|
|
236
|
+
* address), and `where` says where it is by hand.
|
|
237
|
+
*
|
|
238
|
+
* In the Mac app macOS asks about Six Degrees. Started with npx, Chrome's
|
|
239
|
+
* starter is the app Six Degrees was started from (Terminal, for example), and
|
|
240
|
+
* that is the one macOS names.
|
|
241
|
+
*/
|
|
242
|
+
export function appManagementStep(status, settings) {
|
|
243
|
+
if (!status) return null;
|
|
244
|
+
const mac = status.checks?.mac;
|
|
245
|
+
if (!mac || (mac.version != null && mac.version < 13)) return false;
|
|
246
|
+
if (settings === undefined) return null;
|
|
247
|
+
if (!settings || settings.appManagement) return false;
|
|
248
|
+
const asked = mac.app ? 'Six Degrees' : 'the app you started Six Degrees from (Terminal, for example)';
|
|
249
|
+
const allow = mac.app ? 'Allow Six Degrees' : 'Allow that app';
|
|
250
|
+
return {
|
|
251
|
+
title: APP_MANAGEMENT_TITLE,
|
|
252
|
+
text: `When the scanner starts Google Chrome, macOS may ask whether ${asked} can manage apps. `
|
|
253
|
+
+ 'That’s Chrome updating itself while the scanner uses it. '
|
|
254
|
+
+ `${allow} once in App Management and macOS won’t ask again. Scanning works either way.`,
|
|
255
|
+
href: mac.version != null && mac.version < 15 ? APP_MANAGEMENT_URL_OLDER : APP_MANAGEMENT_URL,
|
|
256
|
+
where: APP_MANAGEMENT_WHERE,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
181
260
|
/**
|
|
182
261
|
* Whether the Scan page asks for your field (app/components/FieldStep.js):
|
|
183
262
|
* once, when your own connections are in (step 3) and before who they know is
|
|
@@ -17,6 +17,7 @@ import { SCAN_RISK_SETTING } from './scan-risk.js';
|
|
|
17
17
|
import { AUTO_ACCEPTED_SETTING } from './auto-connect.js';
|
|
18
18
|
import { THEME_SETTING } from './themes.js';
|
|
19
19
|
import { GALAXY_LAYOUTS_SETTING } from './galaxy-layouts.js';
|
|
20
|
+
import { APP_MANAGEMENT_SETTING } from './scanner-setup.js';
|
|
20
21
|
|
|
21
22
|
export const SETTINGS = {
|
|
22
23
|
// Scores → Your sector: industries whose companies count for more (lib/sector-focus.js).
|
|
@@ -29,6 +30,8 @@ export const SETTINGS = {
|
|
|
29
30
|
titleRanking: TITLE_RANKING_SETTING,
|
|
30
31
|
// The Scan page's "I understand" before the first scan: when it was given (lib/scan-risk.js).
|
|
31
32
|
scanRiskAccepted: SCAN_RISK_SETTING,
|
|
33
|
+
// The Scan page's optional App Management item on a Mac: Done or Skip, so it's offered once (lib/scanner-setup.js).
|
|
34
|
+
appManagement: APP_MANAGEMENT_SETTING,
|
|
32
35
|
// Auto's one-time question before its first connection request: when it was answered yes (lib/auto-connect.js).
|
|
33
36
|
autoConnectAccepted: AUTO_ACCEPTED_SETTING,
|
|
34
37
|
// Settings → Appearance: the look (lib/themes.js). Here so it travels with a
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "six-degrees",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "1.0.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"os": [
|
|
12
12
|
"darwin",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blake Burford",
|
|
@@ -21,10 +21,15 @@
|
|
|
21
21
|
// Chromium instead would make the scanner MORE detectable, not less, which
|
|
22
22
|
// defeats the point of driving the user's real browser.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// Signing (DESKTOP.md D4, scripts/mac-sign.mjs). With a Developer ID identity
|
|
25
|
+
// in SIX_DEGREES_SIGN_IDENTITY, every program and library is signed inside out
|
|
26
|
+
// with the hardened runtime, and with SIX_DEGREES_NOTARY_KEY, _KEY_ID and
|
|
27
|
+
// _ISSUER the app and the disk image are notarized by Apple and stapled: it
|
|
28
|
+
// opens like any Mac app. Releases do this (release.yml). Without an identity
|
|
29
|
+
// (forks, pull requests, a local build) the app is signed ad hoc, exactly as
|
|
30
|
+
// before D4: it costs nothing and avoids the separate "app is damaged" failure
|
|
31
|
+
// that unsigned arm64 binaries otherwise hit, and a copy downloaded in a
|
|
32
|
+
// browser then needs one approval in System Settings.
|
|
28
33
|
|
|
29
34
|
import { execFileSync } from 'node:child_process';
|
|
30
35
|
import { createHash } from 'node:crypto';
|
|
@@ -38,6 +43,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
38
43
|
import {
|
|
39
44
|
standaloneBuild, downloadVerified, machoSlices, machoSlice, megabytes, IMPORTS,
|
|
40
45
|
} from '../lib/scanner-python.js';
|
|
46
|
+
import {
|
|
47
|
+
signingSetup, signDeveloperId, signatureInfo, signerName, signImage, notarize, staple, stapleValid, assess, SigningError,
|
|
48
|
+
} from './mac-sign.mjs';
|
|
41
49
|
|
|
42
50
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
51
|
const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
@@ -53,6 +61,16 @@ if (!['classic', 'electron'].includes(SHELL)) {
|
|
|
53
61
|
console.error(`\n ✗ --shell must be classic or electron, not ${SHELL}\n`);
|
|
54
62
|
process.exit(1);
|
|
55
63
|
}
|
|
64
|
+
// How this build is signed: ad hoc, or Developer ID (and notarized) when the
|
|
65
|
+
// environment names an identity (scripts/mac-sign.mjs). A setting that can't be
|
|
66
|
+
// right stops the build here, before anything is built.
|
|
67
|
+
const SIGNING = signingSetup(process.env, { exists: existsSync });
|
|
68
|
+
if (SIGNING.error) {
|
|
69
|
+
console.error(`\n ✗ ${SIGNING.error}\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const DEVELOPER_ID = SIGNING.mode === 'developer-id';
|
|
73
|
+
const NOTARIZE = Boolean(DEVELOPER_ID && SIGNING.notary);
|
|
56
74
|
const APP_NAME = 'Six Degrees';
|
|
57
75
|
const OUT = path.join(ROOT, 'dist');
|
|
58
76
|
const APP = path.join(OUT, `${APP_NAME}.app`);
|
|
@@ -115,6 +133,9 @@ function detach(mountPoint, device) {
|
|
|
115
133
|
rmSync(OUT, { recursive: true, force: true });
|
|
116
134
|
|
|
117
135
|
step('Building the app');
|
|
136
|
+
console.log(DEVELOPER_ID
|
|
137
|
+
? ` it will be signed with Developer ID${NOTARIZE ? ' and notarized by Apple' : ', not notarized (no SIX_DEGREES_NOTARY_* settings)'}`
|
|
138
|
+
: ' it will be signed ad hoc (no SIX_DEGREES_SIGN_IDENTITY)');
|
|
118
139
|
if (process.argv.includes('--fast') && existsSync(path.join(ROOT, '.next', 'standalone', 'server.js'))) {
|
|
119
140
|
console.log(' --fast: reusing the existing build (make sure it is current)');
|
|
120
141
|
} else {
|
|
@@ -290,7 +311,10 @@ writeFileSync(path.join(APP, 'Contents', 'Info.plist'), `<?xml version="1.0" enc
|
|
|
290
311
|
</plist>
|
|
291
312
|
`);
|
|
292
313
|
|
|
293
|
-
// ---- 4. sign
|
|
314
|
+
// ---- 4. sign ---------------------------------------------------------------
|
|
315
|
+
if (DEVELOPER_ID) {
|
|
316
|
+
signWithDeveloperId(APP);
|
|
317
|
+
} else {
|
|
294
318
|
step('Signing (ad-hoc)');
|
|
295
319
|
try {
|
|
296
320
|
run('codesign', ['--force', '--deep', '--sign', '-', APP]);
|
|
@@ -298,6 +322,7 @@ try {
|
|
|
298
322
|
} catch {
|
|
299
323
|
console.log(' codesign failed; the app will still run after the one-time approval');
|
|
300
324
|
}
|
|
325
|
+
}
|
|
301
326
|
} // end of the classic shell
|
|
302
327
|
|
|
303
328
|
// ---- the Electron shell (--shell=electron) -----------------------------------
|
|
@@ -348,6 +373,10 @@ async function buildElectronShell() {
|
|
|
348
373
|
// and nothing copies them again (TRAPS §37).
|
|
349
374
|
await bundlePython(path.join(APP, 'Contents', 'Resources'));
|
|
350
375
|
|
|
376
|
+
if (DEVELOPER_ID) {
|
|
377
|
+
signWithDeveloperId(APP);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
351
380
|
step('Signing (ad-hoc)');
|
|
352
381
|
// Electron's framework and helpers must all carry the same kind of signature,
|
|
353
382
|
// or macOS refuses to load them; re-signing the whole bundle ad hoc does that.
|
|
@@ -359,6 +388,96 @@ async function buildElectronShell() {
|
|
|
359
388
|
console.log(' signed ad-hoc and verified — no Apple account needed, still unnotarised');
|
|
360
389
|
}
|
|
361
390
|
|
|
391
|
+
// ---- Developer ID and notarization (DESKTOP.md D4) ----------------------------
|
|
392
|
+
// Inside out with the hardened runtime (scripts/mac-sign.mjs signDeveloperId:
|
|
393
|
+
// every loose program and library, then the helper apps and frameworks deepest
|
|
394
|
+
// first, then the app), then the signed programs are run once to prove their
|
|
395
|
+
// entitlements are enough. With the notary settings, the app goes to Apple and
|
|
396
|
+
// comes back with its ticket stapled to it, and only then is the disk image made
|
|
397
|
+
// from it: the app people drag to Applications carries its own ticket, and so
|
|
398
|
+
// does the copy install.sh and the in-app updater put in place with ditto.
|
|
399
|
+
function signWithDeveloperId(app) {
|
|
400
|
+
step('Signing with Developer ID (inside out, hardened runtime, timestamped)');
|
|
401
|
+
const { info } = guard(() => signDeveloperId(app, SIGNING));
|
|
402
|
+
checkSignedRuntime(app, info);
|
|
403
|
+
if (!NOTARIZE) return;
|
|
404
|
+
step('Notarizing the app (Apple; usually a few minutes)');
|
|
405
|
+
const zip = path.join(OUT, 'notarize-app.zip');
|
|
406
|
+
run('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', app, zip]);
|
|
407
|
+
guard(() => notarize(zip, SIGNING.notary, { what: 'the app' }));
|
|
408
|
+
rmSync(zip, { force: true });
|
|
409
|
+
guard(() => staple(app));
|
|
410
|
+
const verdict = assess(app, 'exec');
|
|
411
|
+
if (!verdict.ok && !verdict.disabled) fail(`Gatekeeper doesn't accept the stapled app as notarized: ${oneLine(verdict.text)}`);
|
|
412
|
+
console.log(verdict.ok ? ` stapled; Gatekeeper says: ${verdict.source}` : gatekeeperOff());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// The signed programs, run as the app runs them: the server's Node (and sharp,
|
|
416
|
+
// the one native module it loads), the scanner's Python with its packages, and
|
|
417
|
+
// Playwright's driver. A missing entitlement or a library that fails library
|
|
418
|
+
// validation stops the build here, minutes before notarization would, and long
|
|
419
|
+
// before a user's scan. Nothing here writes into the app (no bytecode: -B and
|
|
420
|
+
// PYTHONDONTWRITEBYTECODE), which the image check would catch anyway.
|
|
421
|
+
function checkSignedRuntime(app, info) {
|
|
422
|
+
step('Running the signed programs under the hardened runtime');
|
|
423
|
+
if (!info.team) {
|
|
424
|
+
// A self-signed test identity has no Team ID, and library validation then
|
|
425
|
+
// refuses even the app's own libraries. A Developer ID always has one.
|
|
426
|
+
console.log(' skipped: this identity has no Team ID (a test identity, not a Developer ID)');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const res = path.join(app, 'Contents', 'Resources');
|
|
430
|
+
const env = {
|
|
431
|
+
HOME: os.homedir(), TMPDIR: os.tmpdir(), PATH: '/usr/bin:/bin:/usr/sbin:/sbin', LANG: 'en_US.UTF-8',
|
|
432
|
+
PYTHONNOUSERSITE: '1', PYTHONDONTWRITEBYTECODE: '1',
|
|
433
|
+
};
|
|
434
|
+
const server = [path.join(res, 'server'), path.join(res, 'app')].find((d) => existsSync(path.join(d, 'server.js')));
|
|
435
|
+
const sharp = server && existsSync(path.join(server, 'node_modules', 'sharp')) ? path.join(server, 'node_modules', 'sharp') : null;
|
|
436
|
+
console.log(` the server's Node: ${mustRun(path.join(res, 'node'), ['-e',
|
|
437
|
+
`require('node:sqlite');${sharp ? ` require(${JSON.stringify(sharp)});` : ''} console.log(process.version${sharp ? ", '· sharp loads'" : ''})`],
|
|
438
|
+
env, 'The app\'s Node doesn\'t run once signed (its entitlements: scripts/entitlements/node.plist)')}`);
|
|
439
|
+
const py = path.join(res, 'python');
|
|
440
|
+
if (!existsSync(path.join(py, 'bin', 'python3'))) return;
|
|
441
|
+
console.log(` the scanner's Python: ${mustRun(path.join(py, 'bin', 'python3'), ['-E', '-s', '-B', '-c',
|
|
442
|
+
`${IMPORTS}; import sys; print(sys.version.split()[0], "· its packages load")`],
|
|
443
|
+
env, 'The scanner\'s Python doesn\'t run once signed')}`);
|
|
444
|
+
const xy = readdirSync(path.join(py, 'lib')).find((n) => /^python3\.\d+$/.test(n));
|
|
445
|
+
const driver = path.join(py, 'lib', xy || 'python3', 'site-packages', 'playwright', 'driver');
|
|
446
|
+
if (existsSync(path.join(driver, 'node'))) {
|
|
447
|
+
console.log(` Playwright's driver: ${mustRun(path.join(driver, 'node'), [path.join(driver, 'package', 'cli.js'), '--version'],
|
|
448
|
+
env, 'Playwright\'s driver doesn\'t start once signed')}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// A step of the signing that failed: its reason, never a credential (mac-sign.mjs
|
|
453
|
+
// never puts one in a message), and the build stops.
|
|
454
|
+
function guard(fn) {
|
|
455
|
+
try {
|
|
456
|
+
return fn();
|
|
457
|
+
} catch (err) {
|
|
458
|
+
fail(err instanceof SigningError ? err.message : `Signing failed: ${String(err.message || err).split('\n')[0]}`);
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function fail(message) {
|
|
464
|
+
console.error(`\n ✗ ${message}\n`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// When this machine has Gatekeeper's assessments turned off, spctl accepts
|
|
469
|
+
// anything and can't confirm notarization; Apple's "Accepted" and the stapled
|
|
470
|
+
// tickets (validated) are what's left, and the log says so.
|
|
471
|
+
function gatekeeperOff() {
|
|
472
|
+
return ' stapled; Gatekeeper\'s assessments are turned off on this machine, so it can\'t be asked: '
|
|
473
|
+
+ 'the stapled tickets and Apple\'s Accepted are the check';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// (Function declarations, not consts: the shells call these before this line runs.)
|
|
477
|
+
function oneLine(text) {
|
|
478
|
+
return String(text || '').trim().split('\n').join(' | ');
|
|
479
|
+
}
|
|
480
|
+
|
|
362
481
|
// ---- the scanner's Python (Electron app only; DESKTOP.md D2) ------------------
|
|
363
482
|
// A standalone CPython from python-build-standalone, with the scanner's packages
|
|
364
483
|
// installed, at Contents/Resources/python. desktop/main.mjs names it to the
|
|
@@ -602,9 +721,11 @@ mkdirSync(staging, { recursive: true });
|
|
|
602
721
|
// on the build machine: anywhere else those paths do not exist (TRAPS §37).
|
|
603
722
|
run('ditto', [APP, path.join(staging, `${APP_NAME}.app`)]);
|
|
604
723
|
|
|
605
|
-
// The picture behind the window says what to do: drag across,
|
|
606
|
-
//
|
|
607
|
-
// this image used to carry.
|
|
724
|
+
// The picture behind the window says what to do: drag across, and that the app
|
|
725
|
+
// is signed and notarized, so it opens like any Mac app (it said Open Anyway
|
|
726
|
+
// until D4). It replaces the READ ME text file this image used to carry. A build
|
|
727
|
+
// signed ad hoc gets the same picture: it never left this Mac, so macOS doesn't
|
|
728
|
+
// ask about it either. Made by scripts/make-dmg-background.mjs from
|
|
608
729
|
// scripts/dmg/background.html; its geometry and the positions below are one
|
|
609
730
|
// layout, so change them together.
|
|
610
731
|
const BACKGROUND = path.join(ROOT, 'scripts', 'dmg', 'background.tiff');
|
|
@@ -742,6 +863,21 @@ detach(mount, device);
|
|
|
742
863
|
run('hdiutil', ['convert', rw, '-format', 'UDZO', '-imagekey', 'zlib-level=9', '-o', dmg, '-ov']);
|
|
743
864
|
rmSync(rw, { force: true });
|
|
744
865
|
rmSync(staging, { recursive: true, force: true });
|
|
866
|
+
|
|
867
|
+
// The image gets its own signature and ticket (DESKTOP.md D4): Gatekeeper judges
|
|
868
|
+
// a downloaded .dmg by them before it looks at the app inside. Made from the
|
|
869
|
+
// stapled app, so the app keeps its own ticket once it is copied out.
|
|
870
|
+
if (DEVELOPER_ID) {
|
|
871
|
+
step('Signing the disk image');
|
|
872
|
+
guard(() => signImage(dmg, SIGNING));
|
|
873
|
+
console.log(' signed and verified');
|
|
874
|
+
if (NOTARIZE) {
|
|
875
|
+
step('Notarizing the disk image (Apple; usually a few minutes)');
|
|
876
|
+
guard(() => notarize(dmg, SIGNING.notary, { what: 'the disk image' }));
|
|
877
|
+
guard(() => staple(dmg));
|
|
878
|
+
console.log(' stapled');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
745
881
|
checkImage(dmg);
|
|
746
882
|
|
|
747
883
|
// Check the app as people will get it: inside the finished image, not the copy
|
|
@@ -763,6 +899,12 @@ function checkImage(image) {
|
|
|
763
899
|
} catch (err) {
|
|
764
900
|
problem = `its signature does not verify: ${(err.stderr || '').toString().trim().split('\n').slice(0, 2).join(' ')}`;
|
|
765
901
|
}
|
|
902
|
+
// Notarized: the app inside carries its ticket, and Gatekeeper accepts it.
|
|
903
|
+
if (NOTARIZE && !problem) {
|
|
904
|
+
const verdict = assess(inside, 'exec');
|
|
905
|
+
if (!stapleValid(inside)) problem = 'it has no valid stapled notarization ticket';
|
|
906
|
+
else if (!verdict.ok && !verdict.disabled) problem = `Gatekeeper doesn't accept it as notarized (${oneLine(verdict.text)})`;
|
|
907
|
+
}
|
|
766
908
|
const escaping = execFileSync('find', [inside, '-type', 'l'])
|
|
767
909
|
.toString().split('\n').filter(Boolean)
|
|
768
910
|
.filter((link) => {
|
|
@@ -778,10 +920,24 @@ function checkImage(image) {
|
|
|
778
920
|
console.error(`\n ✗ The app inside ${path.basename(image)} is broken: ${problem}\n`);
|
|
779
921
|
process.exit(1);
|
|
780
922
|
}
|
|
781
|
-
|
|
923
|
+
// And the image itself, as Gatekeeper judges a downloaded one: by its own
|
|
924
|
+
// signature (context:primary-signature) and its own ticket.
|
|
925
|
+
if (NOTARIZE) {
|
|
926
|
+
const verdict = assess(image, 'open');
|
|
927
|
+
if (!stapleValid(image)) fail(`${path.basename(image)} has no valid stapled notarization ticket.`);
|
|
928
|
+
if (!verdict.ok && !verdict.disabled) fail(`Gatekeeper doesn't accept ${path.basename(image)} as notarized: ${oneLine(verdict.text)}`);
|
|
929
|
+
if (verdict.disabled) console.log(gatekeeperOff());
|
|
930
|
+
}
|
|
931
|
+
console.log(` signature verifies, and every link stays inside the app${NOTARIZE ? '; the app and the image are notarized and stapled, and Gatekeeper accepts both' : ''}`);
|
|
782
932
|
}
|
|
783
933
|
|
|
784
934
|
const size = execFileSync('du', ['-h', dmg]).toString().split('\t')[0];
|
|
785
935
|
console.log(`\n✓ ${dmg} (${size})\n`);
|
|
786
|
-
|
|
787
|
-
|
|
936
|
+
if (DEVELOPER_ID) {
|
|
937
|
+
const who = signerName(signatureInfo(APP).authority) || 'this Developer ID';
|
|
938
|
+
if (NOTARIZE) console.log(` Signed by ${who} and notarized by Apple: it opens like any Mac app.\n`);
|
|
939
|
+
else console.log(` Signed by ${who}, not notarized: a copy downloaded in a browser still needs one approval\n in System Settings > Privacy & Security.\n`);
|
|
940
|
+
} else {
|
|
941
|
+
console.log(' Signed ad hoc (no Developer ID here: SIX_DEGREES_SIGN_IDENTITY). A copy downloaded in a browser');
|
|
942
|
+
console.log(' needs one approval in System Settings > Privacy & Security.\n');
|
|
943
|
+
}
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// Menu and Desktop shortcuts. No admin rights asked for.
|
|
15
15
|
// Linux @electron/packager, then a .deb (chrome-sandbox setuid root, which
|
|
16
16
|
// Electron needs on Ubuntu 24.04 and later) and a plain .tar.gz.
|
|
17
|
-
// Unsigned
|
|
18
|
-
// Run anyway" the
|
|
17
|
+
// Unsigned (the Mac app is signed and notarized since D4; Windows signing is
|
|
18
|
+
// D4's other half): Windows shows SmartScreen's "More info → Run anyway" the
|
|
19
|
+
// first time.
|
|
19
20
|
//
|
|
20
21
|
// Built on the platform it is for (release.yml has a job each): pip installs
|
|
21
22
|
// the Python's packages by running that Python. No new npm dependency: packager
|