@better-update/cli 0.29.0 → 0.29.1
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.mjs +57 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region package.json
|
|
37
|
-
var version = "0.29.
|
|
37
|
+
var version = "0.29.1";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -18926,15 +18926,6 @@ const grantRecipient = (args) => Effect.gen(function* () {
|
|
|
18926
18926
|
} });
|
|
18927
18927
|
});
|
|
18928
18928
|
/**
|
|
18929
|
-
* Resolve the passphrase needed to unlock the active identity before a crypto
|
|
18930
|
-
* operation: prompt for it when the identity is the on-disk file, or return
|
|
18931
|
-
* `undefined` when the raw `BETTER_UPDATE_IDENTITY` env key is in use (CI). The
|
|
18932
|
-
* resolved value is threaded into {@link unlockVaultKey} by the cipher helpers.
|
|
18933
|
-
*/
|
|
18934
|
-
const resolveVaultPassphrase = Effect.gen(function* () {
|
|
18935
|
-
return (yield* activeRecipient).source === "file" ? yield* promptPassword("Passphrase to unlock this device's identity:") : void 0;
|
|
18936
|
-
});
|
|
18937
|
-
/**
|
|
18938
18929
|
* Unlock the org vault key for an interactive command, reusing a cached vault key
|
|
18939
18930
|
* from the OS keychain when one is present and unexpired — so the device
|
|
18940
18931
|
* passphrase is prompted at most once per cache TTL rather than on every command
|
|
@@ -18942,6 +18933,12 @@ const resolveVaultPassphrase = Effect.gen(function* () {
|
|
|
18942
18933
|
* The CI `BETTER_UPDATE_IDENTITY` key carries no passphrase and is never cached:
|
|
18943
18934
|
* it skips straight to the raw unwrap. On a cache miss the full unlock runs —
|
|
18944
18935
|
* prompt, Argon2id, fetch + unwrap — and the result is cached for next time.
|
|
18936
|
+
*
|
|
18937
|
+
* The cached key is the unwrapped vault key, which both unwraps (decrypt/read)
|
|
18938
|
+
* and wraps (encrypt/write) DEKs — so this single entry point backs every vault
|
|
18939
|
+
* operation: download/build-resolve reads, seal-for-upload + generate writes, and
|
|
18940
|
+
* rotation. There is no read-only cache: an unlock makes the next write seamless
|
|
18941
|
+
* too.
|
|
18945
18942
|
*/
|
|
18946
18943
|
const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
|
|
18947
18944
|
const recipient = yield* activeRecipient;
|
|
@@ -18953,6 +18950,17 @@ const unlockVaultKeyInteractive = (api) => Effect.gen(function* () {
|
|
|
18953
18950
|
yield* cache.set(recipient.publicKey, vault);
|
|
18954
18951
|
return vault;
|
|
18955
18952
|
}).pipe(Effect.provide(VaultCacheLive));
|
|
18953
|
+
/**
|
|
18954
|
+
* Forget the active recipient's cached vault key. Called after a rotation re-keys
|
|
18955
|
+
* the vault: the cached key + version are now stale, so leaving them would make
|
|
18956
|
+
* the next seal upload a key/version the server CAS-rejects (and the next decrypt
|
|
18957
|
+
* fail integrity). Clearing forces a fresh unlock at the new version next time —
|
|
18958
|
+
* which also correctly locks out a device that just revoked its own access.
|
|
18959
|
+
*/
|
|
18960
|
+
const forgetCachedVaultKey = Effect.gen(function* () {
|
|
18961
|
+
const recipient = yield* activeRecipient;
|
|
18962
|
+
yield* (yield* VaultCache).clear(recipient.publicKey);
|
|
18963
|
+
}).pipe(Effect.provide(VaultCacheLive));
|
|
18956
18964
|
/** Look up a registered recipient by its key id or full `SHA256:` fingerprint. */
|
|
18957
18965
|
const findRecipient = (api, selector) => Effect.gen(function* () {
|
|
18958
18966
|
const { items } = yield* api.userEncryptionKeys.list();
|
|
@@ -18992,13 +19000,6 @@ const getActiveOrgId = (api) => Effect.gen(function* () {
|
|
|
18992
19000
|
if (me.activeOrganization === null) return yield* new IdentityError({ message: "No active organization for this token." });
|
|
18993
19001
|
return me.activeOrganization.id;
|
|
18994
19002
|
});
|
|
18995
|
-
/** Resolve the active org id and unlock this device's vault key — the once-per-command I/O. */
|
|
18996
|
-
const openVaultSession = (api, passphrase) => Effect.gen(function* () {
|
|
18997
|
-
return {
|
|
18998
|
-
orgId: yield* getActiveOrgId(api),
|
|
18999
|
-
vault: yield* unlockVaultKey(api, passphrase)
|
|
19000
|
-
};
|
|
19001
|
-
});
|
|
19002
19003
|
/**
|
|
19003
19004
|
* {@link openVaultSession} that unlocks the vault key interactively — reusing the
|
|
19004
19005
|
* OS-keychain-cached key when one is live (no prompt), prompting for the device
|
|
@@ -19610,7 +19611,7 @@ const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(funct
|
|
|
19610
19611
|
...compact({ validityDays: input.validityDays })
|
|
19611
19612
|
});
|
|
19612
19613
|
const bytes = yield* fs.readFile(keystorePath);
|
|
19613
|
-
const session = yield*
|
|
19614
|
+
const session = yield* openVaultSessionInteractive(api);
|
|
19614
19615
|
const metadata = { keyAlias: input.keyAlias };
|
|
19615
19616
|
const envelope = yield* sealForUpload({
|
|
19616
19617
|
session,
|
|
@@ -19661,7 +19662,7 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
|
|
|
19661
19662
|
step: "p12-build",
|
|
19662
19663
|
message: cause.message
|
|
19663
19664
|
})));
|
|
19664
|
-
const session = yield*
|
|
19665
|
+
const session = yield* openVaultSessionInteractive(api);
|
|
19665
19666
|
const metadata = {
|
|
19666
19667
|
serialNumber: bundle.metadata.serialNumber,
|
|
19667
19668
|
appleTeamIdentifier: bundle.metadata.appleTeamId,
|
|
@@ -20667,7 +20668,7 @@ const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effec
|
|
|
20667
20668
|
step: "parse-p12",
|
|
20668
20669
|
message: cause.message
|
|
20669
20670
|
})));
|
|
20670
|
-
const session = yield*
|
|
20671
|
+
const session = yield* openVaultSessionInteractive(api);
|
|
20671
20672
|
const envelopeMetadata = {
|
|
20672
20673
|
serialNumber: metadata.serialNumber,
|
|
20673
20674
|
appleTeamIdentifier: metadata.appleTeamId,
|
|
@@ -21065,14 +21066,12 @@ const isMissingResolveError = (cause) => hasTag(cause) && (cause._tag === "NotFo
|
|
|
21065
21066
|
const randomKeystoreSecret = () => randomBytes(24).toString("base64url");
|
|
21066
21067
|
const generateKeystoreAuto = (api, applicationIdentifier) => Effect.gen(function* () {
|
|
21067
21068
|
yield* Console.log("Generating a new Android Keystore...");
|
|
21068
|
-
const passphrase = yield* resolveVaultPassphrase;
|
|
21069
21069
|
return (yield* generateAndUploadKeystore(api, {
|
|
21070
21070
|
keyAlias: "upload",
|
|
21071
21071
|
storePassword: randomKeystoreSecret(),
|
|
21072
21072
|
keyPassword: randomKeystoreSecret(),
|
|
21073
21073
|
commonName: applicationIdentifier,
|
|
21074
|
-
organization: "better-update"
|
|
21075
|
-
...compact({ passphrase })
|
|
21074
|
+
organization: "better-update"
|
|
21076
21075
|
})).id;
|
|
21077
21076
|
});
|
|
21078
21077
|
const generateKeystoreInteractive = (api) => Effect.gen(function* () {
|
|
@@ -21081,15 +21080,13 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
|
|
|
21081
21080
|
const keyPassword = yield* promptPassword("Key password");
|
|
21082
21081
|
const commonName = yield* promptText("Common name (CN)", { placeholder: "Your App" });
|
|
21083
21082
|
const organization = yield* promptText("Organization (O)", { placeholder: "Your Company" });
|
|
21084
|
-
const passphrase = yield* resolveVaultPassphrase;
|
|
21085
21083
|
yield* Console.log("Generating keystore with keytool...");
|
|
21086
21084
|
return (yield* generateAndUploadKeystore(api, {
|
|
21087
21085
|
keyAlias: alias,
|
|
21088
21086
|
storePassword,
|
|
21089
21087
|
keyPassword,
|
|
21090
21088
|
commonName,
|
|
21091
|
-
organization
|
|
21092
|
-
...compact({ passphrase })
|
|
21089
|
+
organization
|
|
21093
21090
|
})).id;
|
|
21094
21091
|
});
|
|
21095
21092
|
const pickExistingKeystore = (api) => Effect.gen(function* () {
|
|
@@ -21112,6 +21109,25 @@ const resolveAndroidAppId = (api, input) => Effect.gen(function* () {
|
|
|
21112
21109
|
})).id;
|
|
21113
21110
|
});
|
|
21114
21111
|
const resolveAndroidKeystoreId = (api, choice) => choice === "generate" ? generateKeystoreInteractive(api) : pickExistingKeystore(api);
|
|
21112
|
+
const bindAndroidKeystore = (api, appId, keystoreId) => Effect.gen(function* () {
|
|
21113
|
+
const existing = yield* api.androidBuildCredentials.list({ path: { applicationIdentifierId: appId } });
|
|
21114
|
+
const target = existing.items.find((group) => group.isDefault) ?? existing.items.at(0);
|
|
21115
|
+
if (target === void 0) {
|
|
21116
|
+
yield* api.androidBuildCredentials.create({
|
|
21117
|
+
path: { applicationIdentifierId: appId },
|
|
21118
|
+
payload: {
|
|
21119
|
+
name: "Default",
|
|
21120
|
+
isDefault: true,
|
|
21121
|
+
androidUploadKeystoreId: keystoreId
|
|
21122
|
+
}
|
|
21123
|
+
});
|
|
21124
|
+
return;
|
|
21125
|
+
}
|
|
21126
|
+
yield* api.androidBuildCredentials.update({
|
|
21127
|
+
path: { id: target.id },
|
|
21128
|
+
payload: { androidUploadKeystoreId: keystoreId }
|
|
21129
|
+
});
|
|
21130
|
+
});
|
|
21115
21131
|
const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
|
|
21116
21132
|
yield* Console.log("");
|
|
21117
21133
|
yield* Console.log(`No Android build credentials configured for ${input.applicationIdentifier}.`);
|
|
@@ -21134,15 +21150,7 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
|
|
|
21134
21150
|
message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
|
|
21135
21151
|
hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
|
|
21136
21152
|
});
|
|
21137
|
-
|
|
21138
|
-
yield* api.androidBuildCredentials.create({
|
|
21139
|
-
path: { applicationIdentifierId: appId },
|
|
21140
|
-
payload: {
|
|
21141
|
-
name: "Default",
|
|
21142
|
-
isDefault: true,
|
|
21143
|
-
androidUploadKeystoreId: keystoreId
|
|
21144
|
-
}
|
|
21145
|
-
});
|
|
21153
|
+
yield* bindAndroidKeystore(api, appId, yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api));
|
|
21146
21154
|
yield* Console.log("Android build credentials configured.");
|
|
21147
21155
|
});
|
|
21148
21156
|
const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.resolve({
|
|
@@ -26049,7 +26057,7 @@ const uploadIosDistributionCertificate = (api, input, bytes) => Effect.gen(funct
|
|
|
26049
26057
|
validUntil: info.expiresAt.toISOString()
|
|
26050
26058
|
};
|
|
26051
26059
|
const envelope = yield* sealForUpload({
|
|
26052
|
-
session: yield*
|
|
26060
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26053
26061
|
credentialType: "distribution-certificate",
|
|
26054
26062
|
metadata,
|
|
26055
26063
|
secret: {
|
|
@@ -26075,7 +26083,7 @@ const uploadIosPushKey = (api, input, bytes) => Effect.gen(function* () {
|
|
|
26075
26083
|
appleTeamIdentifier: input.appleTeamIdentifier
|
|
26076
26084
|
};
|
|
26077
26085
|
const envelope = yield* sealForUpload({
|
|
26078
|
-
session: yield*
|
|
26086
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26079
26087
|
credentialType: "push-key",
|
|
26080
26088
|
metadata,
|
|
26081
26089
|
secret: { p8Pem: toUtf8(bytes) }
|
|
@@ -26100,7 +26108,7 @@ const uploadIosAscApiKey = (api, input, bytes) => Effect.gen(function* () {
|
|
|
26100
26108
|
appleTeamIdentifier: input.appleTeamIdentifier
|
|
26101
26109
|
});
|
|
26102
26110
|
const envelope = yield* sealForUpload({
|
|
26103
|
-
session: yield*
|
|
26111
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26104
26112
|
credentialType: "asc-api-key",
|
|
26105
26113
|
metadata,
|
|
26106
26114
|
secret: { p8Pem: toUtf8(bytes) }
|
|
@@ -26134,7 +26142,7 @@ const uploadAndroidKeystore = (api, input, bytes) => Effect.gen(function* () {
|
|
|
26134
26142
|
keyPassword: input.keyPassword
|
|
26135
26143
|
})).keyAlias };
|
|
26136
26144
|
const envelope = yield* sealForUpload({
|
|
26137
|
-
session: yield*
|
|
26145
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26138
26146
|
credentialType: "keystore",
|
|
26139
26147
|
metadata,
|
|
26140
26148
|
secret: {
|
|
@@ -26162,7 +26170,7 @@ const uploadAndroidGoogleServiceAccountKey = (api, input, bytes) => Effect.gen(f
|
|
|
26162
26170
|
googleProjectId: parsed.googleProjectId
|
|
26163
26171
|
};
|
|
26164
26172
|
const envelope = yield* sealForUpload({
|
|
26165
|
-
session: yield*
|
|
26173
|
+
session: yield* openVaultSessionInteractive(api),
|
|
26166
26174
|
credentialType: "google-service-account-key",
|
|
26167
26175
|
metadata,
|
|
26168
26176
|
secret: { json }
|
|
@@ -26191,10 +26199,7 @@ const uploadCredential = (api, input) => Effect.gen(function* () {
|
|
|
26191
26199
|
const hasKey = (candidate) => Object.hasOwn(uploadHandlers, candidate);
|
|
26192
26200
|
const handler = hasKey(key) ? uploadHandlers[key] : void 0;
|
|
26193
26201
|
if (!handler) return yield* new CredentialValidationError({ message: `Unsupported credential combination: platform=${input.platform} type=${input.type}` });
|
|
26194
|
-
return yield* handler(api, input
|
|
26195
|
-
...input,
|
|
26196
|
-
...compact({ passphrase: yield* resolveVaultPassphrase })
|
|
26197
|
-
}, bytes);
|
|
26202
|
+
return yield* handler(api, input, bytes);
|
|
26198
26203
|
});
|
|
26199
26204
|
const deleteCredential = (api, input) => {
|
|
26200
26205
|
const path = { id: input.id };
|
|
@@ -27185,10 +27190,14 @@ const runCredentialsManager = Effect.gen(function* () {
|
|
|
27185
27190
|
* one, re-wrap the new vault key to each recipient, then submit the rotation
|
|
27186
27191
|
* atomically (the server CAS-guards on the current version and requires a
|
|
27187
27192
|
* recovery recipient in the set). Drops every recipient not in `recipients`.
|
|
27193
|
+
*
|
|
27194
|
+
* Unlocks via the cache-aware path (reusing a live `credentials unlock` session),
|
|
27195
|
+
* then drops that cached key once the re-key lands — it is now stale, so the next
|
|
27196
|
+
* operation must re-unlock at the new version.
|
|
27188
27197
|
*/
|
|
27189
27198
|
const rotateVaultTo = (args) => Effect.gen(function* () {
|
|
27190
27199
|
const orgId = yield* getActiveOrgId(args.api);
|
|
27191
|
-
const current = yield*
|
|
27200
|
+
const current = yield* unlockVaultKeyInteractive(args.api);
|
|
27192
27201
|
const newVaultKey = generateVaultKey();
|
|
27193
27202
|
const newVersion = current.vaultVersion + 1;
|
|
27194
27203
|
const { deks } = yield* args.api.orgVault.listCredentialDeks();
|
|
@@ -27226,11 +27235,13 @@ const rotateVaultTo = (args) => Effect.gen(function* () {
|
|
|
27226
27235
|
recipient: recipient.publicKey
|
|
27227
27236
|
}))
|
|
27228
27237
|
})), { concurrency: "unbounded" });
|
|
27229
|
-
|
|
27238
|
+
const rotated = yield* args.api.orgVault.rotate({ payload: {
|
|
27230
27239
|
fromVersion: current.vaultVersion,
|
|
27231
27240
|
recipientWraps,
|
|
27232
27241
|
credentialDeks
|
|
27233
27242
|
} });
|
|
27243
|
+
yield* forgetCachedVaultKey;
|
|
27244
|
+
return rotated;
|
|
27234
27245
|
});
|
|
27235
27246
|
/** The encryption keys currently holding the vault key, joined with their public keys. */
|
|
27236
27247
|
const currentRecipients = (api) => Effect.gen(function* () {
|
|
@@ -27365,7 +27376,6 @@ const rotateCommand = defineCommand({
|
|
|
27365
27376
|
yield* confirmRecipients(recipients, args.yes === true);
|
|
27366
27377
|
const rotated = yield* rotateVaultTo({
|
|
27367
27378
|
api,
|
|
27368
|
-
passphrase: yield* resolveVaultPassphrase,
|
|
27369
27379
|
recipients: recipients.map(toRotationRecipient)
|
|
27370
27380
|
});
|
|
27371
27381
|
yield* printHuman(`Rotated the vault to version ${String(rotated.vaultVersion)} (${String(recipients.length)} recipients).`);
|
|
@@ -27401,7 +27411,6 @@ const revokeCommand$1 = defineCommand({
|
|
|
27401
27411
|
yield* confirmRecipients(surviving, args.yes === true);
|
|
27402
27412
|
const rotated = yield* rotateVaultTo({
|
|
27403
27413
|
api,
|
|
27404
|
-
passphrase: yield* resolveVaultPassphrase,
|
|
27405
27414
|
recipients: surviving.map(toRotationRecipient)
|
|
27406
27415
|
});
|
|
27407
27416
|
yield* printHuman(`Revoked ${target.label} and rotated the vault to version ${String(rotated.vaultVersion)}.`);
|
|
@@ -27487,7 +27496,6 @@ const recoveryCommand = defineCommand({
|
|
|
27487
27496
|
yield* confirmRecipients(surviving, args.yes === true);
|
|
27488
27497
|
const rotated = yield* rotateVaultTo({
|
|
27489
27498
|
api,
|
|
27490
|
-
passphrase: yield* resolveVaultPassphrase,
|
|
27491
27499
|
recipients: [...surviving.map(toRotationRecipient), {
|
|
27492
27500
|
userEncryptionKeyId: registered.id,
|
|
27493
27501
|
publicKey: newRecovery.publicKey
|