@excom/web-authn 0.1.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 (33) hide show
  1. package/.rush/temp/chunked-rush-logs/web-authn.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/web-authn.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/web-authn.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +4 -0
  14. package/config/rig.json +5 -0
  15. package/index.ts +17 -0
  16. package/package.json +44 -0
  17. package/rush-logs/web-authn.apply-exports.cache.log +1 -0
  18. package/rush-logs/web-authn.apply-exports.log +1 -0
  19. package/rush-logs/web-authn.build_docs.cache.log +1 -0
  20. package/rush-logs/web-authn.build_docs.log +1 -0
  21. package/rush-logs/web-authn.build_package-metas.cache.log +1 -0
  22. package/rush-logs/web-authn.build_package-metas.log +1 -0
  23. package/support/custom-elements.json +156 -0
  24. package/support/demos/authenticate.html +24 -0
  25. package/support/demos/register.html +24 -0
  26. package/support/dist-docs/web-authn.md +205 -0
  27. package/support/docs/README.md +81 -0
  28. package/support/package-meta.json +240 -0
  29. package/support/tests/authenticate.view.test.ts +28 -0
  30. package/support/tests/register.view.test.ts +29 -0
  31. package/support/tests/web-authn.test.ts +566 -0
  32. package/tsconfig.json +5 -0
  33. package/web-authn.ts +219 -0
