diff --git a/env.d.ts b/env.d.ts
index 11f02fe..ad3afc1 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -1 +1,12 @@
///
+
+interface ImportMetaEnv {
+ /** Hex- or bech32-encoded npub of the inquiry recipient. */
+ readonly VITE_OWNER_NPUB?: string
+ /** Comma-separated wss:// relay list; falls back to a default set if unset. */
+ readonly VITE_NOSTR_RELAYS?: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/package.json b/package.json
index 8c678b7..1486224 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@vueuse/core": "^14.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "nostr-tools": "^2.23.5",
"pinia": "^3.0.4",
"reka-ui": "^2.9.8",
"tailwind-merge": "^3.6.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 48b159c..f90a373 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ nostr-tools:
+ specifier: ^2.23.5
+ version: 2.23.5(typescript@6.0.3)
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
@@ -257,6 +260,18 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
+ '@noble/ciphers@2.1.1':
+ resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
+ engines: {node: '>= 20.19.0'}
+
+ '@noble/curves@2.0.1':
+ resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==}
+ engines: {node: '>= 20.19.0'}
+
+ '@noble/hashes@2.0.1':
+ resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
+ engines: {node: '>= 20.19.0'}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -374,6 +389,15 @@ packages:
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+ '@scure/base@2.0.0':
+ resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==}
+
+ '@scure/bip32@2.0.1':
+ resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==}
+
+ '@scure/bip39@2.0.1':
+ resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==}
+
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
@@ -1126,6 +1150,17 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ nostr-tools@2.23.5:
+ resolution: {integrity: sha512-Fa7ZlUdjfUW1P4E7H3yBexhOHYi18XNyvd2n7eNHkYR085xADX6Y8V8Vm7nT/XQajaFOBrptXmVIGkJ2E4vfVw==}
+ peerDependencies:
+ typescript: '>=5.0.0'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ nostr-wasm@0.1.0:
+ resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==}
+
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -1667,6 +1702,14 @@ snapshots:
'@tybys/wasm-util': 0.10.2
optional: true
+ '@noble/ciphers@2.1.1': {}
+
+ '@noble/curves@2.0.1':
+ dependencies:
+ '@noble/hashes': 2.0.1
+
+ '@noble/hashes@2.0.1': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -1734,6 +1777,19 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {}
+ '@scure/base@2.0.0': {}
+
+ '@scure/bip32@2.0.1':
+ dependencies:
+ '@noble/curves': 2.0.1
+ '@noble/hashes': 2.0.1
+ '@scure/base': 2.0.0
+
+ '@scure/bip39@2.0.1':
+ dependencies:
+ '@noble/hashes': 2.0.1
+ '@scure/base': 2.0.0
+
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
@@ -2477,6 +2533,20 @@ snapshots:
natural-compare@1.4.0: {}
+ nostr-tools@2.23.5(typescript@6.0.3):
+ dependencies:
+ '@noble/ciphers': 2.1.1
+ '@noble/curves': 2.0.1
+ '@noble/hashes': 2.0.1
+ '@scure/base': 2.0.0
+ '@scure/bip32': 2.0.1
+ '@scure/bip39': 2.0.1
+ nostr-wasm: 0.1.0
+ optionalDependencies:
+ typescript: 6.0.3
+
+ nostr-wasm@0.1.0: {}
+
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
diff --git a/src/features/nostr/store.ts b/src/features/nostr/store.ts
new file mode 100644
index 0000000..6d8c763
--- /dev/null
+++ b/src/features/nostr/store.ts
@@ -0,0 +1,30 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { SimplePool, type Event } from 'nostr-tools'
+
+const DEFAULT_RELAYS = [
+ 'wss://relay.damus.io',
+ 'wss://nos.lol',
+ 'wss://relay.nostr.band',
+]
+
+function parseRelays(): string[] {
+ const env = import.meta.env.VITE_NOSTR_RELAYS
+ if (!env) return DEFAULT_RELAYS
+ const list = env
+ .split(',')
+ .map((r: string) => r.trim())
+ .filter((r: string) => r.startsWith('wss://') || r.startsWith('ws://'))
+ return list.length ? list : DEFAULT_RELAYS
+}
+
+export const useNostrStore = defineStore('nostr', () => {
+ const pool = new SimplePool()
+ const relays = ref(parseRelays())
+
+ async function publish(event: Event): Promise[]> {
+ return Promise.allSettled(pool.publish(relays.value, event))
+ }
+
+ return { relays, publish }
+})
diff --git a/src/features/nostr/submitInquiry.ts b/src/features/nostr/submitInquiry.ts
new file mode 100644
index 0000000..24ee305
--- /dev/null
+++ b/src/features/nostr/submitInquiry.ts
@@ -0,0 +1,69 @@
+import { nip17, nip19 } from 'nostr-tools'
+import { generateSecretKey } from 'nostr-tools/pure'
+import { useNostrStore } from './store'
+
+export type ContactMethod = 'email' | 'whatsapp' | 'signal' | 'telegram' | 'nostr'
+
+export interface InquiryPayload {
+ name?: string
+ contactMethod: ContactMethod
+ contactValue: string
+ message: string
+}
+
+export interface SubmitResult {
+ ok: boolean
+ acceptedBy: number
+ attempted: number
+}
+
+const METHOD_LABEL: Record = {
+ email: 'Email',
+ whatsapp: 'WhatsApp',
+ signal: 'Signal',
+ telegram: 'Telegram',
+ nostr: 'Nostr',
+}
+
+function ownerHex(): string {
+ const npub = import.meta.env.VITE_OWNER_NPUB
+ if (!npub) {
+ throw new Error(
+ 'VITE_OWNER_NPUB is not set — the inquiry form has no destination.',
+ )
+ }
+ const decoded = nip19.decode(npub.trim() as `npub1${string}`)
+ if (decoded.type !== 'npub') {
+ throw new Error(
+ `VITE_OWNER_NPUB must be an npub (got ${(decoded as { type: string }).type}).`,
+ )
+ }
+ return decoded.data
+}
+
+function formatMessage(p: InquiryPayload): string {
+ const lines: string[] = []
+ lines.push('New inquiry — Earth Walker Design')
+ lines.push('')
+ if (p.name) lines.push(`From: ${p.name}`)
+ lines.push(`Preferred contact: ${METHOD_LABEL[p.contactMethod]} — ${p.contactValue}`)
+ lines.push('')
+ lines.push(p.message)
+ return lines.join('\n')
+}
+
+export async function submitInquiry(payload: InquiryPayload): Promise {
+ const recipient = ownerHex()
+ const senderSk = generateSecretKey()
+ const wrapped = nip17.wrapEvent(senderSk, { publicKey: recipient }, formatMessage(payload))
+
+ const nostr = useNostrStore()
+ const results = await nostr.publish(wrapped)
+
+ const accepted = results.filter((r) => r.status === 'fulfilled').length
+ return {
+ ok: accepted > 0,
+ acceptedBy: accepted,
+ attempted: results.length,
+ }
+}