@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.2
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/AUTHORING.md +134 -0
- package/CHANGELOG.md +61 -0
- package/README.md +21 -9
- package/SUBMISSION.md +10 -11
- package/bin/apifuse-pack-check.ts +22 -0
- package/bin/apifuse-submit-check.ts +1747 -388
- package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.js +7 -31
- package/dist/cli/create.js +45 -30
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/contract.js +1 -0
- package/dist/define.d.ts +6 -1
- package/dist/define.js +140 -70
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +27 -0
- package/dist/provider.d.ts +4 -2
- package/dist/provider.js +2 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +203 -0
- package/dist/runtime/http.js +46 -336
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/stealth.d.ts +5 -2
- package/dist/runtime/stealth.js +206 -206
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +98 -0
- package/dist/server/self-test.js +555 -0
- package/dist/server/serve.d.ts +6 -0
- package/dist/server/serve.js +38 -10
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +1 -0
- package/dist/testing/run.js +5 -1
- package/dist/types.d.ts +150 -0
- package/package.json +9 -2
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +9 -43
- package/src/cli/create.ts +60 -97
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +198 -71
- package/src/index.ts +16 -0
- package/src/lint.ts +33 -0
- package/src/provider.ts +27 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/browser.ts +293 -1
- package/src/runtime/http.ts +84 -530
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/stealth.ts +269 -353
- package/src/server/index.ts +36 -0
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +725 -0
- package/src/server/serve.ts +75 -6
- package/src/server/types.ts +1 -0
- package/src/testing/run.ts +9 -1
- package/src/types.ts +186 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: normalization-standards
|
|
3
|
+
description: Public output contract rules — field naming, timestamps, enums, units, nullability. Load before writing or editing any output schema or mapper.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Normalization standards
|
|
7
|
+
|
|
8
|
+
Public output is the product. Apply these to every schema + mapper pair.
|
|
9
|
+
|
|
10
|
+
## Field naming
|
|
11
|
+
- `snake_case`, English, semantic. `emergency_phone`, not `dutyTel3`.
|
|
12
|
+
- Never expose vendor key vocabularies (`hv1`..`hv12`, `MKioskTy*`, `duty*`)
|
|
13
|
+
as public field names OR as dynamic record keys. A
|
|
14
|
+
`z.record(z.string(), ...)` keyed by vendor codes is still a vendor leak —
|
|
15
|
+
map codes to a stable public vocabulary (enum keys or an array of
|
|
16
|
+
`{ code, label, status }` objects with normalized status).
|
|
17
|
+
|
|
18
|
+
## Timestamps and dates
|
|
19
|
+
- Public: ISO 8601 (`2026-07-07T22:28:55+09:00`, dates `2026-07-07`,
|
|
20
|
+
clock times `HH:MM`). Include the upstream's timezone offset; Korean public
|
|
21
|
+
APIs are KST (+09:00) — verify, then encode it.
|
|
22
|
+
- Vendor formats (`YYYYMMDDHHmmss`, `HHmm`, unpadded `900`) are parsed inside
|
|
23
|
+
the mapper. Unparseable → `null`, plus a test for that row shape.
|
|
24
|
+
- Never emit a raw vendor timestamp string in public output, including
|
|
25
|
+
fixtures.
|
|
26
|
+
|
|
27
|
+
## Enums
|
|
28
|
+
- Vendor status values (codes, `Y`/`N`, Korean labels like `불가능`,
|
|
29
|
+
`정보미제공`) → declared `z.enum`. Unknown value → explicit `unknown` member
|
|
30
|
+
or fail closed; never pass raw text through.
|
|
31
|
+
- Map from the OFFICIAL code table, not from guessing what live samples mean.
|
|
32
|
+
Add a regression test per enum member.
|
|
33
|
+
|
|
34
|
+
## Numbers and units
|
|
35
|
+
- Field name states the unit: `distance_meters`, `radius_meters`,
|
|
36
|
+
`price_krw`. Mapper proves the conversion (upstream km → `* 1000`).
|
|
37
|
+
- Verify the upstream unit from spec or live-data sanity check (a "distance"
|
|
38
|
+
of `1.2` from a nearby search is km, not meters). Sibling endpoints of the
|
|
39
|
+
same vendor may differ — verify each one.
|
|
40
|
+
- Value-domain constraints (`nonnegative`, `min`, `max`) must reflect the
|
|
41
|
+
upstream's REAL domain observed in live data, not what seems sensible.
|
|
42
|
+
A wrong `nonnegative()` turns real negative values into `null`/errors
|
|
43
|
+
silently.
|
|
44
|
+
|
|
45
|
+
## Nullability
|
|
46
|
+
- `null` means "upstream did not provide it" — never "parsing failed" and
|
|
47
|
+
never a placeholder for invented data.
|
|
48
|
+
- Required-for-identity fields (ids, names) missing → throw
|
|
49
|
+
`UPSTREAM_SCHEMA_ERROR`; do not emit partial rows.
|
|
50
|
+
|
|
51
|
+
## Checklist before submitting a schema/mapper change
|
|
52
|
+
- [ ] No vendor key visible in any public field name or record key
|
|
53
|
+
- [ ] All timestamps ISO 8601 with timezone; parsing tested for real vendor
|
|
54
|
+
shapes (padded/unpadded, string/number)
|
|
55
|
+
- [ ] All status-like strings are declared enums with official-table mapping
|
|
56
|
+
- [ ] Every numeric field's unit is in its name and conversion is tested
|
|
57
|
+
- [ ] Constraints checked against live data, not intuition
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pagination-and-counts
|
|
3
|
+
description: total_count semantics, client-side filtering, and paging honesty. Load before implementing any list/search operation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pagination and counts
|
|
7
|
+
|
|
8
|
+
A caller uses `total_count`, `page`, and `limit` to plan iteration. If those
|
|
9
|
+
numbers don't describe what the caller can actually page through, the
|
|
10
|
+
operation is lying.
|
|
11
|
+
|
|
12
|
+
## The client-side filtering trap
|
|
13
|
+
If the upstream has no server-side filter for one of your inputs (e.g. no
|
|
14
|
+
radius param) and you filter rows after fetching:
|
|
15
|
+
|
|
16
|
+
- Upstream `totalCount` counts UNFILTERED rows. Returning it as your
|
|
17
|
+
`total_count` while returning filtered rows means: caller sees
|
|
18
|
+
`total_count: 541`, gets 1 row on page 1, and pages 2..28 return rows that
|
|
19
|
+
are outside the filter or empty. This is a contract failure, not a nuance.
|
|
20
|
+
|
|
21
|
+
Acceptable resolutions, in preference order:
|
|
22
|
+
1. **Don't accept the input.** If the upstream can't filter by it and you
|
|
23
|
+
can't enumerate all pages, drop the input from the schema and document the
|
|
24
|
+
upstream's real semantics (e.g. "results are distance-sorted; no radius
|
|
25
|
+
cutoff").
|
|
26
|
+
2. **Expose upstream semantics honestly.** Distance-sorted paging with a
|
|
27
|
+
documented "no radius filter" contract and no fake `radius` input.
|
|
28
|
+
3. **Filter AND fix the metadata.** If you must filter client-side, do not
|
|
29
|
+
return the upstream total. Return only what you can prove (`returned_count`
|
|
30
|
+
plus a `has_more` you can actually compute) and document that totals are
|
|
31
|
+
unavailable.
|
|
32
|
+
|
|
33
|
+
Never combine: accepted filter input + client-side filter + upstream total.
|
|
34
|
+
|
|
35
|
+
## Count integrity
|
|
36
|
+
- Parse failure of `totalCount` → `UPSTREAM_SCHEMA_ERROR`, not `0`.
|
|
37
|
+
A fail-open zero disguises upstream drift as an empty dataset.
|
|
38
|
+
- If `total_count > 0` but the page's row array normalizes to empty on
|
|
39
|
+
page 1, throw — that combination means broken extraction, not empty data.
|
|
40
|
+
|
|
41
|
+
## Page/limit echo
|
|
42
|
+
- Echo the EFFECTIVE values: if you clamp `limit` to the upstream max, return
|
|
43
|
+
the clamped value, not the requested one.
|
|
44
|
+
- `page`/`limit` semantics must match the upstream's paging model
|
|
45
|
+
(1-indexed vs 0-indexed) — verify with two live pages, checking the
|
|
46
|
+
returned rows actually differ.
|
|
47
|
+
|
|
48
|
+
## Checklist
|
|
49
|
+
- [ ] No input is filtered client-side while `total_count` comes from upstream
|
|
50
|
+
- [ ] totalCount parse failure fails closed
|
|
51
|
+
- [ ] Effective (clamped) limit echoed
|
|
52
|
+
- [ ] Two-page live check proves paging advances
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: upstream-contract-verification
|
|
3
|
+
description: How to establish evidence for upstream request params and response fields before coding. Load before wiring any new endpoint or mapping new fields.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Upstream contract verification
|
|
7
|
+
|
|
8
|
+
Most provider P0s come from guessed upstream contracts. Every param name and
|
|
9
|
+
response field needs evidence BEFORE it ships.
|
|
10
|
+
|
|
11
|
+
## Request parameters
|
|
12
|
+
1. Start from the official spec document (data.go.kr 활용가이드, vendor API
|
|
13
|
+
docs). Copy exact names — casing and underscores matter
|
|
14
|
+
(`WGS84_LAT` ≠ `WGS84LAT`; the wrong one is often silently ignored).
|
|
15
|
+
2. Confirm with ONE live call per endpoint. A param being ignored does not
|
|
16
|
+
produce an error — it produces plausible-looking wrong results, so compare:
|
|
17
|
+
- filtered vs unfiltered `totalCount` (identical → param ignored)
|
|
18
|
+
- a dense-area query returning 0 rows (→ param name/format wrong)
|
|
19
|
+
3. Do NOT copy param names from a sibling endpoint or sibling API of the same
|
|
20
|
+
vendor without re-verifying. Same vendor ≠ same contract; endpoints drift.
|
|
21
|
+
4. If a param only works together with another param (district requires
|
|
22
|
+
province), encode that dependency in the input schema with a clear error.
|
|
23
|
+
Test it: dependent-param-alone must be rejected, not silently national.
|
|
24
|
+
|
|
25
|
+
## Response fields
|
|
26
|
+
- Map exactly the field names present in your recorded live fixtures.
|
|
27
|
+
- No speculative fallback chains (`row.distance ?? row.dist ?? row.Distance`).
|
|
28
|
+
If two shapes genuinely exist, you need a recorded fixture proving EACH
|
|
29
|
+
branch plus a row-level test per branch; otherwise map one name only.
|
|
30
|
+
- Field presence varies by endpoint within the same vendor. Detail endpoints
|
|
31
|
+
often return more/differently-named fields than list endpoints — record
|
|
32
|
+
fixtures per endpoint, not per vendor.
|
|
33
|
+
|
|
34
|
+
## When results look wrong
|
|
35
|
+
- Same response body across different request payloads → the upstream is
|
|
36
|
+
ignoring your variation; stop tuning fields and re-check param names/auth.
|
|
37
|
+
- Empty result for a query that must have data (city-center radius search,
|
|
38
|
+
major-district listing) → treat as a request bug. Never record it as a
|
|
39
|
+
fixture and never ship it.
|
|
40
|
+
|
|
41
|
+
## Deliverables per endpoint
|
|
42
|
+
- [ ] Spec reference (URL or doc name + section) noted in the PR/commit
|
|
43
|
+
- [ ] One recorded live fixture proving request params take effect
|
|
44
|
+
- [ ] Negative evidence checked: filtered count differs from unfiltered
|
|
45
|
+
- [ ] Param dependencies enforced in the input schema with tests
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Upstream notes
|
|
2
|
+
|
|
3
|
+
Per-vendor / per-API-family pitfalls proven by live evidence. These are the
|
|
4
|
+
highest-value files in this workspace: general principles are knowable, but
|
|
5
|
+
"this API silently ignores param X" is only discoverable by getting burned.
|
|
6
|
+
|
|
7
|
+
- Read EVERY file here before your first upstream call.
|
|
8
|
+
- When you discover a new upstream quirk (silently ignored param, unit
|
|
9
|
+
surprise, undocumented value domain, error-shape oddity), ADD it here in
|
|
10
|
+
the same format — evidence line included. Reviewers treat contributed
|
|
11
|
+
upstream notes as part of submission quality.
|
|
12
|
+
|
|
13
|
+
Format per entry: **Symptom → Cause → Rule → Evidence**.
|
package/src/contract.ts
CHANGED
|
@@ -170,6 +170,7 @@ function extractHealthCheck(
|
|
|
170
170
|
if (!value) return undefined;
|
|
171
171
|
return compactObject({
|
|
172
172
|
interval: value.interval,
|
|
173
|
+
schedule: toJsonValue(value.schedule),
|
|
173
174
|
timeoutMs: value.timeoutMs,
|
|
174
175
|
degradedThresholdMs: value.degradedThresholdMs,
|
|
175
176
|
requiresConnection: value.requiresConnection,
|
package/src/define.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
|
|
1
3
|
import { ProviderError, ValidationError } from "./errors";
|
|
2
4
|
import { safeParseSchemaSync } from "./schema";
|
|
3
5
|
import type {
|
|
@@ -10,6 +12,7 @@ import type {
|
|
|
10
12
|
HealthCheckUnsupported,
|
|
11
13
|
HealthJourneyDefinition,
|
|
12
14
|
HealthJourneySchedule,
|
|
15
|
+
HealthScheduleRandomization,
|
|
13
16
|
InferSchemaOutput,
|
|
14
17
|
OperationDefinition,
|
|
15
18
|
OperationHandlerResult,
|
|
@@ -118,49 +121,25 @@ const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
|
118
121
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
119
122
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
120
123
|
|
|
121
|
-
const MS_DURATION_UNITS = new Set([
|
|
122
|
-
"years",
|
|
123
|
-
"year",
|
|
124
|
-
"yrs",
|
|
125
|
-
"yr",
|
|
126
|
-
"y",
|
|
127
|
-
"weeks",
|
|
128
|
-
"week",
|
|
129
|
-
"w",
|
|
130
|
-
"days",
|
|
131
|
-
"day",
|
|
132
|
-
"d",
|
|
133
|
-
"hours",
|
|
134
|
-
"hour",
|
|
135
|
-
"hrs",
|
|
136
|
-
"hr",
|
|
137
|
-
"h",
|
|
138
|
-
"minutes",
|
|
139
|
-
"minute",
|
|
140
|
-
"mins",
|
|
141
|
-
"min",
|
|
142
|
-
"m",
|
|
143
|
-
"seconds",
|
|
144
|
-
"second",
|
|
145
|
-
"secs",
|
|
146
|
-
"sec",
|
|
147
|
-
"s",
|
|
148
|
-
"milliseconds",
|
|
149
|
-
"millisecond",
|
|
150
|
-
"msecs",
|
|
151
|
-
"msec",
|
|
152
|
-
"ms",
|
|
153
|
-
]);
|
|
154
124
|
const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
|
|
155
125
|
|
|
156
126
|
function isPositiveMsDurationString(value: unknown): value is string {
|
|
157
127
|
if (typeof value !== "string") return false;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
128
|
+
return parsePositiveMsDuration(value) !== undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function msDurationMs(value: string): number {
|
|
132
|
+
return parsePositiveMsDuration(value) ?? 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parsePositiveMsDuration(value: string): number | undefined {
|
|
136
|
+
const trimmed = value.trim();
|
|
137
|
+
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
138
|
+
const parsed = ms(
|
|
139
|
+
(trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
|
|
140
|
+
);
|
|
141
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
142
|
+
return parsed;
|
|
164
143
|
}
|
|
165
144
|
|
|
166
145
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
@@ -285,6 +264,8 @@ export interface ProviderConfig<
|
|
|
285
264
|
};
|
|
286
265
|
operations: OperationMapConfig<TOperations>;
|
|
287
266
|
healthMonitor?: ProviderHealthMonitorConfig;
|
|
267
|
+
/** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
|
|
268
|
+
healthProbe?: ProviderHealthMonitorConfig;
|
|
288
269
|
healthJourneys?: readonly HealthJourneyDefinition[];
|
|
289
270
|
}
|
|
290
271
|
|
|
@@ -374,6 +355,14 @@ function validateProviderShape(config: unknown): void {
|
|
|
374
355
|
VALID_AUTH_MODES,
|
|
375
356
|
String(config.id),
|
|
376
357
|
);
|
|
358
|
+
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
359
|
+
throw new ProviderError(
|
|
360
|
+
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
361
|
+
{
|
|
362
|
+
fix: "Use the single canonical auth interface: auth.flow. Gateway calls auth.flow.start/continue/poll/abort/refresh only and persists complete turn data.credential as-is, so put login/token/session exchange inside auth.flow.continue.",
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
}
|
|
377
366
|
if (
|
|
378
367
|
auth &&
|
|
379
368
|
typeof auth === "object" &&
|
|
@@ -1116,6 +1105,7 @@ function validateOperationTransports(
|
|
|
1116
1105
|
|
|
1117
1106
|
const HEALTH_CHECK_SUITE_FIELDS = new Set([
|
|
1118
1107
|
"interval",
|
|
1108
|
+
"schedule",
|
|
1119
1109
|
"timeoutMs",
|
|
1120
1110
|
"degradedThresholdMs",
|
|
1121
1111
|
"cases",
|
|
@@ -1225,6 +1215,7 @@ function assertBoundedIntegerMs(
|
|
|
1225
1215
|
function validateProviderHealthMonitor(
|
|
1226
1216
|
providerId: string,
|
|
1227
1217
|
healthMonitor: unknown,
|
|
1218
|
+
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1228
1219
|
): void {
|
|
1229
1220
|
if (healthMonitor === undefined) return;
|
|
1230
1221
|
if (
|
|
@@ -1233,21 +1224,21 @@ function validateProviderHealthMonitor(
|
|
|
1233
1224
|
Array.isArray(healthMonitor)
|
|
1234
1225
|
)
|
|
1235
1226
|
throw new ValidationError(
|
|
1236
|
-
`Provider "${providerId}" has invalid
|
|
1227
|
+
`Provider "${providerId}" has invalid ${field}: must be an object.`,
|
|
1237
1228
|
{
|
|
1238
|
-
fix: `Set
|
|
1229
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1239
1230
|
},
|
|
1240
1231
|
);
|
|
1241
1232
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1242
1233
|
rejectUnknownFields(
|
|
1243
1234
|
healthMonitorRecord,
|
|
1244
1235
|
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1245
|
-
|
|
1236
|
+
field,
|
|
1246
1237
|
);
|
|
1247
1238
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1248
1239
|
assertBoundedIntegerMs(
|
|
1249
1240
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
1250
|
-
`Provider "${providerId}"
|
|
1241
|
+
`Provider "${providerId}" ${field}.defaultProbeTimeoutMs`,
|
|
1251
1242
|
{
|
|
1252
1243
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1253
1244
|
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
@@ -1258,7 +1249,7 @@ function validateProviderHealthMonitor(
|
|
|
1258
1249
|
if (healthMonitorRecord.defaultDegradedThresholdMs !== undefined) {
|
|
1259
1250
|
assertBoundedIntegerMs(
|
|
1260
1251
|
healthMonitorRecord.defaultDegradedThresholdMs,
|
|
1261
|
-
`Provider "${providerId}"
|
|
1252
|
+
`Provider "${providerId}" ${field}.defaultDegradedThresholdMs`,
|
|
1262
1253
|
{
|
|
1263
1254
|
min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
1264
1255
|
max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
@@ -1270,12 +1261,12 @@ function validateProviderHealthMonitor(
|
|
|
1270
1261
|
if (requiredSecrets !== undefined) {
|
|
1271
1262
|
if (!Array.isArray(requiredSecrets))
|
|
1272
1263
|
throw new ValidationError(
|
|
1273
|
-
`Provider "${providerId}" has invalid
|
|
1264
|
+
`Provider "${providerId}" has invalid ${field}.requiredSecrets: must be string[].`,
|
|
1274
1265
|
);
|
|
1275
1266
|
for (const [index, secret] of requiredSecrets.entries()) {
|
|
1276
1267
|
if (typeof secret !== "string" || secret.length === 0)
|
|
1277
1268
|
throw new ValidationError(
|
|
1278
|
-
`Provider "${providerId}" has invalid
|
|
1269
|
+
`Provider "${providerId}" has invalid ${field}.requiredSecrets[${index}]: must be a non-empty string.`,
|
|
1279
1270
|
);
|
|
1280
1271
|
}
|
|
1281
1272
|
}
|
|
@@ -1287,23 +1278,23 @@ function validateProviderHealthMonitor(
|
|
|
1287
1278
|
Array.isArray(credentialInputs)
|
|
1288
1279
|
) {
|
|
1289
1280
|
throw new ValidationError(
|
|
1290
|
-
`Provider "${providerId}" has invalid
|
|
1281
|
+
`Provider "${providerId}" has invalid ${field}.credentialInputs: must be an object mapping auth input fields to env var names.`,
|
|
1291
1282
|
);
|
|
1292
1283
|
}
|
|
1293
|
-
for (const [
|
|
1294
|
-
if (
|
|
1284
|
+
for (const [inputField, envVar] of Object.entries(credentialInputs)) {
|
|
1285
|
+
if (inputField.trim().length === 0) {
|
|
1295
1286
|
throw new ValidationError(
|
|
1296
|
-
`Provider "${providerId}" has invalid
|
|
1287
|
+
`Provider "${providerId}" has invalid ${field}.credentialInputs key: must be a non-empty auth input field.`,
|
|
1297
1288
|
);
|
|
1298
1289
|
}
|
|
1299
1290
|
if (typeof envVar !== "string" || envVar.trim().length === 0) {
|
|
1300
1291
|
throw new ValidationError(
|
|
1301
|
-
`Provider "${providerId}" has invalid
|
|
1292
|
+
`Provider "${providerId}" has invalid ${field}.credentialInputs.${inputField}: must be a non-empty env var name.`,
|
|
1302
1293
|
);
|
|
1303
1294
|
}
|
|
1304
1295
|
if (Array.isArray(requiredSecrets) && !requiredSecrets.includes(envVar)) {
|
|
1305
1296
|
throw new ValidationError(
|
|
1306
|
-
`Provider "${providerId}"
|
|
1297
|
+
`Provider "${providerId}" ${field}.credentialInputs.${inputField} references ${envVar}, which must also be listed in ${field}.requiredSecrets.`,
|
|
1307
1298
|
);
|
|
1308
1299
|
}
|
|
1309
1300
|
}
|
|
@@ -1317,32 +1308,32 @@ function validateProviderHealthMonitor(
|
|
|
1317
1308
|
Array.isArray(probeOverrides)
|
|
1318
1309
|
)
|
|
1319
1310
|
throw new ValidationError(
|
|
1320
|
-
`Provider "${providerId}" has invalid
|
|
1311
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1321
1312
|
);
|
|
1322
1313
|
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
1323
1314
|
if (probeId.length === 0)
|
|
1324
1315
|
throw new ValidationError(
|
|
1325
|
-
`Provider "${providerId}" has invalid
|
|
1316
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides key: must be a non-empty probe id.`,
|
|
1326
1317
|
);
|
|
1327
1318
|
if (!override || typeof override !== "object" || Array.isArray(override))
|
|
1328
1319
|
throw new ValidationError(
|
|
1329
|
-
`Provider "${providerId}" has invalid
|
|
1320
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"]: must be an object.`,
|
|
1330
1321
|
);
|
|
1331
1322
|
const overrideRecord = Object.fromEntries(Object.entries(override));
|
|
1332
1323
|
rejectUnknownFields(
|
|
1333
1324
|
overrideRecord,
|
|
1334
1325
|
PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS,
|
|
1335
|
-
|
|
1326
|
+
`${field}.probeOverrides["${probeId}"]`,
|
|
1336
1327
|
);
|
|
1337
1328
|
const interval = overrideRecord.interval;
|
|
1338
1329
|
if (interval !== undefined && !isPositiveMsDurationString(interval))
|
|
1339
1330
|
throw new ValidationError(
|
|
1340
|
-
`Provider "${providerId}" has invalid
|
|
1331
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"].interval: must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`,
|
|
1341
1332
|
);
|
|
1342
1333
|
if (overrideRecord.timeoutMs !== undefined) {
|
|
1343
1334
|
assertBoundedIntegerMs(
|
|
1344
1335
|
overrideRecord.timeoutMs,
|
|
1345
|
-
`Provider "${providerId}"
|
|
1336
|
+
`Provider "${providerId}" ${field}.probeOverrides["${probeId}"].timeoutMs`,
|
|
1346
1337
|
{
|
|
1347
1338
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1348
1339
|
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
@@ -1353,7 +1344,7 @@ function validateProviderHealthMonitor(
|
|
|
1353
1344
|
if (overrideRecord.degradedThresholdMs !== undefined) {
|
|
1354
1345
|
assertBoundedIntegerMs(
|
|
1355
1346
|
overrideRecord.degradedThresholdMs,
|
|
1356
|
-
`Provider "${providerId}"
|
|
1347
|
+
`Provider "${providerId}" ${field}.probeOverrides["${probeId}"].degradedThresholdMs`,
|
|
1357
1348
|
{
|
|
1358
1349
|
min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
1359
1350
|
max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
@@ -1369,7 +1360,7 @@ function validateProviderHealthMonitor(
|
|
|
1369
1360
|
(typeof serviceAccount !== "string" || serviceAccount.length === 0)
|
|
1370
1361
|
)
|
|
1371
1362
|
throw new ValidationError(
|
|
1372
|
-
`Provider "${providerId}" has invalid
|
|
1363
|
+
`Provider "${providerId}" has invalid ${field}.serviceAccount: must be a non-empty string.`,
|
|
1373
1364
|
);
|
|
1374
1365
|
}
|
|
1375
1366
|
|
|
@@ -1463,6 +1454,35 @@ function validateHealthCheckSuite(
|
|
|
1463
1454
|
fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
|
|
1464
1455
|
},
|
|
1465
1456
|
);
|
|
1457
|
+
if (s.schedule !== undefined) {
|
|
1458
|
+
if (
|
|
1459
|
+
!s.schedule ||
|
|
1460
|
+
typeof s.schedule !== "object" ||
|
|
1461
|
+
Array.isArray(s.schedule)
|
|
1462
|
+
) {
|
|
1463
|
+
throw new ValidationError(
|
|
1464
|
+
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
if (Reflect.get(s.schedule, "jitter") !== undefined) {
|
|
1468
|
+
throw new ValidationError(
|
|
1469
|
+
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
rejectUnknownFields(
|
|
1473
|
+
s.schedule,
|
|
1474
|
+
new Set(["randomize"]),
|
|
1475
|
+
`${fieldPath}.schedule`,
|
|
1476
|
+
);
|
|
1477
|
+
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1478
|
+
if (randomize !== undefined) {
|
|
1479
|
+
validateScheduleRandomization(
|
|
1480
|
+
randomize,
|
|
1481
|
+
`Provider "${providerId}" ${fieldPath}.schedule.randomize`,
|
|
1482
|
+
msDurationMs(s.interval),
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1466
1486
|
if (s.timeoutMs !== undefined) {
|
|
1467
1487
|
assertBoundedIntegerMs(
|
|
1468
1488
|
s.timeoutMs,
|
|
@@ -1561,7 +1581,12 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1561
1581
|
"steps",
|
|
1562
1582
|
"run",
|
|
1563
1583
|
]);
|
|
1564
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1584
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1585
|
+
"kind",
|
|
1586
|
+
"interval",
|
|
1587
|
+
"jitter",
|
|
1588
|
+
"randomize",
|
|
1589
|
+
]);
|
|
1565
1590
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1566
1591
|
"id",
|
|
1567
1592
|
"description",
|
|
@@ -1734,6 +1759,54 @@ function isoDurationMs(value: string): number {
|
|
|
1734
1759
|
);
|
|
1735
1760
|
}
|
|
1736
1761
|
|
|
1762
|
+
function scheduleRandomizationMs(
|
|
1763
|
+
randomize: unknown,
|
|
1764
|
+
fieldPath: string,
|
|
1765
|
+
): number {
|
|
1766
|
+
const mode = Reflect.get(randomize as object, "mode");
|
|
1767
|
+
switch (mode) {
|
|
1768
|
+
case "centered": {
|
|
1769
|
+
const maxOffset = Reflect.get(randomize as object, "maxOffset");
|
|
1770
|
+
assertIsoDuration(maxOffset, `${fieldPath}.maxOffset`);
|
|
1771
|
+
return isoDurationMs(maxOffset);
|
|
1772
|
+
}
|
|
1773
|
+
case "delayed": {
|
|
1774
|
+
const maxDelay = Reflect.get(randomize as object, "maxDelay");
|
|
1775
|
+
assertIsoDuration(maxDelay, `${fieldPath}.maxDelay`);
|
|
1776
|
+
return isoDurationMs(maxDelay);
|
|
1777
|
+
}
|
|
1778
|
+
default:
|
|
1779
|
+
throw new ValidationError(
|
|
1780
|
+
`${fieldPath}.mode must be "centered" or "delayed".`,
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function validateScheduleRandomization(
|
|
1786
|
+
randomize: unknown,
|
|
1787
|
+
fieldPath: string,
|
|
1788
|
+
intervalMs: number,
|
|
1789
|
+
): void {
|
|
1790
|
+
if (!randomize || typeof randomize !== "object" || Array.isArray(randomize)) {
|
|
1791
|
+
throw new ValidationError(`${fieldPath} must be an object.`);
|
|
1792
|
+
}
|
|
1793
|
+
const mode = Reflect.get(randomize, "mode");
|
|
1794
|
+
const allowedFields =
|
|
1795
|
+
mode === "centered"
|
|
1796
|
+
? new Set(["mode", "maxOffset"])
|
|
1797
|
+
: new Set(["mode", "maxDelay"]);
|
|
1798
|
+
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1799
|
+
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1800
|
+
if (offsetMs <= 0) {
|
|
1801
|
+
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1802
|
+
}
|
|
1803
|
+
if (offsetMs >= intervalMs) {
|
|
1804
|
+
throw new ValidationError(
|
|
1805
|
+
`${fieldPath} duration must be shorter than schedule interval.`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1737
1810
|
function assertIsoCountry(
|
|
1738
1811
|
value: unknown,
|
|
1739
1812
|
fieldPath: string,
|
|
@@ -1749,13 +1822,21 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1749
1822
|
const trimmed = input.trim();
|
|
1750
1823
|
const shorthand = /^(\d+)(s|m|h|d)$/i.exec(trimmed);
|
|
1751
1824
|
if (shorthand) {
|
|
1752
|
-
const
|
|
1825
|
+
const durationMs = msDurationMs(trimmed);
|
|
1826
|
+
const unit = shorthand[2]?.toLowerCase();
|
|
1827
|
+
const amount =
|
|
1828
|
+
unit === "s"
|
|
1829
|
+
? durationMs / 1_000
|
|
1830
|
+
: unit === "m"
|
|
1831
|
+
? durationMs / 60_000
|
|
1832
|
+
: unit === "h"
|
|
1833
|
+
? durationMs / 3_600_000
|
|
1834
|
+
: durationMs / 86_400_000;
|
|
1753
1835
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1754
1836
|
throw new ValidationError(
|
|
1755
1837
|
`Journey schedule interval must be a positive duration.`,
|
|
1756
1838
|
);
|
|
1757
1839
|
}
|
|
1758
|
-
const unit = shorthand[2]?.toLowerCase();
|
|
1759
1840
|
if (unit === "s") return `PT${amount}S`;
|
|
1760
1841
|
if (unit === "m") return `PT${amount}M`;
|
|
1761
1842
|
if (unit === "h") return `PT${amount}H`;
|
|
@@ -1767,18 +1848,34 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1767
1848
|
|
|
1768
1849
|
export function every(
|
|
1769
1850
|
interval: string,
|
|
1770
|
-
options: { jitter?: string } = {},
|
|
1851
|
+
options: { jitter?: string; randomize?: HealthScheduleRandomization } = {},
|
|
1771
1852
|
): HealthJourneySchedule {
|
|
1853
|
+
if (options.jitter !== undefined && options.randomize !== undefined) {
|
|
1854
|
+
throw new ValidationError(
|
|
1855
|
+
`Schedule cannot define both jitter and randomize. Use randomize instead.`,
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1772
1858
|
const schedule: HealthJourneySchedule = {
|
|
1773
1859
|
kind: "interval",
|
|
1774
1860
|
interval: normalizeIntervalDuration(interval),
|
|
1775
1861
|
};
|
|
1862
|
+
if (options.randomize !== undefined) {
|
|
1863
|
+
schedule.randomize = options.randomize;
|
|
1864
|
+
}
|
|
1776
1865
|
if (options.jitter !== undefined) {
|
|
1777
1866
|
schedule.jitter = normalizeIntervalDuration(options.jitter);
|
|
1778
1867
|
}
|
|
1779
1868
|
return schedule;
|
|
1780
1869
|
}
|
|
1781
1870
|
|
|
1871
|
+
export function centered(maxOffset: string): HealthScheduleRandomization {
|
|
1872
|
+
return { mode: "centered", maxOffset: normalizeIntervalDuration(maxOffset) };
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
export function delayed(maxDelay: string): HealthScheduleRandomization {
|
|
1876
|
+
return { mode: "delayed", maxDelay: normalizeIntervalDuration(maxDelay) };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1782
1879
|
function countCapturingGroups(pattern: RegExp): number {
|
|
1783
1880
|
let count = 0;
|
|
1784
1881
|
const source = pattern.source;
|
|
@@ -1993,15 +2090,29 @@ function validateHealthJourneySchedule(
|
|
|
1993
2090
|
throw new ValidationError(
|
|
1994
2091
|
`Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
|
|
1995
2092
|
);
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2093
|
+
const interval = Reflect.get(schedule, "interval");
|
|
2094
|
+
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
2095
|
+
const randomize = Reflect.get(schedule, "randomize");
|
|
2096
|
+
if (
|
|
2097
|
+
Reflect.get(schedule, "jitter") !== undefined &&
|
|
2098
|
+
randomize !== undefined
|
|
2099
|
+
) {
|
|
2100
|
+
throw new ValidationError(
|
|
2101
|
+
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2000
2104
|
if (Reflect.get(schedule, "jitter") !== undefined)
|
|
2001
2105
|
assertIsoDuration(
|
|
2002
2106
|
Reflect.get(schedule, "jitter"),
|
|
2003
2107
|
`Provider "${providerId}" ${fieldPath}.jitter`,
|
|
2004
2108
|
);
|
|
2109
|
+
if (randomize !== undefined) {
|
|
2110
|
+
validateScheduleRandomization(
|
|
2111
|
+
randomize,
|
|
2112
|
+
`Provider "${providerId}" ${fieldPath}.randomize`,
|
|
2113
|
+
isoDurationMs(interval),
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2005
2116
|
}
|
|
2006
2117
|
|
|
2007
2118
|
function validateHealthJourneys(
|
|
@@ -2244,7 +2355,9 @@ export function defineProvider<
|
|
|
2244
2355
|
if (Object.keys(config.operations).length === 0)
|
|
2245
2356
|
throw new ProviderError(
|
|
2246
2357
|
`Provider "${config.id}" must define at least one operation`,
|
|
2247
|
-
{
|
|
2358
|
+
{
|
|
2359
|
+
fix: "Add at least one operation to the operations object",
|
|
2360
|
+
},
|
|
2248
2361
|
);
|
|
2249
2362
|
validateOperationIds(config.id, config.operations);
|
|
2250
2363
|
validateOperationAnnotations(config.id, config.operations);
|
|
@@ -2262,7 +2375,18 @@ export function defineProvider<
|
|
|
2262
2375
|
config.operations,
|
|
2263
2376
|
journeyCoveredOperations,
|
|
2264
2377
|
);
|
|
2265
|
-
|
|
2378
|
+
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2379
|
+
throw new ValidationError(
|
|
2380
|
+
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
2381
|
+
{
|
|
2382
|
+
fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
|
|
2383
|
+
},
|
|
2384
|
+
);
|
|
2385
|
+
validateProviderHealthMonitor(
|
|
2386
|
+
config.id,
|
|
2387
|
+
config.healthProbe ?? config.healthMonitor,
|
|
2388
|
+
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2389
|
+
);
|
|
2266
2390
|
validateOperationFixtures(config.id, config.operations);
|
|
2267
2391
|
validateProviderProxy(config);
|
|
2268
2392
|
validateProviderStt(config);
|
|
@@ -2295,7 +2419,10 @@ export function defineProvider<
|
|
|
2295
2419
|
context: config.context,
|
|
2296
2420
|
meta: config.meta,
|
|
2297
2421
|
operations: config.operations,
|
|
2298
|
-
healthMonitor:
|
|
2422
|
+
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
2423
|
+
// was declared onto both so old and new consumers keep working.
|
|
2424
|
+
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
|
2425
|
+
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
2299
2426
|
healthJourneys: config.healthJourneys,
|
|
2300
2427
|
};
|
|
2301
2428
|
}
|