From 2b64f94dba9b1e51f7dc9d0733d2ea1ead50ee61 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Thu, 22 Oct 2026 17:43:59 -0700 Subject: [PATCH 1/3] feat: check the stored APNs Bundle ID and guide the no-key case The view-app read already returns apns_bundle_id, the Bundle ID that OneSignal sends to APNs as the topic. The script now prints it under platforms.apns.bundle_id, and the credentials and setup skills compare it with the project's Bundle ID when APNs is configured. A mismatch is the one stored-credential fault visible before a test send. When the presence check cannot run, the credentials skill now sends the Push Platforms URL and asks the user what the dashboard shows, instead of a portal walkthrough that may create a key the team already has. The .p8 flow says that a key is team-level, that Apple allows 2 per team, and which key scopes do not cover this app. --- references/api-reference.md | 2 +- scripts/onesignal_api.py | 17 ++++++++++++----- skills/credentials/SKILL.md | 10 +++++----- skills/setup/SKILL.md | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/references/api-reference.md b/references/api-reference.md index 2616eac..96039f7 100644 --- a/references/api-reference.md +++ b/references/api-reference.md @@ -39,7 +39,7 @@ The plugin needs one answer per platform: does the platform already have credent | Platform | Configured when | Notes | |---|---|---| -| APNs | `apns_env` is non-empty (`production` or `sandbox`) | Auth type: `apns_key_id` and `apns_team_id` both set → p8 key; both empty with `apns_env` set → p12 certificate; exactly one set → unknown (report `null`). Both upload paths always set `apns_env`; only the p8 path sets the key and team IDs. | +| APNs | `apns_env` is non-empty (`production` or `sandbox`) | Auth type: `apns_key_id` and `apns_team_id` both set → p8 key; both empty with `apns_env` set → p12 certificate; exactly one set → unknown (report `null`). Both upload paths always set `apns_env`; only the p8 path sets the key and team IDs. `apns_bundle_id` (non-secret; the script prints it as `platforms.apns.bundle_id`) is the Bundle ID the stored credential serves, and the server sends it to APNs as the topic of every push. APNs rejects a push whose topic differs from the app's Bundle ID, so a stored value that differs from the project's Bundle ID means no delivery to this app. Presence stays `true`; the fix is dashboard-only. | | FCM | `fcm_sender_id` is non-empty | Match the exact name `fcm_sender_id`. Do not match `gcm_sender_id` or `chrome_web_gcm_sender_id`: a web-only app has `chrome_web_gcm_sender_id` set while `fcm_sender_id` is `null`. No non-secret field separates FCM v1 from the legacy key; do not try to tell them apart. | | Web | `chrome_web_origin` is non-empty | The unauthenticated "Web platform config probe" below is the preferred web read; this field is the authenticated equivalent. | diff --git a/scripts/onesignal_api.py b/scripts/onesignal_api.py index 764c6fb..38b9714 100755 --- a/scripts/onesignal_api.py +++ b/scripts/onesignal_api.py @@ -9,8 +9,9 @@ 404 = feature flag off (fall back to dashboard), 401 = wrong app key - delivery stat `failed` counts UNSUBSCRIBED targets, not errors - the view-app read decides credential presence from the non-secret fields - (apns_env, apns_key_id, apns_team_id, fcm_sender_id, chrome_web_origin), - never from the plaintext credential fields the server plans to remove + (apns_env, apns_key_id, apns_team_id, apns_bundle_id, fcm_sender_id, + chrome_web_origin), never from the plaintext credential fields the server + plans to remove - the write-once credential upload echoes the stored credential fields back in its 2xx body; `provision` drops that body and prints a verdict, so the secret never reaches the agent transcript @@ -220,8 +221,9 @@ def cmd_app(args): "platforms": platforms, "cross_check": cross_check, "note": "Presence only, from non-secret fields (apns_env, apns_key_id, apns_team_id, " - "fcm_sender_id, chrome_web_origin). Not a validity check: the test send in " - "the verify skill proves the credential works."} + "apns_bundle_id, fcm_sender_id, chrome_web_origin). Not a validity check: the " + "test send in the verify skill proves the credential works. On iOS, compare " + "platforms.apns.bundle_id with the project's Bundle ID."} print(json.dumps(out, indent=2)) @@ -253,8 +255,13 @@ def _presence(data): "apns_listed": apns_listed, "fcm_listed": gcm_listed, "mismatch": (apns_listed != apns_configured) or (gcm_listed != fcm_configured)} + # The stored Bundle ID is the APNs topic the server sends with each push. + # It is not a secret, and a push to a device whose app has a different + # Bundle ID is rejected by APNs, so the caller compares it with the project. + bundle_id = data.get("apns_bundle_id") platforms = { - "apns": {"configured": apns_configured, "auth_type": apns_auth}, + "apns": {"configured": apns_configured, "auth_type": apns_auth, + "bundle_id": bundle_id if _present(bundle_id) else None}, "fcm": {"configured": fcm_configured}, "web": {"configured": web_configured}, } diff --git a/skills/credentials/SKILL.md b/skills/credentials/SKILL.md index ec6c1dd..c2718df 100644 --- a/skills/credentials/SKILL.md +++ b/skills/credentials/SKILL.md @@ -100,13 +100,13 @@ Many existing apps already have credentials for the target platform. Check for t The probe reads and their response semantics come from [../../references/api-reference.md](../../references/api-reference.md) (the view-app read and the "Web platform config probe"), and `python3 /scripts/onesignal_api.py ` encodes them. Use those; do not hand-roll the calls. -1. **Push platforms (iOS / Android):** run `onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{app_id}` — with an app-scoped key (the script takes `--key` or reads `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`). No MCP tool returns the per-app platform config (api-reference.md), so this read has no MCP path. The script prints a per-platform presence verdict under `platforms` — `apns.configured`, `fcm.configured`, `web.configured` — and that verdict decides. It reads only the **non-secret presence fields** the api-reference names (`apns_env` for APNs, `fcm_sender_id` for FCM, `chrome_web_origin` for web), and treats `null`, `""`, and an absent key the same. Never decide from the plaintext credential fields (`apns_p8`, `apns_certificates`, `fcm_v1_service_account_json`, `gcm_key`): the server plans to remove them from the response, and their absence does not mean the platform is unconfigured. If you must read the raw `GET` without the script, apply the presence fields **only** to a 2xx response whose body is the app's JSON object (it carries this App ID under `"id"`), and inspect those fields and nothing else; any other response — a non-2xx, an empty or non-JSON body, a curl error — is item 7, not "not configured". `cross_check.mismatch: true` (from `channels.push.platforms`) is not a different verdict: it usually means the dashboard platform toggle and the stored credentials disagree, so tell the user in one sentence to check Settings > Push Platforms, and keep the verdict (api-reference.md "View an app"). A `status: error` from the script is "presence unknown", not "not configured" — re-run it once, and if it still errors, go to item 7. +1. **Push platforms (iOS / Android):** run `onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{app_id}` — with an app-scoped key (the script takes `--key` or reads `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`). No MCP tool returns the per-app platform config (api-reference.md), so this read has no MCP path. The script prints a per-platform presence verdict under `platforms` — `apns.configured`, `fcm.configured`, `web.configured` — and that verdict decides. For APNs it also prints `apns.auth_type` (`p8` or `p12`) and `apns.bundle_id`, the Bundle ID the stored credential serves; item 4 uses both. It reads only the **non-secret presence fields** the api-reference names (`apns_env`, `apns_key_id`, `apns_team_id`, and `apns_bundle_id` for APNs, `fcm_sender_id` for FCM, `chrome_web_origin` for web), and treats `null`, `""`, and an absent key the same. Never decide from the plaintext credential fields (`apns_p8`, `apns_certificates`, `fcm_v1_service_account_json`, `gcm_key`): the server plans to remove them from the response, and their absence does not mean the platform is unconfigured. If you must read the raw `GET` without the script, apply the presence fields **only** to a 2xx response whose body is the app's JSON object (it carries this App ID under `"id"`), and inspect those fields and nothing else; any other response — a non-2xx, an empty or non-JSON body, a curl error — is item 7, not "not configured". `cross_check.mismatch: true` (from `channels.push.platforms`) is not a different verdict: it usually means the dashboard platform toggle and the stored credentials disagree, so tell the user in one sentence to check Settings > Push Platforms, and keep the verdict (api-reference.md "View an app"). A `status: error` from the script is "presence unknown", not "not configured" — re-run it once, and if it still errors, go to item 7. 2. **Web:** run `onesignal_api.py web-probe ` — no key needed. The script wraps the unauthenticated sync probe and always appends the throwaway `?fresh=` param that bypasses the ~1 h CDN cache (api-reference.md). A `status: provisioned` line means the web platform is provisioned (the script wraps the raw `success: true` as that status). 3. **Email / SMS:** no credential-presence endpoint exists for these channels here — go straight to [guided-channels.md](guided-channels.md). -4. **Platform configured →** tell the user which platform and App ID already have credentials, and move on: continue into the `verify` skill (or `setup` if the SDK is not installed yet). Do not upload anything, and do not re-validate the stored credentials. If the user believes the stored credentials are wrong, replacement is dashboard-only (Settings > Push Platforms) or org-key — never this skill's endpoint. +4. **Platform configured →** tell the user which platform and App ID already have credentials, and move on: continue into the `verify` skill (or `setup` if the SDK is not installed yet). Do not upload anything, and do not re-validate the stored credentials. If the user believes the stored credentials are wrong, replacement is dashboard-only (Settings > Push Platforms, `https://dashboard.onesignal.com/apps//settings/push/platforms`) or org-key — never this skill's endpoint. **iOS adds one comparison.** Read the project's Bundle ID the way the [.p8 flow](#apple-apns-p8-flow) does (the Xcode project first) and compare it with `apns.bundle_id`. A match needs no comment. A difference is the one stored-credential fault you can detect without a test send: APNs rejects every push whose topic is not the app's Bundle ID (api-reference.md "View an app"), so push will not deliver to this app. Say so in one sentence — name both values and the auth type (`p8` key or `p12` certificate) — and send the dashboard URL above; the fix is dashboard-only. Keep the verdict and continue. Stay silent when `apns.bundle_id` is `null` or the project's Bundle ID is unknown. 5. **Platform not configured →** record that fact, then continue into the matching flow. This record matters later: the 409 disambiguation in the [validation loop](#credential-validation-loop) depends on the platform's state before your first upload attempt, and this check is that state. 6. **Wrong or unknown App ID** (`no_such_app` from the web probe, `not_found` from the app read) **→ STOP.** Do not start a portal walkthrough and do not route toward an upload — the endpoint is write-once, and a credential aimed at a mistyped App ID lands on the wrong app. Re-ask the user for the App ID (it is public — grep the init code for it) and re-run this step. A probe `status: unknown` is not a decision either: re-run the probe or fall back to the raw `GET` before you continue. -7. **Check cannot run** (no key available, the `GET` itself fails, or the script returns `status: error`) **→** say so and continue into the flow anyway, but record the Step 1 state as **unknown** — never as "not configured". The write-once endpoint still guards the case: an upload against an already-configured platform returns a 409, and the validation loop maps it. With an unknown before-state, that 409 is indeterminate (case (b) in the [validation loop](#credential-validation-loop)), so a recovery re-call cannot claim success from it. +7. **Check cannot run** (no key available, the `GET` itself fails, or the script returns `status: error`) **→** say so, in one sentence that names the cause, and ask the user to read the dashboard for you before any portal walkthrough. A session with only the OneSignal MCP connected is the common case: the MCP signs in with OAuth and exposes no view-app read, so this check needs an app-scoped key the session does not have. Send `https://dashboard.onesignal.com/apps//settings/push/platforms` and ask one structured question (safety contract §14): "Does the page show Apple iOS (APNs) as configured?" (or Google Android (FCM) for Android), with "Yes, a key is already uploaded", "No, nothing is configured", and "I am not sure" as the choices. Record "Yes" as item 4 and skip the portal walkthrough (on iOS, the dashboard shows the auth type and Bundle ID; ask the user to compare the Bundle ID with the project's if they can), "No" as item 5, and "I am not sure" as **unknown** — never as "not configured". On unknown, continue into the flow anyway. The write-once endpoint still guards the case: an upload against an already-configured platform returns a 409, and the validation loop maps it. With an unknown before-state, that 409 is indeterminate (case (b) in the [validation loop](#credential-validation-loop)), so a recovery re-call cannot claim success from it. **Checkpoint** (telemetry contract rules apply, consent included): report `credentials.detected` the moment this step resolves — it is the presence verdict for the target platform. Email and SMS have no presence check here and send no row. `fail credentials_missing` is the normal entry into the flows below, not a stop. On the wrong-App-ID stop, fire the row **before you end the turn to re-ask** (a session that never resumes otherwise leaves no trace); when the step re-runs with a good App ID, the new row records the recovery. Run exactly one of: @@ -114,7 +114,7 @@ The probe reads and their response semantics come from [../../references/api-ref bash /scripts/checkpoint.sh credentials.detected ok # platform already configured bash /scripts/checkpoint.sh credentials.detected fail credentials_missing # not configured — continue into the flow bash /scripts/checkpoint.sh credentials.detected fail invalid_app_id # wrong or unknown App ID — stop and re-ask -bash /scripts/checkpoint.sh credentials.detected fail unknown probe_unavailable # the check cannot run (item 7) +bash /scripts/checkpoint.sh credentials.detected fail unknown probe_unavailable # the check cannot run and the user is not sure (item 7) ``` On a direct run, append `--app-id ` to the line you pick ("Checkpoint consent" above): the script records the App ID and flushes the events held before it. @@ -171,7 +171,7 @@ Read the "Credential provisioning" section of [../../references/api-reference.md Full portal detail (screenshots-equivalent steps, .p8-vs-.p12 disambiguation, troubleshooting) is in [api-uploaded-credentials.md](api-uploaded-credentials.md#apple-apns-p8). Summary of the hand-off: -1. **Human, in the Apple Developer portal:** Certificates, Identifiers & Profiles → **Keys** → blue **+** → select **Apple Push Notifications service (APNs)** with **Sandbox & Production** → name, Continue, Register → **Download the `.p8`** (one-time download — it cannot be re-downloaded). Requires the **paid** Apple Developer account. +1. **Human, in the Apple Developer portal:** Certificates, Identifiers & Profiles → **Keys** → blue **+** → select **Apple Push Notifications service (APNs)** with **Sandbox & Production** → name, Continue, Register → **Download the `.p8`** (one-time download — it cannot be re-downloaded). Requires the **paid** Apple Developer account. **Ask before they create a key:** a `.p8` key belongs to the Apple team, not to one app, so one key serves every app and Bundle ID under the team, and Apple allows at most 2 APNs keys per team. If the team already has an APNs key — the Keys section lists it — reuse that key's `.p8` file and Key ID instead of creating another; only the Bundle ID in step 2 is specific to this app. Two exceptions, both visible on the key's detail page: a key that Apple scoped to one environment must cover **Sandbox & Production**, and a **Topic Specific** key must list this app's Bundle ID. A `.p12` certificate is the other credential type; this endpoint does not accept it (api-uploaded-credentials.md). 2. **Human captures four values** and gives you the **file path** to the downloaded `.p8` plus the three below. Ask for the path, the Key ID, and the Team ID with the structured-question tool, one question per value (safety contract §14): the user types the value in the free-text field, and every listed option is a fallback — "Help me find it" (repeat the portal location) and "Pause — I'll come back". Do not ask for these values as a plain paste-into-chat message. The Bundle ID rarely needs an ask — read it from the Xcode project first and confirm it. If that read fails (no `.xcodeproj` yet — e.g. Expo before prebuild), returns more than one candidate, or the user rejects the value, ask for the Bundle ID the same way as the Key ID. Never guess it: all four params are required, and the endpoint is write-once. - **Key ID** — 10-char string next to the key name in the Keys section. - **Team ID** — 10-char string by the team name, top-right of the Apple Developer account. **Not the same as Key ID** — the most common misconfiguration is swapping them. If both are 10 chars and you're unsure, ask the user to re-confirm which came from where. diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md index 4bc5b92..08a6a6c 100644 --- a/skills/setup/SKILL.md +++ b/skills/setup/SKILL.md @@ -246,7 +246,7 @@ Both buffer. Nothing sends by itself: they go out when you record `setup.app_id The single worst onboarding failure is installing the SDK before push credentials exist: the app builds, the device registers, and it shows up **unsubscribed** because OneSignal has nothing to hand APNs/FCM. Close credentials FIRST. Never skip this step silently. -1. **Check what's configured.** With an app-scoped key available (`$ONESIGNAL_SETUP_TOKEN` / `$ONESIGNAL_REST_API_KEY`), run `python3 /scripts/onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{APP_ID}` (app auth works) — and read the verdict for the platform you're about to install: Android → `platforms.fcm.configured`, iOS → `platforms.apns.configured`, Web → `platforms.web.configured` (or the free `web-probe` below). The script decides from the **non-secret presence fields** only (`apns_env`, `fcm_sender_id`, `chrome_web_origin`; api-reference "View an app"), and treats `null`, `""`, and an absent key the same. Without Python (Step 0.2), `curl` the same `GET` with whichever key env var is set, and keep only the HTTP status, the app `id`, and the presence fields — the raw body still carries the plaintext credentials until the server removes them, so never print or paste the whole body: `curl -sS -w '\nHTTP %{http_code}\n' -H "Authorization: Key ${ONESIGNAL_REST_API_KEY:-$ONESIGNAL_SETUP_TOKEN}" "https://api.onesignal.com/api/v1/apps/" | grep -oE '"(id|apns_env|apns_key_id|apns_team_id|fcm_sender_id|chrome_web_origin)":[^,}]*|^HTTP [0-9]+'`. A verdict exists **only** when the `HTTP` line is 2xx **and** the `"id"` line shows this App ID — that pair is your proof the body is the app object. Then read the presence fields and nothing else — never the plaintext credential fields (`apns_p8`, `fcm_v1_service_account_json`, `gcm_key`). Any other output (no `HTTP` line, a non-2xx status, no `"id"` line, a curl error) is **presence unknown, not "missing"**: do not start the credentials skill on it — take the no-key path below instead. In a sandboxed runtime, a script exit code of 3 or a curl error on the first try means the sandbox blocked the request: re-run that one command with escalation first ("Network access" above), and read presence unknown only if the re-run also fails or the user declines. No key available, or presence unknown → ask the user to check the dashboard and tell you. Send the page as a clickable URL built from the App ID: `https://dashboard.onesignal.com/apps//settings/push/platforms` (Settings > Push Platforms). +1. **Check what's configured.** With an app-scoped key available (`$ONESIGNAL_SETUP_TOKEN` / `$ONESIGNAL_REST_API_KEY`), run `python3 /scripts/onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{APP_ID}` (app auth works) — and read the verdict for the platform you're about to install: Android → `platforms.fcm.configured`, iOS → `platforms.apns.configured`, Web → `platforms.web.configured` (or the free `web-probe` below). The script decides from the **non-secret presence fields** only (`apns_env`, `fcm_sender_id`, `chrome_web_origin`; api-reference "View an app"), and treats `null`, `""`, and an absent key the same. Without Python (Step 0.2), `curl` the same `GET` with whichever key env var is set, and keep only the HTTP status, the app `id`, and the presence fields — the raw body still carries the plaintext credentials until the server removes them, so never print or paste the whole body: `curl -sS -w '\nHTTP %{http_code}\n' -H "Authorization: Key ${ONESIGNAL_REST_API_KEY:-$ONESIGNAL_SETUP_TOKEN}" "https://api.onesignal.com/api/v1/apps/" | grep -oE '"(id|apns_env|apns_key_id|apns_team_id|apns_bundle_id|fcm_sender_id|chrome_web_origin)":[^,}]*|^HTTP [0-9]+'`. A verdict exists **only** when the `HTTP` line is 2xx **and** the `"id"` line shows this App ID — that pair is your proof the body is the app object. Then read the presence fields and nothing else — never the plaintext credential fields (`apns_p8`, `fcm_v1_service_account_json`, `gcm_key`). **iOS, when APNs is configured:** compare `platforms.apns.bundle_id` (the `apns_bundle_id` field) with the project's Bundle ID. A difference means APNs will reject every push to this app; say so in one sentence with both values and the dashboard URL below, and keep going (credentials skill Step 1, item 4, has the detail). Any other output (no `HTTP` line, a non-2xx status, no `"id"` line, a curl error) is **presence unknown, not "missing"**: do not start the credentials skill on it — take the no-key path below instead. In a sandboxed runtime, a script exit code of 3 or a curl error on the first try means the sandbox blocked the request: re-run that one command with escalation first ("Network access" above), and read presence unknown only if the re-run also fails or the user declines. No key available, or presence unknown → ask the user to check the dashboard and tell you. Send the page as a clickable URL built from the App ID: `https://dashboard.onesignal.com/apps//settings/push/platforms` (Settings > Push Platforms). 2. **Missing → run the credentials skill NOW**, before writing any code. It walks the human through the Apple/Firebase console steps and uploads the file itself via the write-once endpoint (`POST /api/v1/apps/{APP_ID}/credentials`). Do not proceed until it reports success or the user explicitly defers. 3. **Confirm the config is LIVE before any device ever runs** — use the API script, which encodes the cache-bust and poll-ordering quirks (no MCP tool covers these config reads — see api-reference "OneSignal MCP server"). In a sandboxed runtime the probe needs escalation; the sandbox block shows as exit code 3 on the first try, so re-run that one command with escalation ("Network access" above): - **Android:** `python3 /scripts/onesignal_api.py android-params ` — polls until `android_sender_id` appears (`status: fcm_live`). ⚠️ Run only AFTER the credential upload; fetching before credentials exist primes a CDN cache with the empty response on a fresh app. From 58adae7c48211c8ce2cd2de47649a34a33664a75 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 28 Oct 2026 18:30:23 -0700 Subject: [PATCH 2/3] fix: read the main app target's Bundle ID and guard the setup comparison The credentials and setup skills now compare the stored APNs Bundle ID with the main app target only, never an extension target. The setup copy stays silent on a null or unknown value, and its dashboard fallback uses the credentials question. The APNs key text now gives the Team Scoped limit per environment and the Topic Specific case. --- skills/credentials/SKILL.md | 6 +++--- skills/setup/SKILL.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skills/credentials/SKILL.md b/skills/credentials/SKILL.md index c2718df..e324440 100644 --- a/skills/credentials/SKILL.md +++ b/skills/credentials/SKILL.md @@ -103,7 +103,7 @@ The probe reads and their response semantics come from [../../references/api-ref 1. **Push platforms (iOS / Android):** run `onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{app_id}` — with an app-scoped key (the script takes `--key` or reads `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`). No MCP tool returns the per-app platform config (api-reference.md), so this read has no MCP path. The script prints a per-platform presence verdict under `platforms` — `apns.configured`, `fcm.configured`, `web.configured` — and that verdict decides. For APNs it also prints `apns.auth_type` (`p8` or `p12`) and `apns.bundle_id`, the Bundle ID the stored credential serves; item 4 uses both. It reads only the **non-secret presence fields** the api-reference names (`apns_env`, `apns_key_id`, `apns_team_id`, and `apns_bundle_id` for APNs, `fcm_sender_id` for FCM, `chrome_web_origin` for web), and treats `null`, `""`, and an absent key the same. Never decide from the plaintext credential fields (`apns_p8`, `apns_certificates`, `fcm_v1_service_account_json`, `gcm_key`): the server plans to remove them from the response, and their absence does not mean the platform is unconfigured. If you must read the raw `GET` without the script, apply the presence fields **only** to a 2xx response whose body is the app's JSON object (it carries this App ID under `"id"`), and inspect those fields and nothing else; any other response — a non-2xx, an empty or non-JSON body, a curl error — is item 7, not "not configured". `cross_check.mismatch: true` (from `channels.push.platforms`) is not a different verdict: it usually means the dashboard platform toggle and the stored credentials disagree, so tell the user in one sentence to check Settings > Push Platforms, and keep the verdict (api-reference.md "View an app"). A `status: error` from the script is "presence unknown", not "not configured" — re-run it once, and if it still errors, go to item 7. 2. **Web:** run `onesignal_api.py web-probe ` — no key needed. The script wraps the unauthenticated sync probe and always appends the throwaway `?fresh=` param that bypasses the ~1 h CDN cache (api-reference.md). A `status: provisioned` line means the web platform is provisioned (the script wraps the raw `success: true` as that status). 3. **Email / SMS:** no credential-presence endpoint exists for these channels here — go straight to [guided-channels.md](guided-channels.md). -4. **Platform configured →** tell the user which platform and App ID already have credentials, and move on: continue into the `verify` skill (or `setup` if the SDK is not installed yet). Do not upload anything, and do not re-validate the stored credentials. If the user believes the stored credentials are wrong, replacement is dashboard-only (Settings > Push Platforms, `https://dashboard.onesignal.com/apps//settings/push/platforms`) or org-key — never this skill's endpoint. **iOS adds one comparison.** Read the project's Bundle ID the way the [.p8 flow](#apple-apns-p8-flow) does (the Xcode project first) and compare it with `apns.bundle_id`. A match needs no comment. A difference is the one stored-credential fault you can detect without a test send: APNs rejects every push whose topic is not the app's Bundle ID (api-reference.md "View an app"), so push will not deliver to this app. Say so in one sentence — name both values and the auth type (`p8` key or `p12` certificate) — and send the dashboard URL above; the fix is dashboard-only. Keep the verdict and continue. Stay silent when `apns.bundle_id` is `null` or the project's Bundle ID is unknown. +4. **Platform configured →** tell the user which platform and App ID already have credentials, and move on: continue into the `verify` skill (or `setup` if the SDK is not installed yet). Do not upload anything, and do not re-validate the stored credentials. If the user believes the stored credentials are wrong, replacement is dashboard-only (Settings > Push Platforms, `https://dashboard.onesignal.com/apps//settings/push/platforms`) or org-key — never this skill's endpoint. **iOS adds one comparison.** Read the Bundle ID of the main app target the way the [.p8 flow](#apple-apns-p8-flow) does (the Xcode project first, never an extension target) and compare it with `apns.bundle_id`. A match needs no comment. A difference is the one stored-credential fault you can detect without a test send: APNs rejects every push whose topic is not the app's Bundle ID (api-reference.md "View an app"), so push will not deliver to this app. Say so in one sentence — name both values and the auth type (`p8` key or `p12` certificate; when `apns.auth_type` is `null`, the stored fields do not show the type, so do not name one) — and send the dashboard URL above; the fix is dashboard-only. Keep the verdict and continue. Stay silent when `apns.bundle_id` is `null` or the project's Bundle ID is unknown. 5. **Platform not configured →** record that fact, then continue into the matching flow. This record matters later: the 409 disambiguation in the [validation loop](#credential-validation-loop) depends on the platform's state before your first upload attempt, and this check is that state. 6. **Wrong or unknown App ID** (`no_such_app` from the web probe, `not_found` from the app read) **→ STOP.** Do not start a portal walkthrough and do not route toward an upload — the endpoint is write-once, and a credential aimed at a mistyped App ID lands on the wrong app. Re-ask the user for the App ID (it is public — grep the init code for it) and re-run this step. A probe `status: unknown` is not a decision either: re-run the probe or fall back to the raw `GET` before you continue. 7. **Check cannot run** (no key available, the `GET` itself fails, or the script returns `status: error`) **→** say so, in one sentence that names the cause, and ask the user to read the dashboard for you before any portal walkthrough. A session with only the OneSignal MCP connected is the common case: the MCP signs in with OAuth and exposes no view-app read, so this check needs an app-scoped key the session does not have. Send `https://dashboard.onesignal.com/apps//settings/push/platforms` and ask one structured question (safety contract §14): "Does the page show Apple iOS (APNs) as configured?" (or Google Android (FCM) for Android), with "Yes, a key is already uploaded", "No, nothing is configured", and "I am not sure" as the choices. Record "Yes" as item 4 and skip the portal walkthrough (on iOS, the dashboard shows the auth type and Bundle ID; ask the user to compare the Bundle ID with the project's if they can), "No" as item 5, and "I am not sure" as **unknown** — never as "not configured". On unknown, continue into the flow anyway. The write-once endpoint still guards the case: an upload against an already-configured platform returns a 409, and the validation loop maps it. With an unknown before-state, that 409 is indeterminate (case (b) in the [validation loop](#credential-validation-loop)), so a recovery re-call cannot claim success from it. @@ -171,8 +171,8 @@ Read the "Credential provisioning" section of [../../references/api-reference.md Full portal detail (screenshots-equivalent steps, .p8-vs-.p12 disambiguation, troubleshooting) is in [api-uploaded-credentials.md](api-uploaded-credentials.md#apple-apns-p8). Summary of the hand-off: -1. **Human, in the Apple Developer portal:** Certificates, Identifiers & Profiles → **Keys** → blue **+** → select **Apple Push Notifications service (APNs)** with **Sandbox & Production** → name, Continue, Register → **Download the `.p8`** (one-time download — it cannot be re-downloaded). Requires the **paid** Apple Developer account. **Ask before they create a key:** a `.p8` key belongs to the Apple team, not to one app, so one key serves every app and Bundle ID under the team, and Apple allows at most 2 APNs keys per team. If the team already has an APNs key — the Keys section lists it — reuse that key's `.p8` file and Key ID instead of creating another; only the Bundle ID in step 2 is specific to this app. Two exceptions, both visible on the key's detail page: a key that Apple scoped to one environment must cover **Sandbox & Production**, and a **Topic Specific** key must list this app's Bundle ID. A `.p12` certificate is the other credential type; this endpoint does not accept it (api-uploaded-credentials.md). -2. **Human captures four values** and gives you the **file path** to the downloaded `.p8` plus the three below. Ask for the path, the Key ID, and the Team ID with the structured-question tool, one question per value (safety contract §14): the user types the value in the free-text field, and every listed option is a fallback — "Help me find it" (repeat the portal location) and "Pause — I'll come back". Do not ask for these values as a plain paste-into-chat message. The Bundle ID rarely needs an ask — read it from the Xcode project first and confirm it. If that read fails (no `.xcodeproj` yet — e.g. Expo before prebuild), returns more than one candidate, or the user rejects the value, ask for the Bundle ID the same way as the Key ID. Never guess it: all four params are required, and the endpoint is write-once. +1. **Human, in the Apple Developer portal:** Certificates, Identifiers & Profiles → **Keys** → blue **+** → select **Apple Push Notifications service (APNs)** with **Sandbox & Production** → name, Continue, Register → **Download the `.p8`** (one-time download — it cannot be re-downloaded). Requires the **paid** Apple Developer account. **Ask before they create a key:** a `.p8` key belongs to the Apple team, not to one app. A **Team Scoped** key serves every app and Bundle ID under the team, and Apple allows at most 2 Team Scoped keys per environment (Sandbox, Production). A **Topic Specific** key serves only the Bundle IDs it lists, and Apple allows many more of them. If the team already has an APNs key — the Keys section lists it — reuse that key's `.p8` file and Key ID instead of creating another; only the Bundle ID in step 2 is specific to this app. Two exceptions, both visible on the key's detail page: a key that Apple scoped to one environment must cover **Sandbox & Production**, and a **Topic Specific** key must list this app's Bundle ID. A `.p12` certificate is the other credential type; this endpoint does not accept it (api-uploaded-credentials.md). +2. **Human captures four values** and gives you the **file path** to the downloaded `.p8` plus the three below. Ask for the path, the Key ID, and the Team ID with the structured-question tool, one question per value (safety contract §14): the user types the value in the free-text field, and every listed option is a fallback — "Help me find it" (repeat the portal location) and "Pause — I'll come back". Do not ask for these values as a plain paste-into-chat message. The Bundle ID rarely needs an ask — read it from the Xcode project first and confirm it. Read it from the main app target (`productType = "com.apple.product-type.application"`), never from an extension target such as the Notification Service Extension, which has its own Bundle ID. If that read fails (no `.xcodeproj` yet — e.g. Expo before prebuild), returns more than one candidate, or the user rejects the value, ask for the Bundle ID the same way as the Key ID. Never guess it: all four params are required, and the endpoint is write-once. - **Key ID** — 10-char string next to the key name in the Keys section. - **Team ID** — 10-char string by the team name, top-right of the Apple Developer account. **Not the same as Key ID** — the most common misconfiguration is swapping them. If both are 10 chars and you're unsure, ask the user to re-confirm which came from where. - **App Bundle ID** — reverse-domain string (e.g. `com.example.app`) from the Identifiers section or Xcode → Signing & Capabilities. diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md index 08a6a6c..44601af 100644 --- a/skills/setup/SKILL.md +++ b/skills/setup/SKILL.md @@ -246,7 +246,7 @@ Both buffer. Nothing sends by itself: they go out when you record `setup.app_id The single worst onboarding failure is installing the SDK before push credentials exist: the app builds, the device registers, and it shows up **unsubscribed** because OneSignal has nothing to hand APNs/FCM. Close credentials FIRST. Never skip this step silently. -1. **Check what's configured.** With an app-scoped key available (`$ONESIGNAL_SETUP_TOKEN` / `$ONESIGNAL_REST_API_KEY`), run `python3 /scripts/onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{APP_ID}` (app auth works) — and read the verdict for the platform you're about to install: Android → `platforms.fcm.configured`, iOS → `platforms.apns.configured`, Web → `platforms.web.configured` (or the free `web-probe` below). The script decides from the **non-secret presence fields** only (`apns_env`, `fcm_sender_id`, `chrome_web_origin`; api-reference "View an app"), and treats `null`, `""`, and an absent key the same. Without Python (Step 0.2), `curl` the same `GET` with whichever key env var is set, and keep only the HTTP status, the app `id`, and the presence fields — the raw body still carries the plaintext credentials until the server removes them, so never print or paste the whole body: `curl -sS -w '\nHTTP %{http_code}\n' -H "Authorization: Key ${ONESIGNAL_REST_API_KEY:-$ONESIGNAL_SETUP_TOKEN}" "https://api.onesignal.com/api/v1/apps/" | grep -oE '"(id|apns_env|apns_key_id|apns_team_id|apns_bundle_id|fcm_sender_id|chrome_web_origin)":[^,}]*|^HTTP [0-9]+'`. A verdict exists **only** when the `HTTP` line is 2xx **and** the `"id"` line shows this App ID — that pair is your proof the body is the app object. Then read the presence fields and nothing else — never the plaintext credential fields (`apns_p8`, `fcm_v1_service_account_json`, `gcm_key`). **iOS, when APNs is configured:** compare `platforms.apns.bundle_id` (the `apns_bundle_id` field) with the project's Bundle ID. A difference means APNs will reject every push to this app; say so in one sentence with both values and the dashboard URL below, and keep going (credentials skill Step 1, item 4, has the detail). Any other output (no `HTTP` line, a non-2xx status, no `"id"` line, a curl error) is **presence unknown, not "missing"**: do not start the credentials skill on it — take the no-key path below instead. In a sandboxed runtime, a script exit code of 3 or a curl error on the first try means the sandbox blocked the request: re-run that one command with escalation first ("Network access" above), and read presence unknown only if the re-run also fails or the user declines. No key available, or presence unknown → ask the user to check the dashboard and tell you. Send the page as a clickable URL built from the App ID: `https://dashboard.onesignal.com/apps//settings/push/platforms` (Settings > Push Platforms). +1. **Check what's configured.** With an app-scoped key available (`$ONESIGNAL_SETUP_TOKEN` / `$ONESIGNAL_REST_API_KEY`), run `python3 /scripts/onesignal_api.py app ` — the view-app read, `GET /api/v1/apps/{APP_ID}` (app auth works) — and read the verdict for the platform you're about to install: Android → `platforms.fcm.configured`, iOS → `platforms.apns.configured`, Web → `platforms.web.configured` (or the free `web-probe` below). The script decides from the **non-secret presence fields** only (`apns_env`, `fcm_sender_id`, `chrome_web_origin`; api-reference "View an app"), and treats `null`, `""`, and an absent key the same. Without Python (Step 0.2), `curl` the same `GET` with whichever key env var is set, and keep only the HTTP status, the app `id`, and the presence fields — the raw body still carries the plaintext credentials until the server removes them, so never print or paste the whole body: `curl -sS -w '\nHTTP %{http_code}\n' -H "Authorization: Key ${ONESIGNAL_REST_API_KEY:-$ONESIGNAL_SETUP_TOKEN}" "https://api.onesignal.com/api/v1/apps/" | grep -oE '"(id|apns_env|apns_key_id|apns_team_id|apns_bundle_id|fcm_sender_id|chrome_web_origin)":[^,}]*|^HTTP [0-9]+'`. A verdict exists **only** when the `HTTP` line is 2xx **and** the `"id"` line shows this App ID — that pair is your proof the body is the app object. Then read the presence fields and nothing else — never the plaintext credential fields (`apns_p8`, `fcm_v1_service_account_json`, `gcm_key`). **iOS, when APNs is configured:** compare `platforms.apns.bundle_id` (the `apns_bundle_id` field) with the Bundle ID of the main app target (`productType = "com.apple.product-type.application"`), never an extension target. A difference means APNs will reject every push to this app; say so in one sentence with both values and the dashboard URL below, and keep going (credentials skill Step 1, item 4, has the detail). Stay silent when `platforms.apns.bundle_id` is `null` or the project's Bundle ID is unknown. Any other output (no `HTTP` line, a non-2xx status, no `"id"` line, a curl error) is **presence unknown, not "missing"**: do not start the credentials skill on it — take the no-key path below instead. In a sandboxed runtime, a script exit code of 3 or a curl error on the first try means the sandbox blocked the request: re-run that one command with escalation first ("Network access" above), and read presence unknown only if the re-run also fails or the user declines. No key available, or presence unknown → ask the user to check the dashboard with the structured question in credentials skill Step 1, item 7, and apply its rules to the answer ("I am not sure" is unknown, never "missing"). Send the page as a clickable URL built from the App ID: `https://dashboard.onesignal.com/apps//settings/push/platforms` (Settings > Push Platforms). 2. **Missing → run the credentials skill NOW**, before writing any code. It walks the human through the Apple/Firebase console steps and uploads the file itself via the write-once endpoint (`POST /api/v1/apps/{APP_ID}/credentials`). Do not proceed until it reports success or the user explicitly defers. 3. **Confirm the config is LIVE before any device ever runs** — use the API script, which encodes the cache-bust and poll-ordering quirks (no MCP tool covers these config reads — see api-reference "OneSignal MCP server"). In a sandboxed runtime the probe needs escalation; the sandbox block shows as exit code 3 on the first try, so re-run that one command with escalation ("Network access" above): - **Android:** `python3 /scripts/onesignal_api.py android-params ` — polls until `android_sender_id` appears (`status: fcm_live`). ⚠️ Run only AFTER the credential upload; fetching before credentials exist primes a CDN cache with the empty response on a fresh app. From ce1263f2be2edde6c4d9953353dd67a837666322 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 28 Oct 2026 18:30:23 -0700 Subject: [PATCH 3/3] docs: leave the version bump to the release process The Create Release PR workflow writes the version into the 3 places, so feature and fix change sets do not bump it. --- AGENT.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/AGENT.md b/AGENT.md index 0b54c98..a49698e 100644 --- a/AGENT.md +++ b/AGENT.md @@ -192,11 +192,9 @@ The plugin version lives in 3 places. Bump all 3 together — they must never di - `PLUGIN_VERSION` in `scripts/checkpoint.sh` — every checkpoint reports this value as `skill_version`. -Keep the version current: when a change set affects plugin behavior (skills, scripts, -references, templates, or manifests), bump the version in that same change set. Do not -leave the version stale. One bump per release is enough: if an unreleased change set on -`main` already moved the version past the last release, later change sets in the same -release do not bump it again. +Do not bump the version in a feature or fix change set. The release process below writes +the new version into the 3 places, so a change set that affects plugin behavior leaves +the version at the last release. Each release ships 2 artifacts: the tagged tree (Claude Code and the Codex marketplace install from it) and `onesignal-skills-.zip` (the OpenAI directory upload). The @@ -210,7 +208,7 @@ The workflows in `.github/workflows/` reuse `OneSignal/sdk-shared`: merged PR titles since the latest stable version tag and picks the bump (`feat:` → minor, `fix:`/`perf:` → patch, `!:` → major), or takes a version override. It creates or rebases `rel/`, writes the version into the 3 files, commits - `chore: Release ` (an empty commit when a change set already bumped the tree), + `chore: Release ` (an empty commit when the tree already has that version), and opens the `chore: Release ` PR with release notes built from the PR titles. 2. Before merge, the release PR records 3 results, each with its session IDs or command: - The behavior evals against the repository tree in Claude Code (check 7 in "How to