Compare commits

..
3 Commits
7 changed files with 56 additions and 19 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.88
0.1.89
+7 -6
View File
@@ -57,12 +57,13 @@ module.exports = {
supportsTablet: true,
bundleIdentifier: 'org.bulwarkmail.mobile',
buildNumber: IOS_BUILD_NUMBER,
config: {
// PGP and S/MIME (#58) add end-to-end encryption beyond HTTPS/TLS,
// so the app declares non-exempt encryption; App Store Connect asks
// for the export compliance documentation on upload.
usesNonExemptEncryption: true,
},
// No usesNonExemptEncryption here. PGP and S/MIME (#58) are non-exempt
// encryption, but only standard algorithms from open-source libraries
// and the app is not on the French store, so App Store Connect keeps no
// declaration and has no ITSEncryptionExportComplianceCode to give. An
// Info.plist saying YES without that code fails upload validation
// (90592), so the key stays out and scripts/testflight-public.mjs
// answers the question for each build through the API.
},
android: {
adaptiveIcon: {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bulwark-mobile",
"version": "0.1.88",
"version": "0.1.89",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bulwark-mobile",
"version": "0.1.88",
"version": "0.1.89",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bulwark-mobile",
"version": "0.1.88",
"version": "0.1.89",
"license": "AGPL-3.0-only",
"main": "index.ts",
"scripts": {
+14 -2
View File
@@ -99,7 +99,7 @@ if (!group) {
// ── Wait for the build ─────────────────────────────────────────
const query =
`/v1/builds?filter[app]=${app.id}&filter[version]=${encodeURIComponent(buildNumber)}` +
`&filter[preReleaseVersion.version]=${encodeURIComponent(version)}&fields[builds]=version,processingState`;
`&filter[preReleaseVersion.version]=${encodeURIComponent(version)}&fields[builds]=version,processingState,usesNonExemptEncryption`;
const started = Date.now();
let build;
for (;;) {
@@ -121,6 +121,18 @@ for (;;) {
}
console.log(`Build ${version} (${buildNumber}) is processed.`);
// ── Export compliance ──────────────────────────────────────────
// The app uses non-exempt encryption (PGP and S/MIME, #58) with standard
// algorithms only and is not on the French store: App Store Connect needs no
// documentation, so Info.plist has no key and the answer is given here.
if (build.attributes.usesNonExemptEncryption == null) {
const answer = await api('PATCH', `/v1/builds/${build.id}`, {
data: { type: 'builds', id: build.id, attributes: { usesNonExemptEncryption: true } },
});
if (!answer.ok) fail('Answering export compliance', answer);
console.log('Export compliance answered: standard encryption, no documentation needed.');
}
// ── Add it to the group ────────────────────────────────────────
const add = await api('POST', `/v1/betaGroups/${group.id}/relationships/builds`, {
data: [{ type: 'builds', id: build.id }],
@@ -150,7 +162,7 @@ const detail = await api('GET', `/v1/builds/${build.id}/buildBetaDetail`);
if (!detail.ok) fail('Reading the build state', detail);
const external = detail.json.data.attributes.externalBuildState;
if (external === 'MISSING_EXPORT_COMPLIANCE') {
console.error('The build has no export compliance answer; set usesNonExemptEncryption in app.config.js.');
console.error('The build still has no export compliance answer; answer it in App Store Connect > TestFlight.');
process.exit(1);
}
if (external === 'READY_FOR_BETA_SUBMISSION') {
@@ -15,6 +15,7 @@ vi.mock('../booking-calendar-actions', async () => {
});
import type { Email } from '../../../../api/types';
import { usePolicyStore } from '../../../../stores/policy-store';
import { useSettingsStore } from '../../../../stores/settings-store';
import { StructuredMailBanner } from '../StructuredMailBanner';
import { byTestId, render, textOf } from './render-native';
@@ -37,7 +38,11 @@ function mail(ar: string | null, subject = 'Your order'): Email {
}
const initialSettings = useSettingsStore.getState();
afterEach(() => useSettingsStore.setState(initialSettings, true));
const initialPolicy = usePolicyStore.getState();
afterEach(() => {
useSettingsStore.setState(initialSettings, true);
usePolicyStore.setState(initialPolicy, true);
});
describe('StructuredMailBanner', () => {
it('shows the order card of a verified mail', () => {
@@ -67,6 +72,8 @@ describe('StructuredMailBanner', () => {
});
it('loads product images only when remote content would load', () => {
// The webmail's config is known and allows third-party traffic.
usePolicyStore.setState({ configPending: false, links: {} });
useSettingsStore.setState({ externalContentPolicy: 'block', trustedSenders: [] });
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
useSettingsStore.setState({ trustedSenders: ['orders@shop.example'] });
@@ -74,4 +81,12 @@ describe('StructuredMailBanner', () => {
useSettingsStore.setState({ externalContentPolicy: 'allow', trustedSenders: [] });
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(1);
});
it('loads no product images while the webmail config is unknown or forbids third-party traffic', () => {
useSettingsStore.setState({ externalContentPolicy: 'allow', trustedSenders: [] });
usePolicyStore.setState({ configPending: true, links: {} });
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
usePolicyStore.setState({ configPending: false, links: { noThirdPartyTraffic: true } });
expect(byTestId(render(createElement(StructuredMailBanner, { email: mail(PASS) })), 'structured-mail-item-image')).toHaveLength(0);
});
});
+15 -6
View File
@@ -2,13 +2,22 @@ import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
// With PGP and S/MIME in the app (#58) the encryption is no longer only
// HTTPS/TLS, so the App Store export compliance answer must say so.
// PGP and S/MIME (#58) are non-exempt encryption with standard algorithms, and
// the app is not on the French store, so App Store Connect has no declaration
// and no compliance code. Info.plist must not claim YES (upload validation
// rejects it without the code, 90592) nor NO (that would be untrue); the
// TestFlight script answers per build instead.
const root = path.resolve(__dirname, '../../..');
describe('export compliance', () => {
it('declares non-exempt encryption for iOS', () => {
const src = fs.readFileSync(path.resolve(__dirname, '../../../app.config.js'), 'utf8');
expect(src).toMatch(/usesNonExemptEncryption: true/);
expect(src).not.toMatch(/usesNonExemptEncryption: false/);
it('leaves the encryption key out of the iOS config', () => {
const src = fs.readFileSync(path.join(root, 'app.config.js'), 'utf8');
expect(src).not.toMatch(/usesNonExemptEncryption\s*:/);
});
it('answers non-exempt encryption for each TestFlight build', () => {
const src = fs.readFileSync(path.join(root, 'scripts/testflight-public.mjs'), 'utf8');
expect(src).toMatch(/attributes: \{ usesNonExemptEncryption: true \}/);
});
});