has_secure_passkey 0.2.7 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d60ec46a0f356a08daa382861505f23961c77d8e5f1de1b80e0d6d6e830b7939
4
- data.tar.gz: ee2bba7266aca050dd3cf56ac3b349c712c1e3718c9f465306e860ca81c1cef8
3
+ metadata.gz: d8701c9e46b1dfce56e62338235a894088e6973b2ae425ba531cd683c3e1d436
4
+ data.tar.gz: 8b5312f7fecd005bdf6cd058a5a5d03f8c884424d0d759ad4e8b444e73a236bf
5
5
  SHA512:
6
- metadata.gz: e9d6e8c611b7db01e03876aed6ed627d7793621f4503c52a312b291e7658e6016c365c294091b41b3c3b583cfa42339a064d9e7d8d27a7bfb7774039c03e14b6
7
- data.tar.gz: 6ec3fbd29b8d09d2803aec6505d286c661d030952011dcc7c06dd826a0b126617b6fabe0de4692f0d30592d74dc3798430fca34de1b19546d8eac2290de32001
6
+ metadata.gz: 47ea5e397d57f8bf7b6646674f6a3fee6cdac26302de8d9f02da0e721675a50110af46c35e50cd2bade07b549fc7282d9e64253967b3bb2c3528af88857d69af
7
+ data.tar.gz: b5bae0850c9e4238798ce232ab0ff0bb746b96c8466c7571e748ca84e9cb045a54d2b1754bc7e3253208dcfd0f25820436815de85553fef9d8fcecc5472ac22e
@@ -1,5 +1,4 @@
1
- import * as WebAuthnJSON from "@github/webauthn-json"
2
- import { post } from "@rails/request"
1
+ import { post } from "@rails/request.js"
3
2
 
