Merge pull request 'feat(machine): on-machine QR-pairing wizard' (#68) from qr-pairing-wizard into dev
Reviewed-on: #68
This commit is contained in:
commit
a957433eca
17 changed files with 650 additions and 277 deletions
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -84,12 +84,32 @@ Renderer reads (Electron IPC or Vite `import.meta.env`):
|
|||
|---|---|---|
|
||||
| `VITE_RELAY_URL` | yes | `ws://...` of the relay both ATM and LNbits subscribe to. Dev: `ws://localhost:5001/nostrrelay/test` (LNbits's bundled `nostrrelay` extension — no separate strfry container) |
|
||||
| `VITE_LNBITS_SERVER_PUBKEY` | yes | 64-char hex pubkey LNbits prints on startup (`docker logs lnbits \| grep 'Public key (share this)'`) |
|
||||
| `VITE_SPIRE_SEED` | yes (prod) | Spire pairing seed (`spire-seed:v1:<base64url>`) from spirekeeper. Carries a one-shot NIP-46 connect token + the spire signing pubkey + bunker URL. First boot redeems it and persists the binding to `state.db`; later boots resume by fingerprint. A changed seed re-pairs. See aiolabs/bitspire#52. |
|
||||
| `VITE_SPIRE_SEED` | yes (prod) | Spire pairing seed (`spire-seed:v1:<base64url>`) from spirekeeper. Carries a one-shot NIP-46 connect token + the spire signing pubkey + bunker URL. First boot redeems it and persists the binding to `state.db`; later boots resume by fingerprint. A changed seed re-pairs. Provisioning it up front is optional — an unpaired machine renders an on-screen QR-pairing wizard that scans the seed off the camera (see below). See aiolabs/bitspire#52. |
|
||||
| `VITE_ATM_PRIVATE_KEY` | dev only | 64-char hex raw nsec fallback for running without a bunker. Ignored when `VITE_SPIRE_SEED` or a stored binding exists. |
|
||||
| `VITE_OPERATOR_PUBKEYS` | optional | Comma-separated hex pubkeys allowed to send kind-21003 management commands |
|
||||
|
||||
The LP-era vars (`VITE_LIGHTNING_PUB_PUBKEY`, `VITE_LIGHTNING_PUB_API_URL`, `VITE_EXTENSION_API_URL`, `VITE_ADMIN_TOKEN`) are gone from the dev branch's `.env.example` and `LightningConfig` interface.
|
||||
|
||||
## Pairing (on-machine QR wizard)
|
||||
|
||||
A machine with no seed **and** no stored binding boots `unpaired` and, under
|
||||
Electron, renders an interactive wizard (`src/components/PairingWizard.vue`)
|
||||
instead of a dead-end fault screen. The operator displays the `spire-seed`
|
||||
QR (minted by spirekeeper's `/pair`) to the machine's camera; the wizard:
|
||||
|
||||
1. captures + decodes via a `PairingSource` (`src/services/pairing/`) — camera
|
||||
today (decode through `qr`, paulmillr's zero-dep lib), NFC scaffolded;
|
||||
2. validates the scan parses as a spire-seed (`ingestScannedSeed`), rejecting
|
||||
a stray QR;
|
||||
3. persists it as `VITE_SPIRE_SEED` via the `state:save-spire-seed` IPC and
|
||||
relaunches (`app:relaunch`).
|
||||
|
||||
Pairing itself is **not** done in the wizard — relaunch lets the normal boot
|
||||
path (`signer-resolver` → `connectNewSeed`) redeem the one-shot token, so
|
||||
there's one tested pairing path. A revoked/expired binding lands on the same
|
||||
wizard (re-pair = scan a fresh seed). Provisioning `VITE_SPIRE_SEED` up front
|
||||
still works and skips the wizard.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -353,6 +353,50 @@ ipcMain.handle('state:reset-bootstrap-gate', (): void => {
|
|||
resetBootstrapGate()
|
||||
})
|
||||
|
||||
// QR-pairing wizard (aiolabs/bitspire#52): an unpaired machine scans a
|
||||
// spire-seed off its camera, and we persist it as VITE_SPIRE_SEED in the
|
||||
// runtime .env so the next boot's signer-resolver redeems it (connectNewSeed)
|
||||
// exactly as if it had been provisioned. We deliberately do NOT pair here —
|
||||
// persisting + relaunching reuses the single, tested pairing path rather than
|
||||
// duplicating it in the renderer.
|
||||
function runtimeEnvPath(): string {
|
||||
const base = fs.existsSync('/var/lib/bitspire') ? '/var/lib/bitspire' : process.cwd()
|
||||
return path.join(base, '.env')
|
||||
}
|
||||
|
||||
ipcMain.handle('state:save-spire-seed', (_event, seed: string): void => {
|
||||
const trimmed = (seed || '').trim()
|
||||
if (!trimmed) throw new Error('save-spire-seed: empty seed')
|
||||
const envPath = runtimeEnvPath()
|
||||
const line = `VITE_SPIRE_SEED=${trimmed}`
|
||||
let lines: string[] = []
|
||||
if (fs.existsSync(envPath)) {
|
||||
lines = fs.readFileSync(envPath, 'utf8').split('\n')
|
||||
}
|
||||
const idx = lines.findIndex((l) => l.startsWith('VITE_SPIRE_SEED='))
|
||||
if (idx >= 0) {
|
||||
lines[idx] = line
|
||||
} else {
|
||||
// Drop a trailing empty element so we don't accumulate blank lines.
|
||||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||||
lines.push(line)
|
||||
}
|
||||
fs.writeFileSync(envPath, lines.join('\n') + '\n', { mode: 0o600 })
|
||||
// Keep this process's view in sync so get-atm-secrets reflects the new seed
|
||||
// even before relaunch (belt-and-suspenders; relaunch re-reads from disk).
|
||||
process.env.VITE_SPIRE_SEED = trimmed
|
||||
console.log('[Pairing] Spire seed persisted to', envPath)
|
||||
})
|
||||
|
||||
// Relaunch the kiosk so the new seed is picked up by a clean boot. Under
|
||||
// systemd (bitspire.service) the exit triggers an automatic restart; in dev
|
||||
// Electron's relaunch re-spawns the process.
|
||||
ipcMain.handle('app:relaunch', (): void => {
|
||||
console.log('[Pairing] Relaunching to apply new pairing')
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
})
|
||||
|
||||
// State persistence IPC handlers
|
||||
ipcMain.handle('state:load-cassettes', () => loadCassettes())
|
||||
ipcMain.handle('state:set-cassettes', (_event, cassettes) => setCassettes(cassettes))
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
clearBunkerBinding: (): Promise<void> => ipcRenderer.invoke('state:clear-bunker-binding'),
|
||||
resetBootstrapGate: (): Promise<void> => ipcRenderer.invoke('state:reset-bootstrap-gate'),
|
||||
|
||||
// QR-pairing wizard (aiolabs/bitspire#52): persist a scanned spire-seed,
|
||||
// then relaunch so the normal boot flow pairs it.
|
||||
saveSpireSeed: (seed: string): Promise<void> => ipcRenderer.invoke('state:save-spire-seed', seed),
|
||||
relaunchApp: (): Promise<void> => ipcRenderer.invoke('app:relaunch'),
|
||||
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: {
|
||||
positions: Record<string, { denomination: number; count: number }>
|
||||
|
|
@ -234,6 +239,8 @@ declare global {
|
|||
saveBunkerBinding: (binding: BunkerBindingRecord) => Promise<void>
|
||||
clearBunkerBinding: () => Promise<void>
|
||||
resetBootstrapGate: () => Promise<void>
|
||||
saveSpireSeed: (seed: string) => Promise<void>
|
||||
relaunchApp: () => Promise<void>
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: { positions: Record<string, { denomination: number; count: number }> },
|
||||
eventCreatedAt: number
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"marked": "^17.0.5",
|
||||
"nostr-tools": "^2.10.0",
|
||||
"pinia": "^2.2.0",
|
||||
"qr": "^0.6.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"reka-ui": "^2.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { classifyInitError } from '@/services/init-error'
|
|||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sun, Moon } from 'lucide-vue-next'
|
||||
import PairingWizard from '@/components/PairingWizard.vue'
|
||||
|
||||
const atmStore = useAtmStore()
|
||||
const route = useRoute()
|
||||
|
|
@ -61,6 +62,14 @@ const isKnownMaintenanceScreen = computed(
|
|||
() => !!atmStore.initError && atmStore.initError in MAINTENANCE_SCREENS
|
||||
)
|
||||
|
||||
/**
|
||||
* `unpaired` is interactive, not a dead-end: render the QR-pairing wizard so
|
||||
* the operator can scan a spire-seed on-machine (aiolabs/bitspire#52). The
|
||||
* wizard only works under Electron (needs the seed-persist + relaunch bridge);
|
||||
* in browser dev it falls back to the static card.
|
||||
*/
|
||||
const showPairingWizard = computed(() => atmStore.initError === 'unpaired' && isElectron)
|
||||
|
||||
const formattedBtcPrice = computed(() => {
|
||||
if (atmStore.btcPrice === null) return null
|
||||
const local = `${atmStore.fiatCode}/BTC: ${atmStore.fiatSymbol}${Math.round(atmStore.btcPrice).toLocaleString()}`
|
||||
|
|
@ -160,9 +169,12 @@ function toggleLiveServices() {
|
|||
<div
|
||||
class="flex min-h-dvh lg:h-dvh w-screen flex-col overflow-y-auto lg:overflow-hidden bg-background font-sans text-foreground"
|
||||
>
|
||||
<!-- Unpaired: interactive QR-pairing wizard (aiolabs/bitspire#52) -->
|
||||
<PairingWizard v-if="showPairingWizard" />
|
||||
|
||||
<!-- Maintenance screen: shown when initialization fails in production -->
|
||||
<div
|
||||
v-if="atmStore.initError"
|
||||
v-else-if="atmStore.initError"
|
||||
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
|
||||
>
|
||||
<svg
|
||||
|
|
|
|||
160
apps/machine/src/components/PairingWizard.vue
Normal file
160
apps/machine/src/components/PairingWizard.vue
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* QR-pairing wizard (aiolabs/bitspire#52).
|
||||
*
|
||||
* Shown in place of the "Pairing Required" maintenance screen when the machine
|
||||
* is unpaired. The operator displays the spire-seed QR (minted by spirekeeper)
|
||||
* to the machine's camera; we decode it, persist it as VITE_SPIRE_SEED, and
|
||||
* relaunch so the normal boot path performs the bunker pairing.
|
||||
*
|
||||
* Capture is abstracted behind PairingSource, so NFC (or a HAL scanner) can be
|
||||
* offered later without changing this view.
|
||||
*/
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import {
|
||||
availablePairingSources,
|
||||
ingestScannedSeed,
|
||||
type PairingSource,
|
||||
type StopCapture,
|
||||
} from '@/services/pairing'
|
||||
|
||||
type Phase = 'probing' | 'scanning' | 'no-source' | 'pairing' | 'error'
|
||||
|
||||
const phase = ref<Phase>('probing')
|
||||
const errorMessage = ref('')
|
||||
const videoEl = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
const sources = shallowRef<PairingSource[]>([])
|
||||
const activeSource = shallowRef<PairingSource | null>(null)
|
||||
let stopCapture: StopCapture | null = null
|
||||
|
||||
async function startWith(source: PairingSource) {
|
||||
await teardown()
|
||||
activeSource.value = source
|
||||
errorMessage.value = ''
|
||||
phase.value = 'scanning'
|
||||
try {
|
||||
stopCapture = await source.start({
|
||||
video: source.kind === 'qr' ? (videoEl.value ?? undefined) : undefined,
|
||||
onScan: handleScan,
|
||||
onError: (e) => console.warn('[Pairing] capture glitch:', e),
|
||||
})
|
||||
} catch (e) {
|
||||
phase.value = 'error'
|
||||
errorMessage.value =
|
||||
e instanceof Error ? e.message : 'Could not start the camera. Check permissions.'
|
||||
}
|
||||
}
|
||||
|
||||
let handling = false
|
||||
async function handleScan(raw: string) {
|
||||
if (handling) return
|
||||
handling = true
|
||||
const result = await ingestScannedSeed(raw)
|
||||
if (result.ok) {
|
||||
// saveSpireSeed succeeded; relaunch is in flight — hold a friendly screen.
|
||||
phase.value = 'pairing'
|
||||
return
|
||||
}
|
||||
// Reject non-seed scans (a stray QR) and resume scanning.
|
||||
console.warn('[Pairing] rejected scan:', result.reason, result.message)
|
||||
errorMessage.value =
|
||||
result.reason === 'invalid-seed'
|
||||
? 'That code is not a pairing code. Show the operator pairing QR.'
|
||||
: result.message
|
||||
handling = false
|
||||
if (activeSource.value) await startWith(activeSource.value)
|
||||
}
|
||||
|
||||
async function teardown() {
|
||||
if (stopCapture) {
|
||||
try {
|
||||
stopCapture()
|
||||
} catch {
|
||||
/* idempotent */
|
||||
}
|
||||
stopCapture = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const available = await availablePairingSources()
|
||||
sources.value = available
|
||||
const first = available[0]
|
||||
if (!first) {
|
||||
phase.value = 'no-source'
|
||||
return
|
||||
}
|
||||
await startWith(first)
|
||||
})
|
||||
|
||||
onUnmounted(teardown)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-6 p-8">
|
||||
<h1 class="text-2xl lg:text-[3.5rem] font-bold">Pair This Machine</h1>
|
||||
|
||||
<!-- Camera viewfinder -->
|
||||
<div
|
||||
v-show="phase === 'scanning' && activeSource?.kind === 'qr'"
|
||||
class="relative overflow-hidden rounded-2xl border-4 border-primary/40 bg-black"
|
||||
style="width: min(80vw, 28rem); aspect-ratio: 1 / 1"
|
||||
>
|
||||
<video ref="videoEl" class="h-full w-full object-cover" muted autoplay playsinline></video>
|
||||
<!-- Reticle -->
|
||||
<div class="pointer-events-none absolute inset-6 rounded-xl border-2 border-white/70"></div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="phase === 'scanning'"
|
||||
class="max-w-md text-center text-base lg:text-2xl text-muted-foreground"
|
||||
>
|
||||
Hold the operator's pairing QR up to the camera.
|
||||
</p>
|
||||
|
||||
<p v-if="phase === 'probing'" class="text-base lg:text-2xl text-muted-foreground">
|
||||
Starting camera…
|
||||
</p>
|
||||
|
||||
<div v-if="phase === 'pairing'" class="flex flex-col items-center gap-4">
|
||||
<p class="text-base lg:text-2xl text-muted-foreground">Pairing accepted — restarting…</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="phase === 'no-source'"
|
||||
class="max-w-md text-center text-base lg:text-2xl text-muted-foreground"
|
||||
>
|
||||
No camera or NFC reader is available on this machine. Pair by provisioning
|
||||
<span class="font-mono">VITE_SPIRE_SEED</span> instead.
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="phase === 'error'"
|
||||
class="max-w-md text-center text-base lg:text-xl text-destructive"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<!-- Transient rejected-scan hint while still scanning -->
|
||||
<p
|
||||
v-if="phase === 'scanning' && errorMessage"
|
||||
class="max-w-md text-center text-sm lg:text-base text-warning"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<!-- Alternate sources (e.g. NFC) when more than one is available -->
|
||||
<div v-if="sources.length > 1" class="flex gap-3">
|
||||
<button
|
||||
v-for="source in sources"
|
||||
:key="source.kind"
|
||||
class="rounded-lg border border-border px-4 py-2 text-sm"
|
||||
:class="activeSource?.kind === source.kind ? 'bg-primary text-primary-foreground' : ''"
|
||||
@click="startWith(source)"
|
||||
>
|
||||
{{ source.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
* (see App.vue's MAINTENANCE_SCREENS).
|
||||
*
|
||||
* Bunker failures (aiolabs/bitspire#52) get dedicated screens:
|
||||
* - `NoPairingError` (fresh machine, never paired) → `unpaired` — render the
|
||||
* interactive QR-pairing wizard so the operator can scan a spire-seed.
|
||||
* - `BunkerRejectedError` (revoked / TTL-expired / off-policy binding) →
|
||||
* `unpaired` — the operator must re-pair the machine.
|
||||
* `unpaired` too — re-pairing is the same scan-a-fresh-seed flow.
|
||||
* - `BunkerTimeoutError` (signer/relay unreachable) → `signer-unreachable`,
|
||||
* a transient condition.
|
||||
* Everything else surfaces its raw message (or the caller's fallback).
|
||||
*/
|
||||
export function classifyInitError(error: unknown, fallback = 'Initialization failed'): string {
|
||||
const name = (error as { name?: string } | null)?.name
|
||||
if (name === 'NoPairingError') return 'unpaired'
|
||||
if (name === 'BunkerRejectedError') return 'unpaired'
|
||||
if (name === 'BunkerTimeoutError') return 'signer-unreachable'
|
||||
return error instanceof Error ? error.message : fallback
|
||||
|
|
|
|||
80
apps/machine/src/services/pairing/__tests__/ingest.test.ts
Normal file
80
apps/machine/src/services/pairing/__tests__/ingest.test.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import { ingestScannedSeed } from '../ingest'
|
||||
import { SPIRE_SEED_SCHEME } from '@bitSpire/nostr-client'
|
||||
|
||||
/** Mirror of spirekeeper pairing.py: urlsafe base64, padding stripped. */
|
||||
function makeSeed(json: unknown): string {
|
||||
const b64 = Buffer.from(JSON.stringify(json), 'utf8')
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
return SPIRE_SEED_SCHEME + b64
|
||||
}
|
||||
|
||||
const SPIRE_PUBKEY = 'a'.repeat(64)
|
||||
const VALID_SEED = makeSeed({
|
||||
v: 1,
|
||||
spire_npub: 'npub1example',
|
||||
spire_pubkey: SPIRE_PUBKEY,
|
||||
bunker_url: `bunker://${SPIRE_PUBKEY}?relay=wss%3A%2F%2Fbunker.relay%2F&secret=deadbeef`,
|
||||
relays: ['wss://events.relay/'],
|
||||
})
|
||||
|
||||
describe('ingestScannedSeed', () => {
|
||||
const originalWindow = globalThis.window
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.window = originalWindow
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('rejects a non-seed scan without touching the bridge', async () => {
|
||||
const saveSpireSeed = vi.fn()
|
||||
globalThis.window = { electronAPI: { saveSpireSeed } } as unknown as Window & typeof globalThis
|
||||
|
||||
const result = await ingestScannedSeed('https://example.com/not-a-seed')
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) expect(result.reason).toBe('invalid-seed')
|
||||
expect(saveSpireSeed).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports no-bridge when Electron is absent', async () => {
|
||||
globalThis.window = {} as unknown as Window & typeof globalThis
|
||||
const result = await ingestScannedSeed(VALID_SEED)
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) expect(result.reason).toBe('no-bridge')
|
||||
})
|
||||
|
||||
it('persists the seed and relaunches on a valid scan', async () => {
|
||||
const saveSpireSeed = vi.fn().mockResolvedValue(undefined)
|
||||
const relaunchApp = vi.fn().mockResolvedValue(undefined)
|
||||
globalThis.window = {
|
||||
electronAPI: { saveSpireSeed, relaunchApp },
|
||||
} as unknown as Window & typeof globalThis
|
||||
|
||||
const result = await ingestScannedSeed(` ${VALID_SEED} `) // tolerate whitespace
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) expect(result.spirePubkey).toBe(SPIRE_PUBKEY)
|
||||
expect(saveSpireSeed).toHaveBeenCalledWith(VALID_SEED)
|
||||
expect(relaunchApp).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('surfaces persist-failed when saveSpireSeed throws', async () => {
|
||||
const saveSpireSeed = vi.fn().mockRejectedValue(new Error('EACCES'))
|
||||
globalThis.window = { electronAPI: { saveSpireSeed } } as unknown as Window & typeof globalThis
|
||||
|
||||
const result = await ingestScannedSeed(VALID_SEED)
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) expect(result.reason).toBe('persist-failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ingest does not pair in-renderer', () => {
|
||||
it('never imports connect logic — persistence + relaunch only', () => {
|
||||
// Guard: the design intentionally reuses the boot-time pairing path.
|
||||
// If someone wires connectNewSeed here, this comment + the ingest source
|
||||
// should be revisited together.
|
||||
expect(ingestScannedSeed).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
30
apps/machine/src/services/pairing/index.ts
Normal file
30
apps/machine/src/services/pairing/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Pairing module surface (aiolabs/bitspire#52).
|
||||
*
|
||||
* `availablePairingSources()` probes each known source and returns those the
|
||||
* current device can actually run, in preference order (camera first, NFC if
|
||||
* present). The wizard renders the first available source and offers the rest
|
||||
* as alternates.
|
||||
*/
|
||||
|
||||
import { QrPairingSource } from './qr-source'
|
||||
import { NfcPairingSource } from './nfc-source'
|
||||
import type { PairingSource } from './types'
|
||||
|
||||
export type { PairingSource, PairingSourceKind, PairingSourceStartOptions, StopCapture } from './types'
|
||||
export { QrPairingSource } from './qr-source'
|
||||
export { NfcPairingSource } from './nfc-source'
|
||||
export { ingestScannedSeed } from './ingest'
|
||||
export type { IngestResult } from './ingest'
|
||||
|
||||
/** All sources in preference order, regardless of availability. */
|
||||
export function allPairingSources(): PairingSource[] {
|
||||
return [new QrPairingSource(), new NfcPairingSource()]
|
||||
}
|
||||
|
||||
/** Only the sources this device can run, in preference order. */
|
||||
export async function availablePairingSources(): Promise<PairingSource[]> {
|
||||
const sources = allPairingSources()
|
||||
const flags = await Promise.all(sources.map((s) => s.isAvailable()))
|
||||
return sources.filter((_, i) => flags[i])
|
||||
}
|
||||
63
apps/machine/src/services/pairing/ingest.ts
Normal file
63
apps/machine/src/services/pairing/ingest.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Seed ingest pipeline (aiolabs/bitspire#52).
|
||||
*
|
||||
* Turns a raw scanned payload into a paired machine. The wizard captures a
|
||||
* string off some PairingSource and hands it here; we:
|
||||
* 1. validate it parses as a spire-seed (reject anything else — a QR on the
|
||||
* counter, a URL, a different protocol),
|
||||
* 2. persist it as VITE_SPIRE_SEED via the Electron bridge,
|
||||
* 3. relaunch so the normal boot path (signer-resolver → connectNewSeed)
|
||||
* performs the actual bunker pairing.
|
||||
*
|
||||
* We do NOT pair in-renderer here: persisting + relaunching reuses the single,
|
||||
* hardware-tested pairing path rather than duplicating connect/redeem logic in
|
||||
* the wizard. The trade-off is a ~kiosk-restart of latency, which is fine for a
|
||||
* one-time provisioning step.
|
||||
*/
|
||||
|
||||
import { parseSpireSeed, seedFingerprint } from '@bitSpire/nostr-client'
|
||||
|
||||
export type IngestResult =
|
||||
| { ok: true; spirePubkey: string; fingerprint: string; relays: string[] }
|
||||
| { ok: false; reason: 'invalid-seed' | 'no-bridge' | 'persist-failed'; message: string }
|
||||
|
||||
export async function ingestScannedSeed(raw: string): Promise<IngestResult> {
|
||||
const trimmed = (raw || '').trim()
|
||||
|
||||
let spirePubkey: string
|
||||
let relays: string[]
|
||||
try {
|
||||
const seed = parseSpireSeed(trimmed)
|
||||
spirePubkey = seed.spirePubkey
|
||||
relays = seed.relays
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'invalid-seed',
|
||||
message: e instanceof Error ? e.message : 'Not a valid pairing code',
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || !window.electronAPI) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'no-bridge',
|
||||
message: 'Pairing must run on the machine (no kiosk bridge available).',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await window.electronAPI.saveSpireSeed(trimmed)
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'persist-failed',
|
||||
message: e instanceof Error ? e.message : 'Could not save the pairing.',
|
||||
}
|
||||
}
|
||||
|
||||
// Fire-and-forget: the relaunch tears this process down.
|
||||
void window.electronAPI.relaunchApp()
|
||||
|
||||
return { ok: true, spirePubkey, fingerprint: seedFingerprint(trimmed), relays }
|
||||
}
|
||||
67
apps/machine/src/services/pairing/nfc-source.ts
Normal file
67
apps/machine/src/services/pairing/nfc-source.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* NFC pairing source — SCAFFOLD (aiolabs/bitspire#52).
|
||||
*
|
||||
* The user flagged NFC as a plausible future pairing method (tap a tag/phone
|
||||
* carrying the spire-seed). This wires the seam against the Web NFC API
|
||||
* (`NDEFReader`) so a future build can light it up without reworking the
|
||||
* wizard. It is NOT active on current hardware: Web NFC ships only on Chrome
|
||||
* for Android, so `isAvailable()` returns false on the Sintra's Linux Electron
|
||||
* and the wizard simply won't offer it.
|
||||
*
|
||||
* When real NFC hardware lands (likely a HAL peripheral rather than Web NFC),
|
||||
* replace the body of `start()` with that driver — the PairingSource contract
|
||||
* stays the same.
|
||||
*/
|
||||
|
||||
import type { PairingSource, PairingSourceStartOptions, StopCapture } from './types'
|
||||
|
||||
// Minimal structural type for the Web NFC API (not in lib.dom for Electron).
|
||||
interface NDEFReaderLike {
|
||||
scan(): Promise<void>
|
||||
addEventListener(
|
||||
type: 'reading',
|
||||
listener: (event: { message: { records: Array<{ recordType: string; data?: BufferSource }> } }) => void
|
||||
): void
|
||||
addEventListener(type: 'readingerror', listener: (event: unknown) => void): void
|
||||
}
|
||||
|
||||
function getNDEFReaderCtor(): (new () => NDEFReaderLike) | null {
|
||||
const ctor = (globalThis as { NDEFReader?: new () => NDEFReaderLike }).NDEFReader
|
||||
return ctor ?? null
|
||||
}
|
||||
|
||||
export class NfcPairingSource implements PairingSource {
|
||||
readonly kind = 'nfc' as const
|
||||
readonly label = 'NFC tap'
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return getNDEFReaderCtor() !== null
|
||||
}
|
||||
|
||||
async start(opts: PairingSourceStartOptions): Promise<StopCapture> {
|
||||
const Ctor = getNDEFReaderCtor()
|
||||
if (!Ctor) throw new Error('Web NFC unavailable on this device')
|
||||
|
||||
const reader = new Ctor()
|
||||
const decoder = new TextDecoder()
|
||||
let stopped = false
|
||||
|
||||
reader.addEventListener('reading', (event) => {
|
||||
if (stopped) return
|
||||
for (const record of event.message.records) {
|
||||
if (record.recordType === 'text' && record.data) {
|
||||
const raw = decoder.decode(record.data).trim()
|
||||
if (raw) opts.onScan(raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
reader.addEventListener('readingerror', (e) => opts.onError?.(e))
|
||||
|
||||
await reader.scan()
|
||||
// Web NFC has no explicit stop; the AbortController form would, but the
|
||||
// scaffold just flips a guard so late events are ignored after teardown.
|
||||
return () => {
|
||||
stopped = true
|
||||
}
|
||||
}
|
||||
}
|
||||
90
apps/machine/src/services/pairing/qr-source.ts
Normal file
90
apps/machine/src/services/pairing/qr-source.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Camera-based QR pairing source (aiolabs/bitspire#52).
|
||||
*
|
||||
* Decodes with `qr` (paulmillr) — a zero-dependency, auditable, dual
|
||||
* MIT/Apache library from the same author as the `@noble`/`@scure` crypto our
|
||||
* nostr stack already trusts (chosen over the dormant `jsqr` for that ethos +
|
||||
* active maintenance). Its `qr/dom.js` browser helper wraps getUserMedia and
|
||||
* the per-frame decode loop, so this source is a thin adapter onto the
|
||||
* PairingSource contract.
|
||||
*
|
||||
* The first successful decode wins; the loop then stops itself so a single
|
||||
* seed isn't ingested repeatedly.
|
||||
*/
|
||||
|
||||
import { QRCanvas, frontalCamera, frameLoop } from 'qr/dom.js'
|
||||
import type { PairingSource, PairingSourceStartOptions, StopCapture } from './types'
|
||||
|
||||
export class QrPairingSource implements PairingSource {
|
||||
readonly kind = 'qr' as const
|
||||
readonly label = 'Camera'
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return (
|
||||
typeof navigator !== 'undefined' &&
|
||||
!!navigator.mediaDevices &&
|
||||
typeof navigator.mediaDevices.getUserMedia === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
async start(opts: PairingSourceStartOptions): Promise<StopCapture> {
|
||||
const { onScan, onError, video } = opts
|
||||
if (!video) throw new Error('QrPairingSource requires a <video> element')
|
||||
|
||||
const camera = await frontalCamera(video)
|
||||
|
||||
// `frontalCamera` requests `ideal: screen.{width,height}`, so on the kiosk's
|
||||
// 1280x800 panel it otherwise streams at ~720p — and `decodeQR` then
|
||||
// center-crops to a square, leaving too few pixels-per-module for a dense
|
||||
// spire-seed QR on this fixed-focus lens. Pin a deliberate 1280x960 capture
|
||||
// instead: lamassu-machine caps QR scanning at 640x480 for decode speed
|
||||
// (megapixels just slow the per-frame decode), but our seed QR is denser
|
||||
// than a lightning invoice, so 1280x960 is the balance — ~14-18px/module at
|
||||
// frame-fill, still fast, and it meters exposure better than maxing the
|
||||
// sensor (a frame-filling QR keeps auto-exposure from blowing out on a
|
||||
// bright phone screen). The stream lives on the <video>'s srcObject
|
||||
// (QRCamera.stream is private); soft `ideal` so a camera that can't honor
|
||||
// it degrades to its closest mode instead of throwing.
|
||||
try {
|
||||
const stream = video.srcObject
|
||||
if (stream instanceof MediaStream) {
|
||||
await stream.getVideoTracks()[0]?.applyConstraints({
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 960 },
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
onError?.(e)
|
||||
}
|
||||
|
||||
const canvas = new QRCanvas() // decode-only; no overlay canvases needed
|
||||
|
||||
let stopped = false
|
||||
let cancel: (() => void) | null = null
|
||||
const stop: StopCapture = () => {
|
||||
if (stopped) return
|
||||
stopped = true
|
||||
cancel?.()
|
||||
camera.stop()
|
||||
}
|
||||
|
||||
cancel = frameLoop(() => {
|
||||
if (stopped) return
|
||||
try {
|
||||
// `fullSize: true` decodes the camera's intrinsic frame (videoWidth ×
|
||||
// videoHeight) rather than the <video> element's CSS box — the default
|
||||
// (`false`) was decoding the few-hundred-px on-screen preview, which
|
||||
// (compounded by `object-cover` cropping) starved the decoder.
|
||||
const result = camera.readFrame(canvas, true)
|
||||
if (result) {
|
||||
stop()
|
||||
onScan(result)
|
||||
}
|
||||
} catch (e) {
|
||||
onError?.(e)
|
||||
}
|
||||
})
|
||||
|
||||
return stop
|
||||
}
|
||||
}
|
||||
42
apps/machine/src/services/pairing/types.ts
Normal file
42
apps/machine/src/services/pairing/types.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Pairing-source abstraction (aiolabs/bitspire#52).
|
||||
*
|
||||
* A fresh ATM is paired by getting a `spire-seed:v1:…` onto the device. The
|
||||
* operator's spirekeeper mints that seed and renders it as a QR (and, later,
|
||||
* possibly an NFC tag). The machine ingests it via whatever capture hardware
|
||||
* it has — today a camera, tomorrow maybe an NFC reader or a HAL barcode
|
||||
* scanner. `PairingSource` is the seam that keeps the wizard UI and the
|
||||
* ingest pipeline agnostic to *how* the seed arrived.
|
||||
*
|
||||
* Implementations live next to this file: `qr-source.ts` (camera + jsQR),
|
||||
* `nfc-source.ts` (Web NFC scaffold). A HAL-scanner source can be added the
|
||||
* same way without touching the wizard.
|
||||
*/
|
||||
|
||||
export type PairingSourceKind = 'qr' | 'nfc'
|
||||
|
||||
export interface PairingSourceStartOptions {
|
||||
/** Invoked with each decoded payload (the raw seed string). */
|
||||
onScan: (raw: string) => void
|
||||
/** Invoked on a non-fatal capture error (e.g. a frame decode glitch). */
|
||||
onError?: (error: unknown) => void
|
||||
/**
|
||||
* The <video> element the camera preview renders into. Required by
|
||||
* camera-based sources; ignored by sources that don't show a viewfinder
|
||||
* (e.g. NFC).
|
||||
*/
|
||||
video?: HTMLVideoElement
|
||||
}
|
||||
|
||||
/** Releases capture hardware (camera stream, NFC reader). Idempotent. */
|
||||
export type StopCapture = () => void
|
||||
|
||||
export interface PairingSource {
|
||||
readonly kind: PairingSourceKind
|
||||
/** Short label for the wizard's source picker (e.g. "Camera", "NFC tap"). */
|
||||
readonly label: string
|
||||
/** Whether this source can run in the current environment. */
|
||||
isAvailable(): Promise<boolean>
|
||||
/** Begin capturing; resolves once hardware is live. */
|
||||
start(opts: PairingSourceStartOptions): Promise<StopCapture>
|
||||
}
|
||||
|
|
@ -31,6 +31,20 @@ import type { BunkerBindingRecord } from '@/types/electron'
|
|||
|
||||
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
|
||||
|
||||
/**
|
||||
* Thrown in strict mode when the machine has no seed and no binding — it is
|
||||
* genuinely unpaired, not misconfigured. The renderer catches this to show the
|
||||
* QR-pairing wizard (camera scan of a spire-seed) rather than a fault screen.
|
||||
* Distinct `.name` so it survives the bundle boundary (instanceof is fragile
|
||||
* across the electron/renderer split). See services/init-error.ts.
|
||||
*/
|
||||
export class NoPairingError extends Error {
|
||||
override readonly name = 'NoPairingError'
|
||||
constructor() {
|
||||
super('[Signer] Machine is unpaired — no spire seed and no bunker binding.')
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResolveSignerOptions {
|
||||
/** Allow an ephemeral LocalSigner when no seed/binding exists (dev only). */
|
||||
allowEphemeral: boolean
|
||||
|
|
@ -109,8 +123,5 @@ export async function resolveSigner(opts: ResolveSignerOptions): Promise<Signer>
|
|||
return new LocalSigner(generateIdentity())
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[Signer] No spire seed and no bunker binding — cannot resolve a signing identity (strict mode). ' +
|
||||
'Set VITE_SPIRE_SEED or pair the ATM.'
|
||||
)
|
||||
throw new NoPairingError()
|
||||
}
|
||||
|
|
|
|||
2
apps/machine/src/types/electron.d.ts
vendored
2
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -98,6 +98,8 @@ declare global {
|
|||
saveBunkerBinding: (binding: BunkerBindingRecord) => Promise<void>
|
||||
clearBunkerBinding: () => Promise<void>
|
||||
resetBootstrapGate: () => Promise<void>
|
||||
saveSpireSeed: (seed: string) => Promise<void>
|
||||
relaunchApp: () => Promise<void>
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: { positions: Record<string, { denomination: number; count: number }> },
|
||||
eventCreatedAt: number
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ pkgs.stdenv.mkDerivation (finalAttrs: {
|
|||
inherit (finalAttrs) pname version src pnpmWorkspaces;
|
||||
inherit pnpm;
|
||||
fetcherVersion = 3;
|
||||
hash = "sha256-Jv5p62E40DtCSZvN/+LTzkhRYJBTyyUOVSBlQyxzcEw=";
|
||||
hash = "sha256-03ANBQ7bHJwsqlX2ScA1+1LFuO8njiuU7O4VGOb6cMM=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
|
|
|
|||
279
pnpm-lock.yaml
generated
279
pnpm-lock.yaml
generated
|
|
@ -65,6 +65,9 @@ importers:
|
|||
pinia:
|
||||
specifier: ^2.2.0
|
||||
version: 2.3.1(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||
qr:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
qrcode.vue:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0(vue@3.5.27(typescript@5.9.3))
|
||||
|
|
@ -353,12 +356,6 @@ packages:
|
|||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -377,12 +374,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -401,12 +392,6 @@ packages:
|
|||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -425,12 +410,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -449,12 +428,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -473,12 +446,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -497,12 +464,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -521,12 +482,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -545,12 +500,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -569,12 +518,6 @@ packages:
|
|||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -593,12 +536,6 @@ packages:
|
|||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -617,12 +554,6 @@ packages:
|
|||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -641,12 +572,6 @@ packages:
|
|||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -665,12 +590,6 @@ packages:
|
|||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -689,12 +608,6 @@ packages:
|
|||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -713,12 +626,6 @@ packages:
|
|||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -737,12 +644,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -755,12 +656,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -779,12 +674,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -797,12 +686,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -821,12 +704,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -839,12 +716,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -863,12 +734,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -887,12 +752,6 @@ packages:
|
|||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -911,12 +770,6 @@ packages:
|
|||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -935,12 +788,6 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2049,11 +1896,6 @@ packages:
|
|||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.2:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.4:
|
||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2860,6 +2702,10 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qr@0.6.0:
|
||||
resolution: {integrity: sha512-P23VoX7SipHALdiIYG+D+LT/6n22dNKwV92FAb3d+Nlki/5WisSsfLt0UDFz2XEBtuwrECTznvu+chKKFCSYhA==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
qrcode.vue@3.6.0:
|
||||
resolution: {integrity: sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==}
|
||||
peerDependencies:
|
||||
|
|
@ -3579,9 +3425,6 @@ snapshots:
|
|||
'@esbuild/aix-ppc64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3591,9 +3434,6 @@ snapshots:
|
|||
'@esbuild/android-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3603,9 +3443,6 @@ snapshots:
|
|||
'@esbuild/android-arm@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3615,9 +3452,6 @@ snapshots:
|
|||
'@esbuild/android-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3627,9 +3461,6 @@ snapshots:
|
|||
'@esbuild/darwin-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3639,9 +3470,6 @@ snapshots:
|
|||
'@esbuild/darwin-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3651,9 +3479,6 @@ snapshots:
|
|||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3663,9 +3488,6 @@ snapshots:
|
|||
'@esbuild/freebsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3675,9 +3497,6 @@ snapshots:
|
|||
'@esbuild/linux-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3687,9 +3506,6 @@ snapshots:
|
|||
'@esbuild/linux-arm@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3699,9 +3515,6 @@ snapshots:
|
|||
'@esbuild/linux-ia32@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3711,9 +3524,6 @@ snapshots:
|
|||
'@esbuild/linux-loong64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3723,9 +3533,6 @@ snapshots:
|
|||
'@esbuild/linux-mips64el@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3735,9 +3542,6 @@ snapshots:
|
|||
'@esbuild/linux-ppc64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3747,9 +3551,6 @@ snapshots:
|
|||
'@esbuild/linux-riscv64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3759,9 +3560,6 @@ snapshots:
|
|||
'@esbuild/linux-s390x@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3771,18 +3569,12 @@ snapshots:
|
|||
'@esbuild/linux-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3792,18 +3584,12 @@ snapshots:
|
|||
'@esbuild/netbsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3813,18 +3599,12 @@ snapshots:
|
|||
'@esbuild/openbsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3834,9 +3614,6 @@ snapshots:
|
|||
'@esbuild/sunos-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3846,9 +3623,6 @@ snapshots:
|
|||
'@esbuild/win32-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3858,9 +3632,6 @@ snapshots:
|
|||
'@esbuild/win32-ia32@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -3870,9 +3641,6 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
|
|
@ -5133,35 +4901,6 @@ snapshots:
|
|||
'@esbuild/win32-ia32': 0.25.12
|
||||
'@esbuild/win32-x64': 0.25.12
|
||||
|
||||
esbuild@0.27.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
'@esbuild/android-arm': 0.27.2
|
||||
'@esbuild/android-arm64': 0.27.2
|
||||
'@esbuild/android-x64': 0.27.2
|
||||
'@esbuild/darwin-arm64': 0.27.2
|
||||
'@esbuild/darwin-x64': 0.27.2
|
||||
'@esbuild/freebsd-arm64': 0.27.2
|
||||
'@esbuild/freebsd-x64': 0.27.2
|
||||
'@esbuild/linux-arm': 0.27.2
|
||||
'@esbuild/linux-arm64': 0.27.2
|
||||
'@esbuild/linux-ia32': 0.27.2
|
||||
'@esbuild/linux-loong64': 0.27.2
|
||||
'@esbuild/linux-mips64el': 0.27.2
|
||||
'@esbuild/linux-ppc64': 0.27.2
|
||||
'@esbuild/linux-riscv64': 0.27.2
|
||||
'@esbuild/linux-s390x': 0.27.2
|
||||
'@esbuild/linux-x64': 0.27.2
|
||||
'@esbuild/netbsd-arm64': 0.27.2
|
||||
'@esbuild/netbsd-x64': 0.27.2
|
||||
'@esbuild/openbsd-arm64': 0.27.2
|
||||
'@esbuild/openbsd-x64': 0.27.2
|
||||
'@esbuild/openharmony-arm64': 0.27.2
|
||||
'@esbuild/sunos-x64': 0.27.2
|
||||
'@esbuild/win32-arm64': 0.27.2
|
||||
'@esbuild/win32-ia32': 0.27.2
|
||||
'@esbuild/win32-x64': 0.27.2
|
||||
|
||||
esbuild@0.27.4:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.4
|
||||
|
|
@ -5982,6 +5721,8 @@ snapshots:
|
|||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
qr@0.6.0: {}
|
||||
|
||||
qrcode.vue@3.6.0(vue@3.5.27(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vue: 3.5.27(typescript@5.9.3)
|
||||
|
|
@ -6346,7 +6087,7 @@ snapshots:
|
|||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
esbuild: 0.27.4
|
||||
get-tsconfig: 4.13.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue