Browser quickstart
Use a public client with PKCE, registered origins, and no client secret.
Register a public browser app, including the exact HTTPS callback URL. Your app's origin must match a registered callback origin for cross-origin token and library requests. Do not ship a client secret.
1. Start PKCE in the browser
Run this after the user presses “Connect Comick.” The Web Crypto API requires a secure context (HTTPS or localhost).
const clientId = "YOUR_PUBLIC_CLIENT_ID"
const redirectUri = "https://your-app.example/oauth/comick/callback"
const resource = "https://api.comick.dev/integrations/v1"
function base64url(bytes: Uint8Array): string {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "")
}
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)))
const state = base64url(crypto.getRandomValues(new Uint8Array(32)))
const challenge = base64url(
new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)),
),
)
sessionStorage.setItem(
"comick.oauth.attempt",
JSON.stringify({
state,
verifier,
redirectUri,
createdAt: Date.now(),
}),
)
const authorize = new URL("https://comick.dev/api/auth/oauth2/authorize")
authorize.search = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
scope: "library:read",
resource,
state,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString()
location.assign(authorize.toString())This example supports one pending attempt per tab. Start the flow and handle its callback in the same tab and origin.
2. Validate and exchange on your callback page
Use the same clientId and resource constants as above. This code runs only on your registered callback route.
const query = new URLSearchParams(location.search)
const stored = sessionStorage.getItem("comick.oauth.attempt")
sessionStorage.removeItem("comick.oauth.attempt")
history.replaceState(null, "", location.pathname)
const attempt = stored ? JSON.parse(stored) : null
if (
!attempt ||
query.getAll("state").length !== 1 ||
query.get("state") !== attempt.state ||
Date.now() - attempt.createdAt > 10 * 60_000
) {
throw new Error("Invalid or expired authorization attempt")
}
if (query.has("error"))
throw new Error("Comick connection was declined or failed")
if (query.getAll("code").length !== 1 || !query.get("code")) {
throw new Error("Missing authorization code")
}
const response = await fetch("https://comick.dev/api/auth/oauth2/token", {
method: "POST",
credentials: "omit",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: clientId,
code: query.get("code")!,
redirect_uri: attempt.redirectUri,
code_verifier: attempt.verifier,
resource,
}),
})
if (!response.ok) throw new Error(`Comick exchange failed: ${response.status}`)
const tokens = await response.json()3. Read the user's library
const response = await fetch(
"https://api.comick.dev/integrations/v1/me/library",
{
credentials: "omit",
cache: "no-store",
headers: { Authorization: `Bearer ${tokens.access_token}` },
},
)
if (!response.ok) throw new Error(`Comick library failed: ${response.status}`)
const library = await response.json()Keep tokens in memory and reconnect when needed. The example uses session storage only for the short-lived PKCE attempt, never for access or refresh tokens. Avoid persistent token storage in localStorage; use a trusted backend if you need synchronization after the browser closes.
For optional refresh, request offline_access and follow the rotation rules. Browser clients must handle declined offline consent and cannot assume a refresh token will be returned.