new export improvements
This commit is contained in:
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
Binary file not shown.
@@ -168,7 +168,9 @@ Even in Stage 1, users need a way to log in to like courses and have those likes
|
||||
6. Worker sets the encrypted blob as an HTTP-only, Secure, SameSite=Lax cookie named `rn_session`.
|
||||
7. On all subsequent authenticated requests, the Worker decrypts the cookie to recover the athlete ID and access token. No D1 lookup needed.
|
||||
|
||||
**D1 is not required in Stage 1.** The cookie carries all state needed for browser-based authentication. D1 is introduced in Stage 2 for the `is_organizer` flag and challenge-related state.
|
||||
**Token refresh in Stage 1:** On each authenticated request the Worker decrypts the cookie and checks `expiresAt`. If the access token has expired, the Worker calls `POST https://intervals.icu/oauth/token` with the `refreshToken` from the cookie, receives a new `{accessToken, refreshToken, expiresAt}`, and rewrites the `rn_session` cookie before continuing. The refresh token is never written to D1 or KV — the cookie is the only storage. This is sufficient for Stage 1.
|
||||
|
||||
**D1 is not required in Stage 1.** The cookie is entirely self-contained. D1 is introduced in Stage 2 for the `is_organizer` flag and challenge-related state. At that point the refresh token can optionally migrate to D1 (stored encrypted), which would enable server-side session revocation — currently not possible since there is no server-side session record to invalidate.
|
||||
|
||||
**CrewNerd API key:**
|
||||
|
||||
@@ -183,12 +185,13 @@ async function apiKeyForAthlete(athleteId: string, secret: string): Promise<stri
|
||||
const sig = await crypto.subtle.sign(
|
||||
'HMAC', key, new TextEncoder().encode(athleteId)
|
||||
);
|
||||
return btoa(String.fromCharCode(...new Uint8Array(sig)))
|
||||
const mac = btoa(String.fromCharCode(...new Uint8Array(sig)))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
return `${athleteId}.${mac}`;
|
||||
}
|
||||
```
|
||||
|
||||
The same function run twice on the same athlete ID always produces the same key. On an incoming CrewNerd API request, the Worker derives the expected key from the athlete ID embedded in the request and compares — no storage or lookup needed. The API key is shown to the user on their profile page so they can configure CrewNerd.
|
||||
The same function run twice on the same athlete ID always produces the same key. On an incoming CrewNerd API request, the Worker splits the key on `.`, recomputes the HMAC for the embedded athlete ID, and compares using constant-time equality — no storage or lookup needed.
|
||||
|
||||
**Scopes:** both `PROFILE_READ` and `ACTIVITY_READ` are requested at Stage 1 login. `ACTIVITY_READ` is not used until Stage 2 GPS validation, but requesting it upfront avoids a re-authorisation prompt when Stage 2 launches.
|
||||
|
||||
@@ -212,7 +215,13 @@ Pre-generated KML files are stored in `kml/{id}.kml` in the repository and serve
|
||||
|
||||
These endpoints must be present and respond identically to the current Rowsandall endpoints. Authentication is via `Authorization: ApiKey {key}` header — the same scheme CrewNerd uses today with Rowsandall, requiring no code change on Tony's side.
|
||||
|
||||
The API key is verified by re-deriving it from the athlete ID using `apiKeyForAthlete()` and comparing — no KV or D1 lookup needed.
|
||||
**API key format:** `{athleteId}.{HMAC(athleteId, TOKEN_ENCRYPTION_KEY)}` — for example `i12345.abc123...`. The athlete ID is embedded in the key so the Worker can recover it without any KV or D1 lookup.
|
||||
|
||||
**Verification** (`apiKeyForAthlete()`):
|
||||
1. Split the presented key on `.` → `athleteId` + `mac`.
|
||||
2. Recompute `HMAC(athleteId, TOKEN_ENCRYPTION_KEY)`.
|
||||
3. Compare using constant-time equality — if it matches, the key is authentic and `athleteId` is trusted.
|
||||
4. Use `athleteId` directly for all subsequent operations (e.g. `liked:{athleteId}` KV lookup).
|
||||
|
||||
**API key issuance for CrewNerd — no browser redirect needed:**
|
||||
|
||||
@@ -225,7 +234,7 @@ Authorization: Bearer {intervals_access_token}
|
||||
← {"api_key": "abc123..."}
|
||||
```
|
||||
|
||||
The Worker verifies the bearer token by calling `GET https://intervals.icu/api/v1/athlete/self` with it, extracts the athlete ID from the response, derives the API key using `apiKeyForAthlete()`, and returns it. CrewNerd stores the key and uses it for all subsequent calls. From the user's perspective: tap "Connect to rownative.icu" in CrewNerd, done — no browser redirect, no manual key entry. This requires agreement with Tony Andrews on the endpoint design (see open question 1).
|
||||
The Worker verifies the bearer token by calling `GET https://intervals.icu/api/v1/athlete/self` with it, extracts the `athleteId` from the response, constructs the key as `{athleteId}.{HMAC(athleteId, TOKEN_ENCRYPTION_KEY)}`, and returns it. CrewNerd stores the key and uses it for all subsequent calls. From the user's perspective: tap "Connect to rownative.icu" in CrewNerd, done — no browser redirect, no manual key entry. This requires agreement with Tony Andrews on the endpoint design (see open question 1).
|
||||
|
||||
**Course endpoints:**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user