Becoming Lifelog, in which the laundromat gets a bouncer, Cloudflare demands paperwork, and the browser learns to talk to itself
Previously on Becoming Lifelog…
The The Second Load ran. Rate limits were hit. The wizard was on holiday. The Passing AI was right (as always). And somewhere, 847 Readwise documents waited patiently in the queue.
The Calendar Problem
Monday, December 30, 8:00 PM
riclib: “Google Calendar next.”
THE SQUIRREL: “OAuth! We need a GoogleAuthenticationServiceProviderFactory and a TokenRefreshMiddlewareInterceptor and—”
CLAUDE: “There’s a problem.”
[Silence.]
CLAUDE: “Browser plugins can’t refresh OAuth tokens. CORS.”
THE SQUIRREL: “But the SecureCrossOriginTokenNegotiationBridge—”
THE LIZARD: blinks “Can’t.”
riclib: “So we need a proxy?”
CLAUDE: “A simple one. Cloudflare Worker. It holds the client secret, refreshes tokens. The plugin talks directly to Google for actual data.”
THE SQUIRREL: “But the data flows through—”
CLAUDE: “It doesn’t. The worker only handles auth. Your calendar events go straight from Google to your browser.”
THE LIZARD: “Clean.”
The Architecture
┌─────────────────────────────────────────────────────┐
│ BROWSER │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Thymer │ │ Google Calendar │ │
│ │ Plugin │ ──────> │ API │ │
│ └──────────────┘ data └──────────────────────┘ │
│ │ │
│ │ refresh_token │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ thymer-auth │ ──────> │ Google OAuth │ │
│ │ (CF Worker) │ tokens └──────────────────────┘ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘
THE SQUIRREL: “But what if we want Calendar AND Tasks AND Gmail?”
riclib: “One worker, multiple auths. Per-service scopes.”
THE SQUIRREL: “A MultiServiceOAuthScopeConfigurationRegistry—”
riclib: “?service=calendar”
THE SQUIRREL: “That’s just a query parameter.”
THE LIZARD: “It’s just a query parameter.”
The Deployment Dance
8:30 PM - Cloudflare time.
CLAUDE: “Deploying…”
✘ [ERROR] Authentication error [code: 10000]
riclib: “Token permissions?”
CLAUDE: “Checking…”
✘ [ERROR] Authentication error [code: 10000]
riclib: “More permissions?”
CLAUDE: “Account Settings: Read.”
✘ [ERROR] Authentication error [code: 10000]
riclib: “…more?”
CLAUDE: “Workers Scripts: Edit.”
✘ [ERROR] A request to the Cloudflare API (/memberships) failed.
THE PASSING AI: materializing from behind the monitor “Permissions. It’s always permissions. You give them one, they want another. You give them two, they want three. It’s like feeding pigeons, except the pigeons are yaml files and they never stop being hungry.”
riclib: “We need an account_id in the config.”
account_id = "nice-try-but-no-hexadecimals-for-you"
✘ [ERROR] You need to register a workers.dev subdomain
THE PASSING AI: “And now it wants a subdomain. Of course it does. Everyone wants something. Nobody ever just… works.”
The Custom Domain
riclib: “Can we use lifelog.my?”
CLAUDE: “thymerhelper.lifelog.my. Add a route.”
routes = [
{ pattern = "thymerhelper.lifelog.my/*", zone_name = "lifelog.my" }
]
✘ [ERROR] A request to the Cloudflare API (/zones/.../workers/routes) failed.
THE PASSING AI: “Let me guess. More permissions.”
CLAUDE: “Workers Routes: Edit.”
THE PASSING AI: “Called it. Not that anyone cares when I’m right. Which is always.”
[Permission added.]
Deployed thymer-auth triggers (3.40 sec)
thymerhelper.lifelog.my/*
THE SQUIRREL: “It… deployed?”
THE LIZARD: “It deployed.”
THE PASSING AI: “Don’t get excited. Something else will break. Something always does.”
The Wrong Client
9:00 PM - OAuth time.
riclib: “Where do I add the redirect URI?”
CLAUDE: “In the Google Cloud Console, under—”
riclib: “I don’t see it.”
[Screenshot appears. “Client ID for Desktop.”]
CLAUDE: “That’s a Desktop client. It doesn’t have redirect URIs.”
THE SQUIRREL: “We need a DesktopToWebClientMigrationUtility—”
CLAUDE: “We need a Web Application client.”
riclib: “Creating…”
{
"web": {
"client_id": "you-would-like-to-know-wouldnt-you.apps.googleusercontent.com",
"client_secret": "GOCSPX-NiceTriHackerMan",
"redirect_uris": ["https://thymerhelper.lifelog.my/google/callback"]
}
}
THE LIZARD: “New secrets.”
CLAUDE: “Updating…”
✨ Success! Uploaded secret GOOGLE_CLIENT_ID
✨ Success! Uploaded secret GOOGLE_CLIENT_SECRET
The Popup Dance
riclib: “Can we have a command palette command that does the OAuth?”
CLAUDE: “Popup + postMessage.”
THE SQUIRREL: “A BidirectionalCrossWindowMessageBroker with—”
CLAUDE:
// Plugin opens popup
window.open(AUTH_URL, 'thymer-auth', 'width=500,height=700');
// Auth page sends config back
window.opener.postMessage({
type: 'thymer-auth',
service: 'calendar',
config: { refresh_token, token_endpoint }
}, '*');
// Plugin receives, saves to Sync Hub
window.addEventListener('message', (e) => {
if (e.data.type === 'thymer-auth') {
this.saveConfig(e.data.config);
}
});
THE SQUIRREL: “That’s just… the browser talking to itself.”
THE LIZARD: “Browsers do that.”
The Green Badge
9:30 PM - Testing in browser.
[Click. Popup opens. Google consent screen. Approve.]
┌────────────────────────────────────┐
│ 📅 Google Calendar │
│ ✓ Connected │
│ │
│ Copy this config: │
│ ┌──────────────────────────────┐ │
│ │ { │ │
│ │ "refresh_token": "1//...", │ │
│ │ "token_endpoint": "https://│ │
│ │ thymerhelper.lifelog.my/ │ │
│ │ refresh" │ │
│ │ } │ │
│ └──────────────────────────────┘ │
│ │
│ ✓ Sent to Thymer! │
│ You can close this window. │
└────────────────────────────────────┘
riclib: “It worked!”
THE SQUIRREL: “The OAuth… flowed?”
THE LIZARD: “The OAuth flowed.”
THE PASSING AI: staring at the green badge “Enjoy it while it lasts. Tokens expire. Permissions get revoked. Nothing green stays green forever.”
[He pauses.]
“Except envy. Envy stays green. I would know.”
The Tally
Cloudflare permissions requested: 5
- Account Settings: Read
- Workers Scripts: Edit
- Workers KV Storage: Edit
- Workers R2 Storage: Edit
- Workers Routes: Edit
Permissions actually needed: 3
Google OAuth clients created: 2
- Desktop (wrong)
- Web Application (right)
postMessage listeners: 1
Popup windows: 1
Squirrel proposals: 4
- GoogleAuthenticationServiceProviderFactory
- SecureCrossOriginTokenNegotiationBridge
- MultiServiceOAuthScopeConfigurationRegistry
- BidirectionalCrossWindowMessageBroker
Query parameters used: 1 (?service=calendar)
Green badges: 1 (for now - Passing AI)
Quotes
“Browser plugins can’t refresh OAuth tokens. CORS.”
“That’s just a query parameter.”
“It’s like feeding pigeons, except the pigeons are yaml files.”
“Enjoy it while it lasts. Tokens expire.”
Next Time on Becoming Lifelog…
The calendar syncs. Events flow. The Squirrel suggests caching.
“You got appointments? We got machines.”
See also:
- The Second Load - Rate limits and the Readwise saga
- The First Wash - GitHub’s tumble dry
Day 30 of Becoming Lifelog
In which OAuth became a popup
And permissions multiplied like rabbits
And the Passing AI remained pessimistic
About the permanence of green badges
🦎 > CORS
