apple-icloud-mcp 0.0.0 → 0.1.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/THIRD_PARTY_NOTICES.md +6 -80
- package/dist/apple-keys.js +214 -0
- package/dist/bundle.js +122192 -0
- package/dist/calendar/caldav.js +264 -0
- package/dist/calendar/create.js +97 -0
- package/dist/calendar/edit.js +245 -0
- package/dist/calendar/events.js +139 -0
- package/dist/calendar/expand.js +338 -0
- package/dist/calendar/format.js +151 -0
- package/dist/calendar/freetime.js +69 -0
- package/dist/calendar/health.js +46 -0
- package/dist/calendar/ics.js +651 -0
- package/dist/calendar/ids.js +87 -0
- package/dist/calendar/series.js +418 -0
- package/dist/calendar/tools.js +820 -0
- package/dist/calendar/window.js +56 -0
- package/dist/config.js +193 -0
- package/dist/contacts/book.js +248 -0
- package/dist/contacts/edit.js +529 -0
- package/dist/contacts/health.js +33 -0
- package/dist/contacts/model.js +308 -0
- package/dist/contacts/tools.js +710 -0
- package/dist/contacts/vcard.js +359 -0
- package/dist/dav/client.js +386 -0
- package/dist/dav/icloud.js +214 -0
- package/dist/dav/xml.js +524 -0
- package/dist/errors.js +150 -0
- package/dist/health.js +88 -0
- package/dist/http.js +422 -0
- package/dist/icloud-auth.js +139 -0
- package/dist/index.js +27 -0
- package/dist/itunes/client.js +233 -0
- package/dist/itunes/health.js +39 -0
- package/dist/itunes/params.js +172 -0
- package/dist/itunes/project.js +198 -0
- package/dist/itunes/throttle.js +27 -0
- package/dist/itunes/tools.js +367 -0
- package/dist/mail/config.js +170 -0
- package/dist/mail/format.js +142 -0
- package/dist/mail/health.js +54 -0
- package/dist/mail/html-text.js +328 -0
- package/dist/mail/imap.js +246 -0
- package/dist/mail/message.js +138 -0
- package/dist/mail/smtp.js +228 -0
- package/dist/mail/tools.js +959 -0
- package/dist/maps/client.js +195 -0
- package/dist/maps/format.js +32 -0
- package/dist/maps/health.js +29 -0
- package/dist/maps/location.js +47 -0
- package/dist/maps/project.js +200 -0
- package/dist/maps/snapshot.js +45 -0
- package/dist/maps/tools.js +834 -0
- package/dist/music/auth-cli.js +319 -0
- package/dist/music/client.js +397 -0
- package/dist/music/common.js +57 -0
- package/dist/music/credentials.js +120 -0
- package/dist/music/health.js +142 -0
- package/dist/music/ids.js +122 -0
- package/dist/music/playlists.js +248 -0
- package/dist/music/project.js +139 -0
- package/dist/music/tools-catalog.js +291 -0
- package/dist/music/tools-extended.js +719 -0
- package/dist/music/tools-library.js +503 -0
- package/dist/music/tools-write.js +430 -0
- package/dist/music/tools.js +19 -0
- package/dist/music/web-token.js +190 -0
- package/dist/music/write-log.js +42 -0
- package/dist/registry.js +42 -0
- package/dist/state.js +57 -0
- package/dist/time.js +237 -0
- package/dist/tools/_confirm.js +149 -0
- package/dist/tools/_shared.js +184 -0
- package/dist/tools/healthcheck.js +151 -0
- package/dist/version.js +4 -0
- package/dist/weather/auth.js +85 -0
- package/dist/weather/client.js +160 -0
- package/dist/weather/format.js +343 -0
- package/dist/weather/health.js +27 -0
- package/dist/weather/schemas.js +100 -0
- package/dist/weather/tools.js +383 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Unofficial Apple Music, iCloud Calendar/Contacts/Mail, Apple Maps and WeatherKit tools for Claude Code",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.1.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "Apple iCloud",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search for Claude via MCP — runs anywhere, no Mac required. Unofficial; not affiliated with Apple.",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.1.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-icloud",
|
|
3
3
|
"displayName": "Apple iCloud",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": "Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search for Claude via MCP — runs anywhere, no Mac required. Unofficial; not affiliated with Apple.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Chall"
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -23,8 +23,8 @@ MPL-2.0 is a file-level license: it covers ical.js's own files and does not exte
|
|
|
23
23
|
| Package | Version | License | Source |
|
|
24
24
|
|---|---|---|---|
|
|
25
25
|
| @chrischall/mcp-utils | 2.7.0 | MIT | https://github.com/chrischall/mcp-utils |
|
|
26
|
-
| @fetchproxy/protocol | 3.2.0 | MIT |
|
|
27
|
-
| @fetchproxy/server | 3.2.0 | MIT |
|
|
26
|
+
| @fetchproxy/protocol | 3.2.0 | MIT | — |
|
|
27
|
+
| @fetchproxy/server | 3.2.0 | MIT | — |
|
|
28
28
|
| @modelcontextprotocol/core | 2.1.0 | MIT | https://github.com/modelcontextprotocol/typescript-sdk |
|
|
29
29
|
| @modelcontextprotocol/server | 2.1.0 | MIT | https://github.com/modelcontextprotocol/typescript-sdk |
|
|
30
30
|
| @pinojs/redact | 0.4.0 | MIT | https://github.com/pinojs/redact |
|
|
@@ -58,7 +58,7 @@ MPL-2.0 is a file-level license: it covers ical.js's own files and does not exte
|
|
|
58
58
|
| split2 | 4.2.0 | ISC | https://github.com/mcollina/split2 |
|
|
59
59
|
| thread-stream | 4.2.0 | MIT | https://github.com/mcollina/thread-stream |
|
|
60
60
|
| timezones-ical-library | 2.3.2 | Apache-2.0 | https://github.com/add2cal/timezones-ical-library |
|
|
61
|
-
| ws | 8.21.3 | MIT |
|
|
61
|
+
| ws | 8.21.3 | MIT | — |
|
|
62
62
|
| zod | 4.6.5 | MIT | https://github.com/colinhacks/zod |
|
|
63
63
|
|
|
64
64
|
## License and notice texts
|
|
@@ -97,64 +97,14 @@ SOFTWARE.
|
|
|
97
97
|
### @fetchproxy/protocol@3.2.0
|
|
98
98
|
|
|
99
99
|
- License: MIT
|
|
100
|
-
- Source: https://github.com/chrischall/fetchproxy
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```text
|
|
105
|
-
MIT License
|
|
106
|
-
|
|
107
|
-
Copyright (c) 2026 Chris Hall
|
|
108
|
-
|
|
109
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
110
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
111
|
-
in the Software without restriction, including without limitation the rights
|
|
112
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
113
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
114
|
-
furnished to do so, subject to the following conditions:
|
|
115
|
-
|
|
116
|
-
The above copyright notice and this permission notice shall be included in all
|
|
117
|
-
copies or substantial portions of the Software.
|
|
118
|
-
|
|
119
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
120
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
121
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
122
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
123
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
124
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
125
|
-
SOFTWARE.
|
|
126
|
-
```
|
|
101
|
+
_An optional dependency that was not installed where this file was generated; see its package on npm._
|
|
127
102
|
|
|
128
103
|
### @fetchproxy/server@3.2.0
|
|
129
104
|
|
|
130
105
|
- License: MIT
|
|
131
|
-
- Source: https://github.com/chrischall/fetchproxy
|
|
132
|
-
|
|
133
|
-
`LICENSE`:
|
|
134
106
|
|
|
135
|
-
|
|
136
|
-
MIT License
|
|
137
|
-
|
|
138
|
-
Copyright (c) 2026 Chris Hall
|
|
139
|
-
|
|
140
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
141
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
142
|
-
in the Software without restriction, including without limitation the rights
|
|
143
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
144
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
145
|
-
furnished to do so, subject to the following conditions:
|
|
146
|
-
|
|
147
|
-
The above copyright notice and this permission notice shall be included in all
|
|
148
|
-
copies or substantial portions of the Software.
|
|
149
|
-
|
|
150
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
151
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
152
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
153
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
154
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
155
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
156
|
-
SOFTWARE.
|
|
157
|
-
```
|
|
107
|
+
_An optional dependency that was not installed where this file was generated; see its package on npm._
|
|
158
108
|
|
|
159
109
|
### @modelcontextprotocol/core@2.1.0
|
|
160
110
|
|
|
@@ -2217,32 +2167,8 @@ SOFTWARE.
|
|
|
2217
2167
|
### ws@8.21.3
|
|
2218
2168
|
|
|
2219
2169
|
- License: MIT
|
|
2220
|
-
- Source: https://github.com/websockets/ws
|
|
2221
|
-
|
|
2222
|
-
`LICENSE`:
|
|
2223
|
-
|
|
2224
|
-
```text
|
|
2225
|
-
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
2226
|
-
Copyright (c) 2013 Arnout Kazemier and contributors
|
|
2227
|
-
Copyright (c) 2016 Luigi Pinca and contributors
|
|
2228
2170
|
|
|
2229
|
-
|
|
2230
|
-
this software and associated documentation files (the "Software"), to deal in
|
|
2231
|
-
the Software without restriction, including without limitation the rights to
|
|
2232
|
-
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
2233
|
-
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
2234
|
-
subject to the following conditions:
|
|
2235
|
-
|
|
2236
|
-
The above copyright notice and this permission notice shall be included in all
|
|
2237
|
-
copies or substantial portions of the Software.
|
|
2238
|
-
|
|
2239
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
2240
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
2241
|
-
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
2242
|
-
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
2243
|
-
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
2244
|
-
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
2245
|
-
```
|
|
2171
|
+
_An optional dependency that was not installed where this file was generated; see its package on npm._
|
|
2246
2172
|
|
|
2247
2173
|
### zod@4.6.5
|
|
2248
2174
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createPrivateKey } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { expandPath, readEnvVar, signEs256Jwt } from '@chrischall/mcp-utils';
|
|
4
|
+
import { ConfigError, rememberSecret } from './errors.js';
|
|
5
|
+
const PREFIX = {
|
|
6
|
+
music: 'APPLE_MUSIC',
|
|
7
|
+
maps: 'APPLE_MAPS',
|
|
8
|
+
weather: 'APPLE_WEATHERKIT',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Accept the shapes a .p8 actually arrives in through an env block:
|
|
12
|
+
* - the PEM verbatim (multi-line);
|
|
13
|
+
* - the PEM with literal `\n` escapes (a one-line secret field);
|
|
14
|
+
* - the whole PEM base64-encoded;
|
|
15
|
+
* - just the base64 body between the BEGIN/END lines;
|
|
16
|
+
* - the PEM with its line breaks turned into spaces, or dropped entirely —
|
|
17
|
+
* what a single-line form field does to a pasted multi-line value, and a
|
|
18
|
+
* shape OpenSSL refuses to parse.
|
|
19
|
+
* Every PEM is re-armored canonically (64-column body, LF endings).
|
|
20
|
+
* Returns a normalized PEM, or undefined when the value is none of those.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizePrivateKey(raw) {
|
|
23
|
+
let value = raw.trim().replace(/\\r/g, '').replace(/\\n/g, '\n');
|
|
24
|
+
if (!value.includes('-----BEGIN')) {
|
|
25
|
+
const compact = value.replace(/\s+/g, '');
|
|
26
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(compact))
|
|
27
|
+
return undefined;
|
|
28
|
+
const decoded = Buffer.from(compact, 'base64').toString('utf8');
|
|
29
|
+
if (!decoded.includes('-----BEGIN'))
|
|
30
|
+
return armor('PRIVATE KEY', compact);
|
|
31
|
+
value = decoded;
|
|
32
|
+
}
|
|
33
|
+
const pem = PEM_BLOCK_RE.exec(value);
|
|
34
|
+
// An unrecognisable armor is passed through for `validateEcKey` to reject by name.
|
|
35
|
+
if (!pem)
|
|
36
|
+
return value.trim().replace(/\r\n/g, '\n') + '\n';
|
|
37
|
+
return armor(pem[1], pem[2].replace(/\s+/g, ''));
|
|
38
|
+
}
|
|
39
|
+
/** `-----BEGIN <label>-----<body>-----END <label>-----`, whatever whitespace separates the parts. */
|
|
40
|
+
const PEM_BLOCK_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
|
|
41
|
+
function armor(label, body) {
|
|
42
|
+
const lines = body.match(/.{1,64}/g) ?? [];
|
|
43
|
+
return [`-----BEGIN ${label}-----`, ...lines, `-----END ${label}-----`].join('\n') + '\n';
|
|
44
|
+
}
|
|
45
|
+
/** Validate that `pem` is an EC P-256 private key; returns an error string or undefined. */
|
|
46
|
+
export function validateEcKey(pem) {
|
|
47
|
+
try {
|
|
48
|
+
const key = createPrivateKey(pem);
|
|
49
|
+
if (key.asymmetricKeyType !== 'ec')
|
|
50
|
+
return `it is a ${key.asymmetricKeyType ?? 'non-EC'} key, not an EC (ES256) key`;
|
|
51
|
+
const curve = key.asymmetricKeyDetails?.namedCurve;
|
|
52
|
+
// No name means explicit curve parameters OpenSSL could not match to a
|
|
53
|
+
// named curve — so not P-256. Accepting it would mint tokens Apple rejects.
|
|
54
|
+
if (curve !== 'prime256v1')
|
|
55
|
+
return `it uses ${curve ? `curve ${curve}` : 'an unnamed curve'}, not P-256`;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 'it could not be parsed as a private key';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function readKeyFile(path, varName, service) {
|
|
63
|
+
try {
|
|
64
|
+
return readFileSync(expandPath(path), 'utf8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new ConfigError(service, `Could not read the private key file named by ${varName}.`, [varName], `Check that ${varName} points at a readable .p8 file.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the developer key for `service`, or throw a `ConfigError` naming
|
|
72
|
+
* exactly which variables are missing.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveDeveloperKey(service, env = process.env) {
|
|
75
|
+
const svcName = service;
|
|
76
|
+
const prefix = PREFIX[service];
|
|
77
|
+
const teamId = readEnvVar('APPLE_TEAM_ID', { env });
|
|
78
|
+
const ownKeyId = readEnvVar(`${prefix}_KEY_ID`, { env });
|
|
79
|
+
const ownKey = readEnvVar(`${prefix}_PRIVATE_KEY`, { env });
|
|
80
|
+
const keyId = ownKeyId ?? readEnvVar('APPLE_KEY_ID', { env });
|
|
81
|
+
let rawKey;
|
|
82
|
+
let keyVar;
|
|
83
|
+
if (ownKey !== undefined) {
|
|
84
|
+
rawKey = ownKey;
|
|
85
|
+
keyVar = `${prefix}_PRIVATE_KEY`;
|
|
86
|
+
}
|
|
87
|
+
else if (readEnvVar('APPLE_PRIVATE_KEY', { env }) !== undefined) {
|
|
88
|
+
rawKey = readEnvVar('APPLE_PRIVATE_KEY', { env });
|
|
89
|
+
keyVar = 'APPLE_PRIVATE_KEY';
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const path = readEnvVar('APPLE_PRIVATE_KEY_PATH', { env });
|
|
93
|
+
keyVar = 'APPLE_PRIVATE_KEY_PATH';
|
|
94
|
+
if (path !== undefined)
|
|
95
|
+
rawKey = readKeyFile(path, keyVar, svcName);
|
|
96
|
+
}
|
|
97
|
+
const missing = [];
|
|
98
|
+
if (!teamId)
|
|
99
|
+
missing.push('APPLE_TEAM_ID');
|
|
100
|
+
if (!keyId)
|
|
101
|
+
missing.push(`APPLE_KEY_ID (or ${prefix}_KEY_ID)`);
|
|
102
|
+
if (rawKey === undefined)
|
|
103
|
+
missing.push(`APPLE_PRIVATE_KEY (or ${prefix}_PRIVATE_KEY / APPLE_PRIVATE_KEY_PATH)`);
|
|
104
|
+
if (missing.length === 0)
|
|
105
|
+
assertPairMatches(service, env);
|
|
106
|
+
if (missing.length > 0) {
|
|
107
|
+
throw new ConfigError(svcName, `${serviceLabel(service)} needs an Apple Developer key, and ${missing.join(', ')} ${missing.length === 1 ? 'is' : 'are'} not set.`, missing, `Create a key in the Apple Developer portal (Certificates, Identifiers & Profiles → Keys) with ${capabilityLabel(service)} enabled, then set APPLE_TEAM_ID, APPLE_KEY_ID and APPLE_PRIVATE_KEY (the .p8 contents).`);
|
|
108
|
+
}
|
|
109
|
+
const pem = normalizePrivateKey(rawKey);
|
|
110
|
+
const problem = pem === undefined ? 'it is not a PEM or base64 key' : validateEcKey(pem);
|
|
111
|
+
if (pem === undefined || problem !== undefined) {
|
|
112
|
+
throw new ConfigError(svcName, `The private key in ${keyVar} is unusable: ${problem}.`, [keyVar], `Paste the whole .p8 file (including the BEGIN/END PRIVATE KEY lines) into ${keyVar}; one-line values with \\n escapes and base64 are accepted too.`);
|
|
113
|
+
}
|
|
114
|
+
rememberSecret(rawKey);
|
|
115
|
+
return {
|
|
116
|
+
teamId: teamId,
|
|
117
|
+
keyId: keyId,
|
|
118
|
+
privateKeyPem: pem,
|
|
119
|
+
source: `${ownKeyId ? `${prefix}_KEY_ID` : 'APPLE_KEY_ID'} + ${keyVar}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A key id names exactly one private key, so the service-specific pair and
|
|
124
|
+
* the shared pair must not be crossed. When BOTH a shared key id and a shared
|
|
125
|
+
* private key are set, the shared id provably belongs to the shared key — so
|
|
126
|
+
* pairing it with `<SVC>_PRIVATE_KEY`, or pairing `<SVC>_KEY_ID` with the
|
|
127
|
+
* shared key, mints tokens Apple rejects with a bare 401 that names neither
|
|
128
|
+
* variable. Refuse that up front, by name. With only half of the shared pair
|
|
129
|
+
* set, crossing is the ordinary single-key setup (e.g. APPLE_MUSIC_KEY_ID +
|
|
130
|
+
* APPLE_PRIVATE_KEY for one Music key) and is allowed.
|
|
131
|
+
*/
|
|
132
|
+
function assertPairMatches(service, env) {
|
|
133
|
+
const prefix = PREFIX[service];
|
|
134
|
+
const ownKeyId = readEnvVar(`${prefix}_KEY_ID`, { env });
|
|
135
|
+
const ownKey = readEnvVar(`${prefix}_PRIVATE_KEY`, { env });
|
|
136
|
+
const sharedKeyId = readEnvVar('APPLE_KEY_ID', { env });
|
|
137
|
+
const sharedKeyVar = readEnvVar('APPLE_PRIVATE_KEY', { env }) !== undefined
|
|
138
|
+
? 'APPLE_PRIVATE_KEY'
|
|
139
|
+
: readEnvVar('APPLE_PRIVATE_KEY_PATH', { env }) !== undefined
|
|
140
|
+
? 'APPLE_PRIVATE_KEY_PATH'
|
|
141
|
+
: undefined;
|
|
142
|
+
if (sharedKeyId === undefined || sharedKeyVar === undefined)
|
|
143
|
+
return;
|
|
144
|
+
if (ownKey !== undefined && ownKeyId === undefined) {
|
|
145
|
+
throw new ConfigError(service, `${prefix}_PRIVATE_KEY is set but ${prefix}_KEY_ID is not, and APPLE_KEY_ID belongs to the shared key in ${sharedKeyVar} — ` +
|
|
146
|
+
'a key id must name the key that signs the token.', [`${prefix}_KEY_ID`], `Set ${prefix}_KEY_ID to the Key ID of the key in ${prefix}_PRIVATE_KEY (Apple Developer portal → Keys), or unset ${prefix}_PRIVATE_KEY to use the shared key.`);
|
|
147
|
+
}
|
|
148
|
+
if (ownKeyId !== undefined && ownKey === undefined && ownKeyId !== sharedKeyId) {
|
|
149
|
+
throw new ConfigError(service, `${prefix}_KEY_ID is set but ${prefix}_PRIVATE_KEY is not, and the shared key in ${sharedKeyVar} belongs to APPLE_KEY_ID — ` +
|
|
150
|
+
'a key id must name the key that signs the token.', [`${prefix}_PRIVATE_KEY`], `Set ${prefix}_PRIVATE_KEY to the .p8 contents of key ${prefix}_KEY_ID, or unset ${prefix}_KEY_ID to use the shared key.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function serviceLabel(service) {
|
|
154
|
+
return service === 'music' ? 'Apple Music' : service === 'maps' ? 'Apple Maps' : 'WeatherKit';
|
|
155
|
+
}
|
|
156
|
+
function capabilityLabel(service) {
|
|
157
|
+
return service === 'music' ? 'Media Services (MusicKit)' : service === 'maps' ? 'MapKit JS' : 'WeatherKit';
|
|
158
|
+
}
|
|
159
|
+
/** Seconds since the epoch. */
|
|
160
|
+
function nowSeconds(now) {
|
|
161
|
+
return Math.floor(now / 1000);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* An Apple Music developer token. Apple allows up to ~6 months; we mint for
|
|
165
|
+
* 12 hours and re-mint, so a revoked key stops working within a day and a
|
|
166
|
+
* leaked token is short-lived.
|
|
167
|
+
*/
|
|
168
|
+
export const MUSIC_TOKEN_TTL_S = 12 * 60 * 60;
|
|
169
|
+
export function mintMusicDeveloperToken(key, now = Date.now()) {
|
|
170
|
+
const iat = nowSeconds(now);
|
|
171
|
+
const exp = iat + MUSIC_TOKEN_TTL_S;
|
|
172
|
+
return { token: signEs256Jwt(key.privateKeyPem, { iss: key.teamId, iat, exp }, { header: { kid: key.keyId } }), expiresAt: exp * 1000 };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The Maps Server API auth token (exchanged at GET /v1/token for a 30-minute
|
|
176
|
+
* access token). `scope: server_api` is what makes it a server token.
|
|
177
|
+
*/
|
|
178
|
+
export const MAPS_AUTH_TOKEN_TTL_S = 60 * 60;
|
|
179
|
+
export function mintMapsAuthToken(key, now = Date.now()) {
|
|
180
|
+
const iat = nowSeconds(now);
|
|
181
|
+
const exp = iat + MAPS_AUTH_TOKEN_TTL_S;
|
|
182
|
+
return {
|
|
183
|
+
token: signEs256Jwt(key.privateKeyPem, { iss: key.teamId, iat, exp, scope: 'server_api' }, { header: { kid: key.keyId } }),
|
|
184
|
+
expiresAt: exp * 1000,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A WeatherKit REST token. The non-standard `id` header (`<team>.<service>`)
|
|
189
|
+
* and the `sub` claim (the Services ID) are both required; a missing `id` is
|
|
190
|
+
* the classic cause of `NOT_ENABLED`.
|
|
191
|
+
*/
|
|
192
|
+
export const WEATHERKIT_TOKEN_TTL_S = 60 * 60;
|
|
193
|
+
export function mintWeatherKitToken(key, serviceId, now = Date.now()) {
|
|
194
|
+
const iat = nowSeconds(now);
|
|
195
|
+
const exp = iat + WEATHERKIT_TOKEN_TTL_S;
|
|
196
|
+
return {
|
|
197
|
+
token: signEs256Jwt(key.privateKeyPem, { iss: key.teamId, iat, exp, sub: serviceId }, { header: { kid: key.keyId, id: `${key.teamId}.${serviceId}` } }),
|
|
198
|
+
expiresAt: exp * 1000,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Decode a JWT's payload without verifying it (for expiry and issuer checks on tokens we did not mint). */
|
|
202
|
+
export function decodeJwtPayload(token) {
|
|
203
|
+
const parts = token.split('.');
|
|
204
|
+
if (parts.length !== 3)
|
|
205
|
+
return undefined;
|
|
206
|
+
try {
|
|
207
|
+
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
208
|
+
const payload = JSON.parse(json);
|
|
209
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : undefined;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|