Extends the SyncProvider interface with an optional pushRoute method plus PushRoutePayload, PushRouteResult, and a typed PushError so the slice-4 action route can map error codes to user-facing copy without parsing HTTP statuses itself. Wahoo provider: - adds routes_write to the requested OAuth scope set - implements pushRoute: base64-encodes the FIT, builds the form body Wahoo's POST /v1/routes expects, parses the remote id out of the response, and throws typed PushError on 401/403/422/429/5xx - saveConnection now persists the requested scopes as grantedScopes on exchangeCode (Wahoo doesn't return a scope field, so the requested set is the granted set) Token refresh on 401 is deferred to the slice-4 action route — same pattern as the existing webhook handler in this repo, which does inline refresh rather than wrapping every call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { eq, and } from "drizzle-orm";
|
|
import { getDb } from "../db.ts";
|
|
import { syncConnections } from "@trails-cool/db/schema/journal";
|
|
import type { TokenSet } from "./types.ts";
|
|
|
|
export async function saveConnection(
|
|
userId: string,
|
|
provider: string,
|
|
tokens: TokenSet,
|
|
grantedScopes: string[] = [],
|
|
) {
|
|
const db = getDb();
|
|
// Upsert: delete existing connection for this user+provider, then insert
|
|
await db
|
|
.delete(syncConnections)
|
|
.where(and(eq(syncConnections.userId, userId), eq(syncConnections.provider, provider)));
|
|
await db.insert(syncConnections).values({
|
|
id: randomUUID(),
|
|
userId,
|
|
provider,
|
|
accessToken: tokens.accessToken,
|
|
refreshToken: tokens.refreshToken,
|
|
expiresAt: tokens.expiresAt,
|
|
providerUserId: tokens.providerUserId ?? null,
|
|
grantedScopes,
|
|
});
|
|
}
|
|
|
|
export async function getConnection(userId: string, provider: string) {
|
|
const db = getDb();
|
|
const [conn] = await db
|
|
.select()
|
|
.from(syncConnections)
|
|
.where(and(eq(syncConnections.userId, userId), eq(syncConnections.provider, provider)));
|
|
return conn ?? null;
|
|
}
|
|
|
|
export async function getConnectionByProviderUser(provider: string, providerUserId: string) {
|
|
const db = getDb();
|
|
const [conn] = await db
|
|
.select()
|
|
.from(syncConnections)
|
|
.where(
|
|
and(eq(syncConnections.provider, provider), eq(syncConnections.providerUserId, providerUserId)),
|
|
);
|
|
return conn ?? null;
|
|
}
|
|
|
|
export async function updateTokens(
|
|
connectionId: string,
|
|
tokens: TokenSet,
|
|
) {
|
|
const db = getDb();
|
|
await db
|
|
.update(syncConnections)
|
|
.set({
|
|
accessToken: tokens.accessToken,
|
|
refreshToken: tokens.refreshToken,
|
|
expiresAt: tokens.expiresAt,
|
|
})
|
|
.where(eq(syncConnections.id, connectionId));
|
|
}
|
|
|
|
export async function deleteConnection(userId: string, provider: string) {
|
|
const db = getDb();
|
|
await db
|
|
.delete(syncConnections)
|
|
.where(and(eq(syncConnections.userId, userId), eq(syncConnections.provider, provider)));
|
|
}
|