@duckity/js 0.2.0 → 1.0.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/index.d.ts CHANGED
@@ -17,9 +17,35 @@ interface GetDuckityChallengeOptions {
17
17
  *
18
18
  * @param protectionProfileId The ID of the protection profile to get the challenge for.
19
19
  * @param options Optional parameters for the challenge issuance.
20
+ *
20
21
  * @returns The solution to the challenge issued by Duckity.
21
22
  */
22
23
  declare function solve(protectionProfileId: string, options?: GetDuckityChallengeOptions): Promise<string>;
24
+ /**
25
+ * Options for validating a Duckity solution with the API.
26
+ */
27
+ interface ValidateDuckitySolutionOptions {
28
+ /**
29
+ * The base URL to the API endpoint. Defaults to `https://api.duckity.com/d1` if not provided.
30
+ *
31
+ * The version must be specified in the URL path.
32
+ *
33
+ * Only update this when self-hosting a Duckling.
34
+ */
35
+ api?: string;
36
+ }
37
+ /**
38
+ * Validates a client-submitted solution token.
39
+ *
40
+ * @param solution The solution token to validate.
41
+ * @param clientIp The IP of the client that submitted the solution token.
42
+ * @param applicationSecret The protection profile's application secret.
43
+ * @param protectionProfileId The ID of the protection profile for which this challenge was issued.
44
+ * @param options Additional options to customize the behavior of this call.
45
+ *
46
+ * @returns Whether the solution was valid or not.
47
+ */
48
+ declare function validate(solution: string, clientIp: string, applicationSecret: string, protectionProfileId: string, options?: ValidateDuckitySolutionOptions): Promise<boolean>;
23
49
 
24
- export { solve };
25
- export type { GetDuckityChallengeOptions };
50
+ export { solve, validate };
51
+ export type { GetDuckityChallengeOptions, ValidateDuckitySolutionOptions };
package/dist/index.js CHANGED
@@ -3,4 +3,4 @@
3
3
  * Copyright 2019 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- const e=Symbol("Comlink.proxy"),t=Symbol("Comlink.endpoint"),n=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),a=Symbol("Comlink.thrown"),i=e=>"object"==typeof e&&null!==e||"function"==typeof e,s=new Map([["proxy",{canHandle:t=>i(t)&&t[e],serialize(e){const{port1:t,port2:n}=new MessageChannel;return o(e,t),[n,[n]]},deserialize:e=>(e.start(),u(e))}],["throw",{canHandle:e=>i(e)&&a in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function o(t,n=globalThis,i=["*"]){n.addEventListener("message",function s(u){if(!u||!u.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(i,u.origin))return void console.warn(`Invalid origin '${u.origin}' for comlink proxy`);const{id:l,type:p,path:f}=Object.assign({path:[]},u.data),g=(u.data.argumentList||[]).map(w);let d;try{const n=f.slice(0,-1).reduce((e,t)=>e[t],t),r=f.reduce((e,t)=>e[t],t);switch(p){case"GET":d=r;break;case"SET":n[f.slice(-1)[0]]=w(u.data.value),d=!0;break;case"APPLY":d=r.apply(n,g);break;case"CONSTRUCT":d=function(t){return Object.assign(t,{[e]:!0})}(new r(...g));break;case"ENDPOINT":{const{port1:e,port2:n}=new MessageChannel;o(t,n),d=function(e,t){return m.set(e,t),e}(e,[e])}break;case"RELEASE":d=void 0;break;default:return}}catch(e){d={value:e,[a]:0}}Promise.resolve(d).catch(e=>({value:e,[a]:0})).then(e=>{const[a,i]=y(e);n.postMessage(Object.assign(Object.assign({},a),{id:l}),i),"RELEASE"===p&&(n.removeEventListener("message",s),c(n),r in t&&"function"==typeof t[r]&&t[r]())}).catch(e=>{const[t,r]=y({value:new TypeError("Unserializable return value"),[a]:0});n.postMessage(Object.assign(Object.assign({},t),{id:l}),r)})}),n.start&&n.start()}function c(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function u(e,t){const n=new Map;return e.addEventListener("message",function(e){const{data:t}=e;if(!t||!t.id)return;const r=n.get(t.id);if(r)try{r(t)}finally{n.delete(t.id)}}),d(e,n,[],t)}function l(e){if(e)throw new Error("Proxy has been released and is not useable")}function p(e){return E(e,new Map,{type:"RELEASE"}).then(()=>{c(e)})}const f=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry(e=>{const t=(f.get(e)||0)-1;f.set(e,t),0===t&&p(e)});function d(e,r,a=[],i=function(){}){let s=!1;const o=new Proxy(i,{get(t,i){if(l(s),i===n)return()=>{!function(e){g&&g.unregister(e)}(o),p(e),r.clear(),s=!0};if("then"===i){if(0===a.length)return{then:()=>o};const t=E(e,r,{type:"GET",path:a.map(e=>e.toString())}).then(w);return t.then.bind(t)}return d(e,r,[...a,i])},set(t,n,i){l(s);const[o,c]=y(i);return E(e,r,{type:"SET",path:[...a,n].map(e=>e.toString()),value:o},c).then(w)},apply(n,i,o){l(s);const c=a[a.length-1];if(c===t)return E(e,r,{type:"ENDPOINT"}).then(w);if("bind"===c)return d(e,r,a.slice(0,-1));const[u,p]=h(o);return E(e,r,{type:"APPLY",path:a.map(e=>e.toString()),argumentList:u},p).then(w)},construct(t,n){l(s);const[i,o]=h(n);return E(e,r,{type:"CONSTRUCT",path:a.map(e=>e.toString()),argumentList:i},o).then(w)}});return function(e,t){const n=(f.get(t)||0)+1;f.set(t,n),g&&g.register(e,t,e)}(o,e),o}function h(e){const t=e.map(y);return[t.map(e=>e[0]),(n=t.map(e=>e[1]),Array.prototype.concat.apply([],n))];var n}const m=new WeakMap;function y(e){for(const[t,n]of s)if(n.canHandle(e)){const[r,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:r},a]}return[{type:"RAW",value:e},m.get(e)||[]]}function w(e){switch(e.type){case"HANDLER":return s.get(e.name).deserialize(e.value);case"RAW":return e.value}}function E(e,t,n,r){return new Promise(a=>{const i=new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");t.set(i,a),e.start&&e.start(),e.postMessage(Object.assign({id:i},n),r)})}var b=u(new Worker(new URL("./worker.js",import.meta.url),{type:"module"}));function v(e,t){return async function(e,t){const n=await fetch(e,t);let r;try{r=await n.json()}catch{r=await n.text()}if(!n.ok)throw new Error(r?.message||"Request failed");return r}(e,{method:"POST",headers:{...t?.headers},...t,body:void 0})}async function k(e,t){t?.api&&t.api.endsWith("/")&&(t.api=t.api.slice(0,-1));let n=await v(`${t?.api||"https://api.duckity.com/d1"}/challenges/${e}/issue`);return await b.solve(n.challenge)}export{k as solve};
6
+ const e=Symbol("Comlink.proxy"),t=Symbol("Comlink.endpoint"),n=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),a=Symbol("Comlink.thrown"),i=e=>"object"==typeof e&&null!==e||"function"==typeof e,o=new Map([["proxy",{canHandle:t=>i(t)&&t[e],serialize(e){const{port1:t,port2:n}=new MessageChannel;return s(e,t),[n,[n]]},deserialize:e=>(e.start(),u(e))}],["throw",{canHandle:e=>i(e)&&a in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function s(t,n=globalThis,i=["*"]){n.addEventListener("message",function o(u){if(!u||!u.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(i,u.origin))return void console.warn(`Invalid origin '${u.origin}' for comlink proxy`);const{id:l,type:p,path:f}=Object.assign({path:[]},u.data),d=(u.data.argumentList||[]).map(w);let h;try{const n=f.slice(0,-1).reduce((e,t)=>e[t],t),r=f.reduce((e,t)=>e[t],t);switch(p){case"GET":h=r;break;case"SET":n[f.slice(-1)[0]]=w(u.data.value),h=!0;break;case"APPLY":h=r.apply(n,d);break;case"CONSTRUCT":h=function(t){return Object.assign(t,{[e]:!0})}(new r(...d));break;case"ENDPOINT":{const{port1:e,port2:n}=new MessageChannel;s(t,n),h=function(e,t){return g.set(e,t),e}(e,[e])}break;case"RELEASE":h=void 0;break;default:return}}catch(e){h={value:e,[a]:0}}Promise.resolve(h).catch(e=>({value:e,[a]:0})).then(e=>{const[a,i]=m(e);n.postMessage(Object.assign(Object.assign({},a),{id:l}),i),"RELEASE"===p&&(n.removeEventListener("message",o),c(n),r in t&&"function"==typeof t[r]&&t[r]())}).catch(e=>{const[t,r]=m({value:new TypeError("Unserializable return value"),[a]:0});n.postMessage(Object.assign(Object.assign({},t),{id:l}),r)})}),n.start&&n.start()}function c(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function u(e,t){const n=new Map;return e.addEventListener("message",function(e){const{data:t}=e;if(!t||!t.id)return;const r=n.get(t.id);if(r)try{r(t)}finally{n.delete(t.id)}}),h(e,n,[],t)}function l(e){if(e)throw new Error("Proxy has been released and is not useable")}function p(e){return b(e,new Map,{type:"RELEASE"}).then(()=>{c(e)})}const f=new WeakMap,d="FinalizationRegistry"in globalThis&&new FinalizationRegistry(e=>{const t=(f.get(e)||0)-1;f.set(e,t),0===t&&p(e)});function h(e,r,a=[],i=function(){}){let o=!1;const s=new Proxy(i,{get(t,i){if(l(o),i===n)return()=>{!function(e){d&&d.unregister(e)}(s),p(e),r.clear(),o=!0};if("then"===i){if(0===a.length)return{then:()=>s};const t=b(e,r,{type:"GET",path:a.map(e=>e.toString())}).then(w);return t.then.bind(t)}return h(e,r,[...a,i])},set(t,n,i){l(o);const[s,c]=m(i);return b(e,r,{type:"SET",path:[...a,n].map(e=>e.toString()),value:s},c).then(w)},apply(n,i,s){l(o);const c=a[a.length-1];if(c===t)return b(e,r,{type:"ENDPOINT"}).then(w);if("bind"===c)return h(e,r,a.slice(0,-1));const[u,p]=y(s);return b(e,r,{type:"APPLY",path:a.map(e=>e.toString()),argumentList:u},p).then(w)},construct(t,n){l(o);const[i,s]=y(n);return b(e,r,{type:"CONSTRUCT",path:a.map(e=>e.toString()),argumentList:i},s).then(w)}});return function(e,t){const n=(f.get(t)||0)+1;f.set(t,n),d&&d.register(e,t,e)}(s,e),s}function y(e){const t=e.map(m);return[t.map(e=>e[0]),(n=t.map(e=>e[1]),Array.prototype.concat.apply([],n))];var n}const g=new WeakMap;function m(e){for(const[t,n]of o)if(n.canHandle(e)){const[r,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:r},a]}return[{type:"RAW",value:e},g.get(e)||[]]}function w(e){switch(e.type){case"HANDLER":return o.get(e.name).deserialize(e.value);case"RAW":return e.value}}function b(e,t,n,r){return new Promise(a=>{const i=new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");t.set(i,a),e.start&&e.start(),e.postMessage(Object.assign({id:i},n),r)})}let E,v;function k(e,t){return async function(e,t){const n=await fetch(e,t);let r;try{r=await n.json()}catch{r=await n.text()}if(!n.ok)throw new Error(r?.message||"Request failed");return r}(e,{method:"POST",headers:{...t?.body?{"Content-Type":"application/json"}:{},...t?.headers},...t,body:t?.body?JSON.stringify(t.body):void 0})}async function S(e,t){t?.api&&t.api.endsWith("/")&&(t.api=t.api.slice(0,-1));let n=await k(`${t?.api||"https://api.duckity.com/d1"}/challenges/${e}/issue`,{headers:{"x-duckity-csrf":"1"}});return await function(){if("undefined"==typeof window)throw new Error("Worker API can only be used in the browser");return v||(E=new Worker(new URL("./worker.js",import.meta.url),{type:"module"}),v=u(E)),v}().solve(n.challenge)}async function T(e,t,n,r,a){return a?.api&&a.api.endsWith("/")&&(a.api=a.api.slice(0,-1)),(await k(`${a?.api||"https://api.duckity.com/d1"}/challenges/${r}/validate`,{headers:{Authorization:`Bearer ${n}`},body:{solution:e,ip:t}})).is_valid}export{S as solve,T as validate};
package/dist/worker.js CHANGED
@@ -3,4 +3,4 @@
3
3
  * Copyright 2019 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- const n=Symbol("Comlink.proxy"),e=Symbol("Comlink.endpoint"),t=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),o=Symbol("Comlink.thrown"),i=n=>"object"==typeof n&&null!==n||"function"==typeof n,a={canHandle:e=>i(e)&&e[n],serialize(n){const{port1:e,port2:t}=new MessageChannel;return c(n,e),[t,[t]]},deserialize:n=>(n.start(),function(n,e){const t=new Map;return n.addEventListener("message",function(n){const{data:e}=n;if(!e||!e.id)return;const r=t.get(e.id);if(r)try{r(e)}finally{t.delete(e.id)}}),d(n,t,[],e)}(n))},s=new Map([["proxy",a],["throw",{canHandle:n=>i(n)&&o in n,serialize({value:n}){let e;return e=n instanceof Error?{isError:!0,value:{message:n.message,name:n.name,stack:n.stack}}:{isError:!1,value:n},[e,[]]},deserialize(n){if(n.isError)throw Object.assign(new Error(n.value.message),n.value);throw n.value}}]]);function c(e,t=globalThis,i=["*"]){t.addEventListener("message",function a(s){if(!s||!s.data)return;if(!function(n,e){for(const t of n){if(e===t||"*"===t)return!0;if(t instanceof RegExp&&t.test(e))return!0}return!1}(i,s.origin))return void console.warn(`Invalid origin '${s.origin}' for comlink proxy`);const{id:u,type:f,path:p}=Object.assign({path:[]},s.data),g=(s.data.argumentList||[]).map(v);let d;try{const t=p.slice(0,-1).reduce((n,e)=>n[e],e),r=p.reduce((n,e)=>n[e],e);switch(f){case"GET":d=r;break;case"SET":t[p.slice(-1)[0]]=v(s.data.value),d=!0;break;case"APPLY":d=r.apply(t,g);break;case"CONSTRUCT":d=function(e){return Object.assign(e,{[n]:!0})}(new r(...g));break;case"ENDPOINT":{const{port1:n,port2:t}=new MessageChannel;c(e,t),d=function(n,e){return m.set(n,e),n}(n,[n])}break;case"RELEASE":d=void 0;break;default:return}}catch(n){d={value:n,[o]:0}}Promise.resolve(d).catch(n=>({value:n,[o]:0})).then(n=>{const[o,i]=h(n);t.postMessage(Object.assign(Object.assign({},o),{id:u}),i),"RELEASE"===f&&(t.removeEventListener("message",a),l(t),r in e&&"function"==typeof e[r]&&e[r]())}).catch(n=>{const[e,r]=h({value:new TypeError("Unserializable return value"),[o]:0});t.postMessage(Object.assign(Object.assign({},e),{id:u}),r)})}),t.start&&t.start()}function l(n){(function(n){return"MessagePort"===n.constructor.name})(n)&&n.close()}function u(n){if(n)throw new Error("Proxy has been released and is not useable")}function f(n){return E(n,new Map,{type:"RELEASE"}).then(()=>{l(n)})}const p=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry(n=>{const e=(p.get(n)||0)-1;p.set(n,e),0===e&&f(n)});function d(n,r,o=[],i=function(){}){let a=!1;const s=new Proxy(i,{get(e,i){if(u(a),i===t)return()=>{!function(n){g&&g.unregister(n)}(s),f(n),r.clear(),a=!0};if("then"===i){if(0===o.length)return{then:()=>s};const e=E(n,r,{type:"GET",path:o.map(n=>n.toString())}).then(v);return e.then.bind(e)}return d(n,r,[...o,i])},set(e,t,i){u(a);const[s,c]=h(i);return E(n,r,{type:"SET",path:[...o,t].map(n=>n.toString()),value:s},c).then(v)},apply(t,i,s){u(a);const c=o[o.length-1];if(c===e)return E(n,r,{type:"ENDPOINT"}).then(v);if("bind"===c)return d(n,r,o.slice(0,-1));const[l,f]=y(s);return E(n,r,{type:"APPLY",path:o.map(n=>n.toString()),argumentList:l},f).then(v)},construct(e,t){u(a);const[i,s]=y(t);return E(n,r,{type:"CONSTRUCT",path:o.map(n=>n.toString()),argumentList:i},s).then(v)}});return function(n,e){const t=(p.get(e)||0)+1;p.set(e,t),g&&g.register(n,e,n)}(s,n),s}function y(n){const e=n.map(h);return[e.map(n=>n[0]),(t=e.map(n=>n[1]),Array.prototype.concat.apply([],t))];var t}const m=new WeakMap;function h(n){for(const[e,t]of s)if(t.canHandle(n)){const[r,o]=t.serialize(n);return[{type:"HANDLER",name:e,value:r},o]}return[{type:"RAW",value:n},m.get(n)||[]]}function v(n){switch(n.type){case"HANDLER":return s.get(n.name).deserialize(n.value);case"RAW":return n.value}}function E(n,e,t,r){return new Promise(o=>{const i=new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");e.set(i,o),n.start&&n.start(),n.postMessage(Object.assign({id:i},t),r)})}function b(n,e){let t=0n;for(const r of n)t=t<<e|BigInt(r);return t}function w(n,e,t){let r=new Array(e).fill(0);void 0===t&&(t=32n),void 0===e&&(e=parseInt((BigInt(n.toString(2).length)/t).toString()));let o=0n;for(let n=t-1n;n>0;n--)o<<=1n,o&=1n;for(let i=e-1;i>0;i--){let e=parseInt((n&o).toString());n>>=t,r[i]=e}return r}function S(n,e,t){let r=1n;for(;0n!==e;)1n==(1n&e)&&(r*=n,r%=t),n**=2n,n%=t,e/=2n;return r}function A(n,e){if([1n,2n,3n,5n,7n].includes(n))return!0;if(0n==(1n&n))return!1;if(n%5n==0n)return!1;for(let t=0n;t<BigInt(e)&&2n+t<n;t++){if(!k(n,2n+t))return!1}return!0}function k(n,e){let t=n-1n,r=t,o=0;for(;r%2n==0n;)r/=2n,o+=1;let i=S(e,r,n);for(let e=0n;e<o;e++){if(1n==i||i==t)return!0;i=S(i,2n,n)}return!1}var M={encode:function(n,e){let t={y:w(e.y,void 0,32n),pi:w(e.pi,void 0,32n)},r=JSON.stringify(t);return`${n}.${btoa(r)}`},solve:async function(n){let e=n.x;for(let t=0;t<n.t;t++)e=e**BigInt(2)%BigInt(n.t);let t=function(n,e){let t=0;0n!==n&&(t=n.toString(2).length);let r=Math.ceil((t+7)/8);return r=Math.ceil(r/e)*e,r}(e,4),r=w(n.x,t,8n),o=w(e,t,8n),i=Array.from((new TextEncoder).encode("duckity"));i.concat(...r),i.concat(...o);let a=await crypto.subtle.digest("SHA-256",new Uint8Array(i)),s=function(n){for(;;)if(A(n+=1n,40))return n}(b(Array.from(new Uint8Array(a)),8n)),c=1n,l=n.x,u=1n;for(let e=0;e<n.t;e++){let e=2n*u;e>=s?(c=c*l%n.n,u=e-s):u=e}return{y:e,pi:c}},decode:function(n){if(1!=(n.match(/\./g)||[]).length)throw Error("The challenge string contained too many or not enough sections.");let[e,t]=n.split(".",2),r=atob(e),o=JSON.parse(r);return{id:o.challenge_id,ip:o.ip,n:b(o.n,32n),x:b(o.x,32n),t:o.t,timestamp:new Date(o.timestamp)}}};c({async solve(n){let e=M.decode(n),t=await M.solve(e);return M.encode(n,t)}});
6
+ const n=Symbol("Comlink.proxy"),e=Symbol("Comlink.endpoint"),t=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),a=Symbol("Comlink.thrown"),o=n=>"object"==typeof n&&null!==n||"function"==typeof n,i={canHandle:e=>o(e)&&e[n],serialize(n){const{port1:e,port2:t}=new MessageChannel;return c(n,e),[t,[t]]},deserialize:n=>(n.start(),function(n,e){const t=new Map;return n.addEventListener("message",function(n){const{data:e}=n;if(!e||!e.id)return;const r=t.get(e.id);if(r)try{r(e)}finally{t.delete(e.id)}}),d(n,t,[],e)}(n))},s=new Map([["proxy",i],["throw",{canHandle:n=>o(n)&&a in n,serialize({value:n}){let e;return e=n instanceof Error?{isError:!0,value:{message:n.message,name:n.name,stack:n.stack}}:{isError:!1,value:n},[e,[]]},deserialize(n){if(n.isError)throw Object.assign(new Error(n.value.message),n.value);throw n.value}}]]);function c(e,t=globalThis,o=["*"]){t.addEventListener("message",function i(s){if(!s||!s.data)return;if(!function(n,e){for(const t of n){if(e===t||"*"===t)return!0;if(t instanceof RegExp&&t.test(e))return!0}return!1}(o,s.origin))return void console.warn(`Invalid origin '${s.origin}' for comlink proxy`);const{id:u,type:f,path:p}=Object.assign({path:[]},s.data),g=(s.data.argumentList||[]).map(b);let d;try{const t=p.slice(0,-1).reduce((n,e)=>n[e],e),r=p.reduce((n,e)=>n[e],e);switch(f){case"GET":d=r;break;case"SET":t[p.slice(-1)[0]]=b(s.data.value),d=!0;break;case"APPLY":d=r.apply(t,g);break;case"CONSTRUCT":d=function(e){return Object.assign(e,{[n]:!0})}(new r(...g));break;case"ENDPOINT":{const{port1:n,port2:t}=new MessageChannel;c(e,t),d=function(n,e){return m.set(n,e),n}(n,[n])}break;case"RELEASE":d=void 0;break;default:return}}catch(n){d={value:n,[a]:0}}Promise.resolve(d).catch(n=>({value:n,[a]:0})).then(n=>{const[a,o]=h(n);t.postMessage(Object.assign(Object.assign({},a),{id:u}),o),"RELEASE"===f&&(t.removeEventListener("message",i),l(t),r in e&&"function"==typeof e[r]&&e[r]())}).catch(n=>{const[e,r]=h({value:new TypeError("Unserializable return value"),[a]:0});t.postMessage(Object.assign(Object.assign({},e),{id:u}),r)})}),t.start&&t.start()}function l(n){(function(n){return"MessagePort"===n.constructor.name})(n)&&n.close()}function u(n){if(n)throw new Error("Proxy has been released and is not useable")}function f(n){return E(n,new Map,{type:"RELEASE"}).then(()=>{l(n)})}const p=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry(n=>{const e=(p.get(n)||0)-1;p.set(n,e),0===e&&f(n)});function d(n,r,a=[],o=function(){}){let i=!1;const s=new Proxy(o,{get(e,o){if(u(i),o===t)return()=>{!function(n){g&&g.unregister(n)}(s),f(n),r.clear(),i=!0};if("then"===o){if(0===a.length)return{then:()=>s};const e=E(n,r,{type:"GET",path:a.map(n=>n.toString())}).then(b);return e.then.bind(e)}return d(n,r,[...a,o])},set(e,t,o){u(i);const[s,c]=h(o);return E(n,r,{type:"SET",path:[...a,t].map(n=>n.toString()),value:s},c).then(b)},apply(t,o,s){u(i);const c=a[a.length-1];if(c===e)return E(n,r,{type:"ENDPOINT"}).then(b);if("bind"===c)return d(n,r,a.slice(0,-1));const[l,f]=y(s);return E(n,r,{type:"APPLY",path:a.map(n=>n.toString()),argumentList:l},f).then(b)},construct(e,t){u(i);const[o,s]=y(t);return E(n,r,{type:"CONSTRUCT",path:a.map(n=>n.toString()),argumentList:o},s).then(b)}});return function(n,e){const t=(p.get(e)||0)+1;p.set(e,t),g&&g.register(n,e,n)}(s,n),s}function y(n){const e=n.map(h);return[e.map(n=>n[0]),(t=e.map(n=>n[1]),Array.prototype.concat.apply([],t))];var t}const m=new WeakMap;function h(n){for(const[e,t]of s)if(t.canHandle(n)){const[r,a]=t.serialize(n);return[{type:"HANDLER",name:e,value:r},a]}return[{type:"RAW",value:n},m.get(n)||[]]}function b(n){switch(n.type){case"HANDLER":return s.get(n.name).deserialize(n.value);case"RAW":return n.value}}function E(n,e,t,r){return new Promise(a=>{const o=new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");e.set(o,a),n.start&&n.start(),n.postMessage(Object.assign({id:o},t),r)})}const w=512;function v(n,e){let t=0n;for(const r of n)t=t<<e|BigInt(r);return t}function A(n,e,t){void 0===t&&(t=32n);let r=e/Number(t/8n),a=new Array(r).fill(0),o=0n;for(let n=t;n>0;n--)o<<=1n,o|=1n;for(let e=r-1;e>=0;e--){let r=Number(n&o);a[e]=r,n>>=t}return a}function S(n,e,t){let r=1n;for(;0n!==e;)1n==(1n&e)&&(r*=n,r%=t),n**=2n,n%=t,e/=2n;return r}function k(n,e){if([1n,2n,3n,5n,7n].includes(n))return!0;if(0n==(1n&n))return!1;if(n%5n==0n)return!1;for(let t=0n;t<BigInt(e)&&2n+t<n;t++){if(!T(n,2n+t))return!1}return!0}function T(n,e){let t=n-1n,r=t,a=0;for(;r%2n==0n;)r/=2n,a+=1;let o=S(e,r,n);for(let e=0n;e<a;e++){if(1n==o||o==t)return!0;o=S(o,2n,n)}return!1}var M={encode:function(n,e){let t={y:A(e.y,512,32n),pi:A(e.pi,512,32n)},r=JSON.stringify(t);return`${n}.${btoa(r).replaceAll("=","").replaceAll("+","-").replaceAll("/","_")}`},solve:async function(n){let e=n.x;for(let t=0;t<n.t;t++)e=S(e,2n,n.n);let t=Array.from((new TextEncoder).encode("duckity"));t=t.concat(...A(n.n,w,8n)),t=t.concat(...A(n.x,w,8n)),t=t.concat(...A(BigInt(n.t),w,8n)),t=t.concat(...A(e,w,8n));let r=await crypto.subtle.digest("SHA-256",new Uint8Array(t)),a=function(n){for(;;)if(k(n+=1n,40))return n}(v(Array.from(new Uint8Array(r)),8n)),o=1n,i=1n;for(let e=0n;e<n.t;e++)o*=2n,o>=a?(o-=a,i=i*i*n.x%n.n):i=i*i%n.n;return{y:e,pi:i}},decode:function(n){if(![1,2].includes((n.match(/\./g)||[]).length))throw Error("The challenge string contained too many or not enough sections.");let[e,t]=n.split(".",2),r=atob(e),a=JSON.parse(r);return{id:a.challenge_id,ip:a.ip,n:v(a.n,32n),x:v(a.x,32n),t:a.t,timestamp:new Date(a.timestamp)}}};c({async solve(n){let e=M.decode(n),t=await M.solve(e);return M.encode(n,t)}});
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@duckity/js",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "1.0.0",
5
5
  "module": "src/index.ts",
6
6
  "types": "dist/index.d.ts",
7
+ "homepage": "https://duckity.com/docs/sdks/javascript",
8
+ "repository": {
9
+ "url": "https://github.com/duckity-com/sdks"
10
+ },
7
11
  "collaborators": [
8
12
  "Rafael Bradley <rafabradleyrb@gmail.com>"
9
13
  ],
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import wrapper from "./processing/wrapper";
1
+ import { getWorkerApi } from "./processing/wrapper";
2
2
  import { post } from "./requests";
3
3
 
4
4
  /**
@@ -13,19 +13,6 @@ export interface GetDuckityChallengeOptions {
13
13
  * Only update this when self-hosting a Duckling.
14
14
  */
15
15
  api?: string;
16
-
17
- // /**
18
- // * When this is `false`, the client will assume it is allowed to get a challenge the first time
19
- // * one is requested. This may not always be true (when the client has an active penalty or used
20
- // * all of their allowed requests in another session), case in which the client may be penalized.
21
- // *
22
- // * When this is `true`, an extra request will be made to the Duckling API to synchronize the
23
- // * client's state with the server's state. This extra request will not issue a challenge, rather
24
- // * only synchronize the client's state with the server's state. If the client is penalized or has
25
- // * to wait before being allowed to get a challenge, the client will be informed of this. It'll
26
- // * avoid getting it unnecessarily penalized at the cost of an extra request.
27
- // */
28
- // sync?: boolean;
29
16
  }
30
17
 
31
18
  /**
@@ -44,6 +31,7 @@ interface ChallengeResponse {
44
31
  *
45
32
  * @param protectionProfileId The ID of the protection profile to get the challenge for.
46
33
  * @param options Optional parameters for the challenge issuance.
34
+ *
47
35
  * @returns The solution to the challenge issued by Duckity.
48
36
  */
49
37
  export async function solve(
@@ -57,9 +45,77 @@ export async function solve(
57
45
 
58
46
  let response: ChallengeResponse = await post(
59
47
  `${options?.api || "https://api.duckity.com/d1"}/challenges/${protectionProfileId}/issue`,
48
+ {
49
+ headers: {
50
+ "x-duckity-csrf": "1",
51
+ }
52
+ }
60
53
  );
61
54
 
62
- let solution = await wrapper.solve(response.challenge);
55
+ let solution = await getWorkerApi().solve(response.challenge);
63
56
 
64
57
  return solution;
65
58
  }
59
+
60
+ /**
61
+ * Options for validating a Duckity solution with the API.
62
+ */
63
+ export interface ValidateDuckitySolutionOptions {
64
+ /**
65
+ * The base URL to the API endpoint. Defaults to `https://api.duckity.com/d1` if not provided.
66
+ *
67
+ * The version must be specified in the URL path.
68
+ *
69
+ * Only update this when self-hosting a Duckling.
70
+ */
71
+ api?: string;
72
+ }
73
+
74
+ /**
75
+ * The response from the Duckling API when validating a solution.
76
+ */
77
+ interface ValidationResponse {
78
+ /**
79
+ * Whether the submitted solution is valid.
80
+ */
81
+ is_valid: boolean;
82
+ }
83
+
84
+ /**
85
+ * Validates a client-submitted solution token.
86
+ *
87
+ * @param solution The solution token to validate.
88
+ * @param clientIp The IP of the client that submitted the solution token.
89
+ * @param applicationSecret The protection profile's application secret.
90
+ * @param protectionProfileId The ID of the protection profile for which this challenge was issued.
91
+ * @param options Additional options to customize the behavior of this call.
92
+ *
93
+ * @returns Whether the solution was valid or not.
94
+ */
95
+ export async function validate(
96
+ solution: string,
97
+ clientIp: string,
98
+ applicationSecret: string,
99
+ protectionProfileId: string,
100
+ options?: ValidateDuckitySolutionOptions,
101
+ ): Promise<boolean> {
102
+ if (options?.api && options.api.endsWith("/")) {
103
+ // Remove the trailing slash from the API URL if provided
104
+ options.api = options.api.slice(0, -1);
105
+ }
106
+
107
+ let response: ValidationResponse = await post(
108
+ `${options?.api || "https://api.duckity.com/d1"}/challenges/${protectionProfileId}/validate`,
109
+ {
110
+ headers: {
111
+ Authorization: `Bearer ${applicationSecret}`,
112
+ },
113
+ body: {
114
+ solution,
115
+ ip: clientIp,
116
+ },
117
+ },
118
+ );
119
+
120
+ return response.is_valid;
121
+ }
@@ -1,3 +1,5 @@
1
+ const RSA_MODULUS_SIZE = 512;
2
+
1
3
  interface ChallengeMeta {
2
4
  // The challenge's ID.
3
5
  challenge_id: string;
@@ -8,9 +10,9 @@ interface ChallengeMeta {
8
10
  // The ID of the protection profile this challenge was issued for.
9
11
  protection_profile_id: string;
10
12
  // The digits of the N Wesolowski VDF parameter, most significant digit first.
11
- n: Array<number>;
13
+ n: number[];
12
14
  // The digits of the X Wesolowski VDF parameter, most significant digit first.
13
- x: Array<number>;
15
+ x: number[];
14
16
  // The T Wesolowski VDF parameter.
15
17
  t: number;
16
18
  }
@@ -64,7 +66,7 @@ function getBigintWidth(number: bigint, alignment?: number): number {
64
66
  bitWidth = number.toString(2).length;
65
67
  }
66
68
 
67
- let byteWidth = Math.ceil((bitWidth + 7) / 8);
69
+ let byteWidth = Math.floor((bitWidth + 7) / 8);
68
70
 
69
71
  if (alignment !== undefined) {
70
72
  byteWidth = Math.ceil(byteWidth / alignment) * alignment;
@@ -73,33 +75,41 @@ function getBigintWidth(number: bigint, alignment?: number): number {
73
75
  return byteWidth;
74
76
  }
75
77
 
78
+ /**
79
+ * Returns a digit array from a big integer.
80
+ *
81
+ * @param number The number to get the digits from.
82
+ * @param width The byte width of the result. If undefined, this will be the minimum width required
83
+ * to represent the full number.
84
+ * @param digitBits The amount of bits per digit.
85
+ * @returns The digits, MSBF.
86
+ */
76
87
  function getDigitsFromBigint(
77
88
  number: bigint,
78
89
  width?: number,
79
90
  digitBits?: bigint,
80
91
  ): number[] {
81
- let bytes: number[] = new Array(width).fill(0);
82
-
83
92
  if (digitBits === undefined) {
84
93
  digitBits = 32n;
85
94
  }
86
95
 
87
96
  if (width === undefined) {
88
- width = parseInt(
89
- (BigInt(number.toString(2).length) / digitBits).toString(),
90
- );
97
+ width = getBigintWidth(number, 1);
91
98
  }
92
99
 
100
+ let digits = width / Number(digitBits / 8n);
101
+ let bytes: number[] = new Array(digits).fill(0);
102
+
93
103
  let mask = 0n;
94
- for (let i = digitBits - 1n; i > 0; i--) {
104
+ for (let i = digitBits; i > 0; i--) {
95
105
  mask <<= 1n;
96
- mask &= 1n;
106
+ mask |= 1n;
97
107
  }
98
108
 
99
- for (let i = width - 1; i > 0; i--) {
100
- let byte = parseInt((number & mask).toString());
101
- number = number >> digitBits;
109
+ for (let i = digits - 1; i >= 0; i--) {
110
+ let byte = Number(number & mask);
102
111
  bytes[i] = byte;
112
+ number = number >> digitBits;
103
113
  }
104
114
 
105
115
  return bytes;
@@ -153,24 +163,24 @@ function isPrime(number: bigint, tests: number): boolean {
153
163
  }
154
164
 
155
165
  function isPrimeForBase(number: bigint, base: bigint): boolean {
156
- let number_minus_one = number - 1n;
166
+ let numberMinusOne = number - 1n;
157
167
 
158
- let odd = number_minus_one;
159
- let base_times = 0;
168
+ let odd = numberMinusOne;
169
+ let baseTimes = 0;
160
170
 
161
171
  while (odd % 2n == 0n) {
162
172
  odd /= 2n;
163
- base_times += 1;
173
+ baseTimes += 1;
164
174
  }
165
175
 
166
- let odd_power = modPow(base, odd, number);
176
+ let oddPower = modPow(base, odd, number);
167
177
 
168
- for (let i = 0n; i < base_times; i++) {
169
- if (odd_power == 1n || odd_power == number_minus_one) {
178
+ for (let i = 0n; i < baseTimes; i++) {
179
+ if (oddPower == 1n || oddPower == numberMinusOne) {
170
180
  return true;
171
181
  }
172
182
 
173
- odd_power = modPow(odd_power, 2n, number);
183
+ oddPower = modPow(oddPower, 2n, number);
174
184
  }
175
185
 
176
186
  return false;
@@ -186,9 +196,15 @@ function getNextPrime(number: bigint): bigint {
186
196
  }
187
197
  }
188
198
 
199
+ /**
200
+ * Extracts the challenge's data from the string.
201
+ *
202
+ * @param challenge The raw challenge string.
203
+ * @returns The decoded challenge metadata.
204
+ */
189
205
  export function decode(challenge: string): Challenge {
190
- // Counts all dots in the challenge string
191
- if ((challenge.match(/\./g) || []).length != 1) {
206
+ // Counts all dots in the challenge string.
207
+ if (![1, 2].includes((challenge.match(/\./g) || []).length)) {
192
208
  throw Error(
193
209
  "The challenge string contained too many or not enough sections.",
194
210
  );
@@ -222,38 +238,41 @@ export function decode(challenge: string): Challenge {
222
238
  */
223
239
  export async function solve(challenge: Challenge): Promise<Solution> {
224
240
  let y = challenge.x;
225
-
226
241
  for (let i = 0; i < challenge.t; i++) {
227
- y = y ** BigInt(2) % BigInt(challenge.t);
242
+ y = modPow(y, 2n, challenge.n);
228
243
  }
229
244
 
230
- // Align to 4 bytes since each digit is a u32.
231
- let width = getBigintWidth(y, 4);
232
-
233
- let xBytes = getDigitsFromBigint(challenge.x, width, 8n);
234
- let yBytes = getDigitsFromBigint(y, width, 8n);
235
-
236
245
  let bytes = Array.from(new TextEncoder().encode("duckity"));
237
- bytes.concat(...xBytes);
238
- bytes.concat(...yBytes);
246
+ bytes = bytes.concat(
247
+ ...getDigitsFromBigint(challenge.n, RSA_MODULUS_SIZE, 8n),
248
+ );
249
+ bytes = bytes.concat(
250
+ ...getDigitsFromBigint(challenge.x, RSA_MODULUS_SIZE, 8n),
251
+ );
252
+ bytes = bytes.concat(
253
+ ...getDigitsFromBigint(BigInt(challenge.t), RSA_MODULUS_SIZE, 8n),
254
+ );
255
+ bytes = bytes.concat(...getDigitsFromBigint(y, RSA_MODULUS_SIZE, 8n));
239
256
  let hash = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
240
257
  let hashBytes = Array.from(new Uint8Array(hash));
241
258
 
242
259
  let z = getBigintFromDigits(hashBytes, 8n);
243
260
  let l = getNextPrime(z);
244
261
 
262
+ // We need 2^T = Q * L + R but 2^T is too big to calculate. Since 2^T is 2 << T in binary, we can
263
+ // calculate PI on the go by keeping that in mind without actually storing the whole number in
264
+ // memory.
265
+ let r = 1n;
245
266
  let pi = 1n;
246
- let acc = challenge.x;
247
- let exp_mod_l = 1n;
248
267
 
249
- for (let i = 0; i < challenge.t; i++) {
250
- let doubled = exp_mod_l * 2n;
268
+ for (let i = 0n; i < challenge.t; i++) {
269
+ r = r * 2n;
251
270
 
252
- if (doubled >= l) {
253
- pi = (pi * acc) % challenge.n;
254
- exp_mod_l = doubled - l;
271
+ if (r >= l) {
272
+ r = r - l;
273
+ pi = (pi * pi * challenge.x) % challenge.n;
255
274
  } else {
256
- exp_mod_l = doubled;
275
+ pi = (pi * pi) % challenge.n;
257
276
  }
258
277
  }
259
278
 
@@ -265,12 +284,15 @@ export async function solve(challenge: Challenge): Promise<Solution> {
265
284
 
266
285
  export function encode(original: string, solution: Solution): string {
267
286
  let solutionMeta: SolutionMeta = {
268
- y: getDigitsFromBigint(solution.y, undefined, 32n),
269
- pi: getDigitsFromBigint(solution.pi, undefined, 32n),
287
+ y: getDigitsFromBigint(solution.y, 512, 32n),
288
+ pi: getDigitsFromBigint(solution.pi, 512, 32n),
270
289
  };
271
290
 
272
291
  let solutionMetaJson = JSON.stringify(solutionMeta);
273
- let solutionMetaBase64 = btoa(solutionMetaJson);
292
+ let solutionMetaBase64 = btoa(solutionMetaJson)
293
+ .replaceAll("=", "")
294
+ .replaceAll("+", "-")
295
+ .replaceAll("/", "_");
274
296
 
275
297
  return `${original}.${solutionMetaBase64}`;
276
298
  }
@@ -1,17 +1,29 @@
1
1
  import * as Comlink from "comlink";
2
2
 
3
- /**
4
- * Web Worker that handles the processing of the challenges.
5
- */
6
- const worker = new Worker(new URL("./worker.js", import.meta.url), {
7
- type: "module",
8
- });
3
+ type WorkerApi = {
4
+ solve(challenge: string): Promise<string>;
5
+ };
6
+
7
+ let worker: Worker | undefined;
8
+ let api: Comlink.Remote<WorkerApi> | undefined;
9
9
 
10
10
  /**
11
- * Wrap the worker with Comlink to allow for easy communication between the main thread and the
12
- * worker thread. The worker exposes a `solve` function that takes a challenge string and returns a
11
+ * Wraps the worker with Comlink to allow for easy communication between the main thread and the
12
+ * worker thread. The API exposes a `solve` function that takes a challenge string and returns a
13
13
  * promise that resolves to the solution string.
14
14
  */
15
- export default Comlink.wrap<{
16
- solve(challenge: string): Promise<string>;
17
- }>(worker);
15
+ export function getWorkerApi(): Comlink.Remote<WorkerApi> {
16
+ if (typeof window === "undefined") {
17
+ throw new Error("Worker API can only be used in the browser");
18
+ }
19
+
20
+ if (!api) {
21
+ worker = new Worker(new URL("./worker.js", import.meta.url), {
22
+ type: "module",
23
+ });
24
+
25
+ api = Comlink.wrap<WorkerApi>(worker);
26
+ }
27
+
28
+ return api;
29
+ }
package/src/requests.ts CHANGED
@@ -63,7 +63,7 @@ export function get<T>(url: string, options?: RequestInit): Promise<T> {
63
63
  */
64
64
  export function post<T>(
65
65
  url: string,
66
- options?: Omit<RequestInit, "body" | "method"> & { body: any },
66
+ options?: Omit<RequestInit, "body" | "method"> & { body?: any },
67
67
  ): Promise<T> {
68
68
  return request(url, {
69
69
  method: "POST",