@sayren/store-sdk 0.22.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.22.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,
@@ -264,6 +239,7 @@ import {
264
239
  setStorefrontVisibilityRequestSchema,
265
240
  storefrontBuildSchema,
266
241
  storefrontCheckSchema,
242
+ storefrontDeploymentSchema,
267
243
  storefrontEditingSchema,
268
244
  storefrontFileContentSchema,
269
245
  storefrontFileDeleteResultSchema,
@@ -291,6 +267,7 @@ import {
291
267
  type WriteStorefrontFilesRequest,
292
268
  writeStorefrontFilesRequestSchema,
293
269
  } from "./schemas/storefront-hosting";
270
+ import { publicPlansSchema, storeSubscriptionSchema } from "./schemas/subscription";
294
271
  import {
295
272
  type UploadPurpose,
296
273
  type UploadRequest,
@@ -304,20 +281,16 @@ import type { AuthorizeOptions, StoreSession } from "./session";
304
281
  export interface StoreClientOptions {
305
282
  /** 예: https://api.example.com/v1 */
306
283
  baseUrl: string;
307
- /** 유저 토큰 (1단계). 인증/스토어 목록 API에 사용 */
308
- userToken?: TokenSource;
309
- /** 스토어 토큰 (3단계). 테넌트 리소스 API에 사용 */
310
- storeToken?: TokenSource;
311
- /** 테넌트 storeId — storeHttp(테넌트 리소스) 요청에 X-Store-Id로 실린다(userToken 직접 인가). */
312
- storeId?: TokenSource;
313
284
  /**
314
- * 토큰 주입 모드 — PAT/storeToken급(테넌트 바인딩, role 없음)을 storeHttp Bearer로 싣는다.
315
- * userToken이 아니다(비인터랙티브 M2M/CLI 경로용). storeToken과 함께 주면 storeToken이 우선한다.
285
+ * 계정 토큰(accounts가 발급한 JWT — 콘솔 로그인, 개인 액세스 토큰, 연결된 앱의 토큰).
286
+ * 계정 API(`/me/*`)와 스토어 API 모두에 Bearer로 싣는다. `session`을 주면 생략해도 세션 토큰을 쓴다.
316
287
  */
317
- auth?: { apiToken?: string };
288
+ accessToken?: TokenSource;
289
+ /** 대상 스토어 id — 스토어 API 요청에 `X-Store-Id`로 싣는다. 계정 API(`/me/*`)에는 싣지 않는다 */
290
+ storeId?: TokenSource;
318
291
  /**
319
- * SDK 세션 매니저 — 전달하면 userToken(Bearer)·401 refresh·복구불가 시 clear를
320
- * 이 세션에서 자동 배선한다. 개별 옵션(userToken/refresh/onUnauthorized)을 명시하면 그쪽이 우선한다.
292
+ * SDK 세션 매니저 — 전달하면 Bearer 토큰·401 refresh·복구 불가 시 clear를
293
+ * 이 세션에서 자동 배선한다. 개별 옵션(accessToken/refresh/onUnauthorized)을 명시하면 그쪽이 우선한다.
321
294
  */
322
295
  session?: StoreSession;
323
296
  /** 401 시 세션 갱신 시도 — true면 요청을 1회 재시도한다(refresh 토큰 회전) */
@@ -327,44 +300,37 @@ export interface StoreClientOptions {
327
300
  fetch?: typeof globalThis.fetch;
328
301
  }
329
302
 
330
- function formBody(fields: Record<string, string>): string {
331
- return new URLSearchParams(fields).toString();
332
- }
333
-
334
303
  export function createStoreClient(options: StoreClientOptions) {
335
304
  const fetchFn = options.fetch ?? globalThis.fetch;
336
305
  const { session } = options;
337
306
 
338
- // session이 있으면 userToken/refresh/onUnauthorized의 기본값을 세션에서 배선한다.
339
- // 개별 옵션을 명시하면 그쪽이 우선(하위호환) — session 미전달 시 기존 동작 그대로.
340
- const userTokenSource: TokenSource =
341
- options.userToken ?? (session ? () => session.getToken() : undefined);
307
+ // session이 있으면 토큰/refresh/onUnauthorized의 기본값을 세션에서 배선한다. 개별 옵션을 명시하면 그쪽이 우선한다.
308
+ const tokenSource: TokenSource =
309
+ options.accessToken ?? (session ? () => session.getToken() : undefined);
342
310
  const refresh = options.refresh ?? (session ? () => session.refresh() : undefined);
343
311
  const onUnauthorized = options.onUnauthorized ?? (session ? () => session.clear() : undefined);
344
- // storeToken(명시 옵션)이 auth.apiToken(PAT/storeToken급 주입)보다 우선한다.
345
- const storeTokenSource: TokenSource = options.storeToken ?? options.auth?.apiToken;
346
312
 
313
+ function bearer(): Record<string, string> {
314
+ const token = resolveToken(tokenSource);
315
+ return token ? { authorization: `Bearer ${token}` } : {};
316
+ }
317
+
318
+ /** 계정 API(`/me/*`) — 스토어를 고르지 않는다 */
347
319
  const userHttp = createHttp({
348
320
  baseUrl: options.baseUrl,
349
321
  fetch: fetchFn,
350
- headers: () => {
351
- const token = resolveToken(userTokenSource);
352
- return token ? { authorization: `Bearer ${token}` } : ({} as Record<string, string>);
353
- },
322
+ headers: bearer,
354
323
  refresh,
355
324
  onUnauthorized,
356
325
  });
357
326
 
327
+ /** 스토어 API — 같은 계정 토큰 + `X-Store-Id` */
358
328
  const storeHttp = createHttp({
359
329
  baseUrl: options.baseUrl,
360
330
  fetch: fetchFn,
361
331
  headers: () => {
362
- const token = resolveToken(storeTokenSource);
363
332
  const storeId = resolveToken(options.storeId);
364
- return {
365
- ...(token ? { authorization: `Bearer ${token}` } : {}),
366
- ...(storeId ? { "x-store-id": storeId } : {}),
367
- };
333
+ return { ...bearer(), ...(storeId ? { "x-store-id": storeId } : {}) };
368
334
  },
369
335
  refresh,
370
336
  onUnauthorized,
@@ -390,7 +356,7 @@ export function createStoreClient(options: StoreClientOptions) {
390
356
  }
391
357
 
392
358
  return {
393
- /** session의 현재 userToken (session 미배선 시 null — 던지지 않음) */
359
+ /** session의 현재 액세스 토큰 (session 미배선 시 null — 던지지 않음) */
394
360
  get accessToken(): string | null {
395
361
  return session ? session.accessToken : null;
396
362
  },
@@ -404,90 +370,11 @@ export function createStoreClient(options: StoreClientOptions) {
404
370
  userinfo: () => requireSession().userinfo(),
405
371
 
406
372
  auth: {
407
- /**
408
- * 서버 간 연동(M2M) — client_credentials grant (client_secret_basic).
409
- * 클라이언트가 스토어에 바인딩되어 있어 **storeToken이 곧바로 발급**된다.
410
- * ⚠ client_secret은 서버 환경에서만 사용할 것 (브라우저 반입 금지)
411
- */
412
- async issueStoreTokenByClientCredentials(
413
- clientId: string,
414
- clientSecret: string,
415
- scope?: string,
416
- ) {
417
- // RFC 7617 client_secret_basic — UTF-8 바이트를 base64로 인코딩한다.
418
- const credentials = base64Utf8(`${clientId}:${clientSecret}`);
419
- const response = await fetchFn(`${options.baseUrl}/oauth2/token`, {
420
- method: "POST",
421
- headers: {
422
- "content-type": "application/x-www-form-urlencoded",
423
- authorization: `Basic ${credentials}`,
424
- },
425
- body: formBody({ grant_type: "client_credentials", ...(scope ? { scope } : {}) }),
426
- });
427
- if (!response.ok) {
428
- const body = (await response.json().catch(() => null)) as {
429
- error_description?: string;
430
- } | null;
431
- throw new Error(body?.error_description ?? `토큰 발급 실패 (HTTP ${response.status})`);
432
- }
433
- return clientCredentialsTokenResponseSchema.parse(unwrapData(await response.json()));
434
- },
435
- /** 2단계 — 소속 스토어 목록 */
373
+ /** 소속 스토어 목록 */
436
374
  listMyStores: () => userHttp.request("GET", "/me/stores", z.array(myStoreSchema)),
437
- /** RFC 7009 — token을 폐기한다 (public client는 client_id만 동반) */
438
- async revoke(token: string, clientId: string) {
439
- await fetchFn(`${options.baseUrl}/oauth2/revoke`, {
440
- method: "POST",
441
- headers: { "content-type": "application/x-www-form-urlencoded" },
442
- body: formBody({ token, client_id: clientId }),
443
- });
444
- },
445
375
  me: () => userHttp.request("GET", "/me", myAccountSchema),
446
376
  },
447
377
 
448
- /** API 토큰(PAT) 관리 — OWNER/ADMIN 전용. 발급된 토큰은 storeToken처럼 Bearer로 사용 */
449
- apiTokens: {
450
- create: (body: ApiTokenCreateRequest) =>
451
- storeHttp.request("POST", "/store/api-tokens", apiTokenCreatedSchema, {
452
- body: apiTokenCreateRequestSchema.parse(body),
453
- }),
454
- list: () => storeHttp.request("GET", "/store/api-tokens", z.array(apiTokenSchema)),
455
- revoke: (tokenId: string) =>
456
- storeHttp.requestVoid("DELETE", path`/store/api-tokens/${tokenId}`),
457
- },
458
-
459
- /**
460
- * 앱(OAuth 클라이언트) 관리 — OWNER/ADMIN storeToken 전용(PAT·M2M 거부). auth-spec §12, 부록 B-13.
461
- * 시크릿 평문은 register·rotateSecret 응답에서만 한 번 노출된다(apiTokens.create와 같은 성격).
462
- */
463
- apiClients: {
464
- /**
465
- * DCR 등록 (RFC 7591) — `POST /v1/oauth2/register`. OAuth 프로토콜 표면이라 응답이 평면 JSON이다.
466
- * 등록 오류(`invalid_client_metadata`·`invalid_redirect_uri`)는 ApiError(code=error,
467
- * message=error_description)로, Bearer 인증 오류는 엔벨로프 그대로 ApiError로 던진다.
468
- */
469
- register: (body: OAuthClientRegistrationRequest) =>
470
- storeHttp.request("POST", "/oauth2/register", oauthClientRegistrationResponseSchema, {
471
- body: oauthClientRegistrationRequestSchema.parse(body),
472
- }),
473
- list: (params?: ApiClientListParams) =>
474
- storeHttp.request("GET", "/store/api-clients", z.array(apiClientSchema), {
475
- query: params as Query,
476
- }),
477
- get: (clientId: string) =>
478
- storeHttp.request("GET", path`/store/api-clients/${clientId}`, apiClientSchema),
479
- /** confidential 전용. 기존 시크릿은 즉시 무효(유예 없음) */
480
- rotateSecret: (clientId: string) =>
481
- storeHttp.request(
482
- "POST",
483
- path`/store/api-clients/${clientId}/rotate-secret`,
484
- apiClientSecretRotatedSchema,
485
- ),
486
- /** REVOKED로 전환(되돌릴 수 없음). 이미 비활성이면 현재 상태를 그대로 돌려준다 */
487
- deactivate: (clientId: string) =>
488
- storeHttp.request("POST", path`/store/api-clients/${clientId}/deactivate`, apiClientSchema),
489
- },
490
-
491
378
  /**
492
379
  * 미디어 업로드 — 자리를 받아(`POST /uploads`) 파일 바이트는 호출자가
493
380
  * `uploadUrl`로 직접 PUT한다. 업로드가 끝나면 `url`이 공개 주소가 된다.
@@ -1070,7 +957,7 @@ export function createStoreClient(options: StoreClientOptions) {
1070
957
  /**
1071
958
  * 결제(PG) 설정 — **콘솔 OWNER 전용**. ADMIN·STAFF·PAT·M2M·3rd-party 앱 토큰은 스코프가 있어도
1072
959
  * 403 `INSUFFICIENT_ROLE`이다(조회 포함). OWNER 여부는 요청 시점의 스토어 멤버십으로 다시 확인하므로
1073
- * 양도·강등 직후의 옛 storeToken도 403이다(토큰을 재발급받는다). 비밀 값(시크릿 키·API Secret·웹훅 시크릿)은 요청으로만 보내고
960
+ * 양도·강등 직후에도 바로 403이다. 비밀 값(시크릿 키·API Secret·웹훅 시크릿)은 요청으로만 보내고
1074
961
  * 응답에는 `secrets[].masked`(앞 4자리 마스킹)만 나온다.
1075
962
  */
1076
963
  paymentSettings: {
@@ -1358,12 +1245,27 @@ export function createStoreClient(options: StoreClientOptions) {
1358
1245
  query: { slug },
1359
1246
  },
1360
1247
  ),
1361
- /** `POST /v1/storefront/site` (201) — 템플릿으로 사이트를 만들고 v1을 발행한다. 공개 상태는 오픈 준비 중이다 */
1248
+ /**
1249
+ * `POST /v1/storefront/site` (201) — 템플릿으로 사이트를 만들고 v1을 발행한다. 공개 상태는 오픈 준비 중이다.
1250
+ * 사이트를 만드는 유일한 경로다(스토어를 만들어도 사이트는 생기지 않는다). 템플릿은 여기서만 고른다
1251
+ */
1362
1252
  create: (body: CreateStorefrontSiteRequest) =>
1363
1253
  storeHttp.request("POST", "/storefront/site", storefrontSiteSchema, {
1364
1254
  body: createStorefrontSiteRequestSchema.parse(body),
1365
1255
  }),
1366
- /** `POST /v1/storefront/site/template` — 템플릿을 바꾼다. 새 버전을 만들고 기본으로 곧바로 발행한다 */
1256
+ /** `GET /v1/storefront/site/deployment` — 배포 진행(단계·기록). 만든 뒤 반영이 끝날 때까지 폴링한다 */
1257
+ getDeployment: () =>
1258
+ storeHttp.request("GET", "/storefront/site/deployment", storefrontDeploymentSchema),
1259
+ /** `POST /v1/storefront/site/deployment/retry` (202) — 밀린 반영을 곧바로 다시 시도한다. 이미 반영됐으면 그대로다 */
1260
+ retryDeployment: () =>
1261
+ storeHttp.request("POST", "/storefront/site/deployment/retry", storefrontDeploymentSchema, {
1262
+ body: {},
1263
+ }),
1264
+ /**
1265
+ * `POST /v1/storefront/site/template`
1266
+ * @deprecated 템플릿 변경은 지원하지 않는다. 서버가 항상 409 `TEMPLATE_CHANGE_NOT_SUPPORTED`로 답한다 — 템플릿은
1267
+ * 사이트를 만들 때(`create`)만 고른다. 호환을 위해 메서드만 남겼다
1268
+ */
1367
1269
  changeTemplate: (body: ChangeStorefrontTemplateRequest) =>
1368
1270
  storeHttp.request("POST", "/storefront/site/template", storefrontSiteSchema, {
1369
1271
  body: changeStorefrontTemplateRequestSchema.parse(body),
@@ -1567,6 +1469,22 @@ export function createStoreClient(options: StoreClientOptions) {
1567
1469
  }),
1568
1470
  },
1569
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
+
1570
1488
  store: {
1571
1489
  get: () => storeHttp.request("GET", "/store", storeSchema),
1572
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
- }