@danceiny/gotry 0.0.1-rc.17 → 0.0.1-rc.19
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 +185 -148
- package/README.zh-CN.md +195 -154
- package/bin/gotry-bootstrap.js +246 -26
- package/bin/gotry-inner.js +68 -38
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +15 -5
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +285 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
- package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +115 -44
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +121 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +7 -4
- package/dist/src/benchmark-environment-bridge.js +68 -7
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +735 -149
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +6 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/package.json +1 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +14 -6
- package/ts/src/benchmark-environment-bridge.ts +29 -10
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +489 -171
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +361 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import { BOOKING_SURFACE_SCHEMA_V2_SHA256, BOOKING_SURFACE_SCHEMA_VERSION_V2, BOOKING_COPILOT_V2_ACCEPTED_TURN_KINDS, BOOKING_READ_ACTION_KINDS_V2, BOOKING_SURFACE_ALLOWED_ACTIONS_V2 } from './contracts-v2.js';
|
|
2
|
-
import { BookingCopilotTaskRuntimeV2 } from './runtime-v2.js';
|
|
3
|
-
import { validateBookingSurfaceV2 } from './validation-v2.js';
|
|
4
|
-
import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.js';
|
|
5
|
-
const sessionsByAdapter = new WeakMap();
|
|
6
|
-
const decisionFlightsByAdapter = new WeakMap();
|
|
7
|
-
function sessionsFor(adapter) {
|
|
8
|
-
let sessions = sessionsByAdapter.get(adapter);
|
|
9
|
-
if (!sessions) {
|
|
10
|
-
sessions = new Map();
|
|
11
|
-
sessionsByAdapter.set(adapter, sessions);
|
|
12
|
-
}
|
|
13
|
-
return sessions;
|
|
14
|
-
}
|
|
15
|
-
function decisionFlightsFor(adapter) {
|
|
16
|
-
let flights = decisionFlightsByAdapter.get(adapter);
|
|
17
|
-
if (!flights) {
|
|
18
|
-
flights = new Map();
|
|
19
|
-
decisionFlightsByAdapter.set(adapter, flights);
|
|
20
|
-
}
|
|
21
|
-
return flights;
|
|
22
|
-
}
|
|
23
|
-
async function runDecisionSingleFlight(adapter, key, work) {
|
|
24
|
-
const flights = decisionFlightsFor(adapter);
|
|
25
|
-
const prior = flights.get(key);
|
|
26
|
-
if (prior) return prior;
|
|
27
|
-
const current = work();
|
|
28
|
-
flights.set(key, current);
|
|
29
|
-
try {
|
|
30
|
-
return await current;
|
|
31
|
-
} finally{
|
|
32
|
-
if (flights.get(key) === current) flights.delete(key);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function send(res, status, body) {
|
|
36
|
-
res.writeHead(status, {
|
|
37
|
-
'content-type': 'application/json; charset=utf-8'
|
|
38
|
-
});
|
|
39
|
-
res.end(JSON.stringify(body));
|
|
40
|
-
}
|
|
41
|
-
function readBody(req, max) {
|
|
42
|
-
return new Promise((resolve, reject)=>{
|
|
43
|
-
const chunks = [];
|
|
44
|
-
let size = 0;
|
|
45
|
-
let tooLarge = false;
|
|
46
|
-
req.on('data', (chunk)=>{
|
|
47
|
-
size += chunk.length;
|
|
48
|
-
if (size > max) tooLarge = true;
|
|
49
|
-
else chunks.push(chunk);
|
|
50
|
-
});
|
|
51
|
-
req.on('end', ()=>tooLarge ? reject(new Error('payload_too_large')) : resolve(Buffer.concat(chunks).toString('utf8')));
|
|
52
|
-
req.on('error', reject);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function write(res, event) {
|
|
56
|
-
res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
57
|
-
}
|
|
58
|
-
export { normalizeBookingErrorCode as bookingPlannerErrorCode, safeBookingErrorMessage as bookingPlannerErrorMessage } from './error-codes.js';
|
|
59
|
-
function sameCapabilities(a, b) {
|
|
60
|
-
return a.length === b.length && [
|
|
61
|
-
...a
|
|
62
|
-
].sort().every((kind, index)=>kind === [
|
|
63
|
-
...b
|
|
64
|
-
].sort()[index]);
|
|
65
|
-
}
|
|
66
|
-
function exactKeys(value, allowed) {
|
|
67
|
-
return Object.keys(value).length === allowed.length && Object.keys(value).every((key)=>allowed.includes(key));
|
|
68
|
-
}
|
|
69
|
-
const SURFACE_ACTION_MATRIX = BOOKING_SURFACE_ALLOWED_ACTIONS_V2;
|
|
70
|
-
function validSurfaceActions(surface, allowed) {
|
|
71
|
-
if (typeof surface !== 'string' || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, surface)) return false;
|
|
72
|
-
if (!Array.isArray(allowed) || allowed.length < 1 || new Set(allowed).size !== allowed.length) return false;
|
|
73
|
-
const matrix = SURFACE_ACTION_MATRIX[surface];
|
|
74
|
-
return allowed.every((action)=>typeof action === 'string' && matrix.includes(action) && BOOKING_READ_ACTION_KINDS_V2.includes(action));
|
|
75
|
-
}
|
|
76
|
-
function validBoundWorkspaceAuthority(workspace) {
|
|
77
|
-
return workspace.capabilities.surface === workspace.surface && validSurfaceActions(workspace.surface, workspace.capabilities.allowedActions);
|
|
78
|
-
}
|
|
79
|
-
function validIngressBinding(binding, ingress) {
|
|
80
|
-
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return false;
|
|
81
|
-
const candidate = binding;
|
|
82
|
-
if (!exactKeys(candidate, [
|
|
83
|
-
'taskId',
|
|
84
|
-
'turnId',
|
|
85
|
-
'contextRef',
|
|
86
|
-
'surface',
|
|
87
|
-
'allowedActions'
|
|
88
|
-
])) return false;
|
|
89
|
-
if (typeof candidate.taskId !== 'string' || typeof candidate.turnId !== 'string' || typeof candidate.contextRef !== 'string') return false;
|
|
90
|
-
if (typeof candidate.surface !== 'string' || candidate.surface !== ingress.surfaceHint || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, candidate.surface)) return false;
|
|
91
|
-
return validSurfaceActions(candidate.surface, candidate.allowedActions);
|
|
92
|
-
}
|
|
93
|
-
function fallbackErrorEvent(task, code) {
|
|
94
|
-
return {
|
|
95
|
-
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION_V2,
|
|
96
|
-
eventId: `error-${task.taskId}-${task.lastSequence + 1}`,
|
|
97
|
-
taskId: task.taskId,
|
|
98
|
-
contextRef: task.contextRef,
|
|
99
|
-
sequence: task.lastSequence + 1,
|
|
100
|
-
emittedAt: new Date().toISOString(),
|
|
101
|
-
kind: 'error',
|
|
102
|
-
error: {
|
|
103
|
-
code: normalizeBookingErrorCode(code),
|
|
104
|
-
message: safeBookingErrorMessage(normalizeBookingErrorCode(code)),
|
|
105
|
-
retryable: false
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function writeTypedError(res, adapter, task, error, requestKey, includeSubmitted = false) {
|
|
110
|
-
const code = normalizeBookingErrorCode(error);
|
|
111
|
-
try {
|
|
112
|
-
const decision = {
|
|
113
|
-
kind: 'error',
|
|
114
|
-
error: {
|
|
115
|
-
code,
|
|
116
|
-
message: safeBookingErrorMessage(code),
|
|
117
|
-
retryable: false
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
if (requestKey) {
|
|
121
|
-
const events = adapter.runtime.applyDecisionBatch(task.taskId, requestKey, [
|
|
122
|
-
decision
|
|
123
|
-
], includeSubmitted);
|
|
124
|
-
for (const event of events)write(res, event);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
write(res, adapter.runtime.emitEvent(task.taskId, decision));
|
|
128
|
-
} catch {
|
|
129
|
-
write(res, fallbackErrorEvent(task, code));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
export async function handleBookingCopilotV2Request(req, res, adapter, maxBodyBytes) {
|
|
133
|
-
let turn;
|
|
134
|
-
let browserRequestKey;
|
|
135
|
-
let requestBinding;
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(await readBody(req, maxBodyBytes));
|
|
138
|
-
const valid = validateBookingSurfaceV2(parsed);
|
|
139
|
-
if (!valid.ok) {
|
|
140
|
-
send(res, 400, {
|
|
141
|
-
error: {
|
|
142
|
-
code: 'invalid_booking_surface_turn',
|
|
143
|
-
details: valid.errors
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (parsed.kind === 'user.turn.ingress') {
|
|
149
|
-
const ingress = parsed;
|
|
150
|
-
browserRequestKey = ingress.requestKey;
|
|
151
|
-
if (adapter.ingressMode !== 'bff-ingress-binding' || !adapter.ingressBinding || !adapter.principal) {
|
|
152
|
-
send(res, 503, {
|
|
153
|
-
error: {
|
|
154
|
-
code: 'trusted_ingress_binding_required',
|
|
155
|
-
mode: adapter.ingressMode ?? 'bff-bound-turn-only',
|
|
156
|
-
acceptedTurnKinds: [
|
|
157
|
-
...BOOKING_COPILOT_V2_ACCEPTED_TURN_KINDS
|
|
158
|
-
]
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const binding = await adapter.ingressBinding.bind(ingress, adapter.principal);
|
|
164
|
-
if (!validIngressBinding(binding, ingress)) {
|
|
165
|
-
send(res, 502, {
|
|
166
|
-
error: {
|
|
167
|
-
code: 'invalid_ingress_binding'
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const boundWorkspace = {
|
|
173
|
-
...ingress.workspace,
|
|
174
|
-
contextRef: binding.contextRef,
|
|
175
|
-
surface: binding.surface,
|
|
176
|
-
capabilities: {
|
|
177
|
-
surface: binding.surface,
|
|
178
|
-
allowedActions: [
|
|
179
|
-
...binding.allowedActions
|
|
180
|
-
]
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
turn = {
|
|
184
|
-
schemaVersion: 'booking.surface.v2',
|
|
185
|
-
kind: 'user.turn',
|
|
186
|
-
taskId: binding.taskId,
|
|
187
|
-
turnId: binding.turnId,
|
|
188
|
-
workspace: boundWorkspace,
|
|
189
|
-
request: ingress.request
|
|
190
|
-
};
|
|
191
|
-
requestBinding = {
|
|
192
|
-
requestKey: ingress.requestKey,
|
|
193
|
-
principal: adapter.principal,
|
|
194
|
-
...ingress.taskHandle ? {
|
|
195
|
-
taskHandle: ingress.taskHandle
|
|
196
|
-
} : {}
|
|
197
|
-
};
|
|
198
|
-
const boundValid = validateBookingSurfaceV2(turn);
|
|
199
|
-
if (!boundValid.ok) {
|
|
200
|
-
send(res, 502, {
|
|
201
|
-
error: {
|
|
202
|
-
code: 'invalid_ingress_binding',
|
|
203
|
-
details: boundValid.errors
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
} else {
|
|
209
|
-
turn = parsed;
|
|
210
|
-
if (!validBoundWorkspaceAuthority(turn.workspace)) {
|
|
211
|
-
send(res, 403, {
|
|
212
|
-
error: {
|
|
213
|
-
code: 'invalid_bound_turn_authority'
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
const code = normalizeBookingErrorCode(error);
|
|
221
|
-
send(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, {
|
|
222
|
-
error: {
|
|
223
|
-
code
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
let task;
|
|
229
|
-
let fresh = false;
|
|
230
|
-
let replayKey;
|
|
231
|
-
let replayEvents = null;
|
|
232
|
-
let activeDecisionKey;
|
|
233
|
-
try {
|
|
234
|
-
fresh = turn.kind === 'action.receipt.continuation' ? false : !turn.taskId || adapter.runtime.resumeTask(turn.taskId) === null;
|
|
235
|
-
if (turn.kind === 'action.receipt.continuation') {
|
|
236
|
-
replayKey = `receipt:${turn.receipt.actionId}:${adapter.runtime.receiptDigest(turn.receipt)}`;
|
|
237
|
-
task = adapter.runtime.continueWithReceipt(turn);
|
|
238
|
-
if (task.awaitingApproval) {
|
|
239
|
-
replayKey = adapter.runtime.approvalPresentationRequestKey(turn.taskId);
|
|
240
|
-
}
|
|
241
|
-
replayEvents = adapter.runtime.readDecisionBatch(turn.taskId, replayKey);
|
|
242
|
-
if (!replayEvents && task.phase === 'terminal') replayEvents = adapter.runtime.terminalDecisionBatch(turn.taskId, replayKey);
|
|
243
|
-
} else {
|
|
244
|
-
const existing = turn.taskId ? adapter.runtime.resumeTask(turn.taskId) : null;
|
|
245
|
-
if (requestBinding && turn.kind === 'user.turn') adapter.runtime.assertRequestBinding(browserRequestKey, turn, requestBinding);
|
|
246
|
-
const requestReplayKey = existing && browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : undefined;
|
|
247
|
-
const directReplayKey = existing && turn.kind === 'user.turn' && turn.turnId ? `turn:${existing.taskId}:${turn.turnId}` : undefined;
|
|
248
|
-
const stableReplayKey = requestReplayKey ?? directReplayKey;
|
|
249
|
-
const stableReplay = stableReplayKey ? adapter.runtime.readDecisionBatch(existing.taskId, stableReplayKey) : null;
|
|
250
|
-
if (stableReplay) {
|
|
251
|
-
if (existing && turn.kind === 'user.turn' && directReplayKey) adapter.runtime.assertTurnBinding(existing.taskId, turn);
|
|
252
|
-
task = existing;
|
|
253
|
-
replayKey = stableReplayKey;
|
|
254
|
-
replayEvents = stableReplay;
|
|
255
|
-
} else if (existing?.phase === 'terminal' || existing?.phase === 'error') {
|
|
256
|
-
throw new Error('task_terminal');
|
|
257
|
-
} else if (existing?.phase === 'waiting_receipt') {
|
|
258
|
-
const workspace = turn.workspace;
|
|
259
|
-
if (workspace.contextRef !== existing.contextRef || workspace.surface !== existing.surface || workspace.revision !== existing.revision || workspace.capabilities.surface !== existing.surface || !sameCapabilities(workspace.capabilities.allowedActions, existing.allowedActions)) throw new Error('task_conflict:workspace_mismatch');
|
|
260
|
-
if (!turn.turnId || existing.lastTurnId !== turn.turnId) throw new Error('receipt_required');
|
|
261
|
-
replayKey = browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : `turn:${existing.taskId}:${existing.lastTurnId ?? existing.userTurnCount}`;
|
|
262
|
-
replayEvents = adapter.runtime.readDecisionBatch(existing.taskId, replayKey);
|
|
263
|
-
if (!replayEvents) throw new Error('receipt_required');
|
|
264
|
-
task = existing;
|
|
265
|
-
} else {
|
|
266
|
-
task = adapter.runtime.startTask(turn, requestBinding);
|
|
267
|
-
if (turn.kind === 'user.turn' && turn.taskId) {
|
|
268
|
-
replayKey = browserRequestKey ? `ingress:${task.taskId}:${browserRequestKey}` : `turn:${task.taskId}:${turn.turnId ?? task.userTurnCount}`;
|
|
269
|
-
replayEvents = adapter.runtime.readDecisionBatch(task.taskId, replayKey);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
send(res, 409, {
|
|
275
|
-
error: {
|
|
276
|
-
code: normalizeBookingErrorCode(error)
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
res.writeHead(200, {
|
|
282
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
283
|
-
'cache-control': 'no-cache, no-transform',
|
|
284
|
-
connection: 'keep-alive',
|
|
285
|
-
'x-content-type-options': 'nosniff',
|
|
286
|
-
'x-booking-surface-version': BOOKING_SURFACE_SCHEMA_VERSION_V2,
|
|
287
|
-
'x-booking-surface-schema-sha256': BOOKING_SURFACE_SCHEMA_V2_SHA256
|
|
288
|
-
});
|
|
289
|
-
try {
|
|
290
|
-
if (replayEvents) {
|
|
291
|
-
for (const event of replayEvents)write(res, event);
|
|
292
|
-
} else {
|
|
293
|
-
const requestKey = replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`;
|
|
294
|
-
activeDecisionKey = requestKey;
|
|
295
|
-
const flightKey = JSON.stringify([
|
|
296
|
-
task.taskId,
|
|
297
|
-
requestKey
|
|
298
|
-
]);
|
|
299
|
-
const decisionEvents = await runDecisionSingleFlight(adapter, flightKey, async ()=>{
|
|
300
|
-
if (turn.kind === 'action.receipt.continuation' && task.awaitingApproval) {
|
|
301
|
-
return adapter.runtime.applyDecisionBatch(task.taskId, requestKey, [
|
|
302
|
-
adapter.runtime.approvalQuestion(task.taskId)
|
|
303
|
-
], false);
|
|
304
|
-
}
|
|
305
|
-
const sessions = sessionsFor(adapter);
|
|
306
|
-
const session = sessions.get(task.taskId) ?? adapter.plannerFactory(task);
|
|
307
|
-
sessions.set(task.taskId, session);
|
|
308
|
-
const decisions = await session.next({
|
|
309
|
-
turn,
|
|
310
|
-
task: adapter.runtime.resumeTask(task.taskId) ?? task
|
|
311
|
-
});
|
|
312
|
-
const plannerDecisions = decisions.length ? decisions : [
|
|
313
|
-
{
|
|
314
|
-
kind: 'error',
|
|
315
|
-
error: {
|
|
316
|
-
code: 'PLANNER_NO_DECISION',
|
|
317
|
-
message: 'Planner returned no typed decision.',
|
|
318
|
-
retryable: false
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
];
|
|
322
|
-
return adapter.runtime.applyDecisionBatch(task.taskId, requestKey, plannerDecisions, fresh);
|
|
323
|
-
});
|
|
324
|
-
for (const event of decisionEvents)write(res, event);
|
|
325
|
-
}
|
|
326
|
-
} catch (error) {
|
|
327
|
-
writeTypedError(res, adapter, task, error, activeDecisionKey, fresh);
|
|
328
|
-
} finally{
|
|
329
|
-
res.end();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
//# sourceURL=ts/src/booking-surface/server-v2.ts
|
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
|
-
import { BOOKING_READ_ACTION_KINDS_V2, BOOKING_V2_BLOCKER_CODES, BOOKING_V2_GAP_CODES } from './contracts-v2.js';
|
|
4
|
-
const schema = JSON.parse(readFileSync(new URL('../../../schemas/booking.surface.v2.schema.json', import.meta.url), 'utf8'));
|
|
5
|
-
const v1 = JSON.parse(readFileSync(new URL('../../../schemas/booking.surface.v1.schema.json', import.meta.url), 'utf8'));
|
|
6
|
-
const ajv = new Ajv2020({
|
|
7
|
-
allErrors: true,
|
|
8
|
-
strict: true
|
|
9
|
-
});
|
|
10
|
-
ajv.addSchema(v1);
|
|
11
|
-
ajv.addSchema(schema);
|
|
12
|
-
const validate = ajv.compile(schema);
|
|
13
|
-
const validateActionCanonical = ajv.compile({
|
|
14
|
-
$ref: `${schema.$id}#/$defs/Action`
|
|
15
|
-
});
|
|
16
|
-
const validateBlockerCanonical = ajv.compile({
|
|
17
|
-
$ref: `${schema.$id}#/$defs/Blocker`
|
|
18
|
-
});
|
|
19
|
-
const validateApprovalCanonical = ajv.compile({
|
|
20
|
-
$ref: `${schema.$id}#/$defs/Approval`
|
|
21
|
-
});
|
|
22
|
-
const validateEventCanonical = ajv.compile({
|
|
23
|
-
$ref: `${schema.$id}#/$defs/Event`
|
|
24
|
-
});
|
|
25
|
-
const kinds = new Set(BOOKING_READ_ACTION_KINDS_V2);
|
|
26
|
-
const codes = new Set(BOOKING_V2_BLOCKER_CODES);
|
|
27
|
-
const gapCodes = new Set(BOOKING_V2_GAP_CODES);
|
|
28
|
-
const safeOpaque = (value)=>typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(value);
|
|
29
|
-
const safeRequestKey = (value)=>typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{7,127}$/.test(value) && !secret.test(value);
|
|
30
|
-
const secret = /(?:Bearer\s+\S|\bsk-(?!ill(?:s)?\b)[A-Za-z0-9_-]{12,}|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i;
|
|
31
|
-
const technicalEmail = /^(?:schema|version)@[a-z0-9-]+\.example$/i;
|
|
32
|
-
const currencyAmount = /\b(?:[A-Z]{3}\s*\d+(?:\.\d+)?|\d+(?:\.\d+)?\s*[A-Z]{3})\b/;
|
|
33
|
-
const rfc3339DateTime = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/;
|
|
34
|
-
export function isBookingDateTimeV2(value) {
|
|
35
|
-
if (typeof value !== 'string') return false;
|
|
36
|
-
const match = rfc3339DateTime.exec(value);
|
|
37
|
-
if (!match) return false;
|
|
38
|
-
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match;
|
|
39
|
-
const year = Number(yearText);
|
|
40
|
-
const month = Number(monthText);
|
|
41
|
-
const day = Number(dayText);
|
|
42
|
-
const hour = Number(hourText);
|
|
43
|
-
const minute = Number(minuteText);
|
|
44
|
-
const second = Number(secondText);
|
|
45
|
-
const offsetHour = match[9] === undefined ? 0 : Number(match[9]);
|
|
46
|
-
const offsetMinute = match[10] === undefined ? 0 : Number(match[10]);
|
|
47
|
-
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) return false;
|
|
48
|
-
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
49
|
-
if (day < 1 || day > daysInMonth) return false;
|
|
50
|
-
return !Number.isNaN(Date.parse(value));
|
|
51
|
-
}
|
|
52
|
-
function normalizedWords(value) {
|
|
53
|
-
return value.normalize('NFKC').replace(/[^\p{L}\p{N}.]+/gu, ' ').trim();
|
|
54
|
-
}
|
|
55
|
-
function hasSupplierCost(value) {
|
|
56
|
-
const normalized = normalizedWords(value);
|
|
57
|
-
const lower = normalized.toLowerCase();
|
|
58
|
-
return /\b(?:supplier|vendor|internal)\b/.test(lower) && /\b(?:cost|rate|price)\b/.test(lower) && currencyAmount.test(normalized.toUpperCase());
|
|
59
|
-
}
|
|
60
|
-
function hasInternationalPhone(value) {
|
|
61
|
-
const normalized = value.normalize('NFKC');
|
|
62
|
-
for (const plusIndex of normalized.split('').flatMap((char, index)=>char === '+' ? [
|
|
63
|
-
index
|
|
64
|
-
] : [])){
|
|
65
|
-
let digits = '';
|
|
66
|
-
for (const char of normalized.slice(plusIndex + 1, plusIndex + 48)){
|
|
67
|
-
if (/\d/u.test(char)) digits += char;
|
|
68
|
-
else if (/\p{L}|@/u.test(char)) break;
|
|
69
|
-
if (digits.length > 15) break;
|
|
70
|
-
}
|
|
71
|
-
if (/^[1-9]\d{7,14}$/.test(digits)) return true;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
function hasPaymentCard(value) {
|
|
76
|
-
const normalized = value.normalize('NFKC');
|
|
77
|
-
const candidates = normalized.match(/(?:\d[ -]?){12,18}\d/g) ?? [];
|
|
78
|
-
const explicitlyLabeled = /\b(?:card|payment|visa|mastercard)\b/i.test(normalized);
|
|
79
|
-
return candidates.some((candidate)=>(explicitlyLabeled || /[ -]/.test(candidate)) && luhn(candidate));
|
|
80
|
-
}
|
|
81
|
-
function hasSensitiveText(value, opts = {}) {
|
|
82
|
-
const normalized = value.normalize('NFKC');
|
|
83
|
-
if (normalized.includes('@') && !technicalEmail.test(normalized.trim())) return true;
|
|
84
|
-
return secret.test(normalized) || hasInternationalPhone(normalized) || hasSupplierCost(normalized) || !opts.allowOpaqueNumericId && hasPaymentCard(normalized);
|
|
85
|
-
}
|
|
86
|
-
function luhn(value) {
|
|
87
|
-
const digits = value.replaceAll(/\D/g, '');
|
|
88
|
-
if (digits.length < 13 || digits.length > 19) return false;
|
|
89
|
-
let sum = 0;
|
|
90
|
-
for(let i = digits.length - 1, parity = 0; i >= 0; i--, parity++){
|
|
91
|
-
let n = Number(digits[i]);
|
|
92
|
-
if (parity % 2 === 1) {
|
|
93
|
-
n *= 2;
|
|
94
|
-
if (n > 9) n -= 9;
|
|
95
|
-
}
|
|
96
|
-
sum += n;
|
|
97
|
-
}
|
|
98
|
-
return sum % 10 === 0;
|
|
99
|
-
}
|
|
100
|
-
const opaqueFieldNames = new Set([
|
|
101
|
-
'eventId',
|
|
102
|
-
'taskId',
|
|
103
|
-
'contextRef',
|
|
104
|
-
'actionId',
|
|
105
|
-
'hotelRef',
|
|
106
|
-
'offerRef',
|
|
107
|
-
'offerVersionRef',
|
|
108
|
-
'checkedOfferVersionRef',
|
|
109
|
-
'currentOfferVersionRef',
|
|
110
|
-
'verifiedOfferRef',
|
|
111
|
-
'orderRef',
|
|
112
|
-
'searchSessionRef',
|
|
113
|
-
'handoffRef',
|
|
114
|
-
'questionId',
|
|
115
|
-
'blockerId',
|
|
116
|
-
'approvalId',
|
|
117
|
-
'sourceActionId',
|
|
118
|
-
'targetActionId',
|
|
119
|
-
'sourceTurnId',
|
|
120
|
-
'turnId',
|
|
121
|
-
'requestKey',
|
|
122
|
-
'taskHandle',
|
|
123
|
-
'nonce',
|
|
124
|
-
'deliveryNonce',
|
|
125
|
-
'optionDigest',
|
|
126
|
-
'sourceReceiptDigest',
|
|
127
|
-
'valueDigest'
|
|
128
|
-
]);
|
|
129
|
-
function isOpaqueFieldPath(path) {
|
|
130
|
-
const clean = path.replace(/\[\]$/g, '');
|
|
131
|
-
const field = clean.split('.').at(-1);
|
|
132
|
-
return field ? opaqueFieldNames.has(field) || field.endsWith('Ref') || field.endsWith('Refs') : false;
|
|
133
|
-
}
|
|
134
|
-
const scan = (v, path = '$')=>typeof v === 'string' ? hasSensitiveText(v, {
|
|
135
|
-
allowOpaqueNumericId: isOpaqueFieldPath(path)
|
|
136
|
-
}) : Array.isArray(v) ? v.some((child)=>scan(child, `${path}[]`)) : !!v && typeof v === 'object' ? Object.entries(v).some(([k, x])=>/cost|price/i.test(k) ? /supplier|internal|wholesale/i.test(String(x)) : scan(x, `${path}.${k}`)) : false;
|
|
137
|
-
export function isSafeBookingRequestKeyV2(value) {
|
|
138
|
-
return safeRequestKey(value);
|
|
139
|
-
}
|
|
140
|
-
function errors(v) {
|
|
141
|
-
return (v.errors ?? []).map((e)=>`${e.instancePath || '$'}: ${e.message ?? e.keyword}`);
|
|
142
|
-
}
|
|
143
|
-
function workspaceErrors(workspace, path) {
|
|
144
|
-
const e = [];
|
|
145
|
-
if (!workspace || typeof workspace !== 'object' || Array.isArray(workspace)) return e;
|
|
146
|
-
const loadedOffers = Array.isArray(workspace.loadedOffers) ? workspace.loadedOffers : [];
|
|
147
|
-
const offerRefs = loadedOffers.map((offer)=>offer?.offerRef).filter((ref)=>typeof ref === 'string');
|
|
148
|
-
const offerVersionRefs = loadedOffers.map((offer)=>offer?.offerVersionRef).filter((ref)=>typeof ref === 'string');
|
|
149
|
-
if (new Set(offerRefs).size !== offerRefs.length) e.push(`${path}.loadedOffers: duplicate offerRef`);
|
|
150
|
-
if (new Set(offerVersionRefs).size !== offerVersionRefs.length) e.push(`${path}.loadedOffers: duplicate offerVersionRef`);
|
|
151
|
-
const verified = workspace.verifiedOffer;
|
|
152
|
-
if (verified !== undefined) {
|
|
153
|
-
if (!loadedOffers.some((offer)=>offer?.offerRef === verified.offerRef && offer?.offerVersionRef === verified.offerVersionRef)) e.push(`${path}.verifiedOffer: loaded offer version required`);
|
|
154
|
-
if (!safeOpaque(verified.verifiedOfferRef) || !isBookingDateTimeV2(verified.expiresAt)) e.push(`${path}.verifiedOffer: safe ref and parseable expiry required`);
|
|
155
|
-
}
|
|
156
|
-
return e;
|
|
157
|
-
}
|
|
158
|
-
function cross(value) {
|
|
159
|
-
const e = [];
|
|
160
|
-
if (scan(value)) e.push('$: unsafe PII/payment/supplier-cost/credential text');
|
|
161
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return e;
|
|
162
|
-
const x = value;
|
|
163
|
-
const semantic = x.kind === 'action.receipt.continuation' ? x.receipt : x;
|
|
164
|
-
if (x.workspace && x.kind !== 'user.turn.ingress') e.push(...workspaceErrors(x.workspace, '$.workspace'));
|
|
165
|
-
if (x.schemaVersion === 'booking.surface.v2' && Array.isArray(x.loadedOffers)) e.push(...workspaceErrors(x, '$'));
|
|
166
|
-
if (x.kind === 'user.turn.ingress') {
|
|
167
|
-
if (!safeRequestKey(x.requestKey)) e.push('$.requestKey: safe opaque request key required');
|
|
168
|
-
const workspace = x.workspace;
|
|
169
|
-
e.push(...workspaceErrors(workspace, '$.workspace'));
|
|
170
|
-
const visibleHotels = Array.isArray(workspace?.visibleHotels) ? workspace.visibleHotels : [];
|
|
171
|
-
const loadedOffers = Array.isArray(workspace?.loadedOffers) ? workspace.loadedOffers : [];
|
|
172
|
-
if (workspace?.focusedHotelRef !== undefined && !visibleHotels.some((hotel)=>hotel?.hotelRef === workspace.focusedHotelRef)) e.push('$.workspace.focusedHotelRef: visible hotel required');
|
|
173
|
-
const selectedOffer = workspace?.selectedOfferRef === undefined ? undefined : loadedOffers.find((offer)=>offer?.offerRef === workspace.selectedOfferRef);
|
|
174
|
-
if (workspace?.selectedOfferRef !== undefined && !selectedOffer) e.push('$.workspace.selectedOfferRef: loaded offer required');
|
|
175
|
-
if (selectedOffer && !visibleHotels.some((hotel)=>hotel?.hotelRef === selectedOffer.hotelRef)) e.push('$.workspace.selectedOfferRef: offer hotel must be visible');
|
|
176
|
-
}
|
|
177
|
-
if (semantic !== x && semantic?.resultContract?.hardCriteriaMet === true && semantic.resultContract.blockers?.length) e.push('$.receipt.resultContract: hardCriteriaMet cannot coexist with blockers');
|
|
178
|
-
if (kinds.has(x.kind)) {
|
|
179
|
-
const allowed = {
|
|
180
|
-
'search.patch': [
|
|
181
|
-
'patch'
|
|
182
|
-
],
|
|
183
|
-
'search.run': [],
|
|
184
|
-
'results.view.patch': [
|
|
185
|
-
'patch'
|
|
186
|
-
],
|
|
187
|
-
'hotel.focus': [
|
|
188
|
-
'hotelRef'
|
|
189
|
-
],
|
|
190
|
-
'hotel.select': [
|
|
191
|
-
'hotelRef'
|
|
192
|
-
],
|
|
193
|
-
'offers.query': [
|
|
194
|
-
'hotelRefs',
|
|
195
|
-
'criteria'
|
|
196
|
-
],
|
|
197
|
-
'offers.view.patch': [
|
|
198
|
-
'hotelRef',
|
|
199
|
-
'criteria'
|
|
200
|
-
],
|
|
201
|
-
'offers.compare': [
|
|
202
|
-
'offerRefs',
|
|
203
|
-
'requestedCount'
|
|
204
|
-
],
|
|
205
|
-
'offer.select': [
|
|
206
|
-
'offerRef',
|
|
207
|
-
'offerVersionRef'
|
|
208
|
-
],
|
|
209
|
-
'offer.check': [
|
|
210
|
-
'offerRef',
|
|
211
|
-
'offerVersionRef'
|
|
212
|
-
],
|
|
213
|
-
'checkout.prepare': [
|
|
214
|
-
'offerRef',
|
|
215
|
-
'offerVersionRef',
|
|
216
|
-
'verifiedOfferRef'
|
|
217
|
-
],
|
|
218
|
-
'order.observe': [
|
|
219
|
-
'orderRef'
|
|
220
|
-
]
|
|
221
|
-
};
|
|
222
|
-
const input = x.input;
|
|
223
|
-
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some((k)=>!allowed[x.kind].includes(k))) e.push('$.input: closed kind-specific input required');
|
|
224
|
-
}
|
|
225
|
-
if (x.kind === 'action.receipt' && x.resultContract?.hardCriteriaMet === true && x.resultContract.blockers?.length) e.push('$.resultContract: hardCriteriaMet cannot coexist with blockers');
|
|
226
|
-
if (x.kind === 'user.turn' && Array.isArray(x.request?.approval)) e.push('$.request.approval: at most one approval');
|
|
227
|
-
if (x.kind === 'action.receipt' || x.kind === 'action.receipt.continuation') {
|
|
228
|
-
const r = x.kind === 'action.receipt.continuation' ? x.receipt : x;
|
|
229
|
-
if (r?.resultContract?.gapCodes?.some((code)=>!gapCodes.has(String(code)))) e.push('$.resultContract.gapCodes: unregistered gap code');
|
|
230
|
-
const observation = r?.observation;
|
|
231
|
-
if (observation?.kind === 'offer.availability') {
|
|
232
|
-
if (r.status === 'applied' && (observation.available !== true || !observation.verifiedOfferRef || observation.currentOfferVersionRef !== observation.checkedOfferVersionRef || observation.changedFactRefs?.length || observation.gapCodes?.length)) e.push('$.observation: applied availability requires unchanged checked/current version, verified ref, and no gaps');
|
|
233
|
-
if (r.status === 'applied' && (r.resultContract?.outcome !== 'complete' || r.resultContract?.hardCriteriaMet !== true || r.resultContract?.gapCodes?.length || r.resultContract?.blockers?.length)) e.push('$.resultContract: applied availability requires complete hard-criteria result without gaps or blockers');
|
|
234
|
-
if (r.status === 'changed' && (observation.available !== true || !observation.currentOfferVersionRef || observation.currentOfferVersionRef === observation.checkedOfferVersionRef || !observation.changedFactRefs?.length || observation.verifiedOfferRef)) e.push('$.observation: changed availability requires available replacement current version without verified ref');
|
|
235
|
-
if ((r.status === 'unavailable' || r.status === 'no_match') && (observation.available !== false || observation.currentOfferVersionRef || observation.verifiedOfferRef)) e.push('$.observation: negative availability must not expose current or verified version');
|
|
236
|
-
if (r.status !== 'applied' && observation.verifiedOfferRef) e.push('$.observation: only applied availability may expose verified ref');
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
const blocker = x.blocker ?? x.question?.blocker;
|
|
240
|
-
if (blocker && !/^(searchDraft|results|offers|checkout)(\.[A-Za-z][A-Za-z0-9_]*){0,3}$/.test(blocker.criterionPath ?? '')) e.push('$.blocker.criterionPath: closed criterion path required');
|
|
241
|
-
if (blocker?.evidence?.factRefs && new Set(blocker.evidence.factRefs).size !== blocker.evidence.factRefs.length) e.push('$.blocker.evidence.factRefs: unique refs required');
|
|
242
|
-
if (x.kind === 'question' && blocker && Array.isArray(x.question?.approvalOptions)) {
|
|
243
|
-
for (const [i, option] of x.question.approvalOptions.entries()){
|
|
244
|
-
const result = validateApprovalAgainstBlocker(option?.approval, blocker);
|
|
245
|
-
if (!result.ok) e.push(...result.errors.map((error)=>`$.question.approvalOptions[${i}].${error.slice(2)}`));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (kinds.has(String(x.kind)) && x.relaxationApprovalRef) {
|
|
249
|
-
const ref = x.relaxationApprovalRef;
|
|
250
|
-
if (ref.contextRef !== x.contextRef) e.push('$.relaxationApprovalRef.contextRef: must match action');
|
|
251
|
-
if (ref.sourceRevision !== x.expectedRevision) e.push('$.relaxationApprovalRef.sourceRevision: must equal action revision');
|
|
252
|
-
if (ref.targetActionKind !== x.kind) e.push('$.relaxationApprovalRef.targetActionKind: must match action kind');
|
|
253
|
-
if (ref.targetActionId !== x.actionId) e.push('$.relaxationApprovalRef.targetActionId: must match action id');
|
|
254
|
-
if (!safeOpaque(ref.nonce) || !isBookingDateTimeV2(ref.expiresAt)) e.push('$.relaxationApprovalRef: invalid expiry or nonce');
|
|
255
|
-
}
|
|
256
|
-
if (x.kind === 'question' && (!Array.isArray(x.question?.approvalOptions) || x.question.approvalOptions.length < 1 || x.question.approvalOptions.length > 2)) e.push('$.question.approvalOptions: one or two options required');
|
|
257
|
-
return e;
|
|
258
|
-
}
|
|
259
|
-
export function validateBookingSurfaceV2(value) {
|
|
260
|
-
const ok = validate(value);
|
|
261
|
-
const e = ok ? [] : errors(validate);
|
|
262
|
-
e.push(...cross(value));
|
|
263
|
-
return e.length ? {
|
|
264
|
-
ok: false,
|
|
265
|
-
errors: e
|
|
266
|
-
} : {
|
|
267
|
-
ok: true
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
function fromCanonical(fn, value) {
|
|
271
|
-
const ok = fn(value);
|
|
272
|
-
const e = ok ? [] : errors(fn);
|
|
273
|
-
e.push(...cross(value));
|
|
274
|
-
return e.length ? {
|
|
275
|
-
ok: false,
|
|
276
|
-
errors: e
|
|
277
|
-
} : {
|
|
278
|
-
ok: true
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
export function validateBookingReadActionV2(value) {
|
|
282
|
-
return fromCanonical(validateActionCanonical, value);
|
|
283
|
-
}
|
|
284
|
-
export function validateCriterionBlockerV2(value) {
|
|
285
|
-
return fromCanonical(validateBlockerCanonical, value);
|
|
286
|
-
}
|
|
287
|
-
export function validateRelaxationApprovalV2(value) {
|
|
288
|
-
return fromCanonical(validateApprovalCanonical, value);
|
|
289
|
-
}
|
|
290
|
-
export function validateBookingSurfaceEventV2(value) {
|
|
291
|
-
return fromCanonical(validateEventCanonical, value);
|
|
292
|
-
}
|
|
293
|
-
export function validateApprovalAgainstBlocker(approval, blocker) {
|
|
294
|
-
const a = approval;
|
|
295
|
-
const b = blocker;
|
|
296
|
-
const blockerResult = validateCriterionBlockerV2(b);
|
|
297
|
-
if (!blockerResult.ok) return blockerResult;
|
|
298
|
-
const approvalResult = validateRelaxationApprovalV2(a);
|
|
299
|
-
if (!approvalResult.ok) return approvalResult;
|
|
300
|
-
const fields = [
|
|
301
|
-
'blockerId',
|
|
302
|
-
'sourceActionId',
|
|
303
|
-
'sourceReceiptDigest',
|
|
304
|
-
'scope',
|
|
305
|
-
'code',
|
|
306
|
-
'criterionPath',
|
|
307
|
-
'valueDigest'
|
|
308
|
-
];
|
|
309
|
-
const mismatches = fields.filter((field)=>a[field] !== b?.[field]);
|
|
310
|
-
return mismatches.length ? {
|
|
311
|
-
ok: false,
|
|
312
|
-
errors: mismatches.map((field)=>`$.${field}: approval does not match blocker`)
|
|
313
|
-
} : {
|
|
314
|
-
ok: true
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
//# sourceURL=ts/src/booking-surface/validation-v2.ts
|