Add environment-specific verified Android App Links
This commit is contained in:
parent
0dab9873b3
commit
f18e76b201
11
.env.example
11
.env.example
|
|
@ -90,10 +90,21 @@ WNH_FIREBASE_APPLICATION_ID=
|
||||||
WNH_FIREBASE_API_KEY=
|
WNH_FIREBASE_API_KEY=
|
||||||
WNH_FIREBASE_PROJECT_ID=
|
WNH_FIREBASE_PROJECT_ID=
|
||||||
WNH_FIREBASE_GCM_SENDER_ID=
|
WNH_FIREBASE_GCM_SENDER_ID=
|
||||||
|
# Verified Android App Links are configured by the web deployment rather than
|
||||||
|
# embedded as secrets in the application. Use org.whoneedhelp.mobile.staging
|
||||||
|
# with the staging signing certificate on the dev checkout and
|
||||||
|
# org.whoneedhelp.mobile with every active Play signing certificate on
|
||||||
|
# production. Keep both empty until the matching signed APK/AAB is available.
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME=
|
||||||
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=
|
||||||
# Public identifier of the locally held Google Play upload key. The private
|
# Public identifier of the locally held Google Play upload key. The private
|
||||||
# keystore and its randomized password live outside the repository under
|
# keystore and its randomized password live outside the repository under
|
||||||
# ~/.config/who_need_help/android-release/.
|
# ~/.config/who_need_help/android-release/.
|
||||||
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
||||||
|
# The dev-domain staging APK uses a different stable signing identity under
|
||||||
|
# ~/.config/who_need_help/android-staging/. This keeps App Link verification
|
||||||
|
# reproducible without reusing the future production upload key.
|
||||||
|
WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS=who-need-help-staging
|
||||||
WNH_ANDROID_TEST_API_MATRIX="24 30 34 37.0"
|
WNH_ANDROID_TEST_API_MATRIX="24 30 34 37.0"
|
||||||
WNH_ANDROID_TEST_DATA_PARTITION_SIZE=1G
|
WNH_ANDROID_TEST_DATA_PARTITION_SIZE=1G
|
||||||
# Public raster tile template used by MapLibre. Use a provider whose policy and
|
# Public raster tile template used by MapLibre. Use a provider whose policy and
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,11 @@ local Codex CLI authenticated with their ChatGPT subscription.
|
||||||
location foreground service. Its persistent
|
location foreground service. Its persistent
|
||||||
notification exposes Stop, it continues while the Activity is minimized, and
|
notification exposes Stop, it continues while the Activity is minimized, and
|
||||||
it retains only the current point. Reproducible Docker targets export
|
it retains only the current point. Reproducible Docker targets export
|
||||||
distinct local and public-staging debug APKs; production signing and store
|
distinct debug and stable-signed staging APKs plus a production-signed APK
|
||||||
publication are not configured.
|
and Play AAB. The web app publishes environment-specific verified Android
|
||||||
|
App Links metadata and the build verifies package/certificate agreement.
|
||||||
|
Play registration, Play App Signing identity, store review, and physical
|
||||||
|
device FCM delivery are still external release steps.
|
||||||
- Local, advisory Codex category review through the user's ChatGPT-authenticated
|
- Local, advisory Codex category review through the user's ChatGPT-authenticated
|
||||||
Codex CLI. It receives a PII-free export and never writes to the database.
|
Codex CLI. It receives a PII-free export and never writes to the database.
|
||||||
- One immutable release image with `web`, `worker`, combined `app`, and
|
- One immutable release image with `web`, `worker`, combined `app`, and
|
||||||
|
|
@ -94,7 +97,7 @@ local Codex CLI authenticated with their ChatGPT subscription.
|
||||||
replicas by default.
|
replicas by default.
|
||||||
|
|
||||||
Additional social providers, background PWA or unattended location tracking,
|
Additional social providers, background PWA or unattended location tracking,
|
||||||
platform payments, production Android signing/store publication, iOS,
|
platform payments, Android store publication, iOS,
|
||||||
automatic punitive fraud decisions, and jurisdiction-specific public-launch
|
automatic punitive fraud decisions, and jurisdiction-specific public-launch
|
||||||
policies are deliberately not claimed as complete.
|
policies are deliberately not claimed as complete.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ COPY --from=android-sdk \
|
||||||
FROM android-base AS android-staging-sdk
|
FROM android-base AS android-staging-sdk
|
||||||
|
|
||||||
USER gradle
|
USER gradle
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
WORKDIR /workspace/android
|
WORKDIR /workspace/android
|
||||||
|
|
||||||
COPY --chown=gradle:gradle . .
|
COPY --chown=gradle:gradle . .
|
||||||
|
|
@ -165,6 +166,11 @@ ARG WNH_FIREBASE_GCM_SENDER_ID
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
||||||
|
--mount=type=secret,id=android_staging_keystore,required=true,uid=1000,gid=1000,mode=0400 \
|
||||||
|
--mount=type=secret,id=android_staging_password,required=true,uid=1000,gid=1000,mode=0400 \
|
||||||
|
--mount=type=secret,id=android_staging_alias,required=true,env=WNH_ANDROID_SIGNING_KEY_ALIAS \
|
||||||
|
WNH_ANDROID_SIGNING_STORE_FILE=/run/secrets/android_staging_keystore \
|
||||||
|
WNH_ANDROID_SIGNING_PASSWORD_FILE=/run/secrets/android_staging_password \
|
||||||
gradle --no-daemon \
|
gradle --no-daemon \
|
||||||
"-PWNH_BASE_URL=${WNH_BASE_URL}" \
|
"-PWNH_BASE_URL=${WNH_BASE_URL}" \
|
||||||
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
||||||
|
|
@ -177,7 +183,17 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
"-PWNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID}" \
|
"-PWNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID}" \
|
||||||
"-PWNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID}" \
|
"-PWNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID}" \
|
||||||
"-PWNH_TEST_BUILD_TYPE=staging" \
|
"-PWNH_TEST_BUILD_TYPE=staging" \
|
||||||
testStagingUnitTest lintStaging assembleStaging assembleStagingAndroidTest
|
testStagingUnitTest lintStaging assembleStaging assembleStagingAndroidTest \
|
||||||
|
&& "${ANDROID_HOME}/build-tools/37.0.0/apksigner" \
|
||||||
|
verify --verbose --print-certs \
|
||||||
|
app/build/outputs/apk/staging/app-staging.apk \
|
||||||
|
>app/build/outputs/apk/staging/signing-certificate.txt \
|
||||||
|
&& "${ANDROID_HOME}/build-tools/37.0.0/aapt2" dump badging \
|
||||||
|
app/build/outputs/apk/staging/app-staging.apk \
|
||||||
|
| sed -n "s/^package: name='\\([^']*\\)'.*/\\1/p" \
|
||||||
|
>app/build/outputs/apk/staging/package-name.txt \
|
||||||
|
&& grep -Fx "org.whoneedhelp.mobile.staging" \
|
||||||
|
app/build/outputs/apk/staging/package-name.txt
|
||||||
|
|
||||||
FROM scratch AS staging-artifact
|
FROM scratch AS staging-artifact
|
||||||
|
|
||||||
|
|
@ -192,6 +208,12 @@ COPY --from=android-staging-sdk \
|
||||||
COPY --from=android-staging-sdk \
|
COPY --from=android-staging-sdk \
|
||||||
/workspace/android/app/build/reports/lint-results-staging.html \
|
/workspace/android/app/build/reports/lint-results-staging.html \
|
||||||
/lint-results-staging.html
|
/lint-results-staging.html
|
||||||
|
COPY --from=android-staging-sdk \
|
||||||
|
/workspace/android/app/build/outputs/apk/staging/signing-certificate.txt \
|
||||||
|
/signing-certificate.txt
|
||||||
|
COPY --from=android-staging-sdk \
|
||||||
|
/workspace/android/app/build/outputs/apk/staging/package-name.txt \
|
||||||
|
/package-name.txt
|
||||||
|
|
||||||
FROM android-base AS android-release-base
|
FROM android-base AS android-release-base
|
||||||
|
|
||||||
|
|
@ -212,6 +234,7 @@ USER gradle
|
||||||
FROM android-release-base AS android-release-sdk
|
FROM android-release-base AS android-release-sdk
|
||||||
|
|
||||||
USER gradle
|
USER gradle
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
WORKDIR /workspace/android
|
WORKDIR /workspace/android
|
||||||
|
|
||||||
COPY --chown=gradle:gradle . .
|
COPY --chown=gradle:gradle . .
|
||||||
|
|
@ -248,6 +271,12 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
verify --verbose --print-certs \
|
verify --verbose --print-certs \
|
||||||
app/build/outputs/apk/release/app-release.apk \
|
app/build/outputs/apk/release/app-release.apk \
|
||||||
>app/build/outputs/apk/release/signing-certificate.txt \
|
>app/build/outputs/apk/release/signing-certificate.txt \
|
||||||
|
&& "${ANDROID_HOME}/build-tools/37.0.0/aapt2" dump badging \
|
||||||
|
app/build/outputs/apk/release/app-release.apk \
|
||||||
|
| sed -n "s/^package: name='\\([^']*\\)'.*/\\1/p" \
|
||||||
|
>app/build/outputs/apk/release/package-name.txt \
|
||||||
|
&& grep -Fx "org.whoneedhelp.mobile" \
|
||||||
|
app/build/outputs/apk/release/package-name.txt \
|
||||||
&& LC_ALL=C jarsigner -verify -verbose -certs \
|
&& LC_ALL=C jarsigner -verify -verbose -certs \
|
||||||
app/build/outputs/bundle/release/app-release.aab \
|
app/build/outputs/bundle/release/app-release.aab \
|
||||||
>app/build/outputs/bundle/release/signing-verification.txt \
|
>app/build/outputs/bundle/release/signing-verification.txt \
|
||||||
|
|
@ -270,6 +299,9 @@ COPY --from=android-release-sdk \
|
||||||
COPY --from=android-release-sdk \
|
COPY --from=android-release-sdk \
|
||||||
/workspace/android/app/build/outputs/apk/release/signing-certificate.txt \
|
/workspace/android/app/build/outputs/apk/release/signing-certificate.txt \
|
||||||
/signing-certificate.txt
|
/signing-certificate.txt
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/outputs/apk/release/package-name.txt \
|
||||||
|
/package-name.txt
|
||||||
COPY --from=android-release-sdk \
|
COPY --from=android-release-sdk \
|
||||||
/workspace/android/app/build/outputs/bundle/release/signing-verification.txt \
|
/workspace/android/app/build/outputs/bundle/release/signing-verification.txt \
|
||||||
/bundle-signing-verification.txt
|
/bundle-signing-verification.txt
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,9 @@ android {
|
||||||
initWith(getByName("debug"))
|
initWith(getByName("debug"))
|
||||||
applicationIdSuffix = ".staging"
|
applicationIdSuffix = ".staging"
|
||||||
versionNameSuffix = "-staging"
|
versionNameSuffix = "-staging"
|
||||||
|
if (releaseSigningConfigured) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
"BASE_URL",
|
"BASE_URL",
|
||||||
|
|
@ -221,12 +224,12 @@ android {
|
||||||
tasks.matching { it.name == "preReleaseBuild" || it.name == "preStagingBuild" }.configureEach {
|
tasks.matching { it.name == "preReleaseBuild" || it.name == "preStagingBuild" }.configureEach {
|
||||||
doFirst {
|
doFirst {
|
||||||
validateFirebaseConfiguration()
|
validateFirebaseConfiguration()
|
||||||
if (name == "preReleaseBuild" && !releaseSigningConfigured) {
|
if (!releaseSigningConfigured) {
|
||||||
val detail =
|
val detail =
|
||||||
if (releaseSigningPartiallyConfigured) {
|
if (releaseSigningPartiallyConfigured) {
|
||||||
"Release signing is only partially configured"
|
"Android signing is only partially configured"
|
||||||
} else {
|
} else {
|
||||||
"Release signing is not configured"
|
"Android signing is not configured"
|
||||||
}
|
}
|
||||||
throw GradleException(
|
throw GradleException(
|
||||||
"$detail; set WNH_ANDROID_SIGNING_STORE_FILE, "
|
"$detail; set WNH_ANDROID_SIGNING_STORE_FILE, "
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="false">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ x-app-environment: &app-environment
|
||||||
FCM_PROJECT_ID: ${FCM_PROJECT_ID:-}
|
FCM_PROJECT_ID: ${FCM_PROJECT_ID:-}
|
||||||
FCM_SERVICE_ACCOUNT_FILE: ${FCM_SERVICE_ACCOUNT_FILE:-}
|
FCM_SERVICE_ACCOUNT_FILE: ${FCM_SERVICE_ACCOUNT_FILE:-}
|
||||||
FCM_SERVICE_ACCOUNT_JSON_BASE64: ${FCM_SERVICE_ACCOUNT_JSON_BASE64:-}
|
FCM_SERVICE_ACCOUNT_JSON_BASE64: ${FCM_SERVICE_ACCOUNT_JSON_BASE64:-}
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME: ${ANDROID_APP_LINKS_PACKAGE_NAME:-}
|
||||||
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS: ${ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS:-}
|
||||||
OBAN_MAINTENANCE_CONCURRENCY: ${OBAN_MAINTENANCE_CONCURRENCY:-2}
|
OBAN_MAINTENANCE_CONCURRENCY: ${OBAN_MAINTENANCE_CONCURRENCY:-2}
|
||||||
OBAN_PUSH_CONCURRENCY: ${OBAN_PUSH_CONCURRENCY:-1}
|
OBAN_PUSH_CONCURRENCY: ${OBAN_PUSH_CONCURRENCY:-1}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ config :who_need_help,
|
||||||
secure_cookies: false,
|
secure_cookies: false,
|
||||||
rate_limit_policies: %{},
|
rate_limit_policies: %{},
|
||||||
map_tile_url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
map_tile_url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
android_app_links: nil,
|
||||||
web_push_public_key: nil,
|
web_push_public_key: nil,
|
||||||
fcm_goth_source: nil,
|
fcm_goth_source: nil,
|
||||||
device_delivery_options: %{
|
device_delivery_options: %{
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,57 @@ config :who_need_help,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android_app_links =
|
||||||
|
case {
|
||||||
|
System.get_env("ANDROID_APP_LINKS_PACKAGE_NAME"),
|
||||||
|
System.get_env("ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS")
|
||||||
|
} do
|
||||||
|
{package_name, fingerprints}
|
||||||
|
when is_binary(package_name) and package_name != "" and is_binary(fingerprints) and
|
||||||
|
fingerprints != "" ->
|
||||||
|
unless Regex.match?(
|
||||||
|
~r/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/,
|
||||||
|
package_name
|
||||||
|
) do
|
||||||
|
raise "ANDROID_APP_LINKS_PACKAGE_NAME must be a valid Android application ID."
|
||||||
|
end
|
||||||
|
|
||||||
|
normalized_fingerprints =
|
||||||
|
fingerprints
|
||||||
|
|> String.split(",", trim: true)
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.map(fn fingerprint ->
|
||||||
|
hex = fingerprint |> String.replace(":", "") |> String.upcase()
|
||||||
|
|
||||||
|
unless Regex.match?(~r/^[0-9A-F]{64}$/, hex) do
|
||||||
|
raise """
|
||||||
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS must contain comma-separated \
|
||||||
|
SHA-256 certificate fingerprints.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
hex
|
||||||
|
|> String.graphemes()
|
||||||
|
|> Enum.chunk_every(2)
|
||||||
|
|> Enum.map_join(":", &Enum.join/1)
|
||||||
|
end)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|
||||||
|
%{package_name: package_name, sha256_cert_fingerprints: normalized_fingerprints}
|
||||||
|
|
||||||
|
{package_name, fingerprints}
|
||||||
|
when package_name in [nil, ""] and fingerprints in [nil, ""] ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
_partial_configuration ->
|
||||||
|
raise """
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME and \
|
||||||
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS must either both be set or both be empty.
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
config :who_need_help, :android_app_links, android_app_links
|
||||||
|
|
||||||
if config_env() == :prod and app_role in [:web, :worker, :combined] do
|
if config_env() == :prod and app_role in [:web, :worker, :combined] do
|
||||||
metrics_token =
|
metrics_token =
|
||||||
System.get_env("METRICS_TOKEN") ||
|
System.get_env("METRICS_TOKEN") ||
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,51 @@ file. Both applications intentionally share only the external
|
||||||
`who-need-help-production:4000` and `test.whoneedhelp.com` to
|
`who-need-help-production:4000` and `test.whoneedhelp.com` to
|
||||||
`who-need-help-test:4000`.
|
`who-need-help-test:4000`.
|
||||||
|
|
||||||
|
### Environment-specific Android builds and App Links
|
||||||
|
|
||||||
|
Android build inputs belong in the same ignored mode-`0600` `.env` as the web
|
||||||
|
checkout they target. Do not create `.env.android-release`,
|
||||||
|
`.env.production`, or another permanent environment file:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
WNH_BASE_URL=https://dev.example.com
|
||||||
|
WNH_ANDROID_VERSION_CODE=1
|
||||||
|
WNH_ANDROID_VERSION_NAME=0.1.0
|
||||||
|
WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS=who-need-help-staging
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging
|
||||||
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=AA:BB:...
|
||||||
|
```
|
||||||
|
|
||||||
|
The dev checkout uses package `org.whoneedhelp.mobile.staging` and a dedicated
|
||||||
|
stable key under `~/.config/who_need_help/android-staging/`. Generate it once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/init-android-staging-signing.sh
|
||||||
|
./scripts/android-staging-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The build exports the package and certificate reports, verifies that both match
|
||||||
|
the checkout `.env`, and checks the HTTPS
|
||||||
|
`/.well-known/assetlinks.json` response. Losing this key changes the staging
|
||||||
|
certificate and breaks previously installed App Links, so back it up.
|
||||||
|
|
||||||
|
The production checkout instead uses package `org.whoneedhelp.mobile`, the
|
||||||
|
separate upload material under
|
||||||
|
`~/.config/who_need_help/android-release/`, and its own `.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile
|
||||||
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=UPLOAD_OR_PLAY_SHA256
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `./scripts/android-release-build.sh` from that production release checkout.
|
||||||
|
It produces an APK, Play AAB, package report, signing report, and lint report.
|
||||||
|
After Play App Signing is enabled, add the Play signing certificate fingerprint
|
||||||
|
to the comma-separated App Links value; the upload certificate alone does not
|
||||||
|
describe Play-delivered APKs. The production environment validator accepts
|
||||||
|
multiple SHA-256 fingerprints and rejects partial or malformed configuration.
|
||||||
|
|
||||||
Create the test configuration inside the test checkout:
|
Create the test configuration inside the test checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ this audit.
|
||||||
| Social profiles | Manual links implemented; optional GitHub verification implemented and automated-tested | Manual links cannot set verification fields. The optional GitHub flow uses state, PKCE, a user-bound one-time session, unique provider ownership, and an audit record. The local protocol drill also performs real HTTP token/user exchanges without returning an access token to the application. | GitHub OAuth credentials are intentionally absent and are not required for registration or the help flow. The real external provider redirect/callback remains disabled and unverified; other providers remain manual/unverified. |
|
| Social profiles | Manual links implemented; optional GitHub verification implemented and automated-tested | Manual links cannot set verification fields. The optional GitHub flow uses state, PKCE, a user-bound one-time session, unique provider ownership, and an audit record. The local protocol drill also performs real HTTP token/user exchanges without returning an access token to the application. | GitHub OAuth credentials are intentionally absent and are not required for registration or the help flow. The real external provider redirect/callback remains disabled and unverified; other providers remain manual/unverified. |
|
||||||
| Support and content removal | Implemented and browser-verified | Public support, account deletion, general removal, and TAKE IT DOWN forms create separate audited queues; private email status links verify public contacts; authenticated submissions use the account email; moderator-only operations can update status and notify verified contacts. TAKE IT DOWN accepts URLs/text only and records a 48-hour review due time. Authenticated users can download an allowlisted JSON export, and moderators can run a read-only deletion relationship preflight. | The current product hosts no user media and does not claim TAKE IT DOWN coverage. Staffing, jurisdiction-specific legal classification, final retention rules, destructive account erasure/anonymisation, and identical-media-copy handling remain operational/legal work. |
|
| Support and content removal | Implemented and browser-verified | Public support, account deletion, general removal, and TAKE IT DOWN forms create separate audited queues; private email status links verify public contacts; authenticated submissions use the account email; moderator-only operations can update status and notify verified contacts. TAKE IT DOWN accepts URLs/text only and records a 48-hour review due time. Authenticated users can download an allowlisted JSON export, and moderators can run a read-only deletion relationship preflight. | The current product hosts no user media and does not claim TAKE IT DOWN coverage. Staffing, jurisdiction-specific legal classification, final retention rules, destructive account erasure/anonymisation, and identical-media-copy handling remain operational/legal work. |
|
||||||
| Voluntary thanks | Implemented as an external optional link | A helper can expose an optional link after completion; the UI states that the platform does not process the payment. | The platform does not provide payments, escrow, refunds, tax reporting, or payment guarantees. |
|
| Voluntary thanks | Implemented as an external optional link | A helper can expose an optional link after completion; the UI states that the platform does not process the payment. | The platform does not provide payments, escrow, refunds, tax reporting, or payment guarantees. |
|
||||||
| Android client | Local and public-staging clients implemented and emulator-verified | The native packages `org.whoneedhelp.mobile.debug` and `org.whoneedhelp.mobile.staging` launch the same authenticated LiveView app. Existing lifecycle, permission, deep-link, foreground tracking, recoverable main-page failure, notification-Stop, and Activity-destruction tests remain in place. The final local build also covers consent-based FCM token registration, data-only notification routing, and request/notification deep links. | Production signing, Play Store publication, verified Android App Links, physical-device FCM delivery, unattended/background-permission tracking, and iOS are not implemented. |
|
| Android client | Local and public-staging clients implemented and emulator-verified | The native packages `org.whoneedhelp.mobile.debug` and `org.whoneedhelp.mobile.staging` launch the same authenticated LiveView app. Existing lifecycle, permission, deep-link, foreground tracking, recoverable main-page failure, notification-Stop, and Activity-destruction tests remain in place. A stable staging certificate now signs the dev APK, the HTTPS deployment publishes the matching App Links statement, and the build checks its package and SHA-256 certificate. A separate upload key produces a signed production APK and Play AAB. | Play registration/App Signing, on-device domain-verification observation, physical-device FCM delivery, unattended/background-permission tracking, and iOS are not complete. |
|
||||||
| Multiple web/worker instances | Implemented and locally failure/rollout-verified | The final isolated Compose drill passed BEAM crashes and sequential replacement with 3 web/2 worker replicas: all five nodes joined, PubSub passed, and 744/744 readiness requests succeeded. The project-owned kind cluster replaced all 2 web/2 worker pod UIDs under `maxUnavailable=0`; all four replacement pods joined, PubSub passed, and 363/363 samples ultimately succeeded. | Local PostGIS is a single instance. Production database HA, backups, and recovery are operator work and are not claimed complete. |
|
| Multiple web/worker instances | Implemented and locally failure/rollout-verified | The final isolated Compose drill passed BEAM crashes and sequential replacement with 3 web/2 worker replicas: all five nodes joined, PubSub passed, and 744/744 readiness requests succeeded. The project-owned kind cluster replaced all 2 web/2 worker pod UIDs under `maxUnavailable=0`; all four replacement pods joined, PubSub passed, and 363/363 samples ultimately succeeded. | Local PostGIS is a single instance. Production database HA, backups, and recovery are operator work and are not claimed complete. |
|
||||||
| Local observability | Implemented and protocol-verified | Pinned Prometheus scraped the exact 3 web and 2 worker targets with a file Bearer credential; Grafana provisioned a healthy datasource and ten-panel web/worker/BEAM/Ecto/Oban dashboard; Alertmanager delivered firing and resolved webhooks for an induced scoped replica stop. | Local delivery does not establish production retention, notification-provider reliability, on-call policy, or measured alert thresholds. |
|
| Local observability | Implemented and protocol-verified | Pinned Prometheus scraped the exact 3 web and 2 worker targets with a file Bearer credential; Grafana provisioned a healthy datasource and ten-panel web/worker/BEAM/Ecto/Oban dashboard; Alertmanager delivered firing and resolved webhooks for an induced scoped replica stop. | Local delivery does not establish production retention, notification-provider reliability, on-call policy, or measured alert thresholds. |
|
||||||
| Encrypted local backup | Implemented and failure-verified | Pinned Restic streamed PostgreSQL custom format into pinned local MinIO with no host plaintext dump, passed full-data checking and a fresh-database restore, rejected a corrupted repository, and published no snapshot for an interrupted upload. The one-run MinIO project and volume were removed after retaining the non-secret evidence. | The drill proves the local mechanism, not off-site durability, database HA, or a production RPO/RTO/retention policy. |
|
| Encrypted local backup | Implemented and failure-verified | Pinned Restic streamed PostgreSQL custom format into pinned local MinIO with no host plaintext dump, passed full-data checking and a fresh-database restore, rejected a corrupted repository, and published no snapshot for an interrupted upload. The one-run MinIO project and volume were removed after retaining the non-secret evidence. | The drill proves the local mechanism, not off-site durability, database HA, or a production RPO/RTO/retention policy. |
|
||||||
|
|
@ -1300,11 +1300,14 @@ None of the observations below describe the current delivery path.
|
||||||
health endpoints, migrations, Google callback, and authentication-email flow
|
health endpoints, migrations, Google callback, and authentication-email flow
|
||||||
after that promotion; the current test origin still depends on its configured
|
after that promotion; the current test origin still depends on its configured
|
||||||
workstation/VPN/gateway path.
|
workstation/VPN/gateway path.
|
||||||
- Confirm the final Android application ID before creating its Play Console
|
- The final Android application ID is `org.whoneedhelp.mobile`. The application
|
||||||
listing, publish `/.well-known/assetlinks.json` for that ID and the final
|
now publishes environment-specific `/.well-known/assetlinks.json`, and the
|
||||||
signing fingerprint if verified App Links are wanted, and complete store
|
stable-signed dev APK was checked against its HTTPS response. Before a Play
|
||||||
policy/release work. A dedicated upload key and signed APK/AAB have been
|
release, register the application, add the Play App Signing certificate
|
||||||
created and verified locally, but no Play application has been registered.
|
fingerprint alongside any sideload/upload fingerprint, repeat Android's
|
||||||
|
domain verification on a device, and complete store policy/release work. A
|
||||||
|
dedicated upload key and signed APK/AAB exist, but no Play application has
|
||||||
|
been registered.
|
||||||
- Operate PostgreSQL/PostGIS with off-site backups, recovery testing, monitoring,
|
- Operate PostgreSQL/PostGIS with off-site backups, recovery testing, monitoring,
|
||||||
and the availability model selected for real usage.
|
and the availability model selected for real usage.
|
||||||
- After provider approval, verify that delivered MIME contains neither open nor
|
- After provider approval, verify that delivered MIME contains neither open nor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
defmodule WhoNeedHelpWeb.AndroidAppLinksController do
|
||||||
|
use WhoNeedHelpWeb, :controller
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
case Application.get_env(:who_need_help, :android_app_links) do
|
||||||
|
%{
|
||||||
|
package_name: package_name,
|
||||||
|
sha256_cert_fingerprints: fingerprints
|
||||||
|
} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_header("cache-control", "public, max-age=300")
|
||||||
|
|> json([
|
||||||
|
%{
|
||||||
|
relation: ["delegate_permission/common.handle_all_urls"],
|
||||||
|
target: %{
|
||||||
|
namespace: "android_app",
|
||||||
|
package_name: package_name,
|
||||||
|
sha256_cert_fingerprints: fingerprints
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
_not_configured ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "android_app_links_not_configured"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -46,6 +46,12 @@ defmodule WhoNeedHelpWeb.Router do
|
||||||
get "/ready", HealthController, :ready
|
get "/ready", HealthController, :ready
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/.well-known", WhoNeedHelpWeb do
|
||||||
|
pipe_through :api
|
||||||
|
|
||||||
|
get "/assetlinks.json", AndroidAppLinksController, :show
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", WhoNeedHelpWeb do
|
scope "/", WhoNeedHelpWeb do
|
||||||
get "/metrics", MetricsController, :show
|
get "/metrics", MetricsController, :show
|
||||||
end
|
end
|
||||||
|
|
|
||||||
111
scripts/android-app-links-verify.sh
Executable file
111
scripts/android-app-links-verify.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
env_file=${1:-"$ROOT/.env"}
|
||||||
|
artifact_dir=${2:-"$ROOT/android/dist-staging"}
|
||||||
|
online_mode=${3:-}
|
||||||
|
|
||||||
|
if [[ "$env_file" != /* ]]; then
|
||||||
|
env_file="$ROOT/$env_file"
|
||||||
|
fi
|
||||||
|
if [[ "$artifact_dir" != /* ]]; then
|
||||||
|
artifact_dir="$ROOT/$artifact_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$env_file" ]] || {
|
||||||
|
echo "Android App Links environment does not exist: $env_file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ -f "$artifact_dir/package-name.txt" ]] || {
|
||||||
|
echo "Android package report does not exist: $artifact_dir/package-name.txt" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ -f "$artifact_dir/signing-certificate.txt" ]] || {
|
||||||
|
echo "Android signing report does not exist: $artifact_dir/signing-certificate.txt" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key=$1
|
||||||
|
|
||||||
|
awk -v key="$key" '
|
||||||
|
index($0, key "=") == 1 {
|
||||||
|
print substr($0, length(key) + 2)
|
||||||
|
found = 1
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
END { if (!found) exit 1 }
|
||||||
|
' "$env_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_package=$(read_env_value ANDROID_APP_LINKS_PACKAGE_NAME 2>/dev/null || true)
|
||||||
|
expected_fingerprints=$(
|
||||||
|
read_env_value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS 2>/dev/null || true
|
||||||
|
)
|
||||||
|
base_url=$(read_env_value WNH_BASE_URL 2>/dev/null || true)
|
||||||
|
observed_package=$(tr -d '\r\n' <"$artifact_dir/package-name.txt")
|
||||||
|
observed_fingerprint=$(
|
||||||
|
awk -F': ' '
|
||||||
|
/certificate SHA-256 digest:/ {
|
||||||
|
print $NF
|
||||||
|
found = 1
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
END { if (!found) exit 1 }
|
||||||
|
' "$artifact_dir/signing-certificate.txt" |
|
||||||
|
tr '[:lower:]' '[:upper:]'
|
||||||
|
)
|
||||||
|
observed_fingerprint=$(
|
||||||
|
printf '%s' "$observed_fingerprint" |
|
||||||
|
sed 's/../&:/g; s/:$//'
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ -n "$expected_package" && -n "$expected_fingerprints" ]] || {
|
||||||
|
echo "Android App Links package and fingerprints are not configured in $env_file." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$observed_package" == "$expected_package" ]] || {
|
||||||
|
echo "The signed APK package does not match ANDROID_APP_LINKS_PACKAGE_NAME." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint_found=false
|
||||||
|
IFS=',' read -r -a fingerprints <<<"$expected_fingerprints"
|
||||||
|
for fingerprint in "${fingerprints[@]}"; do
|
||||||
|
compact=${fingerprint//:/}
|
||||||
|
compact=${compact//[[:space:]]/}
|
||||||
|
normalized=$(printf '%s' "$compact" | tr '[:lower:]' '[:upper:]' | sed 's/../&:/g; s/:$//')
|
||||||
|
if [[ "$normalized" == "$observed_fingerprint" ]]; then
|
||||||
|
fingerprint_found=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ "$fingerprint_found" == true ]] || {
|
||||||
|
echo "The signed APK certificate is absent from the configured App Links fingerprints." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$online_mode" == --online ]]; then
|
||||||
|
[[ "$base_url" == https://* ]] || {
|
||||||
|
echo "Online Android App Links verification requires an HTTPS WNH_BASE_URL." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
payload=$(curl --fail --silent --show-error \
|
||||||
|
"$base_url/.well-known/assetlinks.json")
|
||||||
|
jq -e \
|
||||||
|
--arg package "$observed_package" \
|
||||||
|
--arg fingerprint "$observed_fingerprint" \
|
||||||
|
'
|
||||||
|
any(
|
||||||
|
.[];
|
||||||
|
.target.namespace == "android_app" and
|
||||||
|
.target.package_name == $package and
|
||||||
|
(.relation | index("delegate_permission/common.handle_all_urls")) != null and
|
||||||
|
(.target.sha256_cert_fingerprints | index($fingerprint)) != null
|
||||||
|
)
|
||||||
|
' <<<"$payload" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Android package, signing certificate, and App Links configuration agree."
|
||||||
|
|
@ -74,6 +74,7 @@ for artifact in \
|
||||||
"$OUTPUT_DIR/who-need-help-release.apk" \
|
"$OUTPUT_DIR/who-need-help-release.apk" \
|
||||||
"$OUTPUT_DIR/who-need-help-release.aab" \
|
"$OUTPUT_DIR/who-need-help-release.aab" \
|
||||||
"$OUTPUT_DIR/signing-certificate.txt" \
|
"$OUTPUT_DIR/signing-certificate.txt" \
|
||||||
|
"$OUTPUT_DIR/package-name.txt" \
|
||||||
"$OUTPUT_DIR/bundle-signing-verification.txt" \
|
"$OUTPUT_DIR/bundle-signing-verification.txt" \
|
||||||
"$OUTPUT_DIR/bundletool-validation.txt" \
|
"$OUTPUT_DIR/bundletool-validation.txt" \
|
||||||
"$OUTPUT_DIR/lint-results-release.html"; do
|
"$OUTPUT_DIR/lint-results-release.html"; do
|
||||||
|
|
@ -83,6 +84,11 @@ for artifact in \
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [ -n "${ANDROID_APP_LINKS_PACKAGE_NAME:-}" ] ||
|
||||||
|
[ -n "${ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS:-}" ]; then
|
||||||
|
"$ROOT/scripts/android-app-links-verify.sh" "$ENV_FILE" "$OUTPUT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
sha256sum \
|
sha256sum \
|
||||||
"$OUTPUT_DIR/who-need-help-release.apk" \
|
"$OUTPUT_DIR/who-need-help-release.apk" \
|
||||||
"$OUTPUT_DIR/who-need-help-release.aab"
|
"$OUTPUT_DIR/who-need-help-release.aab"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ set -eu
|
||||||
|
|
||||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
ENV_FILE="$ROOT/.env"
|
ENV_FILE="$ROOT/.env"
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
SIGNING_DIR=${WNH_ANDROID_STAGING_SIGNING_DIR:-"$config_home/who_need_help/android-staging"}
|
||||||
|
KEYSTORE="$SIGNING_DIR/who-need-help-staging.p12"
|
||||||
|
PASSWORD_FILE="$SIGNING_DIR/who-need-help-staging.password"
|
||||||
|
|
||||||
"$ROOT/scripts/ensure-local-public-origin.sh"
|
"$ROOT/scripts/ensure-local-public-origin.sh"
|
||||||
|
|
||||||
|
|
@ -14,11 +18,36 @@ set +a
|
||||||
: "${WNH_BASE_URL:?Set WNH_BASE_URL in .env}"
|
: "${WNH_BASE_URL:?Set WNH_BASE_URL in .env}"
|
||||||
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
||||||
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||||
|
: "${WNH_ANDROID_VERSION_CODE:?Set WNH_ANDROID_VERSION_CODE in .env}"
|
||||||
|
: "${WNH_ANDROID_VERSION_NAME:?Set WNH_ANDROID_VERSION_NAME in .env}"
|
||||||
|
: "${WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS:?Set WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS in .env}"
|
||||||
|
|
||||||
exec docker build \
|
for secret_file in "$KEYSTORE" "$PASSWORD_FILE"; do
|
||||||
|
if [ ! -f "$secret_file" ]; then
|
||||||
|
echo "Missing Android staging signing file: $secret_file" >&2
|
||||||
|
echo "Run scripts/init-android-staging-signing.sh once." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mode=$(stat -c '%a' "$secret_file")
|
||||||
|
case "$mode" in
|
||||||
|
400|600) ;;
|
||||||
|
*)
|
||||||
|
echo "Android staging signing file must have mode 0400 or 0600: $secret_file" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--secret "id=android_staging_keystore,src=$KEYSTORE" \
|
||||||
|
--secret "id=android_staging_password,src=$PASSWORD_FILE" \
|
||||||
|
--secret "id=android_staging_alias,env=WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS" \
|
||||||
--build-arg "WNH_BASE_URL=$WNH_BASE_URL" \
|
--build-arg "WNH_BASE_URL=$WNH_BASE_URL" \
|
||||||
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
||||||
--build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
|
--build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
|
||||||
|
--build-arg "WNH_ANDROID_VERSION_CODE=$WNH_ANDROID_VERSION_CODE" \
|
||||||
|
--build-arg "WNH_ANDROID_VERSION_NAME=$WNH_ANDROID_VERSION_NAME" \
|
||||||
--build-arg "WNH_FIREBASE_APPLICATION_ID=${WNH_FIREBASE_APPLICATION_ID:-}" \
|
--build-arg "WNH_FIREBASE_APPLICATION_ID=${WNH_FIREBASE_APPLICATION_ID:-}" \
|
||||||
--build-arg "WNH_FIREBASE_CLIENT_VALUE=${WNH_FIREBASE_API_KEY:-}" \
|
--build-arg "WNH_FIREBASE_CLIENT_VALUE=${WNH_FIREBASE_API_KEY:-}" \
|
||||||
--build-arg "WNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID:-}" \
|
--build-arg "WNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID:-}" \
|
||||||
|
|
@ -26,3 +55,8 @@ exec docker build \
|
||||||
--target staging-artifact \
|
--target staging-artifact \
|
||||||
--output "type=local,dest=$ROOT/android/dist-staging" \
|
--output "type=local,dest=$ROOT/android/dist-staging" \
|
||||||
"$ROOT/android"
|
"$ROOT/android"
|
||||||
|
|
||||||
|
"$ROOT/scripts/android-app-links-verify.sh" \
|
||||||
|
"$ENV_FILE" \
|
||||||
|
"$ROOT/android/dist-staging" \
|
||||||
|
--online
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ umask 077
|
||||||
|
|
||||||
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
SIGNING_DIR=${WNH_ANDROID_SIGNING_DIR:-"$config_home/who_need_help/android-release"}
|
SIGNING_DIR=${WNH_ANDROID_SIGNING_DIR:-"$config_home/who_need_help/android-release"}
|
||||||
KEYSTORE="$SIGNING_DIR/who-need-help-upload.p12"
|
SIGNING_BASENAME=${WNH_ANDROID_SIGNING_BASENAME:-who-need-help-upload}
|
||||||
PASSWORD_FILE="$SIGNING_DIR/who-need-help-upload.password"
|
KEYSTORE="$SIGNING_DIR/$SIGNING_BASENAME.p12"
|
||||||
|
PASSWORD_FILE="$SIGNING_DIR/$SIGNING_BASENAME.password"
|
||||||
KEY_ALIAS=${WNH_ANDROID_SIGNING_KEY_ALIAS:-who-need-help-upload}
|
KEY_ALIAS=${WNH_ANDROID_SIGNING_KEY_ALIAS:-who-need-help-upload}
|
||||||
|
SUBJECT=${WNH_ANDROID_SIGNING_SUBJECT:-"CN=Who Need Help upload key"}
|
||||||
KEY_IMAGE="gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819c235db886d0fae7"
|
KEY_IMAGE="gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819c235db886d0fae7"
|
||||||
run_id="$$-$(openssl rand -hex 4)"
|
run_id="$$-$(openssl rand -hex 4)"
|
||||||
temporary_keystore="$SIGNING_DIR/.who-need-help-upload.$run_id.p12"
|
temporary_keystore="$SIGNING_DIR/.$SIGNING_BASENAME.$run_id.p12"
|
||||||
temporary_password="$SIGNING_DIR/.who-need-help-upload.$run_id.password"
|
temporary_password="$SIGNING_DIR/.$SIGNING_BASENAME.$run_id.password"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [ -e "$temporary_keystore" ]; then
|
if [ -e "$temporary_keystore" ]; then
|
||||||
|
|
@ -30,6 +32,13 @@ case "$KEY_ALIAS" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
case "$SIGNING_BASENAME" in
|
||||||
|
''|*[!A-Za-z0-9._-]*)
|
||||||
|
echo "WNH_ANDROID_SIGNING_BASENAME must use only letters, digits, dot, underscore, and dash." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if [ -L "$SIGNING_DIR" ]; then
|
if [ -L "$SIGNING_DIR" ]; then
|
||||||
echo "Refusing to use a symlink as the Android signing directory: $SIGNING_DIR" >&2
|
echo "Refusing to use a symlink as the Android signing directory: $SIGNING_DIR" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -61,7 +70,7 @@ docker run --rm \
|
||||||
-keyalg RSA \
|
-keyalg RSA \
|
||||||
-keysize 2048 \
|
-keysize 2048 \
|
||||||
-validity 10000 \
|
-validity 10000 \
|
||||||
-dname "CN=Who Need Help upload key"
|
-dname "$SUBJECT"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--user "$(id -u):$(id -g)" \
|
--user "$(id -u):$(id -g)" \
|
||||||
|
|
@ -78,7 +87,7 @@ chmod 600 "$temporary_keystore"
|
||||||
mv "$temporary_keystore" "$KEYSTORE"
|
mv "$temporary_keystore" "$KEYSTORE"
|
||||||
mv "$temporary_password" "$PASSWORD_FILE"
|
mv "$temporary_password" "$PASSWORD_FILE"
|
||||||
|
|
||||||
echo "Generated a dedicated Android upload key without placing secrets in the repository."
|
echo "Generated dedicated Android signing material without placing secrets in the repository."
|
||||||
echo "Private keystore: $KEYSTORE"
|
echo "Private keystore: $KEYSTORE"
|
||||||
echo "Password file: $PASSWORD_FILE"
|
echo "Password file: $PASSWORD_FILE"
|
||||||
echo "Back up both files before the first Play Console upload."
|
echo "Back up both files before distributing an application signed with this identity."
|
||||||
|
|
|
||||||
10
scripts/init-android-staging-signing.sh
Executable file
10
scripts/init-android-staging-signing.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
|
||||||
|
WNH_ANDROID_SIGNING_DIR=${WNH_ANDROID_STAGING_SIGNING_DIR:-"$config_home/who_need_help/android-staging"} \
|
||||||
|
WNH_ANDROID_SIGNING_BASENAME=who-need-help-staging \
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=${WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS:-who-need-help-staging} \
|
||||||
|
WNH_ANDROID_SIGNING_SUBJECT="CN=Who Need Help staging key" \
|
||||||
|
exec "$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)/init-android-release-signing.sh"
|
||||||
|
|
@ -53,6 +53,8 @@ public_upstream_name=${PRODUCTION_PUBLIC_UPSTREAM_NAME:-who-need-help-production
|
||||||
codex_session_id=${PRODUCTION_CODEX_SESSION_ID:-}
|
codex_session_id=${PRODUCTION_CODEX_SESSION_ID:-}
|
||||||
google_oauth_client_id=${PRODUCTION_GOOGLE_OAUTH_CLIENT_ID:-}
|
google_oauth_client_id=${PRODUCTION_GOOGLE_OAUTH_CLIENT_ID:-}
|
||||||
google_oauth_client_secret=${PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET:-}
|
google_oauth_client_secret=${PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET:-}
|
||||||
|
android_app_links_package_name=${PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME:-}
|
||||||
|
android_app_links_fingerprints=${PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS:-}
|
||||||
test_domain=${PRODUCTION_TEST_DOMAIN:-"test.$domain"}
|
test_domain=${PRODUCTION_TEST_DOMAIN:-"test.$domain"}
|
||||||
test_upstream=${PRODUCTION_TEST_UPSTREAM:-who-need-help-test:4000}
|
test_upstream=${PRODUCTION_TEST_UPSTREAM:-who-need-help-test:4000}
|
||||||
edge_compose_project_name=${PRODUCTION_EDGE_COMPOSE_PROJECT_NAME:-who_need_help_edge}
|
edge_compose_project_name=${PRODUCTION_EDGE_COMPOSE_PROJECT_NAME:-who_need_help_edge}
|
||||||
|
|
@ -69,6 +71,12 @@ if { [ -n "$google_oauth_client_id" ] || [ -n "$google_oauth_client_secret" ]; }
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if { [ -n "$android_app_links_package_name" ] || [ -n "$android_app_links_fingerprints" ]; } &&
|
||||||
|
{ [ -z "$android_app_links_package_name" ] || [ -z "$android_app_links_fingerprints" ]; }; then
|
||||||
|
echo "Production Android App Links package and fingerprints must either both be set or both be empty." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
case "$compose_project_name" in
|
case "$compose_project_name" in
|
||||||
*[!a-zA-Z0-9_-]* | '')
|
*[!a-zA-Z0-9_-]* | '')
|
||||||
echo "PRODUCTION_COMPOSE_PROJECT_NAME must contain only letters, numbers, underscores, or hyphens." >&2
|
echo "PRODUCTION_COMPOSE_PROJECT_NAME must contain only letters, numbers, underscores, or hyphens." >&2
|
||||||
|
|
@ -179,6 +187,8 @@ CODEX_SESSION_ID_VALUE=$codex_session_id \
|
||||||
GIT_SHA_VALUE=$git_sha \
|
GIT_SHA_VALUE=$git_sha \
|
||||||
GOOGLE_OAUTH_CLIENT_ID_VALUE=$google_oauth_client_id \
|
GOOGLE_OAUTH_CLIENT_ID_VALUE=$google_oauth_client_id \
|
||||||
GOOGLE_OAUTH_CLIENT_SECRET_VALUE=$google_oauth_client_secret \
|
GOOGLE_OAUTH_CLIENT_SECRET_VALUE=$google_oauth_client_secret \
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME_VALUE=$android_app_links_package_name \
|
||||||
|
ANDROID_APP_LINKS_FINGERPRINTS_VALUE=$android_app_links_fingerprints \
|
||||||
EDGE_COMPOSE_PROJECT_NAME_VALUE=$edge_compose_project_name \
|
EDGE_COMPOSE_PROJECT_NAME_VALUE=$edge_compose_project_name \
|
||||||
PRIMARY_UPSTREAM_VALUE="$public_upstream_name:4000" \
|
PRIMARY_UPSTREAM_VALUE="$public_upstream_name:4000" \
|
||||||
TEST_DOMAIN_VALUE=$test_domain \
|
TEST_DOMAIN_VALUE=$test_domain \
|
||||||
|
|
@ -234,6 +244,8 @@ TEST_UPSTREAM_VALUE=$test_upstream \
|
||||||
replacement["SUPPORT_INBOX_ADDRESS"] = ENVIRON["SUPPORT_INBOX_ADDRESS_VALUE"]
|
replacement["SUPPORT_INBOX_ADDRESS"] = ENVIRON["SUPPORT_INBOX_ADDRESS_VALUE"]
|
||||||
replacement["GOOGLE_OAUTH_CLIENT_ID"] = ENVIRON["GOOGLE_OAUTH_CLIENT_ID_VALUE"]
|
replacement["GOOGLE_OAUTH_CLIENT_ID"] = ENVIRON["GOOGLE_OAUTH_CLIENT_ID_VALUE"]
|
||||||
replacement["GOOGLE_OAUTH_CLIENT_SECRET"] = ENVIRON["GOOGLE_OAUTH_CLIENT_SECRET_VALUE"]
|
replacement["GOOGLE_OAUTH_CLIENT_SECRET"] = ENVIRON["GOOGLE_OAUTH_CLIENT_SECRET_VALUE"]
|
||||||
|
replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"]
|
||||||
|
replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"]
|
||||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
@ -255,6 +267,7 @@ trap - EXIT HUP INT TERM
|
||||||
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
||||||
unset smtp_password
|
unset smtp_password
|
||||||
unset google_oauth_client_secret
|
unset google_oauth_client_secret
|
||||||
|
unset android_app_links_fingerprints
|
||||||
|
|
||||||
echo "Generated independent deployment secrets without printing them."
|
echo "Generated independent deployment secrets without printing them."
|
||||||
echo "Created the single mode-0600 production configuration: $target"
|
echo "Created the single mode-0600 production configuration: $target"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ mailpit_port=${TEST_MAILPIT_PORT:-8027}
|
||||||
codex_session_id=${TEST_CODEX_SESSION_ID:-}
|
codex_session_id=${TEST_CODEX_SESSION_ID:-}
|
||||||
google_oauth_client_id=${TEST_GOOGLE_OAUTH_CLIENT_ID:-}
|
google_oauth_client_id=${TEST_GOOGLE_OAUTH_CLIENT_ID:-}
|
||||||
google_oauth_client_secret=${TEST_GOOGLE_OAUTH_CLIENT_SECRET:-}
|
google_oauth_client_secret=${TEST_GOOGLE_OAUTH_CLIENT_SECRET:-}
|
||||||
|
android_app_links_package_name=${TEST_ANDROID_APP_LINKS_PACKAGE_NAME:-}
|
||||||
|
android_app_links_fingerprints=${TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS:-}
|
||||||
support_inbox_address=${TEST_SUPPORT_INBOX_ADDRESS:-}
|
support_inbox_address=${TEST_SUPPORT_INBOX_ADDRESS:-}
|
||||||
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
|
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
|
||||||
|
|
||||||
|
|
@ -79,6 +81,13 @@ if [[ -n "$google_oauth_client_id" || -n "$google_oauth_client_secret" ]]; then
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprints" ]]; then
|
||||||
|
[[ -n "$android_app_links_package_name" && -n "$android_app_links_fingerprints" ]] || {
|
||||||
|
echo "Test Android App Links package and fingerprints must either both be set or both be empty." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
for pair in "HTTP:$http_port" "MAILPIT:$mailpit_port"; do
|
for pair in "HTTP:$http_port" "MAILPIT:$mailpit_port"; do
|
||||||
value=${pair#*:}
|
value=${pair#*:}
|
||||||
if [[ ! "$value" =~ ^[0-9]+$ ]] || ((value < 1 || value > 65535)); then
|
if [[ ! "$value" =~ ^[0-9]+$ ]] || ((value < 1 || value > 65535)); then
|
||||||
|
|
@ -123,6 +132,8 @@ RELEASE_COOKIE_VALUE=$release_cookie \
|
||||||
METRICS_TOKEN_VALUE=$metrics_token \
|
METRICS_TOKEN_VALUE=$metrics_token \
|
||||||
GOOGLE_OAUTH_CLIENT_ID_VALUE=$google_oauth_client_id \
|
GOOGLE_OAUTH_CLIENT_ID_VALUE=$google_oauth_client_id \
|
||||||
GOOGLE_OAUTH_CLIENT_SECRET_VALUE=$google_oauth_client_secret \
|
GOOGLE_OAUTH_CLIENT_SECRET_VALUE=$google_oauth_client_secret \
|
||||||
|
ANDROID_APP_LINKS_PACKAGE_NAME_VALUE=$android_app_links_package_name \
|
||||||
|
ANDROID_APP_LINKS_FINGERPRINTS_VALUE=$android_app_links_fingerprints \
|
||||||
SUPPORT_INBOX_ADDRESS_VALUE=$support_inbox_address \
|
SUPPORT_INBOX_ADDRESS_VALUE=$support_inbox_address \
|
||||||
CODEX_SESSION_ID_VALUE=$codex_session_id \
|
CODEX_SESSION_ID_VALUE=$codex_session_id \
|
||||||
awk '
|
awk '
|
||||||
|
|
@ -174,6 +185,8 @@ CODEX_SESSION_ID_VALUE=$codex_session_id \
|
||||||
replacement["SUPPORT_INBOX_ADDRESS"] = ENVIRON["SUPPORT_INBOX_ADDRESS_VALUE"]
|
replacement["SUPPORT_INBOX_ADDRESS"] = ENVIRON["SUPPORT_INBOX_ADDRESS_VALUE"]
|
||||||
replacement["GOOGLE_OAUTH_CLIENT_ID"] = ENVIRON["GOOGLE_OAUTH_CLIENT_ID_VALUE"]
|
replacement["GOOGLE_OAUTH_CLIENT_ID"] = ENVIRON["GOOGLE_OAUTH_CLIENT_ID_VALUE"]
|
||||||
replacement["GOOGLE_OAUTH_CLIENT_SECRET"] = ENVIRON["GOOGLE_OAUTH_CLIENT_SECRET_VALUE"]
|
replacement["GOOGLE_OAUTH_CLIENT_SECRET"] = ENVIRON["GOOGLE_OAUTH_CLIENT_SECRET_VALUE"]
|
||||||
|
replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"]
|
||||||
|
replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"]
|
||||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
@ -189,6 +202,7 @@ trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
||||||
unset google_oauth_client_secret
|
unset google_oauth_client_secret
|
||||||
|
unset android_app_links_fingerprints
|
||||||
|
|
||||||
"$ROOT/scripts/compose.sh" "$target" config --quiet
|
"$ROOT/scripts/compose.sh" "$target" config --quiet
|
||||||
echo "Generated independent test secrets without printing them."
|
echo "Generated independent test secrets without printing them."
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,8 @@ fi
|
||||||
TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||||
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
||||||
TEST_GOOGLE_OAUTH_CLIENT_SECRET=quality-test-secret \
|
TEST_GOOGLE_OAUTH_CLIENT_SECRET=quality-test-secret \
|
||||||
|
TEST_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging \
|
||||||
|
TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF \
|
||||||
./scripts/init-test-env.sh test.help.test "$test_env" >/dev/null
|
./scripts/init-test-env.sh test.help.test "$test_env" >/dev/null
|
||||||
test "$(stat -c '%a' "$test_env")" = 600
|
test "$(stat -c '%a' "$test_env")" = 600
|
||||||
grep -Fx 'DEPLOYMENT_ENV=test' "$test_env" >/dev/null
|
grep -Fx 'DEPLOYMENT_ENV=test' "$test_env" >/dev/null
|
||||||
|
|
@ -142,6 +144,8 @@ grep -Fx 'EMAIL_DELIVERY_PROVIDER=smtp' "$test_env" >/dev/null
|
||||||
grep -Fx 'SMTP_RELAY=mailpit' "$test_env" >/dev/null
|
grep -Fx 'SMTP_RELAY=mailpit' "$test_env" >/dev/null
|
||||||
grep -Fx 'EMAIL_FROM_NAME="Who Need Help Test"' "$test_env" >/dev/null
|
grep -Fx 'EMAIL_FROM_NAME="Who Need Help Test"' "$test_env" >/dev/null
|
||||||
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-test-client' "$test_env" >/dev/null
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-test-client' "$test_env" >/dev/null
|
||||||
|
grep -Fx 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging' "$test_env" >/dev/null
|
||||||
|
grep -Fx 'ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF' "$test_env" >/dev/null
|
||||||
./scripts/validate-test-env.sh "$test_env" test.help.test >/dev/null
|
./scripts/validate-test-env.sh "$test_env" test.help.test >/dev/null
|
||||||
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||||
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
||||||
|
|
@ -182,6 +186,8 @@ PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
||||||
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||||
PRODUCTION_GOOGLE_OAUTH_CLIENT_ID=quality-production-client \
|
PRODUCTION_GOOGLE_OAUTH_CLIENT_ID=quality-production-client \
|
||||||
PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET=quality-production-secret \
|
PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET=quality-production-secret \
|
||||||
|
PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \
|
||||||
|
PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \
|
||||||
./scripts/init-production-env.sh help.test "$production_env" >/dev/null
|
./scripts/init-production-env.sh help.test "$production_env" >/dev/null
|
||||||
test "$(stat -c '%a' "$production_env")" = 600
|
test "$(stat -c '%a' "$production_env")" = 600
|
||||||
./scripts/validate-production-env.sh "$production_env" help.test >/dev/null
|
./scripts/validate-production-env.sh "$production_env" help.test >/dev/null
|
||||||
|
|
@ -190,6 +196,8 @@ grep -E '^APP_IMAGE=who-need-help:production-[0-9a-f]{12}$' "$production_env" >/
|
||||||
grep -Fx 'EDGE_COMPOSE_PROJECT_NAME=who_need_help_edge' "$production_env" >/dev/null
|
grep -Fx 'EDGE_COMPOSE_PROJECT_NAME=who_need_help_edge' "$production_env" >/dev/null
|
||||||
grep -Fx 'PRIMARY_DOMAIN=help.test' "$production_env" >/dev/null
|
grep -Fx 'PRIMARY_DOMAIN=help.test' "$production_env" >/dev/null
|
||||||
grep -Fx 'TEST_DOMAIN=test.help.test' "$production_env" >/dev/null
|
grep -Fx 'TEST_DOMAIN=test.help.test' "$production_env" >/dev/null
|
||||||
|
grep -Fx 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile' "$production_env" >/dev/null
|
||||||
|
grep -Fx 'ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB' "$production_env" >/dev/null
|
||||||
./scripts/validate-edge-env.sh "$production_env" >/dev/null
|
./scripts/validate-edge-env.sh "$production_env" >/dev/null
|
||||||
|
|
||||||
test_checkout="$scan_dir/test-checkout"
|
test_checkout="$scan_dir/test-checkout"
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,8 @@ email_from_address=$(require_value EMAIL_FROM_ADDRESS)
|
||||||
support_inbox_address=$(optional_value SUPPORT_INBOX_ADDRESS)
|
support_inbox_address=$(optional_value SUPPORT_INBOX_ADDRESS)
|
||||||
google_oauth_client_id=$(optional_value GOOGLE_OAUTH_CLIENT_ID)
|
google_oauth_client_id=$(optional_value GOOGLE_OAUTH_CLIENT_ID)
|
||||||
google_oauth_client_secret=$(optional_value GOOGLE_OAUTH_CLIENT_SECRET)
|
google_oauth_client_secret=$(optional_value GOOGLE_OAUTH_CLIENT_SECRET)
|
||||||
|
android_app_links_package_name=$(optional_value ANDROID_APP_LINKS_PACKAGE_NAME)
|
||||||
|
android_app_links_fingerprints=$(optional_value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS)
|
||||||
codex_session_id=$(require_value CODEX_SESSION_ID)
|
codex_session_id=$(require_value CODEX_SESSION_ID)
|
||||||
edge_compose_project_name=$(require_value EDGE_COMPOSE_PROJECT_NAME)
|
edge_compose_project_name=$(require_value EDGE_COMPOSE_PROJECT_NAME)
|
||||||
caddy_image=$(require_value CADDY_IMAGE)
|
caddy_image=$(require_value CADDY_IMAGE)
|
||||||
|
|
@ -316,6 +318,31 @@ if [[ -n "$google_oauth_client_id" || -n "$google_oauth_client_secret" ]]; then
|
||||||
reject_marker GOOGLE_OAUTH_CLIENT_SECRET "$google_oauth_client_secret"
|
reject_marker GOOGLE_OAUTH_CLIENT_SECRET "$google_oauth_client_secret"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprints" ]]; then
|
||||||
|
[[ -n "$android_app_links_package_name" && -n "$android_app_links_fingerprints" ]] || {
|
||||||
|
echo "Android App Links package and fingerprints must either both be set or both be empty." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$android_app_links_package_name" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$ ]] || {
|
||||||
|
echo "ANDROID_APP_LINKS_PACKAGE_NAME is not a valid Android application ID." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
IFS=',' read -r -a android_fingerprints <<<"$android_app_links_fingerprints"
|
||||||
|
[[ ${#android_fingerprints[@]} -gt 0 ]] || {
|
||||||
|
echo "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS is empty." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
for fingerprint in "${android_fingerprints[@]}"; do
|
||||||
|
compact_fingerprint=${fingerprint//:/}
|
||||||
|
compact_fingerprint=${compact_fingerprint//[[:space:]]/}
|
||||||
|
[[ "$compact_fingerprint" =~ ^[0-9A-Fa-f]{64}$ ]] || {
|
||||||
|
echo "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS contains an invalid SHA-256 fingerprint." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
case "$codex_session_id" in
|
case "$codex_session_id" in
|
||||||
not-configured | copy-the-main-local-codex-session-id)
|
not-configured | copy-the-main-local-codex-session-id)
|
||||||
echo "CODEX_SESSION_ID must identify the Build Week Codex session." >&2
|
echo "CODEX_SESSION_ID must identify the Build Week Codex session." >&2
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,29 @@ if [[ -n "$google_id" || -n "$google_secret" ]]; then
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
android_package=$(read_value ANDROID_APP_LINKS_PACKAGE_NAME 2>/dev/null || true)
|
||||||
|
android_fingerprints=$(read_value ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS 2>/dev/null || true)
|
||||||
|
if [[ -n "$android_package" || -n "$android_fingerprints" ]]; then
|
||||||
|
[[ -n "$android_package" && -n "$android_fingerprints" ]] || {
|
||||||
|
echo "Test Android App Links package and fingerprints must be configured together." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$android_package" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$ ]] || {
|
||||||
|
echo "ANDROID_APP_LINKS_PACKAGE_NAME is not a valid Android application ID." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
IFS=',' read -r -a android_fingerprint_values <<<"$android_fingerprints"
|
||||||
|
for fingerprint in "${android_fingerprint_values[@]}"; do
|
||||||
|
compact_fingerprint=${fingerprint//:/}
|
||||||
|
compact_fingerprint=${compact_fingerprint//[[:space:]]/}
|
||||||
|
[[ "$compact_fingerprint" =~ ^[0-9A-Fa-f]{64}$ ]] || {
|
||||||
|
echo "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS contains an invalid SHA-256 fingerprint." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
secrets=(
|
secrets=(
|
||||||
"$(require_value POSTGRES_PASSWORD)"
|
"$(require_value POSTGRES_PASSWORD)"
|
||||||
"$(require_value SECRET_KEY_BASE)"
|
"$(require_value SECRET_KEY_BASE)"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule WhoNeedHelpWeb.AndroidAppLinksControllerTest do
|
||||||
|
use WhoNeedHelpWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
setup do
|
||||||
|
previous = Application.get_env(:who_need_help, :android_app_links)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Application.put_env(:who_need_help, :android_app_links, previous)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 when no signed Android application is configured", %{conn: conn} do
|
||||||
|
Application.put_env(:who_need_help, :android_app_links, nil)
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/.well-known/assetlinks.json")
|
||||||
|
|
||||||
|
assert json_response(conn, 404) == %{"error" => "android_app_links_not_configured"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "publishes the configured Android package and signing fingerprints", %{conn: conn} do
|
||||||
|
fingerprint =
|
||||||
|
"D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:" <>
|
||||||
|
"29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF"
|
||||||
|
|
||||||
|
Application.put_env(:who_need_help, :android_app_links, %{
|
||||||
|
package_name: "org.whoneedhelp.mobile.staging",
|
||||||
|
sha256_cert_fingerprints: [fingerprint]
|
||||||
|
})
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/.well-known/assetlinks.json")
|
||||||
|
|
||||||
|
assert [statement] = json_response(conn, 200)
|
||||||
|
assert statement["relation"] == ["delegate_permission/common.handle_all_urls"]
|
||||||
|
|
||||||
|
assert statement["target"] == %{
|
||||||
|
"namespace" => "android_app",
|
||||||
|
"package_name" => "org.whoneedhelp.mobile.staging",
|
||||||
|
"sha256_cert_fingerprints" => [fingerprint]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert get_resp_header(conn, "cache-control") == ["public, max-age=300"]
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user