@operato/ops-contract 0.9.12 → 0.9.14
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/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/webhook-secret.d.ts +138 -0
- package/dist/webhook-secret.js +154 -0
- 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 +73 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './wms-profile.ts';
|
|
|
18
18
|
export * from './yms-profile.ts';
|
|
19
19
|
export * from './canonical-record.ts';
|
|
20
20
|
export * from './webhook.ts';
|
|
21
|
+
export * from './webhook-secret.ts';
|
|
21
22
|
export * from './oee.ts';
|
|
22
23
|
export * from './rated-usage.ts';
|
|
23
24
|
export * from './yield.ts';
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,8 @@ export * from "./yms-profile.js";
|
|
|
40
40
|
export * from "./canonical-record.js";
|
|
41
41
|
/* `webhook-signature.ts` 는 여기 없다 — `node:crypto` 를 쓰므로 `@operato/ops-contract/webhook` 으로만 나간다. */
|
|
42
42
|
export * from "./webhook.js";
|
|
43
|
+
/* 비밀값 **이름** 규약 — 값을 읽지 않으므로 여기서 나간다(§`webhook-secret`). */
|
|
44
|
+
export * from "./webhook-secret.js";
|
|
43
45
|
export * from "./oee.js";
|
|
44
46
|
export * from "./rated-usage.js";
|
|
45
47
|
export * from "./yield.js";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ① 이름 규약 — **두 벌로 나눈다.**
|
|
3
|
+
*
|
|
4
|
+
* 서명값은 대칭키다. 검증할 수 있는 쪽은 서명도 만들 수 있으므로, 한 벌로 두면 우리 사실을 검증하는
|
|
5
|
+
* 상대가 그 키로 우리에게 지시를 위조해 넣는다.
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* 나가는 쪽이 새면 남의 시스템에 거짓 사실이 앉는다
|
|
9
|
+
* 들어오는 쪽이 새면 남이 우리 공장에 지시를 꽂고, 사람이 그것으로 물건을 만든다
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 뒤엣것이 더 무겁다. 무게가 다르면 키도 달라야 한다.
|
|
13
|
+
*
|
|
14
|
+
* `OUTBOX`/`INTAKE` 가 아니라 `SIGNING`/`VERIFY` 인 이유: 앞엣것은 **우리 구조**의 이름이고 구조는
|
|
15
|
+
* 바뀐다. 서명하나 검증하나는 그 키가 무엇에 쓰이는지라 모듈이 바뀌어도 안 바뀐다.
|
|
16
|
+
*
|
|
17
|
+
* `MES_` 가 아닌 이유: 이 계약은 한 제품의 것이 아니다(§`WEBHOOK_HEADER` 와 같은 판단).
|
|
18
|
+
*/
|
|
19
|
+
export declare const WEBHOOK_SECRET_BASE: {
|
|
20
|
+
/** 우리가 서명할 때 — 나가는 쪽. */
|
|
21
|
+
readonly signing: "WEBHOOK_SIGNING_SECRET";
|
|
22
|
+
/** 우리가 검증할 때 — 들어오는 쪽. */
|
|
23
|
+
readonly verify: "WEBHOOK_VERIFY_SECRET";
|
|
24
|
+
};
|
|
25
|
+
export type WebhookSecretPurpose = keyof typeof WEBHOOK_SECRET_BASE;
|
|
26
|
+
/**
|
|
27
|
+
* ② 파생 규칙이 성립하려면 이름이 이 모양이어야 한다 — 소문자·숫자, 이음은 하이픈 하나.
|
|
28
|
+
*
|
|
29
|
+
* ── 왜 부호화하지 않고 입력을 좁혔나 ───────────────────────────────────────
|
|
30
|
+
* 환경 변수 이름에는 `[A-Za-z0-9_]` 만 쓸 수 있다. 그래서 도메인 이름을 그 집합으로 접어야 하는데,
|
|
31
|
+
* 접으면 **두 이름이 한 이름으로 눌린다** — plant 의 규칙이 `a-b` 와 `a_b` 를 둘 다 `A_B` 로 만들고
|
|
32
|
+
* 있었고, 눌리면 한 도메인이 다른 도메인의 키로 열린다. 그 파일은 그 사실을 주석으로 알고 있었다.
|
|
33
|
+
*
|
|
34
|
+
* 부호화(`_2D` 같은 것)로 풀 수도 있지만, 이 이름은 **사람이 비밀 관리 도구에 손으로 넣는 값**이다.
|
|
35
|
+
* `MIRATEK_2DGUNSAN` 을 읽고 쓰게 하면 그 자리에서 다른 실수가 난다.
|
|
36
|
+
*
|
|
37
|
+
* 그래서 접는 것을 없애는 대신 **접을 필요가 없는 입력만 받는다.** 밑줄이 없는 이름에서
|
|
38
|
+
* `-` → `_` 는 되돌릴 수 있고, 눌리지 않는다. 하이픈이 연달아 오지 못하게 한 것은 구분자
|
|
39
|
+
* `__` 가 그 자리에서만 나오게 하려는 것이다.
|
|
40
|
+
*
|
|
41
|
+
* 이 모양이 아닌 이름은 **거절한다.** 접어서 통과시키면 그 순간부터 눌림이 조용히 산다.
|
|
42
|
+
*/
|
|
43
|
+
export declare const WEBHOOK_SECRET_NAME_SHAPE: RegExp;
|
|
44
|
+
/** 그 조각이 파생에 쓸 수 있는 모양인가. */
|
|
45
|
+
export declare function isWebhookSecretSegment(segment: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* ② 파생 — `(base, domain, peer?)` → 환경 변수 이름.
|
|
48
|
+
*
|
|
49
|
+
* ```
|
|
50
|
+
* ('WEBHOOK_VERIFY_SECRET', 'miratek-gunsan', 'twin-a') → WEBHOOK_VERIFY_SECRET__MIRATEK_GUNSAN__TWIN_A
|
|
51
|
+
* ('WEBHOOK_SIGNING_SECRET', 'miratek-gunsan') → WEBHOOK_SIGNING_SECRET__MIRATEK_GUNSAN
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* **되돌릴 수 있다.** 조각에 밑줄이 없으니 `__` 는 구분자에서만 나오고, `_` 는 하이픈에서만 나온다.
|
|
55
|
+
* 그래서 두 쌍이 한 이름으로 눌릴 수 없다.
|
|
56
|
+
*
|
|
57
|
+
* 모양이 아닌 조각은 던진다 — 접어 통과시키면 눌림이 생기고, 눌림은 **한 공장이 다른 공장의 키로
|
|
58
|
+
* 열리는** 것이다. 이름을 정하는 자리에서 걸리는 것이 낫다.
|
|
59
|
+
*/
|
|
60
|
+
export declare function webhookSecretEnvName(base: string, domain: string, peer?: string): string;
|
|
61
|
+
/** 이 후보가 무엇까지 고정하나 — 사다리의 칸을 이 값으로 판단한다. */
|
|
62
|
+
export type WebhookSecretPin = 'peer' | 'domain' | 'installation';
|
|
63
|
+
export interface WebhookSecretCandidate {
|
|
64
|
+
/** 찾아볼 환경 변수 이름. */
|
|
65
|
+
name: string;
|
|
66
|
+
/** 이 이름이 무엇까지 고정하나. */
|
|
67
|
+
pins: WebhookSecretPin;
|
|
68
|
+
/** 배포된 설치본을 안 깨뜨리려고 남긴 칸인가 — 열리면 알려야 한다. */
|
|
69
|
+
compatibility: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface WebhookSecretLookup {
|
|
72
|
+
purpose: WebhookSecretPurpose;
|
|
73
|
+
/** 우리 테넌트의 subdomain. */
|
|
74
|
+
domain: string;
|
|
75
|
+
/** 상대의 이름 — **우리 이름이 아니다.** */
|
|
76
|
+
peer: string;
|
|
77
|
+
/**
|
|
78
|
+
* 옛 이름들 — 이 설치본이 예전에 쓰던 변수. 배포된 것을 안 깨뜨리려고 받는다.
|
|
79
|
+
*
|
|
80
|
+
* 옛 이름을 끊으면 그 이름으로만 돌아가는 설치본의 사실이 그 순간 멈춘다. 그래서 지우지 않고
|
|
81
|
+
* 받되, 열리면 부르는 쪽이 한 번 알린다 — 조용히 되면 아무도 새 이름으로 옮기지 않는다.
|
|
82
|
+
*/
|
|
83
|
+
legacyBases?: readonly string[];
|
|
84
|
+
/**
|
|
85
|
+
* **검증 쪽에서 넓은 칸까지 내려가는 것을 허락한다.** 기본은 거짓이고, 그것이 ③ 규칙이다.
|
|
86
|
+
*
|
|
87
|
+
* 이 인자를 참으로 주려면 부르는 쪽이 **그 도메인에 상대가 하나뿐임을 확인**해야 한다. 둘이면
|
|
88
|
+
* 둘이 같은 키로 풀리고, 그 순간 주소가 「이 사실이 누구 것인가」를 정하는데 아무도 그것을
|
|
89
|
+
* 검사하지 않는다 — 상대 A 가 제대로 서명한 봉투를 상대 B 의 주소에 앉힐 수 있다
|
|
90
|
+
* (§`webhook-signature` 의 불변식: 주소는 키가 이미 고정한 것만 정할 수 있다).
|
|
91
|
+
*
|
|
92
|
+
* 그리고 그 상태는 **두 번째 상대가 붙는 날 아무 경고 없이 끝난다.** 이름을 명시적으로 주게 한
|
|
93
|
+
* 이유가 그것이다 — 코드에서 찾을 수 있어야 한다.
|
|
94
|
+
*/
|
|
95
|
+
allowSinglePeerFallback?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* ③ 사다리 — 찾아볼 이름을 순서대로.
|
|
99
|
+
*
|
|
100
|
+
* ```
|
|
101
|
+
* 서명(나가는 쪽) 상대별 → 도메인별 → 옛 이름 우리가 상대를 알고 보낸다
|
|
102
|
+
* 검증(들어오는 쪽) 상대별 하나 넓은 칸으로 내려가지 않는다
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* **나누는 선은 「고정 범위가 넓어지는가」다.** 같은 것을 고정한 채 이름만 옛것인 칸은 내려가도
|
|
106
|
+
* 되고, 고정 범위가 넓어지는 칸은 안 된다. 검증 쪽에서 넓어지면 아무 상대나 통과한다.
|
|
107
|
+
*
|
|
108
|
+
* 검증 쪽이 못 찾으면 ④ 로 간다 — 거절한다. 「안 붙었다」와 「붙었는데 아무 키나 통한다」는 다른
|
|
109
|
+
* 상태이고, 뒤엣것은 사람이 볼 수 없다.
|
|
110
|
+
*/
|
|
111
|
+
export declare function webhookSecretCandidates(lookup: WebhookSecretLookup): WebhookSecretCandidate[];
|
|
112
|
+
/**
|
|
113
|
+
* ④ 없을 때 — **거절한다. 그리고 응답에 변수 이름을 넣지 않는다.**
|
|
114
|
+
*
|
|
115
|
+
* 거절 사유는 이미 있다(`WebhookSignatureFailure` 의 `'no-secret'`). 그 사유는 응답에 나가도 된다 —
|
|
116
|
+
* 보내는 쪽이 무엇을 고쳐야 하는지 알아야 한다.
|
|
117
|
+
*
|
|
118
|
+
* **변수 이름은 나가면 안 된다.** 자격을 못 낸 요청에 「이 이름의 변수를 채우면 열린다」고 답하는
|
|
119
|
+
* 것이고, 그 이름이 키 선택 방식과 **어떤 상대가 등록되어 있는지**를 함께 말한다.
|
|
120
|
+
*
|
|
121
|
+
* 그래서 이 문장은 **로그용**이다. 운영하는 사람은 프로세스 로그를 보고, 요청을 보낸 쪽은 못 본다.
|
|
122
|
+
*/
|
|
123
|
+
export declare function webhookSecretMissingNote(candidates: readonly WebhookSecretCandidate[]): string;
|
|
124
|
+
/**
|
|
125
|
+
* 사다리를 실제로 걸어 값을 찾는다 — 환경을 **인자로 받는다.**
|
|
126
|
+
*
|
|
127
|
+
* `process.env` 를 여기서 읽지 않는 이유는 머리말과 같다. 그리고 시험이 값을 손에 들고 걸을 수 있다.
|
|
128
|
+
*
|
|
129
|
+
* 돌려주는 것에 **어느 칸에서 열렸는지**가 있다. `compatibility` 가 참인 칸에서 열렸으면 부르는 쪽이
|
|
130
|
+
* 한 번 알린다 — 조용히 되면 아무도 새 이름으로 옮기지 않는다.
|
|
131
|
+
*/
|
|
132
|
+
export declare function webhookSecretFrom(env: Record<string, string | undefined>, lookup: WebhookSecretLookup): {
|
|
133
|
+
secret: string;
|
|
134
|
+
opened: WebhookSecretCandidate;
|
|
135
|
+
} | {
|
|
136
|
+
secret: undefined;
|
|
137
|
+
tried: WebhookSecretCandidate[];
|
|
138
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
* 어느 키가 어느 문을 여는가 — **이름 규약 · 파생 · 사다리 · 없을 때.**
|
|
4
|
+
*
|
|
5
|
+
* ── 왜 계약에 있나 (2026-09-08) ────────────────────────────────────────────
|
|
6
|
+
* 이 넷을 `operato-plant` 안에서 정하고 있었다(`server/service/domain-secret.ts`). 앱이 정하면
|
|
7
|
+
* `operato-wms` 가 붙는 날 그쪽이 또 적고, 셋이 **서로 다른 사다리**를 갖는다. 그러면 「이 설치본은
|
|
8
|
+
* 어느 키까지 내려가나」에 답할 자리가 없다.
|
|
9
|
+
*
|
|
10
|
+
* 그리고 이 넷은 서명과 검증이 **한 글자까지 같아야** 성립한다 — 서명 입력이 그런 것과 같은
|
|
11
|
+
* 이유로(§`webhook-signature`) 만드는 규칙과 찾는 규칙이 한 파일에 있어야 한다.
|
|
12
|
+
*
|
|
13
|
+
* ── 값은 여기 없다 ────────────────────────────────────────────────────────
|
|
14
|
+
* 이 파일은 **이름만** 만든다. `process.env` 를 읽지 않는다 — 그래야 규칙을 값 없이 시험할 수 있고,
|
|
15
|
+
* 이 패키지를 쓰는 브라우저 번들이 `node:process` 를 묶으려 하지 않는다. 읽는 것은 부르는 쪽이다.
|
|
16
|
+
*
|
|
17
|
+
* 비밀값은 표에 두지 않는다(§`WebhookDestination.params` 도 같은 규율). 데이터베이스·백업·화면
|
|
18
|
+
* 기록에 남고 한 번 새면 되돌릴 수 없다. **값은 환경에 있고, 어느 상대 것인지는 이름이 말한다.**
|
|
19
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* ① 이름 규약 — **두 벌로 나눈다.**
|
|
23
|
+
*
|
|
24
|
+
* 서명값은 대칭키다. 검증할 수 있는 쪽은 서명도 만들 수 있으므로, 한 벌로 두면 우리 사실을 검증하는
|
|
25
|
+
* 상대가 그 키로 우리에게 지시를 위조해 넣는다.
|
|
26
|
+
*
|
|
27
|
+
* ```
|
|
28
|
+
* 나가는 쪽이 새면 남의 시스템에 거짓 사실이 앉는다
|
|
29
|
+
* 들어오는 쪽이 새면 남이 우리 공장에 지시를 꽂고, 사람이 그것으로 물건을 만든다
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* 뒤엣것이 더 무겁다. 무게가 다르면 키도 달라야 한다.
|
|
33
|
+
*
|
|
34
|
+
* `OUTBOX`/`INTAKE` 가 아니라 `SIGNING`/`VERIFY` 인 이유: 앞엣것은 **우리 구조**의 이름이고 구조는
|
|
35
|
+
* 바뀐다. 서명하나 검증하나는 그 키가 무엇에 쓰이는지라 모듈이 바뀌어도 안 바뀐다.
|
|
36
|
+
*
|
|
37
|
+
* `MES_` 가 아닌 이유: 이 계약은 한 제품의 것이 아니다(§`WEBHOOK_HEADER` 와 같은 판단).
|
|
38
|
+
*/
|
|
39
|
+
export const WEBHOOK_SECRET_BASE = {
|
|
40
|
+
/** 우리가 서명할 때 — 나가는 쪽. */
|
|
41
|
+
signing: 'WEBHOOK_SIGNING_SECRET',
|
|
42
|
+
/** 우리가 검증할 때 — 들어오는 쪽. */
|
|
43
|
+
verify: 'WEBHOOK_VERIFY_SECRET'
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* ② 파생 규칙이 성립하려면 이름이 이 모양이어야 한다 — 소문자·숫자, 이음은 하이픈 하나.
|
|
47
|
+
*
|
|
48
|
+
* ── 왜 부호화하지 않고 입력을 좁혔나 ───────────────────────────────────────
|
|
49
|
+
* 환경 변수 이름에는 `[A-Za-z0-9_]` 만 쓸 수 있다. 그래서 도메인 이름을 그 집합으로 접어야 하는데,
|
|
50
|
+
* 접으면 **두 이름이 한 이름으로 눌린다** — plant 의 규칙이 `a-b` 와 `a_b` 를 둘 다 `A_B` 로 만들고
|
|
51
|
+
* 있었고, 눌리면 한 도메인이 다른 도메인의 키로 열린다. 그 파일은 그 사실을 주석으로 알고 있었다.
|
|
52
|
+
*
|
|
53
|
+
* 부호화(`_2D` 같은 것)로 풀 수도 있지만, 이 이름은 **사람이 비밀 관리 도구에 손으로 넣는 값**이다.
|
|
54
|
+
* `MIRATEK_2DGUNSAN` 을 읽고 쓰게 하면 그 자리에서 다른 실수가 난다.
|
|
55
|
+
*
|
|
56
|
+
* 그래서 접는 것을 없애는 대신 **접을 필요가 없는 입력만 받는다.** 밑줄이 없는 이름에서
|
|
57
|
+
* `-` → `_` 는 되돌릴 수 있고, 눌리지 않는다. 하이픈이 연달아 오지 못하게 한 것은 구분자
|
|
58
|
+
* `__` 가 그 자리에서만 나오게 하려는 것이다.
|
|
59
|
+
*
|
|
60
|
+
* 이 모양이 아닌 이름은 **거절한다.** 접어서 통과시키면 그 순간부터 눌림이 조용히 산다.
|
|
61
|
+
*/
|
|
62
|
+
export const WEBHOOK_SECRET_NAME_SHAPE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
63
|
+
/** 그 조각이 파생에 쓸 수 있는 모양인가. */
|
|
64
|
+
export function isWebhookSecretSegment(segment) {
|
|
65
|
+
return WEBHOOK_SECRET_NAME_SHAPE.test(segment);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* ② 파생 — `(base, domain, peer?)` → 환경 변수 이름.
|
|
69
|
+
*
|
|
70
|
+
* ```
|
|
71
|
+
* ('WEBHOOK_VERIFY_SECRET', 'miratek-gunsan', 'twin-a') → WEBHOOK_VERIFY_SECRET__MIRATEK_GUNSAN__TWIN_A
|
|
72
|
+
* ('WEBHOOK_SIGNING_SECRET', 'miratek-gunsan') → WEBHOOK_SIGNING_SECRET__MIRATEK_GUNSAN
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* **되돌릴 수 있다.** 조각에 밑줄이 없으니 `__` 는 구분자에서만 나오고, `_` 는 하이픈에서만 나온다.
|
|
76
|
+
* 그래서 두 쌍이 한 이름으로 눌릴 수 없다.
|
|
77
|
+
*
|
|
78
|
+
* 모양이 아닌 조각은 던진다 — 접어 통과시키면 눌림이 생기고, 눌림은 **한 공장이 다른 공장의 키로
|
|
79
|
+
* 열리는** 것이다. 이름을 정하는 자리에서 걸리는 것이 낫다.
|
|
80
|
+
*/
|
|
81
|
+
export function webhookSecretEnvName(base, domain, peer) {
|
|
82
|
+
const segments = peer === undefined ? [domain] : [domain, peer];
|
|
83
|
+
for (const segment of segments) {
|
|
84
|
+
if (!isWebhookSecretSegment(segment)) {
|
|
85
|
+
throw new Error(`webhookSecretEnvName: "${segment}" 는 비밀값 이름에 쓸 수 없다 — ` +
|
|
86
|
+
'소문자·숫자와 이음 하이픈만 쓴다(예: miratek-gunsan). ' +
|
|
87
|
+
'밑줄이나 대문자가 섞이면 두 이름이 한 환경 변수로 눌리고, 한 쪽이 다른 쪽의 키로 열린다.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [base, ...segments.map(s => s.toUpperCase().replace(/-/g, '_'))].join('__');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* ③ 사다리 — 찾아볼 이름을 순서대로.
|
|
94
|
+
*
|
|
95
|
+
* ```
|
|
96
|
+
* 서명(나가는 쪽) 상대별 → 도메인별 → 옛 이름 우리가 상대를 알고 보낸다
|
|
97
|
+
* 검증(들어오는 쪽) 상대별 하나 넓은 칸으로 내려가지 않는다
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* **나누는 선은 「고정 범위가 넓어지는가」다.** 같은 것을 고정한 채 이름만 옛것인 칸은 내려가도
|
|
101
|
+
* 되고, 고정 범위가 넓어지는 칸은 안 된다. 검증 쪽에서 넓어지면 아무 상대나 통과한다.
|
|
102
|
+
*
|
|
103
|
+
* 검증 쪽이 못 찾으면 ④ 로 간다 — 거절한다. 「안 붙었다」와 「붙었는데 아무 키나 통한다」는 다른
|
|
104
|
+
* 상태이고, 뒤엣것은 사람이 볼 수 없다.
|
|
105
|
+
*/
|
|
106
|
+
export function webhookSecretCandidates(lookup) {
|
|
107
|
+
const { purpose, domain, peer, legacyBases = [], allowSinglePeerFallback = false } = lookup;
|
|
108
|
+
const base = WEBHOOK_SECRET_BASE[purpose];
|
|
109
|
+
const out = [
|
|
110
|
+
{ name: webhookSecretEnvName(base, domain, peer), pins: 'peer', compatibility: false }
|
|
111
|
+
];
|
|
112
|
+
/* 검증은 여기서 멈춘다 — 확인한 사람이 명시적으로 열지 않는 한. */
|
|
113
|
+
if (purpose === 'verify' && !allowSinglePeerFallback)
|
|
114
|
+
return out;
|
|
115
|
+
out.push({ name: webhookSecretEnvName(base, domain), pins: 'domain', compatibility: purpose === 'verify' });
|
|
116
|
+
for (const legacy of legacyBases) {
|
|
117
|
+
out.push({ name: webhookSecretEnvName(legacy, domain), pins: 'domain', compatibility: true });
|
|
118
|
+
out.push({ name: legacy, pins: 'installation', compatibility: true });
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* ④ 없을 때 — **거절한다. 그리고 응답에 변수 이름을 넣지 않는다.**
|
|
124
|
+
*
|
|
125
|
+
* 거절 사유는 이미 있다(`WebhookSignatureFailure` 의 `'no-secret'`). 그 사유는 응답에 나가도 된다 —
|
|
126
|
+
* 보내는 쪽이 무엇을 고쳐야 하는지 알아야 한다.
|
|
127
|
+
*
|
|
128
|
+
* **변수 이름은 나가면 안 된다.** 자격을 못 낸 요청에 「이 이름의 변수를 채우면 열린다」고 답하는
|
|
129
|
+
* 것이고, 그 이름이 키 선택 방식과 **어떤 상대가 등록되어 있는지**를 함께 말한다.
|
|
130
|
+
*
|
|
131
|
+
* 그래서 이 문장은 **로그용**이다. 운영하는 사람은 프로세스 로그를 보고, 요청을 보낸 쪽은 못 본다.
|
|
132
|
+
*/
|
|
133
|
+
export function webhookSecretMissingNote(candidates) {
|
|
134
|
+
const names = candidates.map(c => c.name).join(' · ');
|
|
135
|
+
return (`비밀값이 없어 거절했다 — 찾아본 이름: ${names}. ` +
|
|
136
|
+
'이 문장은 로그에만 남긴다(응답에 변수 이름을 넣으면 자격 없는 요청에 키 선택 방식과 등록된 상대를 알려 준다).');
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 사다리를 실제로 걸어 값을 찾는다 — 환경을 **인자로 받는다.**
|
|
140
|
+
*
|
|
141
|
+
* `process.env` 를 여기서 읽지 않는 이유는 머리말과 같다. 그리고 시험이 값을 손에 들고 걸을 수 있다.
|
|
142
|
+
*
|
|
143
|
+
* 돌려주는 것에 **어느 칸에서 열렸는지**가 있다. `compatibility` 가 참인 칸에서 열렸으면 부르는 쪽이
|
|
144
|
+
* 한 번 알린다 — 조용히 되면 아무도 새 이름으로 옮기지 않는다.
|
|
145
|
+
*/
|
|
146
|
+
export function webhookSecretFrom(env, lookup) {
|
|
147
|
+
const candidates = webhookSecretCandidates(lookup);
|
|
148
|
+
for (const candidate of candidates) {
|
|
149
|
+
const value = env[candidate.name]?.trim();
|
|
150
|
+
if (value)
|
|
151
|
+
return { secret: value, opened: candidate };
|
|
152
|
+
}
|
|
153
|
+
return { secret: undefined, tried: candidates };
|
|
154
|
+
}
|
|
@@ -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
|
@@ -72,6 +72,8 @@ __export(index_exports, {
|
|
|
72
72
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
73
73
|
VOCABULARY_EXCEPTIONS: () => VOCABULARY_EXCEPTIONS,
|
|
74
74
|
VOCABULARY_TYPE: () => VOCABULARY_TYPE,
|
|
75
|
+
WEBHOOK_SECRET_BASE: () => WEBHOOK_SECRET_BASE,
|
|
76
|
+
WEBHOOK_SECRET_NAME_SHAPE: () => WEBHOOK_SECRET_NAME_SHAPE,
|
|
75
77
|
WEBHOOK_STATUS: () => WEBHOOK_STATUS,
|
|
76
78
|
WEBHOOK_TOO_MANY: () => WEBHOOK_TOO_MANY,
|
|
77
79
|
WMS_LOCATION_TYPES: () => WMS_LOCATION_TYPES,
|
|
@@ -157,6 +159,7 @@ __export(index_exports, {
|
|
|
157
159
|
isPlannedStopStatus: () => isPlannedStopStatus,
|
|
158
160
|
isResourceKind: () => isResourceKind,
|
|
159
161
|
isTransformationRecord: () => isTransformationRecord,
|
|
162
|
+
isWebhookSecretSegment: () => isWebhookSecretSegment,
|
|
160
163
|
isoDurationHours: () => isoDurationHours,
|
|
161
164
|
itemKeyOf: () => itemKeyOf,
|
|
162
165
|
judgeAgainstSpec: () => judgeAgainstSpec,
|
|
@@ -219,6 +222,10 @@ __export(index_exports, {
|
|
|
219
222
|
validateEpcisEvent: () => validateEpcisEvent,
|
|
220
223
|
validatePerformance: () => validatePerformance,
|
|
221
224
|
validateScenario: () => validateScenario,
|
|
225
|
+
webhookSecretCandidates: () => webhookSecretCandidates,
|
|
226
|
+
webhookSecretEnvName: () => webhookSecretEnvName,
|
|
227
|
+
webhookSecretFrom: () => webhookSecretFrom,
|
|
228
|
+
webhookSecretMissingNote: () => webhookSecretMissingNote,
|
|
222
229
|
webhookSenderAction: () => webhookSenderAction,
|
|
223
230
|
weekdayAt: () => weekdayAt,
|
|
224
231
|
workingHoursBetween: () => workingHoursBetween,
|
|
@@ -4053,6 +4060,15 @@ function webhookSenderAction(status) {
|
|
|
4053
4060
|
return "partial";
|
|
4054
4061
|
case WEBHOOK_STATUS.notLive:
|
|
4055
4062
|
return "wait";
|
|
4063
|
+
/*
|
|
4064
|
+
* 429 — 「따라오지 못한다」. 표 밖이지만 `wait` 이어야 한다.
|
|
4065
|
+
*
|
|
4066
|
+
* 기본값이 `retry` 가 된 뒤에도 이것은 따로 든다. 곧바로 다시 보내면 그 상태를 더 악화시키고,
|
|
4067
|
+
* 받는 쪽이 준 `retryAfterMs` 를 무시하게 된다. 기본값 변경이 이 예외를 삼키는 것을 시험이
|
|
4068
|
+
* 잡았다(§`sender-action-unknown-status.test.ts`).
|
|
4069
|
+
*/
|
|
4070
|
+
case WEBHOOK_TOO_MANY:
|
|
4071
|
+
return "wait";
|
|
4056
4072
|
case WEBHOOK_STATUS.failed:
|
|
4057
4073
|
return "retry";
|
|
4058
4074
|
case WEBHOOK_STATUS.badPayload:
|
|
@@ -4061,7 +4077,7 @@ function webhookSenderAction(status) {
|
|
|
4061
4077
|
case WEBHOOK_STATUS.unsupported:
|
|
4062
4078
|
return "stop";
|
|
4063
4079
|
default:
|
|
4064
|
-
return
|
|
4080
|
+
return "retry";
|
|
4065
4081
|
}
|
|
4066
4082
|
}
|
|
4067
4083
|
var WEBHOOK_TOO_MANY = 429;
|
|
@@ -4086,6 +4102,55 @@ function checkSequenceRun(lastSeq, seqs) {
|
|
|
4086
4102
|
return { accepted, lastSeq: cursor };
|
|
4087
4103
|
}
|
|
4088
4104
|
|
|
4105
|
+
// src/webhook-secret.ts
|
|
4106
|
+
var WEBHOOK_SECRET_BASE = {
|
|
4107
|
+
/** 우리가 서명할 때 — 나가는 쪽. */
|
|
4108
|
+
signing: "WEBHOOK_SIGNING_SECRET",
|
|
4109
|
+
/** 우리가 검증할 때 — 들어오는 쪽. */
|
|
4110
|
+
verify: "WEBHOOK_VERIFY_SECRET"
|
|
4111
|
+
};
|
|
4112
|
+
var WEBHOOK_SECRET_NAME_SHAPE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
4113
|
+
function isWebhookSecretSegment(segment) {
|
|
4114
|
+
return WEBHOOK_SECRET_NAME_SHAPE.test(segment);
|
|
4115
|
+
}
|
|
4116
|
+
function webhookSecretEnvName(base, domain, peer) {
|
|
4117
|
+
const segments = peer === void 0 ? [domain] : [domain, peer];
|
|
4118
|
+
for (const segment of segments) {
|
|
4119
|
+
if (!isWebhookSecretSegment(segment)) {
|
|
4120
|
+
throw new Error(
|
|
4121
|
+
`webhookSecretEnvName: "${segment}" \uB294 \uBE44\uBC00\uAC12 \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB2E4 \u2014 \uC18C\uBB38\uC790\xB7\uC22B\uC790\uC640 \uC774\uC74C \uD558\uC774\uD508\uB9CC \uC4F4\uB2E4(\uC608: miratek-gunsan). \uBC11\uC904\uC774\uB098 \uB300\uBB38\uC790\uAC00 \uC11E\uC774\uBA74 \uB450 \uC774\uB984\uC774 \uD55C \uD658\uACBD \uBCC0\uC218\uB85C \uB20C\uB9AC\uACE0, \uD55C \uCABD\uC774 \uB2E4\uB978 \uCABD\uC758 \uD0A4\uB85C \uC5F4\uB9B0\uB2E4.`
|
|
4122
|
+
);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
return [base, ...segments.map((s) => s.toUpperCase().replace(/-/g, "_"))].join("__");
|
|
4126
|
+
}
|
|
4127
|
+
function webhookSecretCandidates(lookup) {
|
|
4128
|
+
const { purpose, domain, peer, legacyBases = [], allowSinglePeerFallback = false } = lookup;
|
|
4129
|
+
const base = WEBHOOK_SECRET_BASE[purpose];
|
|
4130
|
+
const out = [
|
|
4131
|
+
{ name: webhookSecretEnvName(base, domain, peer), pins: "peer", compatibility: false }
|
|
4132
|
+
];
|
|
4133
|
+
if (purpose === "verify" && !allowSinglePeerFallback) return out;
|
|
4134
|
+
out.push({ name: webhookSecretEnvName(base, domain), pins: "domain", compatibility: purpose === "verify" });
|
|
4135
|
+
for (const legacy of legacyBases) {
|
|
4136
|
+
out.push({ name: webhookSecretEnvName(legacy, domain), pins: "domain", compatibility: true });
|
|
4137
|
+
out.push({ name: legacy, pins: "installation", compatibility: true });
|
|
4138
|
+
}
|
|
4139
|
+
return out;
|
|
4140
|
+
}
|
|
4141
|
+
function webhookSecretMissingNote(candidates) {
|
|
4142
|
+
const names = candidates.map((c) => c.name).join(" \xB7 ");
|
|
4143
|
+
return `\uBE44\uBC00\uAC12\uC774 \uC5C6\uC5B4 \uAC70\uC808\uD588\uB2E4 \u2014 \uCC3E\uC544\uBCF8 \uC774\uB984: ${names}. \uC774 \uBB38\uC7A5\uC740 \uB85C\uADF8\uC5D0\uB9CC \uB0A8\uAE34\uB2E4(\uC751\uB2F5\uC5D0 \uBCC0\uC218 \uC774\uB984\uC744 \uB123\uC73C\uBA74 \uC790\uACA9 \uC5C6\uB294 \uC694\uCCAD\uC5D0 \uD0A4 \uC120\uD0DD \uBC29\uC2DD\uACFC \uB4F1\uB85D\uB41C \uC0C1\uB300\uB97C \uC54C\uB824 \uC900\uB2E4).`;
|
|
4144
|
+
}
|
|
4145
|
+
function webhookSecretFrom(env, lookup) {
|
|
4146
|
+
const candidates = webhookSecretCandidates(lookup);
|
|
4147
|
+
for (const candidate of candidates) {
|
|
4148
|
+
const value = env[candidate.name]?.trim();
|
|
4149
|
+
if (value) return { secret: value, opened: candidate };
|
|
4150
|
+
}
|
|
4151
|
+
return { secret: void 0, tried: candidates };
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4089
4154
|
// src/oee.ts
|
|
4090
4155
|
function computeOee(c, nowMs) {
|
|
4091
4156
|
const missing = [];
|
|
@@ -4443,6 +4508,8 @@ function commandSpecGaps(specs, command) {
|
|
|
4443
4508
|
UTC_OFFSET,
|
|
4444
4509
|
VOCABULARY_EXCEPTIONS,
|
|
4445
4510
|
VOCABULARY_TYPE,
|
|
4511
|
+
WEBHOOK_SECRET_BASE,
|
|
4512
|
+
WEBHOOK_SECRET_NAME_SHAPE,
|
|
4446
4513
|
WEBHOOK_STATUS,
|
|
4447
4514
|
WEBHOOK_TOO_MANY,
|
|
4448
4515
|
WMS_LOCATION_TYPES,
|
|
@@ -4528,6 +4595,7 @@ function commandSpecGaps(specs, command) {
|
|
|
4528
4595
|
isPlannedStopStatus,
|
|
4529
4596
|
isResourceKind,
|
|
4530
4597
|
isTransformationRecord,
|
|
4598
|
+
isWebhookSecretSegment,
|
|
4531
4599
|
isoDurationHours,
|
|
4532
4600
|
itemKeyOf,
|
|
4533
4601
|
judgeAgainstSpec,
|
|
@@ -4590,6 +4658,10 @@ function commandSpecGaps(specs, command) {
|
|
|
4590
4658
|
validateEpcisEvent,
|
|
4591
4659
|
validatePerformance,
|
|
4592
4660
|
validateScenario,
|
|
4661
|
+
webhookSecretCandidates,
|
|
4662
|
+
webhookSecretEnvName,
|
|
4663
|
+
webhookSecretFrom,
|
|
4664
|
+
webhookSecretMissingNote,
|
|
4593
4665
|
webhookSenderAction,
|
|
4594
4666
|
weekdayAt,
|
|
4595
4667
|
workingHoursBetween,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/ops-contract",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
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",
|