craft-native 0.0.86 → 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.
- package/README.md +71 -0
- package/dist/api/window.d.ts +14 -1
- package/dist/cli.js +75 -3
- package/dist/index.cjs +386 -102
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +374 -90
- package/dist/ios/src/index.d.ts +74 -0
- package/dist/ios/src/index.js +73 -1
- package/dist/ios/templates/CraftApp.swift +510 -112
- package/dist/ios/templates/project.yml.template +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/updater/index.d.ts +104 -1
- package/dist/updater/macos-bundle.d.ts +153 -0
- package/package.json +2 -2
package/dist/types.d.ts
CHANGED
|
@@ -201,9 +201,31 @@ export interface WindowOptions {
|
|
|
201
201
|
/**
|
|
202
202
|
* White/dark native tint opacity over the sidebar material.
|
|
203
203
|
* Higher values reduce desktop bleed-through while keeping subtle vibrancy.
|
|
204
|
+
*
|
|
205
|
+
* `webSidebarMaterial` only — a `webWindowMaterial` window carries no native
|
|
206
|
+
* tint, because the page is the only thing that knows its colour scheme.
|
|
204
207
|
* @default 0.78
|
|
205
208
|
*/
|
|
206
209
|
webSidebarMaterialOpacity?: number;
|
|
210
|
+
/**
|
|
211
|
+
* Draw that same material behind the *whole* web view rather than a leading
|
|
212
|
+
* strip, with nothing opaque beside it.
|
|
213
|
+
*
|
|
214
|
+
* Where `webSidebarMaterial` is Finder — vibrancy under the sidebar, a solid
|
|
215
|
+
* pane beside it — this is System Settings: one material behind everything,
|
|
216
|
+
* and the page's own cards are the only things with edges.
|
|
217
|
+
*
|
|
218
|
+
* Two differences follow, and both are the point rather than side effects:
|
|
219
|
+
* the window is not pinned to light (so `darkMode` and the system setting
|
|
220
|
+
* reach the material, and the page's `prefers-color-scheme` agrees with it),
|
|
221
|
+
* and no native tint is drawn (the page washes the material with its own
|
|
222
|
+
* translucent background, which is the only wash that knows whether the page
|
|
223
|
+
* is light or dark).
|
|
224
|
+
*
|
|
225
|
+
* Wins over `webSidebarMaterial` when both are set.
|
|
226
|
+
* @default false
|
|
227
|
+
*/
|
|
228
|
+
webWindowMaterial?: boolean;
|
|
207
229
|
/**
|
|
208
230
|
* Use native macOS sidebar (Finder-style with vibrancy)
|
|
209
231
|
* Creates a split view with NSOutlineView sidebar and WebView content
|
package/dist/updater/index.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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",
|