@itpay/cli 2.1.34 → 2.1.41
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 +5 -12
- package/dist/src/client/backend.js +2 -33
- package/dist/src/client/http.js +0 -2
- package/dist/src/commands/buy.js +4 -7
- package/dist/src/commands/cart.js +15 -15
- package/dist/src/commands/catalog.js +7 -17
- package/dist/src/commands/checkout.js +5 -146
- package/dist/src/commands/checkout_handoff.js +10 -48
- package/dist/src/commands/docs.js +18 -7
- package/dist/src/commands/guidance.js +19 -69
- package/dist/src/commands/install.js +3 -6
- package/dist/src/commands/order.js +1 -14
- package/dist/src/commands/orders.js +9 -4
- package/dist/src/commands/pay.js +1 -1
- package/dist/src/commands/readyz.js +2 -2
- package/dist/src/commands/refund.js +5 -5
- package/dist/src/commands/services.js +167 -1712
- package/dist/src/commands/skill.js +5 -8
- package/dist/src/commands/vault.js +20 -27
- package/dist/src/commands/vault_handoff.js +0 -18
- package/dist/src/main.js +58 -294
- package/dist/src/render/markdown.js +3 -0
- package/dist/src/render/qr.js +1 -1
- package/dist/src/state/cart_session.js +1 -4
- package/dist/src/state/client_context.js +0 -72
- package/dist/src/state/config.js +14 -44
- package/dist/src/state/device_authority.js +41 -141
- package/docs/agent/buyer/cart-checkout.json +5 -6
- package/docs/agent/buyer/catalog-list.json +6 -24
- package/docs/agent/buyer/identity-and-sessions.json +9 -25
- package/docs/agent/buyer/install-and-setup.json +15 -11
- package/docs/agent/buyer/orders-refunds.json +5 -12
- package/docs/agent/buyer/payment-flow.json +3 -4
- package/docs/agent/buyer/purchased-content.json +4 -18
- package/docs/agent/buyer/quickstart.json +16 -24
- package/docs/agent/buyer/render-hosts.json +12 -23
- package/docs/agent/buyer/service-feedback.json +1 -2
- package/docs/cli-reference/agent-types.md +2 -7
- package/docs/cli-reference/commands/buy.md +1 -2
- package/docs/cli-reference/commands/cart/add.md +1 -1
- package/docs/cli-reference/commands/cart/clear.md +1 -1
- package/docs/cli-reference/commands/cart/index.md +1 -1
- package/docs/cli-reference/commands/cart/next.md +1 -1
- package/docs/cli-reference/commands/cart/remove.md +1 -1
- package/docs/cli-reference/commands/cart/show.md +1 -1
- package/docs/cli-reference/commands/catalog/index.md +1 -1
- package/docs/cli-reference/commands/catalog/list.md +2 -2
- package/docs/cli-reference/commands/checkout.md +2 -12
- package/docs/cli-reference/commands/device.md +11 -18
- package/docs/cli-reference/commands/docs/index.md +1 -1
- package/docs/cli-reference/commands/docs/list.md +1 -1
- package/docs/cli-reference/commands/docs/search.md +1 -1
- package/docs/cli-reference/commands/docs/show.md +1 -1
- package/docs/cli-reference/commands/install.md +6 -8
- package/docs/cli-reference/commands/next.md +1 -1
- package/docs/cli-reference/commands/order.md +1 -1
- package/docs/cli-reference/commands/pay.md +1 -1
- package/docs/cli-reference/commands/readyz.md +10 -58
- package/docs/cli-reference/commands/refund/cancel.md +1 -1
- package/docs/cli-reference/commands/refund/create.md +1 -1
- package/docs/cli-reference/commands/refund/get.md +2 -2
- package/docs/cli-reference/commands/refund/index.md +1 -1
- package/docs/cli-reference/commands/refund/list.md +1 -1
- package/docs/cli-reference/commands/refund/watch.md +1 -1
- package/docs/cli-reference/commands/services/action.md +3 -24
- package/docs/cli-reference/commands/services/checkout.md +2 -13
- package/docs/cli-reference/commands/services/events.md +1 -1
- package/docs/cli-reference/commands/services/get.md +2 -4
- package/docs/cli-reference/commands/services/index.md +3 -4
- package/docs/cli-reference/commands/services/invoke.md +4 -6
- package/docs/cli-reference/commands/services/list.md +1 -1
- package/docs/cli-reference/commands/services/next.md +12 -105
- package/docs/cli-reference/commands/services/quote.md +3 -3
- package/docs/cli-reference/commands/services/read-result.md +6 -17
- package/docs/cli-reference/commands/services/start.md +2 -2
- package/docs/cli-reference/commands/vault/access.md +1 -5
- package/docs/cli-reference/conventions.md +6 -4
- package/docs/cli-reference/index.md +1 -8
- package/docs/skill-bundle-rollout/01-mcp-authentication.md +134 -0
- package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +210 -0
- package/docs/skill-bundle-rollout/03-platform-publishing.md +161 -0
- package/docs/skill-bundle-rollout/04-first-wave-platforms.md +69 -0
- package/docs/skill-bundle-rollout/05-sync-operations.md +62 -0
- package/docs/skill-bundle-rollout/README.md +151 -0
- package/package.json +13 -20
- package/skills/itpay/SKILL.md +105 -101
- package/assets/sell-preview/index.html +0 -6
- package/assets/sell-preview/preview.js +0 -9
- package/bin/darwin-amd64/itpay-sell +0 -0
- package/bin/darwin-arm64/itpay-sell +0 -0
- package/bin/linux-amd64/itpay-sell +0 -0
- package/bin/linux-arm64/itpay-sell +0 -0
- package/bin/windows-amd64/itpay-sell.exe +0 -0
- package/dist/src/commands/presentation.js +0 -153
- package/dist/src/commands/rail_catalog.js +0 -261
- package/dist/src/render/browser.js +0 -65
- package/dist/src/sell/commands.js +0 -80
- package/dist/src/sell/contract.js +0 -86
- package/dist/src/sell/local.js +0 -260
- package/dist/src/sell/mcp.js +0 -49
- package/dist/src/sell/preview.js +0 -70
- package/dist/src/sell/sync.js +0 -145
- package/dist/src/state/account_auth.js +0 -332
- package/dist/src/state/task_journal.js +0 -90
- package/docs/agent/buyer/rail-booking.json +0 -162
- package/docs/agent/buyer/rail-fast-checkout.json +0 -48
- package/docs/cli-reference/commands/auth.md +0 -96
- package/docs/cli-reference/commands/sell.md +0 -1364
- package/docs/cli-reference/commands/services/page.md +0 -71
- package/docs/cli-reference/commands/services/run.md +0 -156
- package/docs/sell.md +0 -84
- /package/bin/{itp.mjs → itp} +0 -0
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { CLI_VERSION, cliDistribution } from "../state/config.js";
|
|
2
|
-
export class RailCatalogEncodingError extends Error {
|
|
3
|
-
constructor(encoding) {
|
|
4
|
-
super(`catalog encoding ${JSON.stringify(encoding)} is not supported by CLI ${CLI_VERSION} ` +
|
|
5
|
-
`(${cliDistribution()}); obtain a compatible exact version through this installation channel. ` +
|
|
6
|
-
`The committed catalog is intact`);
|
|
7
|
-
this.name = "rail_catalog_encoding";
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
function scopeFromProfiles(value) {
|
|
11
|
-
const profiles = Array.isArray(value) ? value : [];
|
|
12
|
-
const selected = profiles.find((p) => Array.isArray(p) && Array.isArray(p[1]) && p[1].includes("balanced")) ?? profiles[0];
|
|
13
|
-
if (!Array.isArray(selected))
|
|
14
|
-
return { origin: "legacy_unknown", destination: "legacy_unknown",
|
|
15
|
-
duration_basis: "legacy_unknown", arrival_basis: "legacy_unknown", cost_basis: "legacy_unknown",
|
|
16
|
-
duration_minutes: null, estimated_total_cost: null, cost_incomplete: true };
|
|
17
|
-
const extras = selected[10] && typeof selected[10] === "object" ? selected[10] : {};
|
|
18
|
-
const total = typeof selected[9] === "number" && extras.ci !== true ? selected[9] / 100 : null;
|
|
19
|
-
return {
|
|
20
|
-
origin: extras.os ?? "legacy_unknown", destination: extras.ds ?? "legacy_unknown",
|
|
21
|
-
duration_basis: extras.db ?? "legacy_unknown", arrival_basis: extras.ab ?? "legacy_unknown",
|
|
22
|
-
cost_basis: extras.cb ?? "legacy_unknown", duration_minutes: selected[8] ?? null,
|
|
23
|
-
estimated_total_cost: total, cost_incomplete: extras.ci === true,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function asString(v) {
|
|
27
|
-
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
28
|
-
}
|
|
29
|
-
function asIndex(v, bound) {
|
|
30
|
-
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v >= bound) {
|
|
31
|
-
throw new Error("shared_rows: index out of bounds");
|
|
32
|
-
}
|
|
33
|
-
return v;
|
|
34
|
-
}
|
|
35
|
-
function asObject(value) {
|
|
36
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
37
|
-
}
|
|
38
|
-
function datedTime(date, value) {
|
|
39
|
-
const raw = asString(value);
|
|
40
|
-
if (!raw)
|
|
41
|
-
return null;
|
|
42
|
-
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(raw))
|
|
43
|
-
return raw.replace("T", " ").slice(0, 16);
|
|
44
|
-
const short = raw.match(/^(\d{2}:\d{2})(?:\+(\d+))?$/);
|
|
45
|
-
const day = asString(date);
|
|
46
|
-
if (!short || !day)
|
|
47
|
-
return null;
|
|
48
|
-
if (!short[2])
|
|
49
|
-
return `${day} ${short[1]}`;
|
|
50
|
-
const shifted = new Date(`${day}T00:00:00Z`);
|
|
51
|
-
if (Number.isNaN(shifted.getTime()))
|
|
52
|
-
return null;
|
|
53
|
-
shifted.setUTCDate(shifted.getUTCDate() + Number(short[2]));
|
|
54
|
-
return `${shifted.toISOString().slice(0, 10)} ${short[1]}`;
|
|
55
|
-
}
|
|
56
|
-
function elapsedMinutes(departure, arrival) {
|
|
57
|
-
if (!departure || !arrival)
|
|
58
|
-
return null;
|
|
59
|
-
const start = Date.parse(departure.replace(" ", "T") + "Z");
|
|
60
|
-
const end = Date.parse(arrival.replace(" ", "T") + "Z");
|
|
61
|
-
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? (end - start) / 60_000 : null;
|
|
62
|
-
}
|
|
63
|
-
function seatOffer(row, names) {
|
|
64
|
-
if (!Array.isArray(row))
|
|
65
|
-
return null;
|
|
66
|
-
const extras = asObject(row[7]);
|
|
67
|
-
const code = asString(row[1]);
|
|
68
|
-
return {
|
|
69
|
-
offer_ref: row[0], seat_type: code ?? null, seat_name: code ? names[code] ?? code : null,
|
|
70
|
-
inventory_status: row[2] ?? null, quantity: row[3] ?? null, unit_price_fen: row[4] ?? null,
|
|
71
|
-
eligible: row[5] === true, requires_quote: row[6] === true,
|
|
72
|
-
price_source: extras.s ?? "sale_price", quote_price_unverified: extras.u === true,
|
|
73
|
-
conflicted: extras.c === true,
|
|
74
|
-
...(Array.isArray(extras.o) ? { price_observations_fen: extras.o } : {}),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function decodeRide(row, wait, stations, services) {
|
|
78
|
-
if (!Array.isArray(row)) {
|
|
79
|
-
const ride = asObject(row);
|
|
80
|
-
if (!Object.keys(ride).length)
|
|
81
|
-
throw new Error("rail catalog: malformed ride row");
|
|
82
|
-
const refs = Array.isArray(ride.service_refs) ? ride.service_refs : [];
|
|
83
|
-
const codes = [...new Set(refs.map((ref) => asObject(services[asString(ref) ?? ""]).tc)
|
|
84
|
-
.filter((code) => typeof code === "string" && code.length > 0))];
|
|
85
|
-
const departure = datedTime(ride.boarding_date, ride.departure_at ?? ride.departure);
|
|
86
|
-
const arrival = datedTime(ride.boarding_date, ride.arrival_at ?? ride.arrival);
|
|
87
|
-
return { train_codes: codes, train_code: codes.join("/"),
|
|
88
|
-
from_station: stations[asString(ride.from_station_code) ?? ""] ?? ride.from_station ?? null,
|
|
89
|
-
to_station: stations[asString(ride.to_station_code) ?? ""] ?? ride.to_station ?? null,
|
|
90
|
-
boarding_date: ride.boarding_date ?? null,
|
|
91
|
-
departure, arrival, duration_minutes: elapsedMinutes(departure, arrival),
|
|
92
|
-
wait_minutes: typeof wait === "number" ? wait : ride.wait_minutes ?? null,
|
|
93
|
-
same_run: ride.same_run === true, onboard_stops: Array.isArray(ride.onboard_stops) ? ride.onboard_stops : [],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const extras = asObject(row[8]);
|
|
97
|
-
const refs = Array.isArray(row[4]) ? row[4] : [];
|
|
98
|
-
const codes = [...new Set(refs.map((ref) => asObject(services[asString(ref) ?? ""]).tc)
|
|
99
|
-
.filter((code) => typeof code === "string" && code.length > 0))];
|
|
100
|
-
const base = row[0];
|
|
101
|
-
const departure = datedTime(base, extras.dep);
|
|
102
|
-
const arrival = datedTime(base, extras.arr);
|
|
103
|
-
return {
|
|
104
|
-
train_codes: codes, train_code: codes.join("/"),
|
|
105
|
-
from_station: stations[asString(row[2]) ?? ""] ?? row[2] ?? null,
|
|
106
|
-
to_station: stations[asString(row[3]) ?? ""] ?? row[3] ?? null,
|
|
107
|
-
boarding_date: base ?? null,
|
|
108
|
-
departure, arrival, duration_minutes: elapsedMinutes(departure, arrival),
|
|
109
|
-
wait_minutes: typeof wait === "number" ? wait : typeof row[5] === "number" ? row[5] : null,
|
|
110
|
-
same_run: row[6] === true, onboard_stops: Array.isArray(row[7]) ? row[7] : [],
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function decodePlans(rawPlans, rawProfiles, packed, catalog, services, names, stations) {
|
|
114
|
-
const plans = Array.isArray(rawPlans) ? rawPlans : [];
|
|
115
|
-
const profiles = Array.isArray(rawProfiles) ? rawProfiles : [];
|
|
116
|
-
const preferred = profiles.find((profile) => Array.isArray(profile) && Array.isArray(profile[1]) && profile[1].includes("balanced")) ?? profiles[0];
|
|
117
|
-
const profile = Array.isArray(preferred) ? preferred : [];
|
|
118
|
-
const serviceIndex = Array.isArray(catalog.service_index) ? catalog.service_index : [];
|
|
119
|
-
const selectedPlan = packed && typeof profile[2] === "number" ? plans[asIndex(profile[2], plans.length)] :
|
|
120
|
-
plans.find((plan) => Array.isArray(plan) && plan[0] === profile[2]);
|
|
121
|
-
const selectedRef = Array.isArray(selectedPlan) ? selectedPlan[0] : null;
|
|
122
|
-
const decoded = plans.map((plan) => {
|
|
123
|
-
if (!Array.isArray(plan))
|
|
124
|
-
throw new Error("rail catalog: malformed ticket plan");
|
|
125
|
-
const refs = (Array.isArray(plan[1]) ? plan[1] : []).map((ref) => packed && typeof ref === "number" ? serviceIndex[asIndex(ref, serviceIndex.length)] : ref);
|
|
126
|
-
const extras = asObject(plan[7]);
|
|
127
|
-
const legs = refs.map((ref, index) => {
|
|
128
|
-
const service = asObject(services[asString(ref) ?? ""]);
|
|
129
|
-
const offers = Array.isArray(service.of) ? service.of : [];
|
|
130
|
-
const selectedOffer = plan[0] === selectedRef && Array.isArray(profile[3]) ? profile[3][index] : undefined;
|
|
131
|
-
const chosen = packed && typeof selectedOffer === "number" ? offers[asIndex(selectedOffer, offers.length)] :
|
|
132
|
-
offers.find((offer) => Array.isArray(offer) && offer[0] === selectedOffer);
|
|
133
|
-
return {
|
|
134
|
-
service_ref: ref ?? null, train_code: service.tc ?? null,
|
|
135
|
-
from_station: stations[asString(service.f) ?? ""] ?? service.f ?? null,
|
|
136
|
-
to_station: stations[asString(service.t) ?? ""] ?? service.t ?? null,
|
|
137
|
-
departure: datedTime(service.d, service.dep), arrival: datedTime(service.d, service.arr),
|
|
138
|
-
seat_options: offers.map((offer) => seatOffer(offer, names)).filter((offer) => offer !== null),
|
|
139
|
-
...(chosen ? { selected_seat: seatOffer(chosen, names) } : {}),
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
const amount = asObject(plan[3]);
|
|
143
|
-
return {
|
|
144
|
-
ticket_plan_ref: plan[0], passenger_count: plan[2] ?? null,
|
|
145
|
-
purchase_support: plan[6] ?? "none", rail_amount_verified: plan[4] === true,
|
|
146
|
-
rail_amount_fen: { default: amount.def ?? null, minimum: amount.min ?? null, maximum: amount.max ?? null },
|
|
147
|
-
service_fee_fen: plan[5] ?? null, quote_price_unverified: extras.qu === true,
|
|
148
|
-
legs,
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
const selected = decoded.find((plan) => plan.ticket_plan_ref === selectedRef);
|
|
152
|
-
return { plans: decoded, ...(selected ? { selected } : {}) };
|
|
153
|
-
}
|
|
154
|
-
// _cn_route_text port: "南头 C7608 → 广州南换乘71分 → G2944 重庆西" — kept
|
|
155
|
-
// byte-identical with the planner so a packed journey renders the same text
|
|
156
|
-
// its object form carried.
|
|
157
|
-
function regenerateRoute(ridePairs, rideTable, stations, services) {
|
|
158
|
-
const name = (code, fallback) => asString(stations[asString(code) ?? ""]) ?? asString(fallback) ?? asString(code) ?? "?";
|
|
159
|
-
const rides = ridePairs.map((pair) => {
|
|
160
|
-
if (!Array.isArray(pair) || pair.length !== 2)
|
|
161
|
-
throw new Error("shared_rows: malformed ride pair");
|
|
162
|
-
const src = rideTable[asIndex(pair[0], rideTable.length)];
|
|
163
|
-
if (!Array.isArray(src))
|
|
164
|
-
throw new Error("shared_rows: malformed ride row");
|
|
165
|
-
return { row: src, wait: pair[1] };
|
|
166
|
-
});
|
|
167
|
-
const first = rides[0];
|
|
168
|
-
if (first === undefined)
|
|
169
|
-
return "";
|
|
170
|
-
const trainCodes = (rideRow) => (Array.isArray(rideRow[4]) ? rideRow[4] : [])
|
|
171
|
-
.map((ref) => {
|
|
172
|
-
const svc = services[asString(ref) ?? ""];
|
|
173
|
-
return asString(svc?.tc);
|
|
174
|
-
})
|
|
175
|
-
.filter((c) => Boolean(c));
|
|
176
|
-
const waitOf = (ride) => typeof ride.wait === "number" ? ride.wait
|
|
177
|
-
: typeof ride.row[5] === "number" ? ride.row[5]
|
|
178
|
-
: undefined;
|
|
179
|
-
const sameRun = (rideRow) => rideRow[6] === true;
|
|
180
|
-
const parts = [`${name(first.row[2], undefined)} ${trainCodes(first.row).join("/")}`];
|
|
181
|
-
for (let i = 1; i < rides.length; i += 1) {
|
|
182
|
-
const prev = rides[i - 1].row;
|
|
183
|
-
const ride = rides[i];
|
|
184
|
-
const via = name(prev[3], undefined);
|
|
185
|
-
const wait = waitOf(ride);
|
|
186
|
-
parts.push(sameRun(ride.row)
|
|
187
|
-
? wait !== undefined ? `${via}同车停${wait}分` : `${via}同车接续`
|
|
188
|
-
: wait !== undefined ? `${via}换乘${wait}分` : `${via}换乘`);
|
|
189
|
-
parts.push(`${trainCodes(ride.row).join("/")} ${name(ride.row[3], undefined)}`);
|
|
190
|
-
}
|
|
191
|
-
if (rides.length === 1) {
|
|
192
|
-
parts.push(name(first.row[3], undefined));
|
|
193
|
-
}
|
|
194
|
-
return parts.filter((s) => s.trim().length > 0).join(" → ");
|
|
195
|
-
}
|
|
196
|
-
// decodeRailCatalogJourneys returns one summary per committed combination in
|
|
197
|
-
// catalog order. Legacy object-form catalogs (no `encoding`) pass through;
|
|
198
|
-
// shared_rows.v1 rows are positional under journey_columns
|
|
199
|
-
// [ref, route, rides, plans, gc, pr, profiles, tc, ev, rep, tw, risk].
|
|
200
|
-
// Any other encoding throws RailCatalogEncodingError — an explicit upgrade
|
|
201
|
-
// hint, never an empty catalog.
|
|
202
|
-
export function decodeRailCatalogJourneys(catalog) {
|
|
203
|
-
const encoding = catalog["encoding"];
|
|
204
|
-
const layerMap = catalog["choice_layers"]?.["journey_layer"] ?? {};
|
|
205
|
-
const layerOf = (ref) => layerMap[ref] === "backup" ? "backup" : "main";
|
|
206
|
-
const stations = asObject(catalog.stations);
|
|
207
|
-
const services = asObject(catalog.services);
|
|
208
|
-
const seatNames = asObject(catalog.seat_names);
|
|
209
|
-
if (encoding === undefined || encoding === null) {
|
|
210
|
-
const rows = Array.isArray(catalog["journeys"]) ? catalog["journeys"] : [];
|
|
211
|
-
return {
|
|
212
|
-
packed: false,
|
|
213
|
-
journeys: rows.map((j) => {
|
|
214
|
-
const rec = (j ?? {});
|
|
215
|
-
const ref = asString(rec["ref"]) ?? asString(rec["journey_id"]) ?? "?";
|
|
216
|
-
const calculationScope = scopeFromProfiles(rec["profiles"]);
|
|
217
|
-
const rides = (Array.isArray(rec.rides) ? rec.rides : []).map((ride) => decodeRide(ride, null, stations, services));
|
|
218
|
-
const ticketPlans = decodePlans(rec.plans, rec.profiles, false, catalog, services, seatNames, stations);
|
|
219
|
-
return {
|
|
220
|
-
ref,
|
|
221
|
-
route: asString(rec["route"]) ?? asString(rec["route_text"]) ?? asString(rec["summary"]) ?? "",
|
|
222
|
-
defaultLayer: layerOf(ref),
|
|
223
|
-
rides,
|
|
224
|
-
...(calculationScope ? { calculation_scope: calculationScope } : {}),
|
|
225
|
-
ticket_plans: ticketPlans.plans,
|
|
226
|
-
...(ticketPlans.selected ? { selected_plan: ticketPlans.selected } : {}),
|
|
227
|
-
};
|
|
228
|
-
}),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
if (encoding !== "shared_rows.v1") {
|
|
232
|
-
throw new RailCatalogEncodingError(encoding);
|
|
233
|
-
}
|
|
234
|
-
const columns = catalog["journey_columns"];
|
|
235
|
-
if (!Array.isArray(columns) || columns[0] !== "ref" || columns[1] !== "route" || columns[2] !== "rides") {
|
|
236
|
-
throw new RailCatalogEncodingError(`${String(encoding)} (unexpected journey_columns)`);
|
|
237
|
-
}
|
|
238
|
-
const rideTable = (Array.isArray(catalog["ride_table"]) ? catalog["ride_table"] : []);
|
|
239
|
-
const journeys = (Array.isArray(catalog["journeys"]) ? catalog["journeys"] : []);
|
|
240
|
-
return {
|
|
241
|
-
packed: true,
|
|
242
|
-
journeys: journeys.map((row) => {
|
|
243
|
-
if (!Array.isArray(row))
|
|
244
|
-
throw new Error("shared_rows: journey row is not an array");
|
|
245
|
-
const ref = asString(row[0]) ?? "?";
|
|
246
|
-
const route = asString(row[1]) ??
|
|
247
|
-
(Array.isArray(row[2]) ? regenerateRoute(row[2], rideTable, stations, services) : "");
|
|
248
|
-
const rides = (Array.isArray(row[2]) ? row[2] : []).map((pair) => {
|
|
249
|
-
if (!Array.isArray(pair) || pair.length !== 2)
|
|
250
|
-
throw new Error("shared_rows: malformed ride pair");
|
|
251
|
-
return decodeRide(rideTable[asIndex(pair[0], rideTable.length)], pair[1], stations, services);
|
|
252
|
-
});
|
|
253
|
-
const calculationScope = scopeFromProfiles(row[6]);
|
|
254
|
-
const ticketPlans = decodePlans(row[3], row[6], true, catalog, services, seatNames, stations);
|
|
255
|
-
return { ref, route, defaultLayer: layerOf(ref), rides,
|
|
256
|
-
...(calculationScope ? { calculation_scope: calculationScope } : {}),
|
|
257
|
-
ticket_plans: ticketPlans.plans,
|
|
258
|
-
...(ticketPlans.selected ? { selected_plan: ticketPlans.selected } : {}) };
|
|
259
|
-
}),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// Local browser dispatch for `--present browser`. Opens the URL via the OS
|
|
2
|
-
// opener with an argv array — never a model-generated shell string. Dispatch
|
|
3
|
-
// proves only that the request was accepted by the OS; it never means the
|
|
4
|
-
// page became visible (that would be `page_loaded`, which needs evidence).
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { platform } from "node:os";
|
|
7
|
-
// Official HTTPS origins (or the loopback dev origin behind ITPAY_CLI_DEV).
|
|
8
|
-
// No userinfo, no non-http(s) schemes, no redirect chains — the opener gets
|
|
9
|
-
// exactly this URL once.
|
|
10
|
-
export function isPresentableURL(raw, baseURL, env = process.env) {
|
|
11
|
-
let url;
|
|
12
|
-
try {
|
|
13
|
-
url = new URL(raw);
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
if (url.username || url.password)
|
|
19
|
-
return false;
|
|
20
|
-
if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(url.origin))
|
|
21
|
-
return true;
|
|
22
|
-
if (url.protocol !== "https:")
|
|
23
|
-
return false;
|
|
24
|
-
const allowedOrigins = new Set(["https://app.itpay.ai", "https://sandbox.itpay.ai", "https://dev.itpay.ai"]);
|
|
25
|
-
if (baseURL) {
|
|
26
|
-
try {
|
|
27
|
-
allowedOrigins.add(new URL(baseURL).origin);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
/* ignore malformed base URL */
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return allowedOrigins.has(url.origin);
|
|
34
|
-
}
|
|
35
|
-
export function openInSystemBrowser(raw, baseURL, env = process.env) {
|
|
36
|
-
if (!isPresentableURL(raw, baseURL, env)) {
|
|
37
|
-
return Promise.resolve({ status: "failed", reason: "url_rejected" });
|
|
38
|
-
}
|
|
39
|
-
const [command, args] = openerArgv(raw);
|
|
40
|
-
if (!command) {
|
|
41
|
-
return Promise.resolve({ status: "unavailable", reason: "no system opener on this platform" });
|
|
42
|
-
}
|
|
43
|
-
return new Promise((resolvePromise) => {
|
|
44
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
45
|
-
child.once("error", (error) => resolvePromise({ status: "failed", reason: error.message }));
|
|
46
|
-
child.once("spawn", () => {
|
|
47
|
-
child.unref();
|
|
48
|
-
resolvePromise({ status: "dispatch_accepted", argv: [command, ...args] });
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
function openerArgv(url) {
|
|
53
|
-
switch (platform()) {
|
|
54
|
-
case "darwin":
|
|
55
|
-
return ["open", [url]];
|
|
56
|
-
case "win32":
|
|
57
|
-
return ["cmd.exe", ["/c", "start", "", url]];
|
|
58
|
-
case "linux":
|
|
59
|
-
case "freebsd":
|
|
60
|
-
case "openbsd":
|
|
61
|
-
return ["xdg-open", [url]];
|
|
62
|
-
default:
|
|
63
|
-
return [undefined, []];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { sellerAuth } from "../state/account_auth.js";
|
|
2
|
-
import { registerSync } from "./sync.js";
|
|
3
|
-
import { preview } from "./preview.js";
|
|
4
|
-
import { serveSellMCP } from "./mcp.js";
|
|
5
|
-
import { registerLocal } from "./local.js";
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { loadConfig, newBackendClient } from '../state/config.js';
|
|
8
|
-
import { SELL_GUIDE, SELL_OPERATIONS, sellRequest } from './contract.js';
|
|
9
|
-
export function registerSell(program) {
|
|
10
|
-
const sell = program.command('sell').description('Create, test and submit your service for review');
|
|
11
|
-
const auth = sell.command('auth').description('Authorize this CLI using normal ItPay account login');
|
|
12
|
-
for (const action of ['login', 'status', 'logout'])
|
|
13
|
-
auth.command(action).action(async () => {
|
|
14
|
-
process.stdout.write(JSON.stringify(await sellerAuth(action, loadConfig().baseURL)) + '\n');
|
|
15
|
-
});
|
|
16
|
-
const groups = new Map([['', sell]]);
|
|
17
|
-
for (const operation of SELL_OPERATIONS) {
|
|
18
|
-
const parts = operation.command.split(' ');
|
|
19
|
-
const leaf = parts.pop();
|
|
20
|
-
let parent = sell, key = '';
|
|
21
|
-
for (const part of parts) {
|
|
22
|
-
key = key ? `${key} ${part}` : part;
|
|
23
|
-
let group = groups.get(key);
|
|
24
|
-
if (!group) {
|
|
25
|
-
group = parent.command(part);
|
|
26
|
-
groups.set(key, group);
|
|
27
|
-
}
|
|
28
|
-
parent = group;
|
|
29
|
-
}
|
|
30
|
-
const command = parent.command(leaf).description(operation.description).option('--json', 'Structured output').option('--input-json <file>', 'Request fields as JSON');
|
|
31
|
-
const required = [...operation.path.matchAll(/\{(\w+)\}/g)].map(m => m[1]);
|
|
32
|
-
for (const field of required)
|
|
33
|
-
command.option(`--${field.replaceAll('_', '-')} <value>`, field);
|
|
34
|
-
if (operation.confirmation)
|
|
35
|
-
command.option('--confirm', 'User explicitly confirmed the disclosed version/action');
|
|
36
|
-
command.action(async (options) => {
|
|
37
|
-
try {
|
|
38
|
-
if (operation.command === 'guide' && !options.merchantId) {
|
|
39
|
-
process.stdout.write(JSON.stringify({ ...SELL_GUIDE, operations: SELL_OPERATIONS }, null, 2) + '\n');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (operation.confirmation && !options.confirm)
|
|
43
|
-
throw new Error('Explicit user confirmation required after reviewing this action; pass --confirm only after user approval');
|
|
44
|
-
const input = options.inputJson ? JSON.parse(readFileSync(String(options.inputJson), 'utf8')) : {};
|
|
45
|
-
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
46
|
-
throw new Error('Input JSON must be an object');
|
|
47
|
-
const params = {};
|
|
48
|
-
for (const field of required)
|
|
49
|
-
params[field] = options[field.replace(/_([a-z])/g, (_, c) => c.toUpperCase())];
|
|
50
|
-
const request = sellRequest(operation, params, input);
|
|
51
|
-
const result = await newBackendClient(loadConfig()).sellRequest(request);
|
|
52
|
-
process.stdout.write(JSON.stringify({ status: 'ok', operation: operation.command, result: result ?? null }, null, 2) + '\n');
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
process.exitCode = 1;
|
|
56
|
-
process.stdout.write(JSON.stringify({ status: 'error', operation: operation.command, message: error instanceof Error ? error.message : 'Sell operation failed', next: 'itpay sell guide --json' }, null, 2) + '\n');
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
registerLocal(sell);
|
|
61
|
-
registerSync(sell);
|
|
62
|
-
sell.commands.find(command => command.name() === "workflow").command("preview").option("--project <directory>", "Project directory", ".").action(async (options) => { const result = await preview(options.project); process.stdout.write(JSON.stringify({ url: result.url, instruction: "Open this local URL to inspect the saved workflow" }) + "\n"); });
|
|
63
|
-
sell.command("mcp").description("Serve local Seller MCP over stdio").requiredOption("--stdio").option("--project <directory>", "Project directory", ".").action(async (options) => { await serveSellMCP(options.project); });
|
|
64
|
-
const submission = sell.commands.find(command => command.name() === "submission");
|
|
65
|
-
submission.command("watch").requiredOption("--merchant-id <id>").requiredOption("--submission-id <id>").option("--timeout <seconds>", "Maximum wait", "120").option("--json").action(async (options) => {
|
|
66
|
-
const timeout = Number(options.timeout);
|
|
67
|
-
if (!Number.isFinite(timeout) || timeout < 0 || timeout > 600)
|
|
68
|
-
throw new Error("timeout must be between 0 and 600");
|
|
69
|
-
const backend = newBackendClient(loadConfig()), until = Date.now() + timeout * 1000;
|
|
70
|
-
for (;;) {
|
|
71
|
-
const result = await backend.sellRequest({ method: "GET", path: "/v1/seller/organizations/" + encodeURIComponent(options.merchantId) + "/service-submissions/" + encodeURIComponent(options.submissionId) });
|
|
72
|
-
if (!["submitted", "pending", "under_review"].includes(result.status) || Date.now() >= until) {
|
|
73
|
-
process.stdout.write(JSON.stringify({ status: result.status, result }) + "\n");
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return sell;
|
|
80
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
const org = "/v1/seller/organizations/{merchant_id}";
|
|
2
|
-
const draft = `${org}/service-drafts/{draft_id}`;
|
|
3
|
-
const read = "itpay.seller.read", write = "itpay.seller.write", test = "itpay.seller.test", submit = "itpay.seller.submit";
|
|
4
|
-
export const SELL_GUIDE = {
|
|
5
|
-
version: "itpay.sell.v1", command: "itpay sell", truth: "workflow.yaml",
|
|
6
|
-
steps: ["auth", "setup", "sources", "workflow", "local_test", "confirm", "push", "platform_verification", "pricing", "submission", "publication"],
|
|
7
|
-
rules: [
|
|
8
|
-
"Use the authenticated user's existing merchant. KYB and payout changes remain in the dashboard.",
|
|
9
|
-
"Resolve every API against its declared method, Content-Type, parameters, response schema and locked version. Ask for missing inputs; never guess credentials or change contracts.",
|
|
10
|
-
"Only saved versions may run. A run is not a saved version. Local evidence cannot grant platform test approval.",
|
|
11
|
-
"Use the platform node catalog and validator for supported pricing, quotas, confirmation and durable fulfillment. Every paid success path must cross payment and delivery gates.",
|
|
12
|
-
"Use own credentials for local requests. Platform-managed credentials stay on the platform. Mark execution location honestly.",
|
|
13
|
-
"Present side effects before live tests. Unknown results stop automatic retries. Local payment/refund/delivery gates are simulated, never real funds.",
|
|
14
|
-
"Show the exact workflow and version before user confirmation. Preview price, delivery and current terms before explicit submission approval.",
|
|
15
|
-
"Use platform guide for next actions. Submission is not publication: only an actual admin approval publishes the service."
|
|
16
|
-
]
|
|
17
|
-
};
|
|
18
|
-
export const SELL_OPERATIONS = [
|
|
19
|
-
{ command: "status", method: "GET", path: "/v1/seller/organizations", scope: read, fields: {}, description: "List existing merchant memberships" },
|
|
20
|
-
{ command: "guide", method: "GET", path: `${org}/guide`, scope: read, fields: { draft_id: "string" }, optional: ["draft_id"], description: "Read authoritative publication blockers and next actions" },
|
|
21
|
-
{ command: "library search", method: "GET", path: "/v1/library/apis", scope: read, fields: { q: "string" }, optional: ["q"], description: "Search available API contracts" },
|
|
22
|
-
{ command: "library get", method: "GET", path: "/v1/library/apis/{library_api_id}", scope: read, fields: {}, description: "Inspect an API contract" },
|
|
23
|
-
{ command: "sources list", method: "GET", path: `${org}/api-intakes`, scope: read, fields: {}, description: "List imported API sources" },
|
|
24
|
-
{ command: "sources inspect", method: "GET", path: `${org}/api-intakes/{intake_id}`, scope: read, fields: {}, description: "Inspect imported operations and verification results" },
|
|
25
|
-
{ command: "sources import", method: "POST", path: `${org}/api-intakes`, scope: write, fields: { provider_key: "string", source_url: "string", document: "document", media_type: "string" }, optional: ["source_url", "document", "media_type"], description: "Import an OpenAPI source without inventing request contracts" },
|
|
26
|
-
{ command: "sources library", method: "POST", path: `${org}/api-intakes/from-library`, scope: write, fields: { library_api_id: "string" }, description: "Bind a platform library source" },
|
|
27
|
-
{ command: "sources probe", method: "POST", path: `${org}/api-intakes/{intake_id}/probe`, scope: test, fields: { credential_profile_id: "string", operation_inputs: "object" }, description: "Verify selected API requests on the platform", confirmation: true },
|
|
28
|
-
{ command: "credentials status", method: "GET", path: `${org}/provider-credential-profiles`, scope: read, fields: {}, description: "Read credential bindings without secret values" },
|
|
29
|
-
{ command: "services list", method: "GET", path: `${org}/service-drafts`, scope: read, fields: {}, description: "List own service drafts and publication state" },
|
|
30
|
-
{ command: "services create", method: "POST", path: `${org}/service-drafts`, scope: write, fields: { service_id: "string", public_name: "string" }, description: "Create a platform service draft" },
|
|
31
|
-
{ command: "services icon", method: "PUT", path: `${draft}/icon/content`, scope: write, fields: { content_base64: "string" }, description: "Upload a PNG/JPEG service icon, max 512 KiB, as signed JSON" },
|
|
32
|
-
{ command: "services get", method: "GET", path: `${draft}/workflow`, scope: read, fields: {}, description: "Read the current workflow and service settings" },
|
|
33
|
-
{ command: "workflow catalog", method: "GET", path: `${org}/node-catalog`, scope: read, fields: {}, description: "Read exact supported node templates and required configuration" },
|
|
34
|
-
{ command: "workflow plan", method: "POST", path: `${draft}/orchestration-runs`, scope: test, fields: { expected_revision: "number", instruction: "string", api_intake_ids: "array", credential_profile_bindings: "object" }, description: "Explicitly use ItPay AI planning and platform verification", confirmation: true },
|
|
35
|
-
{ command: "workflow plan-get", method: "GET", path: `${draft}/orchestration-runs/{run_id}`, scope: read, fields: {}, description: "Read the AI candidate and diagnostics" },
|
|
36
|
-
{ command: "workflow plan-apply", method: "POST", path: `${draft}/orchestration-runs/{run_id}/apply`, scope: write, fields: { expected_revision: "number" }, description: "Apply a reviewed AI candidate to the draft", confirmation: true },
|
|
37
|
-
{ command: "workflow upload", method: "PUT", path: `${draft}/workflow`, scope: write, fields: { expected_revision: "number", public_name: "string", arazzo: "object" }, optional: ["public_name"], description: "Update the platform workflow with optimistic concurrency" },
|
|
38
|
-
{ command: "workflow validate-platform", method: "POST", path: `${draft}/workflow/validate`, scope: write, fields: {}, description: "Validate using the same public runtime rules as approval" },
|
|
39
|
-
{ command: "workflow versions list-platform", method: "GET", path: `${draft}/workflow/versions`, scope: read, fields: {}, description: "List explicitly saved platform versions" },
|
|
40
|
-
{ command: "workflow versions save-platform", method: "POST", path: `${draft}/workflow/versions`, scope: write, fields: { name: "string", source: "string", arazzo_document: "string", expected_revision: "number" }, description: "Save an explicit immutable platform version" },
|
|
41
|
-
{ command: "workflow versions delete-platform", method: "DELETE", path: `${draft}/workflow/versions/{version_id}`, scope: write, fields: { expected_revision: "number" }, description: "Delete an eligible saved version and its runs", confirmation: true },
|
|
42
|
-
{ command: "fixtures get", method: "GET", path: `${draft}/fixtures`, scope: read, fields: {}, description: "Read test inputs" },
|
|
43
|
-
{ command: "fixtures set", method: "PUT", path: `${draft}/fixtures`, scope: write, fields: { expected_revision: "number", fixtures: "array" }, description: "Save explicit test inputs" },
|
|
44
|
-
{ command: "pricing get", method: "GET", path: `${draft}/workflow`, scope: read, fields: {}, description: "Read the current price and refund policy" },
|
|
45
|
-
{ command: "pricing set", method: "PUT", path: `${draft}/pricing`, scope: write, fields: { expected_revision: "number", pricing: "object", policy: "object" }, description: "Set per-call price and platform refund policy" },
|
|
46
|
-
{ command: "verify", method: "POST", path: `${draft}/validation-runs`, scope: test, fields: { workflow_version_id: "string", expected_semantic_revision: "number", expected_fixture_revision: "number" }, description: "Start platform verification for an explicitly saved version", confirmation: true },
|
|
47
|
-
{ command: "tests start-checkout", method: "POST", path: `${draft}/live-checkout-tests`, scope: test, fields: { expected_revision: "number", agent_instance_id: "string" }, description: "Dev admin only: freeze one success fixture into a private real-checkout test bound to your registered Agent. Payment still requires user confirmation; this does not publish the service.", confirmation: true },
|
|
48
|
-
{ command: "tests accept-checkout", method: "POST", path: `${draft}/live-checkout-tests/{execution_id}/accept`, scope: test, fields: {}, description: "Record server-observed issued tickets and completed delivery as evidence for the exact tested revision. Never synthesizes payment or issuance.", confirmation: true },
|
|
49
|
-
{ command: "runs get", method: "GET", path: `${draft}/validation-runs/{run_id}`, scope: read, fields: {}, description: "Inspect platform test nodes and diagnostics" },
|
|
50
|
-
{ command: "runs confirm", method: "POST", path: `${draft}/validation-runs/{run_id}/confirm`, scope: test, fields: { risk_hash: "string" }, description: "Confirm the exact disclosed platform test side effects", confirmation: true },
|
|
51
|
-
{ command: "submission preview", method: "GET", path: `${org}/guide`, scope: read, fields: { draft_id: "string" }, description: "Read publication readiness and required agreements" },
|
|
52
|
-
{ command: "submission submit", method: "POST", path: `${draft}/submit`, scope: submit, fields: { expected_revision: "number", terms_version: "string", confirmations: "array" }, description: "Submit the confirmed version for review, never approve it", confirmation: true },
|
|
53
|
-
{ command: "submission get", method: "GET", path: `${org}/service-submissions/{submission_id}`, scope: read, fields: {}, description: "Read the actual review/publication result" },
|
|
54
|
-
{ command: "submission withdraw", method: "POST", path: `${org}/service-submissions/{submission_id}/withdraw`, scope: submit, fields: {}, description: "Withdraw own pending submission", confirmation: true },
|
|
55
|
-
];
|
|
56
|
-
export function sellRequest(operation, params, input = {}) {
|
|
57
|
-
let path = operation.path.replace(/\{(\w+)\}/g, (_, key) => {
|
|
58
|
-
const value = params[key];
|
|
59
|
-
if (typeof value !== "string" || !value.trim())
|
|
60
|
-
throw new Error(`${key} is required`);
|
|
61
|
-
return encodeURIComponent(value);
|
|
62
|
-
});
|
|
63
|
-
for (const key of Object.keys(input))
|
|
64
|
-
if (!operation.fields[key])
|
|
65
|
-
throw new Error(`Unsupported field: ${key}`);
|
|
66
|
-
for (const [key, type] of Object.entries(operation.fields)) {
|
|
67
|
-
const value = input[key];
|
|
68
|
-
if (value === undefined && operation.optional?.includes(key))
|
|
69
|
-
continue;
|
|
70
|
-
const valid = type === "document" ? (typeof value === "string" || (!!value && typeof value === "object" && !Array.isArray(value))) : type === "array" ? Array.isArray(value) : type === "object" ? !!value && typeof value === "object" && !Array.isArray(value) : typeof value === type;
|
|
71
|
-
if (!valid)
|
|
72
|
-
throw new Error(`${key} must be ${type}`);
|
|
73
|
-
if (type === "number" && (!Number.isSafeInteger(value) || Number(value) < 0))
|
|
74
|
-
throw new Error(`${key} must be a nonnegative integer`);
|
|
75
|
-
}
|
|
76
|
-
if (operation.method === "GET") {
|
|
77
|
-
const query = [];
|
|
78
|
-
for (const [key, value] of Object.entries(input))
|
|
79
|
-
if (value !== undefined)
|
|
80
|
-
query.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
81
|
-
if (query.length)
|
|
82
|
-
path += `?${query.join("&")}`;
|
|
83
|
-
return { path, method: operation.method };
|
|
84
|
-
}
|
|
85
|
-
return { path, method: operation.method, body: input };
|
|
86
|
-
}
|