download
raw
5.71 kB
/**
* Configures a provider that supports pin code authentication. This is usually paired with the
* `CodeUI`.
*
* ```ts
* import { CodeUI } from "@openauthjs/openauth/ui/code"
* import { CodeProvider } from "@openauthjs/openauth/provider/code"
*
* export default issuer({
* providers: {
* code: CodeProvider(
* CodeUI({
* copy: {
* code_info: "We'll send a pin code to your email"
* },
* sendCode: (claims, code) => console.log(claims.email, code)
* })
* )
* },
* // ...
* })
* ```
*
* You can customize the provider using.
*
* ```ts {7-9}
* const ui = CodeUI({
* // ...
* })
*
* export default issuer({
* providers: {
* code: CodeProvider(
* { ...ui, length: 4 }
* )
* },
* // ...
* })
* ```
*
* Behind the scenes, the `CodeProvider` expects callbacks that implements request handlers
* that generate the UI for the following.
*
* ```ts
* CodeProvider({
* // ...
* request: (req, state, form, error) => Promise<Response>
* })
* ```
*
* This allows you to create your own UI.
*
* @packageDocumentation
*/
import { Context } from "hono"
import { Provider } from "./provider.js"
import { generateUnbiasedDigits, timingSafeCompare } from "../random.js"
export interface CodeProviderConfig<
Claims extends Record<string, string> = Record<string, string>,
> {
/**
* The length of the pin code.
*
* @default 6
*/
length?: number
/**
* The request handler to generate the UI for the code flow.
*
* Takes the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
* and optionally [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
* ojects.
*
* Also passes in the current `state` of the flow and any `error` that occurred.
*
* Expects the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
* in return.
*/
request: (
req: Request,
state: CodeProviderState,
form?: FormData,
error?: CodeProviderError,
) => Promise<Response>
/**
* Callback to send the pin code to the user.
*
* @example
* ```ts
* {
* sendCode: async (claims, code) => {
* // Send the code through the email or phone number based on the claims
* }
* }
* ```
*/
sendCode: (claims: Claims, code: string) => Promise<void | CodeProviderError>
}
/**
* The state of the code flow.
*
* | State | Description |
* | ----- | ----------- |
* | `start` | The user is asked to enter their email address or phone number to start the flow. |
* | `code` | The user needs to enter the pin code to verify their _claim_. |
*/
export type CodeProviderState =
| {
type: "start"
}
| {
type: "code"
resend?: boolean
code: string
claims: Record<string, string>
}
/**
* The errors that can happen on the code flow.
*
* | Error | Description |
* | ----- | ----------- |
* | `invalid_code` | The code is invalid. |
* | `invalid_claim` | The _claim_, email or phone number, is invalid. |
*/
export type CodeProviderError =
| {
type: "invalid_code"
}
| {
type: "invalid_claim"
key: string
value: string
}
export function CodeProvider<
Claims extends Record<string, string> = Record<string, string>,
>(config: CodeProviderConfig<Claims>): Provider<{ claims: Claims }> {
const length = config.length || 6
function generate() {
return generateUnbiasedDigits(length)
}
return {
type: "code",
init(routes, ctx) {
async function transition(
c: Context,
next: CodeProviderState,
fd?: FormData,
err?: CodeProviderError,
) {
await ctx.set<CodeProviderState>(c, "provider", 60 * 60 * 24, next)
const resp = ctx.forward(
c,
await config.request(c.req.raw, next, fd, err),
)
return resp
}
routes.get("/authorize", async (c) => {
const resp = await transition(c, {
type: "start",
})
return resp
})
routes.post("/authorize", async (c) => {
const code = generate()
const fd = await c.req.formData()
const state = await ctx.get<CodeProviderState>(c, "provider")
const action = fd.get("action")?.toString()
if (action === "request" || action === "resend") {
const claims = Object.fromEntries(fd) as Claims
delete claims.action
const err = await config.sendCode(claims, code)
if (err) return transition(c, { type: "start" }, fd, err)
return transition(
c,
{
type: "code",
resend: action === "resend",
claims,
code,
},
fd,
)
}
if (
fd.get("action")?.toString() === "verify" &&
state.type === "code"
) {
const fd = await c.req.formData()
const compare = fd.get("code")?.toString()
if (
!state.code ||
!compare ||
!timingSafeCompare(state.code, compare)
) {
return transition(
c,
{
...state,
resend: false,
},
fd,
{ type: "invalid_code" },
)
}
await ctx.unset(c, "provider")
return ctx.forward(
c,
await ctx.success(c, { claims: state.claims as Claims }),
)
}
})
},
}
}
/**
* @internal
*/
export type CodeProviderOptions = Parameters<typeof CodeProvider>[0]

Xet Storage Details

Size:
5.71 kB
·
Xet hash:
085fcaabc13bc349104f9d563abc8a4279e346d477cf7bd57525ae3adabb2915

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.