CI / test (push) Successful in 4s
Demo app (web/):
- Collections: select, rename, remove (x button per row), delete
- Features: add point (lon/lat validation), remove, list in selected collection
- QR codes for pk (private) and pb (public) keys
- Restore public key from private key
- 409 Conflict handled for already-registered login
- Title: Momswap Geo Backend Use-Cases Test
Backend:
- PATCH /v1/collections/{id} for rename
- DELETE /v1/collections/{id}
- Clearer lon/lat validation errors (-180..180, -90..90)
Client:
- updateCollection, deleteCollection, derivePublicKey
Docs:
- docs/frontend-development.md (demo app, local dev)
- README links to all docs
Made-with: Cursor
652 lines
19 KiB
JavaScript
652 lines
19 KiB
JavaScript
// node_modules/@noble/ed25519/index.js
|
|
/*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
|
|
var ed25519_CURVE = {
|
|
p: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,
|
|
n: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,
|
|
h: 8n,
|
|
a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,
|
|
d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,
|
|
Gx: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,
|
|
Gy: 0x6666666666666666666666666666666666666666666666666666666666666658n
|
|
};
|
|
var { p: P, n: N, Gx, Gy, a: _a, d: _d, h } = ed25519_CURVE;
|
|
var L = 32;
|
|
var L2 = 64;
|
|
var captureTrace = (...args) => {
|
|
if ("captureStackTrace" in Error && typeof Error.captureStackTrace === "function") {
|
|
Error.captureStackTrace(...args);
|
|
}
|
|
};
|
|
var err = (message = "") => {
|
|
const e = new Error(message);
|
|
captureTrace(e, err);
|
|
throw e;
|
|
};
|
|
var isBig = (n) => typeof n === "bigint";
|
|
var isStr = (s) => typeof s === "string";
|
|
var isBytes = (a) => a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
|
|
var abytes = (value, length, title = "") => {
|
|
const bytes = isBytes(value);
|
|
const len = value?.length;
|
|
const needsLen = length !== undefined;
|
|
if (!bytes || needsLen && len !== length) {
|
|
const prefix = title && `"${title}" `;
|
|
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
err(prefix + "expected Uint8Array" + ofLen + ", got " + got);
|
|
}
|
|
return value;
|
|
};
|
|
var u8n = (len) => new Uint8Array(len);
|
|
var u8fr = (buf) => Uint8Array.from(buf);
|
|
var padh = (n, pad) => n.toString(16).padStart(pad, "0");
|
|
var bytesToHex = (b) => Array.from(abytes(b)).map((e) => padh(e, 2)).join("");
|
|
var C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
|
|
var _ch = (ch) => {
|
|
if (ch >= C._0 && ch <= C._9)
|
|
return ch - C._0;
|
|
if (ch >= C.A && ch <= C.F)
|
|
return ch - (C.A - 10);
|
|
if (ch >= C.a && ch <= C.f)
|
|
return ch - (C.a - 10);
|
|
return;
|
|
};
|
|
var hexToBytes = (hex) => {
|
|
const e = "hex invalid";
|
|
if (!isStr(hex))
|
|
return err(e);
|
|
const hl = hex.length;
|
|
const al = hl / 2;
|
|
if (hl % 2)
|
|
return err(e);
|
|
const array = u8n(al);
|
|
for (let ai = 0, hi = 0;ai < al; ai++, hi += 2) {
|
|
const n1 = _ch(hex.charCodeAt(hi));
|
|
const n2 = _ch(hex.charCodeAt(hi + 1));
|
|
if (n1 === undefined || n2 === undefined)
|
|
return err(e);
|
|
array[ai] = n1 * 16 + n2;
|
|
}
|
|
return array;
|
|
};
|
|
var cr = () => globalThis?.crypto;
|
|
var subtle = () => cr()?.subtle ?? err("crypto.subtle must be defined, consider polyfill");
|
|
var concatBytes = (...arrs) => {
|
|
const r = u8n(arrs.reduce((sum, a) => sum + abytes(a).length, 0));
|
|
let pad = 0;
|
|
arrs.forEach((a) => {
|
|
r.set(a, pad);
|
|
pad += a.length;
|
|
});
|
|
return r;
|
|
};
|
|
var big = BigInt;
|
|
var assertRange = (n, min, max, msg = "bad number: out of range") => isBig(n) && min <= n && n < max ? n : err(msg);
|
|
var M = (a, b = P) => {
|
|
const r = a % b;
|
|
return r >= 0n ? r : b + r;
|
|
};
|
|
var modN = (a) => M(a, N);
|
|
var invert = (num, md) => {
|
|
if (num === 0n || md <= 0n)
|
|
err("no inverse n=" + num + " mod=" + md);
|
|
let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
|
|
while (a !== 0n) {
|
|
const q = b / a, r = b % a;
|
|
const m = x - u * q, n = y - v * q;
|
|
b = a, a = r, x = u, y = v, u = m, v = n;
|
|
}
|
|
return b === 1n ? M(x, md) : err("no inverse");
|
|
};
|
|
var apoint = (p) => p instanceof Point ? p : err("Point expected");
|
|
var B256 = 2n ** 256n;
|
|
|
|
class Point {
|
|
static BASE;
|
|
static ZERO;
|
|
X;
|
|
Y;
|
|
Z;
|
|
T;
|
|
constructor(X, Y, Z, T) {
|
|
const max = B256;
|
|
this.X = assertRange(X, 0n, max);
|
|
this.Y = assertRange(Y, 0n, max);
|
|
this.Z = assertRange(Z, 1n, max);
|
|
this.T = assertRange(T, 0n, max);
|
|
Object.freeze(this);
|
|
}
|
|
static CURVE() {
|
|
return ed25519_CURVE;
|
|
}
|
|
static fromAffine(p) {
|
|
return new Point(p.x, p.y, 1n, M(p.x * p.y));
|
|
}
|
|
static fromBytes(hex, zip215 = false) {
|
|
const d = _d;
|
|
const normed = u8fr(abytes(hex, L));
|
|
const lastByte = hex[31];
|
|
normed[31] = lastByte & ~128;
|
|
const y = bytesToNumLE(normed);
|
|
const max = zip215 ? B256 : P;
|
|
assertRange(y, 0n, max);
|
|
const y2 = M(y * y);
|
|
const u = M(y2 - 1n);
|
|
const v = M(d * y2 + 1n);
|
|
let { isValid, value: x } = uvRatio(u, v);
|
|
if (!isValid)
|
|
err("bad point: y not sqrt");
|
|
const isXOdd = (x & 1n) === 1n;
|
|
const isLastByteOdd = (lastByte & 128) !== 0;
|
|
if (!zip215 && x === 0n && isLastByteOdd)
|
|
err("bad point: x==0, isLastByteOdd");
|
|
if (isLastByteOdd !== isXOdd)
|
|
x = M(-x);
|
|
return new Point(x, y, 1n, M(x * y));
|
|
}
|
|
static fromHex(hex, zip215) {
|
|
return Point.fromBytes(hexToBytes(hex), zip215);
|
|
}
|
|
get x() {
|
|
return this.toAffine().x;
|
|
}
|
|
get y() {
|
|
return this.toAffine().y;
|
|
}
|
|
assertValidity() {
|
|
const a = _a;
|
|
const d = _d;
|
|
const p = this;
|
|
if (p.is0())
|
|
return err("bad point: ZERO");
|
|
const { X, Y, Z, T } = p;
|
|
const X2 = M(X * X);
|
|
const Y2 = M(Y * Y);
|
|
const Z2 = M(Z * Z);
|
|
const Z4 = M(Z2 * Z2);
|
|
const aX2 = M(X2 * a);
|
|
const left = M(Z2 * M(aX2 + Y2));
|
|
const right = M(Z4 + M(d * M(X2 * Y2)));
|
|
if (left !== right)
|
|
return err("bad point: equation left != right (1)");
|
|
const XY = M(X * Y);
|
|
const ZT = M(Z * T);
|
|
if (XY !== ZT)
|
|
return err("bad point: equation left != right (2)");
|
|
return this;
|
|
}
|
|
equals(other) {
|
|
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
const { X: X2, Y: Y2, Z: Z2 } = apoint(other);
|
|
const X1Z2 = M(X1 * Z2);
|
|
const X2Z1 = M(X2 * Z1);
|
|
const Y1Z2 = M(Y1 * Z2);
|
|
const Y2Z1 = M(Y2 * Z1);
|
|
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
|
|
}
|
|
is0() {
|
|
return this.equals(I);
|
|
}
|
|
negate() {
|
|
return new Point(M(-this.X), this.Y, this.Z, M(-this.T));
|
|
}
|
|
double() {
|
|
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
const a = _a;
|
|
const A = M(X1 * X1);
|
|
const B = M(Y1 * Y1);
|
|
const C2 = M(2n * M(Z1 * Z1));
|
|
const D = M(a * A);
|
|
const x1y1 = X1 + Y1;
|
|
const E = M(M(x1y1 * x1y1) - A - B);
|
|
const G = D + B;
|
|
const F = G - C2;
|
|
const H = D - B;
|
|
const X3 = M(E * F);
|
|
const Y3 = M(G * H);
|
|
const T3 = M(E * H);
|
|
const Z3 = M(F * G);
|
|
return new Point(X3, Y3, Z3, T3);
|
|
}
|
|
add(other) {
|
|
const { X: X1, Y: Y1, Z: Z1, T: T1 } = this;
|
|
const { X: X2, Y: Y2, Z: Z2, T: T2 } = apoint(other);
|
|
const a = _a;
|
|
const d = _d;
|
|
const A = M(X1 * X2);
|
|
const B = M(Y1 * Y2);
|
|
const C2 = M(T1 * d * T2);
|
|
const D = M(Z1 * Z2);
|
|
const E = M((X1 + Y1) * (X2 + Y2) - A - B);
|
|
const F = M(D - C2);
|
|
const G = M(D + C2);
|
|
const H = M(B - a * A);
|
|
const X3 = M(E * F);
|
|
const Y3 = M(G * H);
|
|
const T3 = M(E * H);
|
|
const Z3 = M(F * G);
|
|
return new Point(X3, Y3, Z3, T3);
|
|
}
|
|
subtract(other) {
|
|
return this.add(apoint(other).negate());
|
|
}
|
|
multiply(n, safe = true) {
|
|
if (!safe && (n === 0n || this.is0()))
|
|
return I;
|
|
assertRange(n, 1n, N);
|
|
if (n === 1n)
|
|
return this;
|
|
if (this.equals(G))
|
|
return wNAF(n).p;
|
|
let p = I;
|
|
let f = G;
|
|
for (let d = this;n > 0n; d = d.double(), n >>= 1n) {
|
|
if (n & 1n)
|
|
p = p.add(d);
|
|
else if (safe)
|
|
f = f.add(d);
|
|
}
|
|
return p;
|
|
}
|
|
multiplyUnsafe(scalar) {
|
|
return this.multiply(scalar, false);
|
|
}
|
|
toAffine() {
|
|
const { X, Y, Z } = this;
|
|
if (this.equals(I))
|
|
return { x: 0n, y: 1n };
|
|
const iz = invert(Z, P);
|
|
if (M(Z * iz) !== 1n)
|
|
err("invalid inverse");
|
|
const x = M(X * iz);
|
|
const y = M(Y * iz);
|
|
return { x, y };
|
|
}
|
|
toBytes() {
|
|
const { x, y } = this.assertValidity().toAffine();
|
|
const b = numTo32bLE(y);
|
|
b[31] |= x & 1n ? 128 : 0;
|
|
return b;
|
|
}
|
|
toHex() {
|
|
return bytesToHex(this.toBytes());
|
|
}
|
|
clearCofactor() {
|
|
return this.multiply(big(h), false);
|
|
}
|
|
isSmallOrder() {
|
|
return this.clearCofactor().is0();
|
|
}
|
|
isTorsionFree() {
|
|
let p = this.multiply(N / 2n, false).double();
|
|
if (N % 2n)
|
|
p = p.add(this);
|
|
return p.is0();
|
|
}
|
|
}
|
|
var G = new Point(Gx, Gy, 1n, M(Gx * Gy));
|
|
var I = new Point(0n, 1n, 1n, 0n);
|
|
Point.BASE = G;
|
|
Point.ZERO = I;
|
|
var numTo32bLE = (num) => hexToBytes(padh(assertRange(num, 0n, B256), L2)).reverse();
|
|
var bytesToNumLE = (b) => big("0x" + bytesToHex(u8fr(abytes(b)).reverse()));
|
|
var pow2 = (x, power) => {
|
|
let r = x;
|
|
while (power-- > 0n) {
|
|
r *= r;
|
|
r %= P;
|
|
}
|
|
return r;
|
|
};
|
|
var pow_2_252_3 = (x) => {
|
|
const x2 = x * x % P;
|
|
const b2 = x2 * x % P;
|
|
const b4 = pow2(b2, 2n) * b2 % P;
|
|
const b5 = pow2(b4, 1n) * x % P;
|
|
const b10 = pow2(b5, 5n) * b5 % P;
|
|
const b20 = pow2(b10, 10n) * b10 % P;
|
|
const b40 = pow2(b20, 20n) * b20 % P;
|
|
const b80 = pow2(b40, 40n) * b40 % P;
|
|
const b160 = pow2(b80, 80n) * b80 % P;
|
|
const b240 = pow2(b160, 80n) * b80 % P;
|
|
const b250 = pow2(b240, 10n) * b10 % P;
|
|
const pow_p_5_8 = pow2(b250, 2n) * x % P;
|
|
return { pow_p_5_8, b2 };
|
|
};
|
|
var RM1 = 0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n;
|
|
var uvRatio = (u, v) => {
|
|
const v3 = M(v * v * v);
|
|
const v7 = M(v3 * v3 * v);
|
|
const pow = pow_2_252_3(u * v7).pow_p_5_8;
|
|
let x = M(u * v3 * pow);
|
|
const vx2 = M(v * x * x);
|
|
const root1 = x;
|
|
const root2 = M(x * RM1);
|
|
const useRoot1 = vx2 === u;
|
|
const useRoot2 = vx2 === M(-u);
|
|
const noRoot = vx2 === M(-u * RM1);
|
|
if (useRoot1)
|
|
x = root1;
|
|
if (useRoot2 || noRoot)
|
|
x = root2;
|
|
if ((M(x) & 1n) === 1n)
|
|
x = M(-x);
|
|
return { isValid: useRoot1 || useRoot2, value: x };
|
|
};
|
|
var modL_LE = (hash) => modN(bytesToNumLE(hash));
|
|
var sha512a = (...m) => hashes.sha512Async(concatBytes(...m));
|
|
var hash2extK = (hashed) => {
|
|
const head = hashed.slice(0, L);
|
|
head[0] &= 248;
|
|
head[31] &= 127;
|
|
head[31] |= 64;
|
|
const prefix = hashed.slice(L, L2);
|
|
const scalar = modL_LE(head);
|
|
const point = G.multiply(scalar);
|
|
const pointBytes = point.toBytes();
|
|
return { head, prefix, scalar, point, pointBytes };
|
|
};
|
|
var getExtendedPublicKeyAsync = (secretKey) => sha512a(abytes(secretKey, L)).then(hash2extK);
|
|
var getPublicKeyAsync = (secretKey) => getExtendedPublicKeyAsync(secretKey).then((p) => p.pointBytes);
|
|
var hashFinishA = (res) => sha512a(res.hashable).then(res.finish);
|
|
var _sign = (e, rBytes, msg) => {
|
|
const { pointBytes: P2, scalar: s } = e;
|
|
const r = modL_LE(rBytes);
|
|
const R = G.multiply(r).toBytes();
|
|
const hashable = concatBytes(R, P2, msg);
|
|
const finish = (hashed) => {
|
|
const S = modN(r + modL_LE(hashed) * s);
|
|
return abytes(concatBytes(R, numTo32bLE(S)), L2);
|
|
};
|
|
return { hashable, finish };
|
|
};
|
|
var signAsync = async (message, secretKey) => {
|
|
const m = abytes(message);
|
|
const e = await getExtendedPublicKeyAsync(secretKey);
|
|
const rBytes = await sha512a(e.prefix, m);
|
|
return hashFinishA(_sign(e, rBytes, m));
|
|
};
|
|
var hashes = {
|
|
sha512Async: async (message) => {
|
|
const s = subtle();
|
|
const m = concatBytes(message);
|
|
return u8n(await s.digest("SHA-512", m.buffer));
|
|
},
|
|
sha512: undefined
|
|
};
|
|
var W = 8;
|
|
var scalarBits = 256;
|
|
var pwindows = Math.ceil(scalarBits / W) + 1;
|
|
var pwindowSize = 2 ** (W - 1);
|
|
var precompute = () => {
|
|
const points = [];
|
|
let p = G;
|
|
let b = p;
|
|
for (let w = 0;w < pwindows; w++) {
|
|
b = p;
|
|
points.push(b);
|
|
for (let i = 1;i < pwindowSize; i++) {
|
|
b = b.add(p);
|
|
points.push(b);
|
|
}
|
|
p = b.double();
|
|
}
|
|
return points;
|
|
};
|
|
var Gpows = undefined;
|
|
var ctneg = (cnd, p) => {
|
|
const n = p.negate();
|
|
return cnd ? n : p;
|
|
};
|
|
var wNAF = (n) => {
|
|
const comp = Gpows || (Gpows = precompute());
|
|
let p = I;
|
|
let f = G;
|
|
const pow_2_w = 2 ** W;
|
|
const maxNum = pow_2_w;
|
|
const mask = big(pow_2_w - 1);
|
|
const shiftBy = big(W);
|
|
for (let w = 0;w < pwindows; w++) {
|
|
let wbits = Number(n & mask);
|
|
n >>= shiftBy;
|
|
if (wbits > pwindowSize) {
|
|
wbits -= maxNum;
|
|
n += 1n;
|
|
}
|
|
const off = w * pwindowSize;
|
|
const offF = off;
|
|
const offP = off + Math.abs(wbits) - 1;
|
|
const isEven = w % 2 !== 0;
|
|
const isNeg = wbits < 0;
|
|
if (wbits === 0) {
|
|
f = f.add(ctneg(isEven, comp[offF]));
|
|
} else {
|
|
p = p.add(ctneg(isNeg, comp[offP]));
|
|
}
|
|
}
|
|
if (n !== 0n)
|
|
err("invalid wnaf");
|
|
return { p, f };
|
|
};
|
|
|
|
// src/encoding.ts
|
|
function bytesToBase64Url(bytes) {
|
|
const bin = Array.from(bytes).map((b) => String.fromCharCode(b)).join("");
|
|
if (typeof btoa !== "undefined") {
|
|
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
}
|
|
const b64 = globalThis.Buffer.from(bytes).toString("base64");
|
|
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
}
|
|
function base64UrlToBytes(input) {
|
|
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
const padLen = (4 - normalized.length % 4) % 4;
|
|
const b64 = normalized + "=".repeat(padLen);
|
|
if (typeof atob !== "undefined") {
|
|
const bin = atob(b64);
|
|
const out = new Uint8Array(bin.length);
|
|
for (let i = 0;i < bin.length; i++) {
|
|
out[i] = bin.charCodeAt(i);
|
|
}
|
|
return out;
|
|
}
|
|
return new Uint8Array(globalThis.Buffer.from(b64, "base64"));
|
|
}
|
|
function textToBytes(value) {
|
|
return new TextEncoder().encode(value);
|
|
}
|
|
|
|
// src/keys.ts
|
|
function randomPrivateKey() {
|
|
const out = new Uint8Array(32);
|
|
crypto.getRandomValues(out);
|
|
return out;
|
|
}
|
|
async function generateKeyPair() {
|
|
const privateKey = randomPrivateKey();
|
|
const publicKey = await getPublicKeyAsync(privateKey);
|
|
return {
|
|
publicKey: bytesToBase64Url(publicKey),
|
|
privateKey: bytesToBase64Url(privateKey)
|
|
};
|
|
}
|
|
async function getPublicKeyFromPrivate(privateKeyBase64) {
|
|
const privateKey = base64UrlToBytes(privateKeyBase64);
|
|
const publicKey = await getPublicKeyAsync(privateKey);
|
|
return bytesToBase64Url(publicKey);
|
|
}
|
|
async function signMessage(privateKeyBase64, message) {
|
|
const privateKey = base64UrlToBytes(privateKeyBase64);
|
|
const signature = await signAsync(textToBytes(message), privateKey);
|
|
return bytesToBase64Url(signature);
|
|
}
|
|
|
|
// src/storage.ts
|
|
var DEFAULT_KEYS_STORAGE_KEY = "geo_api_keys_v1";
|
|
function saveKeys(storage, keys, storageKey = DEFAULT_KEYS_STORAGE_KEY) {
|
|
storage.setItem(storageKey, JSON.stringify(keys));
|
|
}
|
|
function loadKeys(storage, storageKey = DEFAULT_KEYS_STORAGE_KEY) {
|
|
const raw = storage.getItem(storageKey);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed.publicKey || !parsed.privateKey) {
|
|
return null;
|
|
}
|
|
return parsed;
|
|
}
|
|
function clearKeys(storage, storageKey = DEFAULT_KEYS_STORAGE_KEY) {
|
|
storage.removeItem(storageKey);
|
|
}
|
|
|
|
// src/GeoApiClient.ts
|
|
class GeoApiClient {
|
|
baseUrl;
|
|
storage;
|
|
storageKey;
|
|
accessToken = null;
|
|
constructor(baseUrl, storage, storageKey = DEFAULT_KEYS_STORAGE_KEY) {
|
|
this.baseUrl = baseUrl.replace(/\/+$/g, "");
|
|
this.storage = storage;
|
|
this.storageKey = storageKey;
|
|
}
|
|
async ensureKeysInStorage() {
|
|
const existing = loadKeys(this.storage, this.storageKey);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
const generated = await generateKeyPair();
|
|
saveKeys(this.storage, generated, this.storageKey);
|
|
return generated;
|
|
}
|
|
getStoredKeys() {
|
|
return loadKeys(this.storage, this.storageKey);
|
|
}
|
|
async derivePublicKey(privateKey) {
|
|
return getPublicKeyFromPrivate(privateKey);
|
|
}
|
|
importKeys(keys) {
|
|
saveKeys(this.storage, keys, this.storageKey);
|
|
}
|
|
exportKeys() {
|
|
return loadKeys(this.storage, this.storageKey);
|
|
}
|
|
setAccessToken(token) {
|
|
this.accessToken = token;
|
|
}
|
|
async request(path, init = {}) {
|
|
const headers = new Headers(init.headers ?? {});
|
|
headers.set("Content-Type", "application/json");
|
|
if (this.accessToken) {
|
|
headers.set("Authorization", `Bearer ${this.accessToken}`);
|
|
}
|
|
const body = init.body === undefined ? undefined : JSON.stringify(init.body);
|
|
const res = await fetch(`${this.baseUrl}${path}`, { ...init, headers, body });
|
|
if (!res.ok) {
|
|
const maybeJson = await res.json().catch(() => ({}));
|
|
let msg = maybeJson.error ?? `HTTP ${res.status}`;
|
|
if (maybeJson.hint)
|
|
msg += `. ${maybeJson.hint}`;
|
|
throw new Error(msg);
|
|
}
|
|
if (res.status === 204) {
|
|
return;
|
|
}
|
|
return await res.json();
|
|
}
|
|
async getServicePublicKey() {
|
|
return this.request("/v1/service-key", { method: "GET" });
|
|
}
|
|
async createChallenge(publicKey) {
|
|
return this.request("/v1/auth/challenge", { method: "POST", body: { publicKey } });
|
|
}
|
|
async registerBySigningServiceKey(publicKey, privateKey) {
|
|
const { publicKey: servicePublicKey } = await this.getServicePublicKey();
|
|
const signature = await signMessage(privateKey, servicePublicKey);
|
|
await this.request("/v1/auth/register-by-signature", {
|
|
method: "POST",
|
|
body: { publicKey, signature }
|
|
});
|
|
}
|
|
async loginWithSignature(publicKey, privateKey) {
|
|
const challenge = await this.createChallenge(publicKey);
|
|
const signature = await signMessage(privateKey, challenge.messageToSign);
|
|
const response = await this.request("/v1/auth/login", {
|
|
method: "POST",
|
|
body: {
|
|
publicKey,
|
|
nonce: challenge.nonce,
|
|
signature
|
|
}
|
|
});
|
|
this.accessToken = response.accessToken;
|
|
return response.accessToken;
|
|
}
|
|
async createInvitation(payload, inviterPrivateKey) {
|
|
const payloadStr = JSON.stringify(payload);
|
|
const payloadB64 = bytesToBase64Url(textToBytes(payloadStr));
|
|
const inviteSignature = await signMessage(inviterPrivateKey, `invite:${payloadB64}`);
|
|
await this.request("/v1/invitations", {
|
|
method: "POST",
|
|
body: {
|
|
invitePayloadB64: payloadB64,
|
|
inviteSignature
|
|
}
|
|
});
|
|
}
|
|
async registerWithInvitation(input) {
|
|
const proofSignature = await signMessage(input.privateKey, `register:${input.publicKey}:${input.jti}`);
|
|
await this.request("/v1/auth/register", {
|
|
method: "POST",
|
|
body: {
|
|
publicKey: input.publicKey,
|
|
invitePayloadB64: input.invitePayloadB64,
|
|
inviteSignature: input.inviteSignature,
|
|
proofSignature
|
|
}
|
|
});
|
|
}
|
|
async listCollections() {
|
|
return this.request("/v1/collections", { method: "GET" });
|
|
}
|
|
async createCollection(name) {
|
|
return this.request("/v1/collections", { method: "POST", body: { name } });
|
|
}
|
|
async updateCollection(collectionId, name) {
|
|
return this.request(`/v1/collections/${collectionId}`, {
|
|
method: "PATCH",
|
|
body: { name }
|
|
});
|
|
}
|
|
async deleteCollection(collectionId) {
|
|
return this.request(`/v1/collections/${collectionId}`, { method: "DELETE" });
|
|
}
|
|
async listFeatures(collectionId) {
|
|
return this.request(`/v1/collections/${collectionId}/features`, { method: "GET" });
|
|
}
|
|
async createPointFeature(collectionId, lon, lat, properties) {
|
|
return this.request(`/v1/collections/${collectionId}/features`, {
|
|
method: "POST",
|
|
body: {
|
|
geometry: { type: "Point", coordinates: [lon, lat] },
|
|
properties
|
|
}
|
|
});
|
|
}
|
|
async deleteFeature(featureId) {
|
|
return this.request(`/v1/features/${featureId}`, { method: "DELETE" });
|
|
}
|
|
}
|
|
export {
|
|
signMessage,
|
|
saveKeys,
|
|
loadKeys,
|
|
getPublicKeyFromPrivate,
|
|
generateKeyPair,
|
|
clearKeys,
|
|
GeoApiClient,
|
|
DEFAULT_KEYS_STORAGE_KEY
|
|
};
|