signet-verify 0.7.0 → 0.7.1
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/README.md +8 -2
- package/dist/signet-verify.d.ts +2 -2
- package/dist/signet-verify.iife.js +1 -1
- package/dist/signet-verify.js +1 -1
- package/llms.txt +67 -0
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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
|
|
|
7
13
|
```html
|
|
@@ -111,7 +117,7 @@ again. This was a real, 100%-reproducible failure before 0.5.2.
|
|
|
111
117
|
| `denied` | The user rejected the request | Nothing; respect it |
|
|
112
118
|
| `aborted` | You aborted via `abortSignal` | Nothing |
|
|
113
119
|
| `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; `
|
|
120
|
+
| `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 |
|
|
115
121
|
| `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** |
|
|
116
122
|
|
|
117
123
|
The code is on both `err.message` and `err.code` — branch on either.
|
|
@@ -129,7 +135,7 @@ Returns `Promise<SignetVerifyResult>`
|
|
|
129
135
|
|
|
130
136
|
| Option | Type | Default | Description |
|
|
131
137
|
|--------|------|---------|-------------|
|
|
132
|
-
| `relayUrl` | `string` | `DEFAULT_RELAY_URL` (`wss://
|
|
138
|
+
| `relayUrl` | `string` | `DEFAULT_RELAY_URL` (`wss://nos.lol`) | Relay URL for cross-device communication. Must serve kind-1059 gift wraps to an unauthenticated reader |
|
|
133
139
|
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Modal colour scheme |
|
|
134
140
|
| `timeout` | `number` | `120000` | Timeout in milliseconds |
|
|
135
141
|
| `verifierCheckUrl` | `string \| null` | `'https://verify.signet.forgesworn.dev'` | Verification bot URL. Set to `null` to skip. |
|
package/dist/signet-verify.d.ts
CHANGED
|
@@ -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://
|
|
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://
|
|
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://
|
|
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:
|
package/dist/signet-verify.js
CHANGED
|
@@ -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://
|
|
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,67 @@
|
|
|
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 QR/modal, waits for the user's Signet app to respond,
|
|
11
|
+
and checks both the credential and the issuing verifier's professional
|
|
12
|
+
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`.
|
|
31
|
+
|
|
32
|
+
## Key Concepts
|
|
33
|
+
|
|
34
|
+
- **Credential, not identity**: the proof carries an age range and verifier
|
|
35
|
+
info, never a name, date of birth or other PII.
|
|
36
|
+
- **Verifier confirmation**: by default a result only counts as `verified`
|
|
37
|
+
when the issuing verifier is checked against a public professional
|
|
38
|
+
register (GMC, SRA, TRA, etc.) via a verification bot; this can be relaxed
|
|
39
|
+
with `acceptUnconfirmed` or pointed at a different bot with
|
|
40
|
+
`verifierCheckUrl`.
|
|
41
|
+
- **Same-device vs cross-device**: `verifyAge` covers the common same-device
|
|
42
|
+
QR/modal flow; `waitForAuthResponse` is the lower-level cross-device
|
|
43
|
+
primitive for a consumer that mints its own challenge and waits on a relay
|
|
44
|
+
for the signed response.
|
|
45
|
+
- **The sign-in deadline**: a cross-device wait has one deadline,
|
|
46
|
+
`issuedAt + AUTH_FRESHNESS_WINDOW_SEC`. `issuedAt` must be passed on every
|
|
47
|
+
call for the same sign-in, including retries after the page was
|
|
48
|
+
backgrounded, or the relay query anchor is wrong.
|
|
49
|
+
|
|
50
|
+
## API Surface
|
|
51
|
+
|
|
52
|
+
- `verifyAge(requiredAgeRange, options?) => Promise<SignetVerifyResult>`:
|
|
53
|
+
the same-device verification flow.
|
|
54
|
+
- `waitForAuthResponse(options) => Promise<SignetAuthResult>`: the
|
|
55
|
+
cross-device primitive; see the README for the full option set
|
|
56
|
+
(`requestId`, `relayUrl`, `sessionPrivKey`, `expectedOrigin`, `issuedAt`,
|
|
57
|
+
`abortSignal`).
|
|
58
|
+
- `AUTH_FRESHNESS_WINDOW_SEC`, `DEFAULT_RELAY_URL`: constants used to
|
|
59
|
+
compute and check the sign-in deadline.
|
|
60
|
+
- Result and option types: `SignetVerifyResult`, `SignetVerifyOptions`,
|
|
61
|
+
`WaitForAuthOptions`, `SignetAuthResult`, `SignetAuthEvent`.
|
|
62
|
+
|
|
63
|
+
## Optional
|
|
64
|
+
|
|
65
|
+
- Full API reference, error codes and age range values: [README](README.md)
|
|
66
|
+
- Cross-device example: [examples/cross-device-login.html](examples/cross-device-login.html)
|
|
67
|
+
- 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.
|
|
3
|
+
"version": "0.7.1",
|
|
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,25 @@
|
|
|
14
14
|
"./iife": "./dist/signet-verify.iife.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"llms.txt"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://github.com/forgesworn/signet-verify#readme",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"signet",
|
|
23
|
+
"age-verification",
|
|
24
|
+
"age-gate",
|
|
25
|
+
"nostr",
|
|
26
|
+
"credential",
|
|
27
|
+
"verifiable-credential",
|
|
28
|
+
"cross-device",
|
|
29
|
+
"sign-in",
|
|
30
|
+
"authentication",
|
|
31
|
+
"privacy",
|
|
32
|
+
"gdpr",
|
|
33
|
+
"compliance",
|
|
34
|
+
"sdk",
|
|
35
|
+
"browser"
|
|
18
36
|
],
|
|
19
37
|
"publishConfig": {
|
|
20
38
|
"provenance": true
|