Skip to content

[pull] main from aws-amplify:main - #681

Merged
pull[bot] merged 2 commits into
MLH-Fellowship:mainfrom
aws-amplify:main
Jul 30, 2026
Merged

[pull] main from aws-amplify:main#681
pull[bot] merged 2 commits into
MLH-Fellowship:mainfrom
aws-amplify:main

Conversation

@pull

@pull pull Bot commented Jul 30, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

soberm added 2 commits July 30, 2026 08:50
…r Push Notifications (#14866)

* feat(notifications): add Connect Customer Profiles push provider as new subpath export

Add an opt-in Push Notifications provider backed by Amazon Connect
Customer Profiles, exposed as a new subpath export alongside the
existing Pinpoint provider (mirroring the Analytics Customer Profiles
provider from #14864):

- aws-amplify/push-notifications/customer-profiles
- @aws-amplify/notifications/push-notifications/customer-profiles

Device-token registration (initializePushNotifications) and identifyUser
register the device with Connect Customer Profiles instead of calling the
Pinpoint UpdateEndpoint API. Authorization mode is selected automatically
from the resolved auth session:

- Authenticated (Cognito user-pool): POST {endpoint}/identify-user with
  Authorization: Bearer <accessToken>; backend keys the profile on `sub`.
- Guest (Identity Pool unauthenticated): POST {endpoint}/identify-user-guest,
  SigV4-signed (execute-api) with the guest credentials; backend keys the
  profile on `identityId`. Enables registering a device token before
  sign-in. On a later authenticated call, options.previousGuestIdentityId
  folds the guest profile (and its devices) into the authenticated profile.

Configured via the new notifications.amazon_connect_customer_profiles
amplify_outputs key (Notifications.PushNotification.CustomerProfiles =
{ endpoint, region }). All public method signatures, the native event bus,
and the token lifecycle match the Pinpoint provider 1:1; IdentifyUserInput
options additionally accept optional deviceId/platform/appVersion/
previousGuestIdentityId device-registration fields. Pinpoint engagement
telemetry (_campaign.*/_journey.*) is intentionally not ported (Connect
Customer Profiles has no client-side event API).

Additive and non-breaking: the default push-notifications export still
resolves to Pinpoint; core gains an optional CustomerProfiles config
type parsed side-by-side with Pinpoint.

* fix(notifications): align Customer Profiles device registration to backend contract

The guest/authed device-registration request built a body with flat top-level
deviceToken/channelType and no userProfile. The deployed identify-user Lambda
requires userProfile to be an object and reads device fields from options
(options.deviceId, options.address, options.channelType), so guest registration
returned HTTP 400 ("userProfile is required and must be an object").

Build the body as { userId, userProfile: userProfile ?? {}, options: { ...options,
deviceId, address: deviceToken, channelType } }. Add getDeviceId(): a stable
per-install UUID persisted in AsyncStorage so token refreshes upsert the same
device object. channelType mapping (Android -> GCM) and resolveConfig unchanged.

* refactor(notifications): extract transport-agnostic push logic into shared provider module

Both push-notification providers (pinpoint, customer-profiles) previously
duplicated ~80% of their surface. Extract the transport-agnostic logic into a
new internal providers/shared/ module and have both providers consume it.

Shared (providers/shared/): getBadgeCount, getLaunchNotification,
getPermissionStatus, requestPermissions, setBadgeCount, onNotificationOpened,
onNotificationReceivedInBackground/Foreground, onTokenReceived (.ts + .native.ts),
plus getChannelType, inflightDeviceRegistration, and the shared API/input/output
types. Each provider's apis/index.ts and utils/index.ts now re-export from
../../shared/...; provider types re-export shared types and keep only
IdentifyUser/IdentifyUserInput/IdentifyUserOptions local.

Provider-specific and unchanged: identifyUser + initializePushNotifications
(pinpoint wires createMessageEventRecorder analytics listeners; customer-profiles
omits them). Pinpoint external behavior is identical after the extraction.

Also relocate the transport-agnostic unit tests to __tests__/.../shared/ and fix
the stale registerDeviceWithCustomerProfiles test (mock @aws-amplify/react-native
and getDeviceId; assert the current nested options:{deviceId,address,channelType}
backend contract).

Net -1349/+250 lines. Build (esm+cjs) and 36 pushNotifications test suites
(118 tests) pass.

* ci(release-preid): trigger preid release on feat/connect-customer-profiles-push

Add the feature branch to the release-preid push trigger so every push
publishes a prerelease. The preid label is derived from the 2nd branch
path segment by the existing parse-preid job, yielding npm dist-tag
'connect-customer-profiles-push' (passes the forbidden-preid check).
No change to release behavior for any other branch.

* chore(notifications): remove changeset from CP push PR

* feat(notifications): support browser identifyUser for Customer Profiles and rename internal engine to identifyUserInternal

- Browser `identifyUser` now performs a device-less profile identify (e.g.
  identify end-users by email); the web build no longer throws
  PlatformNotSupportedError. The browser has no push token, so no device is
  registered.
- Rename the internal engine `registerDeviceWithCustomerProfiles` ->
  `identifyUserInternal` and make it device-optional: device fields
  (deviceId/address/channelType) and the stable per-install deviceId are only
  attached when a device token is present, preserving find-or-create.
- Both callers funnel through the one engine, mirroring the Pinpoint
  two-callers-of-one-core pattern: native `identifyUser` and the
  token-received `registerDevice` helper call `identifyUserInternal`
  directly (registerDevice does not route through identifyUser).
- Keep `@aws-amplify/react-native` out of the web bundle by lazily importing
  `getDeviceId` only on the device-registration path.
- Engine failure surfaces as PushNotificationError name `IdentifyUserException`.

* fix(notifications): resolve deviceId in native layer to avoid __importStar dynamic-import crash in RN

The lazy await import('./getDeviceId') in identifyUserInternal transpiled
(CJS) to the __importStar tslib helper, which is not injected in the
Hermes/React Native runtime -> ReferenceError on the device-registration
path (DeviceRegistrationFailed). Remove the runtime dynamic import entirely:
the engine no longer imports getDeviceId (static or dynamic). Instead the
native callers (identifyUser.native and the token-received registerDevice
helper) resolve the stable per-install deviceId via getDeviceId and inject it
as options.deviceId. Web identifyUser stays device-less and RN-free without
any dynamic import.

* refactor(notifications): rename previousGuestIdentityId to guestIdentityId

Rename the guest-merge wire-contract field previousGuestIdentityId to
guestIdentityId across the Customer Profiles push identify flow, matching
the backend contract rename. The IdentifyUserOptions field name is the
JSON key sent on the wire in the identify-user request body's options,
so this must stay in lockstep with the backend.

* refactor(notifications): remove guestIdentityId from customer-profiles IdentifyUserOptions

The backend no longer merges guest and authenticated Customer Profiles; guest and authenticated profiles are treated as completely separate profiles. Remove the now-dead client-side guestIdentityId guest->auth linking field from IdentifyUserOptions, its passthrough documentation in identifyUserInternal, and the related test assertion.

* feat(notifications): align CP push client to SigV4 identify/register/remove backend contract

Rework the customer-profiles push provider to the new backend contract:
profile-only identifyUser (drops userId), new registerDevice/removeDevice
native APIs, and a single SigV4 (execute-api) signedFetch transport for all
three routes for both authenticated and guest Identity Pool sessions. Server
derives principalId from the signer identity; the Bearer-JWT and guest-route
paths are removed. initializePushNotifications wires TOKEN_RECEIVED->registerDevice
and Hub 'signedOut'->removeDevice.

* feat(notifications): attach Amplify user-agent header to CP push signedFetch

Add the x-amz-user-agent telemetry header (getAmplifyUserAgent with
Category.PushNotification + PushNotificationAction.IdentifyUser) to the
customer-profiles signedFetch transport for all three routes, matching the
prior Pinpoint/SDK path. The header is set before SigV4 signing so it is
covered by the signature and the sent headers stay consistent.

* refactor(core): rename amplify_outputs notifications key amazon_connect_customer_profiles -> amazon_connect

Aligns the client parser with the backend-notifications OUTPUT_KEY rename and
client-config schema v1.5. Updates AmplifyOutputsNotificationsProperties, the
parseNotifications destructuring, and parseAmplifyOutputs test fixtures. The
internal ResourcesConfig.PushNotification.CustomerProfiles key is unchanged.

* fix(notifications,core): address CP-push review findings

- core: add RegisterDevice + RemoveDevice to PushNotificationAction enum.
- signedFetch: take an `action` arg; identify/register/remove pass the correct
  PushNotificationAction so the x-amz-user-agent encodes the per-route action.
- buildDeviceRegistration: guard the token (throw PushNotificationError NoToken
  when neither a supplied nor a current token is available); return non-optional
  token. Add a new NoToken validation error code.
- resolveCredentials: replace assert with an explicit if-throw so TS narrows
  credentials to non-undefined.
- Tests: assert per-route action in the user-agent; assert signing receives no
  Authorization/Bearer header and fetch uses exactly the signer-returned headers;
  add buildDeviceRegistration fallback + no-token throw tests (drop the
  `as unknown as string` hack).
- Docs: note identifyUser.native is intentionally pre-init-callable; document the
  getDeviceId module cache lifecycle.

* fix(notifications): enforce https endpoint in Customer Profiles resolveConfig to prevent SigV4 token exfiltration

resolveConfig is the single choke point for all three Customer Profiles push routes. The endpoint flows untrusted from amplify_outputs.json (amazon_connect.endpoint) into signedFetch, which SigV4-signs requests carrying the Identity Pool x-amz-security-token to whatever host it resolves to. Parse the endpoint with new URL() and assert https: scheme (new InvalidEndpoint validation code), rejecting http/ftp/malformed URLs before any credentialed request is signed. Scheme-only enforcement (no hostname allowlist) to stay compatible with custom domains, aws-cn/aws-us-gov, FIPS, and VPC endpoints.

* fix(notifications): add client-side UserProfile/customAttributes validation mirroring backend bounds (defense-in-depth)

* feat(notifications): warn on the deprecated default push-notifications entry point

The default `aws-amplify/push-notifications` entry point is backed by Amazon
Pinpoint, which reaches end-of-support on October 30, 2026. It cannot be removed
without a breaking change, so all 11 of its runtime APIs now emit a one-time
ConsoleLogger warning steering customers to the Customer Profiles sub-path
export (`aws-amplify/push-notifications/customer-profiles`), which exposes an
equivalent for every one of them.

The warning names the deprecated entry point rather than the individual API:
9 of the 11 are transport-agnostic and are re-exported unchanged by every
provider, so it is the entry point, not the behaviour of the call, that
customers need to migrate away from.

Mirrors the Analytics precedent (#14877). Reuses the existing ConsoleLogger
convention rather than raw console.warn. Non-breaking: the wrapped consts
preserve the exact names, types, and signatures of the underlying APIs
(verified by a bidirectional type-assignability check over all 11), and
synchronous throws and rejected promises both reach the caller untouched.

* chore(notifications): add changeset for the Customer Profiles push provider

Covers @aws-amplify/core, @aws-amplify/notifications, and aws-amplify as minor
for the new Customer Profiles push-notifications provider and its sub-path
exports, plus the default entry point deprecation notice. Follows the repo
precedent of a minor bump for a new provider surface.

* ci(release-preid): drop the Customer Profiles push branch from the preid trigger list

The branch-specific preid publish trigger was only needed while iterating on
this feature branch. Removing it keeps the workflow free of branch names that
disappear once the feature merges, restoring parity with main.

* fix(notifications): restrict Customer Profiles endpoint to execute-api host + normalize trailing slash

Requests to the configured endpoint are SigV4-signed for execute-api, so the
endpoint host is now validated against the API Gateway host of the resolved
region (<api-id>.execute-api.<region>.amazonaws.com). Previously only the
presence of endpoint/region and an https: protocol were checked, so a
misconfigured or attacker-supplied endpoint would have received the signed
credentials.

Also strips trailing slashes from the endpoint so {endpoint}{path} never yields
a double slash. URL.origin is deliberately not used, as it would drop an API
Gateway stage path such as /prod.

* fix(notifications): fetch the signed request url

signRequest returns an HttpRequest whose url is authoritative and covered by
the signature. Fetching the pre-signing url instead could send a url that does
not match what was signed.

* fix(notifications): dedupe concurrent getDeviceId via in-flight promise

AsyncStorage reads are async, so concurrent first-calls could each read no
stored id, mint their own UUID and write it, producing divergent device ids for
one install. The resolution is now memoized as a single in-flight promise so
concurrent callers share one resolution (single UUID, single write). The cached
promise is cleared on failure so a transient storage error does not permanently
wedge subsequent calls.

* fix(notifications): re-register push device on sign-in; drop ineffective sign-out de-registration

The auth Hub listener now re-registers the device on `signedIn`. A returning
user's push token is unchanged across sign-in, so the native TOKEN_RECEIVED
listener short-circuits and nothing re-registered the device — it stayed homed
to the guest principal. Backend register-device is an idempotent
last-writer-wins upsert on `deviceId`, so re-registering re-homes the existing
row instead of creating a duplicate.

The `signedOut` -> `removeDevice()` branch is removed. It could not work:
`signOut()` clears credentials before the `signedOut` event fires, so the
listener signed as a brand-new guest identity, and the backend's
principal-gated delete returns 200 for a non-owner (no-info-leak invariant).
The removal silently no-opped and `.catch` never ran. `removeDevice` remains a
public API for applications to call before `signOut()`.

Adds un-mocked credential-path coverage that drives the real
resolveCredentials/signedFetch/SigV4 chain (only fetchAuthSession and fetch are
stubbed) and asserts which identity signs each register/remove call.

* docs(notifications): document the pre-signOut removeDevice contract

De-registration is signed with the caller's current credentials and the backend
only removes a device the calling principal owns, so `removeDevice()` must be
awaited BEFORE `signOut()`. Documents this on both platform variants of the API
and describes the device lifecycle in the provider changeset.
@pull pull Bot locked and limited conversation to collaborators Jul 30, 2026
@pull pull Bot added the ⤵️ pull label Jul 30, 2026
@pull
pull Bot merged commit e1067a9 into MLH-Fellowship:main Jul 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant