Files
backend/libs/geo-api-client/dist/index.js
Andriy Oblivantsev a5a97a0ad9
CI / test (push) Successful in 5s
Add register-by-signature, web fixes, bin scripts, docs
- Register by signing service key: GET /v1/service-key, POST /v1/auth/register-by-signature
- Login auto-attempts register first for new users
- Web: default API URL momswap.produktor.duckdns.org, /libs/ static handler
- Docker: webbuild stage for geo-api-client, copy web+libs to runtime
- Bin scripts: test.sh, run.sh, up.sh, down.sh
- docs/ed25519-security-use-cases.md: use cases, message formats, examples
- SERVICE_PUBLIC_KEY env (defaults to ADMIN_PUBLIC_KEY)

Made-with: Cursor
2026-03-01 12:59:02 +00:00

629 lines
18 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 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);
}
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(() => ({}));
const msg = maybeJson.error ?? `HTTP ${res.status}`;
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 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
}
});
}
}
export {
signMessage,
saveKeys,
loadKeys,
generateKeyPair,
clearKeys,
GeoApiClient,
DEFAULT_KEYS_STORAGE_KEY
};