4
3
  export default class WebAuthn extends HTMLElement {
5
4
  static observedAttributes = ["action", "callback", "options"];
@@ -16,13 +15,13 @@ export default class WebAuthn extends HTMLElement {
16
15
  }
17
16
 
18
17
  run() {
19
- WebAuthnJSON[this.action](this.options).
18
+ navigator.credentials[this.action]({publicKey: this.publicKey}).
20
19
  then(async (credential) => {
21
20
  this.showProgress()
22
21
 
23
22
  const { response, redirected } = await post(this.callback, {
24
23
  responseKind: "turbo-stream",
25
- body: JSON.stringify(Object.assign(credential, { webauthn_message: this.message }))
24
+ body: JSON.stringify(Object.assign(credential.toJSON(), { webauthn_message: this.message }))
26
25
  })
27
26
 
28
27
  this.hideProgress()
@@ -34,6 +33,7 @@ export default class WebAuthn extends HTMLElement {
34
33
  Turbo.navigator.proposeVisit(new URL(response.url), options)
35
34
  }
36
35
  }).catch((error) => {
36
+ console.error(error)
37
37
  this.onError(error)
38
38
  })
39
39
  }
@@ -77,4 +77,12 @@ export default class WebAuthn extends HTMLElement {
77
77
  get message() {
78
78
  return this.getAttribute('message');
79
79
  }
80
+
81
+ get publicKey() {
82
+ if (this.action === "create") {
83
+ return PublicKeyCredential.parseCreationOptionsFromJSON(this.options);
84
+ } else {
85
+ return PublicKeyCredential.parseRequestOptionsFromJSON(this.options);
86
+ }
87
+ }
80
88
  }
@@ -1,113 +1,7 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true,
7
- configurable: true,
8
- set: (newValue) => all[name] = () => newValue
9
- });
10
- };
11
-
12
- // app/assets/javascripts/@github--webauthn-json.js
13
- var exports__github_webauthn_json = {};
14
- __export(exports__github_webauthn_json, {
15
- supported: () => supported,
16
- schema: () => c,
17
- get: () => get,
18
- create: () => create
19
- });
20
- function base64urlToBuffer(e) {
21
- const r = "==".slice(0, (4 - e.length % 4) % 4);
22
- const t = e.replace(/-/g, "+").replace(/_/g, "/") + r;
23
- const n = atob(t);
24
- const i = new ArrayBuffer(n.length);
25
- const o = new Uint8Array(i);
26
- for (let e2 = 0;e2 < n.length; e2++)
27
- o[e2] = n.charCodeAt(e2);
28
- return i;
29
- }
30
- function bufferToBase64url(e) {
31
- const r = new Uint8Array(e);
32
- let t = "";
33
- for (const e2 of r)
34
- t += String.fromCharCode(e2);
35
- const n = btoa(t);
36
- const i = n.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
37
- return i;
38
- }
39
- var e = "copy";
40
- var r = "convert";
41
- function convert(t, n, i) {
42
- if (n === e)
43
- return i;
44
- if (n === r)
45
- return t(i);
46
- if (n instanceof Array)
47
- return i.map((e2) => convert(t, n[0], e2));
48
- if (n instanceof Object) {
49
- const e2 = {};
50
- for (const [r2, o] of Object.entries(n)) {
51
- if (o.derive) {
52
- const e3 = o.derive(i);
53
- e3 !== undefined && (i[r2] = e3);
54
- }
55
- if (r2 in i)
56
- i[r2] != null ? e2[r2] = convert(t, o.schema, i[r2]) : e2[r2] = null;
57
- else if (o.required)
58
- throw new Error(`Missing key: ${r2}`);
59
- }
60
- return e2;
61
- }
62
- }
63
- function derived(e2, r2) {
64
- return { required: true, schema: e2, derive: r2 };
65
- }
66
- function required(e2) {
67
- return { required: true, schema: e2 };
68
- }
69
- function optional(e2) {
70
- return { required: false, schema: e2 };
71
- }
72
- var t = { type: required(e), id: required(r), transports: optional(e) };
73
- var n = { appid: optional(e), appidExclude: optional(e), credProps: optional(e) };
74
- var i = { appid: optional(e), appidExclude: optional(e), credProps: optional(e) };
75
- var o = { publicKey: required({ rp: required(e), user: required({ id: required(r), name: required(e), displayName: required(e) }), challenge: required(r), pubKeyCredParams: required(e), timeout: optional(e), excludeCredentials: optional([t]), authenticatorSelection: optional(e), attestation: optional(e), extensions: optional(n) }), signal: optional(e) };
76
- var a = { type: required(e), id: required(e), rawId: required(r), authenticatorAttachment: optional(e), response: required({ clientDataJSON: required(r), attestationObject: required(r), transports: derived(e, (e2) => {
77
- var r2;
78
- return ((r2 = e2.getTransports) == null ? undefined : r2.call(e2)) || [];
79
- }) }), clientExtensionResults: derived(i, (e2) => e2.getClientExtensionResults()) };
80
- var u = { mediation: optional(e), publicKey: required({ challenge: required(r), timeout: optional(e), rpId: optional(e), allowCredentials: optional([t]), userVerification: optional(e), extensions: optional(n) }), signal: optional(e) };
81
- var s = { type: required(e), id: required(e), rawId: required(r), authenticatorAttachment: optional(e), response: required({ clientDataJSON: required(r), authenticatorData: required(r), signature: required(r), userHandle: required(r) }), clientExtensionResults: derived(i, (e2) => e2.getClientExtensionResults()) };
82
- var c = { credentialCreationOptions: o, publicKeyCredentialWithAttestation: a, credentialRequestOptions: u, publicKeyCredentialWithAssertion: s };
83
- function createRequestFromJSON(e2) {
84
- return convert(base64urlToBuffer, o, e2);
85
- }
86
- function createResponseToJSON(e2) {
87
- return convert(bufferToBase64url, a, e2);
88
- }
89
- async function create(e2) {
90
- const r2 = await navigator.credentials.create(createRequestFromJSON(e2));
91
- return createResponseToJSON(r2);
92
- }
93
- function getRequestFromJSON(e2) {
94
- return convert(base64urlToBuffer, u, e2);
95
- }
96
- function getResponseToJSON(e2) {
97
- return convert(bufferToBase64url, s, e2);
98
- }
99
- async function get(e2) {
100
- const r2 = await navigator.credentials.get(getRequestFromJSON(e2));
101
- return getResponseToJSON(r2);
102
- }
103
- function supported() {
104
- return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
105
- }
106
-
107
- // app/assets/javascripts/@rails--request.js
1
+ // node_modules/@rails/request.js/src/fetch_response.js
108
2
  class FetchResponse {
109
- constructor(t2) {
110
- this.response = t2;
3
+ constructor(response) {
4
+ this.response = response;
111
5
  }
112
6
  get statusCode() {
113
7
  return this.response.status;
@@ -128,17 +22,23 @@ class FetchResponse {
128
22
  return this.response.headers.get("WWW-Authenticate");
129
23
  }
130
24
  get contentType() {
131
- const t2 = this.response.headers.get("Content-Type") || "";
132
- return t2.replace(/;.*$/, "");
25
+ const contentType = this.response.headers.get("Content-Type") || "";
26
+ return contentType.replace(/;.*$/, "");
133
27
  }
134
28
  get headers() {
135
29
  return this.response.headers;
136
30
  }
137
31
  get html() {
138
- return this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/) ? this.text : Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`));
32
+ if (this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)) {
33
+ return this.text;
34
+ }
35
+ return Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`));
139
36
  }
