@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.
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/dist/index.cjs +1493 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +387 -0
- package/dist/index.d.ts +387 -0
- package/dist/index.js +1463 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/src/client.ts +85 -0
- package/src/core/api_client.ts +344 -0
- package/src/core/config.ts +25 -0
- package/src/core/consts.ts +8 -0
- package/src/core/crypto.ts +212 -0
- package/src/core/http_client.ts +25 -0
- package/src/core/logger.ts +37 -0
- package/src/core/time_manager.ts +28 -0
- package/src/core/types.ts +72 -0
- package/src/core/version.ts +5 -0
- package/src/core/ws_client.ts +423 -0
- package/src/index.ts +19 -0
- package/src/internal/transport.ts +507 -0
- package/src/service/im/index.ts +11 -0
- package/src/service/im/v1/chat.ts +41 -0
- package/src/service/im/v1/chat_model.ts +19 -0
- package/src/service/im/v1/index.ts +9 -0
- package/src/service/im/v1/message.ts +59 -0
- package/src/service/im/v1/message_enum.ts +20 -0
- package/src/service/im/v1/message_event.ts +40 -0
- package/src/service/im/v1/message_model.ts +259 -0
- package/src/service/im/v1/v1.ts +14 -0
|
@@ -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
|
+
}
|