@geekmidas/cli 10.0.0-alpha.34 → 10.0.0-alpha.35

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 (29) hide show
  1. package/dist/{config-DMpzmsQL.cjs → config-DgAHy8nY.cjs} +9 -5
  2. package/dist/config-DgAHy8nY.cjs.map +1 -0
  3. package/dist/{config-Du8AQJRK.mjs → config-Dss1WwtH.mjs} +9 -5
  4. package/dist/config-Dss1WwtH.mjs.map +1 -0
  5. package/dist/config.cjs +1 -1
  6. package/dist/config.mjs +1 -1
  7. package/dist/{github-DN-92LWP.mjs → github-C99BMxf0.mjs} +2 -2
  8. package/dist/{github-DN-92LWP.mjs.map → github-C99BMxf0.mjs.map} +1 -1
  9. package/dist/{github-JhvrmjQp.cjs → github-tZmMZ7td.cjs} +2 -2
  10. package/dist/{github-JhvrmjQp.cjs.map → github-tZmMZ7td.cjs.map} +1 -1
  11. package/dist/index.cjs +205 -85
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.mjs +206 -86
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{migrate-BbhXxQer.mjs → migrate-C-D9xIbb.mjs} +3 -3
  16. package/dist/{migrate-BbhXxQer.mjs.map → migrate-C-D9xIbb.mjs.map} +1 -1
  17. package/dist/{migrate-u1oRHz6K.cjs → migrate-nlvT-GnY.cjs} +3 -3
  18. package/dist/{migrate-u1oRHz6K.cjs.map → migrate-nlvT-GnY.cjs.map} +1 -1
  19. package/dist/openapi.cjs +1 -1
  20. package/dist/openapi.mjs +1 -1
  21. package/dist/{workspace-DB2YHgOl.mjs → workspace-CIiGijNi.mjs} +90 -16
  22. package/dist/workspace-CIiGijNi.mjs.map +1 -0
  23. package/dist/{workspace-LKFnEBX8.cjs → workspace-CRjZkCTW.cjs} +89 -15
  24. package/dist/workspace-CRjZkCTW.cjs.map +1 -0
  25. package/package.json +14 -14
  26. package/dist/config-DMpzmsQL.cjs.map +0 -1
  27. package/dist/config-Du8AQJRK.mjs.map +0 -1
  28. package/dist/workspace-DB2YHgOl.mjs.map +0 -1
  29. package/dist/workspace-LKFnEBX8.cjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
3
3
  import { c as isPartitionedRoutes, i as ConstructGenerator, t as discover } from "./discover-CMxesskG.mjs";
4
- import { a as loadConfig, c as parseModuleConfig, i as loadAppConfig, l as appKey, r as getAppNameFromCwd, s as loadWorkspaceConfig, u as derivedApps } from "./config-Du8AQJRK.mjs";
4
+ import { a as loadConfig, c as parseModuleConfig, i as loadAppConfig, l as appKey, r as getAppNameFromCwd, s as loadWorkspaceConfig, u as derivedApps } from "./config-Dss1WwtH.mjs";
5
5
  import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-CsL76tpc.mjs";
6
6
  import { c as getKeyPath, i as setCustomSecret, n as readStageSecrets, o as toEmbeddableSecrets, r as secretsExist, s as writeStageSecrets, t as maskPassword } from "./storage-BjTLjDDp.mjs";
7
- import { _ as findAvailablePort, b as loadSecretsForApp, c as applyPostgres, d as cacheBackendFor, f as eventsBackendFor, g as createEntryWrapper, h as createCredentialsPreload, i as reconcileWorkspace, l as resourceName, m as storageBackendFor, n as constructGlobs, o as COMPOSE_PATH, p as providerOf, r as derivedContainers, s as LOCAL_CA_PATH, t as backendsOf, u as rootSite, v as isPortAvailable, x as prepareEntryCredentials, y as loadEnvFiles } from "./workspace-DB2YHgOl.mjs";
7
+ import { _ as findAvailablePort, b as loadSecretsForApp, c as applyPostgres, d as cacheBackendFor, f as eventsBackendFor, g as createEntryWrapper, h as createCredentialsPreload, i as reconcileWorkspace, l as resourceName, m as storageBackendFor, n as constructGlobs, o as COMPOSE_PATH, p as providerOf, r as derivedContainers, s as LOCAL_CA_PATH, t as backendsOf, u as rootSite, v as isPortAvailable, x as prepareEntryCredentials, y as loadEnvFiles } from "./workspace-CIiGijNi.mjs";
8
8
  import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-hDINScnz.mjs";
