@hot-updater/cli-tools 1.2.0-rc.4 → 1.2.0-rc.5

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.
package/dist/index.d.mts CHANGED
@@ -411,19 +411,14 @@ interface CredentialResolver {
411
411
  */
412
412
  dispose(): Promise<void>;
413
413
  }
414
- /**
415
- * The command that stores a credential of `kind` you have as `name` through
416
- * the config's `secrets` adapter, from its files.
417
- */
418
- declare const credentialPutCommand: (kind: CredentialKind, name?: string) => string;
419
414
  /** The credentials Hot Updater issues itself after a sign-in. */
420
415
  type IssuedCredentialKind = Exclude<CredentialKind, "android-keystore">;
421
416
  /**
422
- * The command that signs in once, at a terminal, and stores the key Hot
423
- * Updater issues as `name`: an App Store Connect API key after an Apple
424
- * Account sign-in, a Play service account after a Google one.
417
+ * The command that stores the key Hot Updater issues as `name`: an App
418
+ * Store Connect API key after an Apple Account sign-in, a Play service
419
+ * account after a Google one, and an upload keystore it creates.
425
420
  */
426
- declare const credentialIssueCommand: (kind: IssuedCredentialKind, name?: string) => string;
421
+ declare const credentialIssueCommand: (kind: CredentialKind, name?: string) => string;
427
422
  /** The command group of each issued kind's shortcut, `<group> login`. */
