@onlineapps/external-io-contract 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/package.json +43 -0
- package/schemas/callbacks/orion-grit.v1.schema.json +56 -0
- package/schemas/correlation-ref.schema.json +12 -0
- package/schemas/envelopes/delivery-result.v0.schema.json +38 -0
- package/schemas/envelopes/inbound-webhook-envelope.v0.schema.json +66 -0
- package/schemas/envelopes/outbound-envelope.v0.schema.json +60 -0
- package/src/correlation.js +136 -0
- package/src/hmac.js +83 -0
- package/src/index.js +29 -0
- package/src/validators.js +73 -0
- package/test-vectors/correlation.json +49 -0
- package/test-vectors/hmac.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @onlineapps/external-io-contract
|
|
2
|
+
|
|
3
|
+
Sdílený kontrakt pro HTTP komunikaci OA Drive s vnějším světem.
|
|
4
|
+
|
|
5
|
+
Zdrojem pravdy jsou **JSON Schemata v `schemas/`**; kód v `src/` je jen
|
|
6
|
+
jejich spustitelná podoba (validátory se kompilují při načtení modulu).
|
|
7
|
+
|
|
8
|
+
## Kdo to používá
|
|
9
|
+
|
|
10
|
+
| Konzument | K čemu |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `api_gateway` | ověření podpisu příchozích webhooků, rozbor correlation reference, validace obálky |
|
|
13
|
+
| `api_delivery_dispatcher` | podpis odchozích volání, validace outbound obálky a výsledku |
|
|
14
|
+
| `pdke_orion-grit` | podpis odchozích callbacků |
|
|
15
|
+
|
|
16
|
+
**Biz služby tuhle knihovnu nepoužívají.** Dostávají a vracejí prosté
|
|
17
|
+
objekty; transport, podpisy ani obálky neznají. Vlastní-li biz služba
|
|
18
|
+
nějaký typ correlation reference (např. `ing_job`), má nad toutéž
|
|
19
|
+
gramatikou vlastní tenkou vrstvu — bez runtime vazby na tuto knihovnu.
|
|
20
|
+
Kompatibilitu obou stran drží sdílené vektory v `test-vectors/`.
|
|
21
|
+
|
|
22
|
+
## Použití
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const { correlation, hmac, validators, SIGNATURE_HEADER } = require('@onlineapps/external-io-contract');
|
|
26
|
+
|
|
27
|
+
// odchozí: podepsat přesné bajty těla
|
|
28
|
+
const signature = hmac.sign(rawBody, secret); // "sha256=<hex>"
|
|
29
|
+
res.setHeader(SIGNATURE_HEADER, signature);
|
|
30
|
+
|
|
31
|
+
// příchozí: ověřit (false = odmítnout, nikdy výjimka)
|
|
32
|
+
if (!hmac.verify(req.rawBody, secret, req.get(SIGNATURE_HEADER))) return res.sendStatus(401);
|
|
33
|
+
|
|
34
|
+
// zjistit, komu callback patří
|
|
35
|
+
const jobUuid = correlation.refOfType(body.correlation_id, 'ing_job');
|
|
36
|
+
|
|
37
|
+
// zvalidovat tvar payloadu
|
|
38
|
+
const { valid, errors } = validators.orionGritCallbackV1(body);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Zásady, na kterých kód stojí
|
|
42
|
+
|
|
43
|
+
- **Podepisují se raw bajty**, nikdy znovu-serializovaný objekt. Kdo
|
|
44
|
+
ověřuje, musí mít původní tělo (`express.json({ verify })`), jinak se
|
|
45
|
+
podpis rozejde na pořadí klíčů nebo mezerách.
|
|
46
|
+
- **Ověření vrací `false`, nevyhazuje.** Nevalidní vstup od cizí strany
|
|
47
|
+
je očekávaný stav. Výjimka je vyhrazená programátorské chybě (špatný
|
|
48
|
+
typ těla, prázdný secret).
|
|
49
|
+
- **Secret sem chodí jako hodnota, ale do configu se zapisuje jako
|
|
50
|
+
reference.** Volající si ji vyzvedne ze SecretBoxu těsně před použitím.
|
|
51
|
+
- **Slovníky partnerů se sem nepromítají.** `status: "confirmed"` je
|
|
52
|
+
slovník orion-gritu; překlad na platformní `delivered|failed` dělá
|
|
53
|
+
cookbook, ne tato knihovna a ne biz služba.
|
|
54
|
+
|
|
55
|
+
## Formát podpisu (zmrazený)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
X-Signature: sha256=<lowercase hex>
|
|
59
|
+
HMAC-SHA256(raw body bytes, shared secret)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Jazykově neutrální — ověřeno proti nezávislé implementaci (`openssl
|
|
63
|
+
dgst -sha256 -hmac`, vektory v `test-vectors/hmac.json`) i proti PHP
|
|
64
|
+
`hash_hmac` na straně Meditestu. Změna schématu musí projít verzováním
|
|
65
|
+
prefixu, ne tichou úpravou.
|
|
66
|
+
|
|
67
|
+
## Correlation gramatika
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
<biz_ref_type>:<ref> např. ing_job:9a8b7c6d-1234-4567-89ab-cdef01234567
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Dělí se na **první** dvojtečce, takže reference smí další obsahovat.
|
|
74
|
+
Typ: `^[a-z][a-z0-9_]{1,31}$`. Reference:
|
|
75
|
+
`^[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$`.
|
|
76
|
+
|
|
77
|
+
Skládá ji biz služba (zná byznys význam), propaguje dispatcher, vrací
|
|
78
|
+
echem externí systém, rozebírá gateway při zjišťování tenanta.
|
|
79
|
+
|
|
80
|
+
## Schémata
|
|
81
|
+
|
|
82
|
+
| Soubor | Stav |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `schemas/correlation-ref.schema.json` | stabilní |
|
|
85
|
+
| `schemas/callbacks/orion-grit.v1.schema.json` | stabilní (odpovídá `buildPayload` v pdke_orion-grit) |
|
|
86
|
+
| `schemas/envelopes/outbound-envelope.v0.schema.json` | **draft** — stabilizuje se s OAuth2 adapterem |
|
|
87
|
+
| `schemas/envelopes/inbound-webhook-envelope.v0.schema.json` | **draft** — stabilizuje se s gateway receiverem |
|
|
88
|
+
| `schemas/envelopes/delivery-result.v0.schema.json` | **draft** |
|
|
89
|
+
|
|
90
|
+
Obálky nesou `envelope_version`. Evoluce je additive-only; nekompatibilní
|
|
91
|
+
změna dostane nové `v<N>` schéma, ne tichou úpravu stávajícího.
|
|
92
|
+
|
|
93
|
+
## Testy
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm test
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Kontext
|
|
100
|
+
|
|
101
|
+
- `api/docs/architecture/external-io-contract.md` — cílová architektura
|
|
102
|
+
- `api/docs/decisions/0001-external-io-through-platform-infra.md` — proč
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onlineapps/external-io-contract",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared contract for external HTTP I/O — correlation grammar, HMAC webhook signing, and JSON Schemas for outbound/inbound envelopes. Consumed by api_gateway, api_delivery_dispatcher and pdke_orion-grit.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./schemas/*": "./schemas/*"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:watch": "jest --watch",
|
|
13
|
+
"test:coverage": "jest --coverage"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"webhook",
|
|
17
|
+
"hmac",
|
|
18
|
+
"correlation",
|
|
19
|
+
"envelope",
|
|
20
|
+
"contract",
|
|
21
|
+
"external-io"
|
|
22
|
+
],
|
|
23
|
+
"author": "OnlineApps",
|
|
24
|
+
"license": "PROPRIETARY",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ajv": "^8.17.1",
|
|
27
|
+
"ajv-formats": "^3.0.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"jest": "^29.7.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"schemas",
|
|
38
|
+
"test-vectors"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://onlineapps.cz/schemas/external-io/callbacks/orion-grit.v1.schema.json",
|
|
4
|
+
"title": "pdke_orion-grit delivery callback (v1)",
|
|
5
|
+
"description": "Tělo webhooku, kterým pdke_orion-grit hlásí výsledek doručení zásilky. Odpovídá funkci buildPayload v pdke_orion-grit/src/lib/webhook/webhookSender.js. Podepsáno HMAC-SHA256 v hlavičce X-Signature. Pozn.: 'status' je slovník ORION-GRITU, ne platformy — na platformní 'delivered|failed' ho překládá cookbook input_mapping, nikdy biz služba.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["delivery_id", "client_id", "status", "occurred_at"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"delivery_id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"maxLength": 128,
|
|
14
|
+
"description": "Identifikátor zásilky přidělený orion-gritem"
|
|
15
|
+
},
|
|
16
|
+
"client_id": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 1,
|
|
19
|
+
"maxLength": 128,
|
|
20
|
+
"description": "Cílový klient v orion-gritu (např. 'phoenix')"
|
|
21
|
+
},
|
|
22
|
+
"doc_type": {
|
|
23
|
+
"type": ["string", "null"],
|
|
24
|
+
"description": "Typ dokumentu ve slovníku partnera (INVOIC, DESADV, ORDERS)"
|
|
25
|
+
},
|
|
26
|
+
"status": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["confirmed", "failed", "failed_timeout"],
|
|
29
|
+
"description": "Slovník orion-gritu. Překlad na platformní delivered|failed dělá cookbook."
|
|
30
|
+
},
|
|
31
|
+
"external_ref": {
|
|
32
|
+
"type": ["string", "null"],
|
|
33
|
+
"maxLength": 255,
|
|
34
|
+
"description": "Reference, kterou jsme poslali v odchozím volání (např. fak_id)"
|
|
35
|
+
},
|
|
36
|
+
"correlation_id": {
|
|
37
|
+
"type": ["string", "null"],
|
|
38
|
+
"pattern": "^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$",
|
|
39
|
+
"description": "Echo naší correlation reference; podle ní gateway zjistí tenanta"
|
|
40
|
+
},
|
|
41
|
+
"ack_ref": {
|
|
42
|
+
"type": ["string", "null"],
|
|
43
|
+
"maxLength": 255,
|
|
44
|
+
"description": "Jak bylo doručení potvrzeno (např. 'pickup:file_gone')"
|
|
45
|
+
},
|
|
46
|
+
"error": {
|
|
47
|
+
"type": ["string", "null"],
|
|
48
|
+
"description": "Text chyby při status != confirmed"
|
|
49
|
+
},
|
|
50
|
+
"occurred_at": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"format": "date-time",
|
|
53
|
+
"description": "Kdy událost nastala na straně orion-gritu (ISO 8601)"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://onlineapps.cz/schemas/external-io/correlation-ref.schema.json",
|
|
4
|
+
"title": "Correlation reference",
|
|
5
|
+
"description": "Vazba mezi odchozím voláním a příchozím callbackem. Tvar '<biz_ref_type>:<ref>'; dělí se na první dvojtečce, takže ref smí dvojtečku obsahovat. Skládá ji biz služba, vrací echem externí systém, rozebírá ji gateway při resolution tenanta.",
|
|
6
|
+
"type": "string",
|
|
7
|
+
"pattern": "^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$",
|
|
8
|
+
"examples": [
|
|
9
|
+
"ing_job:9a8b7c6d-1234-4567-89ab-cdef01234567",
|
|
10
|
+
"invoice:INV-2026-00042"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://onlineapps.cz/schemas/external-io/envelopes/delivery-result.v0.schema.json",
|
|
4
|
+
"title": "Delivery result (v0 draft)",
|
|
5
|
+
"description": "Výsledek jednoho pokusu o doručení; dispatcher ho publikuje do fronty delivery.result pro audit a monitoring. DRAFT — stabilizuje se s OAuth2 adapterem (ADR-0001 Track B).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["envelope_version", "workflow_id", "destination_name", "status"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"envelope_version": { "type": "string", "const": "0" },
|
|
11
|
+
"workflow_id": { "type": "string", "minLength": 1 },
|
|
12
|
+
"destination_name": { "type": "string" },
|
|
13
|
+
"destination_type": { "type": "string" },
|
|
14
|
+
"status": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["success", "failed", "retrying", "dead_letter", "skipped"]
|
|
17
|
+
},
|
|
18
|
+
"http_status": { "type": ["integer", "null"] },
|
|
19
|
+
"attempts_made": { "type": "integer", "minimum": 0 },
|
|
20
|
+
"first_attempt_at": { "type": ["string", "null"], "format": "date-time" },
|
|
21
|
+
"last_attempt_at": { "type": ["string", "null"], "format": "date-time" },
|
|
22
|
+
"next_retry_at": { "type": ["string", "null"], "format": "date-time" },
|
|
23
|
+
"external_reference": {
|
|
24
|
+
"type": ["string", "null"],
|
|
25
|
+
"description": "Identifikátor, který partner vrátil v odpovědi (např. delivery_id)"
|
|
26
|
+
},
|
|
27
|
+
"error": { "type": ["string", "null"] },
|
|
28
|
+
"expects_callback": {
|
|
29
|
+
"type": ["object", "null"],
|
|
30
|
+
"description": "Vyplněno, pokud tato destination očekává asynchronní callback",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"properties": {
|
|
33
|
+
"provider_slug": { "type": "string" },
|
|
34
|
+
"correlation_id_sent": { "type": ["string", "null"] }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://onlineapps.cz/schemas/external-io/envelopes/inbound-webhook-envelope.v0.schema.json",
|
|
4
|
+
"title": "Inbound webhook envelope (v0 draft)",
|
|
5
|
+
"description": "Co gateway sestaví z ověřeného příchozího webhooku předtím, než z něj složí vstup workflow. DRAFT — stabilizuje se implementací gateway webhook receiveru (ADR-0001 Track C). Pozn.: samostatná fronta 'webhook.received' se nestaví; gateway publikuje standardní workflow start, tato obálka je vnitřní mezistupeň a podklad pro audit.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["envelope_version", "provider_slug", "received_at", "correlation"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"envelope_version": { "type": "string", "const": "0" },
|
|
11
|
+
"webhook_received_id": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "UUID přidělené gateway pro audit trail"
|
|
14
|
+
},
|
|
15
|
+
"provider_slug": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-z][a-z0-9-]{1,63}$",
|
|
18
|
+
"description": "Který provider config zprávu ověřil"
|
|
19
|
+
},
|
|
20
|
+
"provider_message_id": {
|
|
21
|
+
"type": ["string", "null"],
|
|
22
|
+
"description": "Identifikátor zprávy u partnera; klíč pro deduplikaci"
|
|
23
|
+
},
|
|
24
|
+
"received_at": { "type": "string", "format": "date-time" },
|
|
25
|
+
"correlation": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["correlation_id", "tenant_id"],
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"properties": {
|
|
30
|
+
"correlation_id": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$"
|
|
33
|
+
},
|
|
34
|
+
"tenant_id": {
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"description": "Bez resolvovaného tenanta se nesmí publikovat nic — jinak by šlo o neautorizovaný tok dat"
|
|
37
|
+
},
|
|
38
|
+
"workspace_id": { "type": ["integer", "null"] },
|
|
39
|
+
"resolved_reference": {
|
|
40
|
+
"type": ["object", "null"],
|
|
41
|
+
"description": "Co lookup našel (tabulka + uuid), pro audit",
|
|
42
|
+
"properties": {
|
|
43
|
+
"table": { "type": "string" },
|
|
44
|
+
"uuid": { "type": "string" }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"cookbook_trigger": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["cookbook_slug", "input"],
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"properties": {
|
|
54
|
+
"cookbook_slug": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^[a-z][a-z0-9-]{1,63}$",
|
|
57
|
+
"description": "Název souboru cookbooku (bez přípony) v api/config/cookbooks/"
|
|
58
|
+
},
|
|
59
|
+
"input": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"description": "Vstup workflow sestavený podle input_mapping v provider configu; už v platformním slovníku"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://onlineapps.cz/schemas/external-io/envelopes/outbound-envelope.v0.schema.json",
|
|
4
|
+
"title": "Outbound delivery envelope (v0 draft)",
|
|
5
|
+
"description": "Normalizovaný tvar, který dispatcher předá destination adapteru. DRAFT — stabilizuje se implementací OAuth2 adapteru (ADR-0001 Track B). Auth se sem NIKDY nedostane v otevřené podobě; adapter si hodnotu vyzvedne ze SecretBoxu podle reference.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["envelope_version", "workflow_id", "destination", "body"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"envelope_version": { "type": "string", "const": "0" },
|
|
11
|
+
"workflow_id": { "type": "string", "minLength": 1 },
|
|
12
|
+
"correlation_id": {
|
|
13
|
+
"type": ["string", "null"],
|
|
14
|
+
"pattern": "^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$"
|
|
15
|
+
},
|
|
16
|
+
"destination": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["type", "url"],
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"properties": {
|
|
21
|
+
"type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Klíč adapteru v DestinationFactory (webhook, webhook.oauth2_client_credentials, …)"
|
|
24
|
+
},
|
|
25
|
+
"name": { "type": "string" },
|
|
26
|
+
"url": { "type": "string", "format": "uri" },
|
|
27
|
+
"method": { "type": "string", "enum": ["POST", "PUT"], "default": "POST" },
|
|
28
|
+
"timeout_ms": { "type": "integer", "minimum": 1 },
|
|
29
|
+
"idempotency_key": {
|
|
30
|
+
"type": ["string", "null"],
|
|
31
|
+
"description": "Zpravidla workflow_id — retry nesmí u partnera zdvojit efekt"
|
|
32
|
+
},
|
|
33
|
+
"headers": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"description": "Už vyhodnocené hlavičky (bez auth — tu doplní adapter)",
|
|
36
|
+
"additionalProperties": { "type": "string" }
|
|
37
|
+
},
|
|
38
|
+
"auth_ref": {
|
|
39
|
+
"type": ["object", "null"],
|
|
40
|
+
"description": "Reference na tajemství, nikdy hodnota",
|
|
41
|
+
"additionalProperties": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"body": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"description": "Tělo vyhodnocené z body_template v cookbooku"
|
|
48
|
+
},
|
|
49
|
+
"audit_context": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"properties": {
|
|
53
|
+
"tenant_id": { "type": ["integer", "null"] },
|
|
54
|
+
"workspace_id": { "type": ["integer", "null"] },
|
|
55
|
+
"cookbook_name": { "type": ["string", "null"] },
|
|
56
|
+
"dispatched_at": { "type": "string", "format": "date-time" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Correlation reference — gramatika `<biz_ref_type>:<ref>`.
|
|
5
|
+
*
|
|
6
|
+
* Correlation id je jediná vazba mezi odchozím voláním a příchozím
|
|
7
|
+
* callbackem. Skládá ho biz služba (zná byznys význam), propaguje
|
|
8
|
+
* dispatcher, vrací echem externí systém a rozebírá gateway, aby zjistil
|
|
9
|
+
* tenanta.
|
|
10
|
+
*
|
|
11
|
+
* Tento modul je ZÁMĚRNĚ generický — nezná žádný konkrétní typ reference
|
|
12
|
+
* (`ing_job`, `invoice`, …). Znalost typu patří té biz službě, která ho
|
|
13
|
+
* vlastní; ta má vlastní tenkou vrstvu nad touto gramatikou. Kompatibilitu
|
|
14
|
+
* obou stran drží sdílené vektory v `test-vectors/correlation.json`.
|
|
15
|
+
*
|
|
16
|
+
* @module src/correlation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Typ byznys entity: lowercase snake_case, 2..32 znaků. */
|
|
20
|
+
const REF_TYPE_RE = /^[a-z][a-z0-9_]{1,31}$/;
|
|
21
|
+
|
|
22
|
+
/** Referenční hodnota: bezpečné znaky, 1..128; dvojtečka smí být uvnitř. */
|
|
23
|
+
const REF_VALUE_RE = /^[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$/;
|
|
24
|
+
|
|
25
|
+
/** Celý correlation id — pro použití v JSON Schema `pattern`. */
|
|
26
|
+
const CORRELATION_RE = /^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9._-][A-Za-z0-9._:-]{0,127}$/;
|
|
27
|
+
|
|
28
|
+
class CorrelationError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'CorrelationError';
|
|
32
|
+
this.code = 'INVALID_CORRELATION_REF';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sestaví correlation id.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} refType typ byznys entity, např. 'ing_job'
|
|
40
|
+
* @param {string} ref identifikátor v rámci typu
|
|
41
|
+
* @returns {string} `<refType>:<ref>`
|
|
42
|
+
* @throws {CorrelationError}
|
|
43
|
+
*/
|
|
44
|
+
function build(refType, ref) {
|
|
45
|
+
if (typeof refType !== 'string' || !REF_TYPE_RE.test(refType)) {
|
|
46
|
+
throw new CorrelationError(
|
|
47
|
+
`[external-io-contract/correlation] refType must match ${REF_TYPE_RE} - got ${JSON.stringify(refType)}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (typeof ref !== 'string' || !REF_VALUE_RE.test(ref)) {
|
|
51
|
+
throw new CorrelationError(
|
|
52
|
+
`[external-io-contract/correlation] ref must match ${REF_VALUE_RE} - got ${JSON.stringify(ref)}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return `${refType}:${ref}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rozebere correlation id na složky. Dělí se na PRVNÍ dvojtečce, takže
|
|
60
|
+
* `ing_job:a:b` → typ `ing_job`, ref `a:b`.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} correlationId
|
|
63
|
+
* @returns {{ refType: string, ref: string }}
|
|
64
|
+
* @throws {CorrelationError}
|
|
65
|
+
*/
|
|
66
|
+
function parse(correlationId) {
|
|
67
|
+
if (typeof correlationId !== 'string' || correlationId.length === 0) {
|
|
68
|
+
throw new CorrelationError(
|
|
69
|
+
'[external-io-contract/correlation] correlation_id must be a non-empty string'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const sep = correlationId.indexOf(':');
|
|
73
|
+
if (sep <= 0 || sep === correlationId.length - 1) {
|
|
74
|
+
throw new CorrelationError(
|
|
75
|
+
`[external-io-contract/correlation] correlation_id must be "<biz_ref_type>:<ref>" - got ${JSON.stringify(correlationId)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const refType = correlationId.slice(0, sep);
|
|
79
|
+
const ref = correlationId.slice(sep + 1);
|
|
80
|
+
if (!REF_TYPE_RE.test(refType)) {
|
|
81
|
+
throw new CorrelationError(
|
|
82
|
+
`[external-io-contract/correlation] refType part must match ${REF_TYPE_RE} - got ${JSON.stringify(refType)}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!REF_VALUE_RE.test(ref)) {
|
|
86
|
+
throw new CorrelationError(
|
|
87
|
+
`[external-io-contract/correlation] ref part must match ${REF_VALUE_RE} - got ${JSON.stringify(ref)}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return { refType, ref };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Nevýjimková varianta `parse` — pro validační cesty, kde je nevalidní
|
|
95
|
+
* vstup očekávaný stav (příchozí webhook od cizí strany), ne chyba kódu.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} correlationId
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isValid(correlationId) {
|
|
101
|
+
return typeof correlationId === 'string' && CORRELATION_RE.test(correlationId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Vytáhne referenci, jen pokud je očekávaného typu. Callback pro cizí
|
|
106
|
+
* bounded context (`invoice:…` tam, kde se čeká `ing_job:…`) tak nikdy
|
|
107
|
+
* neprojde omylem.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} correlationId
|
|
110
|
+
* @param {string} expectedRefType
|
|
111
|
+
* @returns {string} samotná reference
|
|
112
|
+
* @throws {CorrelationError}
|
|
113
|
+
*/
|
|
114
|
+
function refOfType(correlationId, expectedRefType) {
|
|
115
|
+
const { refType, ref } = parse(correlationId);
|
|
116
|
+
if (refType !== expectedRefType) {
|
|
117
|
+
throw new CorrelationError(
|
|
118
|
+
`[external-io-contract/correlation] expected refType "${expectedRefType}" - got "${refType}" ` +
|
|
119
|
+
'(correlation_id belongs to another bounded context)'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return ref;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
build,
|
|
127
|
+
parse,
|
|
128
|
+
isValid,
|
|
129
|
+
refOfType,
|
|
130
|
+
CorrelationError,
|
|
131
|
+
patterns: Object.freeze({
|
|
132
|
+
REF_TYPE_RE,
|
|
133
|
+
REF_VALUE_RE,
|
|
134
|
+
CORRELATION_RE
|
|
135
|
+
})
|
|
136
|
+
};
|
package/src/hmac.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook payload signing — HMAC-SHA256 nad raw tělem requestu.
|
|
5
|
+
*
|
|
6
|
+
* Schéma (zmrazené, jazykově neutrální):
|
|
7
|
+
* hlavička `X-Signature: sha256=<lowercase hex>`
|
|
8
|
+
* hodnota = HMAC-SHA256(raw body bytes, shared secret)
|
|
9
|
+
*
|
|
10
|
+
* Podepisuje se VŽDY přesně ten bajtový obsah, který jde po drátě —
|
|
11
|
+
* nikdy znovu-serializovaný objekt. Kdo ověřuje, musí mít raw tělo
|
|
12
|
+
* (v Expressu: `express.json({ verify })` nebo `express.raw`), jinak
|
|
13
|
+
* se podpis rozejde na pořadí klíčů či mezerách.
|
|
14
|
+
*
|
|
15
|
+
* @module src/hmac
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { createHmac, timingSafeEqual } = require('node:crypto');
|
|
19
|
+
|
|
20
|
+
/** Název hlavičky, ve které podpis cestuje. */
|
|
21
|
+
const SIGNATURE_HEADER = 'X-Signature';
|
|
22
|
+
|
|
23
|
+
/** Prefix hodnoty hlavičky — drží prostor pro budoucí algoritmy. */
|
|
24
|
+
const SIGNATURE_PREFIX = 'sha256=';
|
|
25
|
+
|
|
26
|
+
function assertBody(rawBody) {
|
|
27
|
+
if (typeof rawBody !== 'string' && !Buffer.isBuffer(rawBody)) {
|
|
28
|
+
throw new TypeError(
|
|
29
|
+
'[external-io-contract/hmac] rawBody must be a string or Buffer - ' +
|
|
30
|
+
'sign and verify the exact bytes sent over the wire, never a re-serialized object'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertSecret(secret) {
|
|
36
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
37
|
+
throw new TypeError('[external-io-contract/hmac] secret must be a non-empty string');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Spočítá hodnotu podpisové hlavičky.
|
|
43
|
+
*
|
|
44
|
+
* @param {string|Buffer} rawBody přesné bajty těla requestu
|
|
45
|
+
* @param {string} secret sdílené tajemství (z SecretBoxu, nikdy z configu)
|
|
46
|
+
* @returns {string} `sha256=<hex>` — hodnota pro hlavičku X-Signature
|
|
47
|
+
*/
|
|
48
|
+
function sign(rawBody, secret) {
|
|
49
|
+
assertBody(rawBody);
|
|
50
|
+
assertSecret(secret);
|
|
51
|
+
const digest = createHmac('sha256', secret)
|
|
52
|
+
.update(Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8'))
|
|
53
|
+
.digest('hex');
|
|
54
|
+
return `${SIGNATURE_PREFIX}${digest}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ověří podpis proti raw tělu. Konstantní v čase vůči obsahu podpisu.
|
|
59
|
+
*
|
|
60
|
+
* Vrací `false` místo výjimky pro každý případ „nesedí to" (chybí
|
|
61
|
+
* hlavička, jiný prefix, jiná délka, jiná hodnota) — volající tak má
|
|
62
|
+
* jednu větev pro odmítnutí. Výjimka je jen pro programátorskou chybu
|
|
63
|
+
* (nesmyslný typ těla nebo prázdný secret).
|
|
64
|
+
*
|
|
65
|
+
* @param {string|Buffer} rawBody přesné bajty přijatého těla
|
|
66
|
+
* @param {string} secret sdílené tajemství
|
|
67
|
+
* @param {string} headerValue obsah hlavičky X-Signature
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function verify(rawBody, secret, headerValue) {
|
|
71
|
+
assertBody(rawBody);
|
|
72
|
+
assertSecret(secret);
|
|
73
|
+
if (typeof headerValue !== 'string' || !headerValue.startsWith(SIGNATURE_PREFIX)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const expected = Buffer.from(sign(rawBody, secret), 'utf8');
|
|
77
|
+
const received = Buffer.from(headerValue, 'utf8');
|
|
78
|
+
// timingSafeEqual vyžaduje shodnou délku; rozdílná délka = nesedí
|
|
79
|
+
if (expected.length !== received.length) return false;
|
|
80
|
+
return timingSafeEqual(expected, received);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { sign, verify, SIGNATURE_HEADER, SIGNATURE_PREFIX };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @onlineapps/external-io-contract
|
|
5
|
+
*
|
|
6
|
+
* Sdílený kontrakt pro HTTP komunikaci s vnějším světem. Používají ho:
|
|
7
|
+
* - api_gateway — ověření podpisu příchozích webhooků,
|
|
8
|
+
* rozbor correlation reference
|
|
9
|
+
* - api_delivery_dispatcher — podpis odchozích volání, validace obálek
|
|
10
|
+
* - pdke_orion-grit — podpis odchozích callbacků
|
|
11
|
+
*
|
|
12
|
+
* Zdroj pravdy jsou JSON Schemata v `schemas/`; kód je jen jejich
|
|
13
|
+
* spustitelná podoba. Biz služby tuhle knihovnu NEPOUŽÍVAJÍ — dostávají
|
|
14
|
+
* a vracejí prosté objekty, transport neznají.
|
|
15
|
+
*
|
|
16
|
+
* @module @onlineapps/external-io-contract
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const correlation = require('./correlation');
|
|
20
|
+
const hmac = require('./hmac');
|
|
21
|
+
const { validators, schemas } = require('./validators');
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
correlation,
|
|
25
|
+
hmac,
|
|
26
|
+
validators,
|
|
27
|
+
schemas,
|
|
28
|
+
SIGNATURE_HEADER: hmac.SIGNATURE_HEADER
|
|
29
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validátory kompilované ze schémat při načtení modulu.
|
|
5
|
+
*
|
|
6
|
+
* Schémata v `schemas/` jsou zdroj pravdy; tenhle modul je jen jejich
|
|
7
|
+
* spustitelná podoba. Kompilace probíhá jednou za život procesu.
|
|
8
|
+
*
|
|
9
|
+
* @module src/validators
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const Ajv = require('ajv');
|
|
13
|
+
const addFormats = require('ajv-formats');
|
|
14
|
+
|
|
15
|
+
const correlationRefSchema = require('../schemas/correlation-ref.schema.json');
|
|
16
|
+
const orionGritCallbackV1 = require('../schemas/callbacks/orion-grit.v1.schema.json');
|
|
17
|
+
const outboundEnvelopeV0 = require('../schemas/envelopes/outbound-envelope.v0.schema.json');
|
|
18
|
+
const inboundWebhookEnvelopeV0 = require('../schemas/envelopes/inbound-webhook-envelope.v0.schema.json');
|
|
19
|
+
const deliveryResultV0 = require('../schemas/envelopes/delivery-result.v0.schema.json');
|
|
20
|
+
|
|
21
|
+
const schemas = Object.freeze({
|
|
22
|
+
correlationRef: correlationRefSchema,
|
|
23
|
+
orionGritCallbackV1,
|
|
24
|
+
outboundEnvelopeV0,
|
|
25
|
+
inboundWebhookEnvelopeV0,
|
|
26
|
+
deliveryResultV0
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
30
|
+
addFormats(ajv);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Jedna chyba validace v čitelné podobě.
|
|
34
|
+
* @typedef {{ path: string, message: string }} ValidationIssue
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Výsledek validace. `valid: false` NENÍ výjimka — nevalidní vstup od
|
|
39
|
+
* externí strany je očekávaný stav, ne chyba kódu.
|
|
40
|
+
* @typedef {{ valid: boolean, errors: ValidationIssue[] }} ValidationResult
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
function toIssues(ajvErrors) {
|
|
44
|
+
if (!ajvErrors) return [];
|
|
45
|
+
return ajvErrors.map((e) => ({
|
|
46
|
+
path: e.instancePath === '' ? '/' : e.instancePath,
|
|
47
|
+
message: e.message || 'invalid'
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compile(schema, label) {
|
|
52
|
+
const validateFn = ajv.compile(schema);
|
|
53
|
+
/**
|
|
54
|
+
* @param {*} data
|
|
55
|
+
* @returns {ValidationResult}
|
|
56
|
+
*/
|
|
57
|
+
return function validate(data) {
|
|
58
|
+
const ok = validateFn(data);
|
|
59
|
+
return ok
|
|
60
|
+
? { valid: true, errors: [] }
|
|
61
|
+
: { valid: false, errors: toIssues(validateFn.errors), schema: label };
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const validators = Object.freeze({
|
|
66
|
+
correlationRef: compile(correlationRefSchema, 'correlation-ref'),
|
|
67
|
+
orionGritCallbackV1: compile(orionGritCallbackV1, 'callbacks/orion-grit.v1'),
|
|
68
|
+
outboundEnvelopeV0: compile(outboundEnvelopeV0, 'envelopes/outbound-envelope.v0'),
|
|
69
|
+
inboundWebhookEnvelopeV0: compile(inboundWebhookEnvelopeV0, 'envelopes/inbound-webhook-envelope.v0'),
|
|
70
|
+
deliveryResultV0: compile(deliveryResultV0, 'envelopes/delivery-result.v0')
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
module.exports = { validators, schemas };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Sdílené vektory gramatiky correlation reference. Tuto knihovnu používá gateway; biz služby mají vlastní tenkou implementaci nad toutéž gramatikou (např. api_biz/ingest/src/lib/correlation.js). Runtime závislost mezi nimi NENÍ — kompatibilitu drží právě tyto vektory. Když se změní gramatika, musí se změnit tady i tam, jinak testy padnou.",
|
|
3
|
+
"valid": [
|
|
4
|
+
{
|
|
5
|
+
"input": "ing_job:9a8b7c6d-1234-4567-89ab-cdef01234567",
|
|
6
|
+
"refType": "ing_job",
|
|
7
|
+
"ref": "9a8b7c6d-1234-4567-89ab-cdef01234567",
|
|
8
|
+
"note": "typický případ — biz-ingest job"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"input": "invoice:INV-2026-00042",
|
|
12
|
+
"refType": "invoice",
|
|
13
|
+
"ref": "INV-2026-00042",
|
|
14
|
+
"note": "jiný bounded context, ref není uuid"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"input": "ing_job:a:b:c",
|
|
18
|
+
"refType": "ing_job",
|
|
19
|
+
"ref": "a:b:c",
|
|
20
|
+
"note": "dělí se na PRVNÍ dvojtečce, ref smí další obsahovat"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"input": "raw_doc:abc",
|
|
24
|
+
"refType": "raw_doc",
|
|
25
|
+
"ref": "abc",
|
|
26
|
+
"note": "snake_case v typu"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"input": "ing_job:ABC.def_ghi-123",
|
|
30
|
+
"refType": "ing_job",
|
|
31
|
+
"ref": "ABC.def_ghi-123",
|
|
32
|
+
"note": "v referenci jsou povolené tečka, podtržítko, pomlčka a velká písmena"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"invalid": [
|
|
36
|
+
{ "input": "IngJob:abc", "reason": "velká písmena v typu" },
|
|
37
|
+
{ "input": "1job:abc", "reason": "typ začíná číslicí" },
|
|
38
|
+
{ "input": "_job:abc", "reason": "typ začíná podtržítkem" },
|
|
39
|
+
{ "input": "ing-job:abc", "reason": "pomlčka v typu" },
|
|
40
|
+
{ "input": "i:abc", "reason": "typ kratší než 2 znaky" },
|
|
41
|
+
{ "input": "ing_job", "reason": "chybí dvojtečka" },
|
|
42
|
+
{ "input": ":abc", "reason": "prázdný typ" },
|
|
43
|
+
{ "input": "ing_job:", "reason": "prázdná reference" },
|
|
44
|
+
{ "input": "ing_job::abc", "reason": "reference začíná dvojtečkou" },
|
|
45
|
+
{ "input": "ing_job:a b", "reason": "mezera v referenci" },
|
|
46
|
+
{ "input": "ing_job:../etc/passwd", "reason": "lomítko v referenci (path traversal)" },
|
|
47
|
+
{ "input": "", "reason": "prázdný řetězec" }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Known-good HMAC-SHA256 vectors generated by an INDEPENDENT implementation (openssl dgst -sha256 -hmac) on 2026-08-19. They freeze the wire format: any change to the signing scheme breaks these tests on purpose. Regenerate only when the scheme is intentionally versioned. Command: printf '%s' \"<body>\" | openssl dgst -sha256 -hmac \"<secret>\"",
|
|
3
|
+
"algorithm": "HMAC-SHA256",
|
|
4
|
+
"header": "X-Signature",
|
|
5
|
+
"value_format": "sha256=<lowercase hex>",
|
|
6
|
+
"vectors": [
|
|
7
|
+
{
|
|
8
|
+
"name": "typical orion-grit callback body",
|
|
9
|
+
"secret": "test-secret-1234567890abcdef",
|
|
10
|
+
"body": "{\"delivery_id\":\"9a8b7c6d\",\"status\":\"confirmed\"}",
|
|
11
|
+
"expected_hex": "e249e8f33480153865e9969d148442d2911520c28da114ebf462f862606aa6bb"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "empty JSON object",
|
|
15
|
+
"secret": "another-secret-key-for-vectors",
|
|
16
|
+
"body": "{}",
|
|
17
|
+
"expected_hex": "b98e92437622a66fce33026d4407af51ce15b34ca7b2e4847336650fe284b58e"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "utf8 in both secret and body (diacritics must be signed as utf8 bytes)",
|
|
21
|
+
"secret": "utf8-secret-ěščřž",
|
|
22
|
+
"body": "{\"note\":\"diakritika ěščřžýáíé\"}",
|
|
23
|
+
"expected_hex": "ef802847967892dc388df4eddbcea26f37478bc82e11852d005dfc2c9e620c05"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|