140
37
  get json() {
141
- return this.contentType.match(/^application\/.*json$/) ? this.responseJson || (this.responseJson = this.response.json()) : Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`));
38
+ if (this.contentType.match(/^application\/.*json$/)) {
39
+ return this.responseJson || (this.responseJson = this.response.json());
40
+ }
41
+ return Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`));
142
42
  }
143
43
  get text() {
144
44
  return this.responseText || (this.responseText = this.response.text());
@@ -150,27 +50,38 @@ class FetchResponse {
150
50
  return this.contentType.match(/\b(?:java|ecma)script\b/);
151
51
  }
152
52
  async renderTurboStream() {
153
- if (!this.isTurboStream)
53
+ if (this.isTurboStream) {
54
+ if (window.Turbo) {
55
+ await window.Turbo.renderStreamMessage(await this.text);
56
+ } else {
57
+ console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
58
+ }
59
+ } else {
154
60
  return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
155
- window.Turbo ? await window.Turbo.renderStreamMessage(await this.text) : console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
61
+ }
156
62
  }
157
63
  async activeScript() {
158
- if (!this.isScript)
64
+ if (this.isScript) {
65
+ const script = document.createElement("script");
66
+ const metaTag = document.querySelector("meta[name=csp-nonce]");
67
+ if (metaTag) {
68
+ const nonce = metaTag.nonce === "" ? metaTag.content : metaTag.nonce;
69
+ if (nonce) {
70
+ script.setAttribute("nonce", nonce);
71
+ }
72
+ }
73
+ script.innerHTML = await this.text;
74
+ document.body.appendChild(script);
75
+ } else {
159
76
  return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`));
160
- {
161
- const t2 = document.createElement("script");
162
- const e2 = document.querySelector("meta[name=csp-nonce]");
163
- const n2 = e2 && e2.content;
164
- n2 && t2.setAttribute("nonce", n2);
165
- t2.innerHTML = await this.text;
166
- document.body.appendChild(t2);
167
77
  }
168
78
  }
169
79
  }
170
80
 
81
+ // node_modules/@rails/request.js/src/request_interceptor.js
171
82
  class RequestInterceptor {
172
- static register(t2) {
173
- this.interceptor = t2;
83
+ static register(interceptor) {
84
+ this.interceptor = interceptor;
174
85
  }
175
86
  static get() {
176
87
  return this.interceptor;
@@ -179,90 +90,130 @@ class RequestInterceptor {
179
90
  this.interceptor = undefined;
180
91
  }
181
92
  }
182
- function getCookie(t2) {
183
- const e2 = document.cookie ? document.cookie.split("; ") : [];
184
- const n2 = `${encodeURIComponent(t2)}=`;
185
- const s2 = e2.find((t3) => t3.startsWith(n2));
186
- if (s2) {
187
- const t3 = s2.split("=").slice(1).join("=");
188
- if (t3)
189
- return decodeURIComponent(t3);
93
+
94
+ // node_modules/@rails/request.js/src/lib/utils.js
95
+ function getCookie(name) {
96
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
97
+ const prefix = `${encodeURIComponent(name)}=`;
98
+ const cookie = cookies.find((cookie2) => cookie2.startsWith(prefix));
99
+ if (cookie) {
100
+ const value = cookie.split("=").slice(1).join("=");
101
+ if (value) {
102
+ return decodeURIComponent(value);
103
+ }
190
104
  }
191
105
  }
192
- function compact(t2) {
193
- const e2 = {};
194
- for (const n2 in t2) {
195
- const s2 = t2[n2];
196
- s2 !== undefined && (e2[n2] = s2);
106
+ function compact(object) {
107
+ const result = {};
108
+ for (const key in object) {
109
+ const value = object[key];
110
+ if (value !== undefined) {
111
+ result[key] = value;
112
+ }
197
113
  }
198
- return e2;
114
+ return result;
199
115
  }
200
- function metaContent(t2) {
201
- const e2 = document.head.querySelector(`meta[name="${t2}"]`);
202
- return e2 && e2.content;
116
+ function metaContent(name) {
117
+ const element = document.head.querySelector(`meta[name="${name}"]`);
118
+ return element && element.content;
203
119
  }
204
- function stringEntriesFromFormData(t2) {
205
- return [...t2].reduce((t3, [e2, n2]) => t3.concat(typeof n2 === "string" ? [[e2, n2]] : []), []);
120
+ function stringEntriesFromFormData(formData) {
121
+ return [...formData].reduce((entries, [name, value]) => {
122
+ return entries.concat(typeof value === "string" ? [[name, value]] : []);
123
+ }, []);
206
124
  }
207
- function mergeEntries(t2, e2) {
208
- for (const [n2, s2] of e2)
209
- if (!(s2 instanceof window.File))
210
- if (t2.has(n2) && !n2.includes("[]")) {
211
- t2.delete(n2);
212
- t2.set(n2, s2);
213
- } else
214
- t2.append(n2, s2);
125
+ function mergeEntries(searchParams, entries) {
126
+ for (const [name, value] of entries) {
127
+ if (value instanceof window.File)
128
+ continue;
129
+ if (searchParams.has(name) && !name.includes("[]")) {
130
+ searchParams.delete(name);
131
+ searchParams.set(name, value);
132
+ } else {
133
+ searchParams.append(name, value);
134
+ }
135
+ }
215
136
  }
216
137
 
138
+ // node_modules/@rails/request.js/src/fetch_request.js
217
139
  class FetchRequest {
218
- constructor(t2, e2, n2 = {}) {
219
- this.method = t2;
220
- this.options = n2;
221
- this.originalUrl = e2.toString();
140
+ constructor(method, url, options = {}) {
141
+ this.method = method;
142
+ this.options = options;
143
+ this.originalUrl = url.toString();
222
144
  }
223
145
  async perform() {
224
146
  try {
225
- const t3 = RequestInterceptor.get();
226
- t3 && await t3(this);
227
- } catch (t3) {
228
- console.error(t3);
147
+ const requestInterceptor = RequestInterceptor.get();
148
+ if (requestInterceptor) {
149
+ await requestInterceptor(this);
150
+ }
151
+ } catch (error) {
152
+ console.error(error);
153
+ }
154
+ const fetch = this.responseKind === "turbo-stream" && window.Turbo ? window.Turbo.fetch : window.fetch;
155
+ const response = new FetchResponse(await fetch(this.url, this.fetchOptions));
156
+ if (response.unauthenticated && response.authenticationURL) {
157
+ return Promise.reject(window.location.href = response.authenticationURL);
158
+ }
159
+ if (response.isScript) {
160
+ await response.activeScript();
161
+ }
162
+ const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
163
+ if (responseStatusIsTurboStreamable && response.isTurboStream) {
164
+ await response.renderTurboStream();
229
165
  }
230
- const t2 = this.responseKind === "turbo-stream" && window.Turbo ? window.Turbo.fetch : window.fetch;
231
- const e2 = new FetchResponse(await t2(this.url, this.fetchOptions));
232
- if (e2.unauthenticated && e2.authenticationURL)
233
- return Promise.reject(window.location.href = e2.authenticationURL);
234
- e2.isScript && await e2.activeScript();
235
- const n2 = e2.ok || e2.unprocessableEntity;
236
- n2 && e2.isTurboStream && await e2.renderTurboStream();
237
- return e2;
238
- }
239
- addHeader(t2, e2) {
240
- const n2 = this.additionalHeaders;
241
- n2[t2] = e2;
242
- this.options.headers = n2;
166
+ return response;
167
+ }
168
+ addHeader(key, value) {
169
+ const headers = this.additionalHeaders;
170
+ headers[key] = value;
171
+ this.options.headers = headers;
243
172
  }
244
173
  sameHostname() {
245
- if (!this.originalUrl.startsWith("http:"))
174
+ if (!this.originalUrl.startsWith("http:") && !this.originalUrl.startsWith("https:")) {
246
175
  return true;
176
+ }
247
177
  try {
248
178
  return new URL(this.originalUrl).hostname === window.location.hostname;
249
- } catch (t2) {
179
+ } catch (_) {
250
180
  return true;
251
181
  }
252
182
  }
253
183
  get fetchOptions() {
254
- return { method: this.method.toUpperCase(), headers: this.headers, body: this.formattedBody, signal: this.signal, credentials: this.credentials, redirect: this.redirect };
184
+ return {
185
+ method: this.method.toUpperCase(),
186
+ headers: this.headers,
187
+ body: this.formattedBody,
188
+ signal: this.signal,
189
+ credentials: this.credentials,
190
+ redirect: this.redirect,
191
+ keepalive: this.keepalive
192
+ };
255
193
  }
256
194
  get headers() {
257
- const t2 = { "X-Requested-With": "XMLHttpRequest", "Content-Type": this.contentType, Accept: this.accept };
258
- this.sameHostname() && (t2["X-CSRF-Token"] = this.csrfToken);
259
- return compact(Object.assign(t2, this.additionalHeaders));
195
+ const baseHeaders = {
196
+ "X-Requested-With": "XMLHttpRequest",
197
+ "Content-Type": this.contentType,
198
+ Accept: this.accept
199
+ };
200
+ if (this.sameHostname()) {
201
+ baseHeaders["X-CSRF-Token"] = this.csrfToken;
202
+ }
203
+ return compact(Object.assign(baseHeaders, this.additionalHeaders));
260
204
  }
261
205
  get csrfToken() {
262
206
  return getCookie(metaContent("csrf-param")) || metaContent("csrf-token");
263
207
  }
264
208
  get contentType() {
265
- return this.options.contentType ? this.options.contentType : this.body == null || this.body instanceof window.FormData ? undefined : this.body instanceof window.File ? this.body.type : "application/json";
209
+ if (this.options.contentType) {
210
+ return this.options.contentType;
211
+ } else if (this.body == null || this.body instanceof window.FormData) {
212
+ return;
213
+ } else if (this.body instanceof window.File) {
214
+ return this.body.type;
215
+ }
216
+ return "application/json";
266
217
  }
267
218
  get accept() {
268
219
  switch (this.responseKind) {
@@ -282,13 +233,19 @@ class FetchRequest {
282
233
  return this.options.body;
283
234
  }
284
235
  get query() {
285
- const t2 = (this.originalUrl.split("?")[1] || "").split("#")[0];
286
- const e2 = new URLSearchParams(t2);
287
- let n2 = this.options.query;
288
- n2 = n2 instanceof window.FormData ? stringEntriesFromFormData(n2) : n2 instanceof window.URLSearchParams ? n2.entries() : Object.entries(n2 || {});
289
- mergeEntries(e2, n2);
290
- const s2 = e2.toString();
291
- return s2.length > 0 ? `?${s2}` : "";
236
+ const originalQuery = (this.originalUrl.split("?")[1] || "").split("#")[0];
237
+ const params = new URLSearchParams(originalQuery);
238
+ let requestQuery = this.options.query;
239
+ if (requestQuery instanceof window.FormData) {
240
+ requestQuery = stringEntriesFromFormData(requestQuery);
241
+ } else if (requestQuery instanceof window.URLSearchParams) {
242
+ requestQuery = requestQuery.entries();
243
+ } else {
244
+ requestQuery = Object.entries(requestQuery || {});
245
+ }
246
+ mergeEntries(params, requestQuery);
247
+ const query = params.toString();
248
+ return query.length > 0 ? `?${query}` : "";
292
249
  }
293
250
  get url() {
294
251
  return this.originalUrl.split("?")[0].split("#")[0] + this.query;
@@ -305,18 +262,26 @@ class FetchRequest {
305
262
  get credentials() {
306
263
  return this.options.credentials || "same-origin";
307
264
  }
265
+ get keepalive() {
266
+ return this.options.keepalive || false;
267
+ }
308
268
  get additionalHeaders() {
309
269
  return this.options.headers || {};
310
270
  }
311
271
  get formattedBody() {
312
- const t2 = Object.prototype.toString.call(this.body) === "[object String]";
313
- const e2 = this.headers["Content-Type"] === "application/json";
314
- return e2 && !t2 ? JSON.stringify(this.body) : this.body;
272
+ const bodyIsAString = Object.prototype.toString.call(this.body) === "[object String]";
273
+ const contentTypeIsJson = this.headers["Content-Type"] === "application/json";
274
+ if (contentTypeIsJson && !bodyIsAString) {
275
+ return JSON.stringify(this.body);
276
+ }
277
+ return this.body;
315
278
  }
316
279
  }
317
- async function post(t2, e2) {
318
- const n2 = new FetchRequest("post", t2, e2);
319
- return n2.perform();
280
+
281
+ // node_modules/@rails/request.js/src/verbs.js
282
+ async function post(url, options) {
283
+ const request = new FetchRequest("post", url, options);
284
+ return request.perform();
320
285
  }
321
286
 
322
287
  // app/assets/javascripts/components/web_authn.js
@@ -332,11 +297,11 @@ class WebAuthn extends HTMLElement {
332
297
  this.run();
333
298
  }
334
299
  run() {
335
- exports__github_webauthn_json[this.action](this.options).then(async (credential) => {
300
+ navigator.credentials[this.action]({ publicKey: this.publicKey }).then(async (credential) => {
336
301
  this.showProgress();
337
302
  const { response, redirected } = await post(this.callback, {
338
303
  responseKind: "turbo-stream",
339
- body: JSON.stringify(Object.assign(credential, { webauthn_message: this.message }))
304
+ body: JSON.stringify(Object.assign(credential.toJSON(), { webauthn_message: this.message }))
340
305
  });
341
306
  this.hideProgress();
342
307
  if (response.ok && redirected) {
@@ -349,6 +314,7 @@ class WebAuthn extends HTMLElement {
349
314
  Turbo.navigator.proposeVisit(new URL(response.url), options);
350
315
  }
351
316
  }).catch((error) => {
317
+ console.error(error);
352
318
  this.onError(error);
353
319
  });
354
320
  }
@@ -383,6 +349,13 @@ class WebAuthn extends HTMLElement {
383
349
  get message() {
384
350
  return this.getAttribute("message");
385
351
  }
352
+ get publicKey() {
353
+ if (this.action === "create") {
354
+ return PublicKeyCredential.parseCreationOptionsFromJSON(this.options);
355
+ } else {
356
+ return PublicKeyCredential.parseRequestOptionsFromJSON(this.options);
357
+ }
358
+ }
386
359
  }
387
360
 
388
361
  // app/assets/javascripts/components/has_secure_passkey.js
@@ -22,6 +22,10 @@ module HasSecurePasskey::ActiveRecordHelpers
22
22
  authenticated
23
23
  end
24
24
 
25
+ define_singleton_method :recover_passkey do |params:|
26
+ HasSecurePasskey::Recovery.new(model: self, params:).run
27
+ end
28
+
25
29
  define_singleton_method :create_by_webauthn do |params:|
26
30
  authenticatable = new(HasSecurePasskey::OptionsForCreate.
27
31
  from_message(params[:webauthn_message]).authenticatable)
@@ -41,6 +45,14 @@ module HasSecurePasskey::ActiveRecordHelpers
41
45
  save
42
46
  end
43
47
 
48
+ define_method :reset_webauthn_id do
49
+ self.webauthn_id = self.class.webauthn_id
50
+ end
51
+
52
+ define_method :passkey_recovery_token do
53
+ to_sgid(expires_in: 1.hour, for: :recovery).to_s
54
+ end
55
+
44
56
  define_method :encode_webauthn_message do
45
57
  HasSecurePasskey::OptionsForCreate.new(authenticatable: self).message
46
58
  end
@@ -28,7 +28,7 @@ class HasSecurePasskey::OptionsForCreate
28
28
  end
29
29
 
30
30
  def as_json
31
- options
31
+ credential.as_json
32
32
  end
33
33
 
34
34
  def challenge
@@ -22,7 +22,7 @@ class HasSecurePasskey::OptionsForGet
22
22
  end
23
23
 
24
24
  def as_json
25
- { publicKey: credential.as_json }
25
+ credential.as_json
26
26
  end
27
27
 
28
28
  private
@@ -0,0 +1,35 @@
1
+ class HasSecurePasskey::Recovery
2
+ def initialize(model:, params:)
3
+ @model = model
4
+ @params = params
5
+ end
6
+
7
+ def run
8
+ old_passkeys = authenticatable.passkeys.to_a
9
+
10
+ ActiveRecord::Base.transaction do
11
+ (authenticatable.update(webauthn_id:) &&
12
+ authenticatable.add_passkey(params:) &&
13
+ old_passkeys.all?(&:destroy) && authenticatable) ||
14
+ raise(ActiveRecord::Rollback)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :params, :model
21
+
22
+ def authenticatable
23
+ model.find(options.authenticatable[:id])
24
+ end
25
+
26
+ def webauthn_id
27
+ options.authenticatable[:webauthn_id]
28
+ end
29
+
30
+ def options
31
+ HasSecurePasskey::OptionsForCreate.from_message(params[:webauthn_message])
32
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
33
+ ""
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module HasSecurePasskey
2
- VERSION = "0.2.7"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -9,4 +9,9 @@ require "has_secure_passkey/authenticate_by"
9
9
  require "has_secure_passkey/add_passkey"
10
10
 
11
11
  module HasSecurePasskey
12
+ def self.find_recovery_token(token)
13
+ GlobalID::Locator.
14
+ locate_signed(token, for: :recovery).
15
+ tap { it&.reset_webauthn_id } || raise(ActiveRecord::RecordNotFound)
16
+ end
12
17
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_secure_passkey
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Pezza
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -84,6 +84,7 @@ files:
84
84
  - lib/has_secure_passkey/engine.rb
85
85
  - lib/has_secure_passkey/options_for_create.rb
86
86
  - lib/has_secure_passkey/options_for_get.rb
87
+ - lib/has_secure_passkey/recovery.rb
87
88
  - lib/has_secure_passkey/version.rb
88
89
  - lib/tasks/has_secure_passkey_tasks.rake
89
90
  homepage: https://github.com/npezza93/has_secure_passkey
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  - !ruby/object:Gem::Version
106
107
  version: '0'
107
108
  requirements: []
108
- rubygems_version: 3.6.2
109
+ rubygems_version: 3.7.1
109
110
  specification_version: 4
110
111
  summary: Add passkey support to Rails
111
112
  test_files: []