@stacksjs/defaults 0.75.310 → 0.75.311

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.
@@ -1,7 +1,7 @@
1
1
  // Shared Chrome executable preparation for the standalone skill and testing package.
2
- import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from 'node:fs'
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
4
+ import { basename, join } from 'node:path'
5
5
  import process from 'node:process'
6
6
 
7
7
  const MAC_CHROMES = [
@@ -56,12 +56,33 @@ export interface PreparedChrome {
56
56
  reused: boolean
57
57
  }
58
58
 
59
+ /** Postorder, without following symlinks: nested frameworks must be sealed before their parents. */
60
+ function frameworkVersions(directory: string): { path: string, identifier: string }[] {
61
+ const result: { path: string, identifier: string }[] = []
62
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
63
+ if (!entry.isDirectory()) continue
64
+ const child = join(directory, entry.name)
65
+ result.push(...frameworkVersions(child))
66
+ if (!entry.name.endsWith('.framework') || !existsSync(join(child, 'Versions'))) continue
67
+ const identifier = plist(join(child, 'Resources/Info.plist'), 'Print :CFBundleIdentifier')
68
+ if (!identifier.ok || !identifier.out) throw new Error(`Framework has no signing identifier: ${child}`)
69
+ for (const version of readdirSync(join(child, 'Versions'), { withFileTypes: true }).filter(item => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)))
70
+ result.push({ path: join(child, 'Versions', version.name), identifier: identifier.out })
71
+ }
72
+ return result
73
+ }
74
+
75
+ function validSignature(app: string): boolean {
76
+ return Bun.spawnSync(['codesign', '--verify', '--deep', '--strict', app], { stdout: 'pipe', stderr: 'pipe' }).exitCode === 0
77
+ }
78
+
59
79
  /**
60
80
  * A private clone of a macOS Chrome that GoogleUpdater leaves alone (see the
61
81
  * module docs): `cp -cR`, the `KS*` keys deleted from `Info.plist`, its own
62
- * `CFBundleIdentifier`, `codesign --force --deep --sign -`. Kept in
63
- * `directory` (default a per-version folder in the temp directory) and
64
- * reused while its version matches the source's.
82
+ * `CFBundleIdentifier`, and a complete ad-hoc signature. Every framework
83
+ * version shares an explicit designated requirement, as required for a
84
+ * multi-version bundle (Apple TN2206). Kept in `directory` (default a
85
+ * per-version temp folder) and reused only after strict signature validation.
65
86
  */
66
87
  export function prepareMacChrome(options: { source?: string, directory?: string } = {}): PreparedChrome {
67
88
  if (process.platform !== 'darwin') throw new Error('prepareMacChrome is for macOS; elsewhere launch Chrome as installed')
@@ -73,12 +94,15 @@ export function prepareMacChrome(options: { source?: string, directory?: string
73
94
  const executableName = plist(sourceInfo, 'Print :CFBundleExecutable').out || 'Google Chrome'
74
95
  const directory = options.directory ?? join(tmpdir(), `stacks-chrome-${version.replace(/[^\w.-]/g, '_')}`)
75
96
  const app = join(directory, 'Chrome.app')
76
- const executable = join(app, 'Contents/MacOS', executableName)
77
- const info = join(app, 'Contents/Info.plist')
78
-
79
- if (existsSync(executable) && plist(info, 'Print :CFBundleIdentifier').out === PRIVATE_BUNDLE_ID
80
- && plist(info, 'Print :CFBundleShortVersionString').out === version)
81
- return { executable, app, reused: true }
97
+ const candidates = existsSync(directory) ? readdirSync(directory, { withFileTypes: true })
98
+ .filter(entry => entry.isDirectory() && (entry.name === 'Chrome.app' || (entry.name.startsWith('Chrome-') && entry.name.endsWith('.app'))))
99
+ .map(entry => join(directory, entry.name)).sort() : []
100
+ for (const candidate of candidates) {
101
+ const executable = join(candidate, 'Contents/MacOS', executableName), info = join(candidate, 'Contents/Info.plist')
102
+ if (existsSync(executable) && plist(info, 'Print :CFBundleIdentifier').out === PRIVATE_BUNDLE_ID
103
+ && plist(info, 'Print :CFBundleShortVersionString').out === version && validSignature(candidate))
104
+ return { executable, app: candidate, reused: true }
105
+ }
82
106
 
83
107
  // Build beside the final path and move it in, so a run killed half-way
84
108
  // never leaves a half-signed app that a later run would reuse.
@@ -94,13 +118,26 @@ export function prepareMacChrome(options: { source?: string, directory?: string
94
118
  const stagedInfo = join(staged, 'Contents/Info.plist')
95
119
  for (const key of UPDATER_KEYS) plist(stagedInfo, `Delete :${key}`)
96
120
  run(['/usr/libexec/PlistBuddy', '-c', `Set :CFBundleIdentifier ${PRIVATE_BUNDLE_ID}`, stagedInfo], 'Renaming the Chrome bundle')
97
- run(['codesign', '--force', '--deep', '--sign', '-', staged], 'Re-signing Chrome')
98
- rmSync(app, { recursive: true, force: true })
99
- renameSync(staged, app)
121
+ const versions = frameworkVersions(staged)
122
+ // --deep only signs the current framework version. Prepare the code in
123
+ // every version first, then seal nested frameworks and their parents.
124
+ for (const item of versions) run(['codesign', '--force', '--deep', '--sign', '-', item.path], 'Re-signing framework code')
125
+ for (const item of versions) run(['codesign', '--force', '--sign', '-', '--requirements', `=designated => identifier ${JSON.stringify(item.identifier)}`, item.path], 'Sealing a framework version')
126
+ run(['codesign', '--force', ...(versions.length ? [] : ['--deep']), '--sign', '-', staged], 'Sealing Chrome')
127
+ run(['codesign', '--verify', '--deep', '--strict', staged], 'Verifying Chrome')
128
+ // Never remove a cached bundle that another browser process may still be
129
+ // using. An invalid cache, or a concurrent preparation, gets a new generation.
130
+ let destination = existsSync(app) ? join(directory, `Chrome-${basename(staging)}.app`) : app
131
+ try { renameSync(staged, destination) }
132
+ catch (error) {
133
+ if (destination !== app || !['EEXIST', 'ENOTEMPTY'].includes((error as NodeJS.ErrnoException).code ?? '')) throw error
134
+ destination = join(directory, `Chrome-${basename(staging)}.app`)
135
+ renameSync(staged, destination)
136
+ }
137
+ return { executable: join(destination, 'Contents/MacOS', executableName), app: destination, reused: false }
100
138
  }
101
139
  finally {
102
140
  rmSync(staging, { recursive: true, force: true })
103
141
  }
104
- return { executable, app, reused: false }
105
142
  }
106
143
 
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.310",
5
+ "version": "0.75.311",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.310",
5
+ "version": "0.75.311",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@iconify-json/f7": "^1.2.2",
47
47
  "@iconify-json/hugeicons": "^1.2.35",
48
- "@stacksjs/mobile": "0.75.310",
48
+ "@stacksjs/mobile": "0.75.311",
49
49
  "@stacksjs/sanitizer": "0.2.691",
50
50
  "@stacksjs/stx": "0.2.691",
51
51
  "@xterm/addon-fit": "^0.11.0",