signet-verify 0.7.0 → 0.7.2

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,111 @@
1
+ # Changelog
2
+
3
+ ## 0.7.2 (2026-10-16)
4
+
5
+ ### Bug Fixes
6
+
7
+ - correct the published docs against the source (#49) (docs)
8
+
9
+
10
+
11
+ ## 0.7.1 (2026-09-25)
12
+
13
+ ### Bug Fixes
14
+
15
+ - default cross-device relay to nos.lol (#46)
16
+
17
+
18
+
19
+ ## 0.7.0 (2026-09-11)
20
+
21
+ ### Features
22
+
23
+ - default to the Signet relay and export it as DEFAULT_RELAY_URL (#44) (verify)
24
+
25
+
26
+
27
+ ## 0.6.1 (2026-09-11)
28
+
29
+ ### Bug Fixes
30
+
31
+ - fail with relay-refused when the relay closes the subscription (#43) (auth)
32
+
33
+
34
+
35
+ ## 0.6.0 (2026-09-11)
36
+
37
+ ### Features
38
+
39
+ - one anchor for the sign-in deadline, and tell the user about it (#42) (auth)
40
+
41
+
42
+
43
+ ## 0.5.2 (2026-09-10)
44
+
45
+ ### Bug Fixes
46
+
47
+ - let a restarted wait keep its original subscription window (#41) (auth)
48
+
49
+
50
+
51
+ ## 0.5.1 (2026-09-08)
52
+
53
+ ### Bug Fixes
54
+
55
+ - replay auth responses after mobile app switching (#39)
56
+
57
+
58
+
59
+ ## 0.5.0 (2026-06-05)
60
+
61
+ ### Features
62
+
63
+ - surface bunkerUri from the auth response (cross-device signer upgrade)
64
+
65
+
66
+
67
+ ## 0.3.1 (2026-05-03)
68
+
69
+ ### Bug Fixes
70
+
71
+ - bump to publish 0.3.0 displayName feature to npm (#11) (release)
72
+
73
+
74
+
75
+ ## 0.3.0 (2026-05-03)
76
+
77
+ ### Features
78
+
79
+ - surface displayName from AuthResponse, sanitised (#9) (auth)
80
+
81
+
82
+
83
+ ## 0.2.2 (2026-05-03)
84
+
85
+ ### Features
86
+
87
+ - `SignetAuthResult.displayName?: string` — surfaces the user's persona handle when they opted to share it on the approval screen. Mirrors the `display_name` URL param delivered by the redirect-back flow. Sanitised on receipt: control + bidi characters stripped, capped at 64 chars, empty values dropped.
88
+
89
+
90
+
91
+ ## 0.2.1 (2026-04-18)
92
+
93
+ ### Bug Fixes
94
+
95
+ - use published signet-protocol@^1.6.0, not file:../signet (deps)
96
+
97
+
98
+
99
+ ## 0.2.0 (2026-04-18)
100
+
101
+ ### Features
102
+
103
+ - reshape signet-verify as a proper ESM npm package
104
+ - waitForAuthResponse SDK helper for cross-device login
105
+ - initial release of signet-verify — drop-in age verification SDK
106
+
107
+ ### Bug Fixes
108
+
109
+ - bump esbuild to ^0.25.12 (GHSA-67mh-4wv8-2f99) (deps)
110
+
111
+
package/README.md CHANGED
@@ -2,10 +2,19 @@
2
2
 
3
3
  Add privacy-preserving age verification to any website. One script tag, one function call. No personal data collected.
4
4
 
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install signet-verify
9
+ ```
10
+
5
11
  ## Quick Start
6
12
 
13
+ Script tag (the IIFE bundle exposes `window.Signet`; the version is pinned, so change `0.7.1` to upgrade):
14
+
7
15
  ```html
8
- <script src="https://cdn.signet.forgesworn.dev/signet-verify.js"></script>
16
+ <button id="verify-btn">Verify your age</button>
17
+ <script src="https://cdn.jsdelivr.net/npm/signet-verify@0.7.1/dist/signet-verify.iife.js"></script>
9
18
  <script>
10
19
  document.getElementById('verify-btn').addEventListener('click', async () => {
11
20
  const result = await Signet.verifyAge('18+');
@@ -17,17 +26,26 @@ Add privacy-preserving age verification to any website. One script tag, one func
17
26
  }
18
27
  });
19
28
  </script>
20
- <button id="verify-btn">Verify your age</button>
29
+ ```
30
+
31
+ ESM (bundler, browser only, since `verifyAge` opens a modal):
32
+
33
+ ```ts
34
+ import { verifyAge } from 'signet-verify';
35
+
36
+ const result = await verifyAge('18+');
37
+ if (!result.verified) console.log('Not verified:', result.error);
21
38
  ```
22
39
 
23
40
  ## How It Works
24
41
 
25
42
  1. Website calls `Signet.verifyAge('18+')`
26
- 2. A modal appears with a QR code
27
- 3. User scans with their Signet app and approves
28
- 4. The app sends a cryptographic proof (no PII)
29
- 5. The SDK verifies the proof AND checks the verifier's professional registration
30
- 6. Returns the result — the user's app can be closed
43
+ 2. A modal opens. The SDK builds a `signet-verify-request` (request ID, required age range, `relayUrl`, timestamp) but the modal shows only a truncated text placeholder, `signet:verify:` plus the first 40 characters of the base64 payload, not a rendered QR code. Those 40 characters stop before the request ID and the relay URL
44
+ 3. The modal listens on a `BroadcastChannel` named `signet-verify-<requestId>`. It opens no WebSocket and never reads from a relay. The proof must arrive as a `signet-verify-response` message carrying that request ID on that channel, so whatever posts it has to run in the same origin and browser profile as the page
45
+ 4. The SDK checks the credential's signature, expiry and age range, then asks the verification bot about the verifier's professional registration
46
+ 5. Returns the result
47
+
48
+ To receive a response from another device over a relay, use `waitForAuthResponse` (see below); `verifyAge` does not do that.
31
49
 
32
50
  ## Verifier Trust
33
51
 
@@ -53,56 +71,79 @@ const result = await Signet.verifyAge('18+', { verifierCheckUrl: null });
53
71
 
54
72
  ## Cross-device Sign-in-with-Signet
55
73
 
56
- `verifyAge` wraps the same-device QR/modal flow. For a cross-device sign-in —
74
+ `verifyAge` resolves with a `SignetVerifyResult` and reports failures in `result.error` rather than rejecting. `waitForAuthResponse` resolves with a `SignetAuthResult` on success and rejects with an `Error` whose `message` and `code` are the failure code (see below). Their `timeout` options also differ: for `verifyAge` it is how long the modal waits before resolving with `error: 'timeout'`; for `waitForAuthResponse` it is an optional, earlier give-up.
75
+
76
+ `verifyAge` is the same-browser modal flow: its response arrives on a
77
+ `BroadcastChannel`, never over a relay. For a cross-device sign-in,
57
78
  a consumer minting its own challenge, opening the auth URL or QR on one
58
79
  device, and waiting on a relay for the response published from the user's
59
- phone — use `waitForAuthResponse` directly:
80
+ phone, use `waitForAuthResponse` directly:
60
81
 
61
82
  ```ts
62
- import { waitForAuthResponse } from 'signet-verify';
83
+ import { schnorr } from '@noble/curves/secp256k1.js'; // install @noble/curves yourself; the SDK does not re-export it
84
+ import { bytesToHex } from '@noble/hashes/utils.js';
85
+ import { waitForAuthResponse, DEFAULT_RELAY_URL } from 'signet-verify';
63
86
 
64
- // Stamp this ONCE, when you mint the challenge and open the auth URL / show the QR.
87
+ // 1. Ephemeral session keypair. The private key must be a 32-byte Uint8Array;
88
+ // a hex string is rejected with `invalid-session-privkey`.
89
+ const sessionPrivKey = schnorr.utils.randomSecretKey();
90
+ const sessionPubkey = bytesToHex(schnorr.getPublicKey(sessionPrivKey));
91
+
92
+ // 2. The challenge: 32 random bytes as 64-character hex. This is the `requestId`.
93
+ const requestId = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
94
+
95
+ // 3. Stamp this ONCE, when you mint the challenge and open the auth URL / show the QR.
65
96
  const issuedAt = Math.floor(Date.now() / 1000);
66
97
 
98
+ // 4. The auth URL (render it as a QR code or a link). The parameters mirror
99
+ // examples/cross-device-login.html; `relay` must match `relayUrl` below.
100
+ const signetAppUrl = 'https://signet-app.example'; // placeholder: your Signet app's origin
101
+ const authUrl = signetAppUrl + '/?' + new URLSearchParams({
102
+ auth: '1',
103
+ challenge: requestId,
104
+ origin: location.origin,
105
+ name: 'My Site',
106
+ callback: location.origin + '/callback', // unused in relay mode, still required
107
+ t: String(issuedAt),
108
+ relay: DEFAULT_RELAY_URL,
109
+ sessionPubkey,
110
+ }).toString();
111
+
112
+ // 5. Wait for the response. Call controller.abort() to cancel.
113
+ const controller = new AbortController();
67
114
  const result = await waitForAuthResponse({
68
- requestId, // the challenge you put in the auth URL / QR
69
- relayUrl,
70
- sessionPrivKey, // your ephemeral session keypair's private key
115
+ requestId,
116
+ relayUrl: DEFAULT_RELAY_URL,
117
+ sessionPrivKey,
71
118
  expectedOrigin: location.origin,
72
- issuedAt, // ← pass on EVERY call for this sign-in, including retries
119
+ issuedAt, // pass on EVERY call for this sign-in, including retries
120
+ abortSignal: controller.signal,
73
121
  });
74
122
  ```
75
123
 
76
124
  ### The sign-in deadline
77
125
 
78
126
  A cross-device sign-in has exactly one deadline: `issuedAt + AUTH_FRESHNESS_WINDOW_SEC`.
79
- Past that point, this library stops listening and reports `expired` — show a countdown
80
- to it.
127
+ `AUTH_FRESHNESS_WINDOW_SEC` is `300` seconds (5 minutes). Past that point, this library
128
+ stops listening and reports `expired`, so show a countdown to it.
81
129
 
82
130
  ```ts
83
- import { waitForAuthResponse, AUTH_FRESHNESS_WINDOW_SEC, DEFAULT_RELAY_URL } from 'signet-verify';
131
+ import { AUTH_FRESHNESS_WINDOW_SEC } from 'signet-verify';
84
132
 
85
- // Stamp this ONCE, when you mint the challenge and open the auth URL / show the QR.
86
- const issuedAt = Math.floor(Date.now() / 1000);
87
- const expiresAt = issuedAt + AUTH_FRESHNESS_WINDOW_SEC; // show a countdown to this
88
-
89
- const result = await waitForAuthResponse({
90
- requestId: challenge,
91
- relayUrl: DEFAULT_RELAY_URL, // the Signet relay — must match the relay= in your auth URL
92
- sessionPrivKey,
93
- expectedOrigin: location.origin,
94
- issuedAt, // ← pass on EVERY call for this sign-in, retries included
95
- abortSignal, // ← optional; cancels the wait and closes the subscription
96
- });
133
+ const expiresAt = issuedAt + AUTH_FRESHNESS_WINDOW_SEC; // unix seconds; count down to this
134
+ const secondsLeft = () => Math.max(0, expiresAt - Math.floor(Date.now() / 1000));
97
135
  ```
98
136
 
137
+ The `timeout` option is in milliseconds. Leave it out when you pass `issuedAt`: the
138
+ wait then lasts until the deadline above.
139
+
99
140
  **Pass `issuedAt` on every call, including a restart.** On mobile the page is
100
141
  suspended the moment it hands off to the signer app. A wait restarted without the
101
142
  anchor asks the relay only for responses newer than the restart — which silently
102
143
  excludes the one published while you were backgrounded, and it is never asked for
103
144
  again. This was a real, 100%-reproducible failure before 0.5.2.
104
145
 
105
- **Distinguish the failures.** The rejection message is:
146
+ **Distinguish the failures.** The rejection is an `Error` whose message is one of:
106
147
 
107
148
  | Code | Meaning | What to offer |
108
149
  |---|---|---|
@@ -110,9 +151,11 @@ again. This was a real, 100%-reproducible failure before 0.5.2.
110
151
  | `timeout` | The wait gave up early — an explicit shorter `timeout`, or no `issuedAt` was supplied at all | A retry, anchored to the same `issuedAt` |
111
152
  | `denied` | The user rejected the request | Nothing; respect it |
112
153
  | `aborted` | You aborted via `abortSignal` | Nothing |
113
- | `relay-error` | The socket failed | A retry, anchored to the same `issuedAt` |
114
- | `relay-refused` | The relay closed the subscription — `err.reason` has its text (e.g. `auth-required`) | A relay that serves gift wraps to an unauthenticated reader. `relay.damus.io` currently does not; `relay.trotters.cc` (the Signet app's relay) does |
115
- | `invalid-issued-at` | `issuedAt` looks like the wrong unit (e.g. milliseconds) or is too far in the future | A bug in your code, not a retry — check you're passing unix **seconds** |
154
+ | `relay-error` | The socket failed. Rejected only where there is no `document` (a non-browser runtime); in a browser the wait reconnects while the page is visible and gives up with `timeout` or `expired` | A retry, anchored to the same `issuedAt` |
155
+ | `relay-closed` | The socket closed. Same rule as `relay-error` | A retry, anchored to the same `issuedAt` |
156
+ | `relay-refused` | The relay closed the subscription — `err.reason` has its text (e.g. `auth-required`) | A relay that serves gift wraps to an unauthenticated reader. `relay.damus.io` currently does not; `nos.lol` (the default) does |
157
+ | `invalid-issued-at` | `issuedAt` looks like the wrong unit (e.g. milliseconds) or is too far in the future | A bug in your code, not a retry; check you're passing unix **seconds** |
158
+ | `invalid-request-id`, `invalid-session-privkey`, `invalid-relay-url`, `invalid-expected-origin` | The matching option failed validation before any connection was made | A bug in your code, not a retry; see the option table below |
116
159
 
117
160
  The code is on both `err.message` and `err.code` — branch on either.
118
161
 
@@ -121,17 +164,17 @@ precedence over `issuedAt`. Prefer `issuedAt`, which derives it correctly.
121
164
 
122
165
  ## API
123
166
 
124
- ### `Signet.verifyAge(requiredAgeRange, options?)`
167
+ ### `verifyAge(requiredAgeRange, options?)` (`Signet.verifyAge` in the IIFE build)
125
168
 
126
- Returns `Promise<SignetVerifyResult>`
169
+ Returns `Promise<SignetVerifyResult>`. Failures are reported in `result.error`, not as a rejection.
127
170
 
128
171
  **Options:**
129
172
 
130
173
  | Option | Type | Default | Description |
131
174
  |--------|------|---------|-------------|
132
- | `relayUrl` | `string` | `DEFAULT_RELAY_URL` (`wss://relay.trotters.cc`) | Relay URL for cross-device communication. Must serve kind-1059 gift wraps to an unauthenticated reader |
175
+ | `relayUrl` | `string` | `DEFAULT_RELAY_URL` (`wss://nos.lol`) | Written into the `relayUrl` field of the request object `verifyAge` builds. `verifyAge` never connects to it, the response does not travel over it (it arrives on a `BroadcastChannel`), and the text the modal shows does not include it. The kind-1059 gift-wrap requirement under `waitForAuthResponse` therefore does not apply to this option |
133
176
  | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Modal colour scheme |
134
- | `timeout` | `number` | `120000` | Timeout in milliseconds |
177
+ | `timeout` | `number` | `120000` | Timeout in milliseconds, clamped to 5000 to 600000 |
135
178
  | `verifierCheckUrl` | `string \| null` | `'https://verify.signet.forgesworn.dev'` | Verification bot URL. Set to `null` to skip. |
136
179
  | `acceptUnconfirmed` | `boolean` | `false` | Accept credentials from unconfirmed verifiers |
137
180
 
@@ -156,11 +199,43 @@ Returns `Promise<SignetVerifyResult>`
156
199
  |-------|---------|
157
200
  | `'cancelled'` | User cancelled the verification |
158
201
  | `'timeout'` | Verification timed out (default: 2 minutes) |
202
+ | `'invalid-age-range'` | `requiredAgeRange` is not one of the five values under Age Range Values; no modal is shown |
159
203
  | `'invalid-credential'` | Credential signature failed verification |
204
+ | `'credential-expired'` | The credential's `expires` time has passed |
160
205
  | `'age-range-not-met'` | Credential age range doesn't satisfy the requirement |
161
206
  | `'verifier-not-confirmed'` | Verifier is not confirmed against a public register |
162
207
  | `'verifier-check-unavailable'` | Verification bot unreachable (credential valid but verifier status unknown) |
163
208
 
209
+ ### `waitForAuthResponse(options)` (`Signet.waitForAuthResponse` in the IIFE build)
210
+
211
+ Returns `Promise<SignetAuthResult>`. It resolves with the verified result and rejects with an `Error` on failure; `err.code` is one of the codes in the failure table under Cross-device Sign-in-with-Signet.
212
+
213
+ **Options (`WaitForAuthOptions`):**
214
+
215
+ | Option | Type | Required | Default | Description |
216
+ |--------|------|----------|---------|-------------|
217
+ | `requestId` | `string` | yes | none | The challenge sent in the auth URL: 64-character hex (32 bytes) |
218
+ | `relayUrl` | `string` | yes | none | The relay you subscribed on. `wss://`, or `ws://` for `localhost` and `127.0.0.1` only |
219
+ | `sessionPrivKey` | `Uint8Array` | yes | none | Ephemeral session private key, 32 bytes. Used to unwrap the NIP-17 response |
220
+ | `expectedOrigin` | `string` | yes | none | Your origin (scheme, host and optional port), verbatim. Must match the `origin` tag of the signed auth event |
221
+ | `issuedAt` | `number` | no, but pass it | none | Unix **seconds** when you minted the challenge. Anchors the relay query and the deadline. More than 300 seconds in the future rejects with `invalid-issued-at` |
222
+ | `timeout` | `number` | no | Remaining validity (`issuedAt + 300` seconds minus now) when `issuedAt` is set, otherwise `120000` | Milliseconds, clamped to 5000 to 600000. Use it only to give up earlier than the default |
223
+ | `since` | `number` | no | `issuedAt - 60` when `issuedAt` is set, otherwise now minus 60 | Raw relay query anchor in unix seconds. Takes precedence over `issuedAt` |
224
+ | `abortSignal` | `AbortSignal` | no | none | Aborts the wait and closes the subscription; rejects with `aborted` |
225
+
226
+ **Result (`SignetAuthResult`):**
227
+
228
+ | Field | Type | Description |
229
+ |-------|------|-------------|
230
+ | `pubkey` | `string` | The user's pubkey (64-character hex); a copy of `authEvent.pubkey` |
231
+ | `authEvent` | `SignetAuthEvent` | The verified, signed kind-21236 event: `id`, `pubkey`, `kind`, `created_at`, `tags`, `content`, `sig` |
232
+ | `credential` | `unknown` (optional) | Credential included in the response (Signet Login flows), passed through as received |
233
+ | `displayName` | `string` (optional) | Persona handle the user chose to share. Control and bidi characters are stripped and it is capped at 64 characters; still escape it when rendering |
234
+ | `bunkerUri` | `string` (optional) | A `bunker://` URI for upgrading to a live signer. Absent on auth-only flows |
235
+ | `createdAt` | `number` | Unix seconds at which the auth event was signed |
236
+
237
+ **Constants:** `AUTH_FRESHNESS_WINDOW_SEC` (`300`, seconds) and `DEFAULT_RELAY_URL` (`'wss://nos.lol'`).
238
+
164
239
  ## Age Range Values
165
240
 
166
241
  | Value | Meaning |
@@ -171,11 +246,12 @@ Returns `Promise<SignetVerifyResult>`
171
246
  | `'13-17'` | Ages 13–17 |
172
247
  | `'18+'` | Adult (18 and over) |
173
248
 
249
+ Matching is exact: the credential's age range must equal `requiredAgeRange`. An `'18+'` credential does not satisfy `'13-17'`, and a `'13-17'` credential does not satisfy `'18+'`. Any other `requiredAgeRange` is rejected: `verifyAge` resolves at once with `error: 'invalid-age-range'` and shows no modal.
250
+
174
251
  ## Privacy
175
252
 
176
253
  - No personal data is transmitted — only a cryptographic proof of age range
177
254
  - No account creation required on the website
178
255
  - No cookies beyond session management
179
- - The proof is verified entirely in the browser — no server-side API calls needed
180
- - Verifier check is a single GET request returning confirmed/not — no user data sent
181
- - Compliant with GDPR, COPPA, UK Online Safety Act, France SREN law
256
+ - Proof verification runs in the browser. `verifyAge` makes one network request, to the verification bot, unless `verifierCheckUrl` is `null`; it opens no relay connection, and the proof reaches the modal over a same-origin `BroadcastChannel`, not the network. `waitForAuthResponse` connects only to the relay you give it
257
+ - The verifier check is a single GET request that sends only the verifier's public key and returns confirmed or not
@@ -29,7 +29,7 @@ export interface SignetVerifyResult {
29
29
  export interface SignetVerifyOptions {
30
30
  /** Required age range to verify (e.g., '18+', '13-17') */
31
31
  requiredAgeRange: string;
32
- /** Relay URL for cross-device communication. Default: `DEFAULT_RELAY_URL` (wss://relay.trotters.cc). */
32
+ /** Relay URL for cross-device communication. Default: `DEFAULT_RELAY_URL` (wss://nos.lol). */
33
33
  relayUrl?: string;
34
34
  /** Callback URL for same-device flow */
35
35
  callbackUrl?: string;
@@ -72,7 +72,7 @@ export declare const AUTH_FRESHNESS_WINDOW_SEC = 300;
72
72
  * relay.damus.io, the previous default, refuses those reads and has AUTH
73
73
  * misconfigured, so nothing can fetch a response from it.
74
74
  */
75
- export declare const DEFAULT_RELAY_URL = "wss://relay.trotters.cc";
75
+ export declare const DEFAULT_RELAY_URL = "wss://nos.lol";
76
76
  /** Options for waiting on a cross-device Sign-in-with-Signet response. */
77
77
  export interface WaitForAuthOptions {
78
78
  /** The challenge (also the `requestId`) sent in the auth URL. 64-char hex. */
@@ -4,7 +4,7 @@
4
4
  <div id="signet-qr" style="display:flex;justify-content:center;margin-bottom:24px;"></div>
5
5
  <p style="margin:0 0 16px;color:${l};font-size:0.8rem;">Waiting for verification...</p>
6
6
  <button id="signet-cancel" style="background:none;border:1px solid ${l};color:${y};padding:10px 24px;border-radius:8px;cursor:pointer;font-size:0.9rem;">Cancel</button>
7
- `,document.body.appendChild(x),x.showModal();let A=x.querySelector("#signet-qr");if(A){let L=document.createElement("div");L.style.cssText=`width:200px;height:200px;background:${u?"#2a2a3e":"#f0f0f0"};border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:0.75rem;color:${l};word-break:break-all;padding:12px;`,L.textContent=`signet:verify:${c.slice(0,40)}...`,A.appendChild(L)}let B=new BroadcastChannel("signet-verify-"+o);x.querySelector("#signet-cancel")?.addEventListener("click",()=>{B.close(),x.close(),x.remove(),a.remove(),f({verified:!1,ageRange:null,tier:null,entityType:null,credentialId:null,verifierPubkey:null,verifierConfirmed:null,verifierMethod:null,issuedAt:null,expiresAt:null,error:"cancelled"})}),B.onmessage=async L=>{let D=L.data;if(typeof D!="object"||D===null)return;let P=D;if(P.type!=="signet-verify-response"||P.requestId!==o||!P.credential||typeof P.credential!="object"||!Array.isArray(P.credential.tags))return;let j=P.credential,G=await Ki(j),K=Gt(j.tags,"age-range"),z=Gt(j.tags,"tier"),Y=Gt(j.tags,"entity-type"),$=Gt(j.tags,"expires"),F=K?zi(K,n.requiredAgeRange):!1,M=await Yi(j.pubkey,n.verifierCheckUrl),C=M?.confirmed??null,H=M?.method??null,h=n.acceptUnconfirmed||C===!0;x.close(),x.remove(),a.remove(),B.close();let b=z?parseInt(z,10):null,v=$?parseInt($,10):null,S=Math.floor(Date.now()/1e3),O=v===null||!isNaN(v)&&v>S,p;G?O?F?h||(p=C===!1?"verifier-not-confirmed":"verifier-check-unavailable"):p="age-range-not-met":p="credential-expired":p="invalid-credential",f({verified:G&&O&&F&&h,ageRange:K||null,tier:b!==null&&!isNaN(b)?b:null,entityType:Y||null,credentialId:j.id,verifierPubkey:j.pubkey,verifierConfirmed:C,verifierMethod:H,issuedAt:j.created_at,expiresAt:v!==null&&!isNaN(v)?v:null,error:p})},setTimeout(()=>{x.close(),x.remove(),a.remove(),B.close(),f({verified:!1,ageRange:null,tier:null,entityType:null,credentialId:null,verifierPubkey:null,verifierConfirmed:null,verifierMethod:null,issuedAt:null,expiresAt:null,error:"timeout"})},n.timeout)})}var ot=300,Qo="wss://relay.trotters.cc",Xo=60;function Qn(t){let e=JSON.stringify([0,t.pubkey,t.created_at,t.kind,t.tags,t.content]),r=$e(new TextEncoder().encode(e));return Re(r)}async function Wi(t,e){try{let r=Xn(e,t.pubkey),n=Jn(t.content,r),o=JSON.parse(n);if(typeof o!="object"||o===null)return null;let s=o;if(s.kind!==13||typeof s.pubkey!="string"||!/^[0-9a-f]{64}$/i.test(s.pubkey)||typeof s.created_at!="number"||!Array.isArray(s.tags)||typeof s.content!="string"||typeof s.id!="string"||!/^[0-9a-f]{64}$/i.test(s.id)||typeof s.sig!="string"||!/^[0-9a-f]{128}$/i.test(s.sig)||Qn({pubkey:s.pubkey,created_at:s.created_at,kind:13,tags:s.tags,content:s.content})!==s.id.toLowerCase())return null;let c=Ae(s.sig),f=Ae(s.id),a=Ae(s.pubkey);if(!at.verify(c,f,a))return null;let u=Xn(e,s.pubkey),d=Jn(s.content,u),y=JSON.parse(d);if(typeof y!="object"||y===null)return null;let l=y;if(typeof l.pubkey!="string"||l.pubkey!==s.pubkey||typeof l.kind!="number"||typeof l.created_at!="number"||!Array.isArray(l.tags)||typeof l.content!="string")return null;let x=typeof l.id=="string"?l.id:Qn({pubkey:l.pubkey,created_at:l.created_at,kind:l.kind,tags:l.tags,content:l.content});return{pubkey:l.pubkey,id:x,kind:l.kind,created_at:l.created_at,tags:l.tags,content:l.content}}catch{return null}}function Gi(t){return es(t,64)}function es(t,e){if(typeof t!="string")return;let r=t.replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202e\u2066-\u2069]/g,"").trim().slice(0,e);return r.length>0?r:void 0}async function ts(t){let e=a=>{let u=new Error(a);return u.code=a,u};if(!/^[0-9a-f]{64}$/i.test(t.requestId))throw e("invalid-request-id");if(!(t.sessionPrivKey instanceof Uint8Array)||t.sessionPrivKey.length!==32)throw e("invalid-session-privkey");if(!/^wss:\/\//i.test(t.relayUrl)&&!/^ws:\/\/(localhost|127\.0\.0\.1)([:\/]|$)/i.test(t.relayUrl))throw e("invalid-relay-url");if(typeof t.expectedOrigin!="string"||t.expectedOrigin.length===0)throw e("invalid-expected-origin");if(Number.isFinite(t.issuedAt)&&t.issuedAt>Math.floor(Date.now()/1e3)+ot)throw e("invalid-issued-at");let r=Re(at.getPublicKey(t.sessionPrivKey)),n=Number.isFinite(t.issuedAt)?(Math.floor(t.issuedAt)+ot)*1e3-Date.now():12e4,o=Math.max(5e3,Math.min(t.timeout??n,6e5)),s=t.requestId.toLowerCase(),i=t.expectedOrigin,c=Number.isFinite(t.since)?Math.floor(t.since):Number.isFinite(t.issuedAt)?Math.floor(t.issuedAt)-Xo:Math.floor(Date.now()/1e3)-Xo,f=()=>Number.isFinite(t.issuedAt)&&Date.now()>=(Math.floor(t.issuedAt)+ot)*1e3-1e3;return new Promise((a,u)=>{let d=`sa-${Math.random().toString(36).slice(2,12)}`,y=!1,l=!1,x,A,B=Date.now()+o,L=F=>{if(!y){y=!0,clearTimeout(P),clearTimeout(A),typeof document<"u"&&document.removeEventListener("visibilitychange",G),t.abortSignal?.removeEventListener("abort",$);try{x?.close()}catch{}F()}},D=()=>L(()=>u(e(l||f()?"expired":"timeout"))),P=setTimeout(()=>{D()},o),j=()=>{!y&&x?.readyState===1&&x.send(JSON.stringify(["REQ",d,{kinds:[1059],"#p":[r],since:c}]))},G=()=>{if(!(y||document.visibilityState!=="visible")){if(Date.now()>=B)return D();!x||x.readyState>=2?Y():j()}};typeof document<"u"&&document.addEventListener("visibilitychange",G);let K=async F=>{if(y)return;let M;try{M=JSON.parse(typeof F.data=="string"?F.data:"")}catch{return}if(!Array.isArray(M))return;if(M[0]==="CLOSED"&&M[1]===d){let U=es(M[2],200);L(()=>{let V=e("relay-refused");U&&(V.reason=U),u(V)});return}if(M[0]!=="EVENT"||M[1]!==d)return;let C=M[2];if(typeof C!="object"||C===null)return;let H=C;if(H.kind!==1059||typeof H.pubkey!="string"||typeof H.content!="string")return;let h=await Wi({pubkey:H.pubkey,content:H.content},t.sessionPrivKey);if(!h||h.kind!==29999)return;let b=h.tags.find(U=>U[0]==="session");if(!b||b[1]!==s)return;let v=h.tags.find(U=>U[0]==="status");if(v?.[1]==="rejected"){L(()=>u(e("denied")));return}if(v?.[1]!=="approved")return;if(Math.abs(Date.now()/1e3-h.created_at)>ot){l=!0;return}let O;try{let U=JSON.parse(h.content);if(typeof U!="object"||U===null)return;O=U}catch{return}if(O.type!=="signet-auth-response"||O.requestId!==s||typeof O.authEvent!="object"||O.authEvent===null)return;let p=O.authEvent;if(typeof p.id!="string"||!/^[0-9a-f]{64}$/i.test(p.id)||typeof p.pubkey!="string"||!/^[0-9a-f]{64}$/i.test(p.pubkey)||typeof p.sig!="string"||!/^[0-9a-f]{128}$/i.test(p.sig)||p.kind!==21236||typeof p.created_at!="number"||!Array.isArray(p.tags)||typeof p.content!="string"||p.pubkey.toLowerCase()!==h.pubkey.toLowerCase()||Qn({pubkey:p.pubkey,created_at:p.created_at,kind:21236,tags:p.tags,content:p.content})!==p.id.toLowerCase())return;let w=!1;try{let U=Ae(p.sig),V=Ae(p.id),Z=Ae(p.pubkey);w=at.verify(U,V,Z)}catch{w=!1}if(!w)return;let g=p.tags,E=g.find(U=>Array.isArray(U)&&U[0]==="challenge");if(!E||typeof E[1]!="string"||E[1].toLowerCase()!==s)return;let I=g.find(U=>Array.isArray(U)&&U[0]==="origin");if(!I||I[1]!==i)return;if(Math.abs(Date.now()/1e3-p.created_at)>ot){l=!0;return}let T={id:p.id.toLowerCase(),pubkey:p.pubkey.toLowerCase(),kind:21236,created_at:p.created_at,tags:g,content:p.content,sig:p.sig},k=Gi(O.displayName),q=typeof O.bunkerUri=="string"&&/^bunker:\/\//i.test(O.bunkerUri)?O.bunkerUri:void 0;L(()=>a({pubkey:T.pubkey,authEvent:T,credential:O.credential,...k!==void 0?{displayName:k}:{},...q!==void 0?{bunkerUri:q}:{},createdAt:T.created_at}))},z=F=>{if(!y){if(typeof document>"u")return L(()=>u(e(F)));clearTimeout(A),document.visibilityState==="visible"&&(A=setTimeout(Y,1e3))}};function Y(){if(y)return;if(clearTimeout(A),Date.now()>=B)return D();let F=x;x=void 0;try{F?.close()}catch{}try{let M=new WebSocket(t.relayUrl);x=M,M.onopen=j,M.onmessage=K,M.onerror=()=>{x===M&&z("relay-error")},M.onclose=()=>{x===M&&z("relay-closed")}}catch{z("relay-error")}}let $=()=>L(()=>u(e("aborted")));if(t.abortSignal?.aborted){L(()=>u(e("aborted")));return}t.abortSignal?.addEventListener("abort",$,{once:!0}),Y()})}typeof window<"u"&&(window.Signet={verifyAge:Jo,waitForAuthResponse:ts});return fs(Xi);})();
7
+ `,document.body.appendChild(x),x.showModal();let A=x.querySelector("#signet-qr");if(A){let L=document.createElement("div");L.style.cssText=`width:200px;height:200px;background:${u?"#2a2a3e":"#f0f0f0"};border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:0.75rem;color:${l};word-break:break-all;padding:12px;`,L.textContent=`signet:verify:${c.slice(0,40)}...`,A.appendChild(L)}let B=new BroadcastChannel("signet-verify-"+o);x.querySelector("#signet-cancel")?.addEventListener("click",()=>{B.close(),x.close(),x.remove(),a.remove(),f({verified:!1,ageRange:null,tier:null,entityType:null,credentialId:null,verifierPubkey:null,verifierConfirmed:null,verifierMethod:null,issuedAt:null,expiresAt:null,error:"cancelled"})}),B.onmessage=async L=>{let D=L.data;if(typeof D!="object"||D===null)return;let P=D;if(P.type!=="signet-verify-response"||P.requestId!==o||!P.credential||typeof P.credential!="object"||!Array.isArray(P.credential.tags))return;let j=P.credential,G=await Ki(j),K=Gt(j.tags,"age-range"),z=Gt(j.tags,"tier"),Y=Gt(j.tags,"entity-type"),$=Gt(j.tags,"expires"),F=K?zi(K,n.requiredAgeRange):!1,M=await Yi(j.pubkey,n.verifierCheckUrl),C=M?.confirmed??null,H=M?.method??null,h=n.acceptUnconfirmed||C===!0;x.close(),x.remove(),a.remove(),B.close();let b=z?parseInt(z,10):null,v=$?parseInt($,10):null,S=Math.floor(Date.now()/1e3),O=v===null||!isNaN(v)&&v>S,p;G?O?F?h||(p=C===!1?"verifier-not-confirmed":"verifier-check-unavailable"):p="age-range-not-met":p="credential-expired":p="invalid-credential",f({verified:G&&O&&F&&h,ageRange:K||null,tier:b!==null&&!isNaN(b)?b:null,entityType:Y||null,credentialId:j.id,verifierPubkey:j.pubkey,verifierConfirmed:C,verifierMethod:H,issuedAt:j.created_at,expiresAt:v!==null&&!isNaN(v)?v:null,error:p})},setTimeout(()=>{x.close(),x.remove(),a.remove(),B.close(),f({verified:!1,ageRange:null,tier:null,entityType:null,credentialId:null,verifierPubkey:null,verifierConfirmed:null,verifierMethod:null,issuedAt:null,expiresAt:null,error:"timeout"})},n.timeout)})}var ot=300,Qo="wss://nos.lol",Xo=60;function Qn(t){let e=JSON.stringify([0,t.pubkey,t.created_at,t.kind,t.tags,t.content]),r=$e(new TextEncoder().encode(e));return Re(r)}async function Wi(t,e){try{let r=Xn(e,t.pubkey),n=Jn(t.content,r),o=JSON.parse(n);if(typeof o!="object"||o===null)return null;let s=o;if(s.kind!==13||typeof s.pubkey!="string"||!/^[0-9a-f]{64}$/i.test(s.pubkey)||typeof s.created_at!="number"||!Array.isArray(s.tags)||typeof s.content!="string"||typeof s.id!="string"||!/^[0-9a-f]{64}$/i.test(s.id)||typeof s.sig!="string"||!/^[0-9a-f]{128}$/i.test(s.sig)||Qn({pubkey:s.pubkey,created_at:s.created_at,kind:13,tags:s.tags,content:s.content})!==s.id.toLowerCase())return null;let c=Ae(s.sig),f=Ae(s.id),a=Ae(s.pubkey);if(!at.verify(c,f,a))return null;let u=Xn(e,s.pubkey),d=Jn(s.content,u),y=JSON.parse(d);if(typeof y!="object"||y===null)return null;let l=y;if(typeof l.pubkey!="string"||l.pubkey!==s.pubkey||typeof l.kind!="number"||typeof l.created_at!="number"||!Array.isArray(l.tags)||typeof l.content!="string")return null;let x=typeof l.id=="string"?l.id:Qn({pubkey:l.pubkey,created_at:l.created_at,kind:l.kind,tags:l.tags,content:l.content});return{pubkey:l.pubkey,id:x,kind:l.kind,created_at:l.created_at,tags:l.tags,content:l.content}}catch{return null}}function Gi(t){return es(t,64)}function es(t,e){if(typeof t!="string")return;let r=t.replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202e\u2066-\u2069]/g,"").trim().slice(0,e);return r.length>0?r:void 0}async function ts(t){let e=a=>{let u=new Error(a);return u.code=a,u};if(!/^[0-9a-f]{64}$/i.test(t.requestId))throw e("invalid-request-id");if(!(t.sessionPrivKey instanceof Uint8Array)||t.sessionPrivKey.length!==32)throw e("invalid-session-privkey");if(!/^wss:\/\//i.test(t.relayUrl)&&!/^ws:\/\/(localhost|127\.0\.0\.1)([:\/]|$)/i.test(t.relayUrl))throw e("invalid-relay-url");if(typeof t.expectedOrigin!="string"||t.expectedOrigin.length===0)throw e("invalid-expected-origin");if(Number.isFinite(t.issuedAt)&&t.issuedAt>Math.floor(Date.now()/1e3)+ot)throw e("invalid-issued-at");let r=Re(at.getPublicKey(t.sessionPrivKey)),n=Number.isFinite(t.issuedAt)?(Math.floor(t.issuedAt)+ot)*1e3-Date.now():12e4,o=Math.max(5e3,Math.min(t.timeout??n,6e5)),s=t.requestId.toLowerCase(),i=t.expectedOrigin,c=Number.isFinite(t.since)?Math.floor(t.since):Number.isFinite(t.issuedAt)?Math.floor(t.issuedAt)-Xo:Math.floor(Date.now()/1e3)-Xo,f=()=>Number.isFinite(t.issuedAt)&&Date.now()>=(Math.floor(t.issuedAt)+ot)*1e3-1e3;return new Promise((a,u)=>{let d=`sa-${Math.random().toString(36).slice(2,12)}`,y=!1,l=!1,x,A,B=Date.now()+o,L=F=>{if(!y){y=!0,clearTimeout(P),clearTimeout(A),typeof document<"u"&&document.removeEventListener("visibilitychange",G),t.abortSignal?.removeEventListener("abort",$);try{x?.close()}catch{}F()}},D=()=>L(()=>u(e(l||f()?"expired":"timeout"))),P=setTimeout(()=>{D()},o),j=()=>{!y&&x?.readyState===1&&x.send(JSON.stringify(["REQ",d,{kinds:[1059],"#p":[r],since:c}]))},G=()=>{if(!(y||document.visibilityState!=="visible")){if(Date.now()>=B)return D();!x||x.readyState>=2?Y():j()}};typeof document<"u"&&document.addEventListener("visibilitychange",G);let K=async F=>{if(y)return;let M;try{M=JSON.parse(typeof F.data=="string"?F.data:"")}catch{return}if(!Array.isArray(M))return;if(M[0]==="CLOSED"&&M[1]===d){let U=es(M[2],200);L(()=>{let V=e("relay-refused");U&&(V.reason=U),u(V)});return}if(M[0]!=="EVENT"||M[1]!==d)return;let C=M[2];if(typeof C!="object"||C===null)return;let H=C;if(H.kind!==1059||typeof H.pubkey!="string"||typeof H.content!="string")return;let h=await Wi({pubkey:H.pubkey,content:H.content},t.sessionPrivKey);if(!h||h.kind!==29999)return;let b=h.tags.find(U=>U[0]==="session");if(!b||b[1]!==s)return;let v=h.tags.find(U=>U[0]==="status");if(v?.[1]==="rejected"){L(()=>u(e("denied")));return}if(v?.[1]!=="approved")return;if(Math.abs(Date.now()/1e3-h.created_at)>ot){l=!0;return}let O;try{let U=JSON.parse(h.content);if(typeof U!="object"||U===null)return;O=U}catch{return}if(O.type!=="signet-auth-response"||O.requestId!==s||typeof O.authEvent!="object"||O.authEvent===null)return;let p=O.authEvent;if(typeof p.id!="string"||!/^[0-9a-f]{64}$/i.test(p.id)||typeof p.pubkey!="string"||!/^[0-9a-f]{64}$/i.test(p.pubkey)||typeof p.sig!="string"||!/^[0-9a-f]{128}$/i.test(p.sig)||p.kind!==21236||typeof p.created_at!="number"||!Array.isArray(p.tags)||typeof p.content!="string"||p.pubkey.toLowerCase()!==h.pubkey.toLowerCase()||Qn({pubkey:p.pubkey,created_at:p.created_at,kind:21236,tags:p.tags,content:p.content})!==p.id.toLowerCase())return;let w=!1;try{let U=Ae(p.sig),V=Ae(p.id),Z=Ae(p.pubkey);w=at.verify(U,V,Z)}catch{w=!1}if(!w)return;let g=p.tags,E=g.find(U=>Array.isArray(U)&&U[0]==="challenge");if(!E||typeof E[1]!="string"||E[1].toLowerCase()!==s)return;let I=g.find(U=>Array.isArray(U)&&U[0]==="origin");if(!I||I[1]!==i)return;if(Math.abs(Date.now()/1e3-p.created_at)>ot){l=!0;return}let T={id:p.id.toLowerCase(),pubkey:p.pubkey.toLowerCase(),kind:21236,created_at:p.created_at,tags:g,content:p.content,sig:p.sig},k=Gi(O.displayName),q=typeof O.bunkerUri=="string"&&/^bunker:\/\//i.test(O.bunkerUri)?O.bunkerUri:void 0;L(()=>a({pubkey:T.pubkey,authEvent:T,credential:O.credential,...k!==void 0?{displayName:k}:{},...q!==void 0?{bunkerUri:q}:{},createdAt:T.created_at}))},z=F=>{if(!y){if(typeof document>"u")return L(()=>u(e(F)));clearTimeout(A),document.visibilityState==="visible"&&(A=setTimeout(Y,1e3))}};function Y(){if(y)return;if(clearTimeout(A),Date.now()>=B)return D();let F=x;x=void 0;try{F?.close()}catch{}try{let M=new WebSocket(t.relayUrl);x=M,M.onopen=j,M.onmessage=K,M.onerror=()=>{x===M&&z("relay-error")},M.onclose=()=>{x===M&&z("relay-closed")}}catch{z("relay-error")}}let $=()=>L(()=>u(e("aborted")));if(t.abortSignal?.aborted){L(()=>u(e("aborted")));return}t.abortSignal?.addEventListener("abort",$,{once:!0}),Y()})}typeof window<"u"&&(window.Signet={verifyAge:Jo,waitForAuthResponse:ts});return fs(Xi);})();
8
8
  /*! Bundled license information:
9
9
 
10
10
  @noble/curves/utils.js:
@@ -299,7 +299,7 @@ export const AUTH_FRESHNESS_WINDOW_SEC = 300;
299
299
  * relay.damus.io, the previous default, refuses those reads and has AUTH
300
300
  * misconfigured, so nothing can fetch a response from it.
301
301
  */
302
- export const DEFAULT_RELAY_URL = 'wss://relay.trotters.cc';
302
+ export const DEFAULT_RELAY_URL = 'wss://nos.lol';
303
303
  /**
304
304
  * Slack subtracted from `issuedAt` when deriving the relay query window, to
305
305
  * absorb clock skew between the consumer and the signer device. Widening the
package/llms.txt ADDED
@@ -0,0 +1,80 @@
1
+ # Signet Verify
2
+
3
+ > Age verification on a website usually means collecting ID or sending users
4
+ > off to a third-party checker. signet-verify is a browser SDK that checks a
5
+ > cryptographic age credential from the Signet app instead, without the site
6
+ > ever seeing personal data.
7
+
8
+ For websites that need to gate content by age range (adult content, age
9
+ brackets for minors) without running their own verification flow. A single
10
+ function call opens a modal, waits for a signed response on a same-origin
11
+ `BroadcastChannel` (no relay connection), and checks both the credential and
12
+ the issuing verifier's professional registration.
13
+
14
+ ## Getting Started
15
+
16
+ ```bash
17
+ npm install signet-verify
18
+ ```
19
+
20
+ ```javascript
21
+ import { verifyAge } from 'signet-verify';
22
+
23
+ const result = await verifyAge('18+');
24
+ if (result.verified) {
25
+ // user confirmed 18+ by a confirmed professional verifier
26
+ }
27
+ ```
28
+
29
+ For a script tag / CDN build, use the IIFE export (`signet-verify/iife`),
30
+ which exposes `window.Signet`, for example
31
+ `https://cdn.jsdelivr.net/npm/signet-verify@0.7.1/dist/signet-verify.iife.js`.
32
+
33
+ ## Key Concepts
34
+
35
+ - **Credential, not identity**: the proof carries an age range and verifier
36
+ info, never a name, date of birth or other PII.
37
+ - **Verifier confirmation**: by default a result only counts as `verified`
38
+ when the issuing verifier is checked against a public professional
39
+ register (GMC, SRA, TRA, etc.) via a verification bot; this can be relaxed
40
+ with `acceptUnconfirmed` or pointed at a different bot with
41
+ `verifierCheckUrl`.
42
+ - **Same-browser vs cross-device**: `verifyAge` covers the modal flow and
43
+ receives its response on a `BroadcastChannel` named
44
+ `signet-verify-<requestId>`; it makes one network request, to the
45
+ verification bot, and never connects to a relay (its `relayUrl` option is
46
+ only written into the request object). `waitForAuthResponse` is the
47
+ lower-level cross-device primitive for a consumer that mints its own
48
+ challenge and waits on a relay for the signed response; only it needs a
49
+ relay that serves kind-1059 gift wraps to an unauthenticated reader.
50
+ - **The sign-in deadline**: a cross-device wait has one deadline,
51
+ `issuedAt + AUTH_FRESHNESS_WINDOW_SEC`. `issuedAt` must be passed on every
52
+ call for the same sign-in, including retries after the page was
53
+ backgrounded, or the relay query anchor is wrong.
54
+
55
+ ## API Surface
56
+
57
+ - `verifyAge(requiredAgeRange, options?) => Promise<SignetVerifyResult>`:
58
+ the same-browser modal flow. `requiredAgeRange` must be one of
59
+ `'0-3'`, `'4-7'`, `'8-12'`, `'13-17'`, `'18+'` and is matched exactly.
60
+ Failures are reported in `result.error`, not as a rejection.
61
+ - `waitForAuthResponse(options) => Promise<SignetAuthResult>`: the
62
+ cross-device primitive. It resolves on success and rejects with an `Error`
63
+ whose `message` and `code` are the failure code (`expired`, `timeout`,
64
+ `denied`, `aborted`, `relay-refused`, and others listed in the README).
65
+ Options: `requestId`, `relayUrl`, `sessionPrivKey` (a 32-byte
66
+ `Uint8Array`) and `expectedOrigin` are required; `issuedAt` (unix
67
+ seconds), `timeout` (milliseconds), `since` (unix seconds) and
68
+ `abortSignal` are optional. The README has the full option and result
69
+ tables.
70
+ - `AUTH_FRESHNESS_WINDOW_SEC` (`300`, seconds), `DEFAULT_RELAY_URL`
71
+ (`'wss://nos.lol'`): constants used to compute and check the sign-in
72
+ deadline.
73
+ - Result and option types: `SignetVerifyResult`, `SignetVerifyOptions`,
74
+ `WaitForAuthOptions`, `SignetAuthResult`, `SignetAuthEvent`.
75
+
76
+ ## Optional
77
+
78
+ - Full API reference, error codes and age range values: [README](README.md)
79
+ - Cross-device example: [examples/cross-device-login.html](https://github.com/forgesworn/signet-verify/blob/main/examples/cross-device-login.html)
80
+ - Release history: [CHANGELOG.md](CHANGELOG.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signet-verify",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Signet SDK — website age verification and cross-device Sign-in-with-Signet",
5
5
  "type": "module",
6
6
  "main": "./dist/signet-verify.js",
@@ -14,7 +14,26 @@
14
14
  "./iife": "./dist/signet-verify.iife.js"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "llms.txt",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "homepage": "https://github.com/forgesworn/signet-verify#readme",
22
+ "keywords": [
23
+ "signet",
24
+ "age-verification",
25
+ "age-gate",
26
+ "nostr",
27
+ "credential",
28
+ "verifiable-credential",
29
+ "cross-device",
30
+ "sign-in",
31
+ "authentication",
32
+ "privacy",
33
+ "gdpr",
34
+ "compliance",
35
+ "sdk",
36
+ "browser"
18
37
  ],
19
38
  "publishConfig": {
20
39
  "provenance": true