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

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 +209 -87
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.mjs +210 -88
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{migrate-u1oRHz6K.cjs → migrate-BsW4xBEO.cjs} +3 -3
  16. package/dist/{migrate-u1oRHz6K.cjs.map → migrate-BsW4xBEO.cjs.map} +1 -1
  17. package/dist/{migrate-BbhXxQer.mjs → migrate-CT8b3nt_.mjs} +3 -3
  18. package/dist/{migrate-BbhXxQer.mjs.map → migrate-CT8b3nt_.mjs.map} +1 -1
  19. package/dist/openapi.cjs +1 -1
  20. package/dist/openapi.mjs +1 -1
  21. package/dist/{workspace-LKFnEBX8.cjs → workspace-C0X8xSXV.cjs} +89 -15
  22. package/dist/workspace-C0X8xSXV.cjs.map +1 -0
  23. package/dist/{workspace-DB2YHgOl.mjs → workspace-EmXwO2qM.mjs} +90 -16
  24. package/dist/workspace-EmXwO2qM.mjs.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-EmXwO2qM.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.36";
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-CT8b3nt_.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-CT8b3nt_.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.36",
8802
+ "@geekmidas/auth": "~10.0.0-alpha.36",
8803
+ "@geekmidas/cache": "~10.0.0-alpha.36",
8804
+ "@geekmidas/client": "~10.0.0-alpha.36",
8805
+ "@geekmidas/cloud": "~10.0.0-alpha.36",
8806
+ "@geekmidas/constructs": "~10.0.0-alpha.36",
8807
+ "@geekmidas/db": "~10.0.0-alpha.36",
8808
+ "@geekmidas/emailkit": "~10.0.0-alpha.36",
8809
+ "@geekmidas/envkit": "~10.0.0-alpha.36",
8810
+ "@geekmidas/errors": "~10.0.0-alpha.36",
8811
+ "@geekmidas/events": "~10.0.0-alpha.36",
8812
+ "@geekmidas/logger": "~10.0.0-alpha.36",
8813
+ "@geekmidas/manifest": "~10.0.0-alpha.36",
8814
+ "@geekmidas/rate-limit": "~10.0.0-alpha.36",
8815
+ "@geekmidas/schema": "~10.0.0-alpha.36",
8816
+ "@geekmidas/services": "~10.0.0-alpha.36",
8817
+ "@geekmidas/storage": "~10.0.0-alpha.36",
8818
+ "@geekmidas/studio": "~10.0.0-alpha.36",
8819
+ "@geekmidas/telescope": "~10.0.0-alpha.36",
8820
+ "@geekmidas/testkit": "~10.0.0-alpha.36",
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,51 @@ 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 — \`${scheme}\`, the same on every stage.
9600
+ * - \`EXPO_PUBLIC_API_URL\` / \`EXPO_PUBLIC_AUTH_URL\`: where the servers
9601
+ * answer. Locally that is their own port on \`localhost\`, which
9602
+ * \`config.ts\` points at the machine Metro was served from.
9603
+ *
9604
+ * Read at runtime from \`Constants.expoConfig.extra.config\` — see \`config.ts\`.
9582
9605
  */
9583
- export default function config(_context: ConfigContext): ExpoConfig {
9606
+ const config = new EnvironmentParser({ ...process.env })
9607
+ .create((get) => ({
9608
+ scheme: get('APP_SCHEME').string(),
9609
+ apiUrl: get('EXPO_PUBLIC_API_URL').string(),
9610
+ authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
9611
+ }))
9612
+ .parse();
9613
+
9614
+ export type Config = typeof config;
9615
+
9616
+ // A bundle id and an Android package allow no hyphens; one per scheme, so
9617
+ // each stage installs beside the others.
9618
+ const bundleId = \`com.\${config.scheme.replace(/[^a-z0-9]/g, '')}.app\`;
9619
+
9620
+ export default function expoConfig({ config: base }: ConfigContext): ExpoConfig {
9584
9621
  return {
9622
+ ...base,
9585
9623
  name: '${options.name}',
9586
9624
  slug: '${slug}',
9587
9625
  version: '0.0.1',
9588
9626
  orientation: 'portrait',
9589
9627
  icon: './assets/icon.png',
9590
- scheme: '${scheme}',
9628
+ scheme: config.scheme,
9591
9629
  userInterfaceStyle: 'automatic',
9592
9630
  newArchEnabled: true,
9593
9631
  ios: {
9594
9632
  supportsTablet: true,
9595
- bundleIdentifier: '${bundleId}',
9633
+ bundleIdentifier: bundleId,
9596
9634
  },
9597
9635
  android: {
9598
- package: '${bundleId}',
9636
+ package: bundleId,
9599
9637
  adaptiveIcon: {
9600
9638
  foregroundImage: './assets/adaptive-icon.png',
9601
9639
  backgroundColor: '#ffffff',
@@ -9621,6 +9659,7 @@ export default function config(_context: ConfigContext): ExpoConfig {
9621
9659
  experiments: {
9622
9660
  typedRoutes: true,
9623
9661
  },
9662
+ extra: { ...base.extra, config },
9624
9663
  };
9625
9664
  }
9626
9665
  `;