428
423
  declare const CREDENTIAL_LOGIN_GROUPS: {
429
424
  readonly "asc-api-key": "apple";
@@ -438,7 +433,8 @@ declare const CREDENTIAL_LOGIN_GROUPS: {
438
433
  declare const credentialLoginCommand: (kind: IssuedCredentialKind, name?: string) => string;
439
434
  /**
440
435
  * The command that stores a missing credential of `kind` as `name`: the
441
- * keystore you have, or the key Hot Updater issues after a sign-in.
436
+ * upload keystore Hot Updater creates, or the key it issues after a
437
+ * sign-in.
442
438
  */
443
439
  declare const credentialStoreCommand: (kind: CredentialKind, name?: string) => string;
444
440
  declare class CredentialUnavailableError extends Error {
@@ -569,6 +565,15 @@ type WriteHotUpdaterConfigResult = {
569
565
  path: string;
570
566
  reason?: string;
571
567
  };
568
+ /**
569
+ * `const <name> = secretEnv([...names])`, a helper a managed config reads
570
+ * credentials with: from the environment and `.env.hotupdater` first, then
571
+ * from the config's `secrets` adapter. `storageSecrets` and
572
+ * `databaseSecrets` read each adapter's own, so a command that reaches only
573
+ * one needs only its credentials; `deploySecrets` reads what both share.
574
+ * Its builder imports `secretEnv` from `hot-updater`.
575
+ */
576
+ declare const secretEnvHelper: (name: "databaseSecrets" | "deploySecrets" | "storageSecrets", names: readonly string[]) => ManagedHelperStatement;
572
577
  declare const createHotUpdaterConfigScaffold: ({ build, storage, database, plugins, extraImports, helperStatements, updateStrategy }: CreateHotUpdaterConfigScaffoldOptions) => HotUpdaterConfigScaffold;
573
578
  declare const createHotUpdaterConfigScaffoldFromBuilder: (builder: ConfigBuilder, { helperStatements, updateStrategy }?: CreateHotUpdaterConfigScaffoldFromBuilderOptions) => HotUpdaterConfigScaffold;
574
579
  /**
@@ -596,7 +601,12 @@ type HotUpdaterInitEnv = {
596
601
  readonly managedEnv: Readonly<Record<string, string>>;
597
602
  };
598
603
  declare const getHotUpdaterInitInputEnv: ({ env, managedEnv }: HotUpdaterInitEnv, nonInteractive: boolean) => Readonly<Record<string, string>>;
599
- declare const readHotUpdaterInitEnv: (cwd: string, envFile?: string) => Promise<HotUpdaterInitEnv>;
604
+ /**
605
+ * Init's inputs: `.env.hotupdater` (`managedEnv`), with `envFile`'s over
606
+ * it (`env`). `secretsEnv`, the credentials init read from a team secrets
607
+ * store, sits under both, as values the file would hold.
608
+ */
609
+ declare const readHotUpdaterInitEnv: (cwd: string, envFile?: string, secretsEnv?: Readonly<Record<string, string>>) => Promise<HotUpdaterInitEnv>;
600
610
  declare const getHotUpdaterEnvValue: (env: Readonly<Record<string, string>>, key: string) => string | undefined;
601
611
  /**
602
612
  * An env file's variables as `process.loadEnvFile` sets them, the way
@@ -668,6 +678,13 @@ type InitProviderInputsDefinition = Readonly<Record<string, InitProviderInputDef
668
678
  type InitProviderDefinition<TInputs extends InitProviderInputsDefinition = InitProviderInputsDefinition> = {
669
679
  readonly inputs: TInputs;
670
680
  readonly label: string;
681
+ /**
682
+ * The variables of the credentials the config init writes reads with
683
+ * `secretEnv`, such as an API token. With a team `secrets` adapter, init
684
+ * stores them there with the app's API key, and reads one the
685
+ * environment and the env file lack from it.
686
+ */
687
+ readonly secrets?: readonly string[];
671
688
  };
672
689
  declare const defineInitProvider: <const TInputs extends InitProviderInputsDefinition>(provider: InitProviderDefinition<TInputs>) => InitProviderDefinition<TInputs>;
673
690
  declare const shouldAutoSelectOnlyInitResource: ({ availableResourceCount, savedIdentifier }: {
@@ -706,6 +723,12 @@ declare const getInitProviderEnvVars: <TInputs extends InitProviderInputsDefinit
706
723
  type RunInitOptions = {
707
724
  readonly build: BuildType;
708
725
  readonly envFile?: string;
726
+ /**
727
+ * Credentials the environment and the env files lack, read from the
728
+ * config's team `secrets` adapter (`InitProviderDefinition.secrets`):
729
+ * init reads them as if `.env.hotupdater` held them.
730
+ */
731
+ readonly secretsEnv?: Readonly<Record<string, string>>;
709
732
  };
710
733
  declare class InitError extends Error {
711
734
  readonly name: string;
@@ -909,8 +932,8 @@ type ConfigResponse = RequiredDeep<Omit<ConfigInput, "database" | "storage" | "p
909
932
  signing?: ReturnType<typeof normalizeSigningConfig>;
910
933
  /**
911
934
  * Where `secret:` references and issued keys are read and stored: the
912
- * config's adapter with `localSecrets()`'s store after it (`withLocalKeyStore`),
913
- * or `localSecrets()` when it sets none.
935
+ * config's adapter alone, or `localSecrets()` when it sets none
936
+ * (`resolveSecretsAdapter`).
914
937
  */
915
938
  secrets: SecretsAdapter;
916
939
  };
@@ -927,6 +950,11 @@ declare const loadPlatformConfigs: <TPlatform extends Platform>(platforms: reado
927
950
  readonly platform: TPlatform;
928
951
  readonly config: ConfigResponse;
929
952
  }[]>;
953
+ /**
954
+ * The names every `secretEnv` the config created while it loaded reads,
955
+ * such as `HOT_UPDATER_CLOUDFLARE_API_TOKEN`, in the order it created them.
956
+ */
957
+ declare const getSecretEnvNames: (config: ConfigResponse) => readonly string[];
930
958
  //#endregion
931
959
  //#region src/serverDefinition.d.ts
932
960
  /** A plugin's endpoint on a server's `handlers.client`. */
@@ -2410,9 +2438,9 @@ declare const LOCAL_SECRETS_NAME = "localSecrets";
2410
2438
  * `~/.hot-updater/secrets/<project-key>/<name>.json`; `set` writes that
2411
2439
  * file, 0600 in a 0700 directory, outside the project, so it survives
2412
2440
  * `node_modules` and is never committed. `HOT_UPDATER_HOME` moves
2413
- * `~/.hot-updater`. The project key is the same for every clone of one app
2414
- * on a machine (its `origin` remote and its directory in the repository),
2415
- * so another machine signs in and issues a key of its own.
2441
+ * `~/.hot-updater`. The project key hashes the checkout's real path
2442
+ * (`getProjectKey`), so each clone of an app has its own directory, and
2443
+ * another clone or machine signs in and issues a key of its own.
2416
2444
  *
2417
2445
  * @example
2418
2446
  * export default defineConfig({ secrets: localSecrets(), ... });
@@ -2424,36 +2452,31 @@ interface LocalSecretsOptions {
2424
2452
  * each, such as `~/.hot-updater/secrets/my-app`: `~` is the home
2425
2453
  * directory, and a relative path is the project's. A directory in a git
2426
2454
  * work tree must be one git ignores. Defaults to
2427
- * `~/.hot-updater/secrets/<project-key>`, whose key is the same for every
2428
- * clone of the app on a machine (`getProjectKey`).
2455
+ * `~/.hot-updater/secrets/<project-key>`, whose key hashes the
2456
+ * checkout's real path, one per clone (`getProjectKey`).
2429
2457
  */
2430
2458
  readonly path?: string;
2431
2459
  }
2432
2460
  /**
2433
- * The config's `secrets` adapter with `localSecrets()`'s store after it:
2434
- * `get` reads each name the adapter does not hold from
2435
- * `~/.hot-updater/secrets/<project-key>/<name>.json`, and `set` stores there
2436
- * when the adapter stores nothing. Without an adapter, it is
2437
- * `localSecrets()` itself. Keys Hot Updater issues, and credentials
2438
- * `secrets put` imports, are reused by the next build on this machine with
2439
- * no step in between.
2461
+ * The `secrets` adapter a config load uses: the config's own adapter as it
2462
+ * is, with no store on this machine under it, or `localSecrets()` when the
2463
+ * config sets none.
2440
2464
  */
2441
- declare const withLocalKeyStore: (configured?: SecretsAdapter) => SecretsAdapter;
2465
+ declare const resolveSecretsAdapter: (configured?: SecretsAdapter) => SecretsAdapter;
2442
2466
  /**
2443
- * The `localSecrets()` store `secrets` reads after its adapter, and stores
2444
- * in when the adapter stores nothing: `localSecrets({ path })`'s, else the
2445
- * default one.
2467
+ * `localSecrets()`'s store, for `localSecrets()` itself; undefined for any
2468
+ * other adapter, which keeps nothing on this machine.
2446
2469
  */
2447
- declare const getLocalSecretsStore: (secrets: SecretsAdapter) => LocalSecretsStore;
2470
+ declare const getLocalSecretsStore: (secrets: SecretsAdapter) => LocalSecretsStore | undefined;
2448
2471
  /**
2449
- * The config's own `secrets` adapter, without `localSecrets()`'s store
2450
- * after it: undefined for `localSecrets()`, and when the config sets none.
2451
- * `secretEnv` reads deploy credentials from it.
2472
+ * The config's own `secrets` adapter: undefined for `localSecrets()`,
2473
+ * which is the adapter when the config sets none. `secretEnv` reads deploy
2474
+ * credentials from it.
2452
2475
  */
2453
2476
  declare const getConfiguredAdapter: (secrets: SecretsAdapter) => SecretsAdapter | undefined;
2454
2477
  /**
2455
- * Where `secrets.set` stores `name`, for messages: the config's adapter
2456
- * when it stores values, else `localSecrets()`'s file.
2478
+ * Where `secrets.set` stores `name`, for messages: the config's adapter,
2479
+ * else `localSecrets()`'s file.
2457
2480
  */
2458
2481
  declare const describeStoredSecret: (secrets: SecretsAdapter, name: string) => string;
2459
2482
  /**
@@ -2473,10 +2496,12 @@ declare const readSecretWithSource: (secrets: SecretsAdapter, name: string, cwd:
2473
2496
  declare const keepsKeysOnThisMachine: (secrets: SecretsAdapter) => boolean;
2474
2497
  /**
2475
2498
  * The `secrets` adapter of one config load, filled once the config it
2476
- * evaluates resolves: each `secretEnv` the evaluation creates reads from it.
2499
+ * evaluates resolves: each `secretEnv` the evaluation creates reads from it,
2500
+ * and adds the names it reads to `names`.
2477
2501
  */
2478
2502
  interface SecretsScope {
2479
2503
  secrets?: SecretsAdapter;
2504
+ names?: Set<string>;
2480
2505
  }
2481
2506
  /**
2482
2507
  * Evaluates a config, the file or a config function, with `scope` as the
@@ -2493,8 +2518,8 @@ declare const withSecretsScope: <T>(scope: SecretsScope, evaluate: () => Promise
2493
2518
  */
2494
2519
  declare const setConfiguredSecrets: (secrets: SecretsAdapter) => void;
2495
2520
  /**
2496
- * The config's `secrets` adapter with `localSecrets()`'s store after it
2497
- * (`withLocalKeyStore`), `localSecrets()` until a config loads.
2521
+ * The config's `secrets` adapter (`resolveSecretsAdapter`), `localSecrets()`
2522
+ * until a config loads.
2498
2523
  */
2499
2524
  declare const getConfiguredSecrets: () => SecretsAdapter;
2500
2525
  /** Where an adapter keeps `name`, for messages. */
@@ -2524,6 +2549,38 @@ declare const describeSecret: (secrets: SecretsAdapter, name: string) => string;
2524
2549
  */
2525
2550
  declare const secretEnv: <const TName extends string>(names: readonly TName[]) => (() => Promise<Readonly<Record<TName, string>>>);
2526
2551
  //#endregion
2552
+ //#region src/teamSecrets.d.ts
2553
+ /**
2554
+ * The client API key the managed servers' `apiKeys()` gives an app, which
2555
+ * init saves (`API_KEY_ENV` in `@hot-updater/plugin-api-keys`). The config
2556
+ * never reads it; a team store shares it with the deploy credentials, so a
2557
+ * teammate's init registers the same key again rather than a new one.
2558
+ */
2559
+ declare const MANAGED_CLIENT_CREDENTIAL_ENV = "HOT_UPDATER_API_KEY";
2560
+ /**
2561
+ * The config's own `secrets` adapter, the team store: undefined when the
2562
+ * config sets none, which is `localSecrets()`. Loading the config runs its
2563
+ * `process.loadEnvFile(".env.hotupdater")`, which init must not see, so
2564
+ * `process.env` is restored after it.
2565
+ */
2566
+ declare const loadTeamSecrets: () => Promise<SecretsAdapter | undefined>;
2567
+ /**
2568
+ * `names` neither the environment nor `env`, init's env files, sets, read
2569
+ * from `secrets` in one call: the values a teammate's init, or a run after
2570
+ * the file's lines were deleted, replays with.
2571
+ */
2572
+ declare const readMissingFromTeamSecrets: (secrets: SecretsAdapter, names: readonly string[], { env, cwd }: {
2573
+ readonly env: Readonly<Record<string, string>>;
2574
+ readonly cwd: string;
2575
+ }) => Promise<Readonly<Record<string, string>>>;
2576
+ /**
2577
+ * Stores each of `names` that `.env.hotupdater` holds through `secrets`,
2578
+ * then reads them back in one call. The file is left as it is. Returns the
2579
+ * names stored, in the order of `names`; throws when the adapter reads
2580
+ * back another value.
2581
+ */
2582
+ declare const storeEnvFileSecrets: (secrets: SecretsAdapter, names: readonly string[], cwd: string) => Promise<readonly string[]>;
2583
+ //#endregion
2527
2584
  //#region src/storageFiles.d.ts
2528
2585
  declare const getStorageFileByteSize: (filePath: string) => Promise<number>;
2529
2586
  declare const putStorageFile: (storage: StorageAdapterWith<"put">, key: string, filePath: string) => Promise<StoragePutResult & {
@@ -2547,4 +2604,4 @@ type TransformTemplateArgs<T extends string> = { [Key in ExtractPlaceholders<T>]
2547
2604
  */
2548
2605
  declare function transformTemplate<T extends string>(templateString: T, values: TransformTemplateArgs<T>): string;
2549
2606
  //#endregion
2550
- export { AppClientCredential, AppClientPlugin, AssembleServerOptions, AssembledServer, BuildFailedError, BuildFailureSummaryConfig, BuildLogger, BuildLoggerConfig, BuildType, BundleSigningSession, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ClientAuthSpec, ClientCredentialSpec, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateCredentialResolverOptions, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, type CredentialFiles, CredentialKind, CredentialRequest, CredentialResolver, CredentialUnavailableError, Credentials, DEFAULT_SIGNING_KEY_SECRET, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, INTERFACE_FINGERPRINT_ENGINE, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, IssuedCredentialKind, LOCAL_SECRETS_NAME, LegacyInfrastructureError, LocalSecretsOptions, LocalSecretsStore, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, NativeDependency, NativeInterface, PromptProgress, PromptSpinner, ProviderConfig, ProvisionedClientCredential, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, SecretsScope, ServerClientEndpoint, ServerDefinition, ServerDefinitionError, WriteHotUpdaterConfigResult, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeSecret, describeStoredSecret, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readHotUpdaterInitEnv, readLoadedHotUpdaterEnv, readLocalKey, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, secretEnv, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, withCredentialResolver, withLocalKeyStore, withSecretsScope, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
2607
+ export { AppClientCredential, AppClientPlugin, AssembleServerOptions, AssembledServer, BuildFailedError, BuildFailureSummaryConfig, BuildLogger, BuildLoggerConfig, BuildType, BundleSigningSession, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ClientAuthSpec, ClientCredentialSpec, ConfigBuilder, ConfigBuilderScaffold, ConfigResponse, CreateCredentialResolverOptions, CreateHotUpdaterConfigScaffoldFromBuilderOptions, CreateHotUpdaterConfigScaffoldOptions, type CredentialFiles, CredentialKind, CredentialRequest, CredentialResolver, CredentialUnavailableError, Credentials, DEFAULT_SIGNING_KEY_SECRET, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, HotUpdaterConfigOptions, HotUpdaterConfigScaffold, HotUpdaterInitEnv, HotUpdaterLogWriter, INTERFACE_FINGERPRINT_ENGINE, ImportInfo, InitEnvFileError, InitError, InitProviderDefinition, InitProviderInputDefinition, InitProviderInputPersistence, IssuedCredentialKind, LOCAL_SECRETS_NAME, LegacyInfrastructureError, LocalSecretsOptions, LocalSecretsStore, MANAGED_CLIENT_CREDENTIAL_ENV, ManagedHelperStatement, ManagedHelperStrategy, MissingInitInputsError, NativeDependency, NativeInterface, PromptProgress, PromptSpinner, ProviderConfig, ProvisionedClientCredential, ReactNativeMetadata, ReadPackageUpResult, RunInitOptions, SecretsScope, ServerClientEndpoint, ServerDefinition, ServerDefinitionError, WriteHotUpdaterConfigResult, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeSecret, describeStoredSecret, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getSecretEnvNames, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readHotUpdaterInitEnv, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secretEnv, secretEnvHelper, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, storeEnvFileSecrets, stripAnsi, transformEnv, transformTemplate, withCredentialResolver, withSecretsScope, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
package/dist/index.mjs CHANGED
@@ -25247,9 +25247,17 @@ const readEnvFile = async (filePath, allowMissing) => {
25247
25247
  return parseEnv$1(content);
25248
25248
  };
25249
25249
  const getHotUpdaterInitInputEnv = ({ env, managedEnv }, nonInteractive) => nonInteractive ? env : managedEnv;
25250
- const readHotUpdaterInitEnv = async (cwd, envFile) => {
25250
+ /**
25251
+ * Init's inputs: `.env.hotupdater` (`managedEnv`), with `envFile`'s over
25252
+ * it (`env`). `secretsEnv`, the credentials init read from a team secrets
25253
+ * store, sits under both, as values the file would hold.
25254
+ */
25255
+ const readHotUpdaterInitEnv = async (cwd, envFile, secretsEnv = {}) => {
25251
25256
  const savedEnvPath = path.resolve(cwd, HOT_UPDATER_ENV_PATH);
25252
- const savedEnv = await readEnvFile(savedEnvPath, true);
25257
+ const savedEnv = {
25258
+ ...secretsEnv,
25259
+ ...await readEnvFile(savedEnvPath, true)
25260
+ };
25253
25261
  if (!envFile) return {
25254
25262
  env: {},
25255
25263
  managedEnv: savedEnv
@@ -25511,8 +25519,7 @@ const RESERVED_PREFIXES = [
25511
25519
  "LD_",
25512
25520
  "DYLD_",
25513
25521
  "GIT_",
25514
- "NPM_CONFIG_",
25515
- "HOT_UPDATER_GOOGLE_OAUTH_CLIENT_"
25522
+ "NPM_CONFIG_"
25516
25523
  ];
25517
25524
  /**
25518
25525
  * Whether `name` is a variable no secret may set: one a loader or
@@ -25572,8 +25579,6 @@ const readSecretVariables = async (names, cwd) => {
25572
25579
  /** `localSecrets()`'s name, the `secrets` adapter when the config sets none. */
25573
25580
  const LOCAL_SECRETS_NAME = "localSecrets";
25574
25581
  const LOCAL_KEY_STORE = Symbol.for("@hot-updater/cli-tools/local-key-store");
25575
- /** `localSecrets()`'s store without a `path`, the one after any adapter. */
25576
- const defaultLocalSecretsStore = createLocalSecretsStore();
25577
25582
  const isLocalKeyStoreSecrets = (secrets) => LOCAL_KEY_STORE in secrets;
25578
25583
  /**
25579
25584
  * Secrets in JSON files on this machine, the `secrets` adapter when the
@@ -25582,77 +25587,52 @@ const isLocalKeyStoreSecrets = (secrets) => LOCAL_KEY_STORE in secrets;
25582
25587
  * `~/.hot-updater/secrets/<project-key>/<name>.json`; `set` writes that
25583
25588
  * file, 0600 in a 0700 directory, outside the project, so it survives
25584
25589
  * `node_modules` and is never committed. `HOT_UPDATER_HOME` moves
25585
- * `~/.hot-updater`. The project key is the same for every clone of one app
25586
- * on a machine (its `origin` remote and its directory in the repository),
25587
- * so another machine signs in and issues a key of its own.
25590
+ * `~/.hot-updater`. The project key hashes the checkout's real path
25591
+ * (`getProjectKey`), so each clone of an app has its own directory, and
25592
+ * another clone or machine signs in and issues a key of its own.
25588
25593
  *
25589
25594
  * @example
25590
25595
  * export default defineConfig({ secrets: localSecrets(), ... });
25591
25596
  */
25592
- const localSecrets = (options = {}) => createLocalKeyStoreSecrets(void 0, createLocalSecretsStore(options));
25597
+ const localSecrets = (options = {}) => createLocalSecrets(createLocalSecretsStore(options));
25593
25598
  /**
25594
- * The config's `secrets` adapter with `localSecrets()`'s store after it:
25595
- * `get` reads each name the adapter does not hold from
25596
- * `~/.hot-updater/secrets/<project-key>/<name>.json`, and `set` stores there
25597
- * when the adapter stores nothing. Without an adapter, it is
25598
- * `localSecrets()` itself. Keys Hot Updater issues, and credentials
25599
- * `secrets put` imports, are reused by the next build on this machine with
25600
- * no step in between.
25599
+ * The `secrets` adapter a config load uses: the config's own adapter as it
25600
+ * is, with no store on this machine under it, or `localSecrets()` when the
25601
+ * config sets none.
25601
25602
  */
25602
- const withLocalKeyStore = (configured) => configured && isLocalKeyStoreSecrets(configured) ? configured : createLocalKeyStoreSecrets(configured, defaultLocalSecretsStore);
25603
- const createLocalKeyStoreSecrets = (configured, store) => {
25604
- const describeFile = (name) => store.file(name, getCwd());
25603
+ const resolveSecretsAdapter = (configured) => configured ?? localSecrets();
25604
+ const createLocalSecrets = (store) => {
25605
25605
  return {
25606
- name: configured?.name ?? "localSecrets",
25607
- [LOCAL_KEY_STORE]: configured ? {
25608
- configured,
25609
- store
25610
- } : { store },
25606
+ name: LOCAL_SECRETS_NAME,
25607
+ [LOCAL_KEY_STORE]: { store },
25611
25608
  get: async (names, context) => {
25612
- const values = configured ? { ...await configured.get(names, context) } : await readSecretVariables(names, context.cwd);
25609
+ const values = await readSecretVariables(names, context.cwd);
25613
25610
  const read = Object.create(null);
25614
- for (const name of names) {
25615
- const value = Object.hasOwn(values, name) ? values[name] : void 0;
25616
- read[name] = typeof value === "string" && value ? value : await store.read(name, context.cwd);
25617
- }
25611
+ for (const name of names) read[name] = values[name] || await store.read(name, context.cwd);
25618
25612
  return read;
25619
25613
  },
25620
25614
  set: async (name, value, context) => {
25621
- if (configured?.set) {
25622
- await configured.set(name, value, context);
25623
- if (await store.read(name, context.cwd) === void 0) return;
25624
- try {
25625
- await store.write(name, value, context.cwd);
25626
- } catch (error) {
25627
- p.log.warn(`The ${configured.name} secrets adapter stored ${name}, and the copy on this machine at ${store.file(name, context.cwd)} still holds the value it replaced: ${error instanceof Error ? error.message : String(error)}. Delete that file.`);
25628
- }
25629
- return;
25630
- }
25631
25615
  await store.write(name, value, context.cwd);
25632
25616
  },
25633
- describe: (name) => configured ? `${describeSecret(configured, name)}, or ${describeFile(name)} on this machine` : `${secretVariableName(name)} in the environment, or ${describeFile(name)} on this machine`
25617
+ describe: (name) => `${secretVariableName(name)} in the environment, or ${store.file(name, getCwd())} on this machine`
25634
25618
  };
25635
25619
  };
25636
25620
  /**
25637
- * The `localSecrets()` store `secrets` reads after its adapter, and stores
25638
- * in when the adapter stores nothing: `localSecrets({ path })`'s, else the
25639
- * default one.
25621
+ * `localSecrets()`'s store, for `localSecrets()` itself; undefined for any
25622
+ * other adapter, which keeps nothing on this machine.
25640
25623
  */
25641
- const getLocalSecretsStore = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].store : defaultLocalSecretsStore;
25624
+ const getLocalSecretsStore = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].store : void 0;
25642
25625
  /**
25643
- * The config's own `secrets` adapter, without `localSecrets()`'s store
25644
- * after it: undefined for `localSecrets()`, and when the config sets none.
25645
- * `secretEnv` reads deploy credentials from it.
25626
+ * The config's own `secrets` adapter: undefined for `localSecrets()`,
25627
+ * which is the adapter when the config sets none. `secretEnv` reads deploy
25628
+ * credentials from it.
25646
25629
  */
25647
- const getConfiguredAdapter = (secrets) => isLocalKeyStoreSecrets(secrets) ? secrets[LOCAL_KEY_STORE].configured : secrets;
25630
+ const getConfiguredAdapter = (secrets) => isLocalKeyStoreSecrets(secrets) ? void 0 : secrets;
25648
25631
  /**
25649
- * Where `secrets.set` stores `name`, for messages: the config's adapter
25650
- * when it stores values, else `localSecrets()`'s file.
25632
+ * Where `secrets.set` stores `name`, for messages: the config's adapter,
25633
+ * else `localSecrets()`'s file.
25651
25634
  */
25652
- const describeStoredSecret = (secrets, name) => {
25653
- const configured = getConfiguredAdapter(secrets);
25654
- return configured?.set ? describeSecret(configured, name) : getLocalSecretsStore(secrets).file(name, getCwd());
25655
- };
25635
+ const describeStoredSecret = (secrets, name) => getLocalSecretsStore(secrets)?.file(name, getCwd()) ?? describeSecret(secrets, name);
25656
25636
  /**
25657
25637
  * `name`'s value, read in the order `secrets.get` reads it, with the one
25658
25638
  * place it came from, for messages: the config's adapter
@@ -25661,27 +25641,19 @@ const describeStoredSecret = (secrets, name) => {
25661
25641
  */
25662
25642
  const readSecretWithSource = async (secrets, name, cwd) => {
25663
25643
  const held = (value) => typeof value === "string" && value.length > 0;
25664
- if (!isLocalKeyStoreSecrets(secrets)) {
25644
+ const store = getLocalSecretsStore(secrets);
25645
+ if (!store) {
25665
25646
  const value = (await secrets.get([name], { cwd }))[name];
25666
25647
  return held(value) ? {
25667
25648
  value,
25668
25649
  source: describeSecret(secrets, name)
25669
25650
  } : void 0;
25670
25651
  }
25671
- const { configured, store } = secrets[LOCAL_KEY_STORE];
25672
- if (configured) {
25673
- const value = (await configured.get([name], { cwd }))[name];
25674
- if (held(value)) return {
25675
- value,
25676
- source: describeSecret(configured, name)
25677
- };
25678
- } else {
25679
- const value = (await readSecretVariables([name], cwd))[name];
25680
- if (held(value)) return {
25681
- value,
25682
- source: `${secretVariableName(name)} (environment)`
25683
- };
25684
- }
25652
+ const variable = (await readSecretVariables([name], cwd))[name];
25653
+ if (held(variable)) return {
25654
+ value: variable,
25655
+ source: `${secretVariableName(name)} (environment)`
25656
+ };
25685
25657
  const value = await store.read(name, cwd);
25686
25658
  return held(value) ? {
25687
25659
  value,
@@ -25721,13 +25693,13 @@ const setConfiguredSecrets = (secrets) => {
25721
25693
  globalThis[CONFIGURED_SECRETS] = secrets;
25722
25694
  };
25723
25695
  /**
25724
- * The config's `secrets` adapter with `localSecrets()`'s store after it
25725
- * (`withLocalKeyStore`), `localSecrets()` until a config loads.
25696
+ * The config's `secrets` adapter (`resolveSecretsAdapter`), `localSecrets()`
25697
+ * until a config loads.
25726
25698
  */
25727
25699
  const getConfiguredSecrets = () => globalThis[CONFIGURED_SECRETS] ?? localSecrets();
25728
25700
  /** Where an adapter keeps `name`, for messages. */
25729
25701
  const describeSecret = (secrets, name) => secrets.describe?.(name) ?? `${name} in ${secrets.name}`;
25730
- const list = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
25702
+ const list$1 = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
25731
25703
  /**
25732
25704
  * Environment variables from the `secrets` adapter of the config that
25733
25705
  * creates it, as a function a config calls when an adapter needs them, so loading
@@ -25756,35 +25728,44 @@ const secretEnv = (names) => {
25756
25728
  const invalid = wanted.filter((name) => !ENV_NAME_PATTERN.test(name));
25757
25729
  if (invalid.length > 0) throw new Error(`secretEnv takes environment variable names: letters, digits, and underscores, not starting with a digit, and ${invalid.map((name) => JSON.stringify(name)).join(", ")} ${invalid.length === 1 ? "is" : "are"} not.`);
25758
25730
  const reserved = wanted.filter(isReservedVariableName);
25759
- if (reserved.length > 0) throw new Error(`secretEnv refuses ${list(reserved)}: ${reserved.length === 1 ? "it changes" : "they change"} how a process loads code or which credentials Hot Updater uses.`);
25731
+ if (reserved.length > 0) throw new Error(`secretEnv refuses ${list$1(reserved)}: ${reserved.length === 1 ? "it changes" : "they change"} how a process loads code or which credentials Hot Updater uses.`);
25760
25732
  const scope = globalThis[LOADING_SCOPE];
25761
- return async () => {
25762
- const cwd = getCwd();
25763
- const fileEnv = await readLoadedHotUpdaterEnv(cwd);
25764
- const variables = Object.create(null);
25765
- const fromStore = [];
25766
- for (const name of wanted) {
25767
- const value = getHotUpdaterEnvValue(fileEnv, name);
25768
- if (value === void 0) fromStore.push(name);
25769
- else variables[name] = value;
25770
- }
25771
- if (fromStore.length > 0) {
25772
- const secrets = getConfiguredAdapter(scope?.secrets ?? getConfiguredSecrets());
25773
- const read = secrets ? await secrets.get(fromStore, { cwd }) : {};
25774
- const missing = fromStore.filter((name) => {
25775
- const value = Object.hasOwn(read, name) ? read[name] : void 0;
25776
- if (typeof value !== "string" || value === "") return true;
25777
- variables[name] = value;
25778
- return false;
25779
- });
25780
- if (missing.length > 0) {
25781
- const them = missing.length === 1 ? "it" : "them";
25782
- throw new Error(secrets === void 0 ? `secretEnv found no value for ${list(missing)}: set ${them} in the environment or .env.hotupdater, or read ${them} from a secrets store with the config's secrets.` : `secretEnv found no value for ${list(missing)}: ${missing.length === 1 ? "it is" : "they are"} not set in the environment or .env.hotupdater, and the ${secrets.name} secrets adapter does not hold ${them} (${missing.map((name) => describeSecret(secrets, name)).join("; ")}).`);
25783
- }
25733
+ if (scope) {
25734
+ scope.names ??= /* @__PURE__ */ new Set();
25735
+ for (const name of wanted) scope.names.add(name);
25736
+ }
25737
+ let read;
25738
+ return () => read ??= readSecretEnv(wanted, scope).catch((error) => {
25739
+ read = void 0;
25740
+ throw error;
25741
+ });
25742
+ };
25743
+ const readSecretEnv = async (wanted, scope) => {
25744
+ const cwd = getCwd();
25745
+ const fileEnv = await readLoadedHotUpdaterEnv(cwd);
25746
+ const variables = Object.create(null);
25747
+ const fromStore = [];
25748
+ for (const name of wanted) {
25749
+ const value = getHotUpdaterEnvValue(fileEnv, name);
25750
+ if (value === void 0) fromStore.push(name);
25751
+ else variables[name] = value;
25752
+ }
25753
+ if (fromStore.length > 0) {
25754
+ const secrets = getConfiguredAdapter(scope?.secrets ?? getConfiguredSecrets());
25755
+ const read = secrets ? await secrets.get(fromStore, { cwd }) : {};
25756
+ const missing = fromStore.filter((name) => {
25757
+ const value = Object.hasOwn(read, name) ? read[name] : void 0;
25758
+ if (typeof value !== "string" || value === "") return true;
25759
+ variables[name] = value;
25760
+ return false;
25761
+ });
25762
+ if (missing.length > 0) {
25763
+ const them = missing.length === 1 ? "it" : "them";
25764
+ throw new Error(secrets === void 0 ? `secretEnv found no value for ${list$1(missing)}: set ${them} in the environment or .env.hotupdater, or read ${them} from a secrets store with the config's secrets.` : `secretEnv found no value for ${list$1(missing)}: ${missing.length === 1 ? "it is" : "they are"} not set in the environment or .env.hotupdater, and the ${secrets.name} secrets adapter does not hold ${them} (${missing.map((name) => describeSecret(secrets, name)).join("; ")}).`);
25784
25765
  }
25785
- for (const value of Object.values(variables)) registerSecretValue(value);
25786
- return Object.freeze(variables);
25787
- };
25766
+ }
25767
+ for (const value of Object.values(variables)) registerSecretValue(value);
25768
+ return Object.freeze(variables);
25788
25769
  };
25789
25770
  //#endregion
25790
25771
  //#region src/localBundleSigning.ts
@@ -38744,21 +38725,33 @@ const formatCommand = (command) => {
38744
38725
  return lines.join("\n");
38745
38726
  };
38746
38727
  /**
38747
- * The command that stores a credential of `kind` you have as `name` through
38748
- * the config's `secrets` adapter, from its files.
38728
+ * `lead` and the command it names on one line when they fit 77 columns,
38729
+ * else the command on the lines after it.
38749
38730
  */
38750
- const credentialPutCommand = (kind, name = kind) => {
38751
- const put = `npx hot-updater secrets put ${name} --kind ${kind}`;
38752
- switch (kind) {
38753
- case "android-keystore": return `${put} --keystore <release.jks> --key-alias <alias>`;
38754
- case "asc-api-key": return `${put} --p8 <AuthKey.p8> --key-id <id> --issuer-id <id>`;
38755
- case "play-service-account": return `${put} --service-account <key.json>`;
38756
- }
38731
+ const withLead = (lead, command) => lead.length + 1 + command.length <= 77 ? `${lead} ${command}` : `${lead}\n${formatCommand(command)}`;
38732
+ /**
38733
+ * Why a build has no upload keystore, in three lines, steering away from a
38734
+ * second key: Play rejects builds signed with a key it does not have.
38735
+ * Under a team's adapter the keystore is someone's already, so it is
38736
+ * imported, never created; only `localSecrets()` alone offers a new one.
38737
+ */
38738
+ const describeMissingKeystore = (store, item, name) => {
38739
+ const configured = getConfiguredAdapter(store);
38740
+ if (configured) return [
38741
+ `No ${item} in ${describeSecret(configured, name)}.`,
38742
+ "Ask whoever set it up to store it there, or import it with:",
38743
+ "npx hot-updater google-play login"
38744
+ ].join("\n");
38745
+ return [
38746
+ `No ${item} in the ${store.name} secrets adapter.`,
38747
+ "Play rejects a new key if the app has one: import it with google-play login.",
38748
+ withLead("Otherwise:", credentialStoreCommand("android-keystore", name))
38749
+ ].join("\n");
38757
38750
  };
38758
38751
  /**
38759
- * The command that signs in once, at a terminal, and stores the key Hot
38760
- * Updater issues as `name`: an App Store Connect API key after an Apple
38761
- * Account sign-in, a Play service account after a Google one.
38752
+ * The command that stores the key Hot Updater issues as `name`: an App
38753
+ * Store Connect API key after an Apple Account sign-in, a Play service
38754
+ * account after a Google one, and an upload keystore it creates.
38762
38755
  */
38763
38756
  const credentialIssueCommand = (kind, name = kind) => `npx hot-updater secrets issue ${kind}${name === kind ? "" : ` ${name}`}`;
38764
38757
  /** The command group of each issued kind's shortcut, `<group> login`. */
@@ -38775,9 +38768,10 @@ const CREDENTIAL_LOGIN_GROUPS = {
38775
38768
  const credentialLoginCommand = (kind, name = kind) => name === kind ? `npx hot-updater ${CREDENTIAL_LOGIN_GROUPS[kind]} login` : credentialIssueCommand(kind, name);
38776
38769
  /**
38777
38770
  * The command that stores a missing credential of `kind` as `name`: the
38778
- * keystore you have, or the key Hot Updater issues after a sign-in.
38771
+ * upload keystore Hot Updater creates, or the key it issues after a
38772
+ * sign-in.
38779
38773
  */
38780
- const credentialStoreCommand = (kind, name = kind) => kind === "android-keystore" ? credentialPutCommand(kind, name) : credentialLoginCommand(kind, name);
38774
+ const credentialStoreCommand = (kind, name = kind) => kind === "android-keystore" ? credentialIssueCommand(kind, name) : credentialLoginCommand(kind, name);
38781
38775
  var CredentialUnavailableError = class extends Error {
38782
38776
  kind;
38783
38777
  /** The environment variables that can provide this credential. */
@@ -39026,7 +39020,7 @@ const createCredentialResolver = ({ env = {}, secrets, cwd } = {}) => {
39026
39020
  const reason = error instanceof Error ? error.message : String(error);
39027
39021
  throw new CredentialUnavailableError(kind, `Couldn't read ${item} from the ${store.name} secrets adapter.\n${reason}`, { cause: error });
39028
39022
  }
39029
- if (value === void 0) throw new CredentialUnavailableError(kind, `No ${item} in the ${store.name} secrets adapter.\nStore it once with:\n${formatCommand(credentialStoreCommand(kind, name))}`);
39023
+ if (value === void 0) throw new CredentialUnavailableError(kind, kind === "android-keystore" ? describeMissingKeystore(store, item, name) : `No ${item} in the ${store.name} secrets adapter.\nStore it once with:\n${formatCommand(credentialStoreCommand(kind, name))}`);
39030
39024
  registerSecretValue(value);
39031
39025
  return fromSecretValue[kind](value, {
39032
39026
  files,
@@ -45532,8 +45526,24 @@ const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
45532
45526
  const MANAGED_HELPER_NAMES = /* @__PURE__ */ new Set([
45533
45527
  "awsOptions",
45534
45528
  "commonOptions",
45535
- "credential"
45529
+ "credential",
45530
+ "databaseSecrets",
45531
+ "deploySecrets",
45532
+ "storageSecrets"
45536
45533
  ]);
45534
+ /**
45535
+ * `const <name> = secretEnv([...names])`, a helper a managed config reads
45536
+ * credentials with: from the environment and `.env.hotupdater` first, then
45537
+ * from the config's `secrets` adapter. `storageSecrets` and
45538
+ * `databaseSecrets` read each adapter's own, so a command that reaches only
45539
+ * one needs only its credentials; `deploySecrets` reads what both share.
45540
+ * Its builder imports `secretEnv` from `hot-updater`.
45541
+ */
45542
+ const secretEnvHelper = (name, names) => ({
45543
+ name,
45544
+ strategy: "replace",
45545
+ code: `const ${name} = secretEnv([\n${names.map((variable) => ` ${JSON.stringify(variable)},`).join("\n")}\n]);`
45546
+ });
45537
45547
  const KNOWN_BUILD_CALLEES = /* @__PURE__ */ new Set([
45538
45548
  "bare",
45539
45549
  "expo",
@@ -45605,6 +45615,18 @@ const getObjectPropertyName = (property) => {
45605
45615
  return null;
45606
45616
  };
45607
45617
  const isDataProperty = (property) => property.type === "Property" && property.kind === "init" && !property.method && !property.shorthand;
45618
+ /** `process.env.HOT_UPDATER_*`, with or without `!`. */
45619
+ const isHotUpdaterEnvRead = (expression) => {
45620
+ const read = expression.type === "TSNonNullExpression" ? expression.expression : expression;
45621
+ return read.type === "MemberExpression" && !read.computed && read.property.type === "Identifier" && read.property.name.startsWith("HOT_UPDATER_") && read.object.type === "MemberExpression" && !read.object.computed && read.object.object.type === "Identifier" && read.object.object.name === "process" && read.object.property.type === "Identifier" && read.object.property.name === "env";
45622
+ };
45623
+ /**
45624
+ * A value init writes into a managed adapter's options: a string, a
45625
+ * `process.env.HOT_UPDATER_*` read, as configs of 1.1 read every value, or
45626
+ * an object of only those, such as `credentials`. Init replaces it with
45627
+ * what its run resolves; any other value is the project's own and kept.
45628
+ */
45629
+ const isInitWrittenValue = (expression) => expression.type === "Literal" && typeof expression.value === "string" || isHotUpdaterEnvRead(expression) || expression.type === "ObjectExpression" && expression.properties.length > 0 && expression.properties.every((property) => isDataProperty(property) && isInitWrittenValue(property.value));
45608
45630
  const dedentBlock = (text) => {
45609
45631
  const lines = text.replace(/\s+$/, "").split("\n");
45610
45632
  const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0);
@@ -45677,9 +45699,17 @@ const mergeObjectLiteralText = (existingObject, newObject, replaceIncompatiblePr
45677
45699
  existingPropertyNames.add(propertyName);
45678
45700
  const nextProperty = newObject.objectExpression.properties.find((candidate) => getObjectPropertyName(candidate) === propertyName);
45679
45701
  if (!nextProperty || !isDataProperty(property) || !isDataProperty(nextProperty)) continue;
45702
+ if (isInitWrittenValue(property.value)) {
45703
+ edits.push({
45704
+ start: property.value.start - existingObject.objectExpression.start,
45705
+ end: property.value.end - existingObject.objectExpression.start,
45706
+ text: getNodeText(newObject.source, nextProperty.value)
45707
+ });
45708
+ continue;
45709
+ }
45680
45710
  const existingCallee = getCallCallee(property.value);
45681
45711
  const nextCallee = getCallCallee(nextProperty.value);
45682
- const hasIncompatibleValue = existingCallee !== null && nextCallee !== null && existingCallee !== nextCallee || property.value.type === "ObjectExpression" !== (nextProperty.value.type === "ObjectExpression");
45712
+ const hasIncompatibleValue = existingCallee !== null && nextCallee !== null && existingCallee !== nextCallee || property.value.type !== nextProperty.value.type;
45683
45713
  if (replaceIncompatibleProperties.includes(propertyName) && hasIncompatibleValue) {
45684
45714
  edits.push({
45685
45715
  start: property.value.start - existingObject.objectExpression.start,
@@ -47084,15 +47114,26 @@ const loadPlatformConfigs = async (platforms, { channel }) => {
47084
47114
  */
47085
47115
  const resolveConfig = (config, scopes) => {
47086
47116
  const mergedConfig = mergeConfigSources(config, getDefaultConfig());
47087
- const secrets = withLocalKeyStore(mergedConfig.secrets);
47117
+ const secrets = resolveSecretsAdapter(mergedConfig.secrets);
47088
47118
  const signing = normalizeSigningConfig(mergedConfig.signing, { secrets });
47089
47119
  for (const scope of scopes) scope.secrets ??= secrets;
47090
47120
  setConfiguredSecrets(secrets);
47091
- return {
47121
+ const resolved = {
47092
47122
  ...mergedConfig,
47093
47123
  signing,
47094
47124
  secrets
47095
47125
  };
47126
+ Object.defineProperty(resolved, SECRET_ENV_NAMES, { value: [...new Set(scopes.flatMap((scope) => [...scope.names ?? []]))] });
47127
+ return resolved;
47128
+ };
47129
+ const SECRET_ENV_NAMES = Symbol.for("@hot-updater/cli-tools/secret-env-names");
47130
+ /**
47131
+ * The names every `secretEnv` the config created while it loaded reads,
47132
+ * such as `HOT_UPDATER_CLOUDFLARE_API_TOKEN`, in the order it created them.
47133
+ */
47134
+ const getSecretEnvNames = (config) => {
47135
+ const names = Reflect.get(config, SECRET_ENV_NAMES);
47136
+ return Array.isArray(names) ? names : [];
47096
47137
  };
47097
47138
  //#endregion
47098
47139
  //#region src/serverDefinition.ts
@@ -47384,6 +47425,74 @@ const resolveHotUpdaterServerVersion = (currentPackageName, options) => {
47384
47425
  return process.env["HOT_UPDATER_SERVER_PACKAGE_VERSION"]?.trim() || resolvePackageVersion(currentPackageName, options);
47385
47426
  };
47386
47427
  //#endregion
47428
+ //#region src/teamSecrets.ts
47429
+ /**
47430
+ * The client API key the managed servers' `apiKeys()` gives an app, which
47431
+ * init saves (`API_KEY_ENV` in `@hot-updater/plugin-api-keys`). The config
47432
+ * never reads it; a team store shares it with the deploy credentials, so a
47433
+ * teammate's init registers the same key again rather than a new one.
47434
+ */
47435
+ const MANAGED_CLIENT_CREDENTIAL_ENV = "HOT_UPDATER_API_KEY";
47436
+ /**
47437
+ * The config's own `secrets` adapter, the team store: undefined when the
47438
+ * config sets none, which is `localSecrets()`. Loading the config runs its
47439
+ * `process.loadEnvFile(".env.hotupdater")`, which init must not see, so
47440
+ * `process.env` is restored after it.
47441
+ */
47442
+ const loadTeamSecrets = async () => {
47443
+ const before = { ...process.env };
47444
+ try {
47445
+ return getConfiguredAdapter((await loadConfig(null)).secrets);
47446
+ } finally {
47447
+ for (const key of Object.keys(process.env)) if (!Object.hasOwn(before, key)) delete process.env[key];
47448
+ Object.assign(process.env, before);
47449
+ }
47450
+ };
47451
+ /**
47452
+ * `names` neither the environment nor `env`, init's env files, sets, read
47453
+ * from `secrets` in one call: the values a teammate's init, or a run after
47454
+ * the file's lines were deleted, replays with.
47455
+ */
47456
+ const readMissingFromTeamSecrets = async (secrets, names, { env, cwd }) => {
47457
+ const missing = names.filter((name) => getHotUpdaterEnvValue(env, name) === void 0);
47458
+ if (missing.length === 0) return {};
47459
+ const held = await secrets.get(missing, { cwd });
47460
+ const values = Object.create(null);
47461
+ for (const name of missing) {
47462
+ const value = Object.hasOwn(held, name) ? held[name] : void 0;
47463
+ if (typeof value === "string" && value !== "") {
47464
+ registerSecretValue(value);
47465
+ values[name] = value;
47466
+ }
47467
+ }
47468
+ return values;
47469
+ };
47470
+ /**
47471
+ * Stores each of `names` that `.env.hotupdater` holds through `secrets`,
47472
+ * then reads them back in one call. The file is left as it is. Returns the
47473
+ * names stored, in the order of `names`; throws when the adapter reads
47474
+ * back another value.
47475
+ */
47476
+ const storeEnvFileSecrets = async (secrets, names, cwd) => {
47477
+ const file = await readLoadedHotUpdaterEnv(cwd);
47478
+ const values = /* @__PURE__ */ new Map();
47479
+ for (const name of names) {
47480
+ const value = Object.hasOwn(file, name) ? file[name]?.trim() : void 0;
47481
+ if (value) values.set(name, value);
47482
+ }
47483
+ if (values.size === 0) return [];
47484
+ const stored = [...values.keys()];
47485
+ for (const [name, value] of values) {
47486
+ registerSecretValue(value);
47487
+ await secrets.set(name, value, { cwd });
47488
+ }
47489
+ const read = await secrets.get(stored, { cwd });
47490
+ const unread = stored.filter((name) => (Object.hasOwn(read, name) ? read[name] : void 0) !== values.get(name));
47491
+ if (unread.length > 0) throw new Error(`The ${secrets.name} secrets adapter did not read back ${list(unread)} (${unread.map((name) => describeSecret(secrets, name)).join("; ")}).`);
47492
+ return stored;
47493
+ };
47494
+ const list = (names) => names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
47495
+ //#endregion
47387
47496
  //#region src/storageFiles.ts
47388
47497
  const getStorageFileByteSize = async (filePath) => {
47389
47498
  const { size } = await fs$2.stat(filePath, { bigint: true });
@@ -47434,4 +47543,4 @@ function transformTemplate(templateString, values) {
47434
47543
  return result;
47435
47544
  }
47436
47545
  //#endregion
47437
- export { BuildFailedError, BuildLogger, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError, DEFAULT_SIGNING_KEY_SECRET, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, InitEnvFileError, InitError, LOCAL_SECRETS_NAME, LegacyInfrastructureError, MissingInitInputsError, ServerDefinitionError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialPutCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeSecret, describeStoredSecret, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readHotUpdaterInitEnv, readLoadedHotUpdaterEnv, readLocalKey, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, secretEnv, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, stripAnsi, transformEnv, transformTemplate, withCredentialResolver, withLocalKeyStore, withSecretsScope, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
47546
+ export { BuildFailedError, BuildLogger, CREDENTIAL_ENV_NAMES, CREDENTIAL_LOGIN_GROUPS, ConfigBuilder, CredentialUnavailableError, DEFAULT_SIGNING_KEY_SECRET, HOT_UPDATER_HOME_ENV, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, HotUpdateDirUtil, INTERFACE_FINGERPRINT_ENGINE, InitEnvFileError, InitError, LOCAL_SECRETS_NAME, LegacyInfrastructureError, MANAGED_CLIENT_CREDENTIAL_ENV, MissingInitInputsError, ServerDefinitionError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitInputs, assertInitProviderInputs, assertUnreservedEnv, autolinkingConfigCommand, banner, clientAuthOf, typedColors as colors, confirmInitInputPersistence, copyDirToTmp, createCredentialResolver, createHotUpdaterConfigScaffold, createHotUpdaterConfigScaffoldFromBuilder, createInterfaceFingerprint, createLocalSecretsStore, createLogWriter, createMigrator, createMinBundleId, createTarBrTargetFiles, createZip, credentialIssueCommand, credentialLoginCommand, credentialStoreCommand, decryptJson, defineInitProvider, describeSecret, describeStoredSecret, encryptJson, ensureInstallPackages, generateClientCredential, generateSchema, generatesSchema, getAndroidSdkPath, getBundleSigningPublicKey, getConfiguredAdapter, getConfiguredSecrets, getCwd, getHotUpdaterEnvValue, getHotUpdaterHome, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, getLocalKeyFile, getLocalKeyStoreDir, getLocalSecretsStore, getMissingInitInputs, getMissingInitProviderInputs, getPackageManager, getProjectKey, getReactNativeMetadatas, getSecretEnvNames, getStorageFileByteSize, isCI, isCodegenSpecFile, isReservedVariableName, isSecretName, isServerDefinition, keepsKeysOnThisMachine, link, loadConfig, loadPlatformConfigs, loadTeamSecrets, localSecrets, log, makeEnv, maskSecretValues, moduleSpecifiersOf, nonInteractiveReason, p, parseLoadedEnv, parseSecretReference, prepareBundleSigning, printAppSetup, printBanner, provisionClientCredential, putStorageFile, reactNativeCliFingerprint, reactNativeConfigCommand, readBundleSigningPublicKeyFile, readHotUpdaterInitEnv, readLoadedHotUpdaterEnv, readLocalKey, readMissingFromTeamSecrets, readPackageUp, readReactNativeInterface, readSecretVariables, readSecretWithSource, registerCredentialValue, registerSecretValue, renderAppImports, renderAppSetup, renderImportStatements, resolveExpoAutolinking, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolveInitProviderInputs, resolveLocalSecretsPath, resolvePackageVersion, resolveSecretsAdapter, secretEnv, secretEnvHelper, secretVariableName, serverDefinitionOf, setConfiguredSecrets, shouldAutoSelectOnlyInitResource, storeEnvFileSecrets, stripAnsi, transformEnv, transformTemplate, withCredentialResolver, withSecretsScope, writeHotUpdaterConfig, writeHotUpdaterFiles, writeLocalKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "1.2.0-rc.4",
3
+ "version": "1.2.0-rc.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
@@ -49,9 +49,9 @@
49
49
  "oxc-parser": "0.141.0",
50
50
  "oxc-transform": "0.141.0",
51
51
  "unconfig": "7.5.0",
52
- "@hot-updater/server": "1.2.0-rc.4",
53
- "@hot-updater/protocol": "1.2.0-rc.4",
54
- "@hot-updater/plugin-core": "1.2.0-rc.4"
52
+ "@hot-updater/plugin-core": "1.2.0-rc.5",
53
+ "@hot-updater/protocol": "1.2.0-rc.5",
54
+ "@hot-updater/server": "1.2.0-rc.5"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@clack/prompts": "1.7.0",
@@ -65,7 +65,7 @@
65
65
  "tar": "^7.5.16",
66
66
  "verkit": "0.4.0",
67
67
  "workspace-tools": "^0.41.7",
68
- "@hot-updater/test-utils": "1.2.0-rc.4"
68
+ "@hot-updater/test-utils": "1.2.0-rc.5"
69
69
  },
70
70
  "inlinedDependencies": {
71
71
  "@babel/code-frame": "7.29.0",