Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Either way, set your production secrets (`SESSION_SECRET`, OAuth credentials) on
| `PATREON_CLIENT_SECRET` | No | N/A | Patreon OAuth2 Client Secret |
| `PATREON_WEBHOOK_SECRET` | No | N/A | Secret for verifying Patreon webhook signatures |

> AT Protocol (Bluesky) login needs **no environment variables at all** — it is a public OAuth client with no secret, so it is configured entirely through the `createStartupAPI` factory (see [Bluesky / AT Protocol](#bluesky--at-protocol-atproto) below).

> Environment variables hold only credentials/secrets (OAuth client IDs and all secrets) plus the per‑deployment values `ORIGIN_URL`, `AUTH_ORIGIN`, `USERS_PATH`, `ADMIN_IDS`, and `ENVIRONMENT`. **All other configuration — OAuth scopes, Patreon campaign id, the access policy, entitlement freshness — is passed to the `createStartupAPI` factory** (see [Access policy & provider entitlements](#access-policy--provider-entitlements)).

### Setting up OAuth
Expand Down Expand Up @@ -99,6 +101,46 @@ Either way, set your production secrets (`SESSION_SECRET`, OAuth credentials) on
3. Add your authorized redirect URI: `https://<your-worker-url>/users/auth/patreon/callback`
4. Copy the **Client ID** and **Client Secret** and add them to your Worker's environment variables

#### Bluesky / AT Protocol (atproto)

atproto login is decentralized: there is **no central provider to register with and no client secret**. Instead the worker acts as a [public OAuth client](https://atproto.com/specs/oauth) identified by a client-metadata document it serves itself, and it discovers the right authorization server **per user** from their handle or DID — so it works with `bsky.social` and any self-hosted PDS alike, with no Bluesky host hardcoded.

Because it has no secrets, atproto is configured **entirely through the `createStartupAPI` factory** (not env vars). Just like the env-credential providers are enabled by the presence of their credentials, atproto is enabled simply by **including its config key** — an empty object is enough:

```ts
import { createStartupAPI } from '@startup-api/cloudflare';

const api = createStartupAPI({
providers: {
atproto: {}, // including the key enables it — no client id/secret needed
// All fields below are optional:
// atproto: {
// clientName: 'My App', // shown on the consent screen (default "StartupAPI")
// plcUrl: 'https://plc.directory', // override the PLC directory for did:plc
// dohUrl: 'https://cloudflare-dns.com/dns-query', // override the DoH resolver
// scopes: 'transition:generic', // extra scopes on top of the base `atproto`
// enabled: false, // explicit opt-out (e.g. for dynamically-built config)
// },
},
});

export default api.default;
export const { UserDO, AccountDO, SystemDO, CredentialDO } = api;
```

1. Include `atproto: {}` in the factory `providers` config (no client id/secret needed). Pass `enabled: false` to opt out explicitly.
2. Deploy over **HTTPS** with a stable hostname. The worker automatically serves its client metadata at `https://<your-worker-url>/users/auth/atproto/client-metadata.json` (this URL is the OAuth `client_id`) and registers the redirect URI `https://<your-worker-url>/users/auth/atproto/callback`.
3. That's it. When a visitor clicks **Continue with Bluesky**, they're asked for their handle (e.g. `alice.bsky.social`) or DID; the worker then resolves it through the full atproto discovery chain and redirects them to _their own_ server to sign in:

```
handle ─▶ DID (HTTPS .well-known/atproto-did, then DNS _atproto.<handle> via DoH)
DID ─▶ DID document (did:plc via the PLC directory, did:web via the domain)
DID doc─▶ PDS endpoint (the #atproto_pds service)
PDS ─▶ auth server (.well-known/oauth-protected-resource → oauth-authorization-server)
```

The flow uses PKCE, DPoP-bound (sender-constrained) tokens, and Pushed Authorization Requests (PAR) as required by the atproto OAuth profile. The PLC directory and DNS-over-HTTPS resolver are generic infrastructure and can be overridden via the `plcUrl` / `dohUrl` factory options.

#### Requesting additional scopes

Each provider requests the minimal scopes needed to sign a user in and read their basic profile. To request more (for example, to read a user's Patreon memberships), set the provider's `scopes` in the factory config (a string or array). The extra scopes are merged with the required base scopes:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@startup-api/cloudflare",
"version": "0.3.2",
"version": "0.4.0",
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
Expand Down
23 changes: 23 additions & 0 deletions public/users/power-strip.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ class PowerStrip extends HTMLElement {
return `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill="#9146FF" stroke="white" stroke-width="1"/><path d="M7 6H6v10h2v3l3-3h3l4-4V6H7zm9 6l-2 2h-3l-2 2v-2H8V7h8v5z" fill="white"/><path d="M14 8.5h1.5v2H14V8.5zm-3 0h1.5v2H11v-2z" fill="white"/></svg>`;
} else if (provider === 'patreon') {
return `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill="#FF424D" stroke="white" stroke-width="1"/><circle cx="14" cy="11" r="3.5" fill="white"/><rect x="6.5" y="6.5" width="2" height="11" fill="white"/></svg>`;
} else if (provider === 'atproto') {
return `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill="#0085FF" stroke="white" stroke-width="1"/><path d="M12 10.5C10.9 8.4 8.2 6.3 6.3 6c-1.5-.2-1.8.7-1.5 2 .2 1 1.5 5 2.3 6 .9 1.2 2 1.4 3 1.2-1.7.3-3.2 1-1.2 3 .9.9 1.6.3 2.1-.6.5-1 .8-2.1 1-2.6.2.5.5 1.6 1 2.6.5.9 1.2 1.5 2.1.6 2-2 .5-2.7-1.2-3 1 .2 2.1 0 3-1.2.8-1 2.1-5 2.3-6 .3-1.3 0-2.2-1.5-2-1.9.3-4.6 2.4-5.7 4.5z" fill="white"/></svg>`;
}
return '';
}
Expand All @@ -221,6 +223,7 @@ class PowerStrip extends HTMLElement {
const googleLink = `${this.basePath}/auth/google?return_url=${returnUrl}`;
const twitchLink = `${this.basePath}/auth/twitch?return_url=${returnUrl}`;
const patreonLink = `${this.basePath}/auth/patreon?return_url=${returnUrl}`;
const atprotoLink = `${this.basePath}/auth/atproto?return_url=${returnUrl}`;
const logoutLink = `${this.basePath}/logout?return_url=${returnUrl}`;

const providersStr = this.getAttribute('providers') || '';
Expand Down Expand Up @@ -257,6 +260,15 @@ class PowerStrip extends HTMLElement {
Continue with Patreon
</a>`;
}
if (providers.includes('atproto')) {
authButtons += `
<a href="${atprotoLink}" class="auth-btn atproto">
<svg viewBox="0 0 24 24">
<path d="M12 10.5C10.9 8.4 8.2 6.3 6.3 6c-1.5-.2-1.8.7-1.5 2 .2 1 1.5 5 2.3 6 .9 1.2 2 1.4 3 1.2-1.7.3-3.2 1-1.2 3 .9.9 1.6.3 2.1-.6.5-1 .8-2.1 1-2.6.2.5.5 1.6 1 2.6.5.9 1.2 1.5 2.1.6 2-2 .5-2.7-1.2-3 1 .2 2.1 0 3-1.2.8-1 2.1-5 2.3-6 .3-1.3 0-2.2-1.5-2-1.9.3-4.6 2.4-5.7 4.5z" fill="currentColor"/>
</svg>
Continue with Bluesky
</a>`;
}

let content = '';
let accountSwitcher = '';
Expand Down Expand Up @@ -585,6 +597,7 @@ class PowerStrip extends HTMLElement {
.provider-badge.google { color: #3c4043; }
.provider-badge.twitch { color: #9146FF; }
.provider-badge.patreon { color: #FF424D; }
.provider-badge.atproto { color: #0085FF; }

.user-info {
display: flex;
Expand Down Expand Up @@ -773,6 +786,16 @@ class PowerStrip extends HTMLElement {
border-color: #e63a44;
}

.auth-btn.atproto {
background-color: #0085FF;
color: white;
border-color: #0085FF;
}
.auth-btn.atproto:hover {
background-color: #006fd6;
border-color: #006fd6;
}

/* Account Switcher Styling */
.account-list {
display: flex;
Expand Down
4 changes: 4 additions & 0 deletions public/users/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ <h3>Link another account</h3>
return `<svg viewBox="0 0 24 24" width="24" height="24" style="color: #FF424D;">
<path d="M14.82 2.41c3.96 0 7.18 3.24 7.18 7.21 0 3.96-3.22 7.18-7.18 7.18-3.97 0-7.21-3.22-7.21-7.18 0-3.97 3.24-7.21 7.21-7.21M2 21.6h3.5V2.41H2V21.6z" fill="currentColor"/>
</svg>`;
} else if (provider === 'atproto') {
return `<svg viewBox="0 0 24 24" width="24" height="24" style="color: #0085FF;">
<path d="M12 10.5C10.9 8.4 8.2 6.3 6.3 6c-1.5-.2-1.8.7-1.5 2 .2 1 1.5 5 2.3 6 .9 1.2 2 1.4 3 1.2-1.7.3-3.2 1-1.2 3 .9.9 1.6.3 2.1-.6.5-1 .8-2.1 1-2.6.2.5.5 1.6 1 2.6.5.9 1.2 1.5 2.1.6 2-2 .5-2.7-1.2-3 1 .2 2.1 0 3-1.2.8-1 2.1-5 2.3-6 .3-1.3 0-2.2-1.5-2-1.9.3-4.6 2.4-5.7 4.5z" fill="currentColor"/>
</svg>`;
}
return '';
}
Expand Down
Loading