Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions apps/desktop/scripts/config-bundle.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { appendArrayInPlace } from 'foxts/append-array-in-place';
import { isObjectEmpty } from 'foxts/is-object-empty';
// Relative on purpose: this module is inlined into the bundled Vite config, which runs under
// plain Node — Node cannot resolve the package's extensionless TS source exports.
import { parseBrandIdentityArtifact } from '../../../packages/foundation/common/src/config/brand-identity';
Expand Down Expand Up @@ -116,14 +115,6 @@ export function loadGeneratedConfigBundle(
'generated config bundle emergency keyring contains the conformance fixture key',
);
}
if (
env.LINKCODE_REQUIRE_CONFIG_BUNDLE === '1' &&
(bundle.endpoints.emergency === null || isObjectEmpty(bundle.keyrings.emergency))
) {
throw new Error(
'LINKCODE_REQUIRE_CONFIG_BUNDLE=1 requires an emergency endpoint and emergency public key',
);
}
const branded = bundle.brandId !== DEFAULT_BRAND_ID;
assertExactFiles(
generatedDir,
Expand Down
5 changes: 2 additions & 3 deletions apps/desktop/src/__tests__/config-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const RE_IMMUTABLE = /immutable/;
const RE_REBUILD = /rebuild before packaging/;
const RE_WRONG_PLATFORM = /targets ios, expected desktop/;
const RE_FIXTURE_KEY = /conformance fixture key/;
const RE_EMERGENCY_BOOTSTRAP = /requires an emergency endpoint and emergency public key/;
const RE_ENABLED_TOGETHER = /enabled together/;
const SAFE_EMERGENCY_PUBLIC_KEY = 'I-ZZtxm_RMtR2fMqJtiENzX13BIMmqE8X9lDWQ-bg4c';
const FIXTURE_PUBLIC_KEYS = [
Expand Down Expand Up @@ -174,7 +173,7 @@ describe('loadGeneratedConfigBundle', () => {
},
);

it('allows an absent emergency bootstrap only when the generated bundle is optional', async () => {
it('allows an absent emergency bootstrap in required release bundles', async () => {
const fixture = JSON.parse(desktopFixture) as {
endpoints: Record<string, unknown>;
keyrings: Record<string, unknown>;
Expand All @@ -187,7 +186,7 @@ describe('loadGeneratedConfigBundle', () => {
expect(() => loadGeneratedConfigBundle(optionalDir, {})).not.toThrow();
expect(() =>
loadGeneratedConfigBundle(optionalDir, { LINKCODE_REQUIRE_CONFIG_BUNDLE: '1' }),
).toThrow(RE_EMERGENCY_BOOTSTRAP);
).not.toThrow();
});

it.each([
Expand Down
19 changes: 10 additions & 9 deletions apps/mobile/scripts/verify-release-config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,18 @@ function loadGeneratedBundle(platform, configDir = CONFIG_DIR) {
if (bundle.platform !== platform) {
throw new Error(`${path} targets ${String(bundle.platform)}, expected ${platform}`);
}
const emergencyEndpoint = bundle.endpoints && bundle.endpoints.emergency;
const emergencyKeyring = bundle.keyrings && bundle.keyrings.emergency;
const emergencyKeyCount =
emergencyKeyring && typeof emergencyKeyring === 'object' && !Array.isArray(emergencyKeyring)
? Reflect.ownKeys(emergencyKeyring).length
: -1;
if (
!bundle.endpoints ||
bundle.endpoints.emergency === null ||
bundle.endpoints.emergency === undefined
(emergencyEndpoint !== null && typeof emergencyEndpoint !== 'string') ||
emergencyKeyCount < 0 ||
(emergencyEndpoint === null) !== (emergencyKeyCount === 0)
) {
throw new Error(`${path} carries no emergency endpoint`);
}
const emergencyKeyring = bundle.keyrings && bundle.keyrings.emergency;
// eslint-disable-next-line sukka/prefer-foxts-object-size -- This pre-install gate has no dependencies.
if (!emergencyKeyring || Object.keys(emergencyKeyring).length === 0) {
throw new Error(`${path} carries no emergency public keys`);
throw new Error(`${path} emergency endpoint and keyring must be enabled together`);
}
if (Object.values(emergencyKeyring).some((key) => CONFORMANCE_FIXTURE_PUBLIC_KEYS.has(key))) {
throw new Error(`${path} emergency keyring contains the conformance fixture key`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ const RE_SENTINEL = /development sentinel/;
const RE_WRONG_PLATFORM = /targets android, expected ios/;
const RE_PROVENANCE_DRIFT = /disagree on target or provenance/;
const RE_MODULE_SHAPE = /generated config module shape/;
const RE_EMERGENCY_ENDPOINT = /no emergency endpoint/;
const RE_EMERGENCY_KEYS = /no emergency public keys/;
const RE_EMERGENCY_PAIR = /emergency endpoint and keyring must be enabled together/;
const RE_FIXTURE_KEY = /conformance fixture key/;
const SAFE_EMERGENCY_PUBLIC_KEY = 'I-ZZtxm_RMtR2fMqJtiENzX13BIMmqE8X9lDWQ-bg4c';
const FIXTURE_PUBLIC_KEYS = [
Expand Down Expand Up @@ -92,19 +91,25 @@ describe('verify-release-config', () => {
},
);

it('rejects a missing emergency endpoint', () => {
it('accepts an explicitly disabled emergency channel', () => {
write(
'ios',
bundleFor('ios', {
endpoints: { emergency: null, normal: null, telemetry: null },
keyrings: { emergency: {}, normal: {} },
}),
);
expect(() => verifyReleaseConfig(['ios'], dir)).toThrow(RE_EMERGENCY_ENDPOINT);
expect(() => verifyReleaseConfig(['ios'], dir)).not.toThrow();
});

it('rejects an empty emergency keyring', () => {
write('ios', bundleFor('ios', { keyrings: { emergency: {}, normal: {} } }));
expect(() => verifyReleaseConfig(['ios'], dir)).toThrow(RE_EMERGENCY_KEYS);
it.each([
{
endpoints: { emergency: null, normal: null, telemetry: null },
},
{ keyrings: { emergency: {}, normal: {} } },
])('rejects a partially enabled emergency channel', (overrides) => {
write('ios', bundleFor('ios', overrides));
expect(() => verifyReleaseConfig(['ios'], dir)).toThrow(RE_EMERGENCY_PAIR);
});

it('fails when a generated module is missing', () => {
Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Desktop signing and R2 secrets live in the repo's GitHub **`release` Environment

## Immutable config bundle (build-time render)

Signed desktop builds and every mobile store build embed an immutable config bundle (bootstrap endpoints, public keyrings, bundled defaults) rendered at build time by the config publisher — the client never re-implements rendering. The `render-config` job in `build-desktop.yml` (signed builds only) and `build-mobile.yml` (always) calls `.github/actions/render-release-config`, which checks out publisher code from protected `CONFIG_PUBLISHER_REPO` at `publisherGitSha` and structural data from protected `CONFIG_SOURCE_REPO` at the independent `sourceGitSha`. It renders through `pnpm -F @linkcode/<app> config:render` and verifies the manifest's digest bindings (revision bytes, public keyring bytes, target identity, telemetry endpoint, expected snapshot SHA-256). Nothing falls back to a mutable ref, an unvalidated or cross-organization repository, a global install, or stale generated output.
Signed desktop builds and every mobile store build embed an immutable config bundle (bootstrap endpoints, public keyrings, bundled defaults) rendered at build time by the config publisher — the client never re-implements rendering. The emergency channel is disabled when its endpoint is `null` and its public keyring is empty; explicit emergency brands must provide both together. The `render-config` job in `build-desktop.yml` (signed builds only) and `build-mobile.yml` (always) calls `.github/actions/render-release-config`, which checks out publisher code from protected `CONFIG_PUBLISHER_REPO` at `publisherGitSha` and structural data from protected `CONFIG_SOURCE_REPO` at the independent `sourceGitSha`. It renders through `pnpm -F @linkcode/<app> config:render` and verifies the manifest's digest bindings (revision bytes, public keyring bytes, target identity, telemetry endpoint, expected snapshot SHA-256). Nothing falls back to a mutable ref, an unvalidated or cross-organization repository, a global install, or stale generated output.

Each checkout uses its own short-lived installation token minted from the organization secrets
`BOT_APP_ID` and `BOT_APP_PRIVATE_KEY`. Trusted workflow steps mint these tokens before checking out
Expand Down
Loading