@connextable/popbill-runtime 1.0.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/LICENSE.md +19 -0
- package/dist/index.d.mts +245 -0
- package/dist/index.mjs +352 -0
- package/package.json +46 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2014 Linkhub
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as Spec from "@connextable/popbill-spec";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/types.d.ts
|
|
4
|
+
type LinkhubTokenApiResponse = Spec.PopbillIssueTokenApiResponse;
|
|
5
|
+
interface LinkhubTokenResponse {
|
|
6
|
+
sessionToken: string;
|
|
7
|
+
serviceId: Spec.PopbillIssueTokenApiResponse['serviceID'];
|
|
8
|
+
linkId: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
partnerCode: string;
|
|
11
|
+
userCode: string;
|
|
12
|
+
scopes: Spec.PopbillIssueTokenApiResponse['scope'];
|
|
13
|
+
ipAddress: string;
|
|
14
|
+
expiredAt: Spec.PopbillIssueTokenApiResponse['expiration'];
|
|
15
|
+
}
|
|
16
|
+
interface IssueTokenRequest {
|
|
17
|
+
serviceId: Spec.PopbillIssueTokenApiRequestPath['serviceId'];
|
|
18
|
+
accessId: string;
|
|
19
|
+
scopes: readonly Spec.PopbillIssueTokenApiRequestBody['scope'][number][];
|
|
20
|
+
forwardedIp?: string;
|
|
21
|
+
}
|
|
22
|
+
interface LinkhubAuthClient {
|
|
23
|
+
issueToken(request: IssueTokenRequest): Promise<LinkhubTokenResponse>;
|
|
24
|
+
}
|
|
25
|
+
interface LinkhubAuthClientConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Linkhub LinkID (required).
|
|
28
|
+
*/
|
|
29
|
+
linkId: string;
|
|
30
|
+
/**
|
|
31
|
+
* Linkhub SecretKey (required, base64-encoded).
|
|
32
|
+
*/
|
|
33
|
+
secretKey: string;
|
|
34
|
+
/**
|
|
35
|
+
* Override auth base URL.
|
|
36
|
+
*/
|
|
37
|
+
authBaseUrl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Use static outbound auth host.
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
useStaticIp?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Use GA auth host.
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
useGaIp?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Use local UTC time for x-lh-date.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
useLocalTime?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Request timeout in milliseconds.
|
|
55
|
+
* @default 180000
|
|
56
|
+
*/
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
}
|
|
59
|
+
interface TokenProvider {
|
|
60
|
+
getToken(businessNumber: string): Promise<LinkhubTokenResponse>;
|
|
61
|
+
}
|
|
62
|
+
interface CreateTokenProviderInput {
|
|
63
|
+
authClient: LinkhubAuthClient;
|
|
64
|
+
serviceId: Spec.PopbillIssueTokenApiRequestPath['serviceId'];
|
|
65
|
+
scopes: readonly Spec.PopbillIssueTokenApiRequestBody['scope'][number][];
|
|
66
|
+
forwardedIp?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Refresh cached tokens this many milliseconds before expiration.
|
|
69
|
+
* @default 60000
|
|
70
|
+
*/
|
|
71
|
+
refreshSkewMs?: number;
|
|
72
|
+
}
|
|
73
|
+
interface ResolvedLinkhubAuthClientConfig {
|
|
74
|
+
linkId: string;
|
|
75
|
+
secretKey: string;
|
|
76
|
+
authBaseUrl?: string;
|
|
77
|
+
useStaticIp: boolean;
|
|
78
|
+
useGaIp: boolean;
|
|
79
|
+
useLocalTime: boolean;
|
|
80
|
+
timeoutMs: number;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/auth/auth-client.d.ts
|
|
84
|
+
declare function createLinkhubAuthClient(config: LinkhubAuthClientConfig): LinkhubAuthClient;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/auth/token-provider.d.ts
|
|
87
|
+
declare function createTokenProvider(input: CreateTokenProviderInput): TokenProvider;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/auth/enums.d.ts
|
|
90
|
+
declare const LinkhubAuthScope: {
|
|
91
|
+
readonly Member: "member";
|
|
92
|
+
readonly TaxInvoice: "110";
|
|
93
|
+
readonly CashReceipt: "140";
|
|
94
|
+
readonly TradeStatement: "121";
|
|
95
|
+
readonly Invoice: "122";
|
|
96
|
+
readonly Estimate: "123";
|
|
97
|
+
readonly PurchaseOrder: "124";
|
|
98
|
+
readonly Deposit: "125";
|
|
99
|
+
readonly Receipt: "126";
|
|
100
|
+
readonly HomeTaxTaxInvoice: "111";
|
|
101
|
+
readonly HomeTaxCashReceipt: "141";
|
|
102
|
+
readonly BusinessCheck: "170";
|
|
103
|
+
readonly CorporateInfo: "171";
|
|
104
|
+
readonly DepositorNameCheck: "182";
|
|
105
|
+
readonly DepositorRealNameCheck: "183";
|
|
106
|
+
readonly AccountCheck: "180";
|
|
107
|
+
readonly AlimTalk: "153";
|
|
108
|
+
readonly BrandMessageI: "156";
|
|
109
|
+
readonly BrandMessageN: "157";
|
|
110
|
+
readonly BrandMessageM: "158";
|
|
111
|
+
readonly SMS: "150";
|
|
112
|
+
readonly LMS: "151";
|
|
113
|
+
readonly MMS: "152";
|
|
114
|
+
readonly Fax: "160";
|
|
115
|
+
readonly IntelligentFax: "161";
|
|
116
|
+
};
|
|
117
|
+
type LinkhubAuthScope = Spec.PopbillAuthScope;
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/api/linkhub-request-client.d.ts
|
|
120
|
+
type LinkhubRequestStage = 'issue_token' | 'request_api';
|
|
121
|
+
interface LinkhubRequestStageError {
|
|
122
|
+
requestStage: LinkhubRequestStage;
|
|
123
|
+
cause: unknown;
|
|
124
|
+
}
|
|
125
|
+
interface LinkhubRequestClientConfig {
|
|
126
|
+
apiBaseUrl: string;
|
|
127
|
+
timeoutMs: number;
|
|
128
|
+
tokenProvider: TokenProvider;
|
|
129
|
+
tokenCacheKey: string;
|
|
130
|
+
userAgent?: string;
|
|
131
|
+
acceptEncoding?: string | null;
|
|
132
|
+
acceptLanguage?: string;
|
|
133
|
+
}
|
|
134
|
+
interface LinkhubRequestOptions {
|
|
135
|
+
uri: string;
|
|
136
|
+
method?: string;
|
|
137
|
+
body?: RequestInit['body'];
|
|
138
|
+
contentType?: string | null;
|
|
139
|
+
headers?: Record<string, string>;
|
|
140
|
+
}
|
|
141
|
+
interface LinkhubRequestClient {
|
|
142
|
+
requestJson<T>(options: LinkhubRequestOptions): Promise<T>;
|
|
143
|
+
}
|
|
144
|
+
declare function createLinkhubRequestClient(config: LinkhubRequestClientConfig): LinkhubRequestClient;
|
|
145
|
+
declare function isLinkhubRequestStageError(error: unknown): error is LinkhubRequestStageError;
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/api/request-client.d.ts
|
|
148
|
+
type PopbillRequestStage = 'issue_token' | 'request_api';
|
|
149
|
+
interface PopbillRequestStageError {
|
|
150
|
+
requestStage: PopbillRequestStage;
|
|
151
|
+
cause: unknown;
|
|
152
|
+
}
|
|
153
|
+
interface PopbillRequestClientConfig {
|
|
154
|
+
apiBaseUrl: string;
|
|
155
|
+
timeoutMs: number;
|
|
156
|
+
tokenProvider: TokenProvider;
|
|
157
|
+
acceptEncoding?: Spec.PopbillAcceptEncoding | null;
|
|
158
|
+
acceptLanguage?: Spec.PopbillAcceptLanguage;
|
|
159
|
+
}
|
|
160
|
+
interface PopbillRequestOptions {
|
|
161
|
+
uri: string;
|
|
162
|
+
corpNum?: string;
|
|
163
|
+
userId?: string;
|
|
164
|
+
method?: string;
|
|
165
|
+
body?: RequestInit['body'];
|
|
166
|
+
contentType?: string | null;
|
|
167
|
+
submitId?: string;
|
|
168
|
+
headers?: Record<string, string>;
|
|
169
|
+
}
|
|
170
|
+
interface PopbillRequestClient {
|
|
171
|
+
requestJson<T>(options: PopbillRequestOptions): Promise<T>;
|
|
172
|
+
}
|
|
173
|
+
declare function createPopbillRequestClient(config: PopbillRequestClientConfig): PopbillRequestClient;
|
|
174
|
+
declare function isPopbillRequestStageError(error: unknown): error is PopbillRequestStageError;
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/spec-constants.d.ts
|
|
177
|
+
declare const PopbillServiceIds: {
|
|
178
|
+
readonly Test: "POPBILL_TEST";
|
|
179
|
+
readonly Production: "POPBILL";
|
|
180
|
+
};
|
|
181
|
+
declare const LinkhubServiceIds: {
|
|
182
|
+
readonly PopbillTest: "POPBILL_TEST";
|
|
183
|
+
readonly PopbillProduction: "POPBILL";
|
|
184
|
+
readonly JusoLink: "JUSOLINK";
|
|
185
|
+
};
|
|
186
|
+
type LinkhubServiceId = Spec.LinkhubServiceId;
|
|
187
|
+
declare const PopbillApiBaseUrls: {
|
|
188
|
+
readonly Test: "https://popbill-test.linkhub.co.kr";
|
|
189
|
+
readonly Production: "https://popbill.linkhub.co.kr";
|
|
190
|
+
readonly StaticTest: "https://static-popbill-test.linkhub.co.kr";
|
|
191
|
+
readonly StaticProduction: "https://static-popbill.linkhub.co.kr";
|
|
192
|
+
readonly GaTest: "https://ga-popbill-test.linkhub.co.kr";
|
|
193
|
+
readonly GaProduction: "https://ga-popbill.linkhub.co.kr";
|
|
194
|
+
};
|
|
195
|
+
declare const LinkhubApiBaseUrls: {
|
|
196
|
+
readonly PopbillTest: "https://popbill-test.linkhub.co.kr";
|
|
197
|
+
readonly PopbillProduction: "https://popbill.linkhub.co.kr";
|
|
198
|
+
readonly PopbillStaticTest: "https://static-popbill-test.linkhub.co.kr";
|
|
199
|
+
readonly PopbillStaticProduction: "https://static-popbill.linkhub.co.kr";
|
|
200
|
+
readonly PopbillGaTest: "https://ga-popbill-test.linkhub.co.kr";
|
|
201
|
+
readonly PopbillGaProduction: "https://ga-popbill.linkhub.co.kr";
|
|
202
|
+
readonly JusoLink: "https://jusolink.linkhub.co.kr";
|
|
203
|
+
};
|
|
204
|
+
declare const PopbillAuthBaseUrls: {
|
|
205
|
+
readonly Default: "https://auth.linkhub.co.kr";
|
|
206
|
+
readonly Static: "https://static-auth.linkhub.co.kr";
|
|
207
|
+
readonly Ga: "https://ga-auth.linkhub.co.kr";
|
|
208
|
+
};
|
|
209
|
+
declare const PopbillAcceptLanguages: {
|
|
210
|
+
readonly KoreanKorea: "ko-KR";
|
|
211
|
+
readonly EnglishUnitedStates: "en-US";
|
|
212
|
+
};
|
|
213
|
+
declare const PopbillHttpMethodOverrides: {
|
|
214
|
+
readonly BulkIssue: "BULKISSUE";
|
|
215
|
+
};
|
|
216
|
+
declare const PopbillLinkhubApiVersion: "2.0";
|
|
217
|
+
declare const PopbillAuthScopes: {
|
|
218
|
+
readonly Member: "member";
|
|
219
|
+
readonly TaxInvoice: "110";
|
|
220
|
+
readonly CashReceipt: "140";
|
|
221
|
+
readonly TradeStatement: "121";
|
|
222
|
+
readonly Invoice: "122";
|
|
223
|
+
readonly Estimate: "123";
|
|
224
|
+
readonly PurchaseOrder: "124";
|
|
225
|
+
readonly Deposit: "125";
|
|
226
|
+
readonly Receipt: "126";
|
|
227
|
+
readonly HomeTaxTaxInvoice: "111";
|
|
228
|
+
readonly HomeTaxCashReceipt: "141";
|
|
229
|
+
readonly BusinessCheck: "170";
|
|
230
|
+
readonly CorporateInfo: "171";
|
|
231
|
+
readonly DepositorNameCheck: "182";
|
|
232
|
+
readonly DepositorRealNameCheck: "183";
|
|
233
|
+
readonly AccountCheck: "180";
|
|
234
|
+
readonly AlimTalk: "153";
|
|
235
|
+
readonly BrandMessageI: "156";
|
|
236
|
+
readonly BrandMessageN: "157";
|
|
237
|
+
readonly BrandMessageM: "158";
|
|
238
|
+
readonly SMS: "150";
|
|
239
|
+
readonly LMS: "151";
|
|
240
|
+
readonly MMS: "152";
|
|
241
|
+
readonly Fax: "160";
|
|
242
|
+
readonly IntelligentFax: "161";
|
|
243
|
+
};
|
|
244
|
+
//#endregion
|
|
245
|
+
export { CreateTokenProviderInput, IssueTokenRequest, LinkhubApiBaseUrls, LinkhubAuthClient, LinkhubAuthClientConfig, LinkhubAuthScope, LinkhubRequestClient, LinkhubRequestClientConfig, LinkhubRequestOptions, LinkhubRequestStage, LinkhubRequestStageError, LinkhubServiceId, LinkhubServiceIds, LinkhubTokenApiResponse, LinkhubTokenResponse, PopbillAcceptLanguages, PopbillApiBaseUrls, PopbillAuthBaseUrls, PopbillAuthScopes, PopbillHttpMethodOverrides, PopbillLinkhubApiVersion, PopbillRequestClient, PopbillRequestClientConfig, PopbillRequestOptions, PopbillRequestStage, PopbillRequestStageError, PopbillServiceIds, ResolvedLinkhubAuthClientConfig, TokenProvider, createLinkhubAuthClient, createLinkhubRequestClient, createPopbillRequestClient, createTokenProvider, isLinkhubRequestStageError, isPopbillRequestStageError };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import * as Spec from "@connextable/popbill-spec";
|
|
2
|
+
import { hmacSha256Base64, isBlank, normalizeOptionalString, sha1Base64, sha256Base64, stringifyWithoutEmptyValues, trimTrailingSlash } from "@connextable/popbill-utils";
|
|
3
|
+
//#region src/transport/fetch-json.ts
|
|
4
|
+
var HttpResponseError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, body) {
|
|
8
|
+
super(`HTTP ${String(status)}`);
|
|
9
|
+
this.name = "HttpResponseError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
async function fetchJson(requestUrl, requestInit, options) {
|
|
15
|
+
const response = await fetchWithTimeout(requestUrl, requestInit, options.timeoutMs);
|
|
16
|
+
const responseBodyText = await response.text();
|
|
17
|
+
if (!response.ok) throw createHttpErrorPayload(response.status, parseErrorResponseBody(responseBodyText));
|
|
18
|
+
return parseJsonResponse(responseBodyText);
|
|
19
|
+
}
|
|
20
|
+
async function fetchText(requestUrl, requestInit, options) {
|
|
21
|
+
const response = await fetchWithTimeout(requestUrl, requestInit, options.timeoutMs);
|
|
22
|
+
const responseText = await response.text();
|
|
23
|
+
if (!response.ok) throw createHttpErrorPayload(response.status, responseText);
|
|
24
|
+
return responseText;
|
|
25
|
+
}
|
|
26
|
+
async function fetchWithTimeout(requestUrl, requestInit, timeoutMs) {
|
|
27
|
+
const externalSignal = requestInit.signal;
|
|
28
|
+
const abortController = new AbortController();
|
|
29
|
+
const abortFromExternalSignal = () => {
|
|
30
|
+
abortController.abort(externalSignal?.reason);
|
|
31
|
+
};
|
|
32
|
+
if (externalSignal?.aborted) abortFromExternalSignal();
|
|
33
|
+
else externalSignal?.addEventListener("abort", abortFromExternalSignal, { once: true });
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
abortController.abort();
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(requestUrl, {
|
|
39
|
+
...requestInit,
|
|
40
|
+
signal: abortController.signal
|
|
41
|
+
});
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
externalSignal?.removeEventListener("abort", abortFromExternalSignal);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parseJsonResponse(responseText) {
|
|
48
|
+
if (responseText.length === 0) return {};
|
|
49
|
+
return JSON.parse(responseText);
|
|
50
|
+
}
|
|
51
|
+
function parseErrorResponseBody(responseText) {
|
|
52
|
+
if (responseText.length === 0) return {};
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(responseText);
|
|
55
|
+
} catch {
|
|
56
|
+
return responseText;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function createHttpErrorPayload(status, body) {
|
|
60
|
+
return new HttpResponseError(status, body);
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/spec-constants.ts
|
|
64
|
+
const PopbillServiceIds = Spec.PopbillServiceIds;
|
|
65
|
+
const LinkhubServiceIds = Spec.LinkhubServiceIds;
|
|
66
|
+
const PopbillApiBaseUrls = Spec.PopbillApiBaseUrls;
|
|
67
|
+
const LinkhubApiBaseUrls = {
|
|
68
|
+
PopbillTest: PopbillApiBaseUrls.Test,
|
|
69
|
+
PopbillProduction: PopbillApiBaseUrls.Production,
|
|
70
|
+
PopbillStaticTest: PopbillApiBaseUrls.StaticTest,
|
|
71
|
+
PopbillStaticProduction: PopbillApiBaseUrls.StaticProduction,
|
|
72
|
+
PopbillGaTest: PopbillApiBaseUrls.GaTest,
|
|
73
|
+
PopbillGaProduction: PopbillApiBaseUrls.GaProduction,
|
|
74
|
+
JusoLink: "https://jusolink.linkhub.co.kr"
|
|
75
|
+
};
|
|
76
|
+
const PopbillAuthBaseUrls = Spec.PopbillAuthBaseUrls;
|
|
77
|
+
const PopbillAcceptLanguages = Spec.PopbillAcceptLanguages;
|
|
78
|
+
const PopbillHttpMethodOverrides = Spec.PopbillHttpMethodOverrides;
|
|
79
|
+
const PopbillLinkhubApiVersion = Spec.PopbillLinkhubApiVersion;
|
|
80
|
+
const PopbillAuthScopes = Spec.PopbillAuthScopes;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/auth/auth-client.ts
|
|
83
|
+
const LINKHUB_VERSION = PopbillLinkhubApiVersion;
|
|
84
|
+
const LINKHUB_USER_AGENT$1 = "NODEJS LINKHUB SDK";
|
|
85
|
+
const DEFAULT_TIMEOUT_MS = 18e4;
|
|
86
|
+
const POPBILL_SCOPE_SET = new Set(Object.values(PopbillAuthScopes));
|
|
87
|
+
function createLinkhubAuthClient(config) {
|
|
88
|
+
return { async issueToken(request) {
|
|
89
|
+
return issueTokenRequest({
|
|
90
|
+
linkId: config.linkId,
|
|
91
|
+
secretKey: config.secretKey,
|
|
92
|
+
authBaseUrl: normalizeAuthBaseUrl(config.authBaseUrl),
|
|
93
|
+
useStaticIp: config.useStaticIp ?? false,
|
|
94
|
+
useGaIp: config.useGaIp ?? false,
|
|
95
|
+
useLocalTime: config.useLocalTime ?? true,
|
|
96
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
97
|
+
}, request);
|
|
98
|
+
} };
|
|
99
|
+
}
|
|
100
|
+
function resolveAuthBaseUrl(config) {
|
|
101
|
+
if (config.authBaseUrl) return config.authBaseUrl;
|
|
102
|
+
const { useStaticIp, useGaIp } = config;
|
|
103
|
+
if (useGaIp) return PopbillAuthBaseUrls.Ga;
|
|
104
|
+
if (useStaticIp) return PopbillAuthBaseUrls.Static;
|
|
105
|
+
return PopbillAuthBaseUrls.Default;
|
|
106
|
+
}
|
|
107
|
+
async function issueTokenRequest(config, request) {
|
|
108
|
+
validateIssueTokenRequest(request);
|
|
109
|
+
const authBaseUrl = resolveAuthBaseUrl(config);
|
|
110
|
+
const resourceUri = `/${request.serviceId}/Token`;
|
|
111
|
+
const body = stringifyWithoutEmptyValues({
|
|
112
|
+
access_id: request.accessId,
|
|
113
|
+
scope: [...request.scopes]
|
|
114
|
+
});
|
|
115
|
+
const bodyDigest = sha256Base64(body);
|
|
116
|
+
const dateHeader = await resolveDateHeader(config, authBaseUrl);
|
|
117
|
+
const signature = hmacSha256Base64(buildSignaturePayload("POST", bodyDigest, dateHeader, canonicalizeLinkhubHeaderValues(buildCanonicalHeaders(request.forwardedIp)), resourceUri), config.secretKey);
|
|
118
|
+
const requestHeaders = {
|
|
119
|
+
Authorization: `LINKHUB ${config.linkId} ${signature}`,
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
"User-Agent": LINKHUB_USER_AGENT$1,
|
|
122
|
+
"x-lh-date": dateHeader,
|
|
123
|
+
"x-lh-version": LINKHUB_VERSION
|
|
124
|
+
};
|
|
125
|
+
if (request.forwardedIp) requestHeaders["x-lh-forwarded"] = request.forwardedIp;
|
|
126
|
+
return mapToLinkhubTokenResponse(await fetchJson(`${authBaseUrl}${resourceUri}`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: requestHeaders,
|
|
129
|
+
body
|
|
130
|
+
}, { timeoutMs: config.timeoutMs }));
|
|
131
|
+
}
|
|
132
|
+
async function resolveDateHeader(config, authBaseUrl) {
|
|
133
|
+
if (config.useLocalTime) return formatUtcDateTime(/* @__PURE__ */ new Date());
|
|
134
|
+
try {
|
|
135
|
+
const normalizedServerTime = normalizeUtcDateTime(await fetchText(`${authBaseUrl}/Time`, { method: "GET" }, { timeoutMs: config.timeoutMs }));
|
|
136
|
+
if (normalizedServerTime) return normalizedServerTime;
|
|
137
|
+
} catch {}
|
|
138
|
+
return formatUtcDateTime(/* @__PURE__ */ new Date());
|
|
139
|
+
}
|
|
140
|
+
function buildCanonicalHeaders(forwardedIp) {
|
|
141
|
+
const headers = { "x-lh-version": LINKHUB_VERSION };
|
|
142
|
+
if (forwardedIp) headers["x-lh-forwarded"] = forwardedIp;
|
|
143
|
+
return headers;
|
|
144
|
+
}
|
|
145
|
+
function canonicalizeLinkhubHeaderValues(headers) {
|
|
146
|
+
const normalizedEntries = [];
|
|
147
|
+
for (const [name, value] of Object.entries(headers)) normalizedEntries.push([name.toLowerCase().trim(), value.trim()]);
|
|
148
|
+
normalizedEntries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
149
|
+
const mergedValues = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const [key, value] of normalizedEntries) {
|
|
151
|
+
const existing = mergedValues.get(key);
|
|
152
|
+
if (existing) {
|
|
153
|
+
existing.push(value);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
mergedValues.set(key, [value]);
|
|
157
|
+
}
|
|
158
|
+
const canonicalValues = [];
|
|
159
|
+
for (const key of [...mergedValues.keys()].sort()) {
|
|
160
|
+
const values = mergedValues.get(key) ?? [];
|
|
161
|
+
canonicalValues.push(values.join(","));
|
|
162
|
+
}
|
|
163
|
+
return canonicalValues.join("\n");
|
|
164
|
+
}
|
|
165
|
+
function buildSignaturePayload(method, contentMd5, dateHeader, canonicalizedHeaderValues, resourceUri) {
|
|
166
|
+
if (canonicalizedHeaderValues.length === 0) return `${method}\n${contentMd5}\n${dateHeader}\n${resourceUri}`;
|
|
167
|
+
return `${method}\n${contentMd5}\n${dateHeader}\n${canonicalizedHeaderValues}\n${resourceUri}`;
|
|
168
|
+
}
|
|
169
|
+
function mapToLinkhubTokenResponse(response) {
|
|
170
|
+
return {
|
|
171
|
+
sessionToken: response.session_token,
|
|
172
|
+
serviceId: response.serviceID,
|
|
173
|
+
linkId: response.linkID,
|
|
174
|
+
userId: response.userID,
|
|
175
|
+
partnerCode: response.partnerCode,
|
|
176
|
+
userCode: response.usercode,
|
|
177
|
+
scopes: response.scope,
|
|
178
|
+
ipAddress: response.ipaddress,
|
|
179
|
+
expiredAt: response.expiration
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function validateIssueTokenRequest(request) {
|
|
183
|
+
if (request.serviceId.trim().length === 0) throw new Error("Linkhub serviceId is required.");
|
|
184
|
+
if (request.scopes.length === 0) throw new Error("At least one Linkhub auth scope is required.");
|
|
185
|
+
for (const scope of request.scopes) if (!POPBILL_SCOPE_SET.has(scope)) throw new Error(`Unsupported Linkhub auth scope: ${scope}`);
|
|
186
|
+
}
|
|
187
|
+
function formatUtcDateTime(date) {
|
|
188
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
189
|
+
}
|
|
190
|
+
function normalizeUtcDateTime(value) {
|
|
191
|
+
const trimmed = value.trim();
|
|
192
|
+
const parsedMilliseconds = Date.parse(trimmed);
|
|
193
|
+
if (Number.isNaN(parsedMilliseconds)) return;
|
|
194
|
+
return formatUtcDateTime(new Date(parsedMilliseconds));
|
|
195
|
+
}
|
|
196
|
+
function normalizeAuthBaseUrl(value) {
|
|
197
|
+
const normalized = normalizeOptionalString(value);
|
|
198
|
+
if (!normalized) return;
|
|
199
|
+
return trimTrailingSlash(normalized);
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/auth/token-provider.ts
|
|
203
|
+
const DEFAULT_TOKEN_REFRESH_SKEW_MS = 6e4;
|
|
204
|
+
function createTokenProvider(input) {
|
|
205
|
+
const tokenCache = /* @__PURE__ */ new Map();
|
|
206
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
207
|
+
const refreshSkewMs = resolveRefreshSkewMs(input.refreshSkewMs);
|
|
208
|
+
return { async getToken(businessNumber) {
|
|
209
|
+
const cachedToken = tokenCache.get(businessNumber);
|
|
210
|
+
if (cachedToken && !isExpiredToken(cachedToken, refreshSkewMs)) return cachedToken;
|
|
211
|
+
const pendingRequest = pendingRequests.get(businessNumber);
|
|
212
|
+
if (pendingRequest) return pendingRequest;
|
|
213
|
+
const issueTokenPromise = input.authClient.issueToken({
|
|
214
|
+
serviceId: input.serviceId,
|
|
215
|
+
accessId: businessNumber,
|
|
216
|
+
scopes: input.scopes,
|
|
217
|
+
forwardedIp: input.forwardedIp
|
|
218
|
+
}).then((issuedToken) => {
|
|
219
|
+
tokenCache.set(businessNumber, issuedToken);
|
|
220
|
+
return issuedToken;
|
|
221
|
+
}).finally(() => {
|
|
222
|
+
pendingRequests.delete(businessNumber);
|
|
223
|
+
});
|
|
224
|
+
pendingRequests.set(businessNumber, issueTokenPromise);
|
|
225
|
+
return issueTokenPromise;
|
|
226
|
+
} };
|
|
227
|
+
}
|
|
228
|
+
function isExpiredToken(token, refreshSkewMs) {
|
|
229
|
+
const expiresAt = Date.parse(token.expiredAt);
|
|
230
|
+
if (Number.isNaN(expiresAt)) return true;
|
|
231
|
+
return Date.now() + refreshSkewMs >= expiresAt;
|
|
232
|
+
}
|
|
233
|
+
function resolveRefreshSkewMs(value) {
|
|
234
|
+
if (value === void 0) return DEFAULT_TOKEN_REFRESH_SKEW_MS;
|
|
235
|
+
if (!Number.isFinite(value) || value < 0) throw new Error("refreshSkewMs must be a non-negative finite number.");
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/auth/enums.ts
|
|
240
|
+
const LinkhubAuthScope = PopbillAuthScopes;
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/api/linkhub-request-client.ts
|
|
243
|
+
const LINKHUB_USER_AGENT = "NODEJS LINKHUB SDK";
|
|
244
|
+
function createLinkhubRequestClient(config) {
|
|
245
|
+
const normalizedApiBaseUrl = trimTrailingSlash(config.apiBaseUrl);
|
|
246
|
+
const normalizedAcceptLanguage = normalizeOptionalString(config.acceptLanguage);
|
|
247
|
+
const normalizedUserAgent = normalizeOptionalString(config.userAgent) ?? LINKHUB_USER_AGENT;
|
|
248
|
+
return { async requestJson(options) {
|
|
249
|
+
const method = (options.method ?? "GET").toUpperCase();
|
|
250
|
+
const requestHeaders = {
|
|
251
|
+
...options.headers,
|
|
252
|
+
"User-Agent": normalizedUserAgent
|
|
253
|
+
};
|
|
254
|
+
if (options.contentType !== null) {
|
|
255
|
+
if (typeof options.contentType === "string") requestHeaders["Content-Type"] = options.contentType;
|
|
256
|
+
else if (!isFormDataBody$1(options.body) && method !== "GET") requestHeaders["Content-Type"] = "application/json;charset=utf-8";
|
|
257
|
+
}
|
|
258
|
+
if (config.acceptEncoding !== null) requestHeaders["Accept-Encoding"] = config.acceptEncoding === void 0 ? "gzip" : config.acceptEncoding;
|
|
259
|
+
if (normalizedAcceptLanguage) requestHeaders["Accept-Language"] = normalizedAcceptLanguage;
|
|
260
|
+
try {
|
|
261
|
+
requestHeaders["Authorization"] = `Bearer ${(await config.tokenProvider.getToken(config.tokenCacheKey)).sessionToken}`;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
throw createLinkhubStageError("issue_token", error);
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return await fetchJson(`${normalizedApiBaseUrl}${options.uri}`, {
|
|
267
|
+
method,
|
|
268
|
+
headers: requestHeaders,
|
|
269
|
+
body: method === "GET" ? void 0 : options.body ?? void 0
|
|
270
|
+
}, { timeoutMs: config.timeoutMs });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw createLinkhubStageError("request_api", error);
|
|
273
|
+
}
|
|
274
|
+
} };
|
|
275
|
+
}
|
|
276
|
+
function isFormDataBody$1(body) {
|
|
277
|
+
return typeof FormData !== "undefined" && body instanceof FormData;
|
|
278
|
+
}
|
|
279
|
+
function isLinkhubRequestStageError(error) {
|
|
280
|
+
if (typeof error !== "object" || error === null) return false;
|
|
281
|
+
if (!("requestStage" in error) || !("cause" in error)) return false;
|
|
282
|
+
const stage = error.requestStage;
|
|
283
|
+
return stage === "issue_token" || stage === "request_api";
|
|
284
|
+
}
|
|
285
|
+
function createLinkhubStageError(stage, cause) {
|
|
286
|
+
return Object.assign(/* @__PURE__ */ new Error(`Linkhub request failed at stage "${stage}".`), {
|
|
287
|
+
requestStage: stage,
|
|
288
|
+
cause
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/api/request-client.ts
|
|
293
|
+
const POPBILL_USER_AGENT = "NODEJS POPBILL SDK";
|
|
294
|
+
function createPopbillRequestClient(config) {
|
|
295
|
+
const normalizedApiBaseUrl = trimTrailingSlash(config.apiBaseUrl);
|
|
296
|
+
const normalizedAcceptLanguage = normalizeOptionalString(config.acceptLanguage);
|
|
297
|
+
return { async requestJson(options) {
|
|
298
|
+
const method = (options.method ?? "GET").toUpperCase();
|
|
299
|
+
const requestHeaders = {
|
|
300
|
+
...options.headers,
|
|
301
|
+
"User-Agent": POPBILL_USER_AGENT
|
|
302
|
+
};
|
|
303
|
+
if (options.contentType !== null) {
|
|
304
|
+
if (typeof options.contentType === "string") requestHeaders["Content-Type"] = options.contentType;
|
|
305
|
+
else if (!isFormDataBody(options.body)) requestHeaders["Content-Type"] = "application/json;charset=utf-8";
|
|
306
|
+
}
|
|
307
|
+
if (config.acceptEncoding !== null) requestHeaders["Accept-Encoding"] = config.acceptEncoding === void 0 ? "gzip" : config.acceptEncoding;
|
|
308
|
+
if (normalizedAcceptLanguage) requestHeaders["Accept-Language"] = normalizedAcceptLanguage;
|
|
309
|
+
if (isNonBlankString(options.userId)) requestHeaders["x-pb-userid"] = options.userId;
|
|
310
|
+
if (method !== "GET" && method !== "POST") {
|
|
311
|
+
requestHeaders["X-HTTP-Method-Override"] = method;
|
|
312
|
+
if (method === PopbillHttpMethodOverrides.BulkIssue) {
|
|
313
|
+
requestHeaders["X-PB-MESSAGE-DIGEST"] = sha1Base64(typeof options.body === "string" ? options.body : "");
|
|
314
|
+
if (isNonBlankString(options.submitId)) requestHeaders["X-PB-SUBMIT-ID"] = options.submitId;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (isNonBlankString(options.corpNum)) try {
|
|
318
|
+
requestHeaders["Authorization"] = `Bearer ${(await config.tokenProvider.getToken(options.corpNum)).sessionToken}`;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw createStageError("issue_token", error);
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
return await fetchJson(`${normalizedApiBaseUrl}${options.uri}`, {
|
|
324
|
+
method: method === "GET" ? "GET" : "POST",
|
|
325
|
+
headers: requestHeaders,
|
|
326
|
+
body: method === "GET" ? void 0 : options.body ?? void 0
|
|
327
|
+
}, { timeoutMs: config.timeoutMs });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw createStageError("request_api", error);
|
|
330
|
+
}
|
|
331
|
+
} };
|
|
332
|
+
}
|
|
333
|
+
function isFormDataBody(body) {
|
|
334
|
+
return typeof FormData !== "undefined" && body instanceof FormData;
|
|
335
|
+
}
|
|
336
|
+
function isNonBlankString(value) {
|
|
337
|
+
return !isBlank(value);
|
|
338
|
+
}
|
|
339
|
+
function isPopbillRequestStageError(error) {
|
|
340
|
+
if (typeof error !== "object" || error === null) return false;
|
|
341
|
+
if (!("requestStage" in error) || !("cause" in error)) return false;
|
|
342
|
+
const stage = error.requestStage;
|
|
343
|
+
return stage === "issue_token" || stage === "request_api";
|
|
344
|
+
}
|
|
345
|
+
function createStageError(stage, cause) {
|
|
346
|
+
return Object.assign(/* @__PURE__ */ new Error(`Popbill request failed at stage "${stage}".`), {
|
|
347
|
+
requestStage: stage,
|
|
348
|
+
cause
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
export { LinkhubApiBaseUrls, LinkhubAuthScope, LinkhubServiceIds, PopbillAcceptLanguages, PopbillApiBaseUrls, PopbillAuthBaseUrls, PopbillAuthScopes, PopbillHttpMethodOverrides, PopbillLinkhubApiVersion, PopbillServiceIds, createLinkhubAuthClient, createLinkhubRequestClient, createPopbillRequestClient, createTokenProvider, isLinkhubRequestStageError, isPopbillRequestStageError };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@connextable/popbill-runtime",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared runtime clients for Popbill authentication and API transport",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/connextable/popbill/issues"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/connextable/popbill.git"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.mjs",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@connextable/popbill-spec": "1.0.0",
|
|
28
|
+
"@connextable/popbill-utils": "1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsdown",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
"lint": "oxlint --type-aware",
|
|
38
|
+
"lint:fix": "oxlint --fix",
|
|
39
|
+
"fmt": "oxfmt",
|
|
40
|
+
"fmt:check": "oxfmt --check",
|
|
41
|
+
"typecheck": "tsgo -b --noEmit",
|
|
42
|
+
"typecheck:lib": "tsgo -p tsconfig.lib.json --noEmit",
|
|
43
|
+
"typecheck:test": "tsgo -p tsconfig.test.json --noEmit",
|
|
44
|
+
"clean": "del node_modules dist"
|
|
45
|
+
}
|
|
46
|
+
}
|