@chaindaddy/js 0.1.0 → 0.2.0
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/dist/api.d.ts +5038 -0
- package/dist/api.js +0 -0
- package/dist/cd.js +14 -2
- package/dist/cd.js.sri +1 -1
- package/dist/embed.d.ts +8 -0
- package/dist/embed.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +188 -1
- package/dist/verify.d.ts +126 -0
- package/dist/verify.js +342 -0
- package/package.json +15 -2
package/dist/api.js
ADDED
|
File without changes
|
package/dist/cd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
1
|
+
"use strict";(()=>{var w="0.2.0";function b(t){let e="";for(let i of t)e+=String.fromCharCode(i);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function g(){return b(crypto.getRandomValues(new Uint8Array(32)))}function v(t,e,i,s){let c=a=>{if(a&&typeof a=="object"&&a.type===t)try{i(a)}catch{}},u=a=>{a.origin===e&&c(a.data)};window.addEventListener("message",u);let d=null;if(s&&typeof BroadcastChannel=="function"&&e===location.origin)try{d=new BroadcastChannel(s),d.onmessage=a=>c(a.data)}catch{d=null}return()=>{window.removeEventListener("message",u),d?.close()}}var h={get(t){try{return sessionStorage.getItem(t)}catch{return null}},set(t,e){try{sessionStorage.setItem(t,e)}catch{}},remove(t){try{sessionStorage.removeItem(t)}catch{}}};var A="https://api.chaindaddy.io",T={"https://api.chaindaddy.io":"https://chaindaddy.io/_oauth/authorize","https://staging-api.chaindaddy.io":"https://staging.chaindaddy.io/_oauth/authorize"},p="cd-signin",E="cd:signin",m="cd.signin";async function U(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return b(new Uint8Array(e))}function M(t="openid"){let e=t.split(/\s+/).filter(Boolean);return(e.includes("openid")?e:["openid",...e]).join(" ")}async function L(t,e,i){let s=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:M(t.scope),state:e.state,nonce:e.nonce,code_challenge:await U(e.verifier),code_challenge_method:"S256"});return t.claims&&s.set("claims",typeof t.claims=="string"?t.claims:JSON.stringify(t.claims)),`${i}?${s}`}function x(t,e){return{code:e,codeVerifier:t.verifier,state:t.state,nonce:t.nonce,issuer:t.issuer,clientId:t.clientId,redirectUri:t.redirectUri}}function f(t){let e=(t.issuer??A).replace(/\/$/,""),i=t.authorizeUrl??T[e];if(!i)return Promise.reject(new Error("authorizeUrl is required for this issuer"));if(!t.clientId||!t.redirectUri)return Promise.reject(new Error("clientId and redirectUri are required"));let s={state:g(),nonce:g(),verifier:g(),issuer:e,clientId:t.clientId,redirectUri:t.redirectUri};h.set(m,JSON.stringify(s));let c=window.open("",p,"popup,width=480,height=720");return(async()=>{let u=await L(t,s,i);if(!c)return location.assign(u),null;c.location.href=u;let d=new URL(t.redirectUri,location.href).origin;return new Promise((a,n)=>{let o=v(E,d,r=>{r.state===s.state&&(o(),h.remove(m),typeof r.error=="string"&&r.error?n(new Error(r.error)):r.iss!==e?n(new Error("the answer came from another issuer")):typeof r.code!="string"||!r.code?n(new Error("the answer carried no code")):a(x(s,r.code)))},p);t.signal?.addEventListener("abort",()=>{o(),n(new Error("aborted"))})})})()}async function S(){let t=new URLSearchParams(location.search),e={type:E,code:t.get("code"),state:t.get("state"),iss:t.get("iss"),error:t.get("error")};if(window.name===p){try{window.opener?.postMessage(e,location.origin)}catch{}try{let c=new BroadcastChannel(p);c.postMessage(e),c.close()}catch{}return window.close(),null}let i=h.get(m);h.remove(m);let s=i?JSON.parse(i):null;if(!s||e.state!==s.state)throw new Error("the sign-in state does not match: start again");if(e.error)throw new Error(e.error);if(e.iss!==s.issuer)throw new Error("the answer came from another issuer");if(!e.code)throw new Error("the answer carried no code");return x(s,e.code)}async function k(t){let e=await fetch(`${t.issuer}/api/v2/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:t.codeVerifier})}),i=await e.json();if(!e.ok)throw new Error(`${i.error??e.status}: ${i.error_description??""}`.trim());return i}var O='<svg viewBox="0 0 24 24" fill="#FFD700" width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z" stroke="#B8860B" stroke-width="0.5"/><path d="M5 18h14v2H5v-2z" stroke="#B8860B" stroke-width="0.5"/></svg>',P=`
|
|
2
2
|
:host { display: inline-block; }
|
|
3
3
|
button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; box-sizing: border-box;
|
|
4
4
|
min-height: 40px; padding: 0 16px; border-radius: 10px; font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
@@ -7,4 +7,16 @@ button[data-theme="dark"] { background: #101828; color: #ffffff; border-color: #
|
|
|
7
7
|
button:focus-visible { outline: 2px solid #f59e0b; outline-offset: 2px; }
|
|
8
8
|
button:disabled { opacity: .6; cursor: default; }
|
|
9
9
|
svg { flex: none; }
|
|
10
|
-
`;function I(){if(typeof customElements>"u"||customElements.get("cd-signin-button"))return;class
|
|
10
|
+
`;function I(){if(typeof customElements>"u"||customElements.get("cd-signin-button"))return;class t extends HTMLElement{constructor(){super(),this.root=this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(){this.isConnected&&this.render()}render(){let i=this.getAttribute("text")==="continue"?"Continue with Chain Daddy":"Sign in with Chain Daddy",s=this.getAttribute("theme")==="dark"?"dark":"light";this.root.innerHTML=`<style>${P}</style><button type="button" part="button" data-theme="${s}">${O}<span></span></button>`;let c=this.root.querySelector("button");c.querySelector("span").textContent=i,c.addEventListener("click",()=>this.start())}start(){let i=s=>this.getAttribute(s)||void 0;f({clientId:i("client-id")??"",redirectUri:i("redirect-uri")??"",scope:i("scope"),claims:i("claims"),issuer:i("issuer"),authorizeUrl:i("authorize-url")}).then(s=>s&&this.dispatchEvent(new CustomEvent("cd-signin",{detail:s,bubbles:!0,composed:!0})),s=>this.dispatchEvent(new CustomEvent("cd-signin-error",{detail:{message:String(s?.message??s)},bubbles:!0,composed:!0})))}}t.observedAttributes=["theme","text"],customElements.define("cd-signin-button",t)}var C="https://embed.chaindaddy.io",y="https://embed-staging.chaindaddy.io";var H=["--cd-accent","--cd-surface","--cd-text","--cd-text-muted","--cd-border"];function _(t){try{if(t&&new URL(t).hostname==="js.staging.chaindaddy.io")return y}catch{}return C}var D=`
|
|
11
|
+
:host { display: block; }
|
|
12
|
+
iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
|
|
13
|
+
:host([data-cd-ready]) iframe { opacity: 1; }
|
|
14
|
+
:host([data-cd-ready]) slot { display: none; }
|
|
15
|
+
:host([data-cd-unavailable]) iframe { display: none; }
|
|
16
|
+
`,z=`
|
|
17
|
+
:host { display: inline-block; }
|
|
18
|
+
button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
|
|
19
|
+
font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
20
|
+
border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
|
|
21
|
+
button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
|
|
22
|
+
`;function R(t={}){try{if(typeof customElements>"u"||typeof HTMLElement>"u")return;let e=t.origin===y?y:C;class i extends HTMLElement{constructor(){super(...arguments);this.kind="app";this.frame=null;this.onMessage=n=>this.receive(n)}connectedCallback(){try{this.shadowRoot||this.attachShadow({mode:"open"}),this.mount()}catch{this.unavailable()}}disconnectedCallback(){try{window.removeEventListener("message",this.onMessage),clearTimeout(this.timer)}catch{}}attributeChangedCallback(){try{this.isConnected&&this.shadowRoot&&this.mount()}catch{this.unavailable()}}src(){let n=(this.getAttribute("crown")??"").trim(),o=(this.getAttribute("chain")??"").trim(),r=(this.getAttribute("app")??"").trim();if(!/^[0-9]{1,18}$/.test(n)||!o||this.kind==="app"&&!r)return null;let l=new URLSearchParams({crown:n,chain:o});return this.kind==="app"?l.set("app",r):l.set("kind","coin"),l.set("mode",this.mode()),l.set("v","1"),`${e}/v1/frame?${l.toString()}`}mode(){let n=this.getAttribute("mode");if(n==="light"||n==="dark")return n;try{return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch{return"light"}}mount(){let n=this.shadowRoot;window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.removeAttribute("data-cd-unavailable");let o=this.src();if(!o){n.innerHTML="<slot></slot>",this.frame=null;return}n.innerHTML=`<style>${D}</style><slot></slot>`;let r=document.createElement("iframe");r.setAttribute("loading","lazy"),r.setAttribute("referrerpolicy","strict-origin-when-cross-origin"),r.setAttribute("allow","clipboard-write; fullscreen"),r.setAttribute("title",this.kind==="coin"?"Coin on Chain Daddy":"App on Chain Daddy"),r.src=o,n.appendChild(r),this.frame=r,window.addEventListener("message",this.onMessage),this.timer=setTimeout(()=>{this.hasAttribute("data-cd-ready")||this.unavailable()},8e3)}receive(n){try{if(n.origin!==e||!this.frame||!n.source||n.source!==this.frame.contentWindow)return;let o=n.data;if(!o||typeof o!="object")return;if(o.type==="cd:ready")clearTimeout(this.timer),this.removeAttribute("data-cd-unavailable"),this.setAttribute("data-cd-ready",""),this.sendTheme(),this.dispatchEvent(new CustomEvent("cd:ready"));else if(o.type==="cd:resize"){let r=Number(o.height);Number.isFinite(r)&&(this.frame.style.height=`${Math.max(1,Math.min(4e3,Math.ceil(r)))}px`)}else o.type==="cd:unavailable"&&this.unavailable()}catch{}}sendTheme(){let n={},o=getComputedStyle(this);for(let r of H){let l=o.getPropertyValue(r).trim();l&&(n[r]=l)}this.frame?.contentWindow?.postMessage({type:"cd:theme",tokens:n},e)}unavailable(){try{if(clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.hasAttribute("data-cd-unavailable"))return;this.setAttribute("data-cd-unavailable",""),this.dispatchEvent(new CustomEvent("cd:unavailable"))}catch{}}}i.observedAttributes=["crown","chain","app","mode"];class s extends i{}class c extends i{constructor(){super(...arguments);this.kind="coin"}}class u extends HTMLElement{connectedCallback(){try{let a=this.querySelector("a[href]"),n=a?new URL(a.getAttribute("href")??"",document.baseURI):null;if(!n||n.protocol!=="https:")return;let o=this.shadowRoot??this.attachShadow({mode:"open"});o.innerHTML=`<style>${z}</style><button type="button" part="button"></button>`;let r=o.querySelector("button");r.textContent=(a.textContent??"").trim()||"Buy on Chain Daddy",r.addEventListener("click",()=>{try{window.open(n.toString(),"cd-buy","popup,width=460,height=760")||window.location.assign(n.toString())}catch{}})}catch{}}}for(let[d,a]of[["cd-app",s],["cd-coin",c],["cd-buy",u]])customElements.get(d)||customElements.define(d,a)}catch{}}var N={version:w,signIn:f,handleCallback:S,exchangeCode:k};window.ChainDaddy=Object.assign(window.ChainDaddy??{},N);I();R({origin:_(document.currentScript?.src)});})();
|
package/dist/cd.js.sri
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-KKpjQXvfR3Rs+FsdOW+hfEJcZpfuYVXR+TcyRX0nbup+s/xRhLx102TTXL29eFE1
|
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The embed origin for a build loaded from scriptSrc: staging's CDN frames staging, anything else prod. */
|
|
2
|
+
export declare function embedOriginFor(scriptSrc?: string | null): string;
|
|
3
|
+
export interface EmbedOptions {
|
|
4
|
+
/** The embed origin. Only the two Chain Daddy embed origins are taken; anything else is prod. */
|
|
5
|
+
origin?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Defines <cd-app>, <cd-coin> and <cd-buy>, each once. cd.js calls it for you. */
|
|
8
|
+
export declare function defineEmbedElements(options?: EmbedOptions): void;
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/embed.ts
|
|
2
|
+
var PROD_ORIGIN = "https://embed.chaindaddy.io";
|
|
3
|
+
var STAGING_ORIGIN = "https://embed-staging.chaindaddy.io";
|
|
4
|
+
var READY_TIMEOUT_MS = 8e3;
|
|
5
|
+
var MAX_HEIGHT = 4e3;
|
|
6
|
+
var THEME_TOKENS = ["--cd-accent", "--cd-surface", "--cd-text", "--cd-text-muted", "--cd-border"];
|
|
7
|
+
function embedOriginFor(scriptSrc) {
|
|
8
|
+
try {
|
|
9
|
+
if (scriptSrc && new URL(scriptSrc).hostname === "js.staging.chaindaddy.io") return STAGING_ORIGIN;
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
return PROD_ORIGIN;
|
|
13
|
+
}
|
|
14
|
+
var STYLE = `
|
|
15
|
+
:host { display: block; }
|
|
16
|
+
iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
|
|
17
|
+
:host([data-cd-ready]) iframe { opacity: 1; }
|
|
18
|
+
:host([data-cd-ready]) slot { display: none; }
|
|
19
|
+
:host([data-cd-unavailable]) iframe { display: none; }
|
|
20
|
+
`;
|
|
21
|
+
var BUY_STYLE = `
|
|
22
|
+
:host { display: inline-block; }
|
|
23
|
+
button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
|
|
24
|
+
font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
25
|
+
border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
|
|
26
|
+
button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
|
|
27
|
+
`;
|
|
28
|
+
function defineEmbedElements(options = {}) {
|
|
29
|
+
try {
|
|
30
|
+
if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") return;
|
|
31
|
+
const origin = options.origin === STAGING_ORIGIN ? STAGING_ORIGIN : PROD_ORIGIN;
|
|
32
|
+
class CdFrame extends HTMLElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
this.kind = "app";
|
|
36
|
+
this.frame = null;
|
|
37
|
+
this.onMessage = (event) => this.receive(event);
|
|
38
|
+
}
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
try {
|
|
41
|
+
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
42
|
+
this.mount();
|
|
43
|
+
} catch {
|
|
44
|
+
this.unavailable();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
try {
|
|
49
|
+
window.removeEventListener("message", this.onMessage);
|
|
50
|
+
clearTimeout(this.timer);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
attributeChangedCallback() {
|
|
55
|
+
try {
|
|
56
|
+
if (this.isConnected && this.shadowRoot) this.mount();
|
|
57
|
+
} catch {
|
|
58
|
+
this.unavailable();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The frame's address, or null when the element does not name enough. */
|
|
62
|
+
src() {
|
|
63
|
+
const crown = (this.getAttribute("crown") ?? "").trim();
|
|
64
|
+
const chain = (this.getAttribute("chain") ?? "").trim();
|
|
65
|
+
const app = (this.getAttribute("app") ?? "").trim();
|
|
66
|
+
if (!/^[0-9]{1,18}$/.test(crown) || !chain || this.kind === "app" && !app) return null;
|
|
67
|
+
const q = new URLSearchParams({ crown, chain });
|
|
68
|
+
if (this.kind === "app") q.set("app", app);
|
|
69
|
+
else q.set("kind", "coin");
|
|
70
|
+
q.set("mode", this.mode());
|
|
71
|
+
q.set("v", "1");
|
|
72
|
+
return `${origin}/v1/frame?${q.toString()}`;
|
|
73
|
+
}
|
|
74
|
+
mode() {
|
|
75
|
+
const attr = this.getAttribute("mode");
|
|
76
|
+
if (attr === "light" || attr === "dark") return attr;
|
|
77
|
+
try {
|
|
78
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
79
|
+
} catch {
|
|
80
|
+
return "light";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
mount() {
|
|
84
|
+
const root = this.shadowRoot;
|
|
85
|
+
window.removeEventListener("message", this.onMessage);
|
|
86
|
+
clearTimeout(this.timer);
|
|
87
|
+
this.removeAttribute("data-cd-ready");
|
|
88
|
+
this.removeAttribute("data-cd-unavailable");
|
|
89
|
+
const src = this.src();
|
|
90
|
+
if (!src) {
|
|
91
|
+
root.innerHTML = "<slot></slot>";
|
|
92
|
+
this.frame = null;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
root.innerHTML = `<style>${STYLE}</style><slot></slot>`;
|
|
96
|
+
const frame = document.createElement("iframe");
|
|
97
|
+
frame.setAttribute("loading", "lazy");
|
|
98
|
+
frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
|
|
99
|
+
frame.setAttribute("allow", "clipboard-write; fullscreen");
|
|
100
|
+
frame.setAttribute("title", this.kind === "coin" ? "Coin on Chain Daddy" : "App on Chain Daddy");
|
|
101
|
+
frame.src = src;
|
|
102
|
+
root.appendChild(frame);
|
|
103
|
+
this.frame = frame;
|
|
104
|
+
window.addEventListener("message", this.onMessage);
|
|
105
|
+
this.timer = setTimeout(() => {
|
|
106
|
+
if (!this.hasAttribute("data-cd-ready")) this.unavailable();
|
|
107
|
+
}, READY_TIMEOUT_MS);
|
|
108
|
+
}
|
|
109
|
+
receive(event) {
|
|
110
|
+
try {
|
|
111
|
+
if (event.origin !== origin || !this.frame || !event.source || event.source !== this.frame.contentWindow) return;
|
|
112
|
+
const data = event.data;
|
|
113
|
+
if (!data || typeof data !== "object") return;
|
|
114
|
+
if (data.type === "cd:ready") {
|
|
115
|
+
clearTimeout(this.timer);
|
|
116
|
+
this.removeAttribute("data-cd-unavailable");
|
|
117
|
+
this.setAttribute("data-cd-ready", "");
|
|
118
|
+
this.sendTheme();
|
|
119
|
+
this.dispatchEvent(new CustomEvent("cd:ready"));
|
|
120
|
+
} else if (data.type === "cd:resize") {
|
|
121
|
+
const h = Number(data.height);
|
|
122
|
+
if (Number.isFinite(h)) this.frame.style.height = `${Math.max(1, Math.min(MAX_HEIGHT, Math.ceil(h)))}px`;
|
|
123
|
+
} else if (data.type === "cd:unavailable") {
|
|
124
|
+
this.unavailable();
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** The page's --cd-* colours, for an app that takes them. */
|
|
130
|
+
sendTheme() {
|
|
131
|
+
const tokens = {};
|
|
132
|
+
const style = getComputedStyle(this);
|
|
133
|
+
for (const token of THEME_TOKENS) {
|
|
134
|
+
const value = style.getPropertyValue(token).trim();
|
|
135
|
+
if (value) tokens[token] = value;
|
|
136
|
+
}
|
|
137
|
+
this.frame?.contentWindow?.postMessage({ type: "cd:theme", tokens }, origin);
|
|
138
|
+
}
|
|
139
|
+
unavailable() {
|
|
140
|
+
try {
|
|
141
|
+
clearTimeout(this.timer);
|
|
142
|
+
this.removeAttribute("data-cd-ready");
|
|
143
|
+
if (this.hasAttribute("data-cd-unavailable")) return;
|
|
144
|
+
this.setAttribute("data-cd-unavailable", "");
|
|
145
|
+
this.dispatchEvent(new CustomEvent("cd:unavailable"));
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
CdFrame.observedAttributes = ["crown", "chain", "app", "mode"];
|
|
151
|
+
class CdApp extends CdFrame {
|
|
152
|
+
}
|
|
153
|
+
class CdCoin extends CdFrame {
|
|
154
|
+
constructor() {
|
|
155
|
+
super(...arguments);
|
|
156
|
+
this.kind = "coin";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
class CdBuy extends HTMLElement {
|
|
160
|
+
connectedCallback() {
|
|
161
|
+
try {
|
|
162
|
+
const link = this.querySelector("a[href]");
|
|
163
|
+
const href = link ? new URL(link.getAttribute("href") ?? "", document.baseURI) : null;
|
|
164
|
+
if (!href || href.protocol !== "https:") return;
|
|
165
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
166
|
+
root.innerHTML = `<style>${BUY_STYLE}</style><button type="button" part="button"></button>`;
|
|
167
|
+
const button = root.querySelector("button");
|
|
168
|
+
button.textContent = (link.textContent ?? "").trim() || "Buy on Chain Daddy";
|
|
169
|
+
button.addEventListener("click", () => {
|
|
170
|
+
try {
|
|
171
|
+
if (!window.open(href.toString(), "cd-buy", "popup,width=460,height=760")) window.location.assign(href.toString());
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const [name, ctor] of [["cd-app", CdApp], ["cd-coin", CdCoin], ["cd-buy", CdBuy]]) {
|
|
180
|
+
if (!customElements.get(name)) customElements.define(name, ctor);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
defineEmbedElements,
|
|
187
|
+
embedOriginFor
|
|
188
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { VERSION } from './core';
|
|
|
2
2
|
export { DEFAULT_ISSUER, challengeFor, exchangeCode, handleCallback, signIn } from './signin';
|
|
3
3
|
export type { SignInOptions, SignInResult } from './signin';
|
|
4
4
|
export { defineSignInButton } from './button';
|
|
5
|
+
export { defineEmbedElements, embedOriginFor } from './embed';
|
|
6
|
+
export type { EmbedOptions } from './embed';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
|
-
var VERSION = true ? "0.
|
|
2
|
+
var VERSION = true ? "0.2.0" : "0.0.0";
|
|
3
3
|
function b64url(bytes) {
|
|
4
4
|
let s = "";
|
|
5
5
|
for (const b of bytes) s += String.fromCharCode(b);
|
|
@@ -212,11 +212,198 @@ function defineSignInButton() {
|
|
|
212
212
|
CdSigninButton.observedAttributes = ["theme", "text"];
|
|
213
213
|
customElements.define("cd-signin-button", CdSigninButton);
|
|
214
214
|
}
|
|
215
|
+
|
|
216
|
+
// src/embed.ts
|
|
217
|
+
var PROD_ORIGIN = "https://embed.chaindaddy.io";
|
|
218
|
+
var STAGING_ORIGIN = "https://embed-staging.chaindaddy.io";
|
|
219
|
+
var READY_TIMEOUT_MS = 8e3;
|
|
220
|
+
var MAX_HEIGHT = 4e3;
|
|
221
|
+
var THEME_TOKENS = ["--cd-accent", "--cd-surface", "--cd-text", "--cd-text-muted", "--cd-border"];
|
|
222
|
+
function embedOriginFor(scriptSrc) {
|
|
223
|
+
try {
|
|
224
|
+
if (scriptSrc && new URL(scriptSrc).hostname === "js.staging.chaindaddy.io") return STAGING_ORIGIN;
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
return PROD_ORIGIN;
|
|
228
|
+
}
|
|
229
|
+
var STYLE2 = `
|
|
230
|
+
:host { display: block; }
|
|
231
|
+
iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
|
|
232
|
+
:host([data-cd-ready]) iframe { opacity: 1; }
|
|
233
|
+
:host([data-cd-ready]) slot { display: none; }
|
|
234
|
+
:host([data-cd-unavailable]) iframe { display: none; }
|
|
235
|
+
`;
|
|
236
|
+
var BUY_STYLE = `
|
|
237
|
+
:host { display: inline-block; }
|
|
238
|
+
button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
|
|
239
|
+
font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
240
|
+
border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
|
|
241
|
+
button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
|
|
242
|
+
`;
|
|
243
|
+
function defineEmbedElements(options = {}) {
|
|
244
|
+
try {
|
|
245
|
+
if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") return;
|
|
246
|
+
const origin = options.origin === STAGING_ORIGIN ? STAGING_ORIGIN : PROD_ORIGIN;
|
|
247
|
+
class CdFrame extends HTMLElement {
|
|
248
|
+
constructor() {
|
|
249
|
+
super(...arguments);
|
|
250
|
+
this.kind = "app";
|
|
251
|
+
this.frame = null;
|
|
252
|
+
this.onMessage = (event) => this.receive(event);
|
|
253
|
+
}
|
|
254
|
+
connectedCallback() {
|
|
255
|
+
try {
|
|
256
|
+
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
257
|
+
this.mount();
|
|
258
|
+
} catch {
|
|
259
|
+
this.unavailable();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
disconnectedCallback() {
|
|
263
|
+
try {
|
|
264
|
+
window.removeEventListener("message", this.onMessage);
|
|
265
|
+
clearTimeout(this.timer);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
attributeChangedCallback() {
|
|
270
|
+
try {
|
|
271
|
+
if (this.isConnected && this.shadowRoot) this.mount();
|
|
272
|
+
} catch {
|
|
273
|
+
this.unavailable();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** The frame's address, or null when the element does not name enough. */
|
|
277
|
+
src() {
|
|
278
|
+
const crown = (this.getAttribute("crown") ?? "").trim();
|
|
279
|
+
const chain = (this.getAttribute("chain") ?? "").trim();
|
|
280
|
+
const app = (this.getAttribute("app") ?? "").trim();
|
|
281
|
+
if (!/^[0-9]{1,18}$/.test(crown) || !chain || this.kind === "app" && !app) return null;
|
|
282
|
+
const q = new URLSearchParams({ crown, chain });
|
|
283
|
+
if (this.kind === "app") q.set("app", app);
|
|
284
|
+
else q.set("kind", "coin");
|
|
285
|
+
q.set("mode", this.mode());
|
|
286
|
+
q.set("v", "1");
|
|
287
|
+
return `${origin}/v1/frame?${q.toString()}`;
|
|
288
|
+
}
|
|
289
|
+
mode() {
|
|
290
|
+
const attr = this.getAttribute("mode");
|
|
291
|
+
if (attr === "light" || attr === "dark") return attr;
|
|
292
|
+
try {
|
|
293
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
294
|
+
} catch {
|
|
295
|
+
return "light";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
mount() {
|
|
299
|
+
const root = this.shadowRoot;
|
|
300
|
+
window.removeEventListener("message", this.onMessage);
|
|
301
|
+
clearTimeout(this.timer);
|
|
302
|
+
this.removeAttribute("data-cd-ready");
|
|
303
|
+
this.removeAttribute("data-cd-unavailable");
|
|
304
|
+
const src = this.src();
|
|
305
|
+
if (!src) {
|
|
306
|
+
root.innerHTML = "<slot></slot>";
|
|
307
|
+
this.frame = null;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
root.innerHTML = `<style>${STYLE2}</style><slot></slot>`;
|
|
311
|
+
const frame = document.createElement("iframe");
|
|
312
|
+
frame.setAttribute("loading", "lazy");
|
|
313
|
+
frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
|
|
314
|
+
frame.setAttribute("allow", "clipboard-write; fullscreen");
|
|
315
|
+
frame.setAttribute("title", this.kind === "coin" ? "Coin on Chain Daddy" : "App on Chain Daddy");
|
|
316
|
+
frame.src = src;
|
|
317
|
+
root.appendChild(frame);
|
|
318
|
+
this.frame = frame;
|
|
319
|
+
window.addEventListener("message", this.onMessage);
|
|
320
|
+
this.timer = setTimeout(() => {
|
|
321
|
+
if (!this.hasAttribute("data-cd-ready")) this.unavailable();
|
|
322
|
+
}, READY_TIMEOUT_MS);
|
|
323
|
+
}
|
|
324
|
+
receive(event) {
|
|
325
|
+
try {
|
|
326
|
+
if (event.origin !== origin || !this.frame || !event.source || event.source !== this.frame.contentWindow) return;
|
|
327
|
+
const data = event.data;
|
|
328
|
+
if (!data || typeof data !== "object") return;
|
|
329
|
+
if (data.type === "cd:ready") {
|
|
330
|
+
clearTimeout(this.timer);
|
|
331
|
+
this.removeAttribute("data-cd-unavailable");
|
|
332
|
+
this.setAttribute("data-cd-ready", "");
|
|
333
|
+
this.sendTheme();
|
|
334
|
+
this.dispatchEvent(new CustomEvent("cd:ready"));
|
|
335
|
+
} else if (data.type === "cd:resize") {
|
|
336
|
+
const h = Number(data.height);
|
|
337
|
+
if (Number.isFinite(h)) this.frame.style.height = `${Math.max(1, Math.min(MAX_HEIGHT, Math.ceil(h)))}px`;
|
|
338
|
+
} else if (data.type === "cd:unavailable") {
|
|
339
|
+
this.unavailable();
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** The page's --cd-* colours, for an app that takes them. */
|
|
345
|
+
sendTheme() {
|
|
346
|
+
const tokens = {};
|
|
347
|
+
const style = getComputedStyle(this);
|
|
348
|
+
for (const token of THEME_TOKENS) {
|
|
349
|
+
const value = style.getPropertyValue(token).trim();
|
|
350
|
+
if (value) tokens[token] = value;
|
|
351
|
+
}
|
|
352
|
+
this.frame?.contentWindow?.postMessage({ type: "cd:theme", tokens }, origin);
|
|
353
|
+
}
|
|
354
|
+
unavailable() {
|
|
355
|
+
try {
|
|
356
|
+
clearTimeout(this.timer);
|
|
357
|
+
this.removeAttribute("data-cd-ready");
|
|
358
|
+
if (this.hasAttribute("data-cd-unavailable")) return;
|
|
359
|
+
this.setAttribute("data-cd-unavailable", "");
|
|
360
|
+
this.dispatchEvent(new CustomEvent("cd:unavailable"));
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
CdFrame.observedAttributes = ["crown", "chain", "app", "mode"];
|
|
366
|
+
class CdApp extends CdFrame {
|
|
367
|
+
}
|
|
368
|
+
class CdCoin extends CdFrame {
|
|
369
|
+
constructor() {
|
|
370
|
+
super(...arguments);
|
|
371
|
+
this.kind = "coin";
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
class CdBuy extends HTMLElement {
|
|
375
|
+
connectedCallback() {
|
|
376
|
+
try {
|
|
377
|
+
const link = this.querySelector("a[href]");
|
|
378
|
+
const href = link ? new URL(link.getAttribute("href") ?? "", document.baseURI) : null;
|
|
379
|
+
if (!href || href.protocol !== "https:") return;
|
|
380
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
381
|
+
root.innerHTML = `<style>${BUY_STYLE}</style><button type="button" part="button"></button>`;
|
|
382
|
+
const button = root.querySelector("button");
|
|
383
|
+
button.textContent = (link.textContent ?? "").trim() || "Buy on Chain Daddy";
|
|
384
|
+
button.addEventListener("click", () => {
|
|
385
|
+
try {
|
|
386
|
+
if (!window.open(href.toString(), "cd-buy", "popup,width=460,height=760")) window.location.assign(href.toString());
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const [name, ctor] of [["cd-app", CdApp], ["cd-coin", CdCoin], ["cd-buy", CdBuy]]) {
|
|
395
|
+
if (!customElements.get(name)) customElements.define(name, ctor);
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
215
400
|
export {
|
|
216
401
|
DEFAULT_ISSUER,
|
|
217
402
|
VERSION,
|
|
218
403
|
challengeFor,
|
|
404
|
+
defineEmbedElements,
|
|
219
405
|
defineSignInButton,
|
|
406
|
+
embedOriginFor,
|
|
220
407
|
exchangeCode,
|
|
221
408
|
handleCallback,
|
|
222
409
|
signIn
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chain (CAIP-2, "eip155:8453") -> every ChainDaddyCheckout on it, lowercase.
|
|
3
|
+
* Frozen: this is the list verifyPayment checks against, and nothing in the
|
|
4
|
+
* process should be able to add to it.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CHECKOUTS: Readonly<Record<string, readonly string[]>>;
|
|
7
|
+
/**
|
|
8
|
+
* chain -> the confirmations Chain Daddy's own server waits on it before it
|
|
9
|
+
* calls an order confirmed. verifyPayment never reads shallower than this,
|
|
10
|
+
* whatever the block asks for: the block's number arrives from an API.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MIN_CONFIRMATIONS: Readonly<Record<string, number>>;
|
|
13
|
+
/** `token` of an order paid in the chain's own coin (ETH on Base, POL on Polygon). */
|
|
14
|
+
export declare const NATIVE_TOKEN = "0x0000000000000000000000000000000000000000";
|
|
15
|
+
export type VerifyErrorCode =
|
|
16
|
+
/** A Solana order: follow the manual recipe for now. */
|
|
17
|
+
'unsupported'
|
|
18
|
+
/** What you passed (options, expect) is missing or malformed. */
|
|
19
|
+
| 'bad_options'
|
|
20
|
+
/** The verification block is malformed. */
|
|
21
|
+
| 'bad_verification'
|
|
22
|
+
/** The block names a contract that is not a Chain Daddy checkout on that chain. */
|
|
23
|
+
| 'unknown_checkout'
|
|
24
|
+
/** The block, your expectation and your RPC endpoint are not all the same chain. */
|
|
25
|
+
| 'wrong_chain'
|
|
26
|
+
/** The block is not the order, asset, payout address or amount you expect. */
|
|
27
|
+
| 'expectation_mismatch'
|
|
28
|
+
/** The RPC endpoint failed or answered something that is not an answer. */
|
|
29
|
+
| 'rpc_error';
|
|
30
|
+
/** Every failure of verifyPayment. A payment that is simply not there yet is not an error: it resolves `paid: false`. */
|
|
31
|
+
export declare class VerifyError extends Error {
|
|
32
|
+
readonly code: VerifyErrorCode;
|
|
33
|
+
constructor(code: VerifyErrorCode, message: string);
|
|
34
|
+
}
|
|
35
|
+
/** The Order struct the checkout contract takes, as the API prints it. */
|
|
36
|
+
export interface CheckoutOrder {
|
|
37
|
+
/** "0x" + 64 hex: the order's id. */
|
|
38
|
+
orderId: string;
|
|
39
|
+
/** The ERC-20 paid in, or NATIVE_TOKEN. */
|
|
40
|
+
token: string;
|
|
41
|
+
payees: readonly string[];
|
|
42
|
+
/** Decimal strings in the asset's smallest unit; amounts[i] goes to payees[i]. */
|
|
43
|
+
amounts: readonly string[];
|
|
44
|
+
/** Unix seconds. */
|
|
45
|
+
deadline: number | string | bigint;
|
|
46
|
+
}
|
|
47
|
+
/** An EVM order's `verification` block (GET /api/v2/iap/orders/{id}). */
|
|
48
|
+
export interface EvmVerification {
|
|
49
|
+
kind: 'evm';
|
|
50
|
+
/** CAIP-2, e.g. "eip155:8453". */
|
|
51
|
+
chainId: string;
|
|
52
|
+
checkout: string;
|
|
53
|
+
feeAddress?: string | null;
|
|
54
|
+
order: CheckoutOrder;
|
|
55
|
+
confirmations: number;
|
|
56
|
+
}
|
|
57
|
+
/** A Solana order's `verification` block. verifyPayment does not check these yet. */
|
|
58
|
+
export interface SolanaVerification {
|
|
59
|
+
kind: 'solana';
|
|
60
|
+
cluster: string;
|
|
61
|
+
reference: string;
|
|
62
|
+
memo: string;
|
|
63
|
+
}
|
|
64
|
+
export type Verification = EvmVerification | SolanaVerification;
|
|
65
|
+
/**
|
|
66
|
+
* What you sold, from your own records. Every field is required: the block
|
|
67
|
+
* arrives from an API, and these are what tie it to your sale.
|
|
68
|
+
*/
|
|
69
|
+
export interface Expectation {
|
|
70
|
+
/** The id of the order you are checking. */
|
|
71
|
+
orderId: string;
|
|
72
|
+
/** The chain you sell on: "eip155:8453" or 8453. */
|
|
73
|
+
chainId: string | number;
|
|
74
|
+
/** The asset you are paid in: an ERC-20 address, or NATIVE_TOKEN for the chain's own coin. */
|
|
75
|
+
token: string;
|
|
76
|
+
/** Your payout address. */
|
|
77
|
+
payee: string;
|
|
78
|
+
/** The least the order must send your payout address, in the asset's smallest unit. A bigint or a decimal string, never a number. */
|
|
79
|
+
amount: bigint | string;
|
|
80
|
+
}
|
|
81
|
+
export interface VerifyOptions {
|
|
82
|
+
expect: Expectation;
|
|
83
|
+
/** A JSON-RPC endpoint for the order's chain. Yours, not Chain Daddy's. */
|
|
84
|
+
rpcUrl?: string;
|
|
85
|
+
/** Used with rpcUrl in place of the global fetch. */
|
|
86
|
+
fetch?: typeof fetch;
|
|
87
|
+
/** Instead of rpcUrl: an EIP-1193 style request function (a viem client's `request`, an ethers provider's `send` wrapped). */
|
|
88
|
+
request?: (args: {
|
|
89
|
+
method: string;
|
|
90
|
+
params: unknown[];
|
|
91
|
+
}) => Promise<unknown>;
|
|
92
|
+
/** A deeper wait than the block's `confirmations` and the chain's MIN_CONFIRMATIONS. A smaller number is ignored: this can only raise the depth. */
|
|
93
|
+
confirmations?: number;
|
|
94
|
+
/** Aborts the RPC requests made through rpcUrl. */
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
}
|
|
97
|
+
export interface PaymentResult {
|
|
98
|
+
/** True only when the contract says this exact order was paid, `depth` blocks deep. */
|
|
99
|
+
paid: boolean;
|
|
100
|
+
/** The wallet that paid, lowercase, or null. */
|
|
101
|
+
payer: string | null;
|
|
102
|
+
/** The confirmations asked for: the largest of the block's, yours and the chain's MIN_CONFIRMATIONS. */
|
|
103
|
+
depth: number;
|
|
104
|
+
/** The checkout contract that was asked, lowercase. */
|
|
105
|
+
checkout: string;
|
|
106
|
+
/** The block the answer was read at: head - depth + 1. */
|
|
107
|
+
blockNumber: number;
|
|
108
|
+
/** The order's EIP-712 hash on that contract. */
|
|
109
|
+
orderHash: string;
|
|
110
|
+
/** CAIP-2. */
|
|
111
|
+
chainId: string;
|
|
112
|
+
}
|
|
113
|
+
/** Calldata for hashOrder(order) on a checkout contract. */
|
|
114
|
+
export declare function encodeHashOrder(order: CheckoutOrder): string;
|
|
115
|
+
/** Calldata for paidBy(orderHash). The key is hashOrder's answer, not the order id. */
|
|
116
|
+
export declare function encodePaidBy(orderHash: string): string;
|
|
117
|
+
/** Whether `checkout` is a ChainDaddyCheckout this package knows on `chainId`. */
|
|
118
|
+
export declare function isKnownCheckout(chainId: string | number, checkout: string): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Checks on chain that an order was paid. Resolves `paid: true` only when the
|
|
121
|
+
* checkout contract itself says so, for exactly the order in `verification`,
|
|
122
|
+
* at least `depth` blocks ago. Rejects with a VerifyError when the block, your
|
|
123
|
+
* expectation or the RPC endpoint cannot be trusted to answer; never resolves
|
|
124
|
+
* `paid: true` on a doubt.
|
|
125
|
+
*/
|
|
126
|
+
export declare function verifyPayment(verification: Verification, options: VerifyOptions): Promise<PaymentResult>;
|