9
9
  import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
10
10
  import { a as writeTestHarness, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as cacheBackendsIn, s as driversFor, t as TEST_READY_ENV } from "./ready-D1BCiaYU.mjs";
@@ -19,7 +19,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
19
19
  import { homedir } from "node:os";
20
20
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
21
21
  import { execFile, execSync, spawn } from "node:child_process";
22
- import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, kebabCase, migrationFolder, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
22
+ import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, kebabCase, migrationFolder, mobileOrigins, provideKey, provisionOrder, schemeBase, scopedName, serviceKey } from "@geekmidas/manifest";
23
23
  import fg from "fast-glob";
24
24
  import chokidar from "chokidar";
25
25
  import { createCipheriv, createHash, randomBytes } from "node:crypto";
@@ -37,7 +37,7 @@ import { Subscriber } from "@geekmidas/constructs/subscribers";
37
37
  import { Topic } from "@geekmidas/constructs/topic";
38
38
  import { lookup } from "node:dns/promises";
39
39
  //#region package.json
40
- var version = "10.0.0-alpha.34";
40
+ var version = "10.0.0-alpha.35";
41
41
  //#endregion
42
42
  //#region src/auth/credentials.ts
43
43
  /**
@@ -1087,9 +1087,9 @@ async function workspaceDevCommand(workspace, options) {
1087
1087
  ...reconciled.env
1088
1088
  };
1089
1089
  if (options.migrate || options.seed) {
1090
- const { prepareDevDatabases } = await import("./migrate-BbhXxQer.mjs");
1090
+ const { prepareDevDatabases } = await import("./migrate-C-D9xIbb.mjs");
1091
1091
  await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
1092
- } else await import("./migrate-BbhXxQer.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1092
+ } else await import("./migrate-C-D9xIbb.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1093
1093
  logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1094
1094
  });
1095
1095
  const appUrls = {};
@@ -1119,6 +1119,13 @@ async function workspaceDevCommand(workspace, options) {
1119
1119
  for (const appName of buildOrder) {
1120
1120
  const app = workspace.apps[appName];
1121
1121
  if (!app) continue;
1122
+ if (app.type === "mobile") {
1123
+ const own = reconciled.plan.resources.find((r) => r.kind === "mobile-app" && appKey(r.id) === appName);
1124
+ const scheme = own ? reconciled.env[own.envKey] : void 0;
1125
+ const device = Object.entries(reconciled.env).find(([key]) => key.endsWith("_DEVICE_URL"))?.[1];
1126
+ logger$10.log(` ${appName.padEnd(width)} ${scheme ? `${scheme}://` : "mobile"}${device ? ` — devices reach ${new URL(device).hostname}` : ""}`);
1127
+ continue;
1128
+ }
1122
1129
  const local = `http://localhost:${app.port}`;
1123
1130
  const url = Object.entries(appUrls).find(([id]) => appKey(id) === appName)?.[1];
1124
1131
  logger$10.log(` ${appName.padEnd(width)} ${url ? `${url} -> ${local}` : local}`);
@@ -3313,9 +3320,13 @@ const PROVISIONERS = {
3313
3320
  if (!url) throw new SurfaceHasNoAddress(declaration.id);
3314
3321
  const origins = [...new Set(dependentsOf(context.manifest, declaration.id).map((caller) => context.addresses?.[caller]).filter((address) => Boolean(address)))].sort();
3315
3322
  const domain = cookieDomain([url, ...origins]);
3323
+ const schemes = dependentsOf(context.manifest, declaration.id).flatMap((caller) => {
3324
+ const app = context.manifest[caller];
3325
+ return app?.kind === "mobile-app" ? mobileOrigins(schemeBase(context.project, app.scheme)) : [];
3326
+ });
3316
3327
  return { provides: {
3317
3328
  [provideKey(declaration.id, "url")]: url,
3318
- [provideKey(declaration.id, "trustedOrigins")]: origins.join(","),
3329
+ [provideKey(declaration.id, "trustedOrigins")]: [...origins, ...schemes].join(","),
3319
3330
  ...domain ? { [provideKey(declaration.id, "cookieDomain")]: domain } : {}
3320
3331
  } };
3321
3332
  }
@@ -8000,11 +8011,11 @@ function routesGlob(structure) {
8000
8011
  }
8001
8012
  }
8002
8013
  /**
8003
- * Plain ids — `Database`, not `Beetlefit`.
8014
+ * Plain ids — `Database`, not `Shop`.
8004
8015
  *
8005
8016
  * The workspace `name` already scopes every physical name
8006
- * (`production-beetlefit-database`), so an id that carried the project too
8007
- * would say it twice: `production-beetlefit-beetlefit-cache`. Within a
8017
+ * (`production-shop-database`), so an id that carried the project too
8018
+ * would say it twice: `production-shop-shop-cache`. Within a
8008
8019
  * workspace the constructs are declared once, at its root, so there is no
8009
8020
  * second app's `Database` to collide with.
8010
8021
  */