package/web-authn.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { FetchableElement } from "@excom/fetchable-element";
2
+ import { Neutron, TEvent } from "@excom/neutron";
3
+
4
+ export type FetchArgs = [url: string, requestInit: RequestInit];
5
+
6
+ export type WebAuthnSubmitEvent = TEvent & {
7
+ type: "web-authn-submit";
8
+ detail: FetchArgs;
9
+ };
10
+
11
+ /** Native form `submit` this element intercepts, not a Neutron emit. */
12
+ export type WebAuthnNativeSubmitEvent = SubmitEvent & {
13
+ type: "submit";
14
+ bubbles: true;
15
+ cancelable: true;
16
+ composed: false;
17
+ };
18
+ import {
19
+ startAuthentication,
20
+ startRegistration,
21
+ } from "@simplewebauthn/browser";
22
+
23
+ /**
24
+ * WebAuthn (passkey) registration or authentication, wired to a plain
25
+ * `<form>`. On submit: fetches ceremony options from `options-url`,
26
+ * passes them to the browser's WebAuthn prompt (via
27
+ * `@simplewebauthn/browser`'s `startRegistration` / `startAuthentication`
28
+ * — installed as a dependency of this package, no extra setup needed),
29
+ * then posts the resulting credential to `verify-url`. Composes
30
+ * `FetchableElement`, so `is-loading` / `is-success` / `is-error` / `provision`
31
+ * and every request-building attribute are inherited — see that
32
+ * package's docs for the full list.
33
+ *
34
+ * Chain a next step off `web-authn-success` — e.g. redirect, or trigger
35
+ * a follow-up `<super-form>` — the same way you'd react to any
36
+ * `{tag}-success` event.
37
+ *
38
+ * @summary Passkey register / authenticate wired to a `<form>`.
39
+ *
40
+ * @fires web-authn-submit - Internal — dispatched once the browser
41
+ * ceremony (register or authenticate) resolves, just before the
42
+ * verify-url fetch runs. The credential is the request body. Default
43
+ * action calls `doFetch()`.
44
+ * @type WebAuthnSubmitEvent
45
+ * @listens submit - The default action of the `<form>` matched by
46
+ * `form-ref`; prevented, then starts the ceremony.
47
+ * @type WebAuthnNativeSubmitEvent
48
+ * @command --submit - Starts the ceremony programmatically (`<button
49
+ * command="--submit" commandfor="…">`) — the only option when `form-ref`
50
+ * points to a form that isn't a descendant.
51
+ * @default-action web-authn-submit - Calls `doFetch(url, requestInit)`
52
+ * with the event's detail (the verify-url request).
53
+ *
54
+ * @example
55
+ * <web-authn options-url="/api/webauthn/register/options"
56
+ * verify-url="/api/webauthn/register/verify" start-method="register">
57
+ * <form><input name="username"><button type="submit">Register</button></form>
58
+ * </web-authn>
59
+ */
60
+ export const WebAuthn = Neutron.compose([
61
+ FetchableElement,
62
+ Neutron({
63
+ tag: "web-authn",
64
+ props: {
65
+ // options
66
+ /**
67
+ * @option
68
+ * CSS selector for the `<form>` to intercept. Must be a
69
+ * descendant to be heard directly — point elsewhere and invoke
70
+ * the `--submit` command instead.
71
+ * @default :scope form
72
+ * @values <CSS Selector>
73
+ */
74
+ formRef: {
75
+ type: String,
76
+ // Non-descendant form: this element won't hear `submit`; invoke `--submit`
77
+ defaultValue: () => ":scope form",
78
+ },
79
+ /**
80
+ * @option
81
+ * HTTP method used for both the `options-url` and `verify-url`
82
+ * requests.
83
+ * @default POST
84
+ */
85
+ apiMethod: {
86
+ type: String,
87
+ defaultValue: () => "POST",
88
+ },
89
+ /**
90
+ * @option
91
+ * Endpoint that returns the WebAuthn ceremony options JSON (from
92
+ * your server's `generateRegistrationOptions` /
93
+ * `generateAuthenticationOptions`). Fetched first, before the
94
+ * browser prompt appears.
95
+ * @values <URL>
96
+ */
97
+ optionsUrl: String,
98
+ /**
99
+ * @option
100
+ * Which WebAuthn ceremony to run.
101
+ * @values register | authenticate
102
+ */
103
+ startMethod: String,
104
+ /**
105
+ * @option
106
+ * Endpoint that verifies the credential produced by the browser
107
+ * prompt (your server's `verifyRegistrationResponse` /
108
+ * `verifyAuthenticationResponse`). Receives the credential as the
109
+ * request body.
110
+ * @values <URL>
111
+ */
112
+ verifyUrl: String,
113
+ // private
114
+ optionsPromise: Promise,
115
+ },
116
+ }),
117
+ ])
118
+ .defineMethods({
119
+ doSubmit: ({
120
+ verifyUrl,
121
+ optionsUrl,
122
+ startMethod,
123
+ apiMethod,
124
+ getFetchArgs,
125
+ getFormElement,
126
+ }) => {
127
+ const formElement = getFormElement();
128
+ if (!verifyUrl || !optionsUrl || !startMethod || !formElement) {
129
+ console.error("Missing required attributes for WebAuthn");
130
+ return {
131
+ emit: [
132
+ "error",
133
+ { detail: "Missing required attributes for WebAuthn" },
134
+ ],
135
+ };
136
+ }
137
+
138
+ return {
139
+ isLoading: true,
140
+ optionsPromise: doAuth({
141
+ fetchArgs: getFetchArgs([optionsUrl, { method: apiMethod }]),
142
+ startMethod,
143
+ }),
144
+ };
145
+ },
146
+ })
147
+ .onEvent("submit", (_, e) => {
148
+ e.preventDefault();
149
+ return { doSubmit: [] };
150
+ })
151
+ .onCommand("--submit", () => ({ doSubmit: [] }))
152
+ .onEventDefault("web-authn-submit", (_, { detail }) => ({
153
+ // `detail` is the `[url, requestInit]` tuple (`onPromiseResolved`
154
+ // below). `doFetch` takes that tuple as one arg.
155
+ doFetch: [detail],
156
+ }))
157
+ .onPromiseResolved(
158
+ "optionsPromise",
159
+ ({ verifyUrl, apiMethod, getFetchArgs }, result) => ({
160
+ emit: [
161
+ "web-authn-submit",
162
+ {
163
+ detail: getFetchArgs([
164
+ verifyUrl,
165
+ { method: apiMethod, body: result.optionsPromise },
166
+ ]),
167
+ },
168
+ ],
169
+ })
170
+ )
171
+ .onPromiseRejected("optionsPromise", (_, result) => {
172
+ console.error("WebAuthn error:", result.optionsPromise);
173
+ return {
174
+ emit: [
175
+ "error",
176
+ {
177
+ detail: {
178
+ message:
179
+ result.optionsPromise instanceof Error
180
+ ? result.optionsPromise.message
181
+ : "WebAuthn operation failed",
182
+ },
183
+ },
184
+ ],
185
+ };
186
+ });
187
+
188
+ async function doAuth({
189
+ fetchArgs,
190
+ startMethod,
191
+ }: {
192
+ fetchArgs: [string, RequestInit];
193
+ startMethod: string;
194
+ }) {
195
+ // 1. Call the optionsUrl endpoint to get the options
196
+ const response = await fetch(fetchArgs[0], {
197
+ ...fetchArgs[1],
198
+ body: JSON.stringify(fetchArgs[1].body),
199
+ });
200
+
201
+ if (!response.ok) {
202
+ throw new Error(`Failed to fetch options: ${response.statusText}`);
203
+ }
204
+
205
+ const options = await response.json();
206
+
207
+ let payload;
208
+ if (startMethod === "register") {
209
+ payload = await startRegistration({ optionsJSON: options });
210
+ } else if (startMethod === "authenticate") {
211
+ payload = await startAuthentication({ optionsJSON: options });
212
+ } else {
213
+ throw new Error(
214
+ `Invalid startMethod: ${startMethod}. Must be 'register' or 'authenticate'`
215
+ );
216
+ }
217
+
218
+ return payload;
219
+ }