@duckity/js 0.1.6 → 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/worker.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2019 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
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)}});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@duckity/js",
3
3
  "type": "module",
4
- "version": "0.1.6",
4
+ "version": "0.2.0",
5
5
  "module": "src/index.ts",
6
6
  "types": "dist/index.d.ts",
7
7
  "collaborators": [
@@ -14,7 +14,6 @@
14
14
  "exports": {
15
15
  ".": {
16
16
  "import": "./dist/index.js",
17
- "require": "./dist/index.cjs",
18
17
  "types": "./dist/index.d.ts"
19
18
  }
20
19
  },
@@ -25,7 +24,7 @@
25
24
  "prepublishOnly": "bun run clean:all && bun run bundle && bun run clean:types"
26
25
  },
27
26
  "dependencies": {
28
- "@duckity/wasm": "1.1.16"
27
+ "comlink": "^4.4.2"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@rollup/plugin-commonjs": "^29.0.3",
@@ -34,10 +33,10 @@
34
33
  "@rollup/plugin-typescript": "^12.3.0",
35
34
  "@rollup/plugin-wasm": "^6.2.2",
36
35
  "@types/bun": "latest",
37
- "rollup": "^4.62.3",
38
- "rollup-plugin-dts": "^6.4.1"
36
+ "rollup": "^4.62.5",
37
+ "rollup-plugin-dts": "^6.5.1"
39
38
  },
40
39
  "peerDependencies": {
41
- "typescript": "^5"
40
+ "typescript": "^5.9.3"
42
41
  }
43
42
  }
package/src/index.ts CHANGED
@@ -6,16 +6,26 @@ import { post } from "./requests";
6
6
  */
7
7
  export interface GetDuckityChallengeOptions {
8
8
  /**
9
- * The custom-context threat correlation keys to be sent with the request.
10
- */
11
- keys?: { [key: string]: string };
12
-
13
- /**
14
- * The base URL to the API endpoint. Defaults to `https://quack.duckity.com` if not provided.
9
+ * The base URL to the API endpoint. Defaults to `https://api.duckity.com/d1` if not provided.
10
+ *
11
+ * The version must be specified in the URL path.
15
12
  *
16
- * Update this when self-hosting a Duckling. No trailing slash must be included in the URL.
13
+ * Only update this when self-hosting a Duckling.
17
14
  */
18
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;
19
29
  }
20
30
 
