-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
335 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,10 @@ | ||
export { DAPClient, DAPClient as default } from "./client"; | ||
export { | ||
DAPClient, | ||
DAPClient as default, | ||
KnownVdafSpec, | ||
VdafMeasurement, | ||
} from "./client"; | ||
export { DAPError } from "./errors"; | ||
export type { ReportOptions } from "./client"; | ||
export { TaskId } from "./taskId"; | ||
export { HpkeConfig, HpkeConfigList } from "./hpkeConfig"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"name": "divviup", | ||
"version": "0.1.0", | ||
"description": "", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"source": "src/index.ts", | ||
"module": "dist/module.js", | ||
"browser": "dist/browser.js", | ||
"type": "module", | ||
"license": "MPL-2.0", | ||
"scripts": { | ||
"clean": "rm -rf dist/*", | ||
"build:clean": "npm run clean && npm run build", | ||
"build": "npm run build:web && npm run build:node", | ||
"build:web": "esbuild browser=src/index.ts --bundle --loader:.wasm=binary --format=esm --outdir=dist --sourcemap --minify", | ||
"build:node": "tsc -p ./tsconfig.json", | ||
"docs": "typedoc src", | ||
"test": "mocha \"src/**/*.spec.ts\"", | ||
"lint": "eslint src --ext .ts && prettier -c src", | ||
"format": "prettier -w src", | ||
"check": "tsc --noEmit -p ./tsconfig.json", | ||
"test:coverage": "c8 npm test" | ||
}, | ||
"dependencies": { | ||
"@divviup/dap": "^0.1.0" | ||
}, | ||
"devDependencies": { | ||
"hpke": "^0.5.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import assert from "assert"; | ||
import { inspect } from "node:util"; | ||
import { DivviupClient, sendMeasurement } from "."; | ||
import { HpkeConfigList, HpkeConfig, TaskId } from "@divviup/dap"; | ||
import * as hpke from "hpke"; | ||
|
||
describe("DivviupClient", () => { | ||
it("fetches task from an id", async () => { | ||
let taskId = TaskId.random().toString(); | ||
let client = new DivviupClient(taskId); | ||
let fetch = mockFetch({ | ||
...dapMocks(taskId), | ||
[`https://api.staging.divviup.org/tasks/${taskId}`]: [ | ||
{ | ||
status: 200, | ||
body: JSON.stringify(task(taskId)), | ||
contentType: "application/json", | ||
}, | ||
], | ||
}); | ||
client.fetch = fetch; | ||
await client.sendMeasurement(10); | ||
assert.equal(fetch.calls.length, 4); | ||
assert.deepEqual(fetch.callStrings(), [ | ||
`GET https://api.staging.divviup.org/tasks/${taskId}`, | ||
`GET https://a.example.com/v1/hpke_config?task_id=${taskId}`, | ||
`GET https://b.example.com/dap/hpke_config?task_id=${taskId}`, | ||
`PUT https://a.example.com/v1/tasks/${taskId}/reports`, | ||
]); | ||
}); | ||
|
||
it("fetches task from a task url", async () => { | ||
let taskId = TaskId.random().toString(); | ||
let client = new DivviupClient( | ||
`https://production.divvi.up/v3/different-url/${taskId}.json`, | ||
); | ||
let fetch = mockFetch({ | ||
...dapMocks(taskId), | ||
[`https://production.divvi.up/v3/different-url/${taskId}.json`]: [ | ||
{ | ||
status: 200, | ||
body: JSON.stringify(task(taskId)), | ||
contentType: "application/json", | ||
}, | ||
], | ||
}); | ||
client.fetch = fetch; | ||
await client.sendMeasurement(10); | ||
assert.equal(fetch.calls.length, 4); | ||
assert.deepEqual(fetch.callStrings(), [ | ||
`GET https://production.divvi.up/v3/different-url/${taskId}.json`, | ||
`GET https://a.example.com/v1/hpke_config?task_id=${taskId}`, | ||
`GET https://b.example.com/dap/hpke_config?task_id=${taskId}`, | ||
`PUT https://a.example.com/v1/tasks/${taskId}/reports`, | ||
]); | ||
}); | ||
}); | ||
|
||
describe("sendMeasurement", () => { | ||
it("fetches task from an id", async () => { | ||
let taskId = TaskId.random().toString(); | ||
let fetch = mockFetch({ | ||
...dapMocks(taskId), | ||
[`https://api.staging.divviup.org/tasks/${taskId}`]: [ | ||
{ | ||
status: 200, | ||
body: JSON.stringify(task(taskId)), | ||
contentType: "application/json", | ||
}, | ||
], | ||
}); | ||
|
||
await sendMeasurement(taskId, 10, fetch); | ||
|
||
assert.equal(fetch.calls.length, 4); | ||
assert.deepEqual(fetch.callStrings(), [ | ||
`GET https://api.staging.divviup.org/tasks/${taskId}`, | ||
`GET https://a.example.com/v1/hpke_config?task_id=${taskId}`, | ||
`GET https://b.example.com/dap/hpke_config?task_id=${taskId}`, | ||
`PUT https://a.example.com/v1/tasks/${taskId}/reports`, | ||
]); | ||
}); | ||
}); | ||
|
||
function dapMocks(taskId: string) { | ||
return { | ||
[`https://a.example.com/v1/hpke_config?task_id=${taskId}`]: [ | ||
hpkeConfigResponse(), | ||
], | ||
|
||
[`https://b.example.com/dap/hpke_config?task_id=${taskId}`]: [ | ||
hpkeConfigResponse(), | ||
], | ||
|
||
[`https://api.staging.divviup.org/tasks/${taskId}`]: [ | ||
{ | ||
status: 200, | ||
body: JSON.stringify(task(taskId)), | ||
contentType: "application/json", | ||
}, | ||
], | ||
[`https://a.example.com/v1/tasks/${taskId}/reports`]: [{ status: 201 }], | ||
}; | ||
} | ||
|
||
interface Fetch { | ||
(input: RequestInfo, init?: RequestInit | undefined): Promise<Response>; | ||
calls: [RequestInfo, RequestInit | undefined][]; | ||
callStrings(): string[]; | ||
} | ||
|
||
interface ResponseSpec { | ||
body?: Buffer | Uint8Array | number[] | string; | ||
contentType?: string; | ||
status?: number; | ||
} | ||
|
||
function mockFetch(mocks: { [url: string]: ResponseSpec[] }): Fetch { | ||
function fakeFetch( | ||
input: RequestInfo, | ||
init?: RequestInit | undefined, | ||
): Promise<Response> { | ||
fakeFetch.calls.push([input, init]); | ||
const responseSpec = mocks[input.toString()]; | ||
const response = responseSpec?.shift(); | ||
|
||
if (!response) { | ||
throw new Error( | ||
`received unhandled request.\n\nurl: ${input.toString()}.\n\nmocks: ${inspect( | ||
mocks, | ||
).slice(1, -1)}`, | ||
); | ||
} | ||
|
||
return Promise.resolve( | ||
new Response(Buffer.from(response.body || ""), { | ||
status: response.status || 200, | ||
headers: { "Content-Type": response.contentType || "text/plain" }, | ||
}), | ||
); | ||
} | ||
|
||
fakeFetch.calls = [] as [RequestInfo, RequestInit | undefined][]; | ||
fakeFetch.callStrings = function () { | ||
return this.calls.map((x) => `${x[1]?.method || "GET"} ${x[0]}`); | ||
}; | ||
return fakeFetch; | ||
} | ||
|
||
function task(taskId: string): { | ||
vdaf: { | ||
type: "sum"; | ||
bits: number; | ||
}; | ||
helper: string; | ||
leader: string; | ||
id: string; | ||
time_precision_seconds: number; | ||
} { | ||
return { | ||
vdaf: { | ||
type: "sum", | ||
bits: 16, | ||
}, | ||
leader: "https://a.example.com/v1", | ||
helper: "https://b.example.com/dap/", | ||
id: taskId, | ||
time_precision_seconds: 1, | ||
}; | ||
} | ||
|
||
function hpkeConfigResponse(config = buildHpkeConfigList()): ResponseSpec { | ||
return { | ||
body: config.encode(), | ||
contentType: "application/dap-hpke-config-list", | ||
}; | ||
} | ||
|
||
function buildHpkeConfigList(): HpkeConfigList { | ||
return new HpkeConfigList([ | ||
new HpkeConfig( | ||
Math.floor(Math.random() * 255), | ||
hpke.Kem.DhP256HkdfSha256, | ||
hpke.Kdf.Sha256, | ||
hpke.Aead.AesGcm128, | ||
Buffer.from(new hpke.Keypair(hpke.Kem.DhP256HkdfSha256).public_key), | ||
), | ||
]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { DAPClient } from "@divviup/dap"; | ||
import { KnownVdafSpec } from "@divviup/dap/dist/client"; | ||
|
||
type Fetch = ( | ||
input: RequestInfo, | ||
init?: RequestInit | undefined, | ||
) => Promise<Response>; | ||
|
||
interface PublicTask { | ||
id: string; | ||
vdaf: KnownVdafSpec; | ||
leader: string; | ||
helper: string; | ||
time_precision_seconds: number; | ||
} | ||
|
||
type AnyMeasurement = number | bigint | boolean; | ||
type GenericDAPClient = DAPClient<KnownVdafSpec, AnyMeasurement>; | ||
|
||
export class DivviupClient { | ||
#baseUrl = new URL("https://api.staging.divviup.org/tasks"); | ||
#fetch: Fetch = globalThis.fetch.bind(globalThis); | ||
#dapClient: null | GenericDAPClient = null; | ||
#taskUrl: URL; | ||
|
||
/** @internal */ | ||
set fetch(fetch: Fetch) { | ||
this.#fetch = fetch; | ||
if (this.#dapClient) this.#dapClient.fetch = fetch; | ||
} | ||
|
||
constructor(urlOrTaskId: string | URL) { | ||
if (typeof urlOrTaskId === "string") { | ||
try { | ||
this.#taskUrl = new URL(urlOrTaskId); | ||
} catch (e) { | ||
this.#taskUrl = new URL(`${this.#baseUrl}/${urlOrTaskId}`); | ||
} | ||
} else { | ||
this.#taskUrl = urlOrTaskId; | ||
} | ||
} | ||
|
||
private async taskClient(): Promise<GenericDAPClient> { | ||
if (this.#dapClient) return this.#dapClient; | ||
let response = await this.#fetch(this.#taskUrl.toString()); | ||
let task = (await response.json()) as PublicTask; | ||
let { leader, helper, vdaf, id, time_precision_seconds } = task; | ||
let client = new DAPClient({ | ||
taskId: id, | ||
leader, | ||
helper, | ||
id, | ||
timePrecisionSeconds: time_precision_seconds, | ||
...vdaf, | ||
}); | ||
client.fetch = this.#fetch; | ||
this.#dapClient = client; | ||
return client; | ||
} | ||
|
||
async sendMeasurement(measurement: AnyMeasurement) { | ||
const client = await this.taskClient(); | ||
return client.sendMeasurement(measurement); | ||
} | ||
} | ||
|
||
export default DivviupClient; | ||
|
||
export async function sendMeasurement( | ||
urlOrTaskId: string | URL, | ||
measurement: AnyMeasurement, | ||
fetch?: Fetch, | ||
) { | ||
let client = new DivviupClient(urlOrTaskId); | ||
if (fetch) client.fetch = fetch; | ||
return client.sendMeasurement(measurement); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"rootDir": "src", | ||
"outDir": "dist" | ||
}, | ||
"include": ["./src/*.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extends": ["../../typedoc.base.json"], | ||
"entryPointStrategy": "expand", | ||
"entryPoints": ["src/index.ts"] | ||
} |