@operato/ops-contract 0.9.12 → 0.9.13
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/dist/webhook-signature.js +37 -0
- package/dist/webhook.d.ts +23 -2
- package/dist/webhook.js +38 -3
- package/dist-cjs/index.cjs +10 -1
- package/package.json +1 -1
|
@@ -44,6 +44,43 @@ export const WEBHOOK_TOLERANCE_MS = 5 * 60_000;
|
|
|
44
44
|
export function webhookSigningInput(timestamp, body) {
|
|
45
45
|
return `${timestamp}.${body}`;
|
|
46
46
|
}
|
|
47
|
+
/*
|
|
48
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
49
|
+
* What the signature does **not** cover — and the one rule that follows.
|
|
50
|
+
*
|
|
51
|
+
* ── The scope, stated plainly (2026-09-08) ─────────────────────────────────
|
|
52
|
+
* The signed string is `timestamp.body`. The **request path is not signed**, and neither is
|
|
53
|
+
* any header other than the timestamp. `x-ops-source` says who is sending, and it is signed
|
|
54
|
+
* by nothing at all — which is why it is declared for logging and never for identity.
|
|
55
|
+
*
|
|
56
|
+
* ── Why a peer segment in the path is still safe ──────────────────────────
|
|
57
|
+
* The receiving side has to know which key to verify with, and the sender's identity is the
|
|
58
|
+
* thing it needs in order to choose. Putting that in the path is sound, but not because the
|
|
59
|
+
* signature protects the path — it does not. It is sound because **the path only selects the
|
|
60
|
+
* key, and the key is what proves the identity.** A request signed with peer A's key, moved
|
|
61
|
+
* onto peer B's path, fails: it is verified against B's key and does not match.
|
|
62
|
+
*
|
|
63
|
+
* That distinction matters. "The signature covers the route" would license signing nothing
|
|
64
|
+
* and trusting the URL, and this scheme does not support that.
|
|
65
|
+
*
|
|
66
|
+
* ── The rule ──────────────────────────────────────────────────────────────
|
|
67
|
+
* **The path may only decide what the key already pins.**
|
|
68
|
+
*
|
|
69
|
+
* A per-peer path is meaningful exactly as long as two peers cannot resolve to the same
|
|
70
|
+
* verifying key. The moment they share one — most easily through a compatibility fallback
|
|
71
|
+
* that lands both on a domain-wide or installation-wide value — the path is deciding
|
|
72
|
+
* something the key does not, and peer A can place a validly signed request on peer B's
|
|
73
|
+
* path. Nothing rejects it, and the receiving side records it as B's.
|
|
74
|
+
*
|
|
75
|
+
* So a fallback ladder for continuity is fine on the rungs that keep the same pin (an older
|
|
76
|
+
* name for the same domain's key), and not fine on the rung that widens it (two peers of one
|
|
77
|
+
* domain sharing a value) — unless that domain has exactly one peer, which is a state that
|
|
78
|
+
* ends without warning the day a second one is added.
|
|
79
|
+
*
|
|
80
|
+
* The same rule is why the receiving tenant is not taken from the path alone: the key has to
|
|
81
|
+
* pin the tenant too, or a valid envelope can be redirected into another factory's journal.
|
|
82
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
+
*/
|
|
47
84
|
/** 서명을 만든다 — hex. */
|
|
48
85
|
export function signWebhook(secret, timestamp, body) {
|
|
49
86
|
return createHmac('sha256', secret).update(webhookSigningInput(timestamp, body)).digest('hex');
|
package/dist/webhook.d.ts
CHANGED
|
@@ -45,8 +45,29 @@ export type WebhookSenderAction = 'accepted' | 'partial' | 'resend-from' | 'wait
|
|
|
45
45
|
* 응답 코드 → 보내는 쪽의 행동. **모르는 코드도 답을 낸다.**
|
|
46
46
|
*
|
|
47
47
|
* 표에 없는 코드가 오는 일이 있다(프록시의 502, 게이트웨이의 504). 그때 「모른다」로 두면 보내는 쪽이
|
|
48
|
-
* 각자 정하고, 한 곳은 버리고 다른 곳은 되풀이한다.
|
|
49
|
-
*
|
|
48
|
+
* 각자 정하고, 한 곳은 버리고 다른 곳은 되풀이한다.
|
|
49
|
+
*
|
|
50
|
+
* ── 모르는 코드는 `retry` 다 (2026-09-08 실측으로 바꿨다) ────────────────────
|
|
51
|
+
* 이 함수는 `status >= 500 ? 'retry' : 'stop'` 이었다. 「4xx 는 보낸 것이 잘못이다」는 읽기였는데,
|
|
52
|
+
* **모르는 4xx 에는 그 읽기가 성립하지 않는다.**
|
|
53
|
+
*
|
|
54
|
+
* 실측: 트윈이 재기동하는 짧은 사이에 405 가 나갔고, 405 는 이 표에 없어서 `stop` 이 되었다.
|
|
55
|
+
* 보내는 쪽은 719 건을 `FAILED` 로 옮기고 다시 보내지 않았다. **문이 아직 안 붙은 상태였고 봉투는
|
|
56
|
+
* 멀쩡했다** — 몇 초 뒤에 보냈으면 다 들어갔다.
|
|
57
|
+
*
|
|
58
|
+
* 판단의 근거는 어느 쪽이 더 그럴듯하냐가 아니라 **틀렸을 때의 값**이다.
|
|
59
|
+
*
|
|
60
|
+
* ```
|
|
61
|
+
* 잘못 retry 했다 재시도 예산을 쓴다. 예산이 끝나면 FAILED 로 서고 사람이 본다
|
|
62
|
+
* 잘못 stop 했다 사실이 흐름에서 빠진다. 되보내는 문이 없으면 영구 손실
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* 모르는 4xx 중 여럿이 실제로 잠깐이다 — 408(타임아웃) · 425(너무 이름) · 405(문이 아직 없음).
|
|
66
|
+
* 그리고 `429` 를 이 표 밖에 따로 든 것이 같은 부류였다: 표에 없는데 `stop` 이면 안 되는 코드였고,
|
|
67
|
+
* 그래서 계약이 예외로 들었다. 하나씩 표에 더하는 것보다 **기본값을 안전한 쪽으로 두는 것**이 맞다.
|
|
68
|
+
*
|
|
69
|
+
* 멈추는 것은 표에 적힌 넷뿐이다(400·401·404·501). 그 넷은 「봉투가 틀렸다」·「자격이 틀렸다」·
|
|
70
|
+
* 「대상이 없다」·「그 커넥터가 밀어 받기를 모른다」이고, 다시 보내도 같다는 것이 **선언된 사실**이다.
|
|
50
71
|
*/
|
|
51
72
|
export declare function webhookSenderAction(status: number): WebhookSenderAction;
|
|
52
73
|
/** 429 를 함께 쓰는 곳이 있다 — 받는 쪽이 따라가지 못한다는 뜻이고, 행동은 503 과 같다. */
|
package/dist/webhook.js
CHANGED
|
@@ -49,8 +49,29 @@ export const WEBHOOK_STATUS = {
|
|
|
49
49
|
* 응답 코드 → 보내는 쪽의 행동. **모르는 코드도 답을 낸다.**
|
|
50
50
|
*
|
|
51
51
|
* 표에 없는 코드가 오는 일이 있다(프록시의 502, 게이트웨이의 504). 그때 「모른다」로 두면 보내는 쪽이
|
|
52
|
-
* 각자 정하고, 한 곳은 버리고 다른 곳은 되풀이한다.
|
|
53
|
-
*
|
|
52
|
+
* 각자 정하고, 한 곳은 버리고 다른 곳은 되풀이한다.
|
|
53
|
+
*
|
|
54
|
+
* ── 모르는 코드는 `retry` 다 (2026-09-08 실측으로 바꿨다) ────────────────────
|
|
55
|
+
* 이 함수는 `status >= 500 ? 'retry' : 'stop'` 이었다. 「4xx 는 보낸 것이 잘못이다」는 읽기였는데,
|
|
56
|
+
* **모르는 4xx 에는 그 읽기가 성립하지 않는다.**
|
|
57
|
+
*
|
|
58
|
+
* 실측: 트윈이 재기동하는 짧은 사이에 405 가 나갔고, 405 는 이 표에 없어서 `stop` 이 되었다.
|
|
59
|
+
* 보내는 쪽은 719 건을 `FAILED` 로 옮기고 다시 보내지 않았다. **문이 아직 안 붙은 상태였고 봉투는
|
|
60
|
+
* 멀쩡했다** — 몇 초 뒤에 보냈으면 다 들어갔다.
|
|
61
|
+
*
|
|
62
|
+
* 판단의 근거는 어느 쪽이 더 그럴듯하냐가 아니라 **틀렸을 때의 값**이다.
|
|
63
|
+
*
|
|
64
|
+
* ```
|
|
65
|
+
* 잘못 retry 했다 재시도 예산을 쓴다. 예산이 끝나면 FAILED 로 서고 사람이 본다
|
|
66
|
+
* 잘못 stop 했다 사실이 흐름에서 빠진다. 되보내는 문이 없으면 영구 손실
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* 모르는 4xx 중 여럿이 실제로 잠깐이다 — 408(타임아웃) · 425(너무 이름) · 405(문이 아직 없음).
|
|
70
|
+
* 그리고 `429` 를 이 표 밖에 따로 든 것이 같은 부류였다: 표에 없는데 `stop` 이면 안 되는 코드였고,
|
|
71
|
+
* 그래서 계약이 예외로 들었다. 하나씩 표에 더하는 것보다 **기본값을 안전한 쪽으로 두는 것**이 맞다.
|
|
72
|
+
*
|
|
73
|
+
* 멈추는 것은 표에 적힌 넷뿐이다(400·401·404·501). 그 넷은 「봉투가 틀렸다」·「자격이 틀렸다」·
|
|
74
|
+
* 「대상이 없다」·「그 커넥터가 밀어 받기를 모른다」이고, 다시 보내도 같다는 것이 **선언된 사실**이다.
|
|
54
75
|
*/
|
|
55
76
|
export function webhookSenderAction(status) {
|
|
56
77
|
switch (status) {
|
|
@@ -66,6 +87,15 @@ export function webhookSenderAction(status) {
|
|
|
66
87
|
return 'partial';
|
|
67
88
|
case WEBHOOK_STATUS.notLive:
|
|
68
89
|
return 'wait';
|
|
90
|
+
/*
|
|
91
|
+
* 429 — 「따라오지 못한다」. 표 밖이지만 `wait` 이어야 한다.
|
|
92
|
+
*
|
|
93
|
+
* 기본값이 `retry` 가 된 뒤에도 이것은 따로 든다. 곧바로 다시 보내면 그 상태를 더 악화시키고,
|
|
94
|
+
* 받는 쪽이 준 `retryAfterMs` 를 무시하게 된다. 기본값 변경이 이 예외를 삼키는 것을 시험이
|
|
95
|
+
* 잡았다(§`sender-action-unknown-status.test.ts`).
|
|
96
|
+
*/
|
|
97
|
+
case WEBHOOK_TOO_MANY:
|
|
98
|
+
return 'wait';
|
|
69
99
|
case WEBHOOK_STATUS.failed:
|
|
70
100
|
return 'retry';
|
|
71
101
|
case WEBHOOK_STATUS.badPayload:
|
|
@@ -74,7 +104,12 @@ export function webhookSenderAction(status) {
|
|
|
74
104
|
case WEBHOOK_STATUS.unsupported:
|
|
75
105
|
return 'stop';
|
|
76
106
|
default:
|
|
77
|
-
|
|
107
|
+
/*
|
|
108
|
+
* 모르는 코드는 사실을 들고 있는다. 5xx 는 받는 쪽 사정이라 처음부터 그랬고, 모르는 4xx 도
|
|
109
|
+
* 같은 쪽으로 둔다 — 그것이 잠깐인 경우가 실제로 있고(§ 위 머리말), 틀렸을 때 잃는 것이
|
|
110
|
+
* 재시도 예산뿐이다.
|
|
111
|
+
*/
|
|
112
|
+
return 'retry';
|
|
78
113
|
}
|
|
79
114
|
}
|
|
80
115
|
/** 429 를 함께 쓰는 곳이 있다 — 받는 쪽이 따라가지 못한다는 뜻이고, 행동은 503 과 같다. */
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -4053,6 +4053,15 @@ function webhookSenderAction(status) {
|
|
|
4053
4053
|
return "partial";
|
|
4054
4054
|
case WEBHOOK_STATUS.notLive:
|
|
4055
4055
|
return "wait";
|
|
4056
|
+
/*
|
|
4057
|
+
* 429 — 「따라오지 못한다」. 표 밖이지만 `wait` 이어야 한다.
|
|
4058
|
+
*
|
|
4059
|
+
* 기본값이 `retry` 가 된 뒤에도 이것은 따로 든다. 곧바로 다시 보내면 그 상태를 더 악화시키고,
|
|
4060
|
+
* 받는 쪽이 준 `retryAfterMs` 를 무시하게 된다. 기본값 변경이 이 예외를 삼키는 것을 시험이
|
|
4061
|
+
* 잡았다(§`sender-action-unknown-status.test.ts`).
|
|
4062
|
+
*/
|
|
4063
|
+
case WEBHOOK_TOO_MANY:
|
|
4064
|
+
return "wait";
|
|
4056
4065
|
case WEBHOOK_STATUS.failed:
|
|
4057
4066
|
return "retry";
|
|
4058
4067
|
case WEBHOOK_STATUS.badPayload:
|
|
@@ -4061,7 +4070,7 @@ function webhookSenderAction(status) {
|
|
|
4061
4070
|
case WEBHOOK_STATUS.unsupported:
|
|
4062
4071
|
return "stop";
|
|
4063
4072
|
default:
|
|
4064
|
-
return
|
|
4073
|
+
return "retry";
|
|
4065
4074
|
}
|
|
4066
4075
|
}
|
|
4067
4076
|
var WEBHOOK_TOO_MANY = 429;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "Operations domain contract — the standard vocabulary that producers and readers agree on (EPCIS 2.0/GS1, ISA-95, IEC 61850/ISO 50001). Types, guards, validation. No state, no engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist-cjs/index.cjs",
|