@0xmonaco/core 1.0.53 → 1.0.55

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.
@@ -20,7 +20,7 @@
20
20
  * }
21
21
  * ```
22
22
  */
23
- import type { SessionCredentials } from "@0xmonaco/types";
23
+ import type { RequestSigning, SessionCredentials } from "@0xmonaco/types";
24
24
  import { type SessionKeypair } from "../crypto/session";
25
25
  export interface RetryOptions {
26
26
  maxRetries?: number;
@@ -32,6 +32,9 @@ export declare abstract class BaseAPI {
32
32
  protected sessionKeypair?: SessionKeypair;
33
33
  /** Application secret key (`sk_...`) sent in the `x-server-key` header for backend-authenticated requests. */
34
34
  protected serverKey?: string;
35
+ private requestSigning;
36
+ /** Opt in only after the target server supports RFC 9421. No automatic downgrade. */
37
+ setRequestSigning(protocol: RequestSigning): void;
35
38
  protected retryOptions: Required<RetryOptions>;
36
39
  /**
37
40
  * Creates a new BaseAPI instance.
@@ -108,10 +111,11 @@ export declare abstract class BaseAPI {
108
111
  /**
109
112
  * Compute the per-request ed25519 signature headers.
110
113
  *
111
- * The signed payload is `METHOD\nPATH_WITH_QUERY\nTIMESTAMP_MS\nSHA256_BODY_HEX`,
114
+ * Legacy signing uses `METHOD\nPATH_WITH_QUERY\nTIMESTAMP_MS\nSHA256_BODY_HEX`,
112
115
  * matching the server (`handlers::auth::compose_signing_string`). `endpoint`
113
116
  * is the path-with-query exactly as sent; the body hash covers the raw bytes
114
- * so a request can't be replayed with a different body.
117
+ * so a request cannot be reused with a different body. RFC 9421 is opt-in
118
+ * and covers the final URL path/query plus Content-Digest.
115
119
  */
116
120
  private buildSignatureHeaders;
117
121
  /**
package/dist/api/base.js CHANGED
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
24
24
  import { StatusCodes } from "http-status-codes";
25
+ import { buildHttpSignatureHeaders } from "../crypto/http-signatures";
25
26
  import { composeSigningString, keypairFromHex, sha256Hex, signMessage } from "../crypto/session";
26
27
  import { APIError } from "../errors";
27
28
  /**
@@ -146,6 +147,13 @@ export class BaseAPI {
146
147
  sessionKeypair;
147
148
  /** Application secret key (`sk_...`) sent in the `x-server-key` header for backend-authenticated requests. */
148
149
  serverKey;
150
+ requestSigning = "legacy";
151
+ /** Opt in only after the target server supports RFC 9421. No automatic downgrade. */
152
+ setRequestSigning(protocol) {
153
+ if (protocol !== "legacy" && protocol !== "rfc9421")
154
+ throw new Error("Unsupported request signing protocol");
155
+ this.requestSigning = protocol;
156
+ }
149
157
  retryOptions;
150
158
  /**
151
159
  * Creates a new BaseAPI instance.
@@ -389,22 +397,32 @@ export class BaseAPI {
389
397
  const url = `${this.apiUrl}${endpoint}`;
390
398
  const requestBody = this.parseRequestBody(options.body);
391
399
  const signatureHeaders = this.buildSignatureHeaders(endpoint, options);
400
+ let extraHeaders = options.headers;
401
+ if (this.requestSigning === "rfc9421") {
402
+ const normalized = {};
403
+ new Headers(options.headers).forEach((value, name) => {
404
+ normalized[name] = value;
405
+ });
406
+ extraHeaders = normalized;
407
+ }
392
408
  return this.executeRequest(url, endpoint, {
393
409
  ...options,
410
+ ...(this.requestSigning === "rfc9421" ? { redirect: "error" } : {}),
394
411
  headers: {
395
412
  "Content-Type": "application/json",
413
+ ...extraHeaders,
396
414
  ...signatureHeaders,
397
- ...options.headers,
398
415
  },
399
416
  }, requestBody);
400
417
  }
401
418
  /**
402
419
  * Compute the per-request ed25519 signature headers.
403
420
  *
404
- * The signed payload is `METHOD\nPATH_WITH_QUERY\nTIMESTAMP_MS\nSHA256_BODY_HEX`,
421
+ * Legacy signing uses `METHOD\nPATH_WITH_QUERY\nTIMESTAMP_MS\nSHA256_BODY_HEX`,
405
422
  * matching the server (`handlers::auth::compose_signing_string`). `endpoint`
406
423
  * is the path-with-query exactly as sent; the body hash covers the raw bytes
407
- * so a request can't be replayed with a different body.
424
+ * so a request cannot be reused with a different body. RFC 9421 is opt-in
425
+ * and covers the final URL path/query plus Content-Digest.
408
426
  */
409
427
  buildSignatureHeaders(endpoint, options) {
410
428
  if (!this.sessionKeypair) {
@@ -414,6 +432,17 @@ export class BaseAPI {
414
432
  });
415
433
  }
416
434
  const method = (options.method ?? "GET").toUpperCase();
435
+ if (this.requestSigning === "rfc9421") {
436
+ if (options.body != null && typeof options.body !== "string")
437
+ throw new Error("Signed requests require a string body");
438
+ const headers = new Headers(options.headers);
439
+ for (const name of ["signature", "signature-input", "content-digest", "x-monaco-publickey", "x-monaco-timestamp", "x-monaco-signature"]) {
440
+ if (headers.has(name))
441
+ throw new Error("Signature headers are managed by the SDK");
442
+ }
443
+ const url = new URL(`${this.apiUrl}${endpoint}`);
444
+ return buildHttpSignatureHeaders(this.sessionKeypair, method, url.pathname + url.search, utf8ToBytes(options.body ?? ""), Math.floor(Date.now() / 1000));
445
+ }
417
446
  const timestampMs = Date.now();
418
447
  const bodyBytes = typeof options.body === "string" ? utf8ToBytes(options.body) : new Uint8Array(0);
419
448
  const bodyHash = sha256Hex(bodyBytes);
@@ -847,10 +847,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
847
847
  sendAuthenticate();
848
848
  resubscribeAll();
849
849
  options.onStatusChange?.("connected");
850
- // Resync-join order: resume the live stream first (auth +
851
- // subscribe above), then tell the app to refetch snapshots and
852
- // reconcile by (id, updatedAt) — events racing the snapshot are
853
- // newer than the rows they duplicate.
850
+ // Send auth/subscribe requests before asking the application to
851
+ // reconcile. This callback does not wait for acknowledgements or
852
+ // snapshots, and the SDK does not perform REST recovery itself.
854
853
  if (pendingResyncCode !== null) {
855
854
  const code = pendingResyncCode;
856
855
  pendingResyncCode = null;
@@ -1242,8 +1241,8 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
1242
1241
  * Subscribe to authenticated user's position updates.
1243
1242
  * Requires authentication - user is identified from the signed session-key
1244
1243
  * handshake. Omit `tradingPairId` for all pairs, or pass one to filter.
1245
- * Lag policy is Conflate: a lagging subscriber is fast-forwarded, no gap
1246
- * signal.
1244
+ * Detected broadcast lag closes the connection with 1013; reconcile
1245
+ * missed keys and terminal updates on reconnect.
1247
1246
  */
1248
1247
  const subscribePositions = (handler, tradingPairId, onSnapshot) => {
1249
1248
  if (!handler) {
@@ -1290,8 +1289,8 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
1290
1289
  * Subscribe to the authenticated user's account-level margin health updates
1291
1290
  * (equity, collateral, free collateral, margin ratio, maintenance
1292
1291
  * requirement). Requires authentication - the user is identified from the
1293
- * signed session-key handshake. Lag policy is Conflate: a lagging subscriber
1294
- * is fast-forwarded, no gap signal. Each frame is a terminal-state snapshot
1292
+ * signed session-key handshake. Detected broadcast lag closes the connection
1293
+ * with 1013; reconnect and reconcile missed state. Each frame is a terminal-state snapshot
1295
1294
  * per margin account, not an additive delta.
1296
1295
  */
1297
1296
  const subscribeAccount = (handler, onSnapshot) => {
@@ -1317,8 +1316,8 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
1317
1316
  * of the current active-pair list arrives before the live diffs; a
1318
1317
  * subscriber joining a channel this client already holds is replayed the
1319
1318
  * current baseline from a local cache instead (the server snapshots only
1320
- * genuinely new wire subscriptions). Lag policy is Conflate: a lagging
1321
- * subscriber is fast-forwarded, no gap signal.
1319
+ * genuinely new wire subscriptions). Detected broadcast lag closes the connection
1320
+ * with 1013; a new wire subscription provides a fresh baseline.
1322
1321
  */
1323
1322
  const subscribeInstruments = (handler, tradingPairId, onSnapshot) => {
1324
1323
  if (!handler) {
@@ -0,0 +1,3 @@
1
+ import type { SessionKeypair } from "./session";
2
+ /** Sign an already URL-encoded path/query and the exact bytes that will be sent. */
3
+ export declare function buildHttpSignatureHeaders(keypair: SessionKeypair, method: string, target: string, body: Uint8Array, created: number): Record<string, string>;
@@ -0,0 +1,26 @@
1
+ /** Monaco's RFC 9421 Ed25519 profile; key registration remains wallet-authorized. */
2
+ import { ed25519 } from "@noble/curves/ed25519";
3
+ import { sha256 } from "@noble/hashes/sha2";
4
+ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
5
+ function base64(bytes) {
6
+ return btoa(String.fromCharCode(...bytes));
7
+ }
8
+ /** Sign an already URL-encoded path/query and the exact bytes that will be sent. */
9
+ export function buildHttpSignatureHeaders(keypair, method, target, body, created) {
10
+ if (!Number.isSafeInteger(created) || created < 0 || created > 999_999_999_999_999)
11
+ throw new Error("Invalid signature creation time");
12
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: reject control characters before constructing signature lines
13
+ if (!/^[A-Z]+$/.test(method) || !target.startsWith("/") || /[\s#\u0000-\u001f\u007f]/.test(target))
14
+ throw new Error("Invalid signature request target");
15
+ const queryIndex = target.indexOf("?");
16
+ const path = queryIndex < 0 ? target : target.slice(0, queryIndex);
17
+ const query = queryIndex < 0 ? "?" : target.slice(queryIndex);
18
+ const digest = `sha-256=:${base64(sha256(body))}:`;
19
+ const params = `("@method" "@path" "@query" "content-digest");created=${created};keyid="${bytesToHex(keypair.publicKey)}";alg="ed25519"`;
20
+ const base = `"@method": ${method}\n"@path": ${path}\n"@query": ${query}\n"content-digest": ${digest}\n"@signature-params": ${params}`;
21
+ return {
22
+ "Signature-Input": `monaco=${params}`,
23
+ Signature: `monaco=:${base64(ed25519.sign(utf8ToBytes(base), keypair.privateKey))}:`,
24
+ "Content-Digest": digest,
25
+ };
26
+ }
package/dist/sdk.js CHANGED
@@ -181,26 +181,30 @@ export class MonacoSDKImpl {
181
181
  this.walletClient = cfg.walletClient;
182
182
  }
183
183
  // Instantiate APIs (wallet-dependent APIs will be initialized lazily or error if wallet not set)
184
- this.applications = new ApplicationsAPIImpl(apiUrl);
185
- this.market = new MarketAPIImpl(apiUrl);
186
- this.marginAccounts = new MarginAccountsAPIImpl(apiUrl);
187
- this.positions = new PositionsAPIImpl(apiUrl);
188
- this.portfolio = new PortfolioAPIImpl(apiUrl);
189
- this.subAccounts = new SubAccountsAPIImpl(apiUrl);
190
- this.faucet = new FaucetAPIImpl(apiUrl);
191
- this.sweeper = new SweeperAPIImpl(apiUrl);
192
- this.whitelist = new WhitelistAPIImpl(apiUrl);
193
- this.pulse = new PulseAPIImpl(apiUrl);
194
- this.auth = new AuthAPIImpl(this.walletClient, this.chain, apiUrl);
195
- this.delegatedAgents = new DelegatedAgentsAPIImpl(apiUrl);
196
- this.fees = new FeesAPIImpl(apiUrl);
197
- this.pitpass = new PitpassAPIImpl(apiUrl);
198
- this.profile = new ProfileAPIImpl(apiUrl);
199
- this.vault = new VaultAPIImpl(this.publicClient, this.walletClient, this.chain, this.applications, this.profile, apiUrl);
200
- this.withdrawals = new WithdrawalsAPIImpl(apiUrl);
201
- this.trading = new TradingAPIImpl(apiUrl);
202
- this.orderbook = new OrderbookAPIImpl(apiUrl);
203
- this.trades = new TradesAPIImpl(apiUrl);
184
+ const configureSigning = (api) => {
185
+ api.setRequestSigning(cfg.requestSigning ?? "legacy");
186
+ return api;
187
+ };
188
+ this.applications = configureSigning(new ApplicationsAPIImpl(apiUrl));
189
+ this.market = configureSigning(new MarketAPIImpl(apiUrl));
190
+ this.marginAccounts = configureSigning(new MarginAccountsAPIImpl(apiUrl));
191
+ this.positions = configureSigning(new PositionsAPIImpl(apiUrl));
192
+ this.portfolio = configureSigning(new PortfolioAPIImpl(apiUrl));
193
+ this.subAccounts = configureSigning(new SubAccountsAPIImpl(apiUrl));
194
+ this.faucet = configureSigning(new FaucetAPIImpl(apiUrl));
195
+ this.sweeper = configureSigning(new SweeperAPIImpl(apiUrl));
196
+ this.whitelist = configureSigning(new WhitelistAPIImpl(apiUrl));
197
+ this.pulse = configureSigning(new PulseAPIImpl(apiUrl));
198
+ this.auth = configureSigning(new AuthAPIImpl(this.walletClient, this.chain, apiUrl));
199
+ this.delegatedAgents = configureSigning(new DelegatedAgentsAPIImpl(apiUrl));
200
+ this.fees = configureSigning(new FeesAPIImpl(apiUrl));
201
+ this.pitpass = configureSigning(new PitpassAPIImpl(apiUrl));
202
+ this.profile = configureSigning(new ProfileAPIImpl(apiUrl));
203
+ this.vault = configureSigning(new VaultAPIImpl(this.publicClient, this.walletClient, this.chain, this.applications, this.profile, apiUrl));
204
+ this.withdrawals = configureSigning(new WithdrawalsAPIImpl(apiUrl));
205
+ this.trading = configureSigning(new TradingAPIImpl(apiUrl));
206
+ this.orderbook = configureSigning(new OrderbookAPIImpl(apiUrl));
207
+ this.trades = configureSigning(new TradesAPIImpl(apiUrl));
204
208
  // Thread the reconnect/resync knobs and callbacks from SDK config into the
205
209
  // internally-built ws client. The session keypair is not passed here — it
206
210
  // is propagated on login via propagateSession/setSessionKeypair.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
4
  "type": "module",
5
5
  "homepage": "https://docs.0xmonaco.com/sdk/typescript",
6
6
  "main": "./dist/index.js",
@@ -19,8 +19,8 @@
19
19
  "viem": "^2.45.2"
20
20
  },
21
21
  "dependencies": {
22
- "@0xmonaco/contracts": "1.0.53",
23
- "@0xmonaco/types": "1.0.53",
22
+ "@0xmonaco/contracts": "1.0.55",
23
+ "@0xmonaco/types": "1.0.55",
24
24
  "@noble/curves": "^1.9.1",
25
25
  "@noble/hashes": "^1.8.0",
26
26
  "http-status-codes": "^2.3.0"