@_mustachio/openauth 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/esm/client.js +186 -0
  2. package/dist/esm/css.d.js +0 -0
  3. package/dist/esm/error.js +73 -0
  4. package/dist/esm/index.js +14 -0
  5. package/dist/esm/issuer.js +558 -0
  6. package/dist/esm/jwt.js +16 -0
  7. package/dist/esm/keys.js +113 -0
  8. package/dist/esm/pkce.js +35 -0
  9. package/dist/esm/provider/apple.js +28 -0
  10. package/dist/esm/provider/arctic.js +43 -0
  11. package/dist/esm/provider/code.js +58 -0
  12. package/dist/esm/provider/cognito.js +16 -0
  13. package/dist/esm/provider/discord.js +15 -0
  14. package/dist/esm/provider/facebook.js +24 -0
  15. package/dist/esm/provider/github.js +15 -0
  16. package/dist/esm/provider/google.js +25 -0
  17. package/dist/esm/provider/index.js +3 -0
  18. package/dist/esm/provider/jumpcloud.js +15 -0
  19. package/dist/esm/provider/keycloak.js +15 -0
  20. package/dist/esm/provider/linkedin.js +15 -0
  21. package/dist/esm/provider/m2m.js +17 -0
  22. package/dist/esm/provider/microsoft.js +24 -0
  23. package/dist/esm/provider/oauth2.js +119 -0
  24. package/dist/esm/provider/oidc.js +69 -0
  25. package/dist/esm/provider/passkey.js +315 -0
  26. package/dist/esm/provider/password.js +306 -0
  27. package/dist/esm/provider/provider.js +10 -0
  28. package/dist/esm/provider/slack.js +15 -0
  29. package/dist/esm/provider/spotify.js +15 -0
  30. package/dist/esm/provider/twitch.js +15 -0
  31. package/dist/esm/provider/x.js +16 -0
  32. package/dist/esm/provider/yahoo.js +15 -0
  33. package/dist/esm/random.js +27 -0
  34. package/dist/esm/storage/aws.js +39 -0
  35. package/dist/esm/storage/cloudflare.js +42 -0
  36. package/dist/esm/storage/dynamo.js +116 -0
  37. package/dist/esm/storage/memory.js +88 -0
  38. package/dist/esm/storage/storage.js +36 -0
  39. package/dist/esm/subject.js +7 -0
  40. package/dist/esm/ui/base.js +407 -0
  41. package/dist/esm/ui/code.js +151 -0
  42. package/dist/esm/ui/form.js +43 -0
  43. package/dist/esm/ui/icon.js +92 -0
  44. package/dist/esm/ui/passkey.js +329 -0
  45. package/dist/esm/ui/password.js +338 -0
  46. package/dist/esm/ui/select.js +187 -0
  47. package/dist/esm/ui/theme.js +115 -0
  48. package/dist/esm/util.js +54 -0
  49. package/dist/types/client.d.ts +466 -0
  50. package/dist/types/client.d.ts.map +1 -0
  51. package/dist/types/error.d.ts +77 -0
  52. package/dist/types/error.d.ts.map +1 -0
  53. package/dist/types/index.d.ts +20 -0
  54. package/dist/types/index.d.ts.map +1 -0
  55. package/dist/types/issuer.d.ts +465 -0
  56. package/dist/types/issuer.d.ts.map +1 -0
  57. package/dist/types/jwt.d.ts +6 -0
  58. package/dist/types/jwt.d.ts.map +1 -0
  59. package/dist/types/keys.d.ts +18 -0
  60. package/dist/types/keys.d.ts.map +1 -0
  61. package/dist/types/pkce.d.ts +7 -0
  62. package/dist/types/pkce.d.ts.map +1 -0
  63. package/dist/types/provider/apple.d.ts +108 -0
  64. package/dist/types/provider/apple.d.ts.map +1 -0
  65. package/dist/types/provider/arctic.d.ts +16 -0
  66. package/dist/types/provider/arctic.d.ts.map +1 -0
  67. package/dist/types/provider/code.d.ts +74 -0
  68. package/dist/types/provider/code.d.ts.map +1 -0
  69. package/dist/types/provider/cognito.d.ts +64 -0
  70. package/dist/types/provider/cognito.d.ts.map +1 -0
  71. package/dist/types/provider/discord.d.ts +38 -0
  72. package/dist/types/provider/discord.d.ts.map +1 -0
  73. package/dist/types/provider/facebook.d.ts +74 -0
  74. package/dist/types/provider/facebook.d.ts.map +1 -0
  75. package/dist/types/provider/github.d.ts +38 -0
  76. package/dist/types/provider/github.d.ts.map +1 -0
  77. package/dist/types/provider/google.d.ts +74 -0
  78. package/dist/types/provider/google.d.ts.map +1 -0
  79. package/dist/types/provider/index.d.ts +4 -0
  80. package/dist/types/provider/index.d.ts.map +1 -0
  81. package/dist/types/provider/jumpcloud.d.ts +38 -0
  82. package/dist/types/provider/jumpcloud.d.ts.map +1 -0
  83. package/dist/types/provider/keycloak.d.ts +67 -0
  84. package/dist/types/provider/keycloak.d.ts.map +1 -0
  85. package/dist/types/provider/linkedin.d.ts +6 -0
  86. package/dist/types/provider/linkedin.d.ts.map +1 -0
  87. package/dist/types/provider/m2m.d.ts +34 -0
  88. package/dist/types/provider/m2m.d.ts.map +1 -0
  89. package/dist/types/provider/microsoft.d.ts +89 -0
  90. package/dist/types/provider/microsoft.d.ts.map +1 -0
  91. package/dist/types/provider/oauth2.d.ts +133 -0
  92. package/dist/types/provider/oauth2.d.ts.map +1 -0
  93. package/dist/types/provider/oidc.d.ts +91 -0
  94. package/dist/types/provider/oidc.d.ts.map +1 -0
  95. package/dist/types/provider/passkey.d.ts +143 -0
  96. package/dist/types/provider/passkey.d.ts.map +1 -0
  97. package/dist/types/provider/password.d.ts +210 -0
  98. package/dist/types/provider/password.d.ts.map +1 -0
  99. package/dist/types/provider/provider.d.ts +29 -0
  100. package/dist/types/provider/provider.d.ts.map +1 -0
  101. package/dist/types/provider/slack.d.ts +59 -0
  102. package/dist/types/provider/slack.d.ts.map +1 -0
  103. package/dist/types/provider/spotify.d.ts +38 -0
  104. package/dist/types/provider/spotify.d.ts.map +1 -0
  105. package/dist/types/provider/twitch.d.ts +38 -0
  106. package/dist/types/provider/twitch.d.ts.map +1 -0
  107. package/dist/types/provider/x.d.ts +38 -0
  108. package/dist/types/provider/x.d.ts.map +1 -0
  109. package/dist/types/provider/yahoo.d.ts +38 -0
  110. package/dist/types/provider/yahoo.d.ts.map +1 -0
  111. package/dist/types/random.d.ts +3 -0
  112. package/dist/types/random.d.ts.map +1 -0
  113. package/dist/types/storage/aws.d.ts +4 -0
  114. package/dist/types/storage/aws.d.ts.map +1 -0
  115. package/dist/types/storage/cloudflare.d.ts +34 -0
  116. package/dist/types/storage/cloudflare.d.ts.map +1 -0
  117. package/dist/types/storage/dynamo.d.ts +65 -0
  118. package/dist/types/storage/dynamo.d.ts.map +1 -0
  119. package/dist/types/storage/memory.d.ts +49 -0
  120. package/dist/types/storage/memory.d.ts.map +1 -0
  121. package/dist/types/storage/storage.d.ts +15 -0
  122. package/dist/types/storage/storage.d.ts.map +1 -0
  123. package/dist/types/subject.d.ts +122 -0
  124. package/dist/types/subject.d.ts.map +1 -0
  125. package/dist/types/ui/base.d.ts +5 -0
  126. package/dist/types/ui/base.d.ts.map +1 -0
  127. package/dist/types/ui/code.d.ts +104 -0
  128. package/dist/types/ui/code.d.ts.map +1 -0
  129. package/dist/types/ui/form.d.ts +6 -0
  130. package/dist/types/ui/form.d.ts.map +1 -0
  131. package/dist/types/ui/icon.d.ts +6 -0
  132. package/dist/types/ui/icon.d.ts.map +1 -0
  133. package/dist/types/ui/passkey.d.ts +5 -0
  134. package/dist/types/ui/passkey.d.ts.map +1 -0
  135. package/dist/types/ui/password.d.ts +139 -0
  136. package/dist/types/ui/password.d.ts.map +1 -0
  137. package/dist/types/ui/select.d.ts +55 -0
  138. package/dist/types/ui/select.d.ts.map +1 -0
  139. package/dist/types/ui/theme.d.ts +207 -0
  140. package/dist/types/ui/theme.d.ts.map +1 -0
  141. package/dist/types/util.d.ts +8 -0
  142. package/dist/types/util.d.ts.map +1 -0
  143. package/package.json +51 -0
  144. package/src/client.ts +749 -0
  145. package/src/css.d.ts +4 -0
  146. package/src/error.ts +120 -0
  147. package/src/index.ts +26 -0
  148. package/src/issuer.ts +1302 -0
  149. package/src/jwt.ts +17 -0
  150. package/src/keys.ts +139 -0
  151. package/src/pkce.ts +40 -0
  152. package/src/provider/apple.ts +127 -0
  153. package/src/provider/arctic.ts +66 -0
  154. package/src/provider/code.ts +227 -0
  155. package/src/provider/cognito.ts +74 -0
  156. package/src/provider/discord.ts +45 -0
  157. package/src/provider/facebook.ts +84 -0
  158. package/src/provider/github.ts +45 -0
  159. package/src/provider/google.ts +85 -0
  160. package/src/provider/index.ts +3 -0
  161. package/src/provider/jumpcloud.ts +45 -0
  162. package/src/provider/keycloak.ts +75 -0
  163. package/src/provider/linkedin.ts +12 -0
  164. package/src/provider/m2m.ts +56 -0
  165. package/src/provider/microsoft.ts +100 -0
  166. package/src/provider/oauth2.ts +297 -0
  167. package/src/provider/oidc.ts +179 -0
  168. package/src/provider/passkey.ts +655 -0
  169. package/src/provider/password.ts +672 -0
  170. package/src/provider/provider.ts +33 -0
  171. package/src/provider/slack.ts +67 -0
  172. package/src/provider/spotify.ts +45 -0
  173. package/src/provider/twitch.ts +45 -0
  174. package/src/provider/x.ts +46 -0
  175. package/src/provider/yahoo.ts +45 -0
  176. package/src/random.ts +24 -0
  177. package/src/storage/aws.ts +59 -0
  178. package/src/storage/cloudflare.ts +77 -0
  179. package/src/storage/dynamo.ts +193 -0
  180. package/src/storage/memory.ts +135 -0
  181. package/src/storage/storage.ts +46 -0
  182. package/src/subject.ts +130 -0
  183. package/src/ui/base.tsx +118 -0
  184. package/src/ui/code.tsx +215 -0
  185. package/src/ui/form.tsx +40 -0
  186. package/src/ui/icon.tsx +95 -0
  187. package/src/ui/passkey.tsx +321 -0
  188. package/src/ui/password.tsx +405 -0
  189. package/src/ui/select.tsx +221 -0
  190. package/src/ui/theme.ts +319 -0
  191. package/src/ui/ui.css +252 -0
  192. package/src/util.ts +58 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Configure OpenAuth to use a simple in-memory store.
