PageSourceSearch

https://jpda.dev/darksky-to-weatherkit

html jpda.dev collected 2026-10-09 21:46:35 UTC 27,979 bytes, 348 lines download raw bytes

1<!DOCTYPE html>
2<html>
3
4  <head>
5  <meta charset="utf-8">
6  <meta http-equiv="X-UA-Compatible" content="IE=edge">
7  <meta name="viewport" content="width=device-width, initial-scale=1">
8
9  <title>DarkSky to WeatherKit: from API keys to signed JWTs</title>
10  <meta name="description"
11    content="">
12  <meta http-equiv="Content-Security-Policy"
13    content="default-src 'self' http://localhost:4000/ http://platform.twitter.com/ https://buttons.github.io/ https://github.githubassets.com/ https://gist.github.com/ https://az416426.vo.msecnd.net/next/ https://dc.services.visualstudio.com/v2/track 'unsafe-inline'; font-src 'self' data:; img-src 'self' https://img.youtube.com https://dev-to-uploads.s3.amazonaws.com/">
14  
15  
16  <!-- <link rel="stylesheet" href="https://jpda.dev/assets/style.css"> -->
17  <link rel="stylesheet" href="assets/style.css">
18  <link rel="canonical" href="https://jpda.dev/darksky-to-weatherkit">
19  <link rel="alternate" type="application/rss+xml" title="jpda.dev" href="https://jpda.dev /feed.xml">
20  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
21  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
22  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
23  <link rel="manifest" href="/site.webmanifest">
24
25  
26  
26<script type="text/javascript">
27    var sdkInstance = "appInsightsSDK"; window[sdkInstance] = "appInsights"; var aiName = window[sdkInstance], aisdk = window[aiName] || function (e) {
28      function n(e) { t[e] = function () { var n = arguments; t.queue.push(function () { t[e].apply(t, n) }) } } var t = { config: e }; t.initialize = !0; var i = document, a = window; setTimeout(function () { var n = i.createElement("script"); n.src = e.url || "https://az416426.vo.msecnd.net/next/ai.2.min.js", i.getElementsByTagName("script")[0].parentNode.appendChild(n) }); try { t.cookie = i.cookie } catch (e) { } t.queue = [], t.version = 2; for (var r = ["Event", "PageView", "Exception", "Trace", "DependencyData", "Metric", "PageViewPerformance"]; r.length;)n("track" + r.pop()); n("startTrackPage"), n("stopTrackPage"); var s = "Track" + r[0]; if (n("start" + s), n("stop" + s), n("setAuthenticatedUserContext"), n("clearAuthenticatedUserContext"), n("flush"), !(!0 === e.disableExceptionTracking || e.extensionConfig && e.extensionConfig.ApplicationInsightsAnalytics && !0 === e.extensionConfig.ApplicationInsightsAnalytics.disableExceptionTracking)) { n("_" + (r = "onerror")); var o = a[r]; a[r] = function (e, n, i, a, s) { var c = o && o(e, n, i, a, s); return !0 !== c && t["_" + r]({ message: e, url: n, lineNumber: i, columnNumber: a, error: s }), c }, e.autoExceptionInstrumented = !0 } return t
29    }({
30      instrumentationKey: "aa50374c-4629-4ab3-a058-5bb4d8bb1924"
31    });
32
33    window[aiName] = aisdk, aisdk.queue && 0 === aisdk.queue.length && aisdk.trackPageView({});
34  </script>
34
35  
36
37  
37<script async defer src="https://buttons.github.io/buttons.js"></script>
37
38</head>
39
40
41  <body>
42
43    <header class="border-bottom-thick px-2 clearfix">
44  <div class="left sm-width-full py-1 mt-1 mt-lg-0">
45    <a class="align-middle link-primary text-accent" href="/">
46      jpda.dev
47    </a>
48  </div>
49  <div class="right sm-width-full">
50    <ul class="list-reset mt-lg-1 mb-2 mb-lg-1">
51      
52        
53        <li class="inline-block">
54          <a class="align-middle link-primary mr-2 mr-lg-0 ml-lg-2" href="/about">
55            About
56          </a>
57        </li>
58        
59      
60        
61      
62        
63      
64        
65      
66        
67      
68        
69        <li class="inline-block">
70          <a class="align-middle link-primary mr-2 mr-lg-0 ml-lg-2" href="/privacy-policy">
71            Policies
72          </a>
73        </li>
74        
75      
76        
77      
78        
79      
80        
81      
82    </ul>
83  </div>
84</header>
85
86
87    <div>
88      <article class="container px-2 mx-auto mb4" itemscope itemtype="http://schema.org/BlogPosting">
89  <h1 class="h0 col-9 sm-width-full py-4 mt-3 inline-block" itemprop="name headline">DarkSky to WeatherKit: from API keys to signed JWTs</h1>
90  <div class="col-4 sm-width-full mt-1 border-top-thin ">
91    <p class="mb-3 py-2 bold h4"><time datetime="2023-04-20T21:19:08+00:00" itemprop="datePublished">Apr 20, 2023</time></p>
92  </div>
93
94  <div class="prose" itemprop="articleBody">
95      <p><a href="https://www.youtube.com/watch?v=oo5fg0EfoCY&amp;t=300"><img src="https://img.youtube.com/vi/oo5fg0EfoCY/maxresdefault.jpg" alt="stream video" /></a></p>
96
97<p>As of March 31st, the Dark Sky API is no more, replaced by Apple’s <a href="https://developer.apple.com/weatherkit/">WeatherKit</a>. I’ve used the Dark Sky API as it had a generous free-tier, was easy to use and flexible enough to customize.</p>
98
99<p>Remember a few years ago when customized terminal prompts were all the rage? Pepperidge Farm remembers - especially people like me who probably went a bit overboard.</p>
100
101<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jue3d8fp25wbzu4z4hh.png" alt="A terminal prompt with too many segments, like weather and stock price" /></p>
102
103<p>Note the weather &amp; stock price. Stock price comes from Yahoo (and follows the same pattern as the weather data), weather data came from Dark Sky. The prompt would read from a cache file.</p>
104
105<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">weatherData</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="s2">"https://api.darksky.net/forecast/</span><span class="k">${</span><span class="nv">API_KEY</span><span class="k">}</span><span class="s2">/</span><span class="k">${</span><span class="nv">LATLON</span><span class="k">}</span><span class="s2">?exclude=minutely,hourly,daily,alerts,flags"</span><span class="si">)</span>
106<span class="nb">echo</span> <span class="nv">$weatherData</span> <span class="o">&gt;</span> <span class="nv">$weatherCacheFile</span>
107</code></pre></div></div>
108
109<p>API key was a static string - could be regenerated on-demand. To run on a trusted machine (like my own), it was no sweat and easy to use. Of course I wouldn’t want to distribute that key, but it was for my use locally. It was <em>especially</em> easy to use in shell scripting.</p>
110
111<h2 id="enter-weatherkit">Enter WeatherKit</h2>
112<p>Apple pushed the Dark Sky cutoff date a few times, of course, I missed it and wondered why I was getting 301s and parse error for weather info. On March 31st, the API was dead and I’d have to migrate over to WeatherKit.</p>
113
114<p>At first glance I figured it’d be pretty similar - this is weather data, after all - but was surprised to see that Apple’s only <a href="https://developer.apple.com/documentation/weatherkitrestapi/request_authentication_for_weatherkit_rest_api">accepted authorization scheme</a> for the API was a bearer token, an EC-signed, asymmetric JWT! Needless to say I was surprised</p>
115
116<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flwq3m0mzcnjmnje5ds1.png" alt="Surprised Pikachu meme" /></p>
117
118<p>Here’s an example:</p>
119
120<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
121  </span><span class="nl">"kid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;KEY ID&gt;
121"</span><span class="p">,</span><span class="w">
122  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;TEAM ID&gt;.&lt;SERVICE ID&gt;"</span><span class="p">,</span><span class="w">
123  </span><span class="nl">"typ"</span><span class="p">:</span><span class="w"> </span><span class="s2">"JWT"</span><span class="p">,</span><span class="w">
124  </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ES256"</span><span class="w">
125</span><span class="p">}</span><span class="err">,</span><span class="w">
126</span><span class="p">{</span><span class="w">
127  </span><span class="nl">"iss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;TEAM ID&gt;"</span><span class="p">,</span><span class="w">
128  </span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="mi">1682008228</span><span class="p">,</span><span class="w">
129  </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1682011828</span><span class="p">,</span><span class="w">
130  </span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;SERVICE ID&gt;"</span><span class="w">
131</span><span class="p">}</span><span class="err">.&lt;signature&gt;</span><span class="w">
132</span></code></pre></div></div>
133
134<h2 id="get-all-of-your-pieces">Get all of your pieces</h2>
135
136<p>Sadly, this is not free - unlike the Dark Sky API, this requires an Apple Dev membership. While the WeatherKit API itself has a generous starting tier (500k calls/month) that doesn’t have any <em>additional</em> cost, you’ll still be out the $99 for the annual membership. Students and non-profits can get it cheaper, so ask around.</p>
137
138<p>Apple’s docs cover most of what you’ll need:</p>
139<ul>
140  <li>your Team ID</li>
141  <li>the <code class="language-plaintext highlighter-rouge">.p8</code> file that contains your key</li>
142  <li>the key ID</li>
143  <li>the service ID</li>
144  <li>lat/lon coordinates of a location</li>
145</ul>
146
147<p>The Service ID is a string (like <code class="language-plaintext highlighter-rouge">dev.jpda.terminal-weather</code>) of your choosing.</p>
148
149<h2 id="testing-with-postmans-excellent-jwt-signer">Testing with Postman’s excellent JWT signer</h2>
150
151<p>To start, Postman has an excellent JWT Bearer auth type that includes an ES256 signer, which makes it much easier to get familiar with the API. I find creating a collection to make this easier (as the authorization settings can be set at the collection level instead of per-request). Here we can configure our token. Choose JWT Bearer as the type, <code class="language-plaintext highlighter-rouge">ES256</code> as the algorithm, get your private key from the .p8 file from Apple.</p>
152
153<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/26a1c21ylrkqxfw66d84.png" alt="Postman authorization screen" /></p>
154
155<h3 id="jwt-payload">JWT Payload</h3>
156
157<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9x42fikmcm1qbyzi8nta.png" alt="JWT payload screen in postman authz" /></p>
158
159<p>The payload is where we’ll add our specific claims - there are four we need: <code class="language-plaintext highlighter-rouge">iss</code>,<code class="language-plaintext highlighter-rouge">iat</code> (issued at), <code class="language-plaintext highlighter-rouge">exp</code> (expires on) and <code class="language-plaintext highlighter-rouge">sub</code>ject (who/what the token is about).</p>
160
161<ul>
162  <li>iss: your team ID</li>
163  <li>sub: your service ID</li>
164</ul>
165
166<p>The two timestamps we’ll have to sort out. Postman has a <code class="language-plaintext highlighter-rouge">$timestamp</code> global that gives us a unix epoch time (seconds since 1/1/1970) making <code class="language-plaintext highlighter-rouge">iat</code> easy. <code class="language-plaintext highlighter-rouge">exp</code> we’ll need calculate off of <code class="language-plaintext highlighter-rouge">iat</code>, which we can do via Pre-request scripts.</p>
167
168<p>I’ve left my information in below as an example:</p>
169
170<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
171    </span><span class="nl">"iss"</span><span class="p">:</span><span class="s2">"6RT85M24SE"</span><span class="p">,</span><span class="w">
172    </span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="p">,</span><span class="w">
173    </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="p">,</span><span class="w">
174    </span><span class="nl">"sub"</span><span class="p">:</span><span class="s2">"dev.jpda.terminal-weather"</span><span class="w">
175</span><span class="p">}</span><span class="w">
176</span></code></pre></div></div>
177
178<p>Pay particular attention to the timestamps - they should <em>not</em> be in quotations, as they are longs. This will complain because <code class="language-plaintext highlighter-rouge">exp_timestamp</code> is not defined, which is OK, we’re going to go define it next.</p>
179
180<h3 id="variables--pre-request-script">Variables &amp; pre-request script</h3>
181
182<p>We’ll need at least one variable to hold the calculated expiration value. I put my coordinates in variables called <code class="language-plaintext highlighter-rouge">HOME_LAT</code> and <code class="language-plaintext highlighter-rouge">HOME_LON</code> since they will be available to the whole collection.</p>
183
184<p>
184Add a variable called <code class="language-plaintext highlighter-rouge">exp_timestamp</code> - it doesn’t need an initial value.</p>
185
186<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2vpa3umogusl15r9bq9h.png" alt="Variables screen" /></p>
187
188<p>Lastly head into your pre-request scripts tab. Here we can calculate the expiration timestamp like so:</p>
189
190<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">now</span> <span class="o">=</span> <span class="nx">pm</span><span class="p">.</span><span class="nx">variables</span><span class="p">.</span><span class="nx">replaceIn</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span><span class="o">*</span><span class="mi">1</span><span class="p">;</span>
191<span class="kd">var</span> <span class="nx">exp</span> <span class="o">=</span> <span class="nx">now</span> <span class="o">+</span> <span class="mi">3600</span><span class="p">;</span> <span class="c1">// adds +1 hour</span>
192<span class="nx">pm</span><span class="p">.</span><span class="nx">collectionVariables</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">exp_timestamp</span><span class="dl">"</span><span class="p">,</span> <span class="nx">exp</span><span class="p">);</span>
193</code></pre></div></div>
194
195<p>Because of how Postman’s variables work, <code class="language-plaintext highlighter-rouge">replaceIn</code> is required to use the global $timestamp variable.</p>
196
197<h2 id="jwt-header">JWT Header</h2>
198
199<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5hyvzzf7mikuue4negeh.png" alt="JWT Header screen in postman authz" /></p>
200
201<p>Back in the Authorization tab, we have one more item to manage - we need to add two fields to the JWT’s header - the <code class="language-plaintext highlighter-rouge">kid</code> (key ID) and <code class="language-plaintext highlighter-rouge">id</code> - the team ID + service ID of your app. This is under ‘advanced configuration.’ We can add our claims and postman will handle adding claims like <code class="language-plaintext highlighter-rouge">alg</code> for the algorithm and <code class="language-plaintext highlighter-rouge">type</code>.</p>
202
203<p>I’ve left my values in as an example:</p>
204
205<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
206    </span><span class="nl">"kid"</span><span class="p">:</span><span class="s2">
206"PMV7JU5DJJ"</span><span class="p">,</span><span class="w">
207    </span><span class="nl">"id"</span><span class="p">:</span><span class="s2">"6RT85M24SE.dev.jpda.terminal-weather"</span><span class="w">
208</span><span class="p">}</span><span class="w">
209</span></code></pre></div></div>
210
211<h2 id="ready-lets-get-the-weather-right">Ready! Let’s get the weather…! Right?</h2>
212
213<p>OK, at long last we are ready to get the temperature! Well, not quite. First we need to see what datasets are available for your location.</p>
214
215<p>Get a new request (making sure to save it/add it to your collection we just configured for authorization) and drop in this url:</p>
216
217<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://weatherkit.apple.com/api/v1/availability//?country=US
218</code></pre></div></div>
219
220<p>Make sure your authorization tab is configured for ‘inherit from parent’ and you have variables set for <code class="language-plaintext highlighter-rouge">and</code> and update your country if necessary. Run this and, if all went well, we should get a response with an array of the available datasets for that location.</p>
221
222<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
223    </span><span class="s2">"currentWeather"</span><span class="p">,</span><span class="w">
224    </span><span class="s2">"forecastDaily"</span><span class="p">,</span><span class="w">
225    </span><span class="s2">"forecastHourly"</span><span class="p">,</span><span class="w">
226    </span><span class="s2">"forecastNextHour"</span><span class="p">,</span><span class="w">
227    </span><span class="s2">"weatherAlerts"</span><span class="w">
228</span><span class="p">]</span><span class="w">
229</span></code></pre></div></div>
230
231<p>If you get an error, take the generated JWT (from the headers tab of the request) and drop it in your favorite JWT decoder (like jwt.io) and check the contents.</p>
232
233<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ot4j3gzof5paq67eytct.png" alt="get authz header" /></p>
234
235<p>Note that these may be out-of-date, to get the specific token used in the failed request, look in the Postman console (View –&gt; Show Console).</p>
236
237<h2 id="ok-so-now-we-can-get-the-weather">Ok so <em>NOW</em> we can get the weather?</h2>
238
239<p>
239At long last, now that we know what’s available, <code class="language-plaintext highlighter-rouge">currentWeather</code> should give us what we’re looking for. Create a new request in Postman in this collection and let’s apply what we’ve determined:</p>
240
241<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://weatherkit.apple.com/api/v1/weather/en//?dataSets=currentWeather&amp;country=US
242</code></pre></div></div>
243
244<p>Note the <code class="language-plaintext highlighter-rouge">/en/</code> in the path - use the ISO country code for your region. I am only interested in current weather, so that’s the dataset I’ll request.</p>
245
246<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
247    </span><span class="nl">"currentWeather"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
248        </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CurrentWeather"</span><span class="p">,</span><span class="w">
249        </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
250            </span><span class="nl">"attributionURL"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://weatherkit.apple.com/legal-attribution.html"</span><span class="p">,</span><span class="w">
251            </span><span class="nl">"expireTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-04-20T21:20:01Z"</span><span class="p">,</span><span class="w">
252            </span><span class="nl">"latitude"</span><span class="p">:</span><span class="w"> </span><span class="mf">42.860</span><span class="p">,</span><span class="w">
253            </span><span class="nl">"longitude"</span><span class="p">:</span><span class="w"> </span><span class="mf">-82.590</span><span class="p">,</span><span class="w">
254            </span><span class="nl">"readTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-04-20T21:15:01Z"</span><span class="p">,</span><span class="w">
255            </span><span class="nl">"reportedTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-04-20T20:02:16Z"</span><span class="p">,</span><span class="w">
256            </span><span class="nl">"units"</span><span class="p">:</span><span class="w"> </span><span class="s2">"m"</span><span class="p">,</span><span class="w">
257            </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
258        </span><span class="p">},</span><span class="w">
259        </span><span class="nl">"asOf"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-04-20T21:15:01Z"</span><span class="p">,</span><span class="w">
260        </span><span class="nl">"cloudCover"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.65</span><span class="p">,</span><span class="w">
261        </span><span class="nl">"cloudCoverLowAltPct"</span><span class="p">:</span><span class="w"> </span><span class="mf">
2610.03</span><span class="p">,</span><span class="w">
262        </span><span class="nl">"cloudCoverMidAltPct"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.66</span><span class="p">,</span><span class="w">
263        </span><span class="nl">"cloudCoverHighAltPct"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.34</span><span class="p">,</span><span class="w">
264        </span><span class="nl">"conditionCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MostlyCloudy"</span><span class="p">,</span><span class="w">
265        </span><span class="nl">"daylight"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
266        </span><span class="nl">"humidity"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.55</span><span class="p">,</span><span class="w">
267        </span><span class="nl">"precipitationIntensity"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.0</span><span class="p">,</span><span class="w">
268        </span><span class="nl">"pressure"</span><span class="p">:</span><span class="w"> </span><span class="mf">1013.66</span><span class="p">,</span><span class="w">
269        </span><span class="nl">"pressureTrend"</span><span class="p">:</span><span class="w"> </span><span class="s2">"falling"</span><span class="p">,</span><span class="w">
270        </span><span class="nl">"temperature"</span><span class="p">:</span><span class="w"> </span><span class="mf">18.98</span><span class="p">,</span><span class="w">
271        </span><span class="nl">"temperatureApparent"</span><span class="p">:</span><span class="w"> </span><span class="mf">18.33</span><span class="p">,</span><span class="w">
272        </span><span class="nl">"temperatureDewPoint"</span><span class="p">:</span><span class="w"> </span><span class="mf">9.65</span><span class="p">,</span><span class="w">
273        </span><span class="nl">"uvIndex"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
274        </span><span class="nl">"visibility"</span><span class="p">:</span><span class="w"> </span><span class="mf">30544.32</span><span class="p">,</span><span class="w">
275        </span><span class="nl">"windDirection"</span><span class="p">:</span><span class="w"> </span><span class="mi">147</span><span class="p">,</span><span class="w">
276        </span><span class="nl">"windGust"</span><span class="p">:</span><span class="w"> </span><span class="mf">28.80</span><span class="p">,</span><span class="w">
277        </span><span class="nl">"windSpeed"</span><span class="p">:</span><span class="w"> </span><span class="mf">16.96</span><span class="w">
278    </span><span class="p">}</span><span class="w">
279</span><span class="p">}</span><span class="w">
280</span></code></pre></div></div>
281
282<p>Success! It’s all in metric units, and the docs seem to indicate that’s what it is and you’ll like it. So a bit of work to convert from Cs to Fs (or Ks if you’re adventurous).</p>
283
284<h2 id="now-what">Now what?</h2>
285
286<p>Now that we can at least <em>get</em> the data again, how are we going to leverage it? Remember I was using this in my terminal - using <code class="language-plaintext highlighter-rouge">curl</code> to go fetch the data with an environment variable with the API key. What’s best here?</p>
287
288<p>Whatever we use is going to need to access the private key - meaning whatever is generating those JWTs will need to be trusted - local, remote, etc.</p>
289
290<h3 id="local-executable">Local executable</h3>
291
292<p>This is pretty straightforward, could easily balance JWT lifetime with overhead. Perhaps once-per-day with a 24-hour JWT? I wouldn’t normally advocate for long-lived tokens but as it’s locally generated, and it’s relatively low-risk data, it seems like a decent tradeoff.</p>
293
294<h3 id="remote">Remote</h3>
295
296<p>Something remote sounds neat too - like a Lambda or Azure Function - but once we leave our trusted space, we’ll have to figure out how only the correct clients can get tokens to call the weather service. Something like AAD, Okta, Auth0, etc to secure the API to get JWTs. Which, at that point, maybe is easier to just let the function/etc get the weather data itself <em>and cache it</em>, further reducing the number of paid API calls to WeatherKit. Something like:</p>
297
298<p>Client –&gt; (secured via cloud auth service) http proxy API (keeps Apple private key + generates JWTs) –&gt; WeatherKit</p>
299
300<p>This is, in fact, a lot like the BFF pattern (backends for frontends) used for things like storing secrets securely, caching API responses, etc.</p>
301
302<p>Remote has tradeoffs, of course, like hosting, downtime, etc. And it’s one more thing to break :D</p>
303
304<h2 id="next-well-pick">Next, we’ll pick!</h2>
305
306<p><a href="https://aka.wtf/twitch">Join me on Twitch</a> and we’ll decide what’s next!</p>
307
308  </div>
309
310</article>
311
312<div class="container mx-auto px-2 py-2 clearfix">
313  <!-- Use if you want to show previous and next for all posts. -->
314
315
316
317  <div class="col-4 sm-width-full left mr-lg-4 mt-3">
318    <a class="no-underline border-top-thin py-1 block" href="https://jpda.dev/gcc-mid-on-prem-data-gateway">
319      <span class="h5 bold text-accent">Previous</span>
320      <p class="bold h3 link-primary mb-1">Hacking on the On-prem data gateway for GCC-mid accounts</p>
321      <p>I had an interesting one come across my desk here at WTF HQ earlier this week - before I knew...</p>
322    </a>
323  </div>
324  
325  
326
327</div>
328
329    </div>
330
331    <div class="border-top-thin clearfix mt-2 mt-lg-4">
332  <div class="container mx-auto px-2">
333    <p class="col-8 sm-width-full left py-2 mb-0">This project is maintained by <a class="text-accent" href="https://github.com/jpda">jpda</a></p>
334    <ul class="list-reset right clearfix sm-width-full py-2 mb-2 mb-lg-0">
335      <li class="inline-block mr-1">
336        <a href="https://twitter.com/share" class="twitter-share-button" data-hashtags="jpda.dev">Tweet</a> 
336<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
336
337      </li>
338      <li class="inline-block">
339        <a class="github-button" href="https://github.com/jpda/jpda.github.io" data-icon="octicon-star" data-count-href="jpda//stargazers" data-count-api="/repos/jpda/#stargazers_count" data-count-aria-label="# stargazers on GitHub" aria-label="Star jpda/ on GitHub">Star</a>
340      </li>
341    </ul>
342  </div>
343</div>
344
345
346  </body>
347
348</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.