···133133134134## OAuth integration notes
135135136136-The login flow requests this scope (see `lib/oauth.ts`):
136136+The login flow currently requests explicit granular scopes (see `lib/oauth.ts`):
137137138138```
139139-atproto include:com.atmosphereaccount.registry.fullPermissions blob:image/*
139139+atproto repo:com.atmosphereaccount.registry.profile repo:com.atmosphereaccount.registry.review repo:com.atmosphereaccount.registry.update blob:image/*
140140```
141141142142-- **`include:...`** — the authorization server resolves this NSID via DNS and
143143- reads the schema record's `title` / `detail` to render the consent dialog. It
144144- also expands the `permissions[]` array into the actual granted scope. If
145145- resolution fails the PDS returns `invalid_scope`, which is exactly what
146146- blocked logins until DNS was set up.
142142+- **`repo:...`** — direct write access to the Atmosphere profile, review, and
143143+ update collections. We request direct scopes so login keeps working on PDSes
144144+ that do not yet resolve DNS-backed permission sets correctly.
147145- **`blob:image/*`** is a top-level scope on purpose. The atproto permission
148146 spec
149147 [explicitly disallows `blob` permissions inside
150148 permission sets](https://atproto.com/specs/permission#permission-sets) — they
151149 must always be requested separately.
152150151151+We still publish `com.atmosphereaccount.registry.fullPermissions` so compatible
152152+authorization servers can present a human-friendly permission set in the future.
153153If you change the permission set's `title`, `detail`, or `permissions[]`,
154154-remember the consent dialog won't reflect it until you `lex:publish:update`
155155-**and** the cache on the user's auth server expires.
154154+remember compatible consent dialogs won't reflect it until you
155155+`lex:publish:update` **and** the cache on the user's auth server expires.
156156157157---
158158
+12-25
lib/oauth.ts
···3838/**
3939 * Minimum-permission scope.
4040 *
4141- * Composed of three parts:
4242- * - `atproto` - identity
4343- * - `include:com.atmosphereaccount.registry.fullPermissions` - repo writes
4444- * to our
4545- * profile +
4646- * review
4747- * collections
4848- * (resolved
4949- * dynamically
5050- * from the
5151- * published
5252- * permission-set
5353- * lexicon)
5454- * - `blob:image/*` - avatar +
5555- * SVG icon
5656- * uploads
4141+ * Composed of explicit granular scopes:
4242+ * - `atproto` - identity
4343+ * - `repo:com.atmosphereaccount.registry.profile` - profile writes
4444+ * - `repo:com.atmosphereaccount.registry.review` - review writes
4545+ * - `repo:com.atmosphereaccount.registry.update` - update writes
4646+ * - `blob:image/*` - avatar +
4747+ * SVG icon
4848+ * uploads
5749 *
5858- * The `blob` scope is intentionally NOT bundled into the permission set
5959- * because the atproto permission spec explicitly disallows blob permissions
6060- * inside permission sets — they must always be requested separately.
6161- * See https://atproto.com/specs/permission ("Permission Sets").
6262- *
6363- * The permission-set lexicon must be published to the DID that holds DNS
6464- * authority for `_lexicon.registry.atmosphereaccount.com` before this scope
6565- * will resolve. See `docs/PUBLISHING_LEXICONS.md` for setup steps.
5050+ * We keep `com.atmosphereaccount.registry.fullPermissions` published as a
5151+ * human-friendly permission set, but request direct repo scopes here so login
5252+ * does not depend on every PDS correctly resolving DNS-backed permission sets.
6653 *
6754 * MUST stay in sync with `routes/oauth/client-metadata.json.ts`.
6855 */
6956const DEFAULT_SCOPE =
7070- "atproto include:com.atmosphereaccount.registry.fullPermissions blob:image/*";
5757+ "atproto repo:com.atmosphereaccount.registry.profile repo:com.atmosphereaccount.registry.review repo:com.atmosphereaccount.registry.update blob:image/*";
7158const STATE_TTL_MS = 10 * 60 * 1000;
7259const ACCESS_TOKEN_REFRESH_THRESHOLD_MS = 60 * 1000;
7360
···3232 listProfileUpdates,
3333 type ProfileUpdateRow,
3434} from "../../lib/profile-updates.ts";
3535+import { syncProfileByIdentifier } from "../../lib/profile-sync.ts";
35363637export const handler = define.handlers({
3738 async GET(ctx) {
···4142 * user's own registry entry so the AccountMenu can deep-link to
4243 * their public page. The lookups are cheap and trigger from the
4344 * same DB connection. */
4444- const [profile, ownerProfile] = await Promise.all([
4545+ let [profile, ownerProfile] = await Promise.all([
4546 getProfileByHandle(handle).catch(() => null),
4647 user ? getProfileByDid(user.did).catch(() => null) : Promise.resolve(
4748 null,
4849 ),
4950 ]);
5151+ if (!profile) {
5252+ const synced = await syncProfileByIdentifier(handle).catch((err) => {
5353+ console.warn(`[explore] profile sync failed for ${handle}:`, err);
5454+ return false;
5555+ });
5656+ if (synced) {
5757+ profile = await getProfileByHandle(handle).catch(() => null);
5858+ }
5959+ }
5060 const [reviewSummary, reviews, ownReview, updates] = profile
5161 ? await Promise.all([
5262 getReviewSummary(profile.did).catch(() => emptyReviewSummary()),
+3-10
routes/oauth/client-metadata.json.ts
···2424 response_types: ["code"],
2525 redirect_uris: [redirectUri()],
2626 /**
2727- * Minimum-permission scope. The `include:` half is resolved by the
2828- * authorization server via DNS-based atproto lexicon resolution
2929- * (TXT record at `_lexicon.registry.atmosphereaccount.com`) and
3030- * rendered in the consent dialog via its `title` / `detail`.
3131- *
3232- * `blob:image/*` is requested as a top-level scope because the
3333- * atproto permission spec explicitly disallows blob permissions
3434- * inside permission sets.
3535- *
3627 * MUST stay in sync with `DEFAULT_SCOPE` in `lib/oauth.ts`.
2828+ * Direct repo scopes avoid depending on every PDS correctly resolving
2929+ * DNS-backed permission sets during login.
3730 */
3831 scope:
3939- "atproto include:com.atmosphereaccount.registry.fullPermissions blob:image/*",
3232+ "atproto repo:com.atmosphereaccount.registry.profile repo:com.atmosphereaccount.registry.review repo:com.atmosphereaccount.registry.update blob:image/*",
4033 dpop_bound_access_tokens: true,
4134 token_endpoint_auth_method: "private_key_jwt",
4235 token_endpoint_auth_signing_alg: "ES256",