21
31
  /**
@@ -40,19 +50,16 @@ export async function solve(
40
50
  protectionProfileId: string,
41
51
  options?: GetDuckityChallengeOptions,
42
52
  ): Promise<string> {
53
+ if (options?.api && options.api.endsWith("/")) {
54
+ // Remove the trailing slash from the API URL if provided
55
+ options.api = options.api.slice(0, -1);
56
+ }
57
+
43
58
  let response: ChallengeResponse = await post(
44
- `${options?.api || "https://quack.duckity.com"}/v1/challenge`,
45
- {
46
- body: {
47
- id: protectionProfileId,
48
- keys: options?.keys || {},
49
- },
50
- },
59
+ `${options?.api || "https://api.duckity.com/d1"}/challenges/${protectionProfileId}/issue`,
51
60
  );
52
61
 
53
62
  let solution = await wrapper.solve(response.challenge);
54
63
 
55
64
  return solution;
56
65
  }
57
-
58
- export default { solve }
@@ -0,0 +1,282 @@
1
+ interface ChallengeMeta {
2
+ // The challenge's ID.
3
+ challenge_id: string;
4
+ // The client's IP.
5
+ ip: string;
6
+ // The unix timestamp, in milliseconds, in which the challenge was issued.
7
+ timestamp: number;
8
+ // The ID of the protection profile this challenge was issued for.
9
+ protection_profile_id: string;
10
+ // The digits of the N Wesolowski VDF parameter, most significant digit first.
11
+ n: Array<number>;
12
+ // The digits of the X Wesolowski VDF parameter, most significant digit first.
13
+ x: Array<number>;
14
+ // The T Wesolowski VDF parameter.
15
+ t: number;
16
+ }
17
+
18
+ export interface Challenge {
19
+ // The challenge's ID.
20
+ id: string;
21
+ // The client's IP.
22
+ ip: string;
23
+ // N Wesolowski VDF parameter.
24
+ n: bigint;
25
+ // X Wesolowski VDF parameter.
26
+ x: bigint;
27
+ // T Wesolowski VDF parameter.
28
+ t: number;
29
+ // The timestamp at which this challenge was issued.
30
+ timestamp: Date;
31
+ }
32
+
33
+ interface SolutionMeta {
34
+ y: number[];
35
+ pi: number[];
36
+ }
37
+
38
+ export interface Solution {
39
+ y: bigint;
40
+ pi: bigint;
41
+ }
42
+
43
+ /**
44
+ * Converts an array of digits to a bigint.
45
+ *
46
+ * @param digits The digits of the bigint, MSF.
47
+ * @param bits The amount of bits each digit consists of.
48
+ * @returns The formed bigint.
49
+ */
50
+ function getBigintFromDigits(digits: Array<number>, bits: bigint): bigint {
51
+ let number = 0n;
52
+
53
+ for (const digit of digits) {
54
+ number = (number << bits) | BigInt(digit);
55
+ }
56
+
57
+ return number;
58
+ }
59
+
60
+ function getBigintWidth(number: bigint, alignment?: number): number {
61
+ let bitWidth = 0;
62
+
63
+ if (number !== 0n) {
64
+ bitWidth = number.toString(2).length;
65
+ }
66
+
67
+ let byteWidth = Math.ceil((bitWidth + 7) / 8);
68
+
69
+ if (alignment !== undefined) {
70
+ byteWidth = Math.ceil(byteWidth / alignment) * alignment;
71
+ }
72
+
73
+ return byteWidth;
74
+ }
75
+
76
+ function getDigitsFromBigint(
77
+ number: bigint,
78
+ width?: number,
79
+ digitBits?: bigint,
80
+ ): number[] {
81
+ let bytes: number[] = new Array(width).fill(0);
82
+
83
+ if (digitBits === undefined) {
84
+ digitBits = 32n;
85
+ }
86
+
87
+ if (width === undefined) {
88
+ width = parseInt(
89
+ (BigInt(number.toString(2).length) / digitBits).toString(),
90
+ );
91
+ }
92
+
93
+ let mask = 0n;
94
+ for (let i = digitBits - 1n; i > 0; i--) {
95
+ mask <<= 1n;
96
+ mask &= 1n;
97
+ }
98
+
99
+ for (let i = width - 1; i > 0; i--) {
100
+ let byte = parseInt((number & mask).toString());
101
+ number = number >> digitBits;
102
+ bytes[i] = byte;
103
+ }
104
+
105
+ return bytes;
106
+ }
107
+
108
+ function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint {
109
+ let result = 1n;
110
+
111
+ while (exponent !== 0n) {
112
+ // (something & 1n) is faster than (something % 2n)
113
+ if ((exponent & 1n) === 1n) {
114
+ result *= base;
115
+ result %= modulus;
116
+ }
117
+
118
+ base **= 2n;
119
+ base %= modulus;
120
+
121
+ exponent /= 2n;
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ // Runs `tests` Miller-Rabin primality checks on `number`.
128
+ function isPrime(number: bigint, tests: number): boolean {
129
+ if ([1n, 2n, 3n, 5n, 7n].includes(number)) {
130
+ return true;
131
+ }
132
+
133
+ // (something & 1n) is faster than (something % 2n)
134
+ if ((number & 1n) === 0n) {
135
+ return false;
136
+ }
137
+
138
+ if (number % 5n === 0n) {
139
+ return false;
140
+ }
141
+
142
+ let base = 2n;
143
+
144
+ for (let i = 0n; i < BigInt(tests) && base + i < number; i++) {
145
+ let prime = isPrimeForBase(number, base + i);
146
+
147
+ if (!prime) {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ return true;
153
+ }
154
+
155
+ function isPrimeForBase(number: bigint, base: bigint): boolean {
156
+ let number_minus_one = number - 1n;
157
+
158
+ let odd = number_minus_one;
159
+ let base_times = 0;
160
+
161
+ while (odd % 2n == 0n) {
162
+ odd /= 2n;
163
+ base_times += 1;
164
+ }
165
+
166
+ let odd_power = modPow(base, odd, number);
167
+
168
+ for (let i = 0n; i < base_times; i++) {
169
+ if (odd_power == 1n || odd_power == number_minus_one) {
170
+ return true;
171
+ }
172
+
173
+ odd_power = modPow(odd_power, 2n, number);
174
+ }
175
+
176
+ return false;
177
+ }
178
+
179
+ function getNextPrime(number: bigint): bigint {
180
+ while (true) {
181
+ number += 1n;
182
+
183
+ if (isPrime(number, 40)) {
184
+ return number;
185
+ }
186
+ }
187
+ }
188
+
189
+ export function decode(challenge: string): Challenge {
190
+ // Counts all dots in the challenge string
191
+ if ((challenge.match(/\./g) || []).length != 1) {
192
+ throw Error(
193
+ "The challenge string contained too many or not enough sections.",
194
+ );
195
+ }
196
+
197
+ let [base64, _signature] = challenge.split(".", 2);
198
+ let json = atob(base64 as string);
199
+ let data: ChallengeMeta = JSON.parse(json as string);
200
+
201
+ let decoded: Challenge = {
202
+ id: data.challenge_id,
203
+ ip: data.ip,
204
+ n: getBigintFromDigits(data.n, 32n),
205
+ x: getBigintFromDigits(data.x, 32n),
206
+ t: data.t,
207
+ timestamp: new Date(data.timestamp),
208
+ };
209
+
210
+ return decoded;
211
+ }
212
+
213
+ /**
214
+ * Solves a challenge and returns the solution to it.
215
+ *
216
+ * The solution must be encoded into the final submittable string with `encode()`.
217
+ *
218
+ * Always call this function from a worker. Not doing so will block the UI thread.
219
+ *
220
+ * @param challenge The decoded challenge string. See `decode()`.
221
+ * @returns The solution to the provided challenge.
222
+ */
223
+ export async function solve(challenge: Challenge): Promise<Solution> {
224
+ let y = challenge.x;
225
+
226
+ for (let i = 0; i < challenge.t; i++) {
227
+ y = y ** BigInt(2) % BigInt(challenge.t);
228
+ }
229
+
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
+ let bytes = Array.from(new TextEncoder().encode("duckity"));
237
+ bytes.concat(...xBytes);
238
+ bytes.concat(...yBytes);
239
+ let hash = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
240
+ let hashBytes = Array.from(new Uint8Array(hash));
241
+
242
+ let z = getBigintFromDigits(hashBytes, 8n);
243
+ let l = getNextPrime(z);
244
+
245
+ let pi = 1n;
246
+ let acc = challenge.x;
247
+ let exp_mod_l = 1n;
248
+
249
+ for (let i = 0; i < challenge.t; i++) {
250
+ let doubled = exp_mod_l * 2n;
251
+
252
+ if (doubled >= l) {
253
+ pi = (pi * acc) % challenge.n;
254
+ exp_mod_l = doubled - l;
255
+ } else {
256
+ exp_mod_l = doubled;
257
+ }
258
+ }
259
+
260
+ return {
261
+ y,
262
+ pi,
263
+ };
264
+ }
265
+
266
+ export function encode(original: string, solution: Solution): string {
267
+ let solutionMeta: SolutionMeta = {
268
+ y: getDigitsFromBigint(solution.y, undefined, 32n),
269
+ pi: getDigitsFromBigint(solution.pi, undefined, 32n),
270
+ };
271
+
272
+ let solutionMetaJson = JSON.stringify(solutionMeta);
273
+ let solutionMetaBase64 = btoa(solutionMetaJson);
274
+
275
+ return `${original}.${solutionMetaBase64}`;
276
+ }
277
+
278
+ export default {
279
+ encode,
280
+ solve,
281
+ decode,
282
+ };
@@ -1,15 +1,16 @@
1
1
  import * as Comlink from "comlink";
2
- import { process } from "@duckity/wasm";
2
+ import processing from ".";
3
3
 
4
4
  /**
5
- * Expose the `process` function from the WASM module to the main thread via Comlink.
6
- *
7
- * This allows the main thread to call the `process` function in the worker thread, which will
8
- * handle the processing of the challenges without blocking the main thread.
5
+ * Solves a challenge in the background.
9
6
  */
10
7
  const api = {
11
- async process(challenge: string) {
12
- return process(challenge);
8
+ async solve(challenge: string) {
9
+ let decodedChallenge = processing.decode(challenge);
10
+ let solution = await processing.solve(decodedChallenge);
11
+ let solutionString = processing.encode(challenge, solution);
12
+
13
+ return solutionString;
13
14
  },
14
15
  };
15
16
 
@@ -1,10 +1,9 @@
1
- import wasm from "@duckity/wasm/worker.wasm";
2
1
  import * as Comlink from "comlink";
3
2
 
4
3
  /**
5
4
  * Web Worker that handles the processing of the challenges.
6
5
  */
7
- const worker = new Worker(wasm, {
6
+ const worker = new Worker(new URL("./worker.js", import.meta.url), {
8
7
  type: "module",
9
8
  });
10
9
 
package/src/requests.ts CHANGED
@@ -61,14 +61,17 @@ export function get<T>(url: string, options?: RequestInit): Promise<T> {
61
61
  * @param options Extra options to pass to the Fetch API, such as headers and body.
62
62
  * @returns The parsed JSON response from the server if the request is successful.
63
63
  */
64
- export function post<T>(url: string, options?: Omit<RequestInit, "body"> & { body: any }): Promise<T> {
64
+ export function post<T>(
65
+ url: string,
66
+ options?: Omit<RequestInit, "body" | "method"> & { body: any },
67
+ ): Promise<T> {
65
68
  return request(url, {
66
69
  method: "POST",
67
70
  headers: {
68
- "Content-Type": "application/json",
71
+ ...(options?.body ? { "Content-Type": "application/json" } : {}),
69
72
  ...options?.headers,
70
73
  },
71
- body: options?.body ? JSON.stringify(options.body) : undefined,
72
74
  ...options,
75
+ body: options?.body ? JSON.stringify(options.body) : undefined,
73
76
  });
74
77
  }