@@ -9633,16 +9672,13 @@ export default function config(_context: ConfigContext): ExpoConfig {
9633
9672
  dev: {
9634
9673
  developmentClient: true,
9635
9674
  distribution: "internal",
9636
- environment: "development",
9637
- env: {
9638
- EXPO_PUBLIC_API_URL: "http://localhost:3000",
9639
- EXPO_PUBLIC_AUTH_URL: "http://localhost:3002"
9640
- }
9675
+ environment: "development"
9641
9676
  },
9642
9677
  preview: {
9643
9678
  distribution: "internal",
9644
9679
  environment: "preview",
9645
9680
  env: {
9681
+ APP_SCHEME: scheme,
9646
9682
  EXPO_PUBLIC_API_URL: "https://api.example.com",
9647
9683
  EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
9648
9684
  }
@@ -9651,6 +9687,7 @@ export default function config(_context: ConfigContext): ExpoConfig {
9651
9687
  autoIncrement: true,
9652
9688
  environment: "production",
9653
9689
  env: {
9690
+ APP_SCHEME: scheme,
9654
9691
  EXPO_PUBLIC_API_URL: "https://api.example.com",
9655
9692
  EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
9656
9693
  }
@@ -9712,23 +9749,49 @@ export default {
9712
9749
  `;
9713
9750
  const expoEnvDts = `/// <reference types="expo/types" />\n`;
9714
9751
  const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
9715
- const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
9752
+ const configTs = `import Constants from 'expo-constants';
9753
+ import * as Device from 'expo-device';
9754
+ import { Platform } from 'react-native';
9755
+
9756
+ import type { Config } from './app.config.ts';
9757
+
9758
+ const built = Constants.expoConfig?.extra?.config as Config;
9759
+
9760
+ /** The host this device loaded the bundle from: Metro's, as the device sees it. */
9761
+ const metro = (Constants.expoConfig?.hostUri ?? '').split(':')[0];
9716
9762
 
9717
9763
  /**
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.
9764
+ * A private address on the local network — what a phone on the same Wi-Fi
9765
+ * reaches this machine on. A tunnel's \`*.exp.direct\` host is not one: a
9766
+ * tunnel forwards only Metro, so the servers are not behind it.
9720
9767
  */
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
- });
9768
+ const isLan = (host: string) =>
9769
+ /^10\\./.test(host) ||
9770
+ /^192\\.168\\./.test(host) ||
9771
+ /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host);
9725
9772
 
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();
9773
+ /** \`localhost\` from inside the Android emulator is the emulator itself. */
9774
+ const isAndroidEmulator = Platform.OS === 'android' && !Device.isDevice;
9775
+
9776
+ /**
9777
+ * A server's URL as this device reaches it.
9778
+ *
9779
+ * Locally the servers are injected as \`http://localhost:<port>\`. That is this
9780
+ * device only on the iOS simulator. The Android emulator reaches this machine
9781
+ * at \`10.0.2.2\`, and a phone at the LAN address Metro was served from.
9782
+ * Deployed URLs have no \`localhost\` in them and pass through untouched.
9783
+ */
9784
+ export function reachable(url: string): string {
9785
+ const host = isLan(metro) ? metro : isAndroidEmulator ? '10.0.2.2' : undefined;
9786
+ if (!host) return url;
9787
+ return url.replace(/\\/\\/(localhost|127\\.0\\.0\\.1)(?=[:/]|$)/, \`//\${host}\`);
9788
+ }
9789
+
9790
+ export const config = {
9791
+ scheme: built.scheme,
9792
+ apiUrl: reachable(built.apiUrl),
9793
+ authUrl: reachable(built.authUrl),
9794
+ };
9732
9795
  `;
9733
9796
  const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
9734
9797
 
@@ -9740,21 +9803,23 @@ export const queryClient = new QueryClient({
9740
9803
  },
9741
9804
  });
9742
9805
  `;
9743
- const authClientTs = `import { expoClient } from '@better-auth/expo/client';
9806
+ const authClientTs = `import { expoClient, getSetCookie } from '@better-auth/expo/client';
9744
9807
  import { magicLinkClient } from 'better-auth/client/plugins';
9745
9808
  import { createAuthClient } from 'better-auth/react';
9809
+ import * as Linking from 'expo-linking';
9746
9810
  import * as SecureStore from 'expo-secure-store';
9811
+ import { useEffect } from 'react';
9747
9812
 
9748
9813
  import { config } from '@/config.ts';
9749
9814
 
9750
- export const STORAGE_PREFIX = '${scheme}';
9815
+ export const STORAGE_PREFIX = config.scheme;
9751
9816
  export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
9752
9817
 
9753
9818
  export const authClient = createAuthClient({
9754
9819
  baseURL: config.authUrl,
9755
9820
  plugins: [
9756
9821
  expoClient({
9757
- scheme: '${scheme}',
9822
+ scheme: config.scheme,
9758
9823
  storagePrefix: STORAGE_PREFIX,
9759
9824
  storage: SecureStore,
9760
9825
  }),
@@ -9764,6 +9829,36 @@ export const authClient = createAuthClient({
9764
9829
 
9765
9830
  export const { signIn, useSession } = authClient;
9766
9831
 
9832
+ /**
9833
+ * Sign in from the emailed link.
9834
+ *
9835
+ * The link opens in the phone's browser, which is what receives the session
9836
+ * cookie — not the app. The auth server's Expo plugin carries the cookie on the
9837
+ * redirect back into the app (\`?cookie=\`), and this stores it where the
9838
+ * Expo client keeps its own, merged with what is already there, then tells the
9839
+ * client the session changed. The Expo client does this itself only for a
9840
+ * sign-in it opened (social); a link opened from mail arrives as a deep link.
9841
+ *
9842
+ * Called once, from the root layout.
9843
+ */
9844
+ export function useSessionFromLink() {
9845
+ const url = Linking.useLinkingURL();
9846
+
9847
+ useEffect(() => {
9848
+ const cookie = url ? Linking.parse(url).queryParams?.cookie : undefined;
9849
+ if (typeof cookie !== 'string') return;
9850
+
9851
+ void (async () => {
9852
+ const previous = await SecureStore.getItemAsync(COOKIE_STORE_KEY);
9853
+ await SecureStore.setItemAsync(
9854
+ COOKIE_STORE_KEY,
9855
+ getSetCookie(cookie, previous ?? undefined),
9856
+ );
9857
+ authClient.$store.notify('$sessionSignal');
9858
+ })();
9859
+ }, [url]);
9860
+ }
9861
+
9767
9862
  export async function signOut() {
9768
9863
  try {
9769
9864
  await authClient.signOut();
@@ -9830,7 +9925,7 @@ import { StatusBar } from 'expo-status-bar';
9830
9925
  import { useEffect, useMemo } from 'react';
9831
9926
  import 'react-native-reanimated';
9832
9927
 
9833
- import { authClient } from '@/lib/auth-client.ts';
9928
+ import { authClient, useSessionFromLink } from '@/lib/auth-client.ts';
9834
9929
  import { ApiProvider } from '@/lib/api-context.tsx';
9835
9930
  import { createAppApi } from '@/lib/api.ts';
9836
9931
  import { queryClient } from '@/lib/query-client.ts';
@@ -9855,6 +9950,8 @@ function AuthGate({ children }: { children: React.ReactNode }) {
9855
9950
 
9856
9951
  export default function RootLayout() {
9857
9952
  const api = useMemo(() => createAppApi(), []);
9953
+ // The emailed sign-in link comes back as a deep link carrying the session.
9954
+ useSessionFromLink();
9858
9955
 
9859
9956
  return (
9860
9957
  <QueryClientProvider client={queryClient}>
@@ -10376,7 +10473,8 @@ function rootConstructDependencies(options) {
10376
10473
  pg: DEPENDENCY_VERSIONS.pg,
10377
10474
  ...options.constructs.uploads ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
10378
10475
  ...options.constructs.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
10379
- ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
10476
+ ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {},
10477
+ ...options.frontendFramework === "expo" ? { "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"] } : {}
10380
10478
  };
10381
10479
  }
10382
10480
  /**
@@ -10809,7 +10907,7 @@ export const authDb = database.schema<Record<string, never>, 'AuthDb'>('AuthDb')
10809
10907
  files.push(usersMigration());
10810
10908
  files.push({
10811
10909
  path: "constructs/auth.ts",
10812
- content: `import { BetterAuth } from '@geekmidas/constructs/auth';
10910
+ content: `${frontendFramework === "expo" ? "import { expo } from '@better-auth/expo';\n" : ""}import { BetterAuth } from '@geekmidas/constructs/auth';
10813
10911
  import { authDb } from './database.ts';
10814
10912
 
10815
10913
  /**
@@ -10824,7 +10922,11 @@ import { authDb } from './database.ts';
10824
10922
  export const auth = new BetterAuth('Auth', {
10825
10923
  path: 'apps/auth',
10826
10924
  database: authDb,
10827
- basePath: '/api/auth',
10925
+ basePath: '/api/auth',${frontendFramework === "expo" ? `
10926
+ // The mobile app signs in through Better Auth's Expo plugin: it sends its
10927
+ // scheme as the origin, and this is what reads it. The construct refuses to
10928
+ // start without it once a MobileApp depends on this server.
10929
+ options: { plugins: [expo()] },` : ""}
10828
10930
  });
10829
10931
  `
10830
10932
  });
@@ -10853,6 +10955,26 @@ export const api = new RestApi('Api', {
10853
10955
  `
10854
10956
  });
10855
10957
  const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
10958
+ if (frontendFramework === "expo") files.push({
10959
+ path: "constructs/app.ts",
10960
+ content: `import { MobileApp } from '@geekmidas/constructs/mobile-app';
10961
+ import { api } from './api.ts';
10962
+ import { auth } from './auth.ts';
10963
+
10964
+ /**
10965
+ * The mobile app — a construct like a site, which is what makes it an app.
10966
+ *
10967
+ * \`.dependsOn()\` is the single fact behind what a mobile app otherwise has
10968
+ * written down by hand: its API and auth URLs, its URL scheme, the
10969
+ * scheme in the auth server's trusted origins, Better Auth's Expo plugin, and
10970
+ * sign-in links a phone can open.
10971
+ */
10972
+ export const app = new MobileApp('App', { path: 'apps/app' }).dependsOn([
10973
+ api,
10974
+ auth,
10975
+ ]);
10976
+ `
10977
+ });
10856
10978
  if (variant !== void 0) files.push({
10857
10979
  path: "constructs/site.ts",
10858
10980
  content: `import { StaticSite } from '@geekmidas/constructs/site';
@@ -15027,7 +15149,7 @@ var NoStoredSecrets = class extends Error {
15027
15149
  }
15028
15150
  };
15029
15151
  async function reconcile(secrets, workspace, stage) {
15030
- const { derivedContainers } = await import("./workspace-DB2YHgOl.mjs").then((n) => n.a);
15152
+ const { derivedContainers } = await import("./workspace-EmXwO2qM.mjs").then((n) => n.a);
15031
15153
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15032
15154
  return result ? {
15033
15155
  secrets: result.secrets,
@@ -15426,7 +15548,7 @@ async function testCommand(options = {}) {
15426
15548
  * starts without the reference data the app needs.
15427
15549
  */
15428
15550
  async function migrateTestStage(workspace, env) {
15429
- const { migrateAndSeed } = await import("./migrate-BbhXxQer.mjs");
15551
+ const { migrateAndSeed } = await import("./migrate-CT8b3nt_.mjs");
15430
15552
  const { discover } = await import("./discover-CMxesskG.mjs").then((n) => n.n);
15431
15553
  const sources = {};
15432
15554
  const manifest = await discover({
@@ -15871,7 +15993,7 @@ program.command("migrate").description("Apply each database construct's migratio
15871
15993
  try {
15872
15994
  const globalOptions = program.opts();
15873
15995
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15874
- const { migrateCommand } = await import("./migrate-BbhXxQer.mjs");
15996
+ const { migrateCommand } = await import("./migrate-CT8b3nt_.mjs");
15875
15997
  await migrateCommand({
15876
15998
  ...options,
15877
15999
  ...construct ? { construct } : {}
@@ -15885,7 +16007,7 @@ program.command("seed").description("Migrate, then run each database construct's
15885
16007
  try {
15886
16008
  const globalOptions = program.opts();
15887
16009
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15888
- const { seedCommand } = await import("./migrate-BbhXxQer.mjs");
16010
+ const { seedCommand } = await import("./migrate-CT8b3nt_.mjs");
15889
16011
  await seedCommand({
15890
16012
  ...options,
15891
16013
  ...construct ? { construct } : {}
@@ -15899,7 +16021,7 @@ program.command("migration").description("Write a construct's next migration: an
15899
16021
  try {
15900
16022
  const globalOptions = program.opts();
15901
16023
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15902
- const { migrationCommand } = await import("./migrate-BbhXxQer.mjs");
16024
+ const { migrationCommand } = await import("./migrate-CT8b3nt_.mjs");
15903
16025
  await migrationCommand({
15904
16026
  construct,
15905
16027
  ...name ? { name } : {}
@@ -16067,7 +16189,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16067
16189
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16068
16190
  process.exit(1);
16069
16191
  }
16070
- const { derivedContainers } = await import("./workspace-DB2YHgOl.mjs").then((n) => n.a);
16192
+ const { derivedContainers } = await import("./workspace-EmXwO2qM.mjs").then((n) => n.a);
16071
16193
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16072
16194
  if (!result) {
16073
16195
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16126,7 +16248,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16126
16248
  try {
16127
16249
  const globalOptions = program.opts();
16128
16250
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16129
- const { deployGithubCommand } = await import("./github-DN-92LWP.mjs");
16251
+ const { deployGithubCommand } = await import("./github-C99BMxf0.mjs");
16130
16252
  await deployGithubCommand(options);
16131
16253
  } catch (error) {
16132
16254
  console.error(formatError(error));