@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/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
- * (token-exchange의 `member-uid`·`member-ref` 확장, auth-spec §12-10). 클라이언트 시크릿을 쓰므로 **서버에서만** 부른다.
4
+ * 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다.
5
+ * 계정 토큰(개인 액세스 토큰 등)을 쓰므로 **서버에서만** 부른다.
6
6
  *
7
- * 스토어에 바인딩된 confidential 클라이언트 + `member:delegate` 스코프가 필요하다. 응답은 구매자 액세스 토큰(30분)이고
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
- interface DelegationOptions {
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
- /** 셀러 자체 인증의 사용자 id로 지목한다. 처음 보는 id면 구매자를 만든다(이메일·이름은 그때만 쓴다) */
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, delegateMemberByEmail, delegateMemberByUid, parseWebhookSignatureHeader, signWebhookPayload, verifyWebhookSignature };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.23.0",
3
+ "version": "0.28.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -1,14 +1,5 @@
1
1
  import { z } from "zod";
2
- import { base64Utf8 } from "./base64";
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
- * 토큰 주입 모드 — PAT/storeToken급(테넌트 바인딩, role 없음)을 storeHttp Bearer로 싣는다.
316
- * userToken이 아니다(비인터랙티브 M2M/CLI 경로용). storeToken과 함께 주면 storeToken이 우선한다.
285
+ * 계정 토큰(accounts가 발급한 JWT — 콘솔 로그인, 개인 액세스 토큰, 연결된 앱의 토큰).
286
+ * 계정 API(`/me/*`)와 스토어 API 모두에 Bearer로 싣는다. `session`을 주면 생략해도 세션 토큰을 쓴다.
317
287
  */
318
- auth?: { apiToken?: string };
288
+ accessToken?: TokenSource;
289
+ /** 대상 스토어 id — 스토어 API 요청에 `X-Store-Id`로 싣는다. 계정 API(`/me/*`)에는 싣지 않는다 */
290
+ storeId?: TokenSource;
319
291
  /**
320
- * SDK 세션 매니저 — 전달하면 userToken(Bearer)·401 refresh·복구불가 시 clear를
321
- * 이 세션에서 자동 배선한다. 개별 옵션(userToken/refresh/onUnauthorized)을 명시하면 그쪽이 우선한다.
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이 있으면 userToken/refresh/onUnauthorized의 기본값을 세션에서 배선한다.
340
- // 개별 옵션을 명시하면 그쪽이 우선(하위호환) — session 미전달 시 기존 동작 그대로.
341
- const userTokenSource: TokenSource =
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의 현재 userToken (session 미배선 시 null — 던지지 않음) */
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
- * 양도·강등 직후의 옛 storeToken도 403이다(토큰을 재발급받는다). 비밀 값(시크릿 키·API Secret·웹훅 시크릿)은 요청으로만 보내고
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";
@@ -1,9 +1,8 @@
1
1
  /**
2
- * 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다
3
- * (token-exchange의 `member-uid`·`member-ref` 확장, auth-spec §12-10). 클라이언트 시크릿을 쓰므로 **서버에서만** 부른다.
2
+ * 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다.
3
+ * 계정 토큰(개인 액세스 토큰 등)을 쓰므로 **서버에서만** 부른다.
4
4
  *
5
- * 스토어에 바인딩된 confidential 클라이언트 + `member:delegate` 스코프가 필요하다. 응답은 구매자 액세스 토큰(30분)이고
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
- interface DelegationOptions {
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
-
23
- /** 셀러 자체 인증의 사용자 id로 지목한다. 처음 보는 id면 구매자를 만든다(이메일·이름은 그때만 쓴다) */
24
- export function delegateMemberByUid(
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
- async function delegate(
59
- options: DelegationOptions,
60
- subject: Record<string, string>,
61
- ): Promise<DelegatedMemberToken> {
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 basic = btoa(`${options.clientId}:${options.clientSecret}`);
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/x-www-form-urlencoded",
68
- authorization: `Basic ${basic}`,
45
+ "content-type": "application/json",
46
+ authorization: `Bearer ${options.accessToken}`,
47
+ "x-store-id": options.storeId,
69
48
  },
70
- body: new URLSearchParams({
71
- grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
72
- ...subject,
73
- }).toString(),
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 Record<string, unknown>;
76
- if (!res.ok || typeof body.access_token !== "string") {
77
- const error = typeof body.error === "string" ? body.error : `http_${res.status}`;
78
- const description = typeof body.error_description === "string" ? body.error_description : "";
79
- throw new MemberDelegationError(error, description, res.status);
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.access_token,
83
- expiresIn: Number(body.expires_in),
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
- }