Contrat push de l'alerte de sûreté MIK
Ce qu'un système externe d'alerte de sûreté du navire envoie à la source push mik-ssa-alerts-v1 - champs, signature, horodatage, réponses et reprises.
Cette page s'adresse à l'équipe qui exploite le système émettant l'alerte de sûreté du navire (SSA) vers l'exemple MIK mer du Nord. Elle décrit exactement ce que vérifie le côté récepteur, pour qu'un émetteur puisse être construit à partir d'elle. Tout ce qui suit est déduit du code qui reçoit la livraison ; la dernière section nomme le fichier de chaque règle. Rien ici n'est une procédure opérationnelle : les champs suivent le plan public d'une SSA (chapitre XI-2, règle 6 de SOLAS, et Code ISPS) et rien de plus.
Le récepteur est le mode d'ingestion push de la source de données mik-ssa-alerts-v1. Les mécanismes génériques de ce mode (adresse, ordre, livraisons mises de côté) sont décrits dans Modes d'ingestion ; cette page est le contrat de cette source précise.
Ce qu'il faut obtenir du côté récepteur
| Élément | Valeur |
|---|---|
| Méthode et adresse | POST https://<hôte push>/api/platform/data-sources/push/<identifiant de liaison>. L'identifiant de liaison fait 64 caractères hexadécimaux minuscules ; il s'affiche dans l'onglet Point de terminaison push de la source, dans Analytics, Sources de données. |
| Secret partagé | La valeur de la connexion mik-ssa-hmac-secret de la source installée. Il n'est jamais dans le manifeste et ne circule jamais dans une requête. Les deux côtés détiennent la même chaîne, utilisée en octets UTF-8. |
| Algorithme | HMAC-SHA256. |
| Contenu d'une livraison | JSON, UTF-8, au plus 1 Mio et 1 000 alertes par livraison. |
Une livraison
Une requête porte un objet JSON dont le tableau alerts contient les alertes (normalement une). Envoyez un tableau : une valeur alerts qui est un objet unique au lieu d'un tableau est aussi lue, comme une seule alerte, mais cela ne fait pas partie du contrat. Trois en-têtes portent l'identité, l'heure de signature et la signature :
| En-tête | Contenu |
|---|---|
x-ssa-alert-id | L'identifiant de livraison : une chaîne de 1 à 512 caractères, unique par livraison (un UUID convient). C'est la clé d'idempotence, pas l'identité de l'alerte. |
x-ssa-signature-timestamp | L'heure de signature en secondes Unix, écrite comme un entier décimal de 1 à 12 chiffres, sans zéro de tête et sans rien d'autre. Le récepteur la lit comme un nombre et signe ce nombre : 01791936000 est signé comme 1791936000, et un condensat calculé sur le texte complété de zéros est refusé. |
x-ssa-signature | sha256=<hex> ou le condensat hexadécimal seul, 64 caractères hexadécimaux, majuscules ou minuscules. |
Les noms d'en-têtes sont comparés sans tenir compte de la casse, comme tous les noms d'en-têtes HTTP. Envoyez Content-Type: application/json.
{
"alerts": [
{
"alertId": "3f0c7c2e-8d1a-4b6e-9a52-7e1d2c4b8f10",
"mmsi": "000000002",
"imo": "9000002",
"shipName": "Demo Aurora Tide",
"callsign": null,
"latitude": 51.62,
"longitude": 2.71,
"alertedAt": "2026-10-14T00:00:00.000Z",
"status": "active",
"provenance": "demo sender"
}
]
}Champs
Chaque alerte est un objet avec ces champs. Les champs inconnus sont ignorés. Les types sont vérifiés alerte par alerte une fois la livraison acceptée (voir Réponses).
| Champ | Type | Obligatoire | Unité et format | Exemple |
|---|---|---|---|---|
alertId | chaîne | oui | L'identité de l'alerte et la clé primaire de la table. Stable pendant toute la vie de l'alerte : l'annulation la reprend. Un UUID est recommandé. | 3f0c7c2e-8d1a-4b6e-9a52-7e1d2c4b8f10 |
mmsi | chaîne | oui | Identité du service mobile maritime, neuf chiffres, envoyée comme chaîne : un nombre JSON est refusé, et un nombre perdrait de toute façon les zéros de tête. | "000000002" |
imo | chaîne ou null | non | Numéro OMI. Peut être null ou absent. | "9000002" |
shipName | chaîne | oui | Le nom du navire. | "Demo Aurora Tide" |
callsign | chaîne ou null | non | Indicatif d'appel radio. Peut être null ou absent. | null |
latitude | nombre | oui | Degrés décimaux, positif vers le nord. Un nombre JSON : une chaîne est refusée. | 51.62 |
longitude | nombre | oui | Degrés décimaux, positif vers l'est. Un nombre JSON : une chaîne est refusée. | 2.71 |
alertedAt | chaîne | oui | Instant RFC 3339 avec fuseau : ...Z ou ...+hh:mm. Une valeur sans fuseau est refusée. Stocké en UTC. | "2026-10-14T00:00:00.000Z" |
status | chaîne | oui | "active" pour une alerte, "cancelled" pour une annulation. Voir ci-dessous. | "active" |
provenance | chaîne | oui | Texte libre indiquant d'où vient l'alerte. | "demo sender" |
Le récepteur ne vérifie ni l'intervalle ni le système géodésique d'une coordonnée : envoyez du WGS 84 et gardez vous-même la latitude entre -90 et 90 et la longitude entre -180 et 180. Un imo ou un callsign absent est stocké comme null.
Alerte et annulation
Il n'y a qu'une forme de message et qu'un point d'entrée. Les deux messages ne diffèrent que par status :
- Alerte :
statusvaut"active". Un nouvelalertIdcrée une ligne. - Annulation : le même corps avec le même
alertIdetstatusà"cancelled", envoyé comme nouvelle livraison (nouvel identifiant de livraison). La source écrit enupsertsuralertId: l'annulation remplace la ligne de l'alerte. Répétez tous les champs obligatoires : la ligne est remplacée en bloc, etalertedAtdoit garder la valeur d'origine de l'alerte. La comparaison est exacte et en minuscules ; toute autre valeur est stockée telle quelle et n'est pas une annulation.
La surveillance qui lève une détection à partir des alertes écarte à la lecture toute alerte dont status vaut "cancelled" : une alerte annulée ne lève aucune détection et n'apparaît pas dans l'application. Une annulation ne retire pas une détection ou un dossier que l'alerte active a déjà ouvert : la surveillance ne lit plus cette ligne, elle n'agit donc pas dessus. Ce contrat ne décide pas si une annulation doit retirer quelque chose.
Horodatage
x-ssa-signature-timestampest en secondes Unix UTC (le même instant partout sur Terre ; il n'y a pas de fuseau à choisir).- Le récepteur accepte une livraison dont l'heure de signature est à 300 secondes ou moins de sa propre horloge, dans les deux sens, bornes comprises. Au-delà, la réponse est
401 push_signature_staleet rien n'est lu dans le corps. - Gardez l'horloge de l'émetteur synchronisée (NTP). Une reprise reçoit un horodatage neuf et le même identifiant de livraison ; l'horodatage ne fait pas partie de l'identité d'une livraison.
alertedAtest autre chose : le moment où l'alerte a été déclenchée à bord. Le récepteur ne le compare pas à l'horloge. La surveillance de détection de l'exemple, en revanche, ne lit que les alertes dontalertedAtdate de moins de 900 secondes : une alerte horodatée plus ancienne est stockée mais ne lève aucune détection.
Signature
La signature couvre ces octets, dans cet ordre, sans rien ajouter :
<x-ssa-signature-timestamp>.<x-ssa-alert-id>.<le corps de la requête, octet pour octet>Le condensat est HMAC-SHA256(clé = le secret en UTF-8, message = ces octets), envoyé en hexadécimal, éventuellement préfixé par sha256=. Un autre préfixe que sha256= est refusé. Le corps est signé tel qu'envoyé : il n'y a pas de forme canonique, donc signez les octets finaux après sérialisation et envoyez exactement ceux-là.
Comme l'horodatage et l'identifiant de livraison sont dans les octets signés, une requête capturée ne peut être rejouée ni plus tard ni sous un autre identifiant de livraison. Il y a un secret par source installée. Le récepteur résout la connexion à neuf pour chaque livraison : changer le secret revient à modifier la connexion ; la première livraison suivante est vérifiée avec la nouvelle valeur, et il n'existe pas de période de recouvrement où les deux secrets seraient acceptés.
Un exemple détaillé. Avec le secret example-shared-secret, l'identifiant de livraison 7d0f3c2e-5b1a-4a8e-9f61-2c4b8e1d0a93 et l'horodatage 1791936000 (2026-10-14T00:00:00Z), la signature du corps ci-dessus sur une seule ligne et sans saut de ligne final donne :
BODY='{"alerts":[{"alertId":"3f0c7c2e-8d1a-4b6e-9a52-7e1d2c4b8f10","mmsi":"000000002","imo":"9000002","shipName":"Demo Aurora Tide","callsign":null,"latitude":51.62,"longitude":2.71,"alertedAt":"2026-10-14T00:00:00.000Z","status":"active","provenance":"demo sender"}]}'
DELIVERY_ID='7d0f3c2e-5b1a-4a8e-9f61-2c4b8e1d0a93'
TS=1791936000
printf '%s.%s.%s' "$TS" "$DELIVERY_ID" "$BODY" | openssl dgst -sha256 -hmac 'example-shared-secret'
# 9e783debd3c597b8cb4b5797c47cc9a4e05b727fda23363d8ef7dc9ba66d0291La requête porte alors x-ssa-signature: sha256=9e783debd3c597b8cb4b5797c47cc9a4e05b727fda23363d8ef7dc9ba66d0291.
Réponses
Les vérifications s'exécutent dans cet ordre ; la première qui échoue répond.
| Statut | error | Cause |
|---|---|---|
405 | method_not_allowed | Ce n'est pas un POST. |
404 | push_binding_not_found, push_binding_invalid, push_ingress_off | L'identifiant de liaison de l'adresse est invalide, inconnu ou ne correspond pas à la source installée ; ou ce déploiement n'accepte aucun push. Ne pas reprendre. |
401 | push_authentication_failed | L'en-tête x-ssa-signature est absent ou n'est pas un condensat bien formé (64 caractères hexadécimaux, préfixe sha256= facultatif). |
400 | push_delivery_id_required | L'en-tête de l'identifiant de livraison est absent ou dépasse 512 caractères. |
401 | push_authentication_failed | L'en-tête x-ssa-signature-timestamp est absent ou n'est pas composé de 1 à 12 chiffres décimaux. |
401 | push_signature_stale | Heure de signature à plus de 300 secondes de l'horloge du récepteur. |
413 | push_body_too_large | Corps de plus de 1 Mio. |
400 | push_body_unreadable | Le corps n'a pas pu être lu jusqu'au bout. |
503 | push_connection_required, push_credential_unavailable | Aucune connexion (secret) exploitable n'est encore configurée pour la source. Reprendre plus tard. |
401 | push_authentication_failed | Le condensat calculé sur les octets reçus ne correspond pas au secret. |
429 | push_rate_limited | Trop de livraisons. Attendre le nombre de secondes de l'en-tête Retry-After, s'il est présent. Seule une livraison correctement signée compte dans la limite. |
400 | push_payload_invalid | Le corps n'est pas de l'UTF-8 ou pas du JSON, ou alerts est absent, null ou un scalaire. |
413 | push_rows_too_large | Plus de 1 000 alertes. Rien n'est mis en file. |
503 | push_acceptance_unavailable, ingest_execution_unavailable | Le récepteur n'a pas pu mettre la livraison en file de façon durable. Reprendre la même livraison. |
202 | aucune ; corps {"status":202,"receiptId":"...","duplicate":false} | Acceptée et mise en file de façon durable. |
Un 202 signifie que la livraison est en file, pas que chaque alerte qu'elle contient est stockée. Après l'acceptation, chaque alerte est vérifiée selon le tableau des champs ci-dessus : une alerte qui y déroge (un nombre là où mmsi doit être une chaîne, un alertedAt sans fuseau, un champ obligatoire absent) est mise de côté seule, conservée une heure, et les autres alertes de la livraison sont écrites. Un émetteur qui veut la confirmation qu'une alerte est arrivée dans la table doit lire la table ou la santé de la source dans Analytics ; la réponse ne le dit pas.
Reprises et idempotence
- Même identifiant de livraison, mêmes alertes (l'horodatage et la signature peuvent différer, et les octets bruts du corps aussi : ce qui est comparé, ce sont les lignes auxquelles le corps correspond, avec le manifeste de la source, la version du descripteur et l'identifiant de liaison) : réponse
202avec"duplicate": trueet lereceiptIdd'origine, et écriture unique. L'identifiant est mémorisé une heure par défaut après l'écriture ou la mise de côté de la livraison (la durée est un réglage du déploiement) ; ensuite un renvoi est une nouvelle livraison. - Même identifiant de livraison, alertes différentes : ne jamais l'envoyer. Le récepteur refuse le conflit en interne et l'émetteur voit
503 push_acceptance_unavailable, qui ressemble à une panne et se répétera tant que l'identifiant est réutilisé. - Reprendre sur
429,503et les échecs réseau, avec le même identifiant de livraison et le même corps, un horodatage neuf et un délai croissant. Ne pas reprendre sur les autres4xxsans corriger la cause. - Les livraisons sont écrites dans l'ordre où elles ont été acceptées, une à la fois pour la source. Les lignes d'alerte sont écrites en
upsertsuralertId: un renvoi tardif d'une alerte sous un nouvel identifiant de livraison réécrit la même ligne ; une annulation envoyée avant une alerte retardée serait écrasée par elle. Envoyez l'annulation après le202de l'alerte.
Correspondance avec le plan public de la SSA
Le plan public d'une alerte de sûreté du navire (chapitre XI-2, règle 6 de SOLAS ; Code ISPS) est le suivant : l'alerte identifie le navire, donne sa position et l'heure, et indique que la sûreté du navire est menacée ou compromise. Elle est envoyée à une autorité compétente à terre et non à d'autres navires. Correspondance avec le contrat :
| Contenu public de la SSA | Champ | Remarque |
|---|---|---|
| Nom du navire | shipName | Obligatoire. |
| Numéro OMI | imo | Facultatif. |
| MMSI | mmsi | Obligatoire, chaîne. |
| Indicatif d'appel | callsign | Facultatif. |
| Position du navire | latitude, longitude | Degrés décimaux, WGS 84 attendu. |
| Date et heure de l'alerte | alertedAt | RFC 3339 avec fuseau. |
| La sûreté du navire est menacée ou compromise | une alerte avec status: "active" | Il n'existe pas d'autre champ de menace : l'arrivée d'une alerte active est la déclaration. |
| Alerte désactivée ou réinitialisée à bord | une alerte avec le même alertId et status: "cancelled" | Remplace la ligne de l'alerte et l'empêche de lever une détection ou d'apparaître dans l'application. Ne retire pas une détection ou un dossier déjà ouvert : voir Alerte et annulation. |
| Hors du plan | alertId, provenance | Ajoutés par ce contrat : identité de l'alerte et origine de l'enregistrement. |
Le contrat n'a pas de champ de cap, de vitesse, de type d'alerte ni de texte libre.
Où chaque règle est appliquée
Les chemins sont relatifs à la racine du dépôt ; chaque règle est désignée par le symbole qui la porte, car les numéros de ligne bougent.
| Règle | Où |
|---|---|
Déclaration de la source : en-têtes, référence d'identifiants, $.alerts, upsert | apps/docs/examples/mik-north-sea/ssa-source.ts : createMikSsaAlertSource, MIK_SSA_DELIVERY_ID_HEADER, MIK_SSA_SIGNATURE_HEADER, MIK_SSA_TIMESTAMP_HEADER, MIK_SSA_CREDENTIAL_REF, MIK_SSA_WIRE_FIELDS |
Émetteur : champs du corps, status: "active", identifiant de livraison neuf, horodatage en secondes Unix, signature | apps/docs/examples/mik-north-sea/ssa-alert.ts : mikSsaAlertForVessel, mikSsaAlertBody, signMikSsaDelivery, sendMikSsaAlert |
| Les alertes annulées sont écartées de la surveillance | apps/docs/examples/mik-north-sea/manifest.ts : la surveillance mik-ship-security-alert, target.exclude ; la lecture dans apps/api-ontology/src/kernel/watch/evaluator.ts |
| Adresse push publique et forme de l'identifiant de liaison | packages/sdk/src/platform/contracts/data-source-ingest.ts : DATA_SOURCE_INGEST_PATHS ; apps/api-ingestion/src/continuous-host.ts : PUSH_BINDING_ID, acceptPush |
| Ordre des vérifications et chaque statut ci-dessus | packages/extension-runtime/src/data-source/push/runtime.ts : accept |
| Horodatage en chiffres uniquement ; tolérance dans les deux sens | runtime.ts : signingTimestamp, withinSignatureTolerance ; packages/sdk-authoring/src/data-sources/manifest.ts : PUSH_SIGNATURE_TOLERANCE_SECONDS |
Octets signés <horodatage>.<identifiant de livraison>.<corps>, HMAC, hexadécimal et préfixe sha256= | packages/common/src/security/inbound-auth.ts : timestampedSigningInput, computeInboundHmac, decodeHmacSignature, inboundCredentialShape, verifyInboundCredential |
| Limites de taille et de lignes | packages/sdk/src/platform/contracts/data-source-ingest.ts : SOURCE_BATCH_LIMITS ; apps/api-ingestion/src/continuous-composition.ts : les limits du service push |
Sélection de alerts, y compris un objet unique lu comme une alerte | packages/extension-runtime/src/data-source/payload.ts : mapDeclarativePayload ; packages/common/src/row-codecs/index.ts : toRowCodecItems |
| Doublon, conflit d'identité, mémoire d'une heure | packages/ingest-buffer/src/scripts.ts : l'opération pushAccept ; packages/ingest-buffer/src/valkey.ts : DEFAULT_INGEST_TOMBSTONE_RETENTION_MS et l'empreinte de livraison dans push.accept |
| Vérification de type et d'horodatage par alerte | packages/sdk-authoring/src/data-sources/validator-from-columns.ts : columnSchema ; runtime.ts : le passage validateRow qui met une ligne de côté |