@sayren/store-sdk 0.23.0 → 0.28.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/dist/index.d.mts +770 -616
- package/dist/index.mjs +259 -236
- package/dist/server.d.mts +19 -28
- package/dist/server.mjs +29 -42
- package/package.json +1 -1
- package/src/client.ts +41 -139
- package/src/index.ts +3 -2
- package/src/member-delegation.ts +51 -77
- package/src/oauth.ts +28 -33
- package/src/schemas/auth.ts +0 -64
- package/src/schemas/customer-delegation.ts +48 -0
- package/src/schemas/product.ts +7 -0
- package/src/schemas/scope.ts +1 -2
- package/src/schemas/store.ts +1 -1
- package/src/schemas/storefront-hosting.ts +28 -0
- package/src/schemas/subscription.ts +222 -0
- package/src/server.ts +1 -2
- package/src/session.ts +46 -29
- package/src/schemas/api-client.ts +0 -121
package/dist/server.d.mts
CHANGED
|
@@ -1,47 +1,38 @@
|
|
|
1
1
|
import { a as WEBHOOK_HEADERS, i as WEBHOOK_EVENT_TYPE_HEADER, n as WEBHOOK_DELIVERY_ID_HEADER, o as WEBHOOK_SIGNATURE_HEADER, r as WEBHOOK_EVENT_ID_HEADER, s as WEBHOOK_USER_AGENT, t as WEBHOOK_DEFAULT_TOLERANCE_SEC } from "./webhook-headers-D6QmPdEJ.mjs";
|
|
2
2
|
//#region src/member-delegation.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로
|
|
5
|
-
* (
|
|
4
|
+
* 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다.
|
|
5
|
+
* 계정 토큰(개인 액세스 토큰 등)을 쓰므로 **서버에서만** 부른다.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
|
|
7
|
+
* 응답은 구매자 액세스 토큰(30분)이고 refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
|
|
9
8
|
*/
|
|
10
9
|
interface DelegatedMemberToken {
|
|
11
10
|
accessToken: string;
|
|
12
11
|
expiresIn: number;
|
|
13
12
|
memberId: string;
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
/** api 주소(예: `https://api.sayren.app`) — `/v1/oauth2/token`을 붙인다 */
|
|
17
|
-
baseUrl: string;
|
|
18
|
-
clientId: string;
|
|
19
|
-
clientSecret: string;
|
|
20
|
-
fetch?: typeof globalThis.fetch;
|
|
21
|
-
}
|
|
22
|
-
/** 셀러 자체 인증의 사용자 id로 지목한다. 처음 보는 id면 구매자를 만든다(이메일·이름은 그때만 쓴다) */
|
|
23
|
-
export declare function delegateMemberByUid(options: DelegationOptions & {
|
|
24
|
-
uid: string;
|
|
25
|
-
email?: string;
|
|
26
|
-
name?: string;
|
|
27
|
-
/**
|
|
28
|
-
* 사용자가 내 서비스에서 실제로 로그인한 시각. 주면 구매자 토큰의 인증 시각이 되고(탈퇴처럼 최근 인증이 필요한 동작에
|
|
29
|
-
* 쓴다), 주지 않으면 인증 시각이 없는 토큰이 나온다
|
|
30
|
-
*/
|
|
31
|
-
authenticatedAt?: Date;
|
|
32
|
-
}): Promise<DelegatedMemberToken>;
|
|
33
|
-
/** 이메일로 지목한다. 처음 보는 이메일이면 구매자를 만든다 */
|
|
34
|
-
export declare function delegateMemberByEmail(options: DelegationOptions & {
|
|
35
|
-
email: string;
|
|
36
|
-
name?: string;
|
|
37
|
-
}): Promise<DelegatedMemberToken>;
|
|
38
|
-
/** 위임 실패 — `error`는 OAuth 오류 코드(`invalid_client`·`invalid_scope`·`invalid_request` 등) */
|
|
14
|
+
/** 위임 실패 — `error`는 api 오류 코드(엔벨로프 `error.code`, 없으면 `http_{status}`) */
|
|
39
15
|
export declare class MemberDelegationError extends Error {
|
|
40
16
|
readonly error: string;
|
|
41
17
|
readonly description: string;
|
|
42
18
|
readonly status: number;
|
|
43
19
|
constructor(error: string, description: string, status: number);
|
|
44
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 계정 토큰으로 구매자 위임(`POST /v1/customers/delegations`) — 계정 토큰(개인 액세스 토큰 등)과 스토어 id를 쓴다.
|
|
23
|
+
* `customer:rw`가 필요하다. `uid`(셀러 자체 인증의 사용자 id) 또는 `email`로 지목하고, 처음 보는 사용자면 구매자를 만든다.
|
|
24
|
+
*/
|
|
25
|
+
export declare function delegateMember(options: {
|
|
26
|
+
/** api 주소(예: `https://api.sayren.app`) */
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
accessToken: string;
|
|
29
|
+
storeId: string;
|
|
30
|
+
uid?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
authenticatedAt?: Date;
|
|
34
|
+
fetch?: typeof globalThis.fetch;
|
|
35
|
+
}): Promise<DelegatedMemberToken>;
|
|
45
36
|
//#endregion
|
|
46
37
|
//#region src/webhook-signature.d.ts
|
|
47
38
|
interface SignWebhookPayloadOptions {
|
package/dist/server.mjs
CHANGED
|
@@ -1,46 +1,6 @@
|
|
|
1
1
|
import { a as WEBHOOK_HEADERS, i as WEBHOOK_EVENT_TYPE_HEADER, n as WEBHOOK_DELIVERY_ID_HEADER, o as WEBHOOK_SIGNATURE_HEADER, r as WEBHOOK_EVENT_ID_HEADER, s as WEBHOOK_USER_AGENT, t as WEBHOOK_DEFAULT_TOLERANCE_SEC } from "./webhook-headers-ap_exsb9.mjs";
|
|
2
2
|
//#region src/member-delegation.ts
|
|
3
|
-
/**
|
|
4
|
-
function delegateMemberByUid(options) {
|
|
5
|
-
return delegate(options, {
|
|
6
|
-
subject_token_type: "urn:sayren:params:oauth:token-type:member-uid",
|
|
7
|
-
subject_token: options.uid,
|
|
8
|
-
...options.authenticatedAt ? { subject_auth_time: String(Math.floor(options.authenticatedAt.getTime() / 1e3)) } : {},
|
|
9
|
-
...options.email ? { subject_email: options.email } : {},
|
|
10
|
-
...options.name ? { subject_name: options.name } : {}
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
/** 이메일로 지목한다. 처음 보는 이메일이면 구매자를 만든다 */
|
|
14
|
-
function delegateMemberByEmail(options) {
|
|
15
|
-
return delegate(options, {
|
|
16
|
-
subject_token_type: "urn:sayren:params:oauth:token-type:member-ref",
|
|
17
|
-
subject_token: options.email,
|
|
18
|
-
...options.name ? { subject_name: options.name } : {}
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
async function delegate(options, subject) {
|
|
22
|
-
const doFetch = options.fetch ?? globalThis.fetch;
|
|
23
|
-
const basic = btoa(`${options.clientId}:${options.clientSecret}`);
|
|
24
|
-
const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/oauth2/token`, {
|
|
25
|
-
method: "POST",
|
|
26
|
-
headers: {
|
|
27
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
28
|
-
authorization: `Basic ${basic}`
|
|
29
|
-
},
|
|
30
|
-
body: new URLSearchParams({
|
|
31
|
-
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
32
|
-
...subject
|
|
33
|
-
}).toString()
|
|
34
|
-
});
|
|
35
|
-
const body = await res.json().catch(() => ({}));
|
|
36
|
-
if (!res.ok || typeof body.access_token !== "string") throw new MemberDelegationError(typeof body.error === "string" ? body.error : `http_${res.status}`, typeof body.error_description === "string" ? body.error_description : "", res.status);
|
|
37
|
-
return {
|
|
38
|
-
accessToken: body.access_token,
|
|
39
|
-
expiresIn: Number(body.expires_in),
|
|
40
|
-
memberId: String(body.memberId)
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/** 위임 실패 — `error`는 OAuth 오류 코드(`invalid_client`·`invalid_scope`·`invalid_request` 등) */
|
|
3
|
+
/** 위임 실패 — `error`는 api 오류 코드(엔벨로프 `error.code`, 없으면 `http_{status}`) */
|
|
44
4
|
var MemberDelegationError = class extends Error {
|
|
45
5
|
error;
|
|
46
6
|
description;
|
|
@@ -53,6 +13,33 @@ var MemberDelegationError = class extends Error {
|
|
|
53
13
|
this.name = "MemberDelegationError";
|
|
54
14
|
}
|
|
55
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* 계정 토큰으로 구매자 위임(`POST /v1/customers/delegations`) — 계정 토큰(개인 액세스 토큰 등)과 스토어 id를 쓴다.
|
|
18
|
+
* `customer:rw`가 필요하다. `uid`(셀러 자체 인증의 사용자 id) 또는 `email`로 지목하고, 처음 보는 사용자면 구매자를 만든다.
|
|
19
|
+
*/
|
|
20
|
+
async function delegateMember(options) {
|
|
21
|
+
const res = await (options.fetch ?? globalThis.fetch)(`${options.baseUrl.replace(/\/+$/, "")}/v1/customers/delegations`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"content-type": "application/json",
|
|
25
|
+
authorization: `Bearer ${options.accessToken}`,
|
|
26
|
+
"x-store-id": options.storeId
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
...options.uid ? { uid: options.uid } : {},
|
|
30
|
+
...options.email ? { email: options.email } : {},
|
|
31
|
+
...options.name ? { name: options.name } : {},
|
|
32
|
+
...options.authenticatedAt ? { authenticatedAt: options.authenticatedAt.toISOString() } : {}
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
const body = await res.json().catch(() => ({}));
|
|
36
|
+
if (!res.ok || typeof body.data?.accessToken !== "string") throw new MemberDelegationError(typeof body.error?.code === "string" ? body.error.code : `http_${res.status}`, typeof body.error?.message === "string" ? body.error.message : "", res.status);
|
|
37
|
+
return {
|
|
38
|
+
accessToken: body.data.accessToken,
|
|
39
|
+
expiresIn: Number(body.data.expiresIn),
|
|
40
|
+
memberId: String(body.data.memberId)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
56
43
|
//#endregion
|
|
57
44
|
//#region src/webhook-signature.ts
|
|
58
45
|
/**
|
|
@@ -177,4 +164,4 @@ async function verifyWebhookSignature(options) {
|
|
|
177
164
|
};
|
|
178
165
|
}
|
|
179
166
|
//#endregion
|
|
180
|
-
export { MemberDelegationError, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT,
|
|
167
|
+
export { MemberDelegationError, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, delegateMember, parseWebhookSignatureHeader, signWebhookPayload, verifyWebhookSignature };
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
ApiError,
|
|
5
|
-
createHttp,
|
|
6
|
-
path,
|
|
7
|
-
type Query,
|
|
8
|
-
resolveToken,
|
|
9
|
-
type TokenSource,
|
|
10
|
-
unwrapData,
|
|
11
|
-
} from "./http";
|
|
2
|
+
import { ApiError, createHttp, path, type Query, resolveToken, type TokenSource } from "./http";
|
|
12
3
|
import {
|
|
13
4
|
type AnalyticsPagesParams,
|
|
14
5
|
type AnalyticsProductsParams,
|
|
@@ -29,14 +20,6 @@ import {
|
|
|
29
20
|
analyticsSourcesQuerySchema,
|
|
30
21
|
analyticsSourcesSchema,
|
|
31
22
|
} from "./schemas/analytics";
|
|
32
|
-
import {
|
|
33
|
-
type ApiClientListParams,
|
|
34
|
-
apiClientSchema,
|
|
35
|
-
apiClientSecretRotatedSchema,
|
|
36
|
-
type OAuthClientRegistrationRequest,
|
|
37
|
-
oauthClientRegistrationRequestSchema,
|
|
38
|
-
oauthClientRegistrationResponseSchema,
|
|
39
|
-
} from "./schemas/api-client";
|
|
40
23
|
import { type ApiLogListOptions, apiLogPageSchema, apiLogSummarySchema } from "./schemas/api-log";
|
|
41
24
|
import {
|
|
42
25
|
type ApprovalDecisionRequest,
|
|
@@ -47,15 +30,7 @@ import {
|
|
|
47
30
|
approvalRequestSchema,
|
|
48
31
|
auditLogPageSchema,
|
|
49
32
|
} from "./schemas/audit";
|
|
50
|
-
import {
|
|
51
|
-
type ApiTokenCreateRequest,
|
|
52
|
-
apiTokenCreatedSchema,
|
|
53
|
-
apiTokenCreateRequestSchema,
|
|
54
|
-
apiTokenSchema,
|
|
55
|
-
clientCredentialsTokenResponseSchema,
|
|
56
|
-
myAccountSchema,
|
|
57
|
-
myStoreSchema,
|
|
58
|
-
} from "./schemas/auth";
|
|
33
|
+
import { myAccountSchema, myStoreSchema } from "./schemas/auth";
|
|
59
34
|
import {
|
|
60
35
|
type CategoryCreateRequest,
|
|
61
36
|
type CategoryDeleteStrategy,
|
|
@@ -292,6 +267,7 @@ import {
|
|
|
292
267
|
type WriteStorefrontFilesRequest,
|
|
293
268
|
writeStorefrontFilesRequestSchema,
|
|
294
269
|
} from "./schemas/storefront-hosting";
|
|
270
|
+
import { publicPlansSchema, storeSubscriptionSchema } from "./schemas/subscription";
|
|
295
271
|
import {
|
|
296
272
|
type UploadPurpose,
|
|
297
273
|
type UploadRequest,
|
|
@@ -305,20 +281,16 @@ import type { AuthorizeOptions, StoreSession } from "./session";
|
|
|
305
281
|
export interface StoreClientOptions {
|
|
306
282
|
/** 예: https://api.example.com/v1 */
|
|
307
283
|
baseUrl: string;
|
|
308
|
-
/** 유저 토큰 (1단계). 인증/스토어 목록 API에 사용 */
|
|
309
|
-
userToken?: TokenSource;
|
|
310
|
-
/** 스토어 토큰 (3단계). 테넌트 리소스 API에 사용 */
|
|
311
|
-
storeToken?: TokenSource;
|
|
312
|
-
/** 테넌트 storeId — storeHttp(테넌트 리소스) 요청에 X-Store-Id로 실린다(userToken 직접 인가). */
|
|
313
|
-
storeId?: TokenSource;
|
|
314
284
|
/**
|
|
315
|
-
* 토큰
|
|
316
|
-
*
|
|
285
|
+
* 계정 토큰(accounts가 발급한 JWT — 콘솔 로그인, 개인 액세스 토큰, 연결된 앱의 토큰).
|
|
286
|
+
* 계정 API(`/me/*`)와 스토어 API 모두에 Bearer로 싣는다. `session`을 주면 생략해도 세션 토큰을 쓴다.
|
|
317
287
|
*/
|
|
318
|
-
|
|
288
|
+
accessToken?: TokenSource;
|
|
289
|
+
/** 대상 스토어 id — 스토어 API 요청에 `X-Store-Id`로 싣는다. 계정 API(`/me/*`)에는 싣지 않는다 */
|
|
290
|
+
storeId?: TokenSource;
|
|
319
291
|
/**
|
|
320
|
-
* SDK 세션 매니저 — 전달하면
|
|
321
|
-
* 이 세션에서 자동 배선한다. 개별 옵션(
|
|
292
|
+
* SDK 세션 매니저 — 전달하면 Bearer 토큰·401 refresh·복구 불가 시 clear를
|
|
293
|
+
* 이 세션에서 자동 배선한다. 개별 옵션(accessToken/refresh/onUnauthorized)을 명시하면 그쪽이 우선한다.
|
|
322
294
|
*/
|
|
323
295
|
session?: StoreSession;
|
|
324
296
|
/** 401 시 세션 갱신 시도 — true면 요청을 1회 재시도한다(refresh 토큰 회전) */
|
|
@@ -328,44 +300,37 @@ export interface StoreClientOptions {
|
|
|
328
300
|
fetch?: typeof globalThis.fetch;
|
|
329
301
|
}
|
|
330
302
|
|
|
331
|
-
function formBody(fields: Record<string, string>): string {
|
|
332
|
-
return new URLSearchParams(fields).toString();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
303
|
export function createStoreClient(options: StoreClientOptions) {
|
|
336
304
|
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
337
305
|
const { session } = options;
|
|
338
306
|
|
|
339
|
-
// session이 있으면
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
options.userToken ?? (session ? () => session.getToken() : undefined);
|
|
307
|
+
// session이 있으면 토큰/refresh/onUnauthorized의 기본값을 세션에서 배선한다. 개별 옵션을 명시하면 그쪽이 우선한다.
|
|
308
|
+
const tokenSource: TokenSource =
|
|
309
|
+
options.accessToken ?? (session ? () => session.getToken() : undefined);
|
|
343
310
|
const refresh = options.refresh ?? (session ? () => session.refresh() : undefined);
|
|
344
311
|
const onUnauthorized = options.onUnauthorized ?? (session ? () => session.clear() : undefined);
|
|
345
|
-
// storeToken(명시 옵션)이 auth.apiToken(PAT/storeToken급 주입)보다 우선한다.
|
|
346
|
-
const storeTokenSource: TokenSource = options.storeToken ?? options.auth?.apiToken;
|
|
347
312
|
|
|
313
|
+
function bearer(): Record<string, string> {
|
|
314
|
+
const token = resolveToken(tokenSource);
|
|
315
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 계정 API(`/me/*`) — 스토어를 고르지 않는다 */
|
|
348
319
|
const userHttp = createHttp({
|
|
349
320
|
baseUrl: options.baseUrl,
|
|
350
321
|
fetch: fetchFn,
|
|
351
|
-
headers:
|
|
352
|
-
const token = resolveToken(userTokenSource);
|
|
353
|
-
return token ? { authorization: `Bearer ${token}` } : ({} as Record<string, string>);
|
|
354
|
-
},
|
|
322
|
+
headers: bearer,
|
|
355
323
|
refresh,
|
|
356
324
|
onUnauthorized,
|
|
357
325
|
});
|
|
358
326
|
|
|
327
|
+
/** 스토어 API — 같은 계정 토큰 + `X-Store-Id` */
|
|
359
328
|
const storeHttp = createHttp({
|
|
360
329
|
baseUrl: options.baseUrl,
|
|
361
330
|
fetch: fetchFn,
|
|
362
331
|
headers: () => {
|
|
363
|
-
const token = resolveToken(storeTokenSource);
|
|
364
332
|
const storeId = resolveToken(options.storeId);
|
|
365
|
-
return {
|
|
366
|
-
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
367
|
-
...(storeId ? { "x-store-id": storeId } : {}),
|
|
368
|
-
};
|
|
333
|
+
return { ...bearer(), ...(storeId ? { "x-store-id": storeId } : {}) };
|
|
369
334
|
},
|
|
370
335
|
refresh,
|
|
371
336
|
onUnauthorized,
|
|
@@ -391,7 +356,7 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
391
356
|
}
|
|
392
357
|
|
|
393
358
|
return {
|
|
394
|
-
/** session의 현재
|
|
359
|
+
/** session의 현재 액세스 토큰 (session 미배선 시 null — 던지지 않음) */
|
|
395
360
|
get accessToken(): string | null {
|
|
396
361
|
return session ? session.accessToken : null;
|
|
397
362
|
},
|
|
@@ -405,90 +370,11 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
405
370
|
userinfo: () => requireSession().userinfo(),
|
|
406
371
|
|
|
407
372
|
auth: {
|
|
408
|
-
/**
|
|
409
|
-
* 서버 간 연동(M2M) — client_credentials grant (client_secret_basic).
|
|
410
|
-
* 클라이언트가 스토어에 바인딩되어 있어 **storeToken이 곧바로 발급**된다.
|
|
411
|
-
* ⚠ client_secret은 서버 환경에서만 사용할 것 (브라우저 반입 금지)
|
|
412
|
-
*/
|
|
413
|
-
async issueStoreTokenByClientCredentials(
|
|
414
|
-
clientId: string,
|
|
415
|
-
clientSecret: string,
|
|
416
|
-
scope?: string,
|
|
417
|
-
) {
|
|
418
|
-
// RFC 7617 client_secret_basic — UTF-8 바이트를 base64로 인코딩한다.
|
|
419
|
-
const credentials = base64Utf8(`${clientId}:${clientSecret}`);
|
|
420
|
-
const response = await fetchFn(`${options.baseUrl}/oauth2/token`, {
|
|
421
|
-
method: "POST",
|
|
422
|
-
headers: {
|
|
423
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
424
|
-
authorization: `Basic ${credentials}`,
|
|
425
|
-
},
|
|
426
|
-
body: formBody({ grant_type: "client_credentials", ...(scope ? { scope } : {}) }),
|
|
427
|
-
});
|
|
428
|
-
if (!response.ok) {
|
|
429
|
-
const body = (await response.json().catch(() => null)) as {
|
|
430
|
-
error_description?: string;
|
|
431
|
-
} | null;
|
|
432
|
-
throw new Error(body?.error_description ?? `토큰 발급 실패 (HTTP ${response.status})`);
|
|
433
|
-
}
|
|
434
|
-
return clientCredentialsTokenResponseSchema.parse(unwrapData(await response.json()));
|
|
435
|
-
},
|
|
436
|
-
/** 2단계 — 소속 스토어 목록 */
|
|
373
|
+
/** 소속 스토어 목록 */
|
|
437
374
|
listMyStores: () => userHttp.request("GET", "/me/stores", z.array(myStoreSchema)),
|
|
438
|
-
/** RFC 7009 — token을 폐기한다 (public client는 client_id만 동반) */
|
|
439
|
-
async revoke(token: string, clientId: string) {
|
|
440
|
-
await fetchFn(`${options.baseUrl}/oauth2/revoke`, {
|
|
441
|
-
method: "POST",
|
|
442
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
443
|
-
body: formBody({ token, client_id: clientId }),
|
|
444
|
-
});
|
|
445
|
-
},
|
|
446
375
|
me: () => userHttp.request("GET", "/me", myAccountSchema),
|
|
447
376
|
},
|
|
448
377
|
|
|
449
|
-
/** API 토큰(PAT) 관리 — OWNER/ADMIN 전용. 발급된 토큰은 storeToken처럼 Bearer로 사용 */
|
|
450
|
-
apiTokens: {
|
|
451
|
-
create: (body: ApiTokenCreateRequest) =>
|
|
452
|
-
storeHttp.request("POST", "/store/api-tokens", apiTokenCreatedSchema, {
|
|
453
|
-
body: apiTokenCreateRequestSchema.parse(body),
|
|
454
|
-
}),
|
|
455
|
-
list: () => storeHttp.request("GET", "/store/api-tokens", z.array(apiTokenSchema)),
|
|
456
|
-
revoke: (tokenId: string) =>
|
|
457
|
-
storeHttp.requestVoid("DELETE", path`/store/api-tokens/${tokenId}`),
|
|
458
|
-
},
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* 앱(OAuth 클라이언트) 관리 — OWNER/ADMIN storeToken 전용(PAT·M2M 거부). auth-spec §12, 부록 B-13.
|
|
462
|
-
* 시크릿 평문은 register·rotateSecret 응답에서만 한 번 노출된다(apiTokens.create와 같은 성격).
|
|
463
|
-
*/
|
|
464
|
-
apiClients: {
|
|
465
|
-
/**
|
|
466
|
-
* DCR 등록 (RFC 7591) — `POST /v1/oauth2/register`. OAuth 프로토콜 표면이라 응답이 평면 JSON이다.
|
|
467
|
-
* 등록 오류(`invalid_client_metadata`·`invalid_redirect_uri`)는 ApiError(code=error,
|
|
468
|
-
* message=error_description)로, Bearer 인증 오류는 엔벨로프 그대로 ApiError로 던진다.
|
|
469
|
-
*/
|
|
470
|
-
register: (body: OAuthClientRegistrationRequest) =>
|
|
471
|
-
storeHttp.request("POST", "/oauth2/register", oauthClientRegistrationResponseSchema, {
|
|
472
|
-
body: oauthClientRegistrationRequestSchema.parse(body),
|
|
473
|
-
}),
|
|
474
|
-
list: (params?: ApiClientListParams) =>
|
|
475
|
-
storeHttp.request("GET", "/store/api-clients", z.array(apiClientSchema), {
|
|
476
|
-
query: params as Query,
|
|
477
|
-
}),
|
|
478
|
-
get: (clientId: string) =>
|
|
479
|
-
storeHttp.request("GET", path`/store/api-clients/${clientId}`, apiClientSchema),
|
|
480
|
-
/** confidential 전용. 기존 시크릿은 즉시 무효(유예 없음) */
|
|
481
|
-
rotateSecret: (clientId: string) =>
|
|
482
|
-
storeHttp.request(
|
|
483
|
-
"POST",
|
|
484
|
-
path`/store/api-clients/${clientId}/rotate-secret`,
|
|
485
|
-
apiClientSecretRotatedSchema,
|
|
486
|
-
),
|
|
487
|
-
/** REVOKED로 전환(되돌릴 수 없음). 이미 비활성이면 현재 상태를 그대로 돌려준다 */
|
|
488
|
-
deactivate: (clientId: string) =>
|
|
489
|
-
storeHttp.request("POST", path`/store/api-clients/${clientId}/deactivate`, apiClientSchema),
|
|
490
|
-
},
|
|
491
|
-
|
|
492
378
|
/**
|
|
493
379
|
* 미디어 업로드 — 자리를 받아(`POST /uploads`) 파일 바이트는 호출자가
|
|
494
380
|
* `uploadUrl`로 직접 PUT한다. 업로드가 끝나면 `url`이 공개 주소가 된다.
|
|
@@ -1071,7 +957,7 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
1071
957
|
/**
|
|
1072
958
|
* 결제(PG) 설정 — **콘솔 OWNER 전용**. ADMIN·STAFF·PAT·M2M·3rd-party 앱 토큰은 스코프가 있어도
|
|
1073
959
|
* 403 `INSUFFICIENT_ROLE`이다(조회 포함). OWNER 여부는 요청 시점의 스토어 멤버십으로 다시 확인하므로
|
|
1074
|
-
* 양도·강등
|
|
960
|
+
* 양도·강등 직후에도 바로 403이다. 비밀 값(시크릿 키·API Secret·웹훅 시크릿)은 요청으로만 보내고
|
|
1075
961
|
* 응답에는 `secrets[].masked`(앞 4자리 마스킹)만 나온다.
|
|
1076
962
|
*/
|
|
1077
963
|
paymentSettings: {
|
|
@@ -1583,6 +1469,22 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
1583
1469
|
}),
|
|
1584
1470
|
},
|
|
1585
1471
|
|
|
1472
|
+
subscription: {
|
|
1473
|
+
/**
|
|
1474
|
+
* `GET /v1/store/subscription` — 스코프 `store:r`. 현재 요금제·상태·사용량 기간·한도·이번 기간 사용량.
|
|
1475
|
+
* 결제 수단·청구 금액은 싣지 않는다. 요금제 변경은 셀러 콘솔에서만 한다
|
|
1476
|
+
*/
|
|
1477
|
+
get: () => storeHttp.request("GET", "/store/subscription", storeSubscriptionSchema),
|
|
1478
|
+
},
|
|
1479
|
+
|
|
1480
|
+
plans: {
|
|
1481
|
+
/**
|
|
1482
|
+
* `GET /v1/public/plans` — 인증 없음. 공개 요금제의 지금 버전(가격·기능 키 값). 값은 서버가 관리하고 언제든 바뀔 수 있어
|
|
1483
|
+
* 코드에 요금제 수치를 두지 말고 이 값을 읽는다
|
|
1484
|
+
*/
|
|
1485
|
+
listPublic: () => storeHttp.request("GET", "/public/plans", publicPlansSchema),
|
|
1486
|
+
},
|
|
1487
|
+
|
|
1586
1488
|
store: {
|
|
1587
1489
|
get: () => storeHttp.request("GET", "/store", storeSchema),
|
|
1588
1490
|
/**
|
package/src/index.ts
CHANGED
|
@@ -12,13 +12,11 @@ export {
|
|
|
12
12
|
buildAuthorizeUrl,
|
|
13
13
|
createPkcePair,
|
|
14
14
|
exchangeAuthorizationCode,
|
|
15
|
-
exchangeStoreTokenViaOAuth,
|
|
16
15
|
type OAuthTokenResponse,
|
|
17
16
|
oauthTokenResponseSchema,
|
|
18
17
|
refreshUserToken,
|
|
19
18
|
} from "./oauth";
|
|
20
19
|
export * from "./schemas/analytics";
|
|
21
|
-
export * from "./schemas/api-client";
|
|
22
20
|
export * from "./schemas/api-log";
|
|
23
21
|
export * from "./schemas/audit";
|
|
24
22
|
export * from "./schemas/auth";
|
|
@@ -27,6 +25,7 @@ export * from "./schemas/claim";
|
|
|
27
25
|
export * from "./schemas/common";
|
|
28
26
|
export * from "./schemas/coupon";
|
|
29
27
|
export * from "./schemas/customer";
|
|
28
|
+
export * from "./schemas/customer-delegation";
|
|
30
29
|
export * from "./schemas/fulfillment";
|
|
31
30
|
export * from "./schemas/login";
|
|
32
31
|
export * from "./schemas/misc";
|
|
@@ -40,6 +39,7 @@ export * from "./schemas/scope";
|
|
|
40
39
|
export * from "./schemas/shipping-policy";
|
|
41
40
|
export * from "./schemas/store";
|
|
42
41
|
export * from "./schemas/storefront-hosting";
|
|
42
|
+
export * from "./schemas/subscription";
|
|
43
43
|
export * from "./schemas/telemetry";
|
|
44
44
|
export * from "./schemas/upload";
|
|
45
45
|
export * from "./schemas/webhook-event";
|
|
@@ -48,6 +48,7 @@ export {
|
|
|
48
48
|
createStoreSession,
|
|
49
49
|
type SessionStorage,
|
|
50
50
|
STORE_SESSION_KEYS,
|
|
51
|
+
type StoreAuthServer,
|
|
51
52
|
type StoreSession,
|
|
52
53
|
type StoreSessionOptions,
|
|
53
54
|
} from "./session";
|
package/src/member-delegation.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로
|
|
3
|
-
* (
|
|
2
|
+
* 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다.
|
|
3
|
+
* 계정 토큰(개인 액세스 토큰 등)을 쓰므로 **서버에서만** 부른다.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
|
|
5
|
+
* 응답은 구매자 액세스 토큰(30분)이고 refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
export interface DelegatedMemberToken {
|
|
@@ -12,87 +11,62 @@ export interface DelegatedMemberToken {
|
|
|
12
11
|
memberId: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
options: DelegationOptions & {
|
|
26
|
-
uid: string;
|
|
27
|
-
email?: string;
|
|
28
|
-
name?: string;
|
|
29
|
-
/**
|
|
30
|
-
* 사용자가 내 서비스에서 실제로 로그인한 시각. 주면 구매자 토큰의 인증 시각이 되고(탈퇴처럼 최근 인증이 필요한 동작에
|
|
31
|
-
* 쓴다), 주지 않으면 인증 시각이 없는 토큰이 나온다
|
|
32
|
-
*/
|
|
33
|
-
authenticatedAt?: Date;
|
|
34
|
-
},
|
|
35
|
-
): Promise<DelegatedMemberToken> {
|
|
36
|
-
return delegate(options, {
|
|
37
|
-
subject_token_type: "urn:sayren:params:oauth:token-type:member-uid",
|
|
38
|
-
subject_token: options.uid,
|
|
39
|
-
...(options.authenticatedAt
|
|
40
|
-
? { subject_auth_time: String(Math.floor(options.authenticatedAt.getTime() / 1000)) }
|
|
41
|
-
: {}),
|
|
42
|
-
...(options.email ? { subject_email: options.email } : {}),
|
|
43
|
-
...(options.name ? { subject_name: options.name } : {}),
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 이메일로 지목한다. 처음 보는 이메일이면 구매자를 만든다 */
|
|
48
|
-
export function delegateMemberByEmail(
|
|
49
|
-
options: DelegationOptions & { email: string; name?: string },
|
|
50
|
-
): Promise<DelegatedMemberToken> {
|
|
51
|
-
return delegate(options, {
|
|
52
|
-
subject_token_type: "urn:sayren:params:oauth:token-type:member-ref",
|
|
53
|
-
subject_token: options.email,
|
|
54
|
-
...(options.name ? { subject_name: options.name } : {}),
|
|
55
|
-
});
|
|
14
|
+
/** 위임 실패 — `error`는 api 오류 코드(엔벨로프 `error.code`, 없으면 `http_{status}`) */
|
|
15
|
+
export class MemberDelegationError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly error: string,
|
|
18
|
+
readonly description: string,
|
|
19
|
+
readonly status: number,
|
|
20
|
+
) {
|
|
21
|
+
super(description ? `${error}: ${description}` : error);
|
|
22
|
+
this.name = "MemberDelegationError";
|
|
23
|
+
}
|
|
56
24
|
}
|
|
57
25
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
26
|
+
/**
|
|
27
|
+
* 계정 토큰으로 구매자 위임(`POST /v1/customers/delegations`) — 계정 토큰(개인 액세스 토큰 등)과 스토어 id를 쓴다.
|
|
28
|
+
* `customer:rw`가 필요하다. `uid`(셀러 자체 인증의 사용자 id) 또는 `email`로 지목하고, 처음 보는 사용자면 구매자를 만든다.
|
|
29
|
+
*/
|
|
30
|
+
export async function delegateMember(options: {
|
|
31
|
+
/** api 주소(예: `https://api.sayren.app`) */
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
accessToken: string;
|
|
34
|
+
storeId: string;
|
|
35
|
+
uid?: string;
|
|
36
|
+
email?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
authenticatedAt?: Date;
|
|
39
|
+
fetch?: typeof globalThis.fetch;
|
|
40
|
+
}): Promise<DelegatedMemberToken> {
|
|
62
41
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
63
|
-
const
|
|
64
|
-
const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/oauth2/token`, {
|
|
42
|
+
const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/customers/delegations`, {
|
|
65
43
|
method: "POST",
|
|
66
44
|
headers: {
|
|
67
|
-
"content-type": "application/
|
|
68
|
-
authorization: `
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
authorization: `Bearer ${options.accessToken}`,
|
|
47
|
+
"x-store-id": options.storeId,
|
|
69
48
|
},
|
|
70
|
-
body:
|
|
71
|
-
|
|
72
|
-
...
|
|
73
|
-
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
...(options.uid ? { uid: options.uid } : {}),
|
|
51
|
+
...(options.email ? { email: options.email } : {}),
|
|
52
|
+
...(options.name ? { name: options.name } : {}),
|
|
53
|
+
...(options.authenticatedAt
|
|
54
|
+
? { authenticatedAt: options.authenticatedAt.toISOString() }
|
|
55
|
+
: {}),
|
|
56
|
+
}),
|
|
74
57
|
});
|
|
75
|
-
const body = (await res.json().catch(() => ({}))) as
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
58
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
59
|
+
data?: { accessToken?: unknown; expiresIn?: unknown; memberId?: unknown };
|
|
60
|
+
error?: { code?: unknown; message?: unknown };
|
|
61
|
+
};
|
|
62
|
+
if (!res.ok || typeof body.data?.accessToken !== "string") {
|
|
63
|
+
const code = typeof body.error?.code === "string" ? body.error.code : `http_${res.status}`;
|
|
64
|
+
const message = typeof body.error?.message === "string" ? body.error.message : "";
|
|
65
|
+
throw new MemberDelegationError(code, message, res.status);
|
|
80
66
|
}
|
|
81
67
|
return {
|
|
82
|
-
accessToken: body.
|
|
83
|
-
expiresIn: Number(body.
|
|
84
|
-
memberId: String(body.memberId),
|
|
68
|
+
accessToken: body.data.accessToken,
|
|
69
|
+
expiresIn: Number(body.data.expiresIn),
|
|
70
|
+
memberId: String(body.data.memberId),
|
|
85
71
|
};
|
|
86
72
|
}
|
|
87
|
-
|
|
88
|
-
/** 위임 실패 — `error`는 OAuth 오류 코드(`invalid_client`·`invalid_scope`·`invalid_request` 등) */
|
|
89
|
-
export class MemberDelegationError extends Error {
|
|
90
|
-
constructor(
|
|
91
|
-
readonly error: string,
|
|
92
|
-
readonly description: string,
|
|
93
|
-
readonly status: number,
|
|
94
|
-
) {
|
|
95
|
-
super(description ? `${error}: ${description}` : error);
|
|
96
|
-
this.name = "MemberDelegationError";
|
|
97
|
-
}
|
|
98
|
-
}
|