@gjsify/cli 0.64.20 → 0.64.21

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.
@@ -35,6 +35,7 @@ import { describeExit, spawnToCompletion } from '../utils/spawn.js';
35
35
  import { buildDeb } from '../utils/ship/deb.js';
36
36
  import { deriveDepends, warnAboutGjsFloor, warnAboutNodeFloor } from '../utils/ship/depends.js';
37
37
  import { isGtkRuntimeTarget, stageAppRuntime } from '../utils/ship/app-runtime.js';
38
+ import { recordedNestedBundles, signedSubordinatesFor, stageSubordinates } from '../utils/ship/nested-bundles.js';
38
39
  import { resolveNodeModulesClosure, stageNodeModules } from '../utils/ship/node-modules.js';
39
40
  import { acquireGtkRuntime } from '../utils/ship/gtk-runtime-acquire.js';
40
41
  import { discoverPayload } from '../utils/ship/discover.js';
@@ -419,6 +420,17 @@ async function assemble(args) {
419
420
  // distribution update ever patches.
420
421
  const runtime = await resolveCarriedRuntime(layout, settings, projectDir);
421
422
  carryNodeModules(runtime, layout, settings, projectDir, ship.nodeModules, ship.nodeModulesExclude);
423
+ // The nested bundles and helper executables ADR 0026 § 6 needed by hand —
424
+ // staged like the runtime above (tree-preserving, stage-relative, through
425
+ // `placeStage`'s `carried` seam), and never through `gjsify.ship.bundledTypelibs`'s
426
+ // flattening map for the reason `app-runtime.ts`'s header gives: a `.appex`'s
427
+ // internal relations survive only if its tree is copied as a tree.
428
+ const subordinates = stageSubordinates({
429
+ layout,
430
+ identity: settings,
431
+ nestedBundles: settings.nestedBundles,
432
+ helperExecutables: settings.helperExecutables,
433
+ });
422
434
  const stageInputs = {
423
435
  bundleFiles: discovered.bundleFiles,
424
436
  launcher: renderLauncher(settings, basename(settings.bundlePath), layout, runtime.launcher),
@@ -489,7 +501,7 @@ async function assemble(args) {
489
501
  : undefined;
490
502
  if (icon !== undefined)
491
503
  console.log(`${LOG} icon: ${iconSizes(icon).join('/')} px, ${icon.source}`);
492
- const staged = placeStage(layout, { ...settings, icon }, planned, runtime.files);
504
+ const staged = placeStage(layout, { ...settings, icon }, planned, [...runtime.files, ...subordinates.files]);
493
505
  writeStage(stageDir, staged);
494
506
  console.log(`${LOG} staged ${staged.length} file(s) for ${layout.name} in ${relative(projectDir, stageDir)}/`);
495
507
  if (args.verbose)
@@ -1078,6 +1090,12 @@ async function packOne(input) {
1078
1090
  // `signed/<id>/` is scratch this function owns. The arriving stage is never
1079
1091
  // written to: `writeStage` wipes what it is pointed at, and a re-signed file
1080
1092
  // in it would fail the very `readStage` above on the next run.
1093
+ // DERIVED, not carried: the stage-relative path of a nested bundle or helper
1094
+ // executable is `layout.dirs(settings).plugins`/`.helpers` joined with its
1095
+ // name, computable from `settings` alone on either host — see
1096
+ // `nested-bundles.ts`'s `signedSubordinatesFor` header for why that is what
1097
+ // keeps this call and `stageSubordinates` from disagreeing about a path.
1098
+ const subordinates = signedSubordinatesFor(layout, settings, settings);
1081
1099
  const payload = input.sign.kind === 'sign'
1082
1100
  ? await signPayload({
1083
1101
  payload: assembled,
@@ -1091,6 +1109,8 @@ async function packOne(input) {
1091
1109
  // with the payload, so an entitlements plist written into it would
1092
1110
  // read as a file `codesign` added.
1093
1111
  entitlementsPath: join(outRoot, 'signed', `${format.id}.entitlements.plist`),
1112
+ nestedBundles: subordinates.nestedBundles,
1113
+ helperExecutables: subordinates.helperExecutables,
1094
1114
  verbose: input.verbose,
1095
1115
  log: (line) => console.log(`${LOG} ${line}`),
1096
1116
  })
@@ -1429,6 +1449,7 @@ async function packOne(input) {
1429
1449
  signature: recordedSignature(input.sign.kind === 'sign' ? input.sign.identity : undefined, ADHOC_IDENTITY),
1430
1450
  notarized,
1431
1451
  stapled,
1452
+ nestedBundles: recordedNestedBundles(payload, subordinates.nestedBundles),
1432
1453
  generator: `@gjsify/cli ${cliVersion()}`,
1433
1454
  });
1434
1455
  if (input.verbose)
@@ -753,6 +753,51 @@ export interface ConfigDataShip extends AppMetadata {
753
753
  * is the default path and a legitimate output (ADR 0024 § A13).
754
754
  */
755
755
  sign?: ShipSignOptions;
756
+ /**
757
+ * Further BUNDLES this package's darwin container embeds — e.g. a Safari Web
758
+ * Extension `.appex` built by `xcrun safari-web-extension-packager`. Each is
759
+ * copied wholesale into `Contents/PlugIns/` and signed+sealed as its OWN
760
+ * bundle, with its OWN entitlements, before the outer `.app` is sealed
761
+ * (ADR 0024 § A12-§ A17; the beifahrer ADR this answers: 0026 § 6).
762
+ *
763
+ * `Contents/PlugIns` is Apple's own nested-code location (`codesign(1)`'s
764
+ * bundle format, beside `Frameworks`/`Helpers`/`XPCServices`); no other
765
+ * layout has an equivalent slot, so this is refused outside
766
+ * `gjsify ship darwin`.
767
+ */
768
+ nestedBundles?: ShipNestedBundleOptions[];
769
+ /**
770
+ * Prebuilt helper EXECUTABLES this package carries beside the carried
771
+ * interpreter, in `Contents/Helpers/` — e.g. a small Swift binary with no GI
772
+ * binding (`SafariServices` has none), execed via `Gio.Subprocess`. Signed
773
+ * with its OWN entitlements before the outer `.app` is sealed. Darwin only,
774
+ * for the same reason as {@link ConfigDataShip.nestedBundles}.
775
+ */
776
+ helperExecutables?: ShipHelperExecutableOptions[];
777
+ }
778
+ /** One entry of `gjsify.ship.nestedBundles`. */
779
+ export interface ShipNestedBundleOptions {
780
+ /** Project-relative directory holding the built bundle (e.g. the `.appex`). */
781
+ source: string;
782
+ /** Destination leaf under `Contents/PlugIns/` — keep the source's own extension. */
783
+ name: string;
784
+ /**
785
+ * Entitlements this bundle is signed and sealed with, REPLACING the outer
786
+ * signer's own set rather than adding to it: a sandboxed extension keeps the
787
+ * sandbox entitlements its packager wrote (`com.apple.security.app-sandbox`,
788
+ * file access) and must not inherit the host app's JIT/dyld-environment
789
+ * grants (ADR 0026 § 5). Absent or `[]` signs with none.
790
+ */
791
+ entitlements?: string[];
792
+ }
793
+ /** One entry of `gjsify.ship.helperExecutables`. */
794
+ export interface ShipHelperExecutableOptions {
795
+ /** Project-relative path of the prebuilt executable. */
796
+ source: string;
797
+ /** Destination leaf under `Contents/Helpers/`. */
798
+ name: string;
799
+ /** Entitlements this helper is signed with. Absent or `[]` signs with none. */
800
+ entitlements?: string[];
756
801
  }
757
802
  /** One entry of `gjsify.ship.executables`. */
758
803
  export interface ShipExecutableOptions {
@@ -233,3 +233,24 @@ export interface StageAppRuntimeInput {
233
233
  * under `gtk/` on the other side of the copy.
234
234
  */
235
235
  export declare function stageAppRuntime(input: StageAppRuntimeInput): StagedAppRuntime;
236
+ /**
237
+ * The mode one staged closure file gets.
238
+ *
239
+ * THREE tests, and each covers a case the others do not:
240
+ *
241
+ * * the source's own execute bit — the truth when the closure came off a real
242
+ * filesystem, and the ONLY thing that knows `libexec/gstreamer-1.0/gst-plugin-scanner`
243
+ * is a program (it has no extension, and GStreamer forks it out-of-process so a
244
+ * plugin that crashes on load cannot take the app down with it);
245
+ * * the shared-library spelling, shared with `plan.ts` rather than restated —
246
+ * load-bearing because `actions/upload-artifact` stores NO POSIX mode, so a
247
+ * closure that arrived through a CI artifact has 0644 on every file and the
248
+ * source-mode test above is blind on exactly the path this milestone's own CI
249
+ * leg takes;
250
+ * * `libexec/` and `bin/`, for the same reason applied to the extensionless
251
+ * programs the artifact round trip flattened.
252
+ *
253
+ * A dylib does not need the execute bit to be `dlopen`'d, so this is not about
254
+ * making the closure loadable — it is about the two files that are EXEC'd.
255
+ */
256
+ export declare function stagedMode(rel: string, abs: string): number;
@@ -445,7 +445,7 @@ function discoveryOptions(input) {
445
445
  * A dylib does not need the execute bit to be `dlopen`'d, so this is not about
446
446
  * making the closure loadable — it is about the two files that are EXEC'd.
447
447
  */
448
- function stagedMode(rel, abs) {
448
+ export function stagedMode(rel, abs) {
449
449
  if (isExecutableAsset(rel))
450
450
  return 0o755;
451
451
  if (rel.startsWith('libexec/') || rel.startsWith('bin/'))
@@ -145,6 +145,17 @@ export interface LayoutDirs {
145
145
  * first place, so its interpreter stays beside the `.cmd`.
146
146
  */
147
147
  helpers: string;
148
+ /**
149
+ * macOS only: where a NESTED BUNDLE lives — `Contents/PlugIns`, Apple's own
150
+ * slot for a `.appex` (`codesign(1)`'s bundle format, beside `Frameworks`,
151
+ * `Helpers`, `XPCServices`). `''` on linux and windows, where neither a
152
+ * prefix nor a program directory has an equivalent: a Flatpak sandbox has no
153
+ * App Extension model and a Windows program directory has nothing Apple's
154
+ * extension points name. `gjsify.ship.nestedBundles` is refused outright on
155
+ * any layout where this is `''` (ADR 0024 amendment, "nested bundles and
156
+ * helper executables"; the beifahrer ADR this answers: 0026 § 6).
157
+ */
158
+ plugins: string;
148
159
  }
149
160
  /**
150
161
  * One operating system's layout.
@@ -140,6 +140,7 @@ export const LAYOUTS = {
140
140
  data: 'share',
141
141
  other: '',
142
142
  helpers: '',
143
+ plugins: '',
143
144
  }),
144
145
  // THE RASTERS THE BUILD-TIME READERS OF THE THEME NEED, and the one row
145
146
  // whose icon is written as ordinary payload files rather than into a
@@ -319,6 +320,8 @@ export const LAYOUTS = {
319
320
  // file there named `<BinaryName>` collides with it bytewise even
320
321
  // when the casing differs.
321
322
  helpers: `${app}/Contents/Helpers`,
323
+ // Apple's own slot for a nested `.appex` — see `LayoutDirs.plugins`.
324
+ plugins: `${app}/Contents/PlugIns`,
322
325
  };
323
326
  },
324
327
  // The two files that make the directory a BUNDLE. Without `Info.plist`
@@ -450,7 +453,15 @@ export const LAYOUTS = {
450
453
  // LOADER: `lib/` is what the launcher prepends to `PATH`, because Windows
451
454
  // has no rpath — a DLL is found on `PATH`, in the directory of the image
452
455
  // that loaded it, or not at all.
453
- dirs: () => ({ launcher: '', bundle: 'app', native: 'lib', data: 'share', other: '', helpers: '' }),
456
+ dirs: () => ({
457
+ launcher: '',
458
+ bundle: 'app',
459
+ native: 'lib',
460
+ data: 'share',
461
+ other: '',
462
+ helpers: '',
463
+ plugins: '',
464
+ }),
454
465
  // NO METADATA, and one PROGRAM. A Windows installer's metadata lives in the
455
466
  // `.msi`'s own tables, not in a file inside the program directory —
456
467
  // `msi.ts` puts every such fact in the `Product`/`Package` attributes of the
@@ -0,0 +1,60 @@
1
+ import type { Layout, LayoutIdentity } from './layout.js';
2
+ import type { PayloadEntry } from './payload.js';
3
+ import type { RecordedNestedBundle } from './release-record.js';
4
+ import type { SignedSubordinate } from './signing.js';
5
+ import type { ShipHelperExecutable, ShipHelperExecutableFacts, ShipNestedBundle, ShipNestedBundleFacts, StagedFile } from './types.js';
6
+ /** The staged files, and what `signPayload` needs to sign them with their OWN entitlements. */
7
+ export interface StagedSubordinates {
8
+ files: StagedFile[];
9
+ nestedBundles: SignedSubordinate[];
10
+ helperExecutables: SignedSubordinate[];
11
+ }
12
+ /**
13
+ * Stage every `gjsify.ship.nestedBundles` entry and `gjsify.ship.helperExecutables`
14
+ * entry into the layout's `plugins`/`helpers` slots.
15
+ *
16
+ * `[]` for both inputs is the common case and produces `{ files: [], ... }` —
17
+ * cheap enough that `commands/ship.ts` can call this unconditionally rather than
18
+ * branching on whether a project declares either key.
19
+ */
20
+ export declare function stageSubordinates(input: {
21
+ layout: Layout;
22
+ identity: LayoutIdentity;
23
+ nestedBundles: readonly ShipNestedBundle[];
24
+ helperExecutables: readonly ShipHelperExecutable[];
25
+ }): StagedSubordinates;
26
+ /**
27
+ * What `signPayload` needs to sign the subordinates `stageSubordinates` staged —
28
+ * DERIVED from the layout and the facts alone, never carried as a separate
29
+ * closure.
30
+ *
31
+ * This is what lets `PackSettings` carry only `{ name, entitlements }` (see
32
+ * `types.ts`): the stage-relative PATH is `layout.dirs(settings).plugins` (or
33
+ * `.helpers`) joined with the name, which both the assembling host (which also
34
+ * has the absolute `source`) and a `--from-stage` finishing host (which has
35
+ * neither) can compute from data they already hold. Calling this twice — once
36
+ * in `assemble()`, once in `packOne` — can therefore never disagree with where
37
+ * {@link stageSubordinates} put the bytes.
38
+ */
39
+ export declare function signedSubordinatesFor(layout: Layout, identity: LayoutIdentity, facts: {
40
+ nestedBundles: readonly ShipNestedBundleFacts[];
41
+ helperExecutables: readonly ShipHelperExecutableFacts[];
42
+ }): {
43
+ nestedBundles: SignedSubordinate[];
44
+ helperExecutables: SignedSubordinate[];
45
+ };
46
+ /**
47
+ * What the release record says about each nested bundle, read off its OWN
48
+ * `Info.plist` rather than off `gjsify.ship.nestedBundles` — the declaration
49
+ * names a `source` directory and an entitlements list, neither of which is the
50
+ * bundle identifier or its extension point, and both of THOSE only exist inside
51
+ * the `.appex` the project built.
52
+ *
53
+ * `extensionPoint` comes from `NSExtension.NSExtensionPointIdentifier`, Apple's
54
+ * own nesting for every extension point (`com.apple.Safari.web-extension` is
55
+ * one value among several, not a special case this reader has to know). A
56
+ * nested bundle that is not an extension at all — or whose `Info.plist` this
57
+ * reader cannot find or parse — simply omits the key, which is the same
58
+ * additive contract the field's own doc states (`release-record.ts`).
59
+ */
60
+ export declare function recordedNestedBundles(payload: readonly PayloadEntry[], nestedBundles: readonly SignedSubordinate[]): RecordedNestedBundle[];
@@ -0,0 +1,140 @@
1
+ // Staging darwin's nested bundles and helper executables into the payload
2
+ // (ADR 0024 amendment, "nested bundles and helper executables"; the beifahrer
3
+ // ADR this answers: 0026 § 6).
4
+ //
5
+ // A consumer whose Xcode-built shell was discarded in favour of ADR 0026's "one
6
+ // app" decision still has ONE bundle `gjsify ship darwin` cannot carry on its
7
+ // own: a Safari Web Extension `.appex`, which macOS registers only when it
8
+ // sits under the PARENT app's `Contents/PlugIns/`. This module is the staging
9
+ // half of that gap — copy the built `.appex` (or any other nested bundle) and
10
+ // any prebuilt helper executable into the layout's own slots for them
11
+ // (`Layout.dirs().plugins` / `.helpers`), tree-preserving for the former and
12
+ // flat for the latter, because a bundle is a DIRECTORY and a helper is a FILE.
13
+ //
14
+ // SIGNING is a separate concern, in `signing.ts`: this module only decides
15
+ // WHERE the bytes land, never how they are signed. `stagedSubordinates` hands
16
+ // back the stage-relative paths `commands/ship.ts` passes to `signPayload` as
17
+ // `nestedBundles`/`helperExecutables`, so the two modules cannot drift on what
18
+ // a "nested bundle" or a "helper executable" even IS.
19
+ import { join, posix } from 'node:path';
20
+ import { listFilesRecursive } from './discover.js';
21
+ import { stagedMode } from './app-runtime.js';
22
+ /**
23
+ * Stage every `gjsify.ship.nestedBundles` entry and `gjsify.ship.helperExecutables`
24
+ * entry into the layout's `plugins`/`helpers` slots.
25
+ *
26
+ * `[]` for both inputs is the common case and produces `{ files: [], ... }` —
27
+ * cheap enough that `commands/ship.ts` can call this unconditionally rather than
28
+ * branching on whether a project declares either key.
29
+ */
30
+ export function stageSubordinates(input) {
31
+ const dirs = input.layout.dirs(input.identity);
32
+ const files = [];
33
+ const nestedBundles = [];
34
+ const helperExecutables = [];
35
+ for (const bundle of input.nestedBundles) {
36
+ if (dirs.plugins === '') {
37
+ throw new Error(`gjsify ship: the ${input.layout.name} layout has no \`Contents/PlugIns\` equivalent, so ` +
38
+ `the nested bundle "${bundle.name}" has nowhere to go. ` +
39
+ '`gjsify.ship.nestedBundles` is darwin-only; `resolveShipSettings` should have refused ' +
40
+ 'this already — report this as a gjsify bug if it did not.');
41
+ }
42
+ const bundleRoot = posix.join(dirs.plugins, bundle.name);
43
+ for (const rel of listFilesRecursive(bundle.source)) {
44
+ const abs = join(bundle.source, ...rel.split(posix.sep));
45
+ files.push({
46
+ path: posix.join(bundleRoot, rel),
47
+ mode: stagedMode(rel, abs),
48
+ source: { kind: 'file', path: abs },
49
+ });
50
+ }
51
+ nestedBundles.push({ path: bundleRoot, entitlements: bundle.entitlements });
52
+ }
53
+ for (const helper of input.helperExecutables) {
54
+ if (dirs.helpers === '') {
55
+ throw new Error(`gjsify ship: the ${input.layout.name} layout has no \`Contents/Helpers\` equivalent, so ` +
56
+ `the helper executable "${helper.name}" has nowhere to go. ` +
57
+ '`gjsify.ship.helperExecutables` is darwin-only; `resolveShipSettings` should have refused ' +
58
+ 'this already — report this as a gjsify bug if it did not.');
59
+ }
60
+ const path = posix.join(dirs.helpers, helper.name);
61
+ files.push({ path, mode: 0o755, source: { kind: 'file', path: helper.source } });
62
+ helperExecutables.push({ path, entitlements: helper.entitlements });
63
+ }
64
+ return { files, nestedBundles, helperExecutables };
65
+ }
66
+ /**
67
+ * What `signPayload` needs to sign the subordinates `stageSubordinates` staged —
68
+ * DERIVED from the layout and the facts alone, never carried as a separate
69
+ * closure.
70
+ *
71
+ * This is what lets `PackSettings` carry only `{ name, entitlements }` (see
72
+ * `types.ts`): the stage-relative PATH is `layout.dirs(settings).plugins` (or
73
+ * `.helpers`) joined with the name, which both the assembling host (which also
74
+ * has the absolute `source`) and a `--from-stage` finishing host (which has
75
+ * neither) can compute from data they already hold. Calling this twice — once
76
+ * in `assemble()`, once in `packOne` — can therefore never disagree with where
77
+ * {@link stageSubordinates} put the bytes.
78
+ */
79
+ export function signedSubordinatesFor(layout, identity, facts) {
80
+ const dirs = layout.dirs(identity);
81
+ return {
82
+ nestedBundles: facts.nestedBundles.map((bundle) => ({
83
+ path: posix.join(dirs.plugins, bundle.name),
84
+ entitlements: bundle.entitlements,
85
+ })),
86
+ helperExecutables: facts.helperExecutables.map((helper) => ({
87
+ path: posix.join(dirs.helpers, helper.name),
88
+ entitlements: helper.entitlements,
89
+ })),
90
+ };
91
+ }
92
+ /**
93
+ * One `<key>NAME</key>` entry's string value, read with a REGEX rather than a
94
+ * plist parser — the same tradeoff this tree already makes in `plist.ts`
95
+ * (renders XML by string template, never parses it): a release record is
96
+ * additive metadata, not a signature the pack path depends on, so a plist this
97
+ * reader cannot follow degrades to an absent field rather than a build failure.
98
+ */
99
+ function plistStringValue(xml, key) {
100
+ const match = new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(xml);
101
+ return match?.[1];
102
+ }
103
+ /**
104
+ * The `Info.plist` of one nested bundle, read back off the PACKED payload —
105
+ * after `signPayload` has run, so this sees exactly the bytes the artifact
106
+ * ships, never the assembling host's copy.
107
+ */
108
+ function nestedBundlePlist(payload, bundlePath) {
109
+ const entry = payload.find((file) => file.path === posix.join(bundlePath, 'Contents', 'Info.plist'));
110
+ return entry === undefined ? undefined : new TextDecoder().decode(entry.data);
111
+ }
112
+ /**
113
+ * What the release record says about each nested bundle, read off its OWN
114
+ * `Info.plist` rather than off `gjsify.ship.nestedBundles` — the declaration
115
+ * names a `source` directory and an entitlements list, neither of which is the
116
+ * bundle identifier or its extension point, and both of THOSE only exist inside
117
+ * the `.appex` the project built.
118
+ *
119
+ * `extensionPoint` comes from `NSExtension.NSExtensionPointIdentifier`, Apple's
120
+ * own nesting for every extension point (`com.apple.Safari.web-extension` is
121
+ * one value among several, not a special case this reader has to know). A
122
+ * nested bundle that is not an extension at all — or whose `Info.plist` this
123
+ * reader cannot find or parse — simply omits the key, which is the same
124
+ * additive contract the field's own doc states (`release-record.ts`).
125
+ */
126
+ export function recordedNestedBundles(payload, nestedBundles) {
127
+ return nestedBundles.map((bundle) => {
128
+ const name = posix.basename(bundle.path);
129
+ const xml = nestedBundlePlist(payload, bundle.path);
130
+ if (xml === undefined)
131
+ return { name };
132
+ const bundleId = plistStringValue(xml, 'CFBundleIdentifier');
133
+ const extensionPoint = plistStringValue(xml, 'NSExtensionPointIdentifier');
134
+ return {
135
+ name,
136
+ ...(bundleId === undefined ? {} : { bundleId }),
137
+ ...(extensionPoint === undefined ? {} : { extensionPoint }),
138
+ };
139
+ });
140
+ }
@@ -24,9 +24,37 @@ export interface ReleaseRecord {
24
24
  signature: RecordedSignature;
25
25
  notarized: boolean;
26
26
  stapled: boolean;
27
+ /**
28
+ * Bundles embedded under `Contents/PlugIns/` (`gjsify.ship.nestedBundles`) —
29
+ * `[]` for an artifact with none. ADDITIVE and backward-compatible: an older
30
+ * record with no such key is still a valid `schema: 1` record, because this
31
+ * is the first consumer of the field and nothing reads its absence as a
32
+ * claim. Named rather than counted, for `@gjsify/site` to tell a Safari
33
+ * extension apart from an unrelated nested bundle a future consumer adds —
34
+ * `extensionPoint` is what lets it do that without parsing `Info.plist`
35
+ * itself.
36
+ */
37
+ nestedBundles: RecordedNestedBundle[];
27
38
  /** `@gjsify/cli <version>` — who wrote this record. */
28
39
  generator: string;
29
40
  }
41
+ /**
42
+ * One nested bundle this artifact's `Contents/PlugIns/` carries, as the site
43
+ * needs to know it — e.g. a Safari Web Extension's `com.apple.Safari.web-extension`.
44
+ */
45
+ export interface RecordedNestedBundle {
46
+ /** Leaf under `Contents/PlugIns/` — `gjsify.ship.nestedBundles[].name`. */
47
+ name: string;
48
+ /** `CFBundleIdentifier` from the bundle's own `Info.plist`, when it has one. */
49
+ bundleId?: string;
50
+ /**
51
+ * `NSExtension.NSExtensionPointIdentifier` from `Info.plist`, when the
52
+ * bundle declares an extension point — `com.apple.Safari.web-extension` for
53
+ * a Safari Web Extension. Absent for a nested bundle that is not an
54
+ * extension at all.
55
+ */
56
+ extensionPoint?: string;
57
+ }
30
58
  export interface ReleaseRecordInput {
31
59
  /** Absolute path of the finished artifact file. */
32
60
  file: string;
@@ -36,6 +64,8 @@ export interface ReleaseRecordInput {
36
64
  signature: RecordedSignature;
37
65
  notarized: boolean;
38
66
  stapled: boolean;
67
+ /** `[]` for an artifact with no nested bundle. */
68
+ nestedBundles: RecordedNestedBundle[];
39
69
  generator: string;
40
70
  }
41
71
  export declare function releaseRecordPath(artifact: string): string;
@@ -44,6 +44,7 @@ export function buildReleaseRecord(input) {
44
44
  signature: input.signature,
45
45
  notarized: input.notarized,
46
46
  stapled: input.stapled,
47
+ nestedBundles: input.nestedBundles,
47
48
  generator: input.generator,
48
49
  };
49
50
  }
@@ -1,5 +1,5 @@
1
1
  import type { AppMetadata, ConfigDataFlatpak, ConfigDataShip, DescriptionBlock, ShipAppOptions } from '../../types/config-data.js';
2
- import type { HostOs, ShipExecutable, ShipSettings } from './types.js';
2
+ import type { HostOs, ShipExecutable, ShipHelperExecutable, ShipNestedBundle, ShipSettings } from './types.js';
3
3
  /** The subset of `package.json` the resolver reads. */
4
4
  export interface ShipPackageManifest {
5
5
  name?: string;
@@ -177,6 +177,31 @@ export declare function resolveShipExecutables(input: {
177
177
  projectDir: string;
178
178
  bundleDir: string;
179
179
  }): ShipExecutable[];
180
+ /**
181
+ * `gjsify.ship.nestedBundles` → the further bundles the darwin container embeds
182
+ * under `Contents/PlugIns/` — e.g. a Safari Web Extension `.appex`.
183
+ *
184
+ * Darwin-only by construction: `layoutOs !== 'darwin'` refuses a non-empty list
185
+ * rather than silently dropping it, because `Contents/PlugIns` has no
186
+ * equivalent on the other two layouts (ADR 0024 amendment, "nested bundles and
187
+ * helper executables"; the beifahrer ADR this answers: 0026 § 6).
188
+ */
189
+ export declare function resolveShipNestedBundles(input: {
190
+ declared: unknown;
191
+ projectDir: string;
192
+ layoutOs: HostOs;
193
+ }): ShipNestedBundle[];
194
+ /**
195
+ * `gjsify.ship.helperExecutables` → prebuilt executables staged into
196
+ * `Contents/Helpers/` beside the carried interpreter — e.g. a small Swift
197
+ * helper with no GI binding. Darwin-only, for the same reason as
198
+ * {@link resolveShipNestedBundles}.
199
+ */
200
+ export declare function resolveShipHelperExecutables(input: {
201
+ declared: unknown;
202
+ projectDir: string;
203
+ layoutOs: HostOs;
204
+ }): ShipHelperExecutable[];
180
205
  /** The entry one ship target installs, which key named it, and what it must NOT carry. */
181
206
  export interface ResolvedShipBundle {
182
207
  /** The declared path as written — project-relative or absolute. */