@@ -8730,30 +8741,32 @@ const TOOLCHAIN_VERSIONS = {
8730
8741
  "@storybook/react-vite": "^10.6.0"
8731
8742
  };
8732
8743
  /**
8733
- * The Expo app's packages, which track an Expo SDK release (55) rather than
8734
- * their own latest: SDK 55 is tested against exactly these, React Native and
8744
+ * The Expo app's packages, which track an Expo SDK release (57) rather than
8745
+ * their own latest: SDK 57 is tested against exactly these, React Native and
8735
8746
  * React included, so they move when the SDK does.
8736
8747
  */
8737
8748
  const EXPO_VERSIONS = {
8738
- expo: "~55.0.0",
8739
- "expo-constants": "~55.0.0",
8740
- "expo-dev-client": "~55.0.0",
8741
- "expo-linking": "~55.0.0",
8742
- "expo-router": "~55.0.0",
8743
- "expo-splash-screen": "~55.0.0",
8744
- "expo-status-bar": "~55.0.0",
8745
- "expo-secure-store": "~14.2.0",
8746
- react: "19.2.0",
8747
- "react-dom": "19.2.0",
8748
- "react-native": "0.83.6",
8749
- "react-native-gesture-handler": "~2.30.0",
8750
- "react-native-reanimated": "~4.2.0",
8751
- "react-native-safe-area-context": "~5.6.0",
8752
- "react-native-screens": "~4.23.0",
8749
+ expo: "~57.0.26",
8750
+ "expo-constants": "~57.0.20",
8751
+ "expo-dev-client": "~57.0.19",
8752
+ "expo-device": "~57.0.2",
8753
+ "expo-linking": "~57.0.11",
8754
+ "expo-router": "~57.0.24",
8755
+ "expo-splash-screen": "~57.0.9",
8756
+ "expo-status-bar": "~57.0.1",
8757
+ "expo-secure-store": "~57.0.4",
8758
+ react: "19.2.3",
8759
+ "react-dom": "19.2.3",
8760
+ "react-native": "0.86.3",
8761
+ "react-native-gesture-handler": "~2.32.0",
8762
+ "react-native-reanimated": "4.5.1",
8763
+ "react-native-worklets": "0.10.1",
8764
+ "react-native-safe-area-context": "~5.7.0",
8765
+ "react-native-screens": "~4.26.0",
8753
8766
  "react-native-web": "~0.21.0",
8754
8767
  "@react-navigation/native": "^7.1.0",
8755
8768
  "@babel/core": "^7.25.0",
8756
- "@types/react": "~19.0.0",
8769
+ "@types/react": "~19.2.2",
8757
8770
  nativewind: "~4.2.0",
8758
8771
  tailwindcss: "~3.4.0"
8759
8772
  };
