/** * githubDeviceSignIn — let the REPORTER sign in, so the issue is filed as them. * * import { githubDeviceSignIn } from 'agentfootprint/observe'; * * const signIn = await githubDeviceSignIn({ clientId: 'Iv1.0123456789abcdef' }); * show(`Go to ${signIn.verificationUri} and enter ${signIn.userCode}`); * const { token, login } = await signIn.completed; // resolves on authorize * * GitHub's OAuth **device flow**, spoken as three plain `fetch` calls and * nothing else: request a code, show it to the human, poll until they approve. * Zero dependencies, and it runs in a browser as well as on a server — there is * no client secret in this flow, which is exactly why it is the one that works * from a page. * * ## Why this exists beside a server token * * A server-side PAT files every report as the application. That is right for an * automated "Report a problem" button: the app owns the repo, the app owns the * token. It is wrong when the value is ATTRIBUTION — a field tester filing * upstream should appear as themselves, so a maintainer can ask them a * follow-up question and so their report counts as theirs. * * | | server PAT | device sign-in | * |---|---|---| * | Who the issue is from | the application | the reporter | * | Where the token lives | server environment | the reporter's session, in memory | * | Human steps | none | one: enter a code, approve | * | Right for | in-app "report a problem" | field testers filing upstream | * * The token this returns is handed to {@link githubBugReporter} as `token`, * with no special-casing anywhere: a token is a token. * * ## Keep it in memory, for the session only * * **Never `localStorage`, never a cookie, never a log line.** A device-flow * token is a live credential for the account that approved it; persisting it in * a browser turns one XSS into a lasting account compromise. Hold it in a * variable, use it, drop it when the tab closes. * * ## Scopes are coarse here, and that is GitHub's design * * The device flow issues a CLASSIC OAuth token, and classic scopes are coarse: * `public_repo` (the default here) grants write across every public repository * the account can reach, and `repo` grants it across private ones too. There is * no fine-grained equivalent in this flow. That trade buys attribution — the * issue is really from that person — and it is the reason the default stops at * `public_repo`: filing an issue needs no more, and the token disappears with * the session. Where least privilege matters more than attribution, use a * fine-grained PAT on a server instead (see {@link githubBugReporter}). * * ## The collaborator caveat, stated rather than discovered * * A reporter who signs in as themselves can file an issue on a public repo, and * can commit evidence ONLY to a repository they can write to. Pointing * `evidenceRepo` at a private repo the reporter is not a collaborator on will * fail with a 404 (GitHub hides private repos from tokens that cannot see * them). Either add the reporter as a collaborator, or let them attach the zip * to the issue by hand — `exportBugReport` gives them the file either way. * * ## Secrecy * * The token appears in no message this module can produce. The three failure * shapes of the flow — the human denied it, the code expired, the poll was * aborted — are named plainly, with GitHub's `error_description` only, never a * response body or a request. */ export interface GithubDeviceSignInOptions { /** * The OAuth App's client id. **Public by design** — the device flow has no * client secret, so this belongs in your front-end code. Create the app once * under the organisation, tick "Enable Device Flow", and copy the id. */ readonly clientId: string; /** * Classic OAuth scopes. Default `['public_repo']` — enough to file an issue * and commit to a public evidence repo, and no more. `['repo']` is what a * private evidence repo needs, and it is a much larger grant; ask for it only * when the flow really commits there. */ readonly scopes?: readonly string[]; /** Cancel the polling (a closed dialog, an unmounted component). */ readonly signal?: AbortSignal; /** GitHub's web origin. GHES: `https://github.your-company.com`. */ readonly authBase?: string; /** GitHub's API root. GHES: `https://github.your-company.com/api/v3`. */ readonly apiBase?: string; /** Test seam — inject `fetch`. Bypasses the network entirely. */ readonly _fetch?: typeof fetch; /** Test seam — inject the wait between polls. */ readonly _sleep?: (ms: number, signal?: AbortSignal) => Promise; } /** Who signed in, and with what. */ export interface GithubDeviceIdentity { /** The access token. Memory-only; see the module docs. */ readonly token: string; /** Usually `bearer`. */ readonly tokenType: string; /** The scopes GitHub actually granted — not necessarily the ones asked for. */ readonly scopes: readonly string[]; /** The GitHub login the issue will be filed as. `undefined` if `/user` refused. */ readonly login?: string; } /** The code to show a human, and the promise that resolves when they approve. */ export interface GithubDeviceSignIn { /** The code the human types — show it verbatim, it is case-sensitive. */ readonly userCode: string; /** The page they type it into. */ readonly verificationUri: string; /** Seconds until `userCode` stops working. */ readonly expiresIn: number; /** Seconds GitHub asked us to wait between polls. */ readonly interval: number; /** * Resolves when the human approves, rejects when they deny it, when the code * expires, or when `signal` aborts. Polling starts immediately — awaiting * this later does not miss an approval. */ readonly completed: Promise; } /** * Start a device-flow sign-in. * * Resolves as soon as GitHub hands back a code — that is the point, because * the human cannot approve a code they have not been shown. The waiting happens * on the returned `completed` promise. * * @throws TypeError when `clientId` is missing (naming where it comes from). * @throws Error naming the status when GitHub refuses to issue a code. */ export declare function githubDeviceSignIn(options: GithubDeviceSignInOptions): Promise; //# sourceMappingURL=githubDeviceSignIn.d.ts.map