open-phiz-sdk 0.1.0

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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # `open-phiz-sdk`
2
+
3
+ Server-side TypeScript SDK for Open Phiz open login. Use this package in the mini-app business
4
+ backend. Do not bundle it into a mini-app, browser application, Flutter package, or native host app.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ bun add open-phiz-sdk
10
+ ```
11
+
12
+ Node.js 22.12 or newer is required.
13
+
14
+ ## Exchange a login code
15
+
16
+ ```ts
17
+ import { OpenPlatformClient } from 'open-phiz-sdk';
18
+
19
+ const openPlatform = new OpenPlatformClient({
20
+ baseUrl: process.env.OPEN_PHIZ_BASE_URL!,
21
+ appId: process.env.OPEN_PHIZ_APP_ID!,
22
+ appSecret: process.env.OPEN_PHIZ_APP_SECRET!,
23
+ });
24
+
25
+ const session = await openPlatform.code2Session(loginCode);
26
+ // Persist your own business session. Do not return sessionKey to the mini-app.
27
+ ```
28
+
29
+ ## Session and phone APIs
30
+
31
+ ```ts
32
+ const checked = await openPlatform.checkSession({
33
+ openId: session.openId,
34
+ sessionKey: session.sessionKey,
35
+ });
36
+
37
+ const reset = await openPlatform.resetSession({
38
+ openId: session.openId,
39
+ sessionKey: session.sessionKey,
40
+ });
41
+
42
+ const phoneInfo = await openPlatform.getPhoneNumber({ code: phoneAuthorizationCode });
43
+ ```
44
+
45
+ The SDK obtains and caches the app `access_token` in memory for session and phone operations.
46
+ Concurrent refreshes share one request. An invalid internally managed token is refreshed once; an
47
+ explicitly supplied token is never retried automatically.
48
+
49
+ ## Error handling
50
+
51
+ ```ts
52
+ import { OpenPlatformSdkError } from 'open-phiz-sdk';
53
+
54
+ try {
55
+ await openPlatform.code2Session(loginCode);
56
+ } catch (error) {
57
+ if (error instanceof OpenPlatformSdkError) {
58
+ console.error({
59
+ code: error.code,
60
+ status: error.status,
61
+ requestId: error.requestId,
62
+ retryable: error.retryable,
63
+ });
64
+ }
65
+ }
66
+ ```
67
+
68
+ Do not log the login code, AppSecret, access token, session key, or phone authorization code. Use the
69
+ returned Request ID with the Open Platform diagnostics page.
70
+
71
+ ## Transport policy
72
+
73
+ - HTTPS is mandatory outside localhost unless `allowInsecureHttp` is explicitly enabled for an
74
+ isolated development network.
75
+ - AppSecret is sent only in JSON request bodies, never in query strings.
76
+ - The SDK does not persist credentials or tokens to disk.
77
+ - Requests time out after 10 seconds by default and accept an `AbortSignal`.
@@ -0,0 +1,98 @@
1
+ interface OpenPlatformClientOptions {
2
+ /** Open Phiz origin or deployment prefix, for example https://open.example.com. */
3
+ baseUrl: string;
4
+ appId: string;
5
+ /** Store this value only in the business backend or its secret manager. */
6
+ appSecret: string;
7
+ /** Defaults to the runtime global fetch implementation. */
8
+ fetch?: typeof globalThis.fetch;
9
+ /** Per-request timeout. Defaults to 10 seconds. */
10
+ timeoutMs?: number;
11
+ /** Refresh cached app access tokens this long before expiry. Defaults to 60 seconds. */
12
+ tokenRefreshSkewMs?: number;
13
+ /** Intended for isolated development networks only. */
14
+ allowInsecureHttp?: boolean;
15
+ }
16
+ interface RequestOptions {
17
+ signal?: AbortSignal;
18
+ }
19
+ interface AccessTokenOptions extends RequestOptions {
20
+ forceRefresh?: boolean;
21
+ }
22
+ interface Code2SessionResult {
23
+ openId: string;
24
+ sessionKey: string;
25
+ expiresIn: number;
26
+ unionId?: string;
27
+ }
28
+ interface AppAccessTokenResult {
29
+ accessToken: string;
30
+ expiresIn: number;
31
+ }
32
+ interface SessionInput extends RequestOptions {
33
+ openId: string;
34
+ sessionKey: string;
35
+ accessToken?: string;
36
+ }
37
+ interface CheckSessionResult {
38
+ openId: string;
39
+ valid: true;
40
+ }
41
+ interface ResetSessionResult {
42
+ openId: string;
43
+ sessionKey: string;
44
+ }
45
+ interface PhoneNumberInput extends RequestOptions {
46
+ code: string;
47
+ accessToken?: string;
48
+ }
49
+ interface PhoneInfo {
50
+ phoneNumber: string;
51
+ purePhoneNumber: string;
52
+ countryCode: string;
53
+ watermark: {
54
+ timestamp: number;
55
+ appid: string;
56
+ };
57
+ }
58
+
59
+ declare class OpenPlatformClient {
60
+ private readonly baseUrl;
61
+ private readonly appId;
62
+ private readonly appSecret;
63
+ private readonly fetchImplementation;
64
+ private readonly timeoutMs;
65
+ private readonly tokenRefreshSkewMs;
66
+ private cachedAccessToken;
67
+ private pendingAccessToken;
68
+ constructor(options: OpenPlatformClientOptions);
69
+ code2Session(code: string, options?: RequestOptions): Promise<Code2SessionResult>;
70
+ getAccessToken(options?: AccessTokenOptions): Promise<AppAccessTokenResult>;
71
+ invalidateAccessToken(): void;
72
+ checkSession(input: SessionInput): Promise<CheckSessionResult>;
73
+ resetSession(input: SessionInput): Promise<ResetSessionResult>;
74
+ getPhoneNumber(input: PhoneNumberInput): Promise<PhoneInfo>;
75
+ private issueAccessToken;
76
+ private authorizedRequest;
77
+ private request;
78
+ private redactServerMessage;
79
+ }
80
+
81
+ type OpenPlatformErrorCode = number | 'ABORTED' | 'INVALID_RESPONSE' | 'NETWORK_ERROR' | 'TIMEOUT';
82
+ interface OpenPlatformSdkErrorOptions {
83
+ code: OpenPlatformErrorCode;
84
+ status?: number;
85
+ requestId?: string;
86
+ }
87
+ declare class OpenPlatformSdkError extends Error {
88
+ readonly code: OpenPlatformErrorCode;
89
+ readonly status: number | undefined;
90
+ readonly requestId: string | undefined;
91
+ readonly retryable: boolean;
92
+ constructor(message: string, options: OpenPlatformSdkErrorOptions);
93
+ }
94
+
95
+ /** Create the signature required by check-session and reset-session. */
96
+ declare function createSessionSignature(sessionKey: string): string;
97
+
98
+ export { type AccessTokenOptions, type AppAccessTokenResult, type CheckSessionResult, type Code2SessionResult, OpenPlatformClient, type OpenPlatformClientOptions, type OpenPlatformErrorCode, OpenPlatformSdkError, type PhoneInfo, type PhoneNumberInput, type RequestOptions, type ResetSessionResult, type SessionInput, createSessionSignature };
package/dist/index.js ADDED
@@ -0,0 +1,311 @@
1
+ import { createHmac } from 'crypto';
2
+
3
+ // src/errors.ts
4
+ var OpenPlatformSdkError = class extends Error {
5
+ code;
6
+ status;
7
+ requestId;
8
+ retryable;
9
+ constructor(message, options) {
10
+ super(message);
11
+ this.name = "OpenPlatformSdkError";
12
+ this.code = options.code;
13
+ this.status = options.status;
14
+ this.requestId = options.requestId;
15
+ this.retryable = options.code === "NETWORK_ERROR" || options.code === "TIMEOUT" || options.code === 42901 || options.status === 429 || options.status !== void 0 && options.status >= 500;
16
+ }
17
+ };
18
+ function createSessionSignature(sessionKey) {
19
+ if (!sessionKey.trim()) throw new TypeError("sessionKey must not be empty");
20
+ return createHmac("sha256", sessionKey).update("").digest("hex");
21
+ }
22
+
23
+ // src/client.ts
24
+ var DEFAULT_TIMEOUT_MS = 1e4;
25
+ var DEFAULT_TOKEN_REFRESH_SKEW_MS = 6e4;
26
+ function requiredString(value, field) {
27
+ if (typeof value !== "string" || !value) {
28
+ throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {
29
+ code: "INVALID_RESPONSE"
30
+ });
31
+ }
32
+ return value;
33
+ }
34
+ function requiredStringValue(value, field) {
35
+ if (typeof value !== "string") {
36
+ throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {
37
+ code: "INVALID_RESPONSE"
38
+ });
39
+ }
40
+ return value;
41
+ }
42
+ function requiredNonNegativeInteger(value, field) {
43
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
44
+ throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {
45
+ code: "INVALID_RESPONSE"
46
+ });
47
+ }
48
+ return value;
49
+ }
50
+ function requiredRecord(value, field) {
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52
+ throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {
53
+ code: "INVALID_RESPONSE"
54
+ });
55
+ }
56
+ return value;
57
+ }
58
+ function normalizeBaseUrl(value, allowInsecureHttp) {
59
+ let url;
60
+ try {
61
+ url = new URL(value);
62
+ } catch {
63
+ throw new TypeError("baseUrl must be a valid HTTP(S) URL");
64
+ }
65
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
66
+ throw new TypeError("baseUrl must use HTTP or HTTPS");
67
+ }
68
+ if (url.username || url.password || url.search || url.hash) {
69
+ throw new TypeError("baseUrl must not contain credentials, query parameters, or a fragment");
70
+ }
71
+ const localHostnames = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
72
+ if (url.protocol === "http:" && !allowInsecureHttp && !localHostnames.has(url.hostname)) {
73
+ throw new TypeError("baseUrl must use HTTPS outside localhost");
74
+ }
75
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/`;
76
+ return url;
77
+ }
78
+ function validateClientOptions(options) {
79
+ if (!options.appId.trim()) throw new TypeError("appId must not be empty");
80
+ if (!options.appSecret.trim()) throw new TypeError("appSecret must not be empty");
81
+ if (options.timeoutMs !== void 0 && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) {
82
+ throw new TypeError("timeoutMs must be a positive integer");
83
+ }
84
+ if (options.tokenRefreshSkewMs !== void 0 && (!Number.isInteger(options.tokenRefreshSkewMs) || options.tokenRefreshSkewMs < 0)) {
85
+ throw new TypeError("tokenRefreshSkewMs must be a non-negative integer");
86
+ }
87
+ }
88
+ var OpenPlatformClient = class {
89
+ baseUrl;
90
+ appId;
91
+ appSecret;
92
+ fetchImplementation;
93
+ timeoutMs;
94
+ tokenRefreshSkewMs;
95
+ cachedAccessToken;
96
+ pendingAccessToken;
97
+ constructor(options) {
98
+ validateClientOptions(options);
99
+ this.baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp === true);
100
+ this.appId = options.appId.trim();
101
+ this.appSecret = options.appSecret.trim();
102
+ this.fetchImplementation = options.fetch ?? globalThis.fetch;
103
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
104
+ this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? DEFAULT_TOKEN_REFRESH_SKEW_MS;
105
+ if (typeof this.fetchImplementation !== "function") {
106
+ throw new TypeError("A fetch implementation is required");
107
+ }
108
+ }
109
+ async code2Session(code, options = {}) {
110
+ if (!code.trim()) throw new TypeError("code must not be empty");
111
+ const data = await this.request("/api/v1/open/sns/code2session", {
112
+ body: { appId: this.appId, appSecret: this.appSecret, code: code.trim() },
113
+ ...options
114
+ });
115
+ const result = requiredRecord(data, "code2session response");
116
+ const unionId = result.unionId;
117
+ if (unionId !== void 0 && typeof unionId !== "string") {
118
+ throw new OpenPlatformSdkError("Open Platform returned an invalid unionId", {
119
+ code: "INVALID_RESPONSE"
120
+ });
121
+ }
122
+ return {
123
+ openId: requiredString(result.openId, "openId"),
124
+ sessionKey: requiredString(result.sessionKey, "sessionKey"),
125
+ expiresIn: requiredNonNegativeInteger(result.expiresIn, "expiresIn"),
126
+ ...unionId ? { unionId } : {}
127
+ };
128
+ }
129
+ async getAccessToken(options = {}) {
130
+ const now = Date.now();
131
+ if (options.forceRefresh !== true && this.cachedAccessToken && now < this.cachedAccessToken.expiresAtMs - this.cachedAccessToken.refreshSkewMs) {
132
+ return {
133
+ accessToken: this.cachedAccessToken.value,
134
+ expiresIn: Math.max(0, Math.floor((this.cachedAccessToken.expiresAtMs - now) / 1e3))
135
+ };
136
+ }
137
+ if (this.pendingAccessToken) return this.pendingAccessToken;
138
+ this.pendingAccessToken = this.issueAccessToken(options).finally(() => {
139
+ this.pendingAccessToken = void 0;
140
+ });
141
+ return this.pendingAccessToken;
142
+ }
143
+ invalidateAccessToken() {
144
+ this.cachedAccessToken = void 0;
145
+ }
146
+ async checkSession(input) {
147
+ if (!input.openId.trim()) throw new TypeError("openId must not be empty");
148
+ const data = await this.authorizedRequest(
149
+ "/api/v1/open/sns/check-session",
150
+ {
151
+ openId: input.openId.trim(),
152
+ signature: createSessionSignature(input.sessionKey),
153
+ sigMethod: "hmac_sha256"
154
+ },
155
+ input
156
+ );
157
+ const result = requiredRecord(data, "check-session response");
158
+ if (result.valid !== true) {
159
+ throw new OpenPlatformSdkError("Open Platform returned an invalid session status", {
160
+ code: "INVALID_RESPONSE"
161
+ });
162
+ }
163
+ return { openId: requiredString(result.openId, "openId"), valid: true };
164
+ }
165
+ async resetSession(input) {
166
+ if (!input.openId.trim()) throw new TypeError("openId must not be empty");
167
+ const data = await this.authorizedRequest(
168
+ "/api/v1/open/sns/reset-session",
169
+ {
170
+ openId: input.openId.trim(),
171
+ signature: createSessionSignature(input.sessionKey),
172
+ sigMethod: "hmac_sha256"
173
+ },
174
+ input
175
+ );
176
+ const result = requiredRecord(data, "reset-session response");
177
+ return {
178
+ openId: requiredString(result.openId, "openId"),
179
+ sessionKey: requiredString(result.sessionKey, "sessionKey")
180
+ };
181
+ }
182
+ async getPhoneNumber(input) {
183
+ if (!input.code.trim()) throw new TypeError("code must not be empty");
184
+ const data = await this.authorizedRequest(
185
+ "/api/v1/open/sns/phone-number",
186
+ { code: input.code.trim() },
187
+ input
188
+ );
189
+ const result = requiredRecord(data, "phone-number response");
190
+ const phoneInfo = requiredRecord(result.phoneInfo, "phoneInfo");
191
+ const watermark = requiredRecord(phoneInfo.watermark, "phoneInfo.watermark");
192
+ return {
193
+ phoneNumber: requiredString(phoneInfo.phoneNumber, "phoneInfo.phoneNumber"),
194
+ purePhoneNumber: requiredString(phoneInfo.purePhoneNumber, "phoneInfo.purePhoneNumber"),
195
+ countryCode: requiredStringValue(phoneInfo.countryCode, "phoneInfo.countryCode"),
196
+ watermark: {
197
+ timestamp: requiredNonNegativeInteger(watermark.timestamp, "phoneInfo.watermark.timestamp"),
198
+ appid: requiredString(watermark.appid, "phoneInfo.watermark.appid")
199
+ }
200
+ };
201
+ }
202
+ async issueAccessToken(options) {
203
+ const data = await this.request("/api/v1/open/sns/access-token", {
204
+ body: { appId: this.appId, appSecret: this.appSecret },
205
+ ...options
206
+ });
207
+ const result = requiredRecord(data, "access-token response");
208
+ const accessToken = requiredString(result.accessToken, "accessToken");
209
+ const expiresIn = requiredNonNegativeInteger(result.expiresIn, "expiresIn");
210
+ const ttlMs = expiresIn * 1e3;
211
+ this.cachedAccessToken = {
212
+ value: accessToken,
213
+ expiresAtMs: Date.now() + ttlMs,
214
+ refreshSkewMs: Math.min(this.tokenRefreshSkewMs, Math.floor(ttlMs / 2))
215
+ };
216
+ return { accessToken, expiresIn };
217
+ }
218
+ async authorizedRequest(path, body, options) {
219
+ const trimmedAccessToken = options.accessToken?.trim();
220
+ const explicitAccessToken = trimmedAccessToken === "" ? void 0 : trimmedAccessToken;
221
+ const accessToken = explicitAccessToken ?? (await this.getAccessToken(options)).accessToken;
222
+ try {
223
+ return await this.request(path, {
224
+ body,
225
+ accessToken,
226
+ ...options.signal ? { signal: options.signal } : {}
227
+ });
228
+ } catch (error) {
229
+ if (explicitAccessToken || !(error instanceof OpenPlatformSdkError) || error.code !== 40016) {
230
+ throw error;
231
+ }
232
+ this.invalidateAccessToken();
233
+ const refreshed = await this.getAccessToken({ ...options, forceRefresh: true });
234
+ return this.request(path, {
235
+ body,
236
+ accessToken: refreshed.accessToken,
237
+ ...options.signal ? { signal: options.signal } : {}
238
+ });
239
+ }
240
+ }
241
+ async request(path, options) {
242
+ const url = new URL(path.replace(/^\/+/, ""), this.baseUrl);
243
+ const controller = new AbortController();
244
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
245
+ const onAbort = () => controller.abort();
246
+ options.signal?.addEventListener("abort", onAbort, { once: true });
247
+ if (options.signal?.aborted) controller.abort();
248
+ let response;
249
+ try {
250
+ response = await this.fetchImplementation(url, {
251
+ method: "POST",
252
+ headers: {
253
+ Accept: "application/json",
254
+ "Content-Type": "application/json",
255
+ ...options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}
256
+ },
257
+ body: JSON.stringify(options.body),
258
+ signal: controller.signal
259
+ });
260
+ } catch {
261
+ const callerAborted = options.signal?.aborted === true;
262
+ const timedOut = controller.signal.aborted && !callerAborted;
263
+ throw new OpenPlatformSdkError(
264
+ callerAborted ? "Open Platform request was aborted" : timedOut ? "Open Platform request timed out" : "Open Platform request failed",
265
+ { code: callerAborted ? "ABORTED" : timedOut ? "TIMEOUT" : "NETWORK_ERROR" }
266
+ );
267
+ } finally {
268
+ clearTimeout(timeout);
269
+ options.signal?.removeEventListener("abort", onAbort);
270
+ }
271
+ const requestId = response.headers.get("x-request-id") ?? void 0;
272
+ let payload;
273
+ try {
274
+ payload = await response.json();
275
+ } catch {
276
+ throw new OpenPlatformSdkError("Open Platform returned non-JSON data", {
277
+ code: "INVALID_RESPONSE",
278
+ status: response.status,
279
+ ...requestId ? { requestId } : {}
280
+ });
281
+ }
282
+ if (!response.ok || payload.code !== 0) {
283
+ const code = typeof payload.code === "number" ? payload.code : "INVALID_RESPONSE";
284
+ const serverMessage = typeof payload.error === "string" ? this.redactServerMessage(payload.error, options) : "Request failed";
285
+ throw new OpenPlatformSdkError(`Open Platform request failed: ${serverMessage}`, {
286
+ code,
287
+ status: response.status,
288
+ ...requestId ? { requestId } : {}
289
+ });
290
+ }
291
+ return payload.data;
292
+ }
293
+ redactServerMessage(message, options) {
294
+ const values = /* @__PURE__ */ new Set([this.appSecret]);
295
+ if (options.accessToken) values.add(options.accessToken);
296
+ for (const [key, value] of Object.entries(options.body)) {
297
+ if (typeof value === "string" && /(code|secret|signature|token)/i.test(key)) {
298
+ values.add(value);
299
+ }
300
+ }
301
+ let redacted = message;
302
+ for (const value of values) {
303
+ if (value) redacted = redacted.replaceAll(value, "[REDACTED]");
304
+ }
305
+ return redacted;
306
+ }
307
+ };
308
+
309
+ export { OpenPlatformClient, OpenPlatformSdkError, createSessionSignature };
310
+ //# sourceMappingURL=index.js.map
311
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/signature.ts","../src/client.ts"],"names":[],"mappings":";;;AAaO,IAAM,oBAAA,GAAN,cAAmC,KAAA,CAAM;AAAA,EACrC,IAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,SAAA;AAAA,EAET,WAAA,CAAY,SAAiB,OAAA,EAAsC;AACjE,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,sBAAA;AACZ,IAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA;AACpB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,YAAY,OAAA,CAAQ,SAAA;AACzB,IAAA,IAAA,CAAK,YACH,OAAA,CAAQ,IAAA,KAAS,eAAA,IACjB,OAAA,CAAQ,SAAS,SAAA,IACjB,OAAA,CAAQ,IAAA,KAAS,KAAA,IACjB,QAAQ,MAAA,KAAW,GAAA,IAClB,QAAQ,MAAA,KAAW,MAAA,IAAa,QAAQ,MAAA,IAAU,GAAA;AAAA,EACvD;AACF;AC7BO,SAAS,uBAAuB,UAAA,EAA4B;AACjE,EAAA,IAAI,CAAC,UAAA,CAAW,IAAA,IAAQ,MAAM,IAAI,UAAU,8BAA8B,CAAA;AAC1E,EAAA,OAAO,UAAA,CAAW,UAAU,UAAU,CAAA,CAAE,OAAO,EAAE,CAAA,CAAE,OAAO,KAAK,CAAA;AACjE;;;ACSA,IAAM,kBAAA,GAAqB,GAAA;AAC3B,IAAM,6BAAA,GAAgC,GAAA;AAmBtC,SAAS,cAAA,CAAe,OAAgB,KAAA,EAAuB;AAC7D,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,EAAO;AACvC,IAAA,MAAM,IAAI,oBAAA,CAAqB,CAAA,kCAAA,EAAqC,KAAK,CAAA,CAAA,EAAI;AAAA,MAC3E,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,mBAAA,CAAoB,OAAgB,KAAA,EAAuB;AAClE,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,MAAM,IAAI,oBAAA,CAAqB,CAAA,kCAAA,EAAqC,KAAK,CAAA,CAAA,EAAI;AAAA,MAC3E,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,0BAAA,CAA2B,OAAgB,KAAA,EAAuB;AACzE,EAAA,IAAI,OAAO,UAAU,QAAA,IAAY,CAAC,OAAO,SAAA,CAAU,KAAK,CAAA,IAAK,KAAA,GAAQ,CAAA,EAAG;AACtE,IAAA,MAAM,IAAI,oBAAA,CAAqB,CAAA,kCAAA,EAAqC,KAAK,CAAA,CAAA,EAAI;AAAA,MAC3E,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,cAAA,CAAe,OAAgB,KAAA,EAAwC;AAC9E,EAAA,IAAI,CAAC,SAAS,OAAO,KAAA,KAAU,YAAY,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC/D,IAAA,MAAM,IAAI,oBAAA,CAAqB,CAAA,kCAAA,EAAqC,KAAK,CAAA,CAAA,EAAI;AAAA,MAC3E,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,gBAAA,CAAiB,OAAe,iBAAA,EAAiC;AACxE,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,KAAK,CAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,UAAU,qCAAqC,CAAA;AAAA,EAC3D;AAEA,EAAA,IAAI,GAAA,CAAI,QAAA,KAAa,QAAA,IAAY,GAAA,CAAI,aAAa,OAAA,EAAS;AACzD,IAAA,MAAM,IAAI,UAAU,gCAAgC,CAAA;AAAA,EACtD;AACA,EAAA,IAAI,IAAI,QAAA,IAAY,GAAA,CAAI,YAAY,GAAA,CAAI,MAAA,IAAU,IAAI,IAAA,EAAM;AAC1D,IAAA,MAAM,IAAI,UAAU,uEAAuE,CAAA;AAAA,EAC7F;AAEA,EAAA,MAAM,cAAA,uBAAqB,GAAA,CAAI,CAAC,aAAa,WAAA,EAAa,KAAA,EAAO,OAAO,CAAC,CAAA;AACzE,EAAA,IAAI,GAAA,CAAI,QAAA,KAAa,OAAA,IAAW,CAAC,iBAAA,IAAqB,CAAC,cAAA,CAAe,GAAA,CAAI,GAAA,CAAI,QAAQ,CAAA,EAAG;AACvF,IAAA,MAAM,IAAI,UAAU,0CAA0C,CAAA;AAAA,EAChE;AAEA,EAAA,GAAA,CAAI,WAAW,CAAA,EAAG,GAAA,CAAI,SAAS,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,CAAA,CAAA;AAClD,EAAA,OAAO,GAAA;AACT;AAEA,SAAS,sBAAsB,OAAA,EAA0C;AACvE,EAAA,IAAI,CAAC,QAAQ,KAAA,CAAM,IAAA,IAAQ,MAAM,IAAI,UAAU,yBAAyB,CAAA;AACxE,EAAA,IAAI,CAAC,QAAQ,SAAA,CAAU,IAAA,IAAQ,MAAM,IAAI,UAAU,6BAA6B,CAAA;AAChF,EAAA,IACE,OAAA,CAAQ,SAAA,KAAc,MAAA,KACrB,CAAC,MAAA,CAAO,SAAA,CAAU,OAAA,CAAQ,SAAS,CAAA,IAAK,OAAA,CAAQ,SAAA,IAAa,CAAA,CAAA,EAC9D;AACA,IAAA,MAAM,IAAI,UAAU,sCAAsC,CAAA;AAAA,EAC5D;AACA,EAAA,IACE,OAAA,CAAQ,kBAAA,KAAuB,MAAA,KAC9B,CAAC,MAAA,CAAO,SAAA,CAAU,OAAA,CAAQ,kBAAkB,CAAA,IAAK,OAAA,CAAQ,kBAAA,GAAqB,CAAA,CAAA,EAC/E;AACA,IAAA,MAAM,IAAI,UAAU,mDAAmD,CAAA;AAAA,EACzE;AACF;AAEO,IAAM,qBAAN,MAAyB;AAAA,EACb,OAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA,mBAAA;AAAA,EACA,SAAA;AAAA,EACA,kBAAA;AAAA,EACT,iBAAA;AAAA,EACA,kBAAA;AAAA,EAER,YAAY,OAAA,EAAoC;AAC9C,IAAA,qBAAA,CAAsB,OAAO,CAAA;AAC7B,IAAA,IAAA,CAAK,UAAU,gBAAA,CAAiB,OAAA,CAAQ,OAAA,EAAS,OAAA,CAAQ,sBAAsB,IAAI,CAAA;AACnF,IAAA,IAAA,CAAK,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,IAAA,EAAK;AAChC,IAAA,IAAA,CAAK,SAAA,GAAY,OAAA,CAAQ,SAAA,CAAU,IAAA,EAAK;AACxC,IAAA,IAAA,CAAK,mBAAA,GAAsB,OAAA,CAAQ,KAAA,IAAS,UAAA,CAAW,KAAA;AACvD,IAAA,IAAA,CAAK,SAAA,GAAY,QAAQ,SAAA,IAAa,kBAAA;AACtC,IAAA,IAAA,CAAK,kBAAA,GAAqB,QAAQ,kBAAA,IAAsB,6BAAA;AAExD,IAAA,IAAI,OAAO,IAAA,CAAK,mBAAA,KAAwB,UAAA,EAAY;AAClD,MAAA,MAAM,IAAI,UAAU,oCAAoC,CAAA;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,MAAM,YAAA,CAAa,IAAA,EAAc,OAAA,GAA0B,EAAC,EAAgC;AAC1F,IAAA,IAAI,CAAC,IAAA,CAAK,IAAA,IAAQ,MAAM,IAAI,UAAU,wBAAwB,CAAA;AAC9D,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAAQ,+BAAA,EAAiC;AAAA,MAC/D,IAAA,EAAM,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAO,SAAA,EAAW,IAAA,CAAK,SAAA,EAAW,IAAA,EAAM,IAAA,CAAK,IAAA,EAAK,EAAE;AAAA,MACxE,GAAG;AAAA,KACJ,CAAA;AAED,IAAA,MAAM,MAAA,GAAS,cAAA,CAAe,IAAA,EAAM,uBAAuB,CAAA;AAC3D,IAAA,MAAM,UAAU,MAAA,CAAO,OAAA;AACvB,IAAA,IAAI,OAAA,KAAY,MAAA,IAAa,OAAO,OAAA,KAAY,QAAA,EAAU;AACxD,MAAA,MAAM,IAAI,qBAAqB,2CAAA,EAA6C;AAAA,QAC1E,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AACA,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,cAAA,CAAe,MAAA,CAAO,MAAA,EAAQ,QAAQ,CAAA;AAAA,MAC9C,UAAA,EAAY,cAAA,CAAe,MAAA,CAAO,UAAA,EAAY,YAAY,CAAA;AAAA,MAC1D,SAAA,EAAW,0BAAA,CAA2B,MAAA,CAAO,SAAA,EAAW,WAAW,CAAA;AAAA,MACnE,GAAI,OAAA,GAAU,EAAE,OAAA,KAAY;AAAC,KAC/B;AAAA,EACF;AAAA,EAEA,MAAM,cAAA,CAAe,OAAA,GAA8B,EAAC,EAAkC;AACpF,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IACE,OAAA,CAAQ,YAAA,KAAiB,IAAA,IACzB,IAAA,CAAK,iBAAA,IACL,GAAA,GAAM,IAAA,CAAK,iBAAA,CAAkB,WAAA,GAAc,IAAA,CAAK,iBAAA,CAAkB,aAAA,EAClE;AACA,MAAA,OAAO;AAAA,QACL,WAAA,EAAa,KAAK,iBAAA,CAAkB,KAAA;AAAA,QACpC,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,IAAA,CAAK,iBAAA,CAAkB,WAAA,GAAc,GAAA,IAAO,GAAI,CAAC;AAAA,OACtF;AAAA,IACF;AAEA,IAAA,IAAI,IAAA,CAAK,kBAAA,EAAoB,OAAO,IAAA,CAAK,kBAAA;AACzC,IAAA,IAAA,CAAK,qBAAqB,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAA,CAAE,QAAQ,MAAM;AACrE,MAAA,IAAA,CAAK,kBAAA,GAAqB,MAAA;AAAA,IAC5B,CAAC,CAAA;AACD,IAAA,OAAO,IAAA,CAAK,kBAAA;AAAA,EACd;AAAA,EAEA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,iBAAA,GAAoB,MAAA;AAAA,EAC3B;AAAA,EAEA,MAAM,aAAa,KAAA,EAAkD;AACnE,IAAA,IAAI,CAAC,MAAM,MAAA,CAAO,IAAA,IAAQ,MAAM,IAAI,UAAU,0BAA0B,CAAA;AACxE,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,iBAAA;AAAA,MACtB,gCAAA;AAAA,MACA;AAAA,QACE,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,IAAA,EAAK;AAAA,QAC1B,SAAA,EAAW,sBAAA,CAAuB,KAAA,CAAM,UAAU,CAAA;AAAA,QAClD,SAAA,EAAW;AAAA,OACb;AAAA,MACA;AAAA,KACF;AACA,IAAA,MAAM,MAAA,GAAS,cAAA,CAAe,IAAA,EAAM,wBAAwB,CAAA;AAC5D,IAAA,IAAI,MAAA,CAAO,UAAU,IAAA,EAAM;AACzB,MAAA,MAAM,IAAI,qBAAqB,kDAAA,EAAoD;AAAA,QACjF,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AACA,IAAA,OAAO,EAAE,QAAQ,cAAA,CAAe,MAAA,CAAO,QAAQ,QAAQ,CAAA,EAAG,OAAO,IAAA,EAAK;AAAA,EACxE;AAAA,EAEA,MAAM,aAAa,KAAA,EAAkD;AACnE,IAAA,IAAI,CAAC,MAAM,MAAA,CAAO,IAAA,IAAQ,MAAM,IAAI,UAAU,0BAA0B,CAAA;AACxE,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,iBAAA;AAAA,MACtB,gCAAA;AAAA,MACA;AAAA,QACE,MAAA,EAAQ,KAAA,CAAM,MAAA,CAAO,IAAA,EAAK;AAAA,QAC1B,SAAA,EAAW,sBAAA,CAAuB,KAAA,CAAM,UAAU,CAAA;AAAA,QAClD,SAAA,EAAW;AAAA,OACb;AAAA,MACA;AAAA,KACF;AACA,IAAA,MAAM,MAAA,GAAS,cAAA,CAAe,IAAA,EAAM,wBAAwB,CAAA;AAC5D,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,cAAA,CAAe,MAAA,CAAO,MAAA,EAAQ,QAAQ,CAAA;AAAA,MAC9C,UAAA,EAAY,cAAA,CAAe,MAAA,CAAO,UAAA,EAAY,YAAY;AAAA,KAC5D;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,KAAA,EAA6C;AAChE,IAAA,IAAI,CAAC,MAAM,IAAA,CAAK,IAAA,IAAQ,MAAM,IAAI,UAAU,wBAAwB,CAAA;AACpE,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,iBAAA;AAAA,MACtB,+BAAA;AAAA,MACA,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,MAAK,EAAE;AAAA,MAC1B;AAAA,KACF;AACA,IAAA,MAAM,MAAA,GAAS,cAAA,CAAe,IAAA,EAAM,uBAAuB,CAAA;AAC3D,IAAA,MAAM,SAAA,GAAY,cAAA,CAAe,MAAA,CAAO,SAAA,EAAW,WAAW,CAAA;AAC9D,IAAA,MAAM,SAAA,GAAY,cAAA,CAAe,SAAA,CAAU,SAAA,EAAW,qBAAqB,CAAA;AAC3E,IAAA,OAAO;AAAA,MACL,WAAA,EAAa,cAAA,CAAe,SAAA,CAAU,WAAA,EAAa,uBAAuB,CAAA;AAAA,MAC1E,eAAA,EAAiB,cAAA,CAAe,SAAA,CAAU,eAAA,EAAiB,2BAA2B,CAAA;AAAA,MACtF,WAAA,EAAa,mBAAA,CAAoB,SAAA,CAAU,WAAA,EAAa,uBAAuB,CAAA;AAAA,MAC/E,SAAA,EAAW;AAAA,QACT,SAAA,EAAW,0BAAA,CAA2B,SAAA,CAAU,SAAA,EAAW,+BAA+B,CAAA;AAAA,QAC1F,KAAA,EAAO,cAAA,CAAe,SAAA,CAAU,KAAA,EAAO,2BAA2B;AAAA;AACpE,KACF;AAAA,EACF;AAAA,EAEA,MAAc,iBAAiB,OAAA,EAAwD;AACrF,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAAQ,+BAAA,EAAiC;AAAA,MAC/D,MAAM,EAAE,KAAA,EAAO,KAAK,KAAA,EAAO,SAAA,EAAW,KAAK,SAAA,EAAU;AAAA,MACrD,GAAG;AAAA,KACJ,CAAA;AACD,IAAA,MAAM,MAAA,GAAS,cAAA,CAAe,IAAA,EAAM,uBAAuB,CAAA;AAC3D,IAAA,MAAM,WAAA,GAAc,cAAA,CAAe,MAAA,CAAO,WAAA,EAAa,aAAa,CAAA;AACpE,IAAA,MAAM,SAAA,GAAY,0BAAA,CAA2B,MAAA,CAAO,SAAA,EAAW,WAAW,CAAA;AAC1E,IAAA,MAAM,QAAQ,SAAA,GAAY,GAAA;AAC1B,IAAA,IAAA,CAAK,iBAAA,GAAoB;AAAA,MACvB,KAAA,EAAO,WAAA;AAAA,MACP,WAAA,EAAa,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA;AAAA,MAC1B,aAAA,EAAe,KAAK,GAAA,CAAI,IAAA,CAAK,oBAAoB,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAC;AAAA,KACxE;AACA,IAAA,OAAO,EAAE,aAAa,SAAA,EAAU;AAAA,EAClC;AAAA,EAEA,MAAc,iBAAA,CACZ,IAAA,EACA,IAAA,EACA,OAAA,EACkB;AAClB,IAAA,MAAM,kBAAA,GAAqB,OAAA,CAAQ,WAAA,EAAa,IAAA,EAAK;AACrD,IAAA,MAAM,mBAAA,GAAsB,kBAAA,KAAuB,EAAA,GAAK,MAAA,GAAY,kBAAA;AACpE,IAAA,MAAM,cAAc,mBAAA,IAAA,CAAwB,MAAM,IAAA,CAAK,cAAA,CAAe,OAAO,CAAA,EAAG,WAAA;AAEhF,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM;AAAA,QAC9B,IAAA;AAAA,QACA,WAAA;AAAA,QACA,GAAI,QAAQ,MAAA,GAAS,EAAE,QAAQ,OAAA,CAAQ,MAAA,KAAW;AAAC,OACpD,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,uBAAuB,EAAE,KAAA,YAAiB,oBAAA,CAAA,IAAyB,KAAA,CAAM,SAAS,KAAA,EAAO;AAC3F,QAAA,MAAM,KAAA;AAAA,MACR;AACA,MAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,MAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,cAAA,CAAe,EAAE,GAAG,OAAA,EAAS,YAAA,EAAc,IAAA,EAAM,CAAA;AAC9E,MAAA,OAAO,IAAA,CAAK,QAAQ,IAAA,EAAM;AAAA,QACxB,IAAA;AAAA,QACA,aAAa,SAAA,CAAU,WAAA;AAAA,QACvB,GAAI,QAAQ,MAAA,GAAS,EAAE,QAAQ,OAAA,CAAQ,MAAA,KAAW;AAAC,OACpD,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,OAAA,CAAQ,IAAA,EAAc,OAAA,EAAmD;AACrF,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,IAAA,CAAK,QAAQ,MAAA,EAAQ,EAAE,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA;AAC1D,IAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,IAAA,MAAM,UAAU,UAAA,CAAW,MAAM,WAAW,KAAA,EAAM,EAAG,KAAK,SAAS,CAAA;AACnE,IAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,KAAA,EAAM;AACvC,IAAA,OAAA,CAAQ,QAAQ,gBAAA,CAAiB,OAAA,EAAS,SAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AACjE,IAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAA,EAAS,UAAA,CAAW,KAAA,EAAM;AAE9C,IAAA,IAAI,QAAA;AACJ,IAAA,IAAI;AACF,MAAA,QAAA,GAAW,MAAM,IAAA,CAAK,mBAAA,CAAoB,GAAA,EAAK;AAAA,QAC7C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,MAAA,EAAQ,kBAAA;AAAA,UACR,cAAA,EAAgB,kBAAA;AAAA,UAChB,GAAI,OAAA,CAAQ,WAAA,GAAc,EAAE,aAAA,EAAe,UAAU,OAAA,CAAQ,WAAW,CAAA,CAAA,EAAG,GAAI;AAAC,SAClF;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAI,CAAA;AAAA,QACjC,QAAQ,UAAA,CAAW;AAAA,OACpB,CAAA;AAAA,IACH,CAAA,CAAA,MAAQ;AACN,MAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,MAAA,EAAQ,OAAA,KAAY,IAAA;AAClD,MAAA,MAAM,QAAA,GAAW,UAAA,CAAW,MAAA,CAAO,OAAA,IAAW,CAAC,aAAA;AAC/C,MAAA,MAAM,IAAI,oBAAA;AAAA,QACR,aAAA,GACI,mCAAA,GACA,QAAA,GACE,iCAAA,GACA,8BAAA;AAAA,QACN,EAAE,IAAA,EAAM,aAAA,GAAgB,SAAA,GAAY,QAAA,GAAW,YAAY,eAAA;AAAgB,OAC7E;AAAA,IACF,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,OAAO,CAAA;AACpB,MAAA,OAAA,CAAQ,MAAA,EAAQ,mBAAA,CAAoB,OAAA,EAAS,OAAO,CAAA;AAAA,IACtD;AAEA,IAAA,MAAM,SAAA,GAAY,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA,IAAK,MAAA;AAC1D,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAW,MAAM,SAAS,IAAA,EAAK;AAAA,IACjC,CAAA,CAAA,MAAQ;AACN,MAAA,MAAM,IAAI,qBAAqB,sCAAA,EAAwC;AAAA,QACrE,IAAA,EAAM,kBAAA;AAAA,QACN,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,GAAI,SAAA,GAAY,EAAE,SAAA,KAAc;AAAC,OAClC,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,IAAM,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtC,MAAA,MAAM,OAAO,OAAO,OAAA,CAAQ,IAAA,KAAS,QAAA,GAAW,QAAQ,IAAA,GAAO,kBAAA;AAC/D,MAAA,MAAM,aAAA,GACJ,OAAO,OAAA,CAAQ,KAAA,KAAU,QAAA,GACrB,KAAK,mBAAA,CAAoB,OAAA,CAAQ,KAAA,EAAO,OAAO,CAAA,GAC/C,gBAAA;AACN,MAAA,MAAM,IAAI,oBAAA,CAAqB,CAAA,8BAAA,EAAiC,aAAa,CAAA,CAAA,EAAI;AAAA,QAC/E,IAAA;AAAA,QACA,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,GAAI,SAAA,GAAY,EAAE,SAAA,KAAc;AAAC,OAClC,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,EACjB;AAAA,EAEQ,mBAAA,CAAoB,SAAiB,OAAA,EAAyC;AACpF,IAAA,MAAM,yBAAS,IAAI,GAAA,CAAY,CAAC,IAAA,CAAK,SAAS,CAAC,CAAA;AAC/C,IAAA,IAAI,OAAA,CAAQ,WAAA,EAAa,MAAA,CAAO,GAAA,CAAI,QAAQ,WAAW,CAAA;AACvD,IAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,OAAA,CAAQ,IAAI,CAAA,EAAG;AACvD,MAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,gCAAA,CAAiC,IAAA,CAAK,GAAG,CAAA,EAAG;AAC3E,QAAA,MAAA,CAAO,IAAI,KAAK,CAAA;AAAA,MAClB;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,GAAW,OAAA;AACf,IAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,MAAA,IAAI,KAAA,EAAO,QAAA,GAAW,QAAA,CAAS,UAAA,CAAW,OAAO,YAAY,CAAA;AAAA,IAC/D;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AACF","file":"index.js","sourcesContent":["export type OpenPlatformErrorCode =\n | number\n | 'ABORTED'\n | 'INVALID_RESPONSE'\n | 'NETWORK_ERROR'\n | 'TIMEOUT';\n\ninterface OpenPlatformSdkErrorOptions {\n code: OpenPlatformErrorCode;\n status?: number;\n requestId?: string;\n}\n\nexport class OpenPlatformSdkError extends Error {\n readonly code: OpenPlatformErrorCode;\n readonly status: number | undefined;\n readonly requestId: string | undefined;\n readonly retryable: boolean;\n\n constructor(message: string, options: OpenPlatformSdkErrorOptions) {\n super(message);\n this.name = 'OpenPlatformSdkError';\n this.code = options.code;\n this.status = options.status;\n this.requestId = options.requestId;\n this.retryable =\n options.code === 'NETWORK_ERROR' ||\n options.code === 'TIMEOUT' ||\n options.code === 42901 ||\n options.status === 429 ||\n (options.status !== undefined && options.status >= 500);\n }\n}\n","import { createHmac } from 'node:crypto';\n\n/** Create the signature required by check-session and reset-session. */\nexport function createSessionSignature(sessionKey: string): string {\n if (!sessionKey.trim()) throw new TypeError('sessionKey must not be empty');\n return createHmac('sha256', sessionKey).update('').digest('hex');\n}\n","import { OpenPlatformSdkError } from './errors';\nimport { createSessionSignature } from './signature';\nimport type {\n AccessTokenOptions,\n AppAccessTokenResult,\n CheckSessionResult,\n Code2SessionResult,\n OpenPlatformClientOptions,\n PhoneInfo,\n PhoneNumberInput,\n RequestOptions,\n ResetSessionResult,\n SessionInput,\n} from './types';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_TOKEN_REFRESH_SKEW_MS = 60_000;\n\ninterface ApiEnvelope {\n code?: unknown;\n error?: unknown;\n data?: unknown;\n}\n\ninterface CachedAccessToken {\n value: string;\n expiresAtMs: number;\n refreshSkewMs: number;\n}\n\ninterface InternalRequestOptions extends RequestOptions {\n body: Record<string, unknown>;\n accessToken?: string;\n}\n\nfunction requiredString(value: unknown, field: string): string {\n if (typeof value !== 'string' || !value) {\n throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {\n code: 'INVALID_RESPONSE',\n });\n }\n return value;\n}\n\nfunction requiredStringValue(value: unknown, field: string): string {\n if (typeof value !== 'string') {\n throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {\n code: 'INVALID_RESPONSE',\n });\n }\n return value;\n}\n\nfunction requiredNonNegativeInteger(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {\n code: 'INVALID_RESPONSE',\n });\n }\n return value;\n}\n\nfunction requiredRecord(value: unknown, field: string): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new OpenPlatformSdkError(`Open Platform returned an invalid ${field}`, {\n code: 'INVALID_RESPONSE',\n });\n }\n return value as Record<string, unknown>;\n}\n\nfunction normalizeBaseUrl(value: string, allowInsecureHttp: boolean): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new TypeError('baseUrl must be a valid HTTP(S) URL');\n }\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new TypeError('baseUrl must use HTTP or HTTPS');\n }\n if (url.username || url.password || url.search || url.hash) {\n throw new TypeError('baseUrl must not contain credentials, query parameters, or a fragment');\n }\n\n const localHostnames = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);\n if (url.protocol === 'http:' && !allowInsecureHttp && !localHostnames.has(url.hostname)) {\n throw new TypeError('baseUrl must use HTTPS outside localhost');\n }\n\n url.pathname = `${url.pathname.replace(/\\/+$/, '')}/`;\n return url;\n}\n\nfunction validateClientOptions(options: OpenPlatformClientOptions): void {\n if (!options.appId.trim()) throw new TypeError('appId must not be empty');\n if (!options.appSecret.trim()) throw new TypeError('appSecret must not be empty');\n if (\n options.timeoutMs !== undefined &&\n (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)\n ) {\n throw new TypeError('timeoutMs must be a positive integer');\n }\n if (\n options.tokenRefreshSkewMs !== undefined &&\n (!Number.isInteger(options.tokenRefreshSkewMs) || options.tokenRefreshSkewMs < 0)\n ) {\n throw new TypeError('tokenRefreshSkewMs must be a non-negative integer');\n }\n}\n\nexport class OpenPlatformClient {\n private readonly baseUrl: URL;\n private readonly appId: string;\n private readonly appSecret: string;\n private readonly fetchImplementation: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n private readonly tokenRefreshSkewMs: number;\n private cachedAccessToken: CachedAccessToken | undefined;\n private pendingAccessToken: Promise<AppAccessTokenResult> | undefined;\n\n constructor(options: OpenPlatformClientOptions) {\n validateClientOptions(options);\n this.baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp === true);\n this.appId = options.appId.trim();\n this.appSecret = options.appSecret.trim();\n this.fetchImplementation = options.fetch ?? globalThis.fetch;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.tokenRefreshSkewMs = options.tokenRefreshSkewMs ?? DEFAULT_TOKEN_REFRESH_SKEW_MS;\n\n if (typeof this.fetchImplementation !== 'function') {\n throw new TypeError('A fetch implementation is required');\n }\n }\n\n async code2Session(code: string, options: RequestOptions = {}): Promise<Code2SessionResult> {\n if (!code.trim()) throw new TypeError('code must not be empty');\n const data = await this.request('/api/v1/open/sns/code2session', {\n body: { appId: this.appId, appSecret: this.appSecret, code: code.trim() },\n ...options,\n });\n\n const result = requiredRecord(data, 'code2session response');\n const unionId = result.unionId;\n if (unionId !== undefined && typeof unionId !== 'string') {\n throw new OpenPlatformSdkError('Open Platform returned an invalid unionId', {\n code: 'INVALID_RESPONSE',\n });\n }\n return {\n openId: requiredString(result.openId, 'openId'),\n sessionKey: requiredString(result.sessionKey, 'sessionKey'),\n expiresIn: requiredNonNegativeInteger(result.expiresIn, 'expiresIn'),\n ...(unionId ? { unionId } : {}),\n };\n }\n\n async getAccessToken(options: AccessTokenOptions = {}): Promise<AppAccessTokenResult> {\n const now = Date.now();\n if (\n options.forceRefresh !== true &&\n this.cachedAccessToken &&\n now < this.cachedAccessToken.expiresAtMs - this.cachedAccessToken.refreshSkewMs\n ) {\n return {\n accessToken: this.cachedAccessToken.value,\n expiresIn: Math.max(0, Math.floor((this.cachedAccessToken.expiresAtMs - now) / 1000)),\n };\n }\n\n if (this.pendingAccessToken) return this.pendingAccessToken;\n this.pendingAccessToken = this.issueAccessToken(options).finally(() => {\n this.pendingAccessToken = undefined;\n });\n return this.pendingAccessToken;\n }\n\n invalidateAccessToken(): void {\n this.cachedAccessToken = undefined;\n }\n\n async checkSession(input: SessionInput): Promise<CheckSessionResult> {\n if (!input.openId.trim()) throw new TypeError('openId must not be empty');\n const data = await this.authorizedRequest(\n '/api/v1/open/sns/check-session',\n {\n openId: input.openId.trim(),\n signature: createSessionSignature(input.sessionKey),\n sigMethod: 'hmac_sha256',\n },\n input\n );\n const result = requiredRecord(data, 'check-session response');\n if (result.valid !== true) {\n throw new OpenPlatformSdkError('Open Platform returned an invalid session status', {\n code: 'INVALID_RESPONSE',\n });\n }\n return { openId: requiredString(result.openId, 'openId'), valid: true };\n }\n\n async resetSession(input: SessionInput): Promise<ResetSessionResult> {\n if (!input.openId.trim()) throw new TypeError('openId must not be empty');\n const data = await this.authorizedRequest(\n '/api/v1/open/sns/reset-session',\n {\n openId: input.openId.trim(),\n signature: createSessionSignature(input.sessionKey),\n sigMethod: 'hmac_sha256',\n },\n input\n );\n const result = requiredRecord(data, 'reset-session response');\n return {\n openId: requiredString(result.openId, 'openId'),\n sessionKey: requiredString(result.sessionKey, 'sessionKey'),\n };\n }\n\n async getPhoneNumber(input: PhoneNumberInput): Promise<PhoneInfo> {\n if (!input.code.trim()) throw new TypeError('code must not be empty');\n const data = await this.authorizedRequest(\n '/api/v1/open/sns/phone-number',\n { code: input.code.trim() },\n input\n );\n const result = requiredRecord(data, 'phone-number response');\n const phoneInfo = requiredRecord(result.phoneInfo, 'phoneInfo');\n const watermark = requiredRecord(phoneInfo.watermark, 'phoneInfo.watermark');\n return {\n phoneNumber: requiredString(phoneInfo.phoneNumber, 'phoneInfo.phoneNumber'),\n purePhoneNumber: requiredString(phoneInfo.purePhoneNumber, 'phoneInfo.purePhoneNumber'),\n countryCode: requiredStringValue(phoneInfo.countryCode, 'phoneInfo.countryCode'),\n watermark: {\n timestamp: requiredNonNegativeInteger(watermark.timestamp, 'phoneInfo.watermark.timestamp'),\n appid: requiredString(watermark.appid, 'phoneInfo.watermark.appid'),\n },\n };\n }\n\n private async issueAccessToken(options: RequestOptions): Promise<AppAccessTokenResult> {\n const data = await this.request('/api/v1/open/sns/access-token', {\n body: { appId: this.appId, appSecret: this.appSecret },\n ...options,\n });\n const result = requiredRecord(data, 'access-token response');\n const accessToken = requiredString(result.accessToken, 'accessToken');\n const expiresIn = requiredNonNegativeInteger(result.expiresIn, 'expiresIn');\n const ttlMs = expiresIn * 1000;\n this.cachedAccessToken = {\n value: accessToken,\n expiresAtMs: Date.now() + ttlMs,\n refreshSkewMs: Math.min(this.tokenRefreshSkewMs, Math.floor(ttlMs / 2)),\n };\n return { accessToken, expiresIn };\n }\n\n private async authorizedRequest(\n path: string,\n body: Record<string, unknown>,\n options: RequestOptions & { accessToken?: string }\n ): Promise<unknown> {\n const trimmedAccessToken = options.accessToken?.trim();\n const explicitAccessToken = trimmedAccessToken === '' ? undefined : trimmedAccessToken;\n const accessToken = explicitAccessToken ?? (await this.getAccessToken(options)).accessToken;\n\n try {\n return await this.request(path, {\n body,\n accessToken,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n } catch (error) {\n if (explicitAccessToken || !(error instanceof OpenPlatformSdkError) || error.code !== 40016) {\n throw error;\n }\n this.invalidateAccessToken();\n const refreshed = await this.getAccessToken({ ...options, forceRefresh: true });\n return this.request(path, {\n body,\n accessToken: refreshed.accessToken,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n }\n }\n\n private async request(path: string, options: InternalRequestOptions): Promise<unknown> {\n const url = new URL(path.replace(/^\\/+/, ''), this.baseUrl);\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n const onAbort = () => controller.abort();\n options.signal?.addEventListener('abort', onAbort, { once: true });\n if (options.signal?.aborted) controller.abort();\n\n let response: Response;\n try {\n response = await this.fetchImplementation(url, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n ...(options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}),\n },\n body: JSON.stringify(options.body),\n signal: controller.signal,\n });\n } catch {\n const callerAborted = options.signal?.aborted === true;\n const timedOut = controller.signal.aborted && !callerAborted;\n throw new OpenPlatformSdkError(\n callerAborted\n ? 'Open Platform request was aborted'\n : timedOut\n ? 'Open Platform request timed out'\n : 'Open Platform request failed',\n { code: callerAborted ? 'ABORTED' : timedOut ? 'TIMEOUT' : 'NETWORK_ERROR' }\n );\n } finally {\n clearTimeout(timeout);\n options.signal?.removeEventListener('abort', onAbort);\n }\n\n const requestId = response.headers.get('x-request-id') ?? undefined;\n let payload: ApiEnvelope;\n try {\n payload = (await response.json()) as ApiEnvelope;\n } catch {\n throw new OpenPlatformSdkError('Open Platform returned non-JSON data', {\n code: 'INVALID_RESPONSE',\n status: response.status,\n ...(requestId ? { requestId } : {}),\n });\n }\n\n if (!response.ok || payload.code !== 0) {\n const code = typeof payload.code === 'number' ? payload.code : 'INVALID_RESPONSE';\n const serverMessage =\n typeof payload.error === 'string'\n ? this.redactServerMessage(payload.error, options)\n : 'Request failed';\n throw new OpenPlatformSdkError(`Open Platform request failed: ${serverMessage}`, {\n code,\n status: response.status,\n ...(requestId ? { requestId } : {}),\n });\n }\n\n return payload.data;\n }\n\n private redactServerMessage(message: string, options: InternalRequestOptions): string {\n const values = new Set<string>([this.appSecret]);\n if (options.accessToken) values.add(options.accessToken);\n for (const [key, value] of Object.entries(options.body)) {\n if (typeof value === 'string' && /(code|secret|signature|token)/i.test(key)) {\n values.add(value);\n }\n }\n\n let redacted = message;\n for (const value of values) {\n if (value) redacted = redacted.replaceAll(value, '[REDACTED]');\n }\n return redacted;\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "open-phiz-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Server-side SDK for Open Phiz open login APIs",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/phiz-clip/open-phiz.git",
9
+ "directory": "packages/open-platform-sdk"
10
+ },
11
+ "homepage": "https://github.com/phiz-clip/open-phiz#readme",
12
+ "bugs": "https://github.com/phiz-clip/open-phiz/issues",
13
+ "keywords": [
14
+ "open-phiz",
15
+ "mini-app",
16
+ "open-login",
17
+ "server-sdk"
18
+ ],
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "sideEffects": false,
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "dev": "tsup --watch",
36
+ "test": "bun test src",
37
+ "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage src",
38
+ "type-check": "tsc --noEmit",
39
+ "lint": "eslint src",
40
+ "lint:fix": "eslint src --fix",
41
+ "pack:check": "npm pack --dry-run --json",
42
+ "clean": "rm -rf dist"
43
+ },
44
+ "engines": {
45
+ "node": ">=22.12.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": false
50
+ },
51
+ "devDependencies": {
52
+ "@phiz/eslint-config": "workspace:*",
53
+ "@phiz/tsconfig": "workspace:*",
54
+ "@types/node": "^22.20.0",
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^6.0.3"
57
+ }
58
+ }