@@ -8785,26 +8798,26 @@ function loadPackageJson() {
8785
8798
  * Run: pnpm --filter @geekmidas/cli sync-versions
8786
8799
  */
8787
8800
  const GEEKMIDAS_VERSIONS = {
8788
- "@geekmidas/audit": "~10.0.0-alpha.34",
8789
- "@geekmidas/auth": "~10.0.0-alpha.34",
8790
- "@geekmidas/cache": "~10.0.0-alpha.34",
8791
- "@geekmidas/client": "~10.0.0-alpha.34",
8792
- "@geekmidas/cloud": "~10.0.0-alpha.34",
8793
- "@geekmidas/constructs": "~10.0.0-alpha.34",
8794
- "@geekmidas/db": "~10.0.0-alpha.34",
8795
- "@geekmidas/emailkit": "~10.0.0-alpha.34",
8796
- "@geekmidas/envkit": "~10.0.0-alpha.34",
8797
- "@geekmidas/errors": "~10.0.0-alpha.34",
8798
- "@geekmidas/events": "~10.0.0-alpha.34",
8799
- "@geekmidas/logger": "~10.0.0-alpha.34",
8800
- "@geekmidas/manifest": "~10.0.0-alpha.34",
8801
- "@geekmidas/rate-limit": "~10.0.0-alpha.34",
8802
- "@geekmidas/schema": "~10.0.0-alpha.34",
8803
- "@geekmidas/services": "~10.0.0-alpha.34",
8804
- "@geekmidas/storage": "~10.0.0-alpha.34",
8805
- "@geekmidas/studio": "~10.0.0-alpha.34",
8806
- "@geekmidas/telescope": "~10.0.0-alpha.34",
8807
- "@geekmidas/testkit": "~10.0.0-alpha.34",
8801
+ "@geekmidas/audit": "~10.0.0-alpha.35",
8802
+ "@geekmidas/auth": "~10.0.0-alpha.35",
8803
+ "@geekmidas/cache": "~10.0.0-alpha.35",
8804
+ "@geekmidas/client": "~10.0.0-alpha.35",
8805
+ "@geekmidas/cloud": "~10.0.0-alpha.35",
8806
+ "@geekmidas/constructs": "~10.0.0-alpha.35",
8807
+ "@geekmidas/db": "~10.0.0-alpha.35",
8808
+ "@geekmidas/emailkit": "~10.0.0-alpha.35",
8809
+ "@geekmidas/envkit": "~10.0.0-alpha.35",
8810
+ "@geekmidas/errors": "~10.0.0-alpha.35",
8811
+ "@geekmidas/events": "~10.0.0-alpha.35",
8812
+ "@geekmidas/logger": "~10.0.0-alpha.35",
8813
+ "@geekmidas/manifest": "~10.0.0-alpha.35",
8814
+ "@geekmidas/rate-limit": "~10.0.0-alpha.35",
8815
+ "@geekmidas/schema": "~10.0.0-alpha.35",
8816
+ "@geekmidas/services": "~10.0.0-alpha.35",
8817
+ "@geekmidas/storage": "~10.0.0-alpha.35",
8818
+ "@geekmidas/studio": "~10.0.0-alpha.35",
8819
+ "@geekmidas/telescope": "~10.0.0-alpha.35",
8820
+ "@geekmidas/testkit": "~10.0.0-alpha.35",
8808
8821
  "@geekmidas/cli": `~${loadPackageJson().version}`
8809
8822
  };
8810
8823
  //#endregion
@@ -9526,8 +9539,7 @@ function generateExpoAppFiles(options) {
9526
9539
  const apiPackage = `@${options.name}/api`;
9527
9540
  const modelsPackage = `@${options.name}/models`;
9528
9541
  const slug = options.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
9529
- const scheme = slug.replace(/-/g, "");
9530
- const bundleId = `com.${scheme}.app`;
9542
+ const scheme = schemeBase(options.name);
9531
9543
  const packageJson = {
9532
9544
  name: packageName,
9533
9545
  version: "0.0.1",
@@ -9552,6 +9564,7 @@ function generateExpoAppFiles(options) {
9552
9564
  expo: EXPO_VERSIONS["expo"],
9553
9565
  "expo-constants": EXPO_VERSIONS["expo-constants"],
9554
9566
  "expo-dev-client": EXPO_VERSIONS["expo-dev-client"],
9567
+ "expo-device": EXPO_VERSIONS["expo-device"],
9555
9568
  "expo-linking": EXPO_VERSIONS["expo-linking"],
9556
9569
  "expo-router": EXPO_VERSIONS["expo-router"],
9557
9570
  "expo-secure-store": EXPO_VERSIONS["expo-secure-store"],
@@ -9563,6 +9576,7 @@ function generateExpoAppFiles(options) {
9563
9576
  "react-native": EXPO_VERSIONS["react-native"],
9564
9577
  "react-native-gesture-handler": EXPO_VERSIONS["react-native-gesture-handler"],
9565
9578
  "react-native-reanimated": EXPO_VERSIONS["react-native-reanimated"],
9579
+ "react-native-worklets": EXPO_VERSIONS["react-native-worklets"],
9566
9580
  "react-native-safe-area-context": EXPO_VERSIONS["react-native-safe-area-context"],
9567
9581
  "react-native-screens": EXPO_VERSIONS["react-native-screens"],
9568
9582
  "react-native-web": EXPO_VERSIONS["react-native-web"],
@@ -9575,27 +9589,53 @@ function generateExpoAppFiles(options) {
9575
9589
  }
9576
9590
  };
9577
9591
  const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
9592
+ import { EnvironmentParser } from '@geekmidas/envkit';
9578
9593
 
9579
9594
  /**
9580
- * Expo runtime config. Public env vars (\`EXPO_PUBLIC_*\`) are inlined
9581
- * at build time; everything else is read at runtime from \`extra\`.
9595
+ * What the app is built with, from the environment \`gkm\` injects — the
9596
+ * \`MobileApp\` construct's \`.dependsOn([api, auth])\` is where every value
9597
+ * here comes from, so none of it is written down.
9598
+ *
9599
+ * - \`APP_SCHEME\`: the URL scheme for this stage — \`${scheme}\` deployed,
9600
+ * \`${scheme}-dev\` locally, so a development build and the store build on
9601
+ * one phone never answer each other's links.
9602
+ * - \`EXPO_PUBLIC_API_URL\` / \`EXPO_PUBLIC_AUTH_URL\`: where the servers
9603
+ * answer. Locally that is their own port on \`localhost\`, which
9604
+ * \`config.ts\` points at the machine Metro was served from.
9605
+ *
9606
+ * Read at runtime from \`Constants.expoConfig.extra.config\` — see \`config.ts\`.
9582
9607
  */
9583
- export default function config(_context: ConfigContext): ExpoConfig {
9608
+ const config = new EnvironmentParser({ ...process.env })
9609
+ .create((get) => ({
9610
+ scheme: get('APP_SCHEME').string(),
9611
+ apiUrl: get('EXPO_PUBLIC_API_URL').string(),
9612
+ authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
9613
+ }))
9614
+ .parse();
9615
+
9616
+ export type Config = typeof config;
9617
+
9618
+ // A bundle id and an Android package allow no hyphens; one per scheme, so
9619
+ // each stage installs beside the others.
9620
+ const bundleId = \`com.\${config.scheme.replace(/[^a-z0-9]/g, '')}.app\`;
9621
+
9622
+ export default function expoConfig({ config: base }: ConfigContext): ExpoConfig {
9584
9623
  return {
9624
+ ...base,
9585
9625
  name: '${options.name}',
9586
9626
  slug: '${slug}',
9587
9627
  version: '0.0.1',
9588
9628
  orientation: 'portrait',
9589
9629
  icon: './assets/icon.png',
9590
- scheme: '${scheme}',
9630
+ scheme: config.scheme,
9591
9631
  userInterfaceStyle: 'automatic',
9592
9632
  newArchEnabled: true,
9593
9633
  ios: {
9594
9634
  supportsTablet: true,
9595
- bundleIdentifier: '${bundleId}',
9635
+ bundleIdentifier: bundleId,
9596
9636
  },
9597
9637
  android: {
9598
- package: '${bundleId}',
9638
+ package: bundleId,
9599
9639
  adaptiveIcon: {
9600
9640
  foregroundImage: './assets/adaptive-icon.png',
9601
9641
  backgroundColor: '#ffffff',
@@ -9621,6 +9661,7 @@ export default function config(_context: ConfigContext): ExpoConfig {
9621
9661
  experiments: {
9622
9662
  typedRoutes: true,
9623
9663
  },
9664
+ extra: { ...base.extra, config },
9624
9665
  };
9625
9666
  }
9626
9667
  `;
@@ -9633,16 +9674,13 @@ export default function config(_context: ConfigContext): ExpoConfig {
9633
9674
  dev: {
9634
9675
  developmentClient: true,
9635
9676
  distribution: "internal",
9636
- environment: "development",
9637
- env: {
9638
- EXPO_PUBLIC_API_URL: "http://localhost:3000",
9639
- EXPO_PUBLIC_AUTH_URL: "http://localhost:3002"
9640
- }
9677
+ environment: "development"
9641
9678
  },
9642
9679
  preview: {
9643
9680
  distribution: "internal",
9644
9681
  environment: "preview",
9645
9682
  env: {
9683
+ APP_SCHEME: scheme,
9646
9684
  EXPO_PUBLIC_API_URL: "https://api.example.com",
9647
9685
  EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
9648
9686
  }
@@ -9651,6 +9689,7 @@ export default function config(_context: ConfigContext): ExpoConfig {
9651
9689
  autoIncrement: true,
9652
9690
  environment: "production",
9653
9691
  env: {
9692
+ APP_SCHEME: scheme,
9654
9693
  EXPO_PUBLIC_API_URL: "https://api.example.com",
9655
9694
  EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
9656
9695
  }
@@ -9712,23 +9751,49 @@ export default {
9712
9751
  `;
9713
9752
  const expoEnvDts = `/// <reference types="expo/types" />\n`;
9714
9753
  const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
9715
- const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
9754
+ const configTs = `import Constants from 'expo-constants';
9755
+ import * as Device from 'expo-device';
9756
+ import { Platform } from 'react-native';
9757
+
9758
+ import type { Config } from './app.config.ts';
9759
+
9760
+ const built = Constants.expoConfig?.extra?.config as Config;
9761
+
9762
+ /** The host this device loaded the bundle from: Metro's, as the device sees it. */
9763
+ const metro = (Constants.expoConfig?.hostUri ?? '').split(':')[0];
9716
9764
 
9717
9765
  /**
9718
- * Public app config. EXPO_PUBLIC_* vars are inlined into the bundle at
9719
- * build time, so they're safe to read in client code.
9766
+ * A private address on the local network — what a phone on the same Wi-Fi
9767
+ * reaches this machine on. A tunnel's \`*.exp.direct\` host is not one: a
9768
+ * tunnel forwards only Metro, so the servers are not behind it.
9720
9769
  */
9721
- const envParser = new EnvironmentParser({
9722
- EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
9723
- EXPO_PUBLIC_AUTH_URL: process.env.EXPO_PUBLIC_AUTH_URL,
9724
- });
9770
+ const isLan = (host: string) =>
9771
+ /^10\\./.test(host) ||
9772
+ /^192\\.168\\./.test(host) ||
9773
+ /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host);
9725
9774
 
9726
- export const config = envParser
9727
- .create((get) => ({
9728
- apiUrl: get('EXPO_PUBLIC_API_URL').string(),
9729
- authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
9730
- }))
9731
- .parse();
9775
+ /** \`localhost\` from inside the Android emulator is the emulator itself. */
9776
+ const isAndroidEmulator = Platform.OS === 'android' && !Device.isDevice;
9777
+
9778
+ /**
9779
+ * A server's URL as this device reaches it.
9780
+ *
9781
+ * Locally the servers are injected as \`http://localhost:<port>\`. That is this
9782
+ * device only on the iOS simulator. The Android emulator reaches this machine
9783
+ * at \`10.0.2.2\`, and a phone at the LAN address Metro was served from.
9784
+ * Deployed URLs have no \`localhost\` in them and pass through untouched.
9785
+ */
9786
+ export function reachable(url: string): string {
9787
+ const host = isLan(metro) ? metro : isAndroidEmulator ? '10.0.2.2' : undefined;
9788
+ if (!host) return url;
9789
+ return url.replace(/\\/\\/(localhost|127\\.0\\.0\\.1)(?=[:/]|$)/, \`//\${host}\`);
9790
+ }
9791
+
9792
+ export const config = {
9793
+ scheme: built.scheme,
9794
+ apiUrl: reachable(built.apiUrl),
9795
+ authUrl: reachable(built.authUrl),
9796
+ };
9732
9797
  `;
9733
9798
  const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
9734
9799
 
@@ -9740,21 +9805,23 @@ export const queryClient = new QueryClient({
9740
9805
  },
9741
9806
  });
9742
9807
  `;
9743
- const authClientTs = `import { expoClient } from '@better-auth/expo/client';
9808
+ const authClientTs = `import { expoClient, getSetCookie } from '@better-auth/expo/client';
9744
9809
  import { magicLinkClient } from 'better-auth/client/plugins';
9745
9810
  import { createAuthClient } from 'better-auth/react';
9811
+ import * as Linking from 'expo-linking';
9746
9812
  import * as SecureStore from 'expo-secure-store';
9813
+ import { useEffect } from 'react';
9747
9814
 
9748
9815
  import { config } from '@/config.ts';
9749
9816
 
9750
- export const STORAGE_PREFIX = '${scheme}';
9817
+ export const STORAGE_PREFIX = config.scheme;
9751
9818
  export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
9752
9819
 
9753
9820
  export const authClient = createAuthClient({
9754
9821
  baseURL: config.authUrl,
9755
9822
  plugins: [
9756
9823
  expoClient({
9757
- scheme: '${scheme}',
9824
+ scheme: config.scheme,
9758
9825
  storagePrefix: STORAGE_PREFIX,
9759
9826
  storage: SecureStore,
9760
9827
  }),
@@ -9764,6 +9831,36 @@ export const authClient = createAuthClient({
9764
9831
 
9765
9832
  export const { signIn, useSession } = authClient;
9766
9833
 
9834
+ /**
9835
+ * Sign in from the emailed link.
9836
+ *
9837
+ * The link opens in the phone's browser, which is what receives the session
9838
+ * cookie — not the app. The auth server's Expo plugin carries the cookie on the
9839
+ * redirect back into the app (\`?cookie=\`), and this stores it where the
9840
+ * Expo client keeps its own, merged with what is already there, then tells the
9841
+ * client the session changed. The Expo client does this itself only for a
9842
+ * sign-in it opened (social); a link opened from mail arrives as a deep link.
9843
+ *
9844
+ * Called once, from the root layout.
9845
+ */
9846
+ export function useSessionFromLink() {
9847
+ const url = Linking.useLinkingURL();
9848
+
9849
+ useEffect(() => {
9850
+ const cookie = url ? Linking.parse(url).queryParams?.cookie : undefined;
9851
+ if (typeof cookie !== 'string') return;
9852
+
9853
+ void (async () => {
9854
+ const previous = await SecureStore.getItemAsync(COOKIE_STORE_KEY);
9855
+ await SecureStore.setItemAsync(
9856
+ COOKIE_STORE_KEY,
9857
+ getSetCookie(cookie, previous ?? undefined),
9858
+ );
9859
+ authClient.$store.notify('$sessionSignal');
9860
+ })();
9861
+ }, [url]);
9862
+ }
9863
+
9767
9864
  export async function signOut() {
9768
9865
  try {
9769
9866
  await authClient.signOut();
@@ -9830,7 +9927,7 @@ import { StatusBar } from 'expo-status-bar';
9830
9927
  import { useEffect, useMemo } from 'react';
9831
9928
  import 'react-native-reanimated';
9832
9929
 
9833
- import { authClient } from '@/lib/auth-client.ts';
9930
+ import { authClient, useSessionFromLink } from '@/lib/auth-client.ts';
9834
9931
  import { ApiProvider } from '@/lib/api-context.tsx';
9835
9932
  import { createAppApi } from '@/lib/api.ts';
9836
9933
  import { queryClient } from '@/lib/query-client.ts';
@@ -9855,6 +9952,8 @@ function AuthGate({ children }: { children: React.ReactNode }) {
9855
9952
 
9856
9953
  export default function RootLayout() {
9857
9954
  const api = useMemo(() => createAppApi(), []);
9955
+ // The emailed sign-in link comes back as a deep link carrying the session.
9956
+ useSessionFromLink();
9858
9957
 
9859
9958
  return (
9860
9959
  <QueryClientProvider client={queryClient}>
@@ -10376,7 +10475,8 @@ function rootConstructDependencies(options) {
10376
10475
  pg: DEPENDENCY_VERSIONS.pg,
10377
10476
  ...options.constructs.uploads ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
10378
10477
  ...options.constructs.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
10379
- ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
10478
+ ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {},
10479
+ ...options.frontendFramework === "expo" ? { "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"] } : {}
10380
10480
  };
10381
10481
  }
10382
10482
  /**
@@ -10853,6 +10953,26 @@ export const api = new RestApi('Api', {
10853
10953
  `
10854
10954
  });
10855
10955
  const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
10956
+ if (frontendFramework === "expo") files.push({
10957
+ path: "constructs/app.ts",
10958
+ content: `import { MobileApp } from '@geekmidas/constructs/mobile-app';
10959
+ import { api } from './api.ts';
10960
+ import { auth } from './auth.ts';
10961
+
10962
+ /**
10963
+ * The mobile app — a construct like a site, which is what makes it an app.
10964
+ *
10965
+ * \`.dependsOn()\` is the single fact behind what a mobile app otherwise has
10966
+ * written down by hand: its API and auth URLs, its URL scheme per stage, the
10967
+ * scheme in the auth server's trusted origins, Better Auth's Expo plugin, and
10968
+ * sign-in links a phone can open.
10969
+ */
10970
+ export const app = new MobileApp('App', { path: 'apps/app' }).dependsOn([
10971
+ api,
10972
+ auth,
10973
+ ]);
10974
+ `
10975
+ });
10856
10976
  if (variant !== void 0) files.push({
10857
10977
  path: "constructs/site.ts",
10858
10978
  content: `import { StaticSite } from '@geekmidas/constructs/site';
@@ -15027,7 +15147,7 @@ var NoStoredSecrets = class extends Error {
15027
15147
  }
15028
15148
  };
15029
15149
  async function reconcile(secrets, workspace, stage) {
15030
- const { derivedContainers } = await import("./workspace-DB2YHgOl.mjs").then((n) => n.a);
15150
+ const { derivedContainers } = await import("./workspace-CIiGijNi.mjs").then((n) => n.a);
15031
15151
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15032
15152
  return result ? {
15033
15153
  secrets: result.secrets,
@@ -15426,7 +15546,7 @@ async function testCommand(options = {}) {
15426
15546
  * starts without the reference data the app needs.
15427
15547
  */
15428
15548
  async function migrateTestStage(workspace, env) {
15429
- const { migrateAndSeed } = await import("./migrate-BbhXxQer.mjs");
15549
+ const { migrateAndSeed } = await import("./migrate-C-D9xIbb.mjs");
15430
15550
  const { discover } = await import("./discover-CMxesskG.mjs").then((n) => n.n);
15431
15551
  const sources = {};
15432
15552
  const manifest = await discover({
@@ -15871,7 +15991,7 @@ program.command("migrate").description("Apply each database construct's migratio
15871
15991
  try {
15872
15992
  const globalOptions = program.opts();
15873
15993
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15874
- const { migrateCommand } = await import("./migrate-BbhXxQer.mjs");
15994
+ const { migrateCommand } = await import("./migrate-C-D9xIbb.mjs");
15875
15995
  await migrateCommand({
15876
15996
  ...options,
15877
15997
  ...construct ? { construct } : {}
@@ -15885,7 +16005,7 @@ program.command("seed").description("Migrate, then run each database construct's
15885
16005
  try {
15886
16006
  const globalOptions = program.opts();
15887
16007
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15888
- const { seedCommand } = await import("./migrate-BbhXxQer.mjs");
16008
+ const { seedCommand } = await import("./migrate-C-D9xIbb.mjs");
15889
16009
  await seedCommand({
15890
16010
  ...options,
15891
16011
  ...construct ? { construct } : {}
@@ -15899,7 +16019,7 @@ program.command("migration").description("Write a construct's next migration: an
15899
16019
  try {
15900
16020
  const globalOptions = program.opts();
15901
16021
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15902
- const { migrationCommand } = await import("./migrate-BbhXxQer.mjs");
16022
+ const { migrationCommand } = await import("./migrate-C-D9xIbb.mjs");
15903
16023
  await migrationCommand({
15904
16024
  construct,
15905
16025
  ...name ? { name } : {}
@@ -16067,7 +16187,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16067
16187
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16068
16188
  process.exit(1);
16069
16189
  }
16070
- const { derivedContainers } = await import("./workspace-DB2YHgOl.mjs").then((n) => n.a);
16190
+ const { derivedContainers } = await import("./workspace-CIiGijNi.mjs").then((n) => n.a);
16071
16191
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16072
16192
  if (!result) {
16073
16193
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16126,7 +16246,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16126
16246
  try {
16127
16247
  const globalOptions = program.opts();
16128
16248
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16129
- const { deployGithubCommand } = await import("./github-DN-92LWP.mjs");
16249
+ const { deployGithubCommand } = await import("./github-C99BMxf0.mjs");
16130
16250
  await deployGithubCommand(options);
16131
16251
  } catch (error) {
16132
16252
  console.error(formatError(error));