3
+ *
4
+ * :::caution
5
+ * This is not meant to be used in production.
6
+ * :::
7
+ *
8
+ * This is useful for testing and development. It's not meant to be used in production.
9
+ *
10
+ * ```ts
11
+ * import { MemoryStorage } from "@openauthjs/openauth/storage/memory"
12
+ *
13
+ * const storage = MemoryStorage()
14
+ *
15
+ * export default issuer({
16
+ * storage,
17
+ * // ...
18
+ * })
19
+ * ```
20
+ *
21
+ * Optionally, you can persist the store to a file.
22
+ *
23
+ * ```ts
24
+ * MemoryStorage({
25
+ * persist: "./persist.json"
26
+ * })
27
+ * ```
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ import { joinKey, splitKey, StorageAdapter } from "./storage.js"
32
+ import { existsSync, readFileSync } from "node:fs"
33
+ import { writeFile } from "node:fs/promises"
34
+
35
+ /**
36
+ * Configure the memory store.
37
+ */
38
+ export interface MemoryStorageOptions {
39
+ /**
40
+ * Optionally, backup the store to a file. So it'll be persisted when the issuer restarts.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * {
45
+ * persist: "./persist.json"
46
+ * }
47
+ * ```
48
+ */
49
+ persist?: string
50
+ }
51
+ export function MemoryStorage(input?: MemoryStorageOptions): StorageAdapter {
52
+ const store = [] as [
53
+ string,
54
+ { value: Record<string, any>; expiry?: number },
55
+ ][]
56
+
57
+ if (input?.persist) {
58
+ if (existsSync(input.persist)) {
59
+ const file = readFileSync(input?.persist)
60
+ store.push(...JSON.parse(file.toString()))
61
+ }
62
+ }
63
+
64
+ async function save() {
65
+ if (!input?.persist) return
66
+ const file = JSON.stringify(store)
67
+ await writeFile(input.persist, file)
68
+ }
69
+
70
+ function search(key: string) {
71
+ let left = 0
72
+ let right = store.length - 1
73
+ while (left <= right) {
74
+ const mid = Math.floor((left + right) / 2)
75
+ const comparison = key.localeCompare(store[mid][0])
76
+
77
+ if (comparison === 0) {
78
+ return { found: true, index: mid }
79
+ } else if (comparison < 0) {
80
+ right = mid - 1
81
+ } else {
82
+ left = mid + 1
83
+ }
84
+ }
85
+ return { found: false, index: left }
86
+ }
87
+ return {
88
+ async get(key: string[]) {
89
+ const match = search(joinKey(key))
90
+ if (!match.found) return undefined
91
+ const entry = store[match.index][1]
92
+ if (entry.expiry && Date.now() >= entry.expiry) {
93
+ store.splice(match.index, 1)
94
+ await save()
95
+ return undefined
96
+ }
97
+ return entry.value
98
+ },
99
+ async set(key: string[], value: any, expiry?: Date) {
100
+ const joined = joinKey(key)
101
+ const match = search(joined)
102
+ // Handle both Date objects and TTL numbers while maintaining Date type in signature
103
+ const entry = [
104
+ joined,
105
+ {
106
+ value,
107
+ expiry: expiry ? expiry.getTime() : expiry,
108
+ },
109
+ ] as (typeof store)[number]
110
+ if (!match.found) {
111
+ store.splice(match.index, 0, entry)
112
+ } else {
113
+ store[match.index] = entry
114
+ }
115
+ await save()
116
+ },
117
+ async remove(key: string[]) {
118
+ const joined = joinKey(key)
119
+ const match = search(joined)
120
+ if (match.found) {
121
+ store.splice(match.index, 1)
122
+ await save()
123
+ }
124
+ },
125
+ async *scan(prefix: string[]) {
126
+ const now = Date.now()
127
+ const prefixStr = joinKey(prefix)
128
+ for (const [key, entry] of store) {
129
+ if (!key.startsWith(prefixStr)) continue
130
+ if (entry.expiry && now >= entry.expiry) continue
131
+ yield [splitKey(key), entry.value]
132
+ }
133
+ },
134
+ }
135
+ }
@@ -0,0 +1,46 @@
1
+ export interface StorageAdapter {
2
+ get(key: string[]): Promise<Record<string, any> | undefined>
3
+ remove(key: string[]): Promise<void>
4
+ set(key: string[], value: any, expiry?: Date): Promise<void>
5
+ scan(prefix: string[]): AsyncIterable<[string[], any]>
6
+ }
7
+
8
+ const SEPERATOR = String.fromCharCode(0x1f)
9
+
10
+ export function joinKey(key: string[]) {
11
+ return key.join(SEPERATOR)
12
+ }
13
+
14
+ export function splitKey(key: string) {
15
+ return key.split(SEPERATOR)
16
+ }
17
+
18
+ export namespace Storage {
19
+ function encode(key: string[]) {
20
+ return key.map((k) => k.replaceAll(SEPERATOR, ""))
21
+ }
22
+ export function get<T>(adapter: StorageAdapter, key: string[]) {
23
+ return adapter.get(encode(key)) as Promise<T | null>
24
+ }
25
+
26
+ export function set(
27
+ adapter: StorageAdapter,
28
+ key: string[],
29
+ value: any,
30
+ ttl?: number,
31
+ ) {
32
+ const expiry = ttl ? new Date(Date.now() + ttl * 1000) : undefined
33
+ return adapter.set(encode(key), value, expiry)
34
+ }
35
+
36
+ export function remove(adapter: StorageAdapter, key: string[]) {
37
+ return adapter.remove(encode(key))
38
+ }
39
+
40
+ export function scan<T>(
41
+ adapter: StorageAdapter,
42
+ key: string[],
43
+ ): AsyncIterable<[string[], T]> {
44
+ return adapter.scan(encode(key))
45
+ }
46
+ }
package/src/subject.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Subjects are what the access token generated at the end of the auth flow will map to. Under
3
+ * the hood, the access token is a JWT that contains this data.
4
+ *
5
+ * #### Define subjects
6
+ *
7
+ * ```ts title="subjects.ts"
8
+ * import { object, string } from "valibot"
9
+ *
10
+ * const subjects = createSubjects({
11
+ * user: object({
12
+ * userID: string()
13
+ * })
14
+ * })
15
+ * ```
16
+ *
17
+ * We are using [valibot](https://github.com/fabian-hiller/valibot) here. You can use any
18
+ * validation library that's following the
19
+ * [standard-schema specification](https://github.com/standard-schema/standard-schema).
20
+ *
21
+ * :::tip
22
+ * You typically want to place subjects in its own file so it can be imported by all of your apps.
23
+ * :::
24
+ *
25
+ * You can start with one subject. Later you can add more for different types of users.
26
+ *
27
+ * #### Set the subjects
28
+ *
29
+ * Then you can pass it to the `issuer`.
30
+ *
31
+ * ```ts title="issuer.ts"
32
+ * import { subjects } from "./subjects"
33
+ *
34
+ * const app = issuer({
35
+ * providers: { ... },
36
+ * subjects,
37
+ * // ...
38
+ * })
39
+ * ```
40
+ *
41
+ * #### Add the subject payload
42
+ *
43
+ * When your user completes the flow, you can add the subject payload in the `success` callback.
44
+ *
45
+ * ```ts title="issuer.ts"
46
+ * const app = issuer({
47
+ * providers: { ... },
48
+ * subjects,
49
+ * async success(ctx, value) {
50
+ * let userID
51
+ * if (value.provider === "password") {
52
+ * console.log(value.email)
53
+ * userID = ... // lookup user or create them
54
+ * }
55
+ * return ctx.subject("user", {
56
+ * userID
57
+ * })
58
+ * },
59
+ * // ...
60
+ * })
61
+ * ```
62
+ *
63
+ * Here we are looking up the userID from our database and adding it to the subject payload.
64
+ *
65
+ * :::caution
66
+ * You should only store properties that won't change for the lifetime of the user.
67
+ * :::
68
+ *
69
+ * Since these will be stored in the access token, you should avoid storing information
70
+ * that'll change often. For example, if you store the user's username, you'll need to
71
+ * revoke the access token when the user changes their username.
72
+ *
73
+ * #### Decode the subject
74
+ *
75
+ * Now when your user logs in, you can use the OpenAuth client to decode the subject. For
76
+ * example, in our SSR app we can do the following.
77
+ *
78
+ * ```ts title="app/page.tsx"
79
+ * import { subjects } from "../subjects"
80
+ *
81
+ * const verified = await client.verify(subjects, cookies.get("access_token")!)
82
+ * console.log(verified.subject.properties.userID)
83
+ * ```
84
+ *
85
+ * All this is typesafe based on the shape of the subjects you defined.
86
+ *
87
+ * @packageDocumentation
88
+ */
89
+ import type { v1 } from "@standard-schema/spec"
90
+ import { Prettify } from "./util.js"
91
+
92
+ /**
93
+ * Subject schema is a map of types that are used to define the subjects.
94
+ */
95
+ export type SubjectSchema = Record<string, v1.StandardSchema>
96
+
97
+ /** @internal */
98
+ export type SubjectPayload<T extends SubjectSchema> = Prettify<
99
+ {
100
+ [type in keyof T & string]: {
101
+ type: type
102
+ properties: v1.InferOutput<T[type]>
103
+ }
104
+ }[keyof T & string]
105
+ >
106
+
107
+ /**
108
+ * Create a subject schema.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const subjects = createSubjects({
113
+ * user: object({
114
+ * userID: string()
115
+ * }),
116
+ * admin: object({
117
+ * workspaceID: string()
118
+ * })
119
+ * })
120
+ * ```
121
+ *
122
+ * This is using [valibot](https://github.com/fabian-hiller/valibot) to define the shape of the
123
+ * subjects. You can use any validation library that's following the
124
+ * [standard-schema specification](https://github.com/standard-schema/standard-schema).
125
+ */
126
+ export function createSubjects<Schema extends SubjectSchema = {}>(
127
+ types: Schema,
128
+ ): Schema {
129
+ return { ...types }
130
+ }
@@ -0,0 +1,118 @@
1
+ import { PropsWithChildren } from "hono/jsx"
2
+ import css from "./ui.css" assert { type: "text" }
3
+ import { getTheme } from "./theme.js"
4
+
5
+ export function Layout(
6
+ props: PropsWithChildren<{
7
+ size?: "small"
8
+ }>,
9
+ ) {
10
+ const theme = getTheme()
11
+ function get(key: "primary" | "background" | "logo", mode: "light" | "dark") {
12
+ if (!theme) return
13
+ if (!theme[key]) return
14
+ if (typeof theme[key] === "string") return theme[key]
15
+
16
+ return theme[key][mode] as string | undefined
17
+ }
18
+
19
+ const radius = (() => {
20
+ if (theme?.radius === "none") return "0"
21
+ if (theme?.radius === "sm") return "1"
22
+ if (theme?.radius === "md") return "1.25"
23
+ if (theme?.radius === "lg") return "1.5"
24
+ if (theme?.radius === "full") return "1000000000001"
25
+ return "1"
26
+ })()
27
+
28
+ const hasLogo = get("logo", "light") && get("logo", "dark")
29
+
30
+ return (
31
+ <html
32
+ style={{
33
+ "--color-background-light": get("background", "light"),
34
+ "--color-background-dark": get("background", "dark"),
35
+ "--color-primary-light": get("primary", "light"),
36
+ "--color-primary-dark": get("primary", "dark"),
37
+ "--font-family": theme?.font?.family,
38
+ "--font-scale": theme?.font?.scale,
39
+ "--border-radius": radius,
40
+ }}
41
+ >
42
+ <head>
43
+ <title>{theme?.title || "OpenAuthJS"}</title>
44
+ <meta charset="utf-8" />
45
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
46
+ {theme?.favicon ? (
47
+ <link rel="icon" href={theme?.favicon} />
48
+ ) : (
49
+ <>
50
+ <link
51
+ rel="icon"
52
+ href="https://openauth.js.org/favicon.ico"
53
+ sizes="48x48"
54
+ />
55
+ <link
56
+ rel="icon"
57
+ href="https://openauth.js.org/favicon.svg"
58
+ media="(prefers-color-scheme: light)"
59
+ />
60
+ <link
61
+ rel="icon"
62
+ href="https://openauth.js.org/favicon-dark.svg"
63
+ media="(prefers-color-scheme: dark)"
64
+ />
65
+ <link
66
+ rel="shortcut icon"
67
+ href="https://openauth.js.org/favicon.svg"
68
+ type="image/svg+xml"
69
+ />
70
+ </>
71
+ )}
72
+ <style dangerouslySetInnerHTML={{ __html: css }} />
73
+ {theme?.css && (
74
+ <style dangerouslySetInnerHTML={{ __html: theme.css }} />
75
+ )}
76
+ </head>
77
+ <body>
78
+ <div data-component="root">
79
+ <div data-component="center" data-size={props.size}>
80
+ {hasLogo ? (
81
+ <>
82
+ <img
83
+ data-component="logo"
84
+ src={get("logo", "light")}
85
+ data-mode="light"
86
+ />
87
+ <img
88
+ data-component="logo"
89
+ src={get("logo", "dark")}
90
+ data-mode="dark"
91
+ />
92
+ </>
93
+ ) : (
94
+ ICON_OPENAUTH
95
+ )}
96
+ {props.children}
97
+ </div>
98
+ </div>
99
+ </body>
100
+ </html>
101
+ )
102
+ }
103
+
104
+ const ICON_OPENAUTH = (
105
+ <svg
106
+ data-component="logo-default"
107
+ width="51"
108
+ height="51"
109
+ viewBox="0 0 51 51"
110
+ fill="none"
111
+ xmlns="http://www.w3.org/2000/svg"
112
+ >
113
+ <path
114
+ d="M0 50.2303V0.12854H50.1017V50.2303H0ZM3.08002 11.8326H11.7041V3.20856H3.08002V11.8326ZM14.8526 11.8326H23.4766V3.20856H14.8526V11.8326ZM26.5566 11.8326H35.1807V3.20856H26.5566V11.8326ZM38.3292 11.8326H47.0217V3.20856H38.3292V11.8326ZM3.08002 23.6052H11.7041V14.9811H3.08002V23.6052ZM14.8526 23.6052H23.4766V14.9811H14.8526V23.6052ZM26.5566 23.6052H35.1807V14.9811H26.5566V23.6052ZM38.3292 23.6052H47.0217V14.9811H38.3292V23.6052ZM3.08002 35.3092H11.7041V26.6852H3.08002V35.3092ZM14.8526 35.3092H23.4766V26.6852H14.8526V35.3092ZM26.5566 35.3092H35.1807V26.6852H26.5566V35.3092ZM38.3292 35.3092H47.0217V26.6852H38.3292V35.3092ZM3.08002 47.1502H11.7041V38.3893H3.08002V47.1502ZM14.8526 47.1502H23.4766V38.3893H14.8526V47.1502ZM26.5566 47.1502H35.1807V38.3893H26.5566V47.1502ZM38.3292 47.1502H47.0217V38.3893H38.3292V47.1502Z"
115
+ fill="currentColor"
116
+ />
117
+ </svg>
118
+ )
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Configure the UI that's used by the Code provider.
3
+ *
4
+ * ```ts {1,7-12}
5
+ * import { CodeUI } from "@openauthjs/openauth/ui/code"
6
+ * import { CodeProvider } from "@openauthjs/openauth/provider/code"
7
+ *
8
+ * export default issuer({
9
+ * providers: {
10
+ * code: CodeAdapter(
11
+ * CodeUI({
12
+ * copy: {
13
+ * code_info: "We'll send a pin code to your email"
14
+ * },
15
+ * sendCode: (claims, code) => console.log(claims.email, code)
16
+ * })
17
+ * )
18
+ * },
19
+ * // ...
20
+ * })
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ /** @jsxImportSource hono/jsx */
26
+
27
+ import { CodeProviderOptions, CodeProviderError } from "../provider/code.js"
28
+ import { UnknownStateError } from "../error.js"
29
+ import { Layout } from "./base.js"
30
+ import { FormAlert } from "./form.js"
31
+
32
+ const DEFAULT_COPY = {
33
+ /**
34
+ * Copy for the email input.
35
+ */
36
+ email_placeholder: "Email",
37
+ /**
38
+ * Error message when the email is invalid.
39
+ */
40
+ email_invalid: "Email address is not valid",
41
+ /**
42
+ * Copy for the continue button.
43
+ */
44
+ button_continue: "Continue",
45
+ /**
46
+ * Copy informing that the pin code will be emailed.
47
+ */
48
+ code_info: "We'll send a pin code to your email.",
49
+ /**
50
+ * Copy for the pin code input.
51
+ */
52
+ code_placeholder: "Code",
53
+ /**
54
+ * Error message when the code is invalid.
55
+ */
56
+ code_invalid: "Invalid code",
57
+ /**
58
+ * Copy for when the code was sent.
59
+ */
60
+ code_sent: "Code sent to ",
61
+ /**
62
+ * Copy for when the code was resent.
63
+ */
64
+ code_resent: "Code resent to ",
65
+ /**
66
+ * Copy for the link to resend the code.
67
+ */
68
+ code_didnt_get: "Didn't get code?",
69
+ /**
70
+ * Copy for the resend button.
71
+ */
72
+ code_resend: "Resend",
73
+ }
74
+
75
+ export type CodeUICopy = typeof DEFAULT_COPY
76
+
77
+ /**
78
+ * Configure the password UI.
79
+ */
80
+ export interface CodeUIOptions {
81
+ /**
82
+ * Callback to send the pin code to the user.
83
+ *
84
+ * The `claims` object contains the email or phone number of the user. You can send the code
85
+ * using this.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * async (claims, code) => {
90
+ * // Send the code via the claim
91
+ * }
92
+ * ```
93
+ */
94
+ sendCode: (
95
+ claims: Record<string, string>,
96
+ code: string,
97
+ ) => Promise<void | CodeProviderError>
98
+ /**
99
+ * Custom copy for the UI.
100
+ */
101
+ copy?: Partial<CodeUICopy>
102
+ /**
103
+ * The mode to use for the input.
104
+ * @default "email"
105
+ */
106
+ mode?: "email" | "phone"
107
+ }
108
+
109
+ /**
110
+ * Creates a UI for the Code provider flow.
111
+ * @param props - Configure the UI.
112
+ */
113
+ export function CodeUI(props: CodeUIOptions): CodeProviderOptions {
114
+ const copy = {
115
+ ...DEFAULT_COPY,
116
+ ...props.copy,
117
+ }
118
+
119
+ const mode = props.mode ?? "email"
120
+
121
+ return {
122
+ sendCode: props.sendCode,
123
+ length: 6,
124
+ request: async (_req, state, _form, error): Promise<Response> => {
125
+ if (state.type === "start") {
126
+ const jsx = (
127
+ <Layout>
128
+ <form data-component="form" method="post">
129
+ {error?.type === "invalid_claim" && (
130
+ <FormAlert message={copy.email_invalid} />
131
+ )}
132
+ <input type="hidden" name="action" value="request" />
133
+ <input
134
+ data-component="input"
135
+ autofocus
136
+ type={mode === "email" ? "email" : "tel"}
137
+ name={mode === "email" ? "email" : "phone"}
138
+ inputmode={mode === "email" ? "email" : "numeric"}
139
+ required
140
+ placeholder={copy.email_placeholder}
141
+ />
142
+ <button data-component="button">{copy.button_continue}</button>
143
+ </form>
144
+ <p data-component="form-footer">{copy.code_info}</p>
145
+ </Layout>
146
+ )
147
+ return new Response(jsx.toString(), {
148
+ headers: {
149
+ "Content-Type": "text/html",
150
+ },
151
+ })
152
+ }
153
+
154
+ if (state.type === "code") {
155
+ const jsx = (
156
+ <Layout>
157
+ <form data-component="form" class="form" method="post">
158
+ {error?.type === "invalid_code" && (
159
+ <FormAlert message={copy.code_invalid} />
160
+ )}
161
+ {state.type === "code" && (
162
+ <FormAlert
163
+ message={
164
+ (state.resend ? copy.code_resent : copy.code_sent) +
165
+ state.claims.email
166
+ }
167
+ color="success"
168
+ />
169
+ )}
170
+ <input type="hidden" name="action" value="verify" />
171
+ <input
172
+ data-component="input"
173
+ autofocus
174
+ minLength={6}
175
+ maxLength={6}
176
+ type="text"
177
+ name="code"
178
+ required
179
+ inputmode="numeric"
180
+ autocomplete="one-time-code"
181
+ placeholder={copy.code_placeholder}
182
+ />
183
+ <button data-component="button">{copy.button_continue}</button>
184
+ </form>
185
+ <form method="post">
186
+ {Object.entries(state.claims).map(([key, value]) => (
187
+ <input
188
+ key={key}
189
+ type="hidden"
190
+ name={key}
191
+ value={value}
192
+ className="hidden"
193
+ />
194
+ ))}
195
+ <input type="hidden" name="action" value="resend" />
196
+ <div data-component="form-footer">
197
+ <span>
198
+ {copy.code_didnt_get}{" "}
199
+ <button data-component="link">{copy.code_resend}</button>
200
+ </span>
201
+ </div>
202
+ </form>
203
+ </Layout>
204
+ )
205
+ return new Response(jsx.toString(), {
206
+ headers: {
207
+ "Content-Type": "text/html",
208
+ },
209
+ })
210
+ }
211
+
212
+ throw new UnknownStateError()
213
+ },
214
+ }
215
+ }
@@ -0,0 +1,40 @@
1
+ /** @jsxImportSource hono/jsx */
2
+
3
+ export function FormAlert(props: {
4
+ message?: string
5
+ color?: "danger" | "success"
6
+ }) {
7
+ return (
8
+ <div data-component="form-alert" data-color={props.color}>
9
+ <svg
10
+ data-slot="icon-success"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ fill="none"
13
+ viewBox="0 0 24 24"
14
+ stroke-width="1.5"
15
+ stroke="currentColor"
16
+ >
17
+ <path
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
21
+ />
22
+ </svg>
23
+ <svg
24
+ data-slot="icon-danger"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ fill="none"
27
+ viewBox="0 0 24 24"
28
+ stroke-width="1.5"
29
+ stroke="currentColor"
30
+ >
31
+ <path
32
+ stroke-linecap="round"
33
+ stroke-linejoin="round"
34
+ d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
35
+ />
36
+ </svg>
37
+ <span data-slot="message">{props.message}</span>
38
+ </div>
39
+ )
40
+ }