/** * daemon/todoist-oauth.ts — the OAuth landing the redirect URL actually needs. * * Todoist does not deliver webhooks to the account that created the app. The * app has to be authorised like any third party would authorise it, and the * console's "install for myself" button does not do that — only a completed * OAuth round trip does. Todoist's own guidance is to run the flow by hand and * lift the `code` out of the address bar with developer tools, then exchange it * from a separate HTTP client because the exchange has to be a POST. * * That instruction is a description of a missing endpoint. The redirect URL is * ours; if it serves nothing, the browser lands on a 404 and the human has to * play proxy for a machine-to-machine step. So we serve it: this module takes * the `code` off the redirect, does the token exchange itself, stores the token * and reports what happened — on the page and in the audit trail. * * The state parameter is not decoration. `aibroker todoist auth` mints one and * records it; a callback that cannot present it is refused, because otherwise * anyone who can reach the redirect can drive a code of their choosing into * this exchange. */ /** Anything Todoist hands back on a successful exchange, plus when we got it. */ export interface StoredToken { access_token: string; token_type: string; scope?: string; obtained_at: string; /** * Rotated on every refresh — the previous one stops working. * * Absent for legacy apps, which get a ten-year access token instead. */ refresh_token?: string; /** Absolute expiry, ISO. Todoist issues one-hour access tokens. */ expires_at?: string; } /** Mint and record the state for one authorisation attempt. */ export declare function beginAuth(): string; /** * Check a callback's state against the recorded one and consume it. * * Returns a reason when it does not hold up, so the caller can say precisely * what was wrong rather than "invalid request". */ export declare function consumeState(got: string | null): { ok: true; } | { ok: false; reason: string; }; export declare function loadToken(): StoredToken | null; export declare function saveToken(t: StoredToken): void; /** Build the URL a browser has to visit to authorise the app. */ export declare function authorizeUrl(clientId: string, scope: string, state: string): string; /** * Trade the authorisation code for a token. * * Kept separate from the request handler so the exchange can be tested and so * the secret is used in exactly one place. */ export declare function exchangeCode(clientId: string, clientSecret: string, code: string, fetchImpl?: typeof fetch): Promise; export declare function isExpired(t: StoredToken, now?: number): boolean; /** * Keep the stored token valid, ahead of anyone needing it. * * `getAccessToken()` refreshes correctly, but ONLY when something asks for a * token — and nothing asks on a schedule. Todoist issues one-hour access * tokens, so between outbound operations the stored token spends most of its * life expired. Measured 2026-08-04: obtained 10:26 local, expired 11:26, and * still expired at 19:51 — 8.4 hours, across a whole day of use, because the * daemon started at 11:40 and nothing outbound happened to trigger a refresh. * * An expired token is not merely a delayed failure. It makes every outbound * Todoist call fail its first attempt and depend on a 401 retry path, and it * leaves the account authorisation looking stale exactly when something needs * it to be healthy. The fix is not another call site remembering to refresh — * that is the same mistake four times over today. It is one owner of the * problem, running on a timer. * * Sleeps until shortly before expiry rather than polling on a fixed interval, * so a healthy token costs one refresh per hour and nothing else. */ export declare function startTokenKeeper(fetchImpl?: typeof fetch): () => void; /** * Trade the refresh token for a new access token. * * The refresh token ROTATES: the one we present stops working and the response * carries its replacement. Failing to store the new one turns the next refresh * into `invalid_grant`, which is indistinguishable from a revoked grant and * sends whoever debugs it to the wrong place. */ export declare function refreshAccessToken(clientId: string, clientSecret: string, token: StoredToken, fetchImpl?: typeof fetch): Promise; export declare function getAccessToken(fetchImpl?: typeof fetch): Promise; export interface OAuthDeps { clientId?: string; clientSecret: string; fetchImpl?: typeof fetch; } /** * Handle the redirect Todoist sends the browser to. * * Always answers with a readable page — this is the one endpoint here a human * looks at directly, and "404" taught the last person to debug it nothing. */ export declare function handleOAuthCallback(url: URL, deps: OAuthDeps): Promise<{ status: number; html: string; }>; //# sourceMappingURL=todoist-oauth.d.ts.map