@0xmonaco/core 1.0.32 → 1.0.33

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.
package/dist/api/base.js CHANGED
@@ -34,6 +34,112 @@ import { APIError } from "../errors";
34
34
  * only takes effect in server runtimes (Node/undici, Bun, Deno).
35
35
  */
36
36
  const SDK_USER_AGENT = "@0xmonaco/core";
37
+ const MONTHS = {
38
+ Jan: 0,
39
+ Feb: 1,
40
+ Mar: 2,
41
+ Apr: 3,
42
+ May: 4,
43
+ Jun: 5,
44
+ Jul: 6,
45
+ Aug: 7,
46
+ Sep: 8,
47
+ Oct: 9,
48
+ Nov: 10,
49
+ Dec: 11,
50
+ };
51
+ // The three RFC 7231 §7.1.1.1 `HTTP-date` grammars a recipient must accept.
52
+ // Each is anchored so trailing junk is rejected, and `Date.parse` is avoided
53
+ // entirely (it is permissive — it reads asctime in local time and rolls
54
+ // impossible dates like 31 Feb forward instead of rejecting them).
55
+ const MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
56
+ const IMF_FIXDATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (${MONTH}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
57
+ const RFC850_DATE = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d{2})-(${MONTH})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
58
+ const ASCTIME_DATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (${MONTH}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
59
+ /**
60
+ * Expand an RFC 850 two-digit year per RFC 7231 §7.1.1.1: a full timestamp that
61
+ * appears more than 50 years in the future is interpreted as the most recent
62
+ * past year with the same last two digits. The comparison is against the exact
63
+ * receipt instant plus 50 years (not just calendar years), so the boundary can
64
+ * fall mid-year — e.g. received 2026-06-01, `31-Dec-76` is 50y7m ahead and maps
65
+ * to 1976, while `01-Jan-76` is under 50 years ahead and stays 2076.
66
+ */
67
+ function expandTwoDigitYear(twoDigitYear, month, day, hour, minute, second) {
68
+ const now = new Date(Date.now());
69
+ const candidateYear = Math.floor(now.getUTCFullYear() / 100) * 100 + twoDigitYear;
70
+ const candidate = Date.UTC(candidateYear, month, day, hour, minute, second);
71
+ const fiftyYearsAhead = Date.UTC(now.getUTCFullYear() + 50, now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
72
+ return candidate > fiftyYearsAhead ? candidateYear - 100 : candidateYear;
73
+ }
74
+ /**
75
+ * Parse an RFC 7231 `HTTP-date` — in any of the three formats a recipient must
76
+ * accept (IMF-fixdate, the obsolete RFC 850 form, and asctime) — into a UTC
77
+ * epoch in milliseconds, or `undefined` if the value is not a well-formed, real
78
+ * calendar date. All three denote UTC, so timestamps are built with `Date.UTC`
79
+ * and the fields are round-tripped to reject impossible dates such as
80
+ * `31 Feb` or `25:00:00`. RFC 850's 2-digit year follows the RFC 7231 rule
81
+ * (see {@link expandTwoDigitYear}). The redundant day-name is intentionally not
82
+ * validated against the date — RFC 7231 encourages robust parsing, and the
83
+ * date/time fields already determine the instant unambiguously, so a mismatched
84
+ * weekday still yields the intended delay rather than a dropped header and an
85
+ * arbitrary fallback.
86
+ */
87
+ function parseHttpDate(value) {
88
+ let day;
89
+ let month;
90
+ let year;
91
+ let hour;
92
+ let minute;
93
+ let second;
94
+ const imf = IMF_FIXDATE.exec(value);
95
+ const rfc850 = RFC850_DATE.exec(value);
96
+ const asctime = ASCTIME_DATE.exec(value);
97
+ if (imf) {
98
+ day = Number(imf[1]);
99
+ month = MONTHS[imf[2]];
100
+ year = Number(imf[3]);
101
+ hour = Number(imf[4]);
102
+ minute = Number(imf[5]);
103
+ second = Number(imf[6]);
104
+ }
105
+ else if (rfc850) {
106
+ day = Number(rfc850[1]);
107
+ month = MONTHS[rfc850[2]];
108
+ hour = Number(rfc850[4]);
109
+ minute = Number(rfc850[5]);
110
+ second = Number(rfc850[6]);
111
+ if (month === undefined)
112
+ return undefined;
113
+ year = expandTwoDigitYear(Number(rfc850[3]), month, day, hour, minute, second);
114
+ }
115
+ else if (asctime) {
116
+ month = MONTHS[asctime[1]];
117
+ day = Number((asctime[2] ?? "").trim());
118
+ hour = Number(asctime[3]);
119
+ minute = Number(asctime[4]);
120
+ second = Number(asctime[5]);
121
+ year = Number(asctime[6]);
122
+ }
123
+ else {
124
+ return undefined;
125
+ }
126
+ if (month === undefined)
127
+ return undefined;
128
+ const timestamp = Date.UTC(year, month, day, hour, minute, second);
129
+ const utc = new Date(timestamp);
130
+ // Reject impossible calendar/clock values that Date.UTC silently rolls over
131
+ // (e.g. 31 Feb, 25:00:00). A `:60` leap second is likewise dropped rather
132
+ // than special-cased — leap seconds never appear in a Retry-After header.
133
+ if (utc.getUTCFullYear() !== year ||
134
+ utc.getUTCMonth() !== month ||
135
+ utc.getUTCDate() !== day ||
136
+ utc.getUTCHours() !== hour ||
137
+ utc.getUTCMinutes() !== minute ||
138
+ utc.getUTCSeconds() !== second) {
139
+ return undefined;
140
+ }
141
+ return timestamp;
142
+ }
37
143
  export class BaseAPI {
38
144
  apiUrl;
39
145
  /** Active session keypair (raw bytes) used to sign authenticated requests. */
@@ -138,12 +244,25 @@ export class BaseAPI {
138
244
  if (!headers)
139
245
  return {};
140
246
  const requestId = headers.get("x-request-id") || headers.get("request-id") || headers.get("x-correlation-id") || undefined;
141
- // Parse Retry-After header with validation
247
+ // Parse the Retry-After header (RFC 7231). It is either a non-negative
248
+ // number of seconds (delay-seconds) or an HTTP-date. Both forms are
249
+ // normalized to a delay in SECONDS, so callers multiply by 1000 for
250
+ // setTimeout. An HTTP-date is converted to the remaining seconds from now
251
+ // (past dates clamp to 0). Values matching neither grammar are dropped so
252
+ // a malformed header can never masquerade as a 0s "retry immediately".
142
253
  const retryAfterHeader = headers.get("retry-after");
143
254
  let retryAfter;
144
255
  if (retryAfterHeader !== null) {
145
- const parsed = Number.parseInt(retryAfterHeader, 10);
146
- retryAfter = Number.isNaN(parsed) ? undefined : parsed;
256
+ const value = retryAfterHeader.trim();
257
+ if (/^\d+$/.test(value)) {
258
+ retryAfter = Number.parseInt(value, 10);
259
+ }
260
+ else {
261
+ const dateMs = parseHttpDate(value);
262
+ if (dateMs !== undefined) {
263
+ retryAfter = Math.max(0, Math.ceil((dateMs - Date.now()) / 1000));
264
+ }
265
+ }
147
266
  }
148
267
  return { requestId, retryAfter };
149
268
  }
@@ -150,6 +150,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
150
150
  * opens. The client signs `WS-AUTH\n<pubkey>\n<ts>` with the session private
151
151
  * key; the server binds the connection to the session. No-op if no session
152
152
  * is set (public channels work unauthenticated).
153
+ *
154
+ * `timestamp` is the current epoch-ms and is bound into the signature; the
155
+ * server rejects it outside a ±30s skew window, so the handshake is freshly
156
+ * signed on every (re)connect rather than cached and replayed.
153
157
  */
154
158
  const sendAuthenticate = () => {
155
159
  if (!session || ws?.readyState !== WebSocket.OPEN)
@@ -504,7 +508,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
504
508
  });
505
509
  };
506
510
  const subscribeMovements = (handler) => {
507
- // User is identified from the JWT token, so channel is just "movements"
511
+ // User is identified from the authenticated session, so channel is just "movements"
508
512
  const channel = "movements";
509
513
  return subscribe(channel, (rawData) => {
510
514
  try {
@@ -541,10 +545,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
541
545
  };
542
546
  /**
543
547
  * Subscribe to authenticated user's order events (all trading pairs)
544
- * Requires authentication - user is identified from the JWT token
548
+ * Requires authentication - user is identified from the signed session-key handshake
545
549
  */
546
550
  const subscribeUserOrders = (handler) => {
547
- // User is identified from the JWT token, so channel is just "orders"
551
+ // User is identified from the authenticated session, so channel is just "orders"
548
552
  const channel = "orders";
549
553
  return subscribe(channel, (rawData) => {
550
554
  try {
@@ -564,10 +568,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
564
568
  };
565
569
  /**
566
570
  * Subscribe to authenticated user's balance events
567
- * Requires authentication - user is identified from the JWT token
571
+ * Requires authentication - user is identified from the signed session-key handshake
568
572
  */
569
573
  const subscribeBalances = (handler) => {
570
- // User is identified from the JWT token, so channel is just "balances"
574
+ // User is identified from the authenticated session, so channel is just "balances"
571
575
  const channel = "balances";
572
576
  return subscribe(channel, (rawData) => {
573
577
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,8 +23,8 @@
23
23
  "viem": "^2.45.2"
24
24
  },
25
25
  "dependencies": {
26
- "@0xmonaco/contracts": "1.0.32",
27
- "@0xmonaco/types": "1.0.32",
26
+ "@0xmonaco/contracts": "1.0.33",
27
+ "@0xmonaco/types": "1.0.33",
28
28
  "@noble/curves": "^1.9.1",
29
29
  "@noble/hashes": "^1.8.0",
30
30
  "http-status-codes": "^2.3.0"