AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
0

Configure Feed

Select the types of activity you want to include in your feed.

Use the conventional client_id so the OAuth screen shows just the host

atproto's OAuth consent screen only collapses the client display to a bare
hostname when client_id matches the "conventional" form —
https://<host>/oauth-client-metadata.json (exact path, no port, no query),
per isConventionalOAuthClientId in @atproto/oauth-types. Any other path makes
the screen render the full metadata URL.

AT Mot served its metadata at /client-metadata.json, so the consent screen
showed the ugly full URL instead of "atmot.herve.bzh". Rename the file to the
conventional name, update client_id to match, and point the build, tests, and
docs at the new path. Adds a regression test asserting the conventional form.

Existing sessions re-authorize once, since their tokens were issued against
the old client_id.

Fixes #1

+31 -7
+10
DECISIONS.md
··· 114 114 from the hash params on load. Dev uses atproto's loopback client via a Vite plugin (no hosted 115 115 metadata needed locally). 116 116 117 + 17a. **`client_id` uses the conventional filename** `oauth-client-metadata.json` (served at 118 + `https://atmot.herve.bzh/oauth-client-metadata.json`), not an arbitrary name like 119 + `client-metadata.json`. atproto's OAuth provider only collapses the consent-screen client display 120 + to the bare hostname (`atmot.herve.bzh`) when `client_id` matches the "conventional" form — 121 + `https://<host>/oauth-client-metadata.json`, exact path, no port, no query string 122 + (`isConventionalOAuthClientId` in `@atproto/oauth-types`). Any other path makes the screen render 123 + the full metadata URL instead. Renaming costs a one-time re-authorization for existing sessions 124 + (tokens were issued against the old `client_id`), which is acceptable for a casual daily game. 125 + See issue #1. 126 + 117 127 17b. **Granular OAuth scopes — no `transition:generic`.** The client requests exactly the writes it 118 128 performs, nothing more: 119 129
+3 -1
README.md
··· 102 102 ``` 103 103 104 104 OAuth in dev uses atproto's loopback client (no hosted metadata needed). In production the SPA serves 105 - `/client-metadata.json`, and `client_id` equals that exact URL. 105 + `/oauth-client-metadata.json`, and `client_id` equals that exact URL. The `oauth-client-metadata.json` 106 + filename is the AT Protocol "conventional" client-ID form, which lets the consent screen show just the 107 + `atmot.herve.bzh` hostname instead of the full metadata URL. 106 108 107 109 AT Mot requests **granular OAuth scopes** (least privilege) rather than the broad `transition:generic` 108 110 — write access is limited to exactly the three collections it creates: `bzh.herve.atmot.result`,
+1 -1
env.d.ts
··· 3 3 /// <reference types="@atcute/bluesky" /> 4 4 5 5 interface ImportMetaEnv { 6 - /** OAuth `client_id` — the public URL of client-metadata.json (or a loopback client_id in dev). */ 6 + /** OAuth `client_id` — the public URL of oauth-client-metadata.json (or a loopback client_id in dev). */ 7 7 readonly VITE_OAUTH_CLIENT_ID: string; 8 8 /** OAuth redirect URI the authorization server sends the user back to. */ 9 9 readonly VITE_OAUTH_REDIRECT_URI: string;
+1 -1
public/client-metadata.json public/oauth-client-metadata.json
··· 1 1 { 2 - "client_id": "https://atmot.herve.bzh/client-metadata.json", 2 + "client_id": "https://atmot.herve.bzh/oauth-client-metadata.json", 3 3 "client_name": "AT Mot", 4 4 "client_uri": "https://atmot.herve.bzh", 5 5 "logo_uri": "https://atmot.herve.bzh/icon.svg",
+14 -2
tests/oauth-scope.test.ts
··· 6 6 // Resolve paths relative to the repo root (this file is in tests/). 7 7 const root = fileURLToPath(new URL('..', import.meta.url)); 8 8 9 - const metadata = JSON.parse(readFileSync(root + 'public/client-metadata.json', 'utf8')); 9 + const metadata = JSON.parse(readFileSync(root + 'public/oauth-client-metadata.json', 'utf8')); 10 10 const scopes: string[] = String(metadata.scope).split(/\s+/).filter(Boolean); 11 11 12 12 /** Find the granted `repo:<nsid>` token for a collection, or undefined. */ 13 13 const repoScope = (nsid: string) => 14 14 scopes.find((s) => s === `repo:${nsid}` || s.startsWith(`repo:${nsid}?`)); 15 15 16 - describe('OAuth scope (client-metadata.json)', () => { 16 + describe('OAuth scope (oauth-client-metadata.json)', () => { 17 17 it('requests the base atproto scope', () => { 18 18 expect(scopes).toContain('atproto'); 19 + }); 20 + 21 + // The atproto OAuth consent screen only collapses the client display to a 22 + // bare hostname (instead of the full metadata URL) when client_id is in the 23 + // "conventional" form: https://<host>/oauth-client-metadata.json, served at 24 + // that exact path with no port or query string. See issue #1. 25 + it('uses the conventional client_id so the consent screen shows just the host', () => { 26 + const url = new URL(metadata.client_id); 27 + expect(url.protocol).toBe('https:'); 28 + expect(url.pathname).toBe('/oauth-client-metadata.json'); 29 + expect(url.port).toBe(''); 30 + expect(url.search).toBe(''); 19 31 }); 20 32 21 33 it('uses granular least-privilege scopes, never broad transition:generic', () => {
+2 -2
vite.config.ts
··· 1 1 import { defineConfig } from 'vite'; 2 - import metadata from './public/client-metadata.json' with { type: 'json' }; 2 + import metadata from './public/oauth-client-metadata.json' with { type: 'json' }; 3 3 4 4 // AT Protocol OAuth forbids `localhost` — use the loopback IP instead. 5 5 const SERVER_HOST = '127.0.0.1'; ··· 11 11 plugins: [ 12 12 { 13 13 // Wires the OAuth client_id / redirect_uri into the build. 14 - // In production we use the real hosted client-metadata.json URL. 14 + // In production we use the real hosted oauth-client-metadata.json URL. 15 15 // In dev, atproto allows a special loopback `client_id` that encodes 16 16 // the redirect_uri + scope, so no public metadata document is needed. 17 17 name: 'atmot-oauth-envs',