@cored-im/openapi-sdk 0.28.102

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.
@@ -0,0 +1,344 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { Config } from './config';
5
+ import type {
6
+ ApiClient,
7
+ ApiRequest,
8
+ ApiResponse,
9
+ EventHeader,
10
+ WrappedEventHandler,
11
+ } from './types';
12
+ import { USER_AGENT } from './version';
13
+ import {
14
+ DEFAULT_PING_PATH,
15
+ DEFAULT_TOKEN_PATH,
16
+ DEFAULT_GATEWAY_PATH,
17
+ } from './consts';
18
+ import { CryptoManager, sha256Hex } from './crypto';
19
+ import { WsClient } from './ws_client';
20
+ import {
21
+ encodeSecureMessage,
22
+ decodeSecureMessage,
23
+ encodeHttpRequest,
24
+ decodeHttpResponse,
25
+ } from '@/internal/transport';
26
+ import type { HttpRequest } from '@/internal/transport';
27
+
28
+ const TIMESTAMP_HEADER = 'X-Cored-Timestamp';
29
+ const NONCE_HEADER = 'X-Cored-Nonce';
30
+
31
+ export class DefaultApiClient implements ApiClient {
32
+ private config: Config;
33
+ private secret: string = '';
34
+ private token: string = '';
35
+ private tokenRefreshAt: number = 0;
36
+ private tokenExpiresAt: number = 0;
37
+ private tokenFetching: boolean = false;
38
+ private tokenPromise: Promise<void> | null = null;
39
+ private pingCalled: boolean = false;
40
+ private pingExpiresAt: number = 0;
41
+ private pingFetching: boolean = false;
42
+ private pingPromise: Promise<void> | null = null;
43
+ private cryptoManager: CryptoManager;
44
+ private ws: WsClient;
45
+
46
+ constructor(config: Config) {
47
+ this.config = config;
48
+ this.cryptoManager = new CryptoManager(config);
49
+ this.ws = new WsClient({
50
+ config,
51
+ getSecret: () => this.secret,
52
+ getToken: () => this.getToken(),
53
+ ensurePing: () => this.ensurePing(),
54
+ cryptoManager: this.cryptoManager,
55
+ });
56
+ }
57
+
58
+ async init(): Promise<void> {
59
+ this.secret = await sha256Hex(`${this.config.appId}:${this.config.appSecret}`);
60
+ }
61
+
62
+ async preheat(): Promise<void> {
63
+ await this.ensurePing();
64
+ await this.getToken();
65
+ }
66
+
67
+ async close(): Promise<void> {
68
+ this.ws.close();
69
+ }
70
+
71
+ onEvent(eventType: string, handler: WrappedEventHandler): void {
72
+ this.ws.onEvent(eventType, handler);
73
+ }
74
+
75
+ offEvent(eventType: string, handler: WrappedEventHandler): void {
76
+ this.ws.offEvent(eventType, handler);
77
+ }
78
+
79
+ async request(req: ApiRequest): Promise<ApiResponse> {
80
+ await this.ensurePing();
81
+
82
+ // Build URL
83
+ let path = req.path;
84
+ if (req.pathParams) {
85
+ for (const [key, value] of Object.entries(req.pathParams)) {
86
+ path = path.replace(`:${key}`, encodeURIComponent(value));
87
+ }
88
+ }
89
+
90
+ let url = this.config.backendUrl + path;
91
+ if (req.queryParams) {
92
+ const params = new URLSearchParams();
93
+ for (const [key, value] of Object.entries(req.queryParams)) {
94
+ if (value) params.set(key, value);
95
+ }
96
+ const qs = params.toString();
97
+ if (qs) url += '?' + qs;
98
+ }
99
+
100
+ // Encrypted path
101
+ if (this.config.enableEncryption && req.withAppAccessToken) {
102
+ const token = await this.getToken();
103
+ const bodyBytes = req.body
104
+ ? new TextEncoder().encode(this.config.jsonMarshal(req.body))
105
+ : new Uint8Array(0);
106
+
107
+ const headers: Record<string, string> = {
108
+ 'Content-Type': 'application/json',
109
+ 'User-Agent': USER_AGENT,
110
+ [TIMESTAMP_HEADER]: String(this.config.timeManager.getServerTimestamp()),
111
+ [NONCE_HEADER]: randomAlphanumeric(16),
112
+ };
113
+ if (req.headerParams) {
114
+ Object.assign(headers, req.headerParams);
115
+ }
116
+
117
+ const httpReq: HttpRequest = {
118
+ method: req.method,
119
+ path: url.replace(this.config.backendUrl, ''),
120
+ headers,
121
+ body: bodyBytes,
122
+ reqId: '',
123
+ };
124
+
125
+ // WebSocket path
126
+ if (req.withWebSocket) {
127
+ const httpResp = await this.ws.httpRequest(httpReq);
128
+ return {
129
+ json: async () => JSON.parse(new TextDecoder().decode(httpResp.body)),
130
+ body: async () => httpResp.body instanceof Uint8Array ? httpResp.body : new Uint8Array(httpResp.body),
131
+ };
132
+ }
133
+
134
+ // Gateway path
135
+ const httpReqBytes = encodeHttpRequest(httpReq);
136
+ const secureMessage = await this.cryptoManager.encryptMessage(this.secret, httpReqBytes);
137
+ const secureBytes = encodeSecureMessage(secureMessage);
138
+
139
+ const gatewayUrl = this.config.backendUrl + DEFAULT_GATEWAY_PATH;
140
+ const resp = await this.config.httpClient.fetch(gatewayUrl, {
141
+ method: 'POST',
142
+ headers: {
143
+ 'Content-Type': 'application/x-protobuf',
144
+ 'Authorization': `Bearer ${token}`,
145
+ 'User-Agent': USER_AGENT,
146
+ },
147
+ body: secureBytes as unknown as BodyInit,
148
+ });
149
+
150
+ const respBytes = new Uint8Array(await resp.arrayBuffer());
151
+ const respSecureMessage = decodeSecureMessage(respBytes);
152
+ const decryptedBytes = await this.cryptoManager.decryptMessage(this.secret, respSecureMessage);
153
+ const httpResp = decodeHttpResponse(decryptedBytes);
154
+
155
+ return {
156
+ json: async () => JSON.parse(new TextDecoder().decode(httpResp.body)),
157
+ body: async () => httpResp.body instanceof Uint8Array ? httpResp.body : new Uint8Array(httpResp.body),
158
+ };
159
+ }
160
+
161
+ // Plain HTTP path
162
+ const timestamp = String(this.config.timeManager.getServerTimestamp());
163
+ const nonce = randomAlphanumeric(16);
164
+ const headers: Record<string, string> = {
165
+ 'Content-Type': 'application/json',
166
+ 'User-Agent': USER_AGENT,
167
+ [TIMESTAMP_HEADER]: timestamp,
168
+ [NONCE_HEADER]: nonce,
169
+ };
170
+
171
+ if (req.withAppAccessToken) {
172
+ const token = await this.getToken();
173
+ headers['Authorization'] = `Bearer ${token}`;
174
+ }
175
+ if (req.headerParams) {
176
+ for (const [key, value] of Object.entries(req.headerParams)) {
177
+ if (value) headers[key] = value;
178
+ }
179
+ }
180
+
181
+ const fetchInit: RequestInit = {
182
+ method: req.method,
183
+ headers,
184
+ };
185
+ if (req.body && req.method !== 'GET') {
186
+ fetchInit.body = this.config.jsonMarshal(req.body);
187
+ }
188
+
189
+ const resp = await this.config.httpClient.fetch(url, fetchInit);
190
+ return {
191
+ json: async () => resp.json(),
192
+ body: async () => new Uint8Array(await resp.arrayBuffer()),
193
+ };
194
+ }
195
+
196
+ // --- Token management ---
197
+
198
+ private async getToken(): Promise<string> {
199
+ const now = this.config.timeManager.getServerTimestamp();
200
+
201
+ // Token still fully valid
202
+ if (this.token && this.tokenRefreshAt > now) {
203
+ return this.token;
204
+ }
205
+
206
+ // Token expired
207
+ if (!this.token || this.tokenExpiresAt <= now) {
208
+ if (this.tokenPromise) {
209
+ await this.tokenPromise;
210
+ return this.token;
211
+ }
212
+ this.tokenPromise = this.fetchToken();
213
+ try {
214
+ await this.tokenPromise;
215
+ } finally {
216
+ this.tokenPromise = null;
217
+ }
218
+ return this.token;
219
+ }
220
+
221
+ // Token near expiry, refresh in background
222
+ if (!this.tokenFetching) {
223
+ this.tokenFetching = true;
224
+ this.fetchToken().finally(() => {
225
+ this.tokenFetching = false;
226
+ });
227
+ }
228
+
229
+ return this.token;
230
+ }
231
+
232
+ private async fetchToken(): Promise<void> {
233
+ const timestamp = String(this.config.timeManager.getServerTimestamp());
234
+ const nonce = randomAlphanumeric(16);
235
+ const signPayload = `${this.config.appId}:${timestamp}:${this.config.appSecret}:${nonce}`;
236
+ const signature = await sha256Hex(signPayload);
237
+
238
+ const url = this.config.backendUrl + DEFAULT_TOKEN_PATH;
239
+ const resp = await this.config.httpClient.fetch(url, {
240
+ method: 'POST',
241
+ headers: {
242
+ 'Content-Type': 'application/json',
243
+ 'User-Agent': USER_AGENT,
244
+ },
245
+ body: JSON.stringify({
246
+ app_id: this.config.appId,
247
+ timestamp,
248
+ nonce,
249
+ signature: `v1:${signature}`,
250
+ }),
251
+ });
252
+
253
+ const data = await resp.json() as {
254
+ code: number;
255
+ msg: string;
256
+ data?: {
257
+ access_token: string;
258
+ expires_in: number;
259
+ };
260
+ };
261
+
262
+ if (data.code !== 0 || !data.data) {
263
+ throw new Error(`fetch token failed: code=${data.code}, msg=${data.msg}`);
264
+ }
265
+
266
+ this.token = data.data.access_token;
267
+ const now = this.config.timeManager.getServerTimestamp();
268
+ this.tokenExpiresAt = now + (data.data.expires_in - 60) * 1000;
269
+ this.tokenRefreshAt = this.tokenExpiresAt - 5 * 60 * 1000;
270
+ }
271
+
272
+ // --- Ping / server time sync ---
273
+
274
+ private async ensurePing(): Promise<void> {
275
+ const now = Date.now();
276
+
277
+ if (this.pingCalled && this.pingExpiresAt > now) {
278
+ return;
279
+ }
280
+
281
+ if (!this.pingCalled || this.pingExpiresAt <= now) {
282
+ if (this.pingPromise) {
283
+ await this.pingPromise;
284
+ return;
285
+ }
286
+ this.pingPromise = this.fetchPing();
287
+ try {
288
+ await this.pingPromise;
289
+ } finally {
290
+ this.pingPromise = null;
291
+ }
292
+ return;
293
+ }
294
+
295
+ // Background refresh
296
+ if (!this.pingFetching) {
297
+ this.pingFetching = true;
298
+ this.fetchPing().finally(() => {
299
+ this.pingFetching = false;
300
+ });
301
+ }
302
+ }
303
+
304
+ private async fetchPing(): Promise<void> {
305
+ const url = this.config.backendUrl + DEFAULT_PING_PATH;
306
+ const resp = await this.config.httpClient.fetch(url, {
307
+ method: 'GET',
308
+ headers: { 'User-Agent': USER_AGENT },
309
+ });
310
+
311
+ const data = await resp.json() as {
312
+ code: number;
313
+ msg: string;
314
+ data?: {
315
+ version: string;
316
+ timestamp: number;
317
+ org_code: string;
318
+ };
319
+ };
320
+
321
+ if (data.code !== 0 || !data.data) {
322
+ throw new Error(`ping failed: code=${data.code}, msg=${data.msg}`);
323
+ }
324
+
325
+ this.config.timeManager.syncServerTimestamp(data.data.timestamp);
326
+ this.pingCalled = true;
327
+ this.pingExpiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
328
+ this.config.logger.info(`ping ok, server version=${data.data.version}, org_code=${data.data.org_code}`);
329
+ }
330
+ }
331
+
332
+ // --- Helpers ---
333
+
334
+ const ALPHANUMERIC = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
335
+
336
+ function randomAlphanumeric(size: number): string {
337
+ const bytes = new Uint8Array(size);
338
+ crypto.getRandomValues(bytes);
339
+ let result = '';
340
+ for (let i = 0; i < size; i++) {
341
+ result += ALPHANUMERIC[bytes[i] % 62];
342
+ }
343
+ return result;
344
+ }
@@ -0,0 +1,25 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type {
5
+ ApiClient,
6
+ HttpClient,
7
+ Logger,
8
+ TimeManager,
9
+ Marshaller,
10
+ Unmarshaller,
11
+ } from './types';
12
+
13
+ export interface Config {
14
+ appId: string;
15
+ appSecret: string;
16
+ backendUrl: string;
17
+ httpClient: HttpClient;
18
+ apiClient: ApiClient;
19
+ enableEncryption: boolean;
20
+ requestTimeout: number;
21
+ timeManager: TimeManager;
22
+ logger: Logger;
23
+ jsonMarshal: Marshaller;
24
+ jsonUnmarshal: Unmarshaller;
25
+ }
@@ -0,0 +1,8 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const DEFAULT_PING_PATH = '/oapi/stat/v1/ping';
5
+ export const DEFAULT_TOKEN_PATH = '/oapi/auth/v1/app/token';
6
+ export const DEFAULT_GATEWAY_PATH = '/oapi/transport/v1/gateway';
7
+ export const DEFAULT_WS_PATH = '/oapi/transport/v1/ws';
8
+ export const DEFAULT_SECURE_VERSION = '1.0';
@@ -0,0 +1,212 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { Config } from './config';
5
+ import type { SecureMessage } from '@/internal/transport';
6
+ import { DEFAULT_SECURE_VERSION } from './consts';
7
+
8
+ const ALPHANUMERIC = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
9
+
10
+ export class CryptoManager {
11
+ private config: Config;
12
+ private prefix: string;
13
+ private counter: number[] = [0, 0, 0, 0, 0];
14
+
15
+ constructor(config: Config) {
16
+ this.config = config;
17
+ this.prefix = randomAlphanumeric(6);
18
+ }
19
+
20
+ async encryptMessage(secret: string, data: Uint8Array): Promise<SecureMessage> {
21
+ const timestamp = this.config.timeManager.getServerTimestamp();
22
+ const nonce = this.getNonce();
23
+
24
+ // Derive initKey = SHA256(timestamp:secret:nonce)
25
+ const initKey = await sha256Bytes(`${timestamp}:${secret}:${nonce}`);
26
+
27
+ // Generate random AES key
28
+ const aesKey = randomBytes(32);
29
+
30
+ // Compress data with gzip
31
+ const compressed = await compressGzip(data);
32
+
33
+ // Encrypt AES key with initKey
34
+ const encryptedKey = await encryptAES256CBC(aesKey, initKey);
35
+
36
+ // Encrypt compressed data with AES key
37
+ const encryptedData = await encryptAES256CBC(compressed, aesKey);
38
+
39
+ return {
40
+ version: DEFAULT_SECURE_VERSION,
41
+ timestamp,
42
+ nonce,
43
+ encryptedKey,
44
+ encryptedData,
45
+ };
46
+ }
47
+
48
+ async decryptMessage(secret: string, message: SecureMessage): Promise<Uint8Array> {
49
+ if (message.version !== DEFAULT_SECURE_VERSION) {
50
+ throw new Error(`unsupported secure message version: ${message.version}`);
51
+ }
52
+
53
+ // Derive initKey
54
+ const initKey = await sha256Bytes(`${message.timestamp}:${secret}:${message.nonce}`);
55
+
56
+ // Decrypt AES key
57
+ const aesKey = await decryptAES256CBC(message.encryptedKey, initKey);
58
+
59
+ // Decrypt data
60
+ const compressed = await decryptAES256CBC(message.encryptedData, aesKey);
61
+
62
+ // Decompress
63
+ return await decompressGzip(compressed);
64
+ }
65
+
66
+ private getNonce(): string {
67
+ const random = randomAlphanumeric(5);
68
+ const counter = this.formatCounter();
69
+ this.addCounter();
70
+ return this.prefix + random + counter;
71
+ }
72
+
73
+ private formatCounter(): string {
74
+ let result = '';
75
+ for (let i = 0; i < 5; i++) {
76
+ result += ALPHANUMERIC[this.counter[i]];
77
+ }
78
+ return result;
79
+ }
80
+
81
+ private addCounter(): void {
82
+ for (let i = 4; i >= 0; i--) {
83
+ this.counter[i]++;
84
+ if (this.counter[i] < 62) {
85
+ break;
86
+ }
87
+ this.counter[i] = 0;
88
+ }
89
+ }
90
+ }
91
+
92
+ // --- Crypto helpers using Web Crypto API ---
93
+
94
+ async function sha256Bytes(input: string): Promise<Uint8Array> {
95
+ const data = new TextEncoder().encode(input);
96
+ const hash = await crypto.subtle.digest('SHA-256', data);
97
+ return new Uint8Array(hash);
98
+ }
99
+
100
+ export async function sha256Hex(input: string): Promise<string> {
101
+ const bytes = await sha256Bytes(input);
102
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
103
+ }
104
+
105
+ async function encryptAES256CBC(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
106
+ const padded = pkcs7Pad(data);
107
+ const iv = randomBytes(16);
108
+ const cryptoKey = await crypto.subtle.importKey('raw', toBuffer(key), { name: 'AES-CBC' }, false, ['encrypt']);
109
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: toBuffer(iv) }, cryptoKey, toBuffer(padded));
110
+ // Return IV + ciphertext
111
+ const result = new Uint8Array(iv.length + encrypted.byteLength);
112
+ result.set(iv, 0);
113
+ result.set(new Uint8Array(encrypted), iv.length);
114
+ return result;
115
+ }
116
+
117
+ async function decryptAES256CBC(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
118
+ if (data.length < 16 || data.length % 16 !== 0) {
119
+ throw new Error('invalid encrypted data length');
120
+ }
121
+ const iv = data.slice(0, 16);
122
+ const ciphertext = data.slice(16);
123
+ const cryptoKey = await crypto.subtle.importKey('raw', toBuffer(key), { name: 'AES-CBC' }, false, ['decrypt']);
124
+ // Web Crypto API handles PKCS7 unpadding automatically for AES-CBC
125
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: toBuffer(iv) }, cryptoKey, toBuffer(ciphertext));
126
+ return new Uint8Array(decrypted);
127
+ }
128
+
129
+ function pkcs7Pad(data: Uint8Array): Uint8Array {
130
+ const blockSize = 16;
131
+ const padding = blockSize - (data.length % blockSize);
132
+ const padded = new Uint8Array(data.length + padding);
133
+ padded.set(data);
134
+ padded.fill(padding, data.length);
135
+ return padded;
136
+ }
137
+
138
+ // --- Compression ---
139
+
140
+ async function compressGzip(data: Uint8Array): Promise<Uint8Array> {
141
+ if (typeof CompressionStream !== 'undefined') {
142
+ const stream = new CompressionStream('gzip');
143
+ const writer = stream.writable.getWriter();
144
+ void writer.write(toBuffer(data));
145
+ void writer.close();
146
+ const chunks: Uint8Array[] = [];
147
+ const reader = stream.readable.getReader();
148
+ for (;;) {
149
+ const { done, value } = await reader.read();
150
+ if (done) break;
151
+ chunks.push(value);
152
+ }
153
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
154
+ const result = new Uint8Array(totalLength);
155
+ let offset = 0;
156
+ for (const chunk of chunks) {
157
+ result.set(chunk, offset);
158
+ offset += chunk.length;
159
+ }
160
+ return result;
161
+ }
162
+ throw new Error('CompressionStream not available. Please use a modern browser or Node.js 18+.');
163
+ }
164
+
165
+ async function decompressGzip(data: Uint8Array): Promise<Uint8Array> {
166
+ if (typeof DecompressionStream !== 'undefined') {
167
+ const stream = new DecompressionStream('gzip');
168
+ const writer = stream.writable.getWriter();
169
+ void writer.write(toBuffer(data));
170
+ void writer.close();
171
+ const chunks: Uint8Array[] = [];
172
+ const reader = stream.readable.getReader();
173
+ for (;;) {
174
+ const { done, value } = await reader.read();
175
+ if (done) break;
176
+ chunks.push(value);
177
+ }
178
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
179
+ const result = new Uint8Array(totalLength);
180
+ let offset = 0;
181
+ for (const chunk of chunks) {
182
+ result.set(chunk, offset);
183
+ offset += chunk.length;
184
+ }
185
+ return result;
186
+ }
187
+ throw new Error('DecompressionStream not available. Please use a modern browser or Node.js 18+.');
188
+ }
189
+
190
+ // --- Buffer helpers ---
191
+
192
+ /** Convert Uint8Array to ArrayBuffer (fixes TS 5.5+ BufferSource strictness). */
193
+ function toBuffer(data: Uint8Array): ArrayBuffer {
194
+ return (data.buffer as ArrayBuffer).slice(data.byteOffset, data.byteOffset + data.byteLength);
195
+ }
196
+
197
+ // --- Random helpers ---
198
+
199
+ function randomBytes(size: number): Uint8Array {
200
+ const bytes = new Uint8Array(size);
201
+ crypto.getRandomValues(bytes);
202
+ return bytes;
203
+ }
204
+
205
+ function randomAlphanumeric(size: number): string {
206
+ const bytes = randomBytes(size);
207
+ let result = '';
208
+ for (let i = 0; i < size; i++) {
209
+ result += ALPHANUMERIC[bytes[i] % 62];
210
+ }
211
+ return result;
212
+ }
@@ -0,0 +1,25 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { HttpClient } from './types';
5
+
6
+ export class DefaultHttpClient implements HttpClient {
7
+ private timeout: number;
8
+
9
+ constructor(timeout: number = 60000) {
10
+ this.timeout = timeout;
11
+ }
12
+
13
+ async fetch(url: string, init: RequestInit): Promise<Response> {
14
+ if (this.timeout > 0) {
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(() => controller.abort(), this.timeout);
17
+ try {
18
+ return await fetch(url, { ...init, signal: controller.signal });
19
+ } finally {
20
+ clearTimeout(timer);
21
+ }
22
+ }
23
+ return fetch(url, init);
24
+ }
25
+ }
@@ -0,0 +1,37 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { LoggerLevel } from './types';
5
+ import type { Logger } from './types';
6
+
7
+ export class DefaultLogger implements Logger {
8
+ private level: LoggerLevel;
9
+
10
+ constructor(level: LoggerLevel = LoggerLevel.Info) {
11
+ this.level = level;
12
+ }
13
+
14
+ debug(msg: string, ...args: unknown[]): void {
15
+ if (this.level <= LoggerLevel.Debug) {
16
+ console.debug(`[sdk] DEBUG ${msg}`, ...args);
17
+ }
18
+ }
19
+
20
+ info(msg: string, ...args: unknown[]): void {
21
+ if (this.level <= LoggerLevel.Info) {
22
+ console.info(`[sdk] INFO ${msg}`, ...args);
23
+ }
24
+ }
25
+
26
+ warn(msg: string, ...args: unknown[]): void {
27
+ if (this.level <= LoggerLevel.Warn) {
28
+ console.warn(`[sdk] WARN ${msg}`, ...args);
29
+ }
30
+ }
31
+
32
+ error(msg: string, ...args: unknown[]): void {
33
+ if (this.level <= LoggerLevel.Error) {
34
+ console.error(`[sdk] ERROR ${msg}`, ...args);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { TimeManager } from './types';
5
+
6
+ export class DefaultTimeManager implements TimeManager {
7
+ private serverTimeBase = 0;
8
+ private systemTimeBase = 0;
9
+
10
+ getSystemTimestamp(): number {
11
+ return Date.now();
12
+ }
13
+
14
+ getServerTimestamp(): number {
15
+ if (this.serverTimeBase === 0) {
16
+ return this.getSystemTimestamp();
17
+ }
18
+ return this.getSystemTimestamp() - this.systemTimeBase + this.serverTimeBase;
19
+ }
20
+
21
+ syncServerTimestamp(timestamp: number): void {
22
+ if (timestamp <= this.serverTimeBase) {
23
+ return;
24
+ }
25
+ this.serverTimeBase = timestamp;
26
+ this.systemTimeBase = this.getSystemTimestamp();
27
+ }
28
+ }