mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-05 00:07:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4ed501d7 | ||
|
|
af580e9f17 | ||
|
|
91046725cc |
+7
-6
@@ -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: {
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bulwark-mobile",
|
||||
"version": "0.1.88",
|
||||
"version": "0.1.89",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 \}/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user