PageSourceSearch

https://littlebirdelectronics.com.au/assets/controllers/ecommerce/passkey_controller-b982b6c2.js

js littlebirdelectronics.com.au collected 2026-09-25 14:10:56 UTC 5,957 bytes, 177 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["registerButton", "loginButton", "labelInput", "list"]
5  static values = {
6    challengeUrl: String,
7    registerUrl: String,
8    authChallengeUrl: String,
9    authUrl: String
10  }
11
12  connect() {
13    // Hide passkey buttons if WebAuthn is not supported
14    if (!window.PublicKeyCredential) {
15      this.element.style.display = "none"
16    }
17  }
18
19  async register(event) {
20    event.preventDefault()
21    const button = this.registerButtonTarget
22    const originalText = button.textContent
23    button.textContent = "Waiting for device..."
24    button.disabled = true
25
26    try {
27      const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
28
29      // Get challenge from server
30      const challengeResponse = await fetch(this.challengeUrlValue, {
31        method: "POST",
32        headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken, "Accept": "application/json" }
33      })
34      const options = await challengeResponse.json()
35
36      // Convert base64url strings to ArrayBuffers
37      options.challenge = this.base64urlToBuffer(options.challenge)
38      options.user.id = this.base64urlToBuffer(options.user.id)
39      if (options.excludeCredentials) {
40        options.excludeCredentials = options.excludeCredentials.map(cred => ({
41          ...cred,
42          id: this.base64urlToBuffer(cred.id)
43        }))
44      }
45
46      // Create credential via browser API
47      const credential = await navigator.credentials.create({ publicKey: options })
48
49      // Determine label
50      const label = this.hasLabelInputTarget && this.labelInputTarget.value.trim()
51        ? this.labelInputTarget.value.trim()
52        : "Passkey"
53
54      // Send credential to server
55      const body = {
56        id: credential.id,
57        rawId: this.bufferToBase64url(credential.rawId),
58        type: credential.type,
59        label: label,
60        response: {
61          attestationObject: this.bufferToBase64url(credential.response.attestationObject),
62          clientDataJSON: this.bufferToBase64url(credential.response.clientDataJSON)
63        }
64      }
65
66      const result = await fetch(this.registerUrlValue, {
67        method: "POST",
68        headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken, "Accept": "application/json" },
69        body: JSON.stringify(body)
70      })
71
72      const data = await result.json()
73
74      if (data.status === "ok") {
75        // Reload to show updated passkey list
76        window.location.reload()
77      } else {
78        alert(data.message || "Failed to register passkey.")
79      }
80    } catch (err) {
81      if (err.name !== "NotAllowedError") {
82        console.error("Passkey registration error:", err)
83        alert("Passkey registration failed. Please try again.")
84      }
85    } finally {
86      button.textContent = originalText
87      button.disabled = false
88    }
89  }
90
91  async login(event) {
92    event.preventDefault()
93    const button = this.loginButtonTarget
94    const originalText = button.textContent
95    button.textContent = "Waiting for device..."
96    button.disabled = true
97
98    try {
99      const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
100
101      // Get challenge from server
102      const challengeResponse = await fetch(this.authChallengeUrlValue, {
103        method: "POST",
104        headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken, "Accept": "application/json" }
105      })
106      const options = await challengeResponse.json()
107
108      // Convert challenge to ArrayBuffer
109      options.challenge = this.base64urlToBuffer(options.challenge)
110      if (options.allowCredentials) {
111        options.allowCredentials = options.allowCredentials.map(cred => ({
112          ...cred,
113          id: this.base64urlToBuffer(cred.id)
114        }))
115      }
116
117      // Get credential via browser API
118      const credential = await navigator.credentials.get({ publicKey: options })
119
120      // Send assertion to server
121      const body = {
122        id: credential.id,
123        rawId: this.bufferToBase64url(credential.rawId),
124        type: credential.type,
125        response: {
126          authenticatorData: this.bufferToBase64url(credential.response.authenticatorData),
127          clientDataJSON: this.bufferToBase64url(credential.response.clientDataJSON),
128          signature: this.bufferToBase64url(credential.response.signature),
129          userHandle: credential.response.userHandle ? this.bufferToBase64url(credential.response.userHandle) : null
130        }
131      }
132
133      const result = await fetch(this.authUrlValue, {
134        method: "POST",
135        headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken, "Accept": "application/json" },
136        body: JSON.stringify(body)
137      })
138
139      const data = await result.json()
140
141      if (data.status === "ok") {
142        Turbo.visit(data.redirect_to)
143      } else {
144        alert(data.message || "Authentication failed.")
145      }
146    } catch (err) {
147      if (err.name !== "NotAllowedError") {
148        console.error("Passkey login error:", err)
149        alert("Passkey login failed. Please try again.")
150      }
151    } finally {
152      button.textContent = originalText
153      button.disabled = false
154    }
155  }
156
157  // Helpers for base64url <-> ArrayBuffer conversion
158  base64urlToBuffer(base64url) {
159    const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/")
160    const padding = "=".repeat((4 - base64.length % 4) % 4)
161    const binary = atob(base64 + padding)
162    const bytes = new Uint8Array(binary.length)
163    for (let i = 0; i < binary.length; i++) {
164      bytes[i] = binary.charCodeAt(i)
165    }
166    return bytes.buffer
167  }
168
169  bufferToBase64url(buffer) {
170    const bytes = new Uint8Array(buffer)
171    let binary = ""
172    for (const byte of bytes) {
173      binary += String.fromCharCode(byte)
174    }
175    return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
176  }
177}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.