@livx.cc/appwrap 0.42.2 → 0.42.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +61 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.42.2",
3
+ "version": "0.42.3",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
package/src/cli.ts CHANGED
@@ -592,7 +592,11 @@ function stampTeamId(outDir: string, cfg: AppwrapConfig, ctx?: { cwd: string; co
592
592
  writeFileSync(xcconfig, src);
593
593
  }
594
594
 
595
- interface ProvProfile { name: string; uuid: string; teamId: string; bundleId: string; exp: number }
595
+ /** Profile kind, derived from the .mobileprovision payload: ProvisionsAllDevices → enterprise;
596
+ * ProvisionedDevices + get-task-allow → development; ProvisionedDevices w/o it → adhoc; else appstore. */
597
+ type ProvKind = 'development' | 'adhoc' | 'enterprise' | 'appstore';
598
+
599
+ interface ProvProfile { name: string; uuid: string; teamId: string; bundleId: string; exp: number; kind: ProvKind }
596
600
 
597
601
  /** Discover provisioning profiles installed on this machine (both the legacy MobileDevice dir and
598
602
  * the modern Xcode UserData dir). Each is decoded with `security cms` and reduced to the fields we
@@ -623,7 +627,11 @@ function findProvisioningProfiles(): ProvProfile[] {
623
627
  if (!name || !uuid || !team || !appId || byUuid.has(uuid)) continue;
624
628
  // application-identifier is `<TEAM>.<bundle.id>`; strip the team prefix to get the bundle id.
625
629
  const bundleId = appId.startsWith(team + '.') ? appId.slice(team.length + 1) : appId;
626
- byUuid.set(uuid, { name, uuid, teamId: team, bundleId, exp: expStr ? Date.parse(expStr) : 0 });
630
+ const kind: ProvKind = /<key>ProvisionsAllDevices<\/key>\s*<true\s*\/>/.test(raw) ? 'enterprise'
631
+ : /<key>ProvisionedDevices<\/key>/.test(raw)
632
+ ? (/<key>get-task-allow<\/key>\s*<true\s*\/>/.test(raw) ? 'development' : 'adhoc')
633
+ : 'appstore';
634
+ byUuid.set(uuid, { name, uuid, teamId: team, bundleId, exp: expStr ? Date.parse(expStr) : 0, kind });
627
635
  } catch { /* skip unreadable profile */ }
628
636
  }
629
637
  }
@@ -635,10 +643,14 @@ function findProvisioningProfiles(): ProvProfile[] {
635
643
  * pinned) → newest-expiry (headless). Returns null when nothing matches (caller warns + skips). */
636
644
  function selectSigningProfile(
637
645
  profiles: ProvProfile[], teamId: string, bundleId: string, cfg: AppwrapConfig,
638
- ctx?: { configPath: string },
646
+ ctx?: { configPath: string }, kind: ProvKind | 'device' = 'device',
639
647
  ): string | null {
640
648
  const now = Date.now();
641
- const matches = profiles.filter((p) => p.teamId === teamId && p.bundleId === bundleId && p.exp > now);
649
+ // 'appstore' (store archive) must sign with an App Store distribution profile; 'device' (deploy)
650
+ // must NOT — an appstore profile can't install on a device. Filtering here is what keeps ONE
651
+ // signingProfiles pin map safe: a pin of the wrong kind simply doesn't match this lane.
652
+ const kindOk = (p: ProvProfile): boolean => (kind === 'device' ? p.kind !== 'appstore' : p.kind === kind);
653
+ const matches = profiles.filter((p) => p.teamId === teamId && p.bundleId === bundleId && p.exp > now && kindOk(p));
642
654
  if (matches.length === 0) return null;
643
655
  const pinned = cfg.signingProfiles?.[bundleId];
644
656
  if (pinned && matches.some((p) => p.name === pinned)) return pinned;
@@ -678,14 +690,31 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
678
690
  console.warn(' ⚠ signing: manual but teamId is unset — skipping manual signing.');
679
691
  return;
680
692
  }
693
+ // Store archives (`appwrap build ios --release`) sign with App Store distribution profiles +
694
+ // "Apple Distribution"; device deploys with development/adhoc profiles + "Apple Development".
695
+ // A signing-DISABLED archive that's re-signed at exportArchive gets its entitlements SYNTHESIZED
696
+ // from the profile (dropping app.entitlements — e.g. declared-age-range); signing the archive
697
+ // itself is what preserves them, so the store lane rides this same manual stamp.
698
+ const kind: ProvKind | 'device' = process.env.APPWRAP_SIGNING_KIND === 'appstore' ? 'appstore' : 'device';
699
+ const identity = kind === 'appstore' ? 'Apple Distribution' : 'Apple Development';
700
+ // Don't pin interactive picks from the store lane — the single signingProfiles map is keyed by
701
+ // bundle id only, so a store pin would clobber the device pin (and vice versa).
702
+ const pinCtx = kind === 'device' ? ctx : undefined;
681
703
  const profiles = findProvisioningProfiles();
