@mmerterden/multi-agent-toolkit-mcp 3.9.0 → 3.11.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 +169 -0
- package/README.md +79 -2
- package/README.tr.md +6 -3
- package/index.js +36 -3
- package/package.json +3 -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/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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sign.js - manifest, signature, archive. And where the passphrase comes from.
|
|
3
|
+
*
|
|
4
|
+
* NO SHELL. Every child is `execFileSync(bin, [argv])`. The passphrase reaches
|
|
5
|
+
* openssl through `-passin env:VAR` and never as an argument, because an
|
|
6
|
+
* argument is world-readable in `ps` for as long as the process runs. It is
|
|
7
|
+
* never written to disk, never logged, and scrubbed out of any error text
|
|
8
|
+
* before that text is returned.
|
|
9
|
+
*
|
|
10
|
+
* WHERE IT COMES FROM, in order: an explicit `passphrase_env` naming an
|
|
11
|
+
* environment variable, then a keychain entry, then nothing. A literal
|
|
12
|
+
* passphrase is not an accepted input at all - a tool that takes one invites a
|
|
13
|
+
* caller to put it in a config file, and that is how the project this replaces
|
|
14
|
+
* ended up publishing its own in a README three times.
|
|
15
|
+
*
|
|
16
|
+
* @module tools/pass-kit/sign
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { readdirSync, readFileSync, statSync, writeFileSync, mkdtempSync } from "node:fs";
|
|
22
|
+
import { join, relative, sep } from "node:path";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
|
|
25
|
+
const ERROR_PREFIX = "ERROR: ";
|
|
26
|
+
|
|
27
|
+
/** Files the manifest never covers, because they are produced from it. */
|
|
28
|
+
const NOT_HASHED = new Set(["manifest.json", "signature"]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every file in a `.pass` directory, as manifest-relative names.
|
|
32
|
+
* Localization folders are included with their `xx.lproj/` prefix.
|
|
33
|
+
*/
|
|
34
|
+
export function listPassFiles(dir) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const walk = (d) => {
|
|
37
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
38
|
+
if (e.name.startsWith(".")) continue;
|
|
39
|
+
const p = join(d, e.name);
|
|
40
|
+
if (e.isDirectory()) walk(p);
|
|
41
|
+
else out.push(relative(dir, p).split(sep).join("/"));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk(dir);
|
|
45
|
+
return out.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SHA-1 of every file, which is what Apple specifies - not a choice.
|
|
50
|
+
*/
|
|
51
|
+
export function buildManifest(dir, files) {
|
|
52
|
+
const manifest = {};
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
if (NOT_HASHED.has(f)) continue;
|
|
55
|
+
manifest[f] = createHash("sha1").update(readFileSync(join(dir, f))).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
return manifest;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read a passphrase without it ever becoming an argument or a file.
|
|
62
|
+
* @returns {{value: string}|{error: string}}
|
|
63
|
+
*/
|
|
64
|
+
export function resolvePassphrase({ passphrase_env, keychain_account, keychain_service }) {
|
|
65
|
+
if (passphrase_env) {
|
|
66
|
+
const v = process.env[passphrase_env];
|
|
67
|
+
if (!v) {
|
|
68
|
+
return {
|
|
69
|
+
error: `${ERROR_PREFIX}the environment variable ${passphrase_env} is unset or empty`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { value: v, source: `env:${passphrase_env}` };
|
|
73
|
+
}
|
|
74
|
+
if (keychain_account) {
|
|
75
|
+
try {
|
|
76
|
+
const v = execFileSync(
|
|
77
|
+
"/usr/bin/security",
|
|
78
|
+
[
|
|
79
|
+
"find-generic-password",
|
|
80
|
+
"-a",
|
|
81
|
+
keychain_account,
|
|
82
|
+
"-s",
|
|
83
|
+
keychain_service || "pkpass",
|
|
84
|
+
"-w",
|
|
85
|
+
],
|
|
86
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10000 },
|
|
87
|
+
).trim();
|
|
88
|
+
if (v) return { value: v, source: `keychain:${keychain_account}` };
|
|
89
|
+
} catch {
|
|
90
|
+
/* fall through to the error below */
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
error: `${ERROR_PREFIX}no keychain entry for account "${keychain_account}" in service "${keychain_service || "pkpass"}". Store one with: security add-generic-password -a ${keychain_account} -s ${keychain_service || "pkpass"} -w '<passphrase>'`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
error: `${ERROR_PREFIX}no passphrase source. Give passphrase_env (the NAME of an environment variable) or keychain_account. A literal passphrase is deliberately not accepted.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Certificate metadata, without ever touching the private key. */
|
|
102
|
+
export function inspectCertificate(certPath) {
|
|
103
|
+
try {
|
|
104
|
+
const subject = openssl(["x509", "-in", certPath, "-noout", "-subject"]).trim();
|
|
105
|
+
const dates = openssl(["x509", "-in", certPath, "-noout", "-dates"]).trim();
|
|
106
|
+
const notAfter = /notAfter=(.+)/.exec(dates)?.[1]?.trim() || null;
|
|
107
|
+
const notBefore = /notBefore=(.+)/.exec(dates)?.[1]?.trim() || null;
|
|
108
|
+
const expiresAt = notAfter ? new Date(notAfter) : null;
|
|
109
|
+
const expired = expiresAt ? expiresAt.getTime() < Date.now() : null;
|
|
110
|
+
const uid = /UID\s*=\s*([^,/]+)/.exec(subject)?.[1]?.trim() || null;
|
|
111
|
+
const ou = /OU\s*=\s*([^,/]+)/.exec(subject)?.[1]?.trim() || null;
|
|
112
|
+
return {
|
|
113
|
+
path: certPath,
|
|
114
|
+
passTypeIdentifier: uid,
|
|
115
|
+
teamIdentifier: ou,
|
|
116
|
+
notBefore,
|
|
117
|
+
notAfter,
|
|
118
|
+
expired,
|
|
119
|
+
daysLeft: expiresAt ? Math.floor((expiresAt.getTime() - Date.now()) / 86400000) : null,
|
|
120
|
+
};
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { path: certPath, error: `cannot read: ${short(e)}` };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function openssl(args, opts = {}) {
|
|
127
|
+
return execFileSync("openssl", args, {
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
130
|
+
timeout: 30000,
|
|
131
|
+
...opts,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function short(e) {
|
|
136
|
+
return String(e?.stderr || e?.message || e)
|
|
137
|
+
.trim()
|
|
138
|
+
.slice(0, 300);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Detached PKCS#7 signature over the manifest.
|
|
143
|
+
*
|
|
144
|
+
* @returns {{ok: true}|{error: string}}
|
|
145
|
+
*/
|
|
146
|
+
export function signManifest({ manifestPath, signaturePath, certificate, privateKey, wwdr, passphrase }) {
|
|
147
|
+
const env = { ...process.env, PKPASS_KEY_PASSPHRASE: passphrase };
|
|
148
|
+
try {
|
|
149
|
+
execFileSync(
|
|
150
|
+
"openssl",
|
|
151
|
+
[
|
|
152
|
+
"smime",
|
|
153
|
+
"-binary",
|
|
154
|
+
"-sign",
|
|
155
|
+
"-certfile",
|
|
156
|
+
wwdr,
|
|
157
|
+
"-signer",
|
|
158
|
+
certificate,
|
|
159
|
+
"-inkey",
|
|
160
|
+
privateKey,
|
|
161
|
+
"-in",
|
|
162
|
+
manifestPath,
|
|
163
|
+
"-out",
|
|
164
|
+
signaturePath,
|
|
165
|
+
"-passin",
|
|
166
|
+
"env:PKPASS_KEY_PASSPHRASE",
|
|
167
|
+
"-outform",
|
|
168
|
+
"DER",
|
|
169
|
+
],
|
|
170
|
+
{ env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 60000 },
|
|
171
|
+
);
|
|
172
|
+
return { ok: true };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return { error: `${ERROR_PREFIX}signing failed: ${scrub(short(e), passphrase)}` };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Verify a signature against its manifest and report the chain.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately does NOT assert trust: the WWDR chain is not in the system store
|
|
182
|
+
* on every machine, and a verification that fails for that reason would read as
|
|
183
|
+
* a broken pass. `-noverify` checks the signature itself; the chain is reported
|
|
184
|
+
* so a human can see what signed it.
|
|
185
|
+
*/
|
|
186
|
+
export function verifySignature({ signaturePath, manifestPath }) {
|
|
187
|
+
try {
|
|
188
|
+
openssl([
|
|
189
|
+
"smime",
|
|
190
|
+
"-verify",
|
|
191
|
+
"-binary",
|
|
192
|
+
"-inform",
|
|
193
|
+
"DER",
|
|
194
|
+
"-in",
|
|
195
|
+
signaturePath,
|
|
196
|
+
"-content",
|
|
197
|
+
manifestPath,
|
|
198
|
+
"-noverify",
|
|
199
|
+
"-out",
|
|
200
|
+
"/dev/null",
|
|
201
|
+
]);
|
|
202
|
+
const certs = openssl(["pkcs7", "-inform", "DER", "-in", signaturePath, "-print_certs", "-noout"]);
|
|
203
|
+
const chain = certs
|
|
204
|
+
.split("\n")
|
|
205
|
+
.map((l) => l.trim())
|
|
206
|
+
.filter((l) => l.startsWith("subject="))
|
|
207
|
+
.map((l) => l.replace(/^subject=\s*/, ""));
|
|
208
|
+
return { valid: true, chain };
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return { valid: false, reason: short(e) };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Zip a directory into a .pkpass, with the entries at the archive root. */
|
|
215
|
+
export function archive(passDir, outPath) {
|
|
216
|
+
try {
|
|
217
|
+
execFileSync("/usr/bin/zip", ["-q", "-r", "-X", outPath, "."], {
|
|
218
|
+
cwd: passDir,
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
221
|
+
timeout: 60000,
|
|
222
|
+
});
|
|
223
|
+
return { ok: true, bytes: statSync(outPath).size };
|
|
224
|
+
} catch (e) {
|
|
225
|
+
return { error: `${ERROR_PREFIX}archiving failed: ${short(e)}` };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Unpack a .pkpass into a temporary directory for inspection. */
|
|
230
|
+
export function unpack(pkpassPath) {
|
|
231
|
+
const dir = mkdtempSync(join(tmpdir(), "pkpass-"));
|
|
232
|
+
try {
|
|
233
|
+
execFileSync("/usr/bin/unzip", ["-q", "-o", pkpassPath, "-d", dir], {
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
236
|
+
timeout: 60000,
|
|
237
|
+
});
|
|
238
|
+
return { dir };
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return { error: `${ERROR_PREFIX}cannot unpack ${pkpassPath}: ${short(e)}` };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function writeManifest(dir, manifest) {
|
|
245
|
+
const p = join(dir, "manifest.json");
|
|
246
|
+
writeFileSync(p, JSON.stringify(manifest, null, 2));
|
|
247
|
+
return p;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function scrub(text, secret) {
|
|
251
|
+
if (!secret) return text;
|
|
252
|
+
return text.split(secret).join("***");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export { ERROR_PREFIX };
|
|
@@ -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
|
+
}
|