craft-native 0.0.87 → 0.0.88

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.
@@ -32,5 +32,6 @@ targets:
32
32
  TARGETED_DEVICE_FAMILY: "{{DEVICE_FAMILIES}}"
33
33
  SWIFT_VERSION: "5.0"
34
34
  ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
35
+ {{CRAFT_RUNTIME_SETTINGS}}
35
36
  {{NATIVE_DEPENDENCIES}}
36
37
  {{NATIVE_TARGETS}}
@@ -3,6 +3,7 @@
3
3
  * Automatic updates with delta/differential update support
4
4
  */
5
5
  import { EventEmitter } from 'events';
6
+ import type { BundleTrustPolicy } from './macos-bundle.js';
6
7
  export interface UpdateInfo {
7
8
  version: string;
8
9
  releaseDate: string;
@@ -52,6 +53,37 @@ export interface UpdaterConfig {
52
53
  * Must match how the build pipeline produced `PlatformUpdate.signature`.
53
54
  */
54
55
  signatureAlgorithm?: 'ed25519' | 'rsa-sha256';
56
+ /**
57
+ * What macOS itself has to say about the bundle before it is installed.
58
+ *
59
+ * This is a different question from the manifest's SHA-256, and a stronger
60
+ * one. The hash proves the download matches what the manifest asked for;
61
+ * whoever can rewrite the manifest can rewrite the hash with it. `codesign`
62
+ * and `spctl` ask whether Apple and *your* Developer ID account vouch for
63
+ * these bytes, which nobody can forge by controlling a JSON file.
64
+ *
65
+ * Pin `teamId` to your team. An updater that only checks notarization
66
+ * accepts a bundle from any Apple developer account in the world.
67
+ *
68
+ * Ignored off macOS.
69
+ */
70
+ /**
71
+ * Where to keep the downloaded archive until it is installed.
72
+ *
73
+ * Defaults to a per-app directory under the OS temp directory. Set it to
74
+ * keep partial downloads somewhere that survives a reboot.
75
+ */
76
+ downloadDir?: string;
77
+ macos?: BundleTrustPolicy;
78
+ /**
79
+ * How to start the new copy after an install, replacing `open -n <app>`.
80
+ *
81
+ * Needed wherever the process running the updater is not the process the
82
+ * user launched — an agent behind a window, a helper started by a launcher.
83
+ * Killing that one and opening the bundle relaunches a child while the real
84
+ * app carries on, so the host has to say what "restart" means for it.
85
+ */
86
+ relaunch?: (appPath: string) => void | Promise<void>;
55
87
  }
56
88
  export interface UpdateProgress {
57
89
  phase: 'checking' | 'downloading' | 'extracting' | 'installing' | 'done' | 'error';
@@ -60,7 +92,7 @@ export interface UpdateProgress {
60
92
  bytesTotal?: number;
61
93
  speed?: number;
62
94
  }
63
- export type UpdaterEvent = 'checking-for-update' | 'update-available' | 'update-not-available' | 'download-progress' | 'update-downloaded' | 'before-quit-for-update' | 'error';
95
+ export type UpdaterEvent = 'checking-for-update' | 'update-available' | 'update-not-available' | 'download-progress' | 'update-downloaded' | 'update-installed' | 'before-quit-for-update' | 'error';
64
96
  export declare class AutoUpdater extends EventEmitter {
65
97
  private config;
66
98
  private updateInfo;
@@ -69,7 +101,30 @@ export declare class AutoUpdater extends EventEmitter {
69
101
  private cachedEtag;
70
102
  private cachedLastModified;
71
103
  private cachedManifest;
104
+ private lastError;
72
105
  private emitError;
106
+ /**
107
+ * Where the downloaded archive is kept until it is installed.
108
+ *
109
+ * The OS temp directory, not a hidden folder beside the app. The original
110
+ * `<appPath>/../.craft-updates` put a 40 MB disk image inside the user's
111
+ * Applications folder, and left the directory behind afterwards; it bought
112
+ * nothing, because the bundle is unpacked to a staging directory before the
113
+ * swap anyway, so the download never has to be on the destination volume.
114
+ */
115
+ private downloadDir;
116
+ /**
117
+ * The failure from the most recent check or download, or null.
118
+ *
119
+ * `checkForUpdates` returns null for two very different outcomes — there is
120
+ * no newer version, and we could not find out. A caller that cannot tell
121
+ * them apart reports "you are up to date" to a user whose network is down,
122
+ * which is the one wrong answer an update check can give: it is confident,
123
+ * it is false, and it stops them looking further.
124
+ *
125
+ * Cleared at the start of each attempt, so it always describes the last one.
126
+ */
127
+ getLastError(): Error | null;
73
128
  constructor(config: UpdaterConfig);
74
129
  /**
75
130
  * Start automatic update checking
@@ -99,9 +154,40 @@ export declare class AutoUpdater extends EventEmitter {
99
154
  * into a CVE.
100
155
  */
101
156
  installUpdate(restartAfter?: boolean): Promise<void>;
157
+ /**
158
+ * Replace the installed bundle on macOS.
159
+ *
160
+ * The order is the point. Unpack to a staging directory, ask macOS whether
161
+ * it trusts what came out, and only then touch the app the user launches —
162
+ * so a bundle that fails verification costs a download and nothing else.
163
+ *
164
+ * The previous implementation deleted the installed app and then copied the
165
+ * new one over its path with `fs.cpSync`. That is two separate faults: an
166
+ * interruption anywhere in the copy leaves no app at all, and `cpSync` does
167
+ * not preserve the extended attributes a code signature covers, so even a
168
+ * clean run produced a bundle that failed `codesign --verify`.
169
+ */
102
170
  private installMacOSUpdate;
171
+ /**
172
+ * Verify a staged bundle against the configured macOS trust policy.
173
+ *
174
+ * Throws rather than returning a verdict: every caller's only sensible
175
+ * response to "macOS does not trust this" is to stop, and a boolean invites
176
+ * a caller that forgets to check it.
177
+ */
178
+ private assertBundleTrusted;
179
+ private installMacOSPackage;
103
180
  private installWindowsUpdate;
104
181
  private installLinuxUpdate;
182
+ /**
183
+ * Start the updated copy and stand down.
184
+ *
185
+ * `config.relaunch`, when given, replaces both halves — the spawn and the
186
+ * exit. That matters for any app whose updater does not run in the process
187
+ * the user launched: an agent behind a webview that calls `process.exit(0)`
188
+ * takes down a child, leaves the window open on a dead server, and never
189
+ * relaunches anything.
190
+ */
105
191
  private restartApp;
106
192
  private getPlatform;
107
193
  private isNewerVersion;
@@ -123,6 +209,16 @@ export declare class AutoUpdater extends EventEmitter {
123
209
  getDownloadPath(): string | null;
124
210
  }
125
211
  export declare class DeltaGenerator {
212
+ /** Cached because it shells out and the answer cannot change mid-process. */
213
+ private static supported;
214
+ /**
215
+ * Whether this machine has a binary-diff tool to apply a patch with.
216
+ *
217
+ * Deltas are an optimisation, and an optimisation that throws is worse than
218
+ * no optimisation. Neither `bspatch` nor `xdelta3` ships with macOS or a
219
+ * default Linux install, so the honest default is "no" and the full bundle.
220
+ */
221
+ static isSupported(): boolean;
126
222
  /**
127
223
  * Generate a delta/patch file between two versions
128
224
  */
@@ -161,4 +257,11 @@ export declare function generateUpdateManifest(options: {
161
257
  }[];
162
258
  }): UpdateInfo;
163
259
  export declare function updaterCommand(args: string[]): Promise<void>;
260
+ /**
261
+ * macOS bundle mechanics, re-exported so an app can verify or swap a bundle
262
+ * without also adopting the whole update pipeline — installers, CI checks and
263
+ * "am I running the build I think I am?" all want the same primitives.
264
+ */
265
+ export { canReplaceBundle, clearQuarantine, dittoBundle, extractBundle, extractBundleFromDmg, extractBundleFromZip, isMacOS, readBundleIdentity, swapBundle, verifyBundleTrust, } from './macos-bundle.js';
266
+ export type { BundleIdentity, BundleTrustFailure, BundleTrustPolicy, BundleTrustResult, StagedBundle, SwapResult, } from './macos-bundle.js';
164
267
  export default AutoUpdater;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * macOS application bundle mechanics for the updater.
3
+ *
4
+ * Replacing a running `.app` is not a file copy. Three things make it its own
5
+ * problem, and every one of them has a wrong answer that looks like it works:
6
+ *
7
+ * **Trust.** A downloaded bundle is only safe to run if macOS itself says
8
+ * so. A SHA-256 from the manifest proves the bytes match what the manifest
9
+ * claimed — it says nothing about who published them, because whoever
10
+ * controls the manifest controls both numbers. `codesign` and `spctl` are
11
+ * the checks Gatekeeper performs on first launch; doing them *before* the
12
+ * swap turns "the user gets a scary dialog after we already deleted their
13
+ * app" into "we declined to install it."
14
+ *
15
+ * **Copying.** `cp -r` — and `fs.cpSync` — drop extended attributes and
16
+ * ACLs, which is enough to invalidate a code signature. `ditto` is the only
17
+ * copy on macOS that preserves everything a signed bundle needs.
18
+ *
19
+ * **Atomicity.** Deleting the installed app and then copying the new one in
20
+ * leaves the user with no app at all if anything fails in between: a full
21
+ * disk, a kernel panic, a killed process. Two renames within one directory
22
+ * are the closest thing POSIX offers to an exchange, and they leave the old
23
+ * bundle intact until the new one is in place.
24
+ */
25
+ /** What a bundle's code signature says about who produced it. */
26
+ export interface BundleIdentity {
27
+ /** Bundle identifier, e.g. `org.stacksjs.system-cleaner`. */
28
+ identifier: string | null;
29
+ /** Apple Developer Team ID, e.g. `3JJRNQW6B7`. */
30
+ teamId: string | null;
31
+ /** Leaf signing authority, e.g. `Developer ID Application: Jane Doe (ABCDE12345)`. */
32
+ authority: string | null;
33
+ }
34
+ export interface BundleTrustPolicy {
35
+ /**
36
+ * Team ID the bundle must be signed by.
37
+ *
38
+ * This is the check that matters. Notarization proves Apple scanned the
39
+ * bundle; it does not prove *you* published it, and any Developer ID
40
+ * account can get a build notarized. Pinning the team turns "signed by
41
+ * someone" into "signed by us".
42
+ */
43
+ teamId?: string;
44
+ /**
45
+ * Require Gatekeeper to accept the bundle outright. Defaults to true.
46
+ *
47
+ * Turn it off only where the assessment cannot succeed by construction —
48
+ * a locally built, ad-hoc signed bundle under test.
49
+ */
50
+ requireNotarized?: boolean;
51
+ }
52
+ export type BundleTrustFailure = 'unreadable' | 'codesign-invalid' | 'gatekeeper-rejected' | 'team-mismatch';
53
+ export interface BundleTrustResult {
54
+ ok: boolean;
55
+ identity: BundleIdentity;
56
+ /** Gatekeeper's verdict line, e.g. `source=Notarized Developer ID`. */
57
+ gatekeeperSource: string | null;
58
+ reason?: BundleTrustFailure;
59
+ detail?: string;
60
+ }
61
+ /** True on macOS, where every function in this module is meaningful. */
62
+ export declare function isMacOS(): boolean;
63
+ /**
64
+ * Read the code signature's own account of a bundle.
65
+ *
66
+ * `codesign -dv` writes its report to stderr, not stdout — an easy detail to
67
+ * get wrong, and the failure mode is a parser that silently matches nothing
68
+ * and reports every field as null.
69
+ */
70
+ export declare function readBundleIdentity(appPath: string): Promise<BundleIdentity>;
71
+ /**
72
+ * Decide whether a bundle is safe to install, using the checks macOS itself
73
+ * would run at launch.
74
+ *
75
+ * Order matters. The signature has to be structurally valid before its claims
76
+ * about a team mean anything, and Gatekeeper's assessment subsumes the
77
+ * signature check but reports a coarser reason — so run `codesign` first and
78
+ * let it produce the specific complaint.
79
+ */
80
+ export declare function verifyBundleTrust(appPath: string, policy?: BundleTrustPolicy): Promise<BundleTrustResult>;
81
+ /**
82
+ * Drop the quarantine flag a download carries.
83
+ *
84
+ * Only ever call this on a bundle that has already passed
85
+ * `verifyBundleTrust` — the flag exists so Gatekeeper gets a chance to run,
86
+ * and clearing it beforehand is how an updater becomes a way to install
87
+ * anything at all. Afterwards it is redundant: the assessment it would have
88
+ * triggered has already happened, and leaving it set makes the app the user
89
+ * just updated ask permission to open.
90
+ */
91
+ export declare function clearQuarantine(appPath: string): Promise<void>;
92
+ /**
93
+ * Copy a bundle the way macOS expects, preserving what the signature covers.
94
+ *
95
+ * `ditto` rather than `cp -R` or `fs.cpSync`: both of those drop extended
96
+ * attributes and ACLs, and a bundle missing them fails `codesign --verify`
97
+ * even though every byte of every file is identical.
98
+ */
99
+ export declare function dittoBundle(source: string, destination: string): Promise<void>;
100
+ /** Where an extracted bundle ended up, and how to clean up after it. */
101
+ export interface StagedBundle {
102
+ /** Path to the `.app` inside the staging directory. */
103
+ appPath: string;
104
+ /** Directory holding it. Remove this when done. */
105
+ stagingDir: string;
106
+ }
107
+ /**
108
+ * Mount a disk image, copy the app out, unmount.
109
+ *
110
+ * The mount point is one we create rather than one we parse out of
111
+ * `hdiutil`'s output. Reading it back invites two bugs that only appear in
112
+ * the field: a volume name with a newline in it, and a second copy of the
113
+ * same image already mounted at `/Volumes/Name 1`.
114
+ */
115
+ export declare function extractBundleFromDmg(dmgPath: string, stagingDir?: string): Promise<StagedBundle>;
116
+ /**
117
+ * Unpack a zipped bundle.
118
+ *
119
+ * `ditto -x -k` rather than `unzip`: it is the counterpart of the `ditto -c
120
+ * -k` that produces these archives, and it is the only unzip on macOS that
121
+ * restores the extended attributes a code signature is computed over.
122
+ */
123
+ export declare function extractBundleFromZip(zipPath: string, stagingDir?: string): Promise<StagedBundle>;
124
+ /** Extract whichever container the download turned out to be. */
125
+ export declare function extractBundle(archivePath: string, stagingDir?: string): Promise<StagedBundle>;
126
+ export interface SwapResult {
127
+ /** Where the replaced bundle was moved to, if it is still there. */
128
+ previousPath: string | null;
129
+ }
130
+ /**
131
+ * Put `stagedPath` where `installedPath` is, without ever leaving that path
132
+ * empty for longer than a rename takes.
133
+ *
134
+ * The sequence is: get the new bundle onto the destination's own volume,
135
+ * rename the old one aside, rename the new one in, then delete the old one.
136
+ * Only the middle two steps touch the path the user launches, they are both
137
+ * renames within one directory, and if the second fails the first is undone.
138
+ *
139
+ * `rename` is what buys that. It is atomic within a filesystem, so there is no
140
+ * moment where a half-copied bundle sits at the app's path — which is exactly
141
+ * what `rm -rf app && cp -R new app` produces when it is interrupted.
142
+ */
143
+ export declare function swapBundle(stagedPath: string, installedPath: string): Promise<SwapResult>;
144
+ /**
145
+ * Whether the process can replace a bundle at this path without asking for
146
+ * privileges.
147
+ *
148
+ * The swap is a rename *in the parent directory*, so what has to be writable
149
+ * is `/Applications`, not the bundle. An app installed under `~/Applications`
150
+ * is always replaceable; one in `/Applications` usually is too, because the
151
+ * installer that put it there left it owned by the installing user.
152
+ */
153
+ export declare function canReplaceBundle(installedPath: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.87",
3
+ "version": "0.0.88",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
@@ -81,7 +81,7 @@
81
81
  "scripts": {
82
82
  "build": "bun build.ts && bun run build:types",
83
83
  "build:lib": "bun build src/index.ts --outdir dist --format esm --target bun && bun build src/index.ts --outfile dist/index.cjs --format cjs --target node",
84
- "build:types": "bun run tsc -p tsconfig.build.json && cp dist/index.d.ts dist/index.d.cts",
84
+ "build:types": "bun scripts/tsc.ts -p tsconfig.build.json && cp dist/index.d.ts dist/index.d.cts",
85
85
  "dev": "bun --watch src/index.ts",
86
86
  "lint": "bunx --bun pickier lint . --config ../../pickier.config.ts --max-warnings 9999",
87
87
  "lint:fix": "bunx --bun pickier lint . --fix --config ../../pickier.config.ts --max-warnings 9999",