Files
gmgn-skills/src/client/signer.ts
T
David LauandClaude Sonnet 4.6 d51f1c49d3 refactor(auth): rename normalRequest/criticalRequest; align auth terminology
- Rename private methods: normalRequest → authExistRequest,
  criticalRequest → authSignedRequest in OpenApiClient.ts (both gmgn-skills
  and scripts/src)
- Replace all "normal auth" / "critical auth" labels across SKILL.md files,
  cli-usage.md, Readme.md, and CLI command descriptions with:
    exist auth  — API Key only, no private key required
    signed auth — API Key + GMGN_PRIVATE_KEY signature required
- Add explicit "requires GMGN_PRIVATE_KEY" notes to signed-auth commands
  and "API Key only" notes to exist-auth commands throughout docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:45:10 +08:00

91 lines
2.6 KiB
TypeScript

import * as crypto from "crypto";
import * as fs from "fs";
import * as path from "path";
export type SignAlgorithm = "Ed25519" | "RSA-SHA256";
/**
* Detect signing algorithm from PEM private key
*/
export function detectAlgorithm(pem: string): SignAlgorithm {
const key = crypto.createPrivateKey(pem);
switch (key.asymmetricKeyType) {
case "ed25519": return "Ed25519";
case "rsa": return "RSA-SHA256";
default:
throw new Error(`Unsupported key type: ${key.asymmetricKeyType}. Supported: Ed25519, RSA`);
}
}
/**
* Build auth query params (timestamp + client_id)
* timestamp: Unix seconds, server validates within ±5s
* client_id: UUID, replays rejected within 7s
*/
export function buildAuthQuery(): { timestamp: number; client_id: string } {
return {
timestamp: Math.floor(Date.now() / 1000),
client_id: crypto.randomUUID(),
};
}
/**
* Build the signature message (signed auth)
* Format: {sub_path}:{sorted_query_string}:{request_body}:{timestamp}
* sorted_query_string: all query params (including timestamp, client_id) sorted alphabetically by key.
* Array values are serialized as repeated k=v pairs (same as buildUrl / URLSearchParams), sorted by value.
*/
export function buildMessage(
subPath: string,
queryParams: Record<string, string | number | string[]>,
body: string,
timestamp: number
): string {
const sortedQs = Object.keys(queryParams)
.sort()
.flatMap((k) => {
const v = queryParams[k];
if (Array.isArray(v)) {
return [...v].sort().map((item) => `${k}=${item}`);
}
return [`${k}=${v}`];
})
.join("&");
return `${subPath}:${sortedQs}:${body}:${timestamp}`;
}
/**
* Load private key file (PEM format)
*/
export function loadPrivateKey(keyPath: string): string {
const resolved = path.resolve(process.cwd(), keyPath);
return fs.readFileSync(resolved, "utf-8");
}
/**
* Sign a message and return the base64-encoded signature
*
* Ed25519: signs raw message bytes (no hashing)
* RSA-SHA256: RSA-PSS + SHA256, salt length = 32 (matches server-side rsa.VerifyPSS nil opts)
*/
export function sign(
message: string,
privateKeyPem: string,
algorithm: SignAlgorithm
): string {
const msgBuf = Buffer.from(message, "utf-8");
if (algorithm === "Ed25519") {
const sig = crypto.sign(null, msgBuf, privateKeyPem);
return sig.toString("base64");
}
// RSA-SHA256 with PSS padding, salt length = digest length (32)
const sig = crypto.sign("sha256", msgBuf, {
key: privateKeyPem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: 32,
});
return sig.toString("base64");
}