@mmerterden/multi-agent-toolkit-mcp 3.9.0 → 3.12.0
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/CHANGELOG.md +239 -0
- package/README.md +79 -2
- package/README.tr.md +6 -3
- package/index.js +160 -21
- package/package.json +9 -3
- package/tools/code-intel/index.js +665 -0
- package/tools/code-intel/kotlin.js +159 -0
- package/tools/code-intel/lsp-client.js +422 -0
- package/tools/code-intel/pool.js +273 -0
- package/tools/code-intel/positions.js +195 -0
- package/tools/code-intel/swift.js +249 -0
- package/tools/ios-app-store-audit/context.js +160 -9
- package/tools/ios-app-store-audit/index.js +24 -1
- package/tools/offload/index.js +28 -8
- package/tools/pass-kit/index.js +432 -0
- package/tools/pass-kit/sign.js +255 -0
- package/tools/pass-kit/spec.js +317 -0
- package/tools/pass-kit/validate.js +329 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec.js - what Apple requires of a pass, as data.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a table rather than a code path, because all of it is
|
|
5
|
+
* somebody else's specification and the only useful property is that it is easy
|
|
6
|
+
* to correct. Nothing in this file is tied to an airline, a brand or a project;
|
|
7
|
+
* the five styles are the five Apple defines.
|
|
8
|
+
*
|
|
9
|
+
* The enum lists matter more than they look. A single invalid
|
|
10
|
+
* `PKPassengerCapability*` value does not produce an error - Wallet silently
|
|
11
|
+
* declines to render the enhanced layout and the pass falls back to the old
|
|
12
|
+
* one, with nothing anywhere saying why. That failure mode is the reason this
|
|
13
|
+
* file exists at all.
|
|
14
|
+
*
|
|
15
|
+
* @module tools/pass-kit/spec
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const STYLES = ["boardingPass", "coupon", "eventTicket", "generic", "storeCard"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Top-level keys every pass must carry, whatever its style.
|
|
22
|
+
* Apple rejects a pass missing any of these outright.
|
|
23
|
+
*/
|
|
24
|
+
export const REQUIRED_TOP_LEVEL = [
|
|
25
|
+
"description",
|
|
26
|
+
"formatVersion",
|
|
27
|
+
"organizationName",
|
|
28
|
+
"passTypeIdentifier",
|
|
29
|
+
"serialNumber",
|
|
30
|
+
"teamIdentifier",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Images, per style.
|
|
35
|
+
*
|
|
36
|
+
* `required` is what Wallet will not render without. `optional` is what the
|
|
37
|
+
* style can use. Anything outside both lists is dead weight in the manifest -
|
|
38
|
+
* worth reporting, because it is usually a naming mistake rather than a spare.
|
|
39
|
+
*
|
|
40
|
+
* Retina variants are implied: `logo` means logo.png, logo@2x.png, logo@3x.png,
|
|
41
|
+
* and the 1x file carries no suffix. That last detail is a real trap - a file
|
|
42
|
+
* named `footer@1x.png` is not the 1x footer, it is a file Wallet ignores.
|
|
43
|
+
*/
|
|
44
|
+
export const ASSETS = {
|
|
45
|
+
boardingPass: { required: ["icon", "logo"], optional: ["footer", "background", "thumbnail"] },
|
|
46
|
+
coupon: { required: ["icon", "logo"], optional: ["strip"] },
|
|
47
|
+
eventTicket: { required: ["icon", "logo"], optional: ["strip", "background", "thumbnail"] },
|
|
48
|
+
generic: { required: ["icon", "logo"], optional: ["thumbnail"] },
|
|
49
|
+
storeCard: { required: ["icon", "logo"], optional: ["strip"] },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Every image name a pass may legitimately contain, at any scale. */
|
|
53
|
+
export const ALL_IMAGE_BASENAMES = [
|
|
54
|
+
"icon",
|
|
55
|
+
"logo",
|
|
56
|
+
"strip",
|
|
57
|
+
"background",
|
|
58
|
+
"thumbnail",
|
|
59
|
+
"footer",
|
|
60
|
+
"personalizationLogo",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export const SCALES = ["", "@2x", "@3x"];
|
|
64
|
+
|
|
65
|
+
/** boardingPass carries one extra required key inside its style dictionary. */
|
|
66
|
+
export const STYLE_REQUIRED_KEYS = {
|
|
67
|
+
boardingPass: ["transitType"],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const TRANSIT_TYPES = [
|
|
71
|
+
"PKTransitTypeAir",
|
|
72
|
+
"PKTransitTypeBoat",
|
|
73
|
+
"PKTransitTypeBus",
|
|
74
|
+
"PKTransitTypeGeneric",
|
|
75
|
+
"PKTransitTypeTrain",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const BARCODE_FORMATS = [
|
|
79
|
+
"PKBarcodeFormatQR",
|
|
80
|
+
"PKBarcodeFormatPDF417",
|
|
81
|
+
"PKBarcodeFormatAztec",
|
|
82
|
+
"PKBarcodeFormatCode128",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export const FIELD_BUCKETS = [
|
|
86
|
+
"headerFields",
|
|
87
|
+
"primaryFields",
|
|
88
|
+
"secondaryFields",
|
|
89
|
+
"auxiliaryFields",
|
|
90
|
+
"backFields",
|
|
91
|
+
// Newer than the other five and only meaningful on a boardingPass. Listed
|
|
92
|
+
// because a real, working, signed enhanced boarding pass uses it: reporting
|
|
93
|
+
// it as "a bucket Apple does not define" was this validator being wrong
|
|
94
|
+
// about somebody else's correct pass, which is worse than saying nothing.
|
|
95
|
+
"footerFields",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Enumerated semantic values.
|
|
100
|
+
*
|
|
101
|
+
* An unknown value in one of these is the quiet failure described in the file
|
|
102
|
+
* header: no error, no warning, and an enhanced pass that silently renders as a
|
|
103
|
+
* legacy one.
|
|
104
|
+
*/
|
|
105
|
+
export const SEMANTIC_ENUMS = {
|
|
106
|
+
passengerCapabilities: [
|
|
107
|
+
"PKPassengerCapabilityPreBoarding",
|
|
108
|
+
"PKPassengerCapabilityPriorityBoarding",
|
|
109
|
+
"PKPassengerCapabilityCarryon",
|
|
110
|
+
"PKPassengerCapabilityPersonalItem",
|
|
111
|
+
],
|
|
112
|
+
departureLocationSecurityPrograms: [
|
|
113
|
+
"PKTransitSecurityProgramTSAPreCheck",
|
|
114
|
+
"PKTransitSecurityProgramGlobalEntry",
|
|
115
|
+
"PKTransitSecurityProgramClear",
|
|
116
|
+
"PKTransitSecurityProgramNexus",
|
|
117
|
+
"PKTransitSecurityProgramSentri",
|
|
118
|
+
"PKTransitSecurityProgramTSAPreCheckTouchlessID",
|
|
119
|
+
"PKTransitSecurityProgramFastTrack",
|
|
120
|
+
],
|
|
121
|
+
eventType: [
|
|
122
|
+
"PKEventTypeGeneric",
|
|
123
|
+
"PKEventTypeLivePerformance",
|
|
124
|
+
"PKEventTypeMovie",
|
|
125
|
+
"PKEventTypeSports",
|
|
126
|
+
"PKEventTypeConference",
|
|
127
|
+
"PKEventTypeConvention",
|
|
128
|
+
"PKEventTypeWorkshop",
|
|
129
|
+
"PKEventTypeSocialGathering",
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
SEMANTIC_ENUMS.destinationLocationSecurityPrograms =
|
|
134
|
+
SEMANTIC_ENUMS.departureLocationSecurityPrograms;
|
|
135
|
+
SEMANTIC_ENUMS.passengerEligibleSecurityPrograms =
|
|
136
|
+
SEMANTIC_ENUMS.departureLocationSecurityPrograms;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Semantic tags, by the type a validator can actually check.
|
|
140
|
+
*
|
|
141
|
+
* Not exhaustive of Apple's catalogue and not trying to be - it covers the
|
|
142
|
+
* shapes that are wrong often enough to be worth catching: a date that is not
|
|
143
|
+
* ISO 8601, a number sent as a string, a location missing a coordinate.
|
|
144
|
+
*/
|
|
145
|
+
export const SEMANTIC_TYPES = {
|
|
146
|
+
// Flight and transit
|
|
147
|
+
airlineCode: "string",
|
|
148
|
+
flightCode: "string",
|
|
149
|
+
flightNumber: "number",
|
|
150
|
+
departureAirportCode: "string",
|
|
151
|
+
departureAirportName: "string",
|
|
152
|
+
departureCityName: "string",
|
|
153
|
+
departureStationName: "string",
|
|
154
|
+
departurePlatform: "string",
|
|
155
|
+
departureLocationDescription: "string",
|
|
156
|
+
departureGate: "string",
|
|
157
|
+
departureTerminal: "string",
|
|
158
|
+
departureLocation: "location",
|
|
159
|
+
departureLocationTimeZone: "string",
|
|
160
|
+
destinationAirportCode: "string",
|
|
161
|
+
destinationAirportName: "string",
|
|
162
|
+
destinationCityName: "string",
|
|
163
|
+
destinationStationName: "string",
|
|
164
|
+
destinationPlatform: "string",
|
|
165
|
+
destinationLocationDescription: "string",
|
|
166
|
+
destinationGate: "string",
|
|
167
|
+
destinationTerminal: "string",
|
|
168
|
+
destinationLocation: "location",
|
|
169
|
+
destinationLocationTimeZone: "string",
|
|
170
|
+
boardingGroup: "string",
|
|
171
|
+
boardingSequenceNumber: "string",
|
|
172
|
+
boardingZone: "string",
|
|
173
|
+
originalBoardingDate: "date",
|
|
174
|
+
currentBoardingDate: "date",
|
|
175
|
+
originalDepartureDate: "date",
|
|
176
|
+
currentDepartureDate: "date",
|
|
177
|
+
originalArrivalDate: "date",
|
|
178
|
+
currentArrivalDate: "date",
|
|
179
|
+
transitProvider: "string",
|
|
180
|
+
transitStatus: "string",
|
|
181
|
+
transitStatusReason: "string",
|
|
182
|
+
vehicleName: "string",
|
|
183
|
+
vehicleNumber: "string",
|
|
184
|
+
vehicleType: "string",
|
|
185
|
+
carNumber: "string",
|
|
186
|
+
confirmationNumber: "string",
|
|
187
|
+
passengerName: "personName",
|
|
188
|
+
membershipProgramName: "string",
|
|
189
|
+
membershipProgramNumber: "string",
|
|
190
|
+
priorityStatus: "string",
|
|
191
|
+
ticketFareClass: "string",
|
|
192
|
+
securityScreening: "string",
|
|
193
|
+
membershipProgramStatus: "string",
|
|
194
|
+
internationalDocumentsAreVerified: "boolean",
|
|
195
|
+
internationalDocumentsVerifiedDeclarationName: "string",
|
|
196
|
+
passengerAirlineSSRs: "array",
|
|
197
|
+
passengerInformationSSRs: "array",
|
|
198
|
+
passengerServiceSSRs: "array",
|
|
199
|
+
loungePlaceIDs: "array",
|
|
200
|
+
wifiAccess: "array",
|
|
201
|
+
seats: "array",
|
|
202
|
+
// Event
|
|
203
|
+
eventName: "string",
|
|
204
|
+
eventType: "string",
|
|
205
|
+
eventStartDate: "date",
|
|
206
|
+
eventEndDate: "date",
|
|
207
|
+
venueName: "string",
|
|
208
|
+
venueLocation: "location",
|
|
209
|
+
venueRoom: "string",
|
|
210
|
+
venueEntrance: "string",
|
|
211
|
+
venueRegionName: "string",
|
|
212
|
+
venuePhoneNumber: "string",
|
|
213
|
+
venueOpenDate: "date",
|
|
214
|
+
venueDoorsOpenDate: "date",
|
|
215
|
+
venueGatesOpenDate: "date",
|
|
216
|
+
venueCloseDate: "date",
|
|
217
|
+
entranceDescription: "string",
|
|
218
|
+
admissionLevelAbbreviation: "string",
|
|
219
|
+
additionalTicketAttributes: "string",
|
|
220
|
+
tailgatingAllowed: "boolean",
|
|
221
|
+
admissionLevel: "string",
|
|
222
|
+
attendeeName: "string",
|
|
223
|
+
performerNames: "array",
|
|
224
|
+
genre: "string",
|
|
225
|
+
sportName: "string",
|
|
226
|
+
leagueName: "string",
|
|
227
|
+
leagueAbbreviation: "string",
|
|
228
|
+
homeTeamName: "string",
|
|
229
|
+
homeTeamAbbreviation: "string",
|
|
230
|
+
homeTeamLocation: "string",
|
|
231
|
+
awayTeamName: "string",
|
|
232
|
+
awayTeamAbbreviation: "string",
|
|
233
|
+
awayTeamLocation: "string",
|
|
234
|
+
artistIDs: "array",
|
|
235
|
+
albumIDs: "array",
|
|
236
|
+
playlistIDs: "array",
|
|
237
|
+
// General
|
|
238
|
+
totalPrice: "currency",
|
|
239
|
+
balance: "currency",
|
|
240
|
+
duration: "number",
|
|
241
|
+
silenceRequested: "boolean",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Keys renamed between iOS 18 and iOS 26.
|
|
246
|
+
*
|
|
247
|
+
* A pass carrying the old spelling is not rejected; the tag is ignored, which
|
|
248
|
+
* means the feature it drives silently does not appear.
|
|
249
|
+
*/
|
|
250
|
+
export const RENAMED_SEMANTICS = {
|
|
251
|
+
departureAirportTimeZone: "departureLocationTimeZone",
|
|
252
|
+
destinationAirportTimeZone: "destinationLocationTimeZone",
|
|
253
|
+
airlinePassengerCapabilities: "passengerCapabilities",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Semantic tags an enhanced boarding pass needs before Wallet will use the
|
|
258
|
+
* richer layout. Missing any of them is not an error - the pass simply renders
|
|
259
|
+
* the way it did before, which is exactly why it is worth reporting.
|
|
260
|
+
*/
|
|
261
|
+
export const ENHANCED_BOARDING_PASS_TAGS = [
|
|
262
|
+
"airlineCode",
|
|
263
|
+
"flightNumber",
|
|
264
|
+
"departureAirportCode",
|
|
265
|
+
"destinationAirportCode",
|
|
266
|
+
"originalDepartureDate",
|
|
267
|
+
"originalArrivalDate",
|
|
268
|
+
"originalBoardingDate",
|
|
269
|
+
"departureLocationTimeZone",
|
|
270
|
+
"destinationLocationTimeZone",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const ISO_8601 =
|
|
274
|
+
/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @returns {string|null} why the value is wrong for its declared type
|
|
278
|
+
*/
|
|
279
|
+
export function checkSemanticValue(key, type, value) {
|
|
280
|
+
switch (type) {
|
|
281
|
+
case "string":
|
|
282
|
+
return typeof value === "string" ? null : `must be a string, got ${typeOf(value)}`;
|
|
283
|
+
case "number":
|
|
284
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
285
|
+
? null
|
|
286
|
+
: `must be a number, got ${typeOf(value)}`;
|
|
287
|
+
case "boolean":
|
|
288
|
+
return typeof value === "boolean" ? null : `must be a boolean, got ${typeOf(value)}`;
|
|
289
|
+
case "array":
|
|
290
|
+
return Array.isArray(value) ? null : `must be an array, got ${typeOf(value)}`;
|
|
291
|
+
case "date":
|
|
292
|
+
if (typeof value !== "string") return `must be an ISO 8601 string, got ${typeOf(value)}`;
|
|
293
|
+
return ISO_8601.test(value) ? null : `"${value}" is not ISO 8601`;
|
|
294
|
+
case "location":
|
|
295
|
+
if (!value || typeof value !== "object") return `must be an object with latitude and longitude`;
|
|
296
|
+
if (typeof value.latitude !== "number" || typeof value.longitude !== "number") {
|
|
297
|
+
return "latitude and longitude are both required and must be numbers";
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
case "currency":
|
|
301
|
+
if (!value || typeof value !== "object") return "must be an object";
|
|
302
|
+
if (typeof value.currencyCode !== "string") return "currencyCode is required";
|
|
303
|
+
if (typeof value.amount !== "string") return "amount must be a STRING, not a number";
|
|
304
|
+
return null;
|
|
305
|
+
case "personName":
|
|
306
|
+
if (!value || typeof value !== "object") return "must be an object of name components";
|
|
307
|
+
return null;
|
|
308
|
+
default:
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function typeOf(v) {
|
|
314
|
+
if (v === null) return "null";
|
|
315
|
+
if (Array.isArray(v)) return "array";
|
|
316
|
+
return typeof v;
|
|
317
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate.js - every check that can be made without signing anything.
|
|
3
|
+
*
|
|
4
|
+
* Pure: it takes a parsed pass.json and a list of file names, and returns
|
|
5
|
+
* findings. No I/O, so it is the same function whether it is checking a draft
|
|
6
|
+
* before a build or an existing `.pkpass` after one.
|
|
7
|
+
*
|
|
8
|
+
* Findings are graded because the consequences genuinely differ:
|
|
9
|
+
*
|
|
10
|
+
* error Wallet refuses the pass, or the pass is wrong about itself.
|
|
11
|
+
* warning Wallet accepts it and quietly does less than intended - the
|
|
12
|
+
* enhanced layout falls back, a localized string never resolves, a
|
|
13
|
+
* renamed tag is ignored. This grade is the point of the file.
|
|
14
|
+
* note Harmless, but somebody meant something by it.
|
|
15
|
+
*
|
|
16
|
+
* @module tools/pass-kit/validate
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
STYLES,
|
|
21
|
+
REQUIRED_TOP_LEVEL,
|
|
22
|
+
ASSETS,
|
|
23
|
+
ALL_IMAGE_BASENAMES,
|
|
24
|
+
STYLE_REQUIRED_KEYS,
|
|
25
|
+
TRANSIT_TYPES,
|
|
26
|
+
BARCODE_FORMATS,
|
|
27
|
+
FIELD_BUCKETS,
|
|
28
|
+
SEMANTIC_ENUMS,
|
|
29
|
+
SEMANTIC_TYPES,
|
|
30
|
+
RENAMED_SEMANTICS,
|
|
31
|
+
ENHANCED_BOARDING_PASS_TAGS,
|
|
32
|
+
checkSemanticValue,
|
|
33
|
+
} from "./spec.js";
|
|
34
|
+
|
|
35
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
36
|
+
const RGB_COLOR = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} pass parsed pass.json
|
|
40
|
+
* @param {string[]} files file names present beside it, e.g. ["icon.png", "en.lproj/pass.strings"]
|
|
41
|
+
* @param {object} [opts]
|
|
42
|
+
* @returns {{style: string|null, errors: object[], warnings: object[], notes: object[], ok: boolean}}
|
|
43
|
+
*/
|
|
44
|
+
export function validatePass(pass, files = [], opts = {}) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
const warnings = [];
|
|
47
|
+
const notes = [];
|
|
48
|
+
const add = (list, where, message) => list.push({ where, message });
|
|
49
|
+
|
|
50
|
+
if (!pass || typeof pass !== "object") {
|
|
51
|
+
return { style: null, errors: [{ where: "pass.json", message: "not a JSON object" }], warnings, notes, ok: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const key of REQUIRED_TOP_LEVEL) {
|
|
55
|
+
if (pass[key] === undefined || pass[key] === "") add(errors, key, "required top-level key is missing");
|
|
56
|
+
}
|
|
57
|
+
if (pass.formatVersion !== undefined && pass.formatVersion !== 1) {
|
|
58
|
+
add(errors, "formatVersion", `must be 1, got ${JSON.stringify(pass.formatVersion)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const present = STYLES.filter((s) => pass[s] !== undefined);
|
|
62
|
+
let style = null;
|
|
63
|
+
if (present.length === 0) {
|
|
64
|
+
add(errors, "pass.json", `no style dictionary: exactly one of ${STYLES.join(", ")} is required`);
|
|
65
|
+
} else if (present.length > 1) {
|
|
66
|
+
add(errors, "pass.json", `${present.length} style dictionaries (${present.join(", ")}); a pass has exactly one`);
|
|
67
|
+
} else {
|
|
68
|
+
style = present[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (style) {
|
|
72
|
+
const body = pass[style] || {};
|
|
73
|
+
for (const key of STYLE_REQUIRED_KEYS[style] || []) {
|
|
74
|
+
if (body[key] === undefined) add(errors, `${style}.${key}`, "required for this style");
|
|
75
|
+
}
|
|
76
|
+
if (style === "boardingPass" && body.transitType && !TRANSIT_TYPES.includes(body.transitType)) {
|
|
77
|
+
add(errors, "boardingPass.transitType", `"${body.transitType}" is not one of ${TRANSIT_TYPES.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
for (const bucket of Object.keys(body)) {
|
|
80
|
+
if (!FIELD_BUCKETS.includes(bucket) && bucket !== "transitType") {
|
|
81
|
+
add(notes, `${style}.${bucket}`, "not a field bucket Apple defines; it will be ignored");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const bucket of FIELD_BUCKETS) {
|
|
85
|
+
const rows = body[bucket];
|
|
86
|
+
if (rows === undefined) continue;
|
|
87
|
+
if (!Array.isArray(rows)) {
|
|
88
|
+
add(errors, `${style}.${bucket}`, "must be an array");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
rows.forEach((f, i) => {
|
|
92
|
+
const at = `${style}.${bucket}[${i}]`;
|
|
93
|
+
if (!f || typeof f !== "object") return add(errors, at, "must be an object");
|
|
94
|
+
if (typeof f.key !== "string" || !f.key) add(errors, at, "`key` is required");
|
|
95
|
+
if (f.value === undefined) add(errors, at, "`value` is required");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
checkColors(pass, errors);
|
|
101
|
+
checkBarcodes(pass, errors, warnings);
|
|
102
|
+
checkSemantics(pass, style, errors, warnings, notes, opts);
|
|
103
|
+
checkWebService(pass, errors, warnings);
|
|
104
|
+
checkAssets(style, files, errors, warnings, notes);
|
|
105
|
+
checkLocalization(pass, style, files, warnings);
|
|
106
|
+
checkLabels(pass, style, files, warnings);
|
|
107
|
+
|
|
108
|
+
return { style, errors, warnings, notes, ok: errors.length === 0 };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkColors(pass, errors) {
|
|
112
|
+
for (const key of ["foregroundColor", "backgroundColor", "labelColor", "stripColor"]) {
|
|
113
|
+
const v = pass[key];
|
|
114
|
+
if (v === undefined) continue;
|
|
115
|
+
if (typeof v !== "string" || (!HEX_COLOR.test(v) && !RGB_COLOR.test(v))) {
|
|
116
|
+
errors.push({ where: key, message: `must be #RRGGBB or rgb(r,g,b), got ${JSON.stringify(v)}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkBarcodes(pass, errors, warnings) {
|
|
122
|
+
const list = pass.barcodes;
|
|
123
|
+
if (list === undefined) {
|
|
124
|
+
if (pass.barcode === undefined) {
|
|
125
|
+
warnings.push({ where: "barcodes", message: "no barcode: the pass will scan nowhere" });
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(list)) return errors.push({ where: "barcodes", message: "must be an array" });
|
|
130
|
+
list.forEach((b, i) => {
|
|
131
|
+
const at = `barcodes[${i}]`;
|
|
132
|
+
if (!b || typeof b !== "object") return errors.push({ where: at, message: "must be an object" });
|
|
133
|
+
if (!BARCODE_FORMATS.includes(b.format)) {
|
|
134
|
+
errors.push({ where: `${at}.format`, message: `"${b.format}" is not one of ${BARCODE_FORMATS.join(", ")}` });
|
|
135
|
+
}
|
|
136
|
+
if (typeof b.message !== "string" || !b.message) {
|
|
137
|
+
errors.push({ where: `${at}.message`, message: "required" });
|
|
138
|
+
}
|
|
139
|
+
if (typeof b.messageEncoding !== "string" || !b.messageEncoding) {
|
|
140
|
+
errors.push({ where: `${at}.messageEncoding`, message: 'required, usually "iso-8859-1"' });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkSemantics(pass, style, errors, warnings, notes, opts) {
|
|
146
|
+
// Semantics live at the top level and may also appear per field. Both are
|
|
147
|
+
// checked; the top-level block is the one the enhanced layouts read.
|
|
148
|
+
const blocks = [{ at: "semantics", value: pass.semantics }];
|
|
149
|
+
if (style && pass[style]) {
|
|
150
|
+
for (const bucket of FIELD_BUCKETS) {
|
|
151
|
+
(pass[style][bucket] || []).forEach((f, i) => {
|
|
152
|
+
if (f && f.semantics) blocks.push({ at: `${style}.${bucket}[${i}].semantics`, value: f.semantics });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const { at, value } of blocks) {
|
|
158
|
+
if (value === undefined) continue;
|
|
159
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
160
|
+
errors.push({ where: at, message: "must be an object" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (const [key, v] of Object.entries(value)) {
|
|
164
|
+
const renamedTo = RENAMED_SEMANTICS[key];
|
|
165
|
+
if (renamedTo) {
|
|
166
|
+
warnings.push({
|
|
167
|
+
where: `${at}.${key}`,
|
|
168
|
+
message: `renamed to "${renamedTo}" in iOS 26. The old spelling is not rejected, it is ignored - so whatever it drives silently does not appear`,
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const allowed = SEMANTIC_ENUMS[key];
|
|
173
|
+
if (allowed) {
|
|
174
|
+
const values = Array.isArray(v) ? v : [v];
|
|
175
|
+
for (const one of values) {
|
|
176
|
+
if (!allowed.includes(one)) {
|
|
177
|
+
errors.push({
|
|
178
|
+
where: `${at}.${key}`,
|
|
179
|
+
message: `"${one}" is not a value Apple defines. One unknown value here does not raise an error: Wallet declines the enhanced layout and falls back silently. Allowed: ${allowed.join(", ")}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const type = SEMANTIC_TYPES[key];
|
|
186
|
+
if (!type) {
|
|
187
|
+
notes.push({ where: `${at}.${key}`, message: "not a semantic tag this validator knows; it may be new or misspelled" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const bad = checkSemanticValue(key, type, v);
|
|
191
|
+
if (bad) errors.push({ where: `${at}.${key}`, message: bad });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const top = pass.semantics || {};
|
|
196
|
+
// `original*` is the schedule and `current*` is the live value. Rewriting
|
|
197
|
+
// `original*` on an update destroys the only record of what changed, which is
|
|
198
|
+
// what the delay notifications are computed from.
|
|
199
|
+
for (const pair of [
|
|
200
|
+
["originalDepartureDate", "currentDepartureDate"],
|
|
201
|
+
["originalArrivalDate", "currentArrivalDate"],
|
|
202
|
+
["originalBoardingDate", "currentBoardingDate"],
|
|
203
|
+
]) {
|
|
204
|
+
if (top[pair[1]] !== undefined && top[pair[0]] === undefined) {
|
|
205
|
+
warnings.push({
|
|
206
|
+
where: `semantics.${pair[1]}`,
|
|
207
|
+
message: `set without ${pair[0]}. The original is the schedule and the current is the live value; with no original there is nothing to compare against`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (style === "boardingPass" && opts.enhanced !== false) {
|
|
213
|
+
const missing = ENHANCED_BOARDING_PASS_TAGS.filter((t) => top[t] === undefined);
|
|
214
|
+
if (missing.length && missing.length < ENHANCED_BOARDING_PASS_TAGS.length) {
|
|
215
|
+
warnings.push({
|
|
216
|
+
where: "semantics",
|
|
217
|
+
message: `an enhanced boarding pass wants ${missing.length} more tag(s): ${missing.join(", ")}. Missing them is not an error - the pass renders the older layout instead, with nothing saying why`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function checkWebService(pass, errors, warnings) {
|
|
224
|
+
const hasUrl = typeof pass.webServiceURL === "string" && pass.webServiceURL !== "";
|
|
225
|
+
const hasToken = typeof pass.authenticationToken === "string" && pass.authenticationToken !== "";
|
|
226
|
+
if (hasUrl !== hasToken) {
|
|
227
|
+
errors.push({
|
|
228
|
+
where: hasUrl ? "authenticationToken" : "webServiceURL",
|
|
229
|
+
message: "webServiceURL and authenticationToken go together; one without the other disables updates",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (hasUrl && !/^https:\/\//i.test(pass.webServiceURL)) {
|
|
233
|
+
errors.push({ where: "webServiceURL", message: "must be https" });
|
|
234
|
+
}
|
|
235
|
+
if (hasToken && pass.authenticationToken.length < 16) {
|
|
236
|
+
warnings.push({
|
|
237
|
+
where: "authenticationToken",
|
|
238
|
+
message: "shorter than 16 characters; Apple requires at least 16",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function checkAssets(style, files, errors, warnings, notes) {
|
|
244
|
+
if (!style) return;
|
|
245
|
+
const names = new Set(files.map((f) => f.split("/").pop()));
|
|
246
|
+
const has = (base) => names.has(`${base}.png`) || names.has(`${base}@2x.png`) || names.has(`${base}@3x.png`);
|
|
247
|
+
const contract = ASSETS[style] || { required: [], optional: [] };
|
|
248
|
+
|
|
249
|
+
for (const base of contract.required) {
|
|
250
|
+
if (!has(base)) {
|
|
251
|
+
// Tagged, because the pass.json-only caller has no files to check and has
|
|
252
|
+
// to drop these. It used to drop them by matching the message text, which
|
|
253
|
+
// stops working the moment the wording is improved - silently, and in the
|
|
254
|
+
// direction of reporting missing images that were never claimed present.
|
|
255
|
+
errors.push({ where: `${base}.png`, message: `required for a ${style}`, kind: "asset" });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const base of ALL_IMAGE_BASENAMES) {
|
|
259
|
+
if (!has(base)) continue;
|
|
260
|
+
if (contract.required.includes(base) || contract.optional.includes(base)) continue;
|
|
261
|
+
notes.push({ where: `${base}.png`, message: `a ${style} does not use this image; it is weight in the manifest` });
|
|
262
|
+
}
|
|
263
|
+
// The 1x file carries NO suffix. `footer@1x.png` is not the 1x footer - it is
|
|
264
|
+
// a file Wallet never looks for, hashed into the manifest for nothing.
|
|
265
|
+
for (const n of names) {
|
|
266
|
+
if (/@1x\.png$/.test(n)) {
|
|
267
|
+
warnings.push({
|
|
268
|
+
where: n,
|
|
269
|
+
message: `the 1x variant has no suffix. Wallet looks for "${n.replace("@1x", "")}" and will not find this file`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const base of contract.required) {
|
|
274
|
+
if (has(base) && !names.has(`${base}@2x.png`)) {
|
|
275
|
+
notes.push({ where: `${base}@2x.png`, message: "no @2x variant; it will be upscaled on every modern device" });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function checkLocalization(pass, style, files, warnings) {
|
|
281
|
+
const lprojs = new Set(
|
|
282
|
+
files.filter((f) => f.includes(".lproj/")).map((f) => f.split(".lproj/")[0].split("/").pop()),
|
|
283
|
+
);
|
|
284
|
+
if (lprojs.size === 0) return;
|
|
285
|
+
const strings = new Set(files.filter((f) => f.endsWith(".lproj/pass.strings")).map((f) => f.split(".lproj/")[0].split("/").pop()));
|
|
286
|
+
for (const l of lprojs) {
|
|
287
|
+
if (!strings.has(l)) {
|
|
288
|
+
warnings.push({ where: `${l}.lproj`, message: "no pass.strings; every label in this language falls back" });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Labels, which are the quietest way a pass ends up half-translated.
|
|
295
|
+
*
|
|
296
|
+
* Deliberately NOT inside checkLocalization: this first lived there, behind its
|
|
297
|
+
* early return, and therefore never ran on a pass with no .lproj folder at all -
|
|
298
|
+
* which is the case where a key-shaped label is worst, because it renders as the
|
|
299
|
+
* key. A test caught it.
|
|
300
|
+
*/
|
|
301
|
+
function checkLabels(pass, style, files, warnings) {
|
|
302
|
+
if (!style || !pass[style]) return;
|
|
303
|
+
const labels = [];
|
|
304
|
+
for (const bucket of FIELD_BUCKETS) {
|
|
305
|
+
(pass[style][bucket] || []).forEach((f) => {
|
|
306
|
+
if (f && typeof f.label === "string" && f.label) labels.push(f.label);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (labels.length === 0) return;
|
|
310
|
+
|
|
311
|
+
const keyish = labels.filter((l) => /^[a-z][A-Za-z0-9_]*$/.test(l) || /_/.test(l));
|
|
312
|
+
const hasStrings = files.some((f) => f.endsWith(".lproj/pass.strings"));
|
|
313
|
+
|
|
314
|
+
if (keyish.length > 0 && !hasStrings) {
|
|
315
|
+
warnings.push({
|
|
316
|
+
where: `${style} labels`,
|
|
317
|
+
message: `${keyish.length} label(s) look like localization keys (${keyish.slice(0, 4).map((l) => JSON.stringify(l)).join(", ")}) but no .lproj/pass.strings was found. An unresolved key renders as the key itself`,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (labels.length >= 2 && keyish.length > 0 && keyish.length < labels.length) {
|
|
323
|
+
const literals = labels.filter((l) => !keyish.includes(l)).slice(0, 4);
|
|
324
|
+
warnings.push({
|
|
325
|
+
where: `${style} labels`,
|
|
326
|
+
message: `${keyish.length} of ${labels.length} labels look like localization keys and the rest look like literal text (e.g. ${literals.map((l) => JSON.stringify(l)).join(", ")}). Mixed labels localize the keys and leave the literals in one language`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|