682
704
  let mainProvision: string | null = null;
683
705
 
684
706
  // Main app target → build.xcconfig.
685
707
  const xcconfig = join(outDir, 'App_Resources/iOS/build.xcconfig');
686
708
  if (existsSync(xcconfig)) {
687
- const name = selectSigningProfile(profiles, teamId, cfg.id, cfg, ctx);
709
+ const name = selectSigningProfile(profiles, teamId, cfg.id, cfg, pinCtx, kind);
688
710
  if (!name) {
711
+ // Store lane must NOT fall through to automatic signing: an unsigned archive gets its
712
+ // entitlements re-synthesized from the profile at export, silently dropping any
713
+ // config-stamped entitlement (the declared-age-range failure this lane exists to prevent).
714
+ if (kind === 'appstore') {
715
+ console.error(` ✗ no valid App Store distribution profile for ${cfg.id} (team ${teamId}). Install one (ASC → Profiles) or seed via Xcode, then retry.`);
716
+ process.exit(1);
717
+ }
689
718
  console.warn(` ⚠ no valid profile for ${cfg.id} (team ${teamId}) — automatic signing will run. Install one or seed via Xcode.`);
690
719
  } else {
691
720
  mainProvision = name;
@@ -696,10 +725,10 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
696
725
  ? s.replace(new RegExp(`^\\s*${key}\\s*=\\s*[^;\\n]*;?`, 'm'), `${key} = ${val};`)
697
726
  : s + `\n${key} = ${val};`;
698
727
  src = setKey(src, 'CODE_SIGN_STYLE', 'Manual');
699
- src = setKey(src, 'CODE_SIGN_IDENTITY', 'Apple Development');
728
+ src = setKey(src, 'CODE_SIGN_IDENTITY', identity);
700
729
  src = setKey(src, 'PROVISIONING_PROFILE_SPECIFIER', name);
701
730
  writeFileSync(xcconfig, src.endsWith('\n') ? src : src + '\n');
702
- console.log(` sign ← ${cfg.id} → "${name}" (manual)`);
731
+ console.log(` sign ← ${cfg.id} → "${name}" (manual, ${identity})`);
703
732
  }
704
733
  }
705
734
 
@@ -715,7 +744,7 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
715
744
  if (!ext.isDirectory()) continue;
716
745
  if (!existsSync(join(extRoot, ext.name, 'extension.json'))) continue;
717
746
  const extBundle = `${cfg.id}.${ext.name}`;
718
- const name = selectSigningProfile(profiles, teamId, extBundle, cfg, ctx);
747
+ const name = selectSigningProfile(profiles, teamId, extBundle, cfg, pinCtx, kind);
719
748
  if (!name) { console.warn(` ⚠ no valid profile for extension ${extBundle} — it may fail to sign.`); continue; }
720
749
  extEntries.push({ bundleId: extBundle, profile: name });
721
750
  console.log(` sign ← ${extBundle} → "${name}" (manual, via hook)`);
@@ -725,7 +754,7 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
725
754
  const hookFile = join(hookDir, 'appwrap-signing.js');
726
755
  if (extEntries.length) {
727
756
  mkdirSync(hookDir, { recursive: true });
728
- writeFileSync(hookFile, SIGNING_HOOK(teamId, extEntries));
757
+ writeFileSync(hookFile, SIGNING_HOOK(teamId, extEntries, identity));
729
758
  // NS's exportOptions reads extension profiles from this file (extensions/provisioning.json) —
730
759
  // it maps each extension bundle id → profile name so `xcodebuild -exportArchive` can build the .ipa.
731
760
  writeFileSync(join(extRoot, 'provisioning.json'),
@@ -751,11 +780,12 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
751
780
  * Self-contained (no deps), zero-arg (NS DI won't choke). Per extension it upserts DEVELOPMENT_TEAM,
752
781
  * PROVISIONING_PROFILE_SPECIFIER, CODE_SIGN_STYLE=Manual, CODE_SIGN_IDENTITY in every buildSettings
753
782
  * block whose PRODUCT_BUNDLE_IDENTIFIER matches the extension's bundle id. */
754
- const SIGNING_HOOK = (team: string, entries: Array<{ bundleId: string; profile: string }>) => `// Generated by \`appwrap\` — stamps manual signing onto extension targets in project.pbxproj.
783
+ const SIGNING_HOOK = (team: string, entries: Array<{ bundleId: string; profile: string }>, identity = 'Apple Development') => `// Generated by \`appwrap\` — stamps manual signing onto extension targets in project.pbxproj.
755
784
  const fs = require('fs');
756
785
  const path = require('path');
757
786
  const TEAM = ${JSON.stringify(team)};
758
787
  const ENTRIES = ${JSON.stringify(entries)};
788
+ const IDENTITY = ${JSON.stringify(identity)};
759
789
  module.exports = function () {
760
790
  const iosDir = path.join(__dirname, '..', '..', 'platforms', 'ios');
761
791
  if (!fs.existsSync(iosDir)) return;
@@ -781,7 +811,7 @@ module.exports = function () {
781
811
  block = block.replace(/^\\s*PROVISIONING_PROFILE\\s*=\\s*[^;\\n]*;\\n?/m, '');
782
812
  block = upsert(block, 'CODE_SIGN_STYLE', 'Manual');
783
813
  block = upsert(block, 'DEVELOPMENT_TEAM', TEAM);
784
- block = upsert(block, 'CODE_SIGN_IDENTITY', q('Apple Development'));
814
+ block = upsert(block, 'CODE_SIGN_IDENTITY', q(IDENTITY));
785
815
  block = upsert(block, 'PROVISIONING_PROFILE_SPECIFIER', q(profile));
786
816
  return block;
787
817
  });
@@ -1918,13 +1948,29 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
1918
1948
  console.error(`✖ ${(e as Error).message}`);
1919
1949
  process.exit(1);
1920
1950
  }
1951
+ const release = 'release' in flags;
1952
+ // iOS release archive = the STORE artifact. With signing:'manual', stamp App Store distribution
1953
+ // signing (profile kind 'appstore' + "Apple Distribution") so xcodebuild archives a SIGNED binary —
1954
+ // an unsigned archive re-signed at exportArchive gets its entitlements SYNTHESIZED from the
1955
+ // provisioning profile, silently dropping app.entitlements (e.g. declared-age-range).
1956
+ if (platform === 'ios' && release) process.env.APPWRAP_SIGNING_KIND = 'appstore';
1921
1957
  // Make sure the wrapper reflects the latest config + PWA before compiling (also validates the config).
1922
1958
  await sync(cwd, flags);
1923
1959
 
1924
- const release = 'release' in flags;
1925
1960
  const args = ['build', platform];
1926
1961
  if (release) args.push('--release');
1927
- if (platform === 'ios' && release) args.push('--for-device');
1962
+ if (platform === 'ios' && release) {
1963
+ args.push('--for-device');
1964
+ // Manual signing: pass the resolved App Store profile so NS emits a manual exportOptions.plist
1965
+ // (app + extensions via extensions/provisioning.json) for `xcodebuild -exportArchive`. The export
1966
+ // method comes from the archive's embedded profile (Distribution → app-store), so this yields a
1967
+ // store .ipa whose entitlements are the archive's own (app.entitlements intact).
1968
+ const sidecar = join(outDir, '.appwrap-signing.json');
1969
+ if (existsSync(sidecar)) {
1970
+ const sc = JSON.parse(readFileSync(sidecar, 'utf8')) as { provision?: string };
1971
+ if (sc.provision) args.push('--provision', sc.provision);
1972
+ }
1973
+ }
1928
1974
  if (platform === 'android' && 'aab' in flags) args.push('--aab');
1929
1975
 
1930
1976
  if (platform === 'android' && release) {
@@ -1949,7 +1995,8 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
1949
1995
  console.log(`▶ ns ${args.join(' ').replace(/(--key-store-password|--key-store-alias-password) [^ ]*/g, '$1 ****')} (cwd: ${outDir})`);
1950
1996
  execFileSync('ns', args, { cwd: outDir, stdio: 'inherit' });
1951
1997
  if (platform === 'ios' && release) {
1952
- console.log('ℹ App Store distribution (archive + upload) goes through the fastlane release lane (native/fastlane) — needs a paid team + ASC API key.');
1998
+ const ipa = join(outDir, 'platforms/ios/build/Release-iphoneos');
1999
+ console.log(`ℹ Signed store .ipa in ${ipa} — upload via \`bunx @livx.cc/mcp-appstores apple-upload\` or the fastlane release lane (native/fastlane).`);
1953
2000
  }
1954
2001
  }
1955
2002