@siax/mobile-notifications (0.1.1)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/mobile-notifications@0.1.1"@siax/mobile-notifications": "0.1.1"About this package
@siax/mobile-notifications
Klientytan för push i K6-appar: enhetsregistrering, användarens notisval, djuplänkning som överlever kallstart, och en validator som hindrar persondata från att någonsin lämna appen i en nyttolast.
Status: implementerad och testad. 74/74 tester gröna (node --test),
tsc --strict rent. Inte publicerad till Giteas npm-registry.
services/device-registry är inte rest (M9) och n0tify-web är oroutad
(B-M0B-3) — därför är varje transport injicerbar och paketet fungerar fullt ut
mot påhittade transporter i test.
Kärnbussgränsen (MOB-02 / PUSH-01)
All notislogik ligger i N0TIFY. Regler, mallar, schemaläggning, tystnadsperioder, sammanslagning, frekvenstak, retries och de faktiska APNs/FCM-transporterna — allt där. Ingen push-tjänst byggs här.
Det som hör hemma på enheten, och bara där:
| Del | Vad den gör | Vad den inte gör |
|---|---|---|
PushRegistrar |
registrerar enhet + token mot services/device-registry, idempotent |
äger inte enhetsregistret, skickar inga notiser |
NotificationPreferencesClient |
speglar användarens val per notisslag (PUSH-07) | utvärderar inga regler — N0TIFY har sista ordet |
DeepLinkQueue |
🔴 PUSH-08: länkar som överlever kallstart | äger ingen router |
validateWakeUpPayload |
🔴 PUSH-05: väckningsdata endast | ersätter inte N0TIFY:s serversidiga kontroll |
Inga hemligheter finns i eller nära paketet (MSEC-01): ingen APNs-p8, inget FCM-servicekonto, ingen projektnyckel. De bor i Infisical och används av N0TIFY.
🔴 PUSH-05 — persondata skickas ALDRIG i nyttolasten
Notisen är en väckning: "det finns något nytt av typ X med referens Y". Innehållet hämtar appen över den autentiserade kanalen.
Regeln är hård eftersom en push passerar Apples och Googles servrar, ligger i klartext på låsskärmen, hamnar i OS-loggar, i notishistoriken, i en Android-backup och på ett parat wearable. Ingen av de platserna omfattas av vårt personuppgiftsbiträdesavtal. Det finns ingen säker "bara förnamnet"-nivå.
Mekaniken är deny-by-default på nyckelnamn — den enda regeln som håller
över tid. Tillåtna nycklar: v, type, ref, deepLink, notificationId,
sentAt, badge, collapseKey, priority, ttlSeconds, titleKey,
bodyKey, contentAvailable, threadKey. Allt annat avvisas oavsett innehåll.
Ovanpå det ligger fyra lager till:
- Formkrav —
typemåste vara en händelsetyp (order.updated),refochnotificationIdogenomskinliga id,titleKey/bodyKeykatalognycklar och inte översatt text. - PII-avkänning i allowlistade strängvärden: e-post, telefonnummer, svenskt personnummer, IBAN och Luhn-giltiga kortnummer. Egna mönster kan skjutas in.
- Djuplänkskontroll —
javascript:/data:/file:avvisas alltid, och query-parametrar med känsliga namn (?email=…) fångas. - Storleksgräns 3800 byte. En väckning som är för stor bär nästan alltid innehåll som inte hör hemma där.
import { assertWakeUpPayload } from '@siax/mobile-notifications';
assertWakeUpPayload({ type: 'order.updated', ref: 'ord_01HX3' }); // ok
assertWakeUpPayload({ title: 'Anna Andersson', body: 'Ring mig' }); // kastar
validateWakeUpPayload() returnerar hela listan på en gång i stället för att
kasta, så att den som skriver en ny notistyp ser alla fel i ett testutfall.
🔴 PUSH-08 — djuplänkar överlever kallstart
Problemet: användaren trycker på en notis medan appen är helt död. OS:et startar processen och levererar länken innan navigationsträdet är monterat. En variabel i minnet tappar den varje gång appen hinner dödas igen.
Lösningen: kön persisteras. enqueue() returnerar inte förrän länken ligger
i lagringen. En ny process över samma lagring hittar den och levererar den så
snart en hanterare registrerats.
const queue = new DeepLinkQueue({
storage: minAsyncStorageAdapter, // MÅSTE vara persistent
validator: createDeepLinkValidator({
allowedSchemes: ['https', 'siaxexempel'],
allowedHosts: ['app.exempel.test'],
allowedPathPrefixes: ['/order', '/faktura'],
}),
});
// Tidigast möjliga punkt i appstarten, före navigationsträdet:
const initial = await Notifications.getLastNotificationResponseAsync();
if (initial) {
await queue.enqueue({ url: …, source: 'cold-start', notificationId: … });
}
// När routern är monterad:
await queue.setHandler((link) => { router.navigate(link.url); return true; });
Garantier, alla testade:
- at-least-once — hanteraren måste returnera
true. Säger denfalseeller kastar den behålls länken för nästa försök. En tyst förlust är värre än ett extra försök. - deduplicering på
notificationId, som överlever omstart via en levererat-historik. OS:et får leverera samma notis två gånger. - FIFO på mottagningstid, utgång efter 7 dygn, överfyllnadsskydd vid 32 poster (äldst faller ut, med händelse).
- korrupt kö slängs — en förlorad djuplänk är en olägenhet, en app som inte startar är ett haveri.
clear()vid utloggning — köade länkar kan peka på förra användarens data.
Säkerhet: en djuplänk kommer från nätet, alltså är den en
navigationsinjektion tills motsatsen bevisats. Validatorn är deny-by-default på
scheman, avvisar alltid javascript:/data:/file:/blob:/about:, avvisar
inbäddade uppgifter (https://app.exempel.test@angripare.example) och kan
begränsa värdar och sökvägsprefix. Avvisade länkar persisteras aldrig.
Kör kön på minneslagring och den skickar ephemeral-storage-warning — då är
PUSH-08 inte uppfylld, och det ska synas.
Enhetsregistrering
PushRegistrar registrerar bara när något faktiskt ändrats (token, plattform,
appversion, byggnummer, OS-version, språk, tidszon, attesteringsstatus) eller när
revalideringsfönstret (7 dygn, eller serverns expiresAt) löpt ut. Kastar
aldrig — returnerar status: 'failed' med felet, så att en död server inte
fäller appstarten.
- PUSH-04, klientsidan:
invalidate('apns-410-unregistered')glömmer den lokala posten när en leverans avvisats, så att nästa start registrerar om sig. Utan det tror klienten för alltid att den är registrerad. - Utloggning:
unregister()rensar lokalt även när servern felar — att tro sig registrerad utan att vara det är det värre feltillståndet. - Push-token maskeras i alla händelser (
redactPushToken). Den är en enhetsidentifierare och en kapningsvektor, inte loggdata.
Inställningar per notisslag (PUSH-07)
Spegling av N0TIFY, inte en egen regelmotor. Cache först så skärmen renderar
direkt, sedan servern. Optimistiska ändringar rullas tillbaka när servern
nekar — ett reglage som står kvar i fel läge får användaren att tro att hen är
avanmäld. required-märkta slag (säkerhet, betalningsfel) kan inte stängas av
och muteAll tystar dem inte.
Filer
| Fil | Ansvar |
|---|---|
src/payload.ts |
PUSH-05-validatorn |
src/deep-links.ts |
PUSH-08-kön + länkvalidatorn |
src/registrar.ts |
enhets- och tokenregistrering |
src/preferences.ts |
PUSH-07-speglingen |
src/types.ts |
delade typer, lagringsadapter, tokenmaskering |
Tester
cd packages/mobile-notifications
node --test # 74 tester, inga beroenden, ingen npm install
tsc -p tsconfig.json
Kör från repo-roten: node --test "packages/mobile-notifications/**/*.test.ts".
(Ett katalogargument fungerar inte på Node 24 — positionsargument tolkas som
globmönster.)
Kallstartstestet simulerar en riktig processomstart: två DeepLinkQueue-objekt
över samma lagring, där det andra aldrig sett det första.
Kräver Node ≥ 22.18 för inbyggd typradering. Node 20 kan inte köra testerna.
Medvetet ogjort
- Ej publicerad till
https://git.siax.io/api/packages/sax3l/npm/. - Inga OS-bindningar. Paketet rör varken
expo-notifications,@react-native-firebase/messagingellerPushNotificationIOS— det hade låst varje K6-app till ett val och lagt ett nativt beroende i ett rent TypeScript-paket. Appen kopplar OS-hookarna tillqueue.enqueue()ochregistrar.register(). Den limkoden finns inte här och hör till M2:s exempelapp. - Ingen behörighetsbegäran (
requestPermissions) — den är OS-specifik och hör till samma limlager. - Ingen HTTP-transport mot enhetsregistret eller N0TIFY, till skillnad från
@siax/mobile-configsom har en. Skälet:services/device-registrysaknar ännu kontrakt icontracts/(M9), och att gissa dess URL-form vore att skriva fast en osann premiss. - Ingen exempelapp (MOB-13 kräver en i CI) — hör till M2.
- Typkontroll av testfilerna kräver
@types/node, medvetet inte installerat i denna omgång.tsconfig.jsontäckersrc/**; testerna är verifierade separat och är rena bortsett från saknade Node-typer.