Repository navigation
TypeScript SDK
React Native, Web, Node용 @seorilabs/platform-sdk. 현재 권장 버전은 0.4.0입니다.
- source:
packages/sdk-ts - 버전 정본:
package.json과src/version.ts의SDK_VERSION - 요구: Node 20 이상,
fetch전역. 의존성 없음
GitHub Release tarball — 인증 없이 됩니다.
pnpm add https://github.com/seorilabs/platform/releases/download/v0.6.7/seorilabs-platform-sdk-0.4.0.tgz무결성을 고정하려면 platform-release.json의 sha256과 대조합니다
(0.4.0: 8d7151f6ec7ff0c0fe8c9fe6fee2e6aa5a1158bd394dc03cc09a4e40f5a6abd7).
GitHub Packages — read:packages 토큰이 필요합니다. .npmrc에는 registry만 둡니다.
@seorilabs:registry=https://npm.pkg.github.compnpm add @seorilabs/platform-sdk@0.4.0NODE_AUTH_TOKEN 값은 파일, 로그, 앱 번들에 넣지 않습니다.
import { createPlatform } from "@seorilabs/platform-sdk";
export const platform = createPlatform({
baseUrl: PLATFORM_API_URL,
iapBaseUrl: PLATFORM_IAP_URL,
ingestBaseUrl: PLATFORM_INGEST_URL,
adsBaseUrl: PLATFORM_ADS_URL,
appId: "my-app",
eventContext: () => ({
platform: "android",
appVersion: "1.2.3",
locale: "ko-KR",
}),
sessionStore: new MySessionStore(),
presenceEnabled: false,
});
platform.start();| 옵션 | 기본 | 의미 |
|---|---|---|
baseUrl |
필수 |
api 역할 URL |
appId |
필수 | 레지스트리의 app_id
|
iapBaseUrl ingestBaseUrl adsBaseUrl
|
baseUrl |
역할별 URL. production은 전부 지정. adsBaseUrl을 주면 세션 발급도 그리로 간다 — AppsInToss mTLS 자격증명이 ads/iap 역할에 격리돼 있기 때문 |
eventContext |
{} |
배치 공통 환경. 객체 또는 함수. 함수면 flush 시점에 평가 |
eventAllowlist |
전부 허용 | 플랫폼으로 보낼 이벤트 이름. 목록 밖은 조용히 버림 |
eventFlushIntervalMs |
10000 |
자동 전송 주기. 0이면 수동 flush만 |
sessionStore |
메모리 | 세션 영속화. 앱 재시작 후 로그인 유지 |
eventOutbox |
메모리 500건 | 실패 배치 보관소 |
configTtlMs |
SDK 기본 | Remote Config 캐시 수명 |
presenceEnabled |
false |
RPI edge heartbeat. 명시적으로 켠 앱만 |
presenceContext |
eventContext의 platform·appVersion
|
heartbeat에 실을 환경 |
maxRetries timeoutMs
|
SDK 기본 | 전송 계층 재시도·제한 시간 |
appCheckToken |
없음 | Firebase App Check 토큰 공급자. require_app_check 앱에 필수 |
fetchImpl now random sleep
|
전역 | 테스트 주입점 |
Platform 인스턴스는 session events iap config ads identity
content presence 모듈을 갖습니다. 전부 같은 전송 계층과 세션을 공유합니다.
type Credential = {
kind: "firebase-id-token" | "ait-login" | "anonymous";
value: string;
referrer?: "DEFAULT" | "SANDBOX"; // ait-login에서 appLogin이 준 값
};| kind | value | 결제 |
|---|---|---|
firebase-id-token |
앱 Firebase Auth의 ID 토큰 | 가능 |
ait-login |
AppsInToss appLogin()의 authorization code |
가능 |
anonymous |
getAnonymousKey() 해시 |
불가 — bearer가 아니라 사칭 가능 |
const session = await platform.signIn({ kind: "firebase-id-token", value: idToken });
session.platformUserId; // pu_...
session.supportCode; // CS 화면에 보여줄 코드
session.isAnonymous;
session.isLinkedAccount; // Kakao·Apple 연결 또는 AppsInToss 세션
session.expiresAt; // epoch ms세션은 만료 전에 SDK가 refresh 토큰으로 갱신합니다. 앱은 platform.session.token()을
직접 부를 일이 거의 없습니다 — 각 모듈이 알아서 붙입니다.
sequenceDiagram
autonumber
participant App as 앱
participant FB as Firebase Auth
participant SDK as Platform SDK
participant API as platform-api
participant FS as Firestore
App->>FB: 로그인
FB-->>App: ID 토큰
App->>SDK: signIn kind firebase-id-token
SDK->>API: POST /v1/auth/session — X-Seori-App 헤더
API->>API: 레지스트리에서 앱 조회 · paused면 403
API->>API: ID 토큰 서명 · aud · iss · exp 검증
API->>FS: 차단 목록 확인
API->>FS: app_id + uid → platform_user_id 매핑 · 없으면 생성
API-->>SDK: platformToken · refreshToken · supportCode · config
SDK->>SDK: SessionStore에 저장
SDK-->>App: Session
세션 갱신은 SDK가 알아서 합니다. 동시에 여러 호출이 만료를 만나도 refresh는 한 번만 나갑니다 — 서버가 refresh 토큰을 회전시키는 순간 나머지가 무효 토큰을 들고 실패하기 때문입니다.
sequenceDiagram
participant App as 앱
participant SDK as Platform SDK
participant API as platform-api
App->>SDK: 인증이 필요한 호출
SDK->>SDK: expiresAt까지 60초 미만인가
alt 아직 유효
SDK-->>App: 저장된 platformToken으로 진행
else 만료 임박 — 동시 호출도 refresh는 한 번
SDK->>API: POST /v1/auth/refresh — refreshToken
alt 200
API-->>SDK: 새 세션
SDK->>SDK: 저장 후 진행
else 401 또는 403
SDK->>API: 보관한 자격증명으로 POST /v1/auth/session 재로그인
Note over SDK: 계정 연결 뒤에는 자격증명을 버리므로<br/>재로그인하지 않고 오류를 던진다
end
end
플랫폼이 앱 Firebase 프로젝트용 custom token을 발급합니다. 레지스트리에
features.firebase_custom_token_bridge가 켜져 있어야 합니다.
// 1. 플랫폼에서 custom token을 받는다 (세션 불필요)
const { firebaseCustomToken, appUserId } = await platform.identity.firebaseCustomToken();
// 2. Firebase에 로그인한다
await auth().signInWithCustomToken(firebaseCustomToken);
// 3. ID 토큰으로 플랫폼 세션을 연다
const idToken = await auth().currentUser!.getIdToken();
await platform.signIn({ kind: "firebase-id-token", value: idToken });sequenceDiagram
autonumber
participant App as 앱
participant SDK as Platform SDK
participant API as platform-api
participant IAM as GCP IAM signJwt
participant FB as Firebase Auth
App->>SDK: identity.firebaseCustomToken
SDK->>API: POST /v1/auth/firebase-custom-token — App Check 헤더
API->>API: uid 생성 · 기존 ID 토큰이 있으면 그 uid 보존
API->>IAM: 앱의 platform-auth 서비스 계정으로 서명
IAM-->>API: custom token
API-->>SDK: firebaseCustomToken · appUserId
SDK-->>App: 결과 — 저장하지 않는다
App->>FB: signInWithCustomToken
FB-->>App: ID 토큰
App->>SDK: signIn kind firebase-id-token
Note over SDK,API: 이후는 일반 세션 발급 흐름
이미 Firebase 계정이 있는 사용자를 이전할 때는 firebaseCustomToken(existingIdToken)으로
기존 uid를 보존합니다. 신규 발급 요청은 자동 재시도하지 않습니다 — 응답이 유실된
뒤 다시 부르면 다른 uid가 생길 수 있습니다. custom token은 한 번 쓰고 저장하지 않습니다.
Firebase ID 토큰이 없습니다. appLogin()이 준 authorization code를 ait-login으로
넘기면 서버가 Toss Login API와 mTLS로 교환합니다. 이 자격증명은 iap·ads 역할에만
있으므로 adsBaseUrl을 지정해 세션 발급을 그쪽으로 보냅니다.
sequenceDiagram
autonumber
participant App as 앱 — AppsInToss
participant AIT as AppsInToss SDK
participant SDK as Platform SDK
participant IAP as platform-iap 또는 platform-ads
participant TOSS as Toss Login API
App->>AIT: appLogin
AIT-->>App: authorization code · referrer
App->>SDK: signIn kind ait-login · referrer
SDK->>IAP: POST /v1/auth/session
IAP->>TOSS: mTLS로 code 교환
TOSS-->>IAP: 사용자 식별자
IAP->>IAP: 앱 범위 platform_user 매핑
IAP-->>SDK: 세션 — isLinkedAccount true
SDK-->>App: Session
Note over SDK,IAP: mTLS 자격증명이 iap · ads 역할에만 있어<br/>세션 발급 host를 그쪽으로 지정한다
유료 접근(iap.require_linked_account)이 켜진 앱은 연결 세션에서만 결제할 수 있습니다.
const challenge = await platform.identity.beginAccountLink("kakao");
// 로그인 SDK에 challenge.nonce를 넣어 OIDC ID 토큰을 받는다
const result = await platform.identity.completeAccountLink("kakao", oidcIdToken, challenge.nonce);
result.session; // 교체된 세션. SDK가 이미 adopt했다
result.restored; // 기존 계정으로 복원됐는지
result.firebaseCustomToken; // 앱 Firebase도 같은 계정으로 맞출 때completeAccountLink는 자동 재시도하지 않습니다. 레지스트리의
auth.account_providers에 provider audience가 등록돼 있어야 합니다.
sequenceDiagram
autonumber
participant App as 앱
participant SDK as Platform SDK
participant API as platform-api
participant PV as Kakao 또는 Apple 로그인 SDK
App->>SDK: identity.beginAccountLink provider
SDK->>API: POST /v1/auth/account-link-challenges — 현재 세션
API-->>SDK: nonce · expiresAt — 5분
SDK-->>App: challenge
App->>PV: nonce를 넣어 로그인
PV-->>App: OIDC ID 토큰
App->>SDK: identity.completeAccountLink provider · idToken · nonce
SDK->>API: POST /v1/auth/account-links — noRetry
API->>API: iss · aud · exp · 서명 · nonce 검증
API->>API: provider subject를 SHA-256으로 앱 범위 매핑 · 원문 미저장
alt 새 게스트가 기존 공급자 계정을 증명
API-->>SDK: 기존 platform 사용자로 전환 — restored true
else 처음 연결
API-->>SDK: 현재 사용자에 연결
else 서로 다른 두 연결 계정
API-->>SDK: 409 account_link_conflict
end
SDK->>SDK: 새 세션 adopt · 보관 자격증명 폐기
SDK-->>App: session · firebaseCustomToken · restored
레지스트리에 require_app_check가 켜진 앱은 custom token 발급과 계정 삭제 경계에서
X-Firebase-AppCheck를 요구합니다.
createPlatform({
...,
appCheckToken: () => appCheck().getToken().then(r => r.token),
});platform.events.track({ name: "level_complete", params: { level: 3, is_new: true } });- 던지지 않습니다. 계측 때문에 제품 흐름이 멈추면 안 됩니다
- 즉시 보내지 않습니다. 20건이 쌓이거나 주기가 되면 배치로 나갑니다
- 실패 배치는 outbox에 남아 다음 주기에 다시 시도합니다
- 세션이 없어도 익명으로 수집됩니다
이름은 spec/events.md의
3층 구조를 따릅니다. seori_*는 SDK 예약입니다.
sequenceDiagram
participant App as 앱
participant SDK as Platform SDK — Events
participant OB as Outbox
participant ING as platform-ingest
App->>SDK: events.track name · params
SDK->>SDK: allowlist 확인 · 파라미터 정규화 · eventId 부여
SDK->>SDK: 버퍼에 추가 — 던지지 않는다
alt 20건 도달 · 10초 주기 · flush 호출
SDK->>OB: 이전 실패분 drain
SDK->>ING: POST /v1/events — 최대 20건 · 세션 토큰은 있으면
alt 성공
ING-->>SDK: ok
else 실패
SDK->>OB: 배치 보관 — 최대 500건
Note over OB: 다음 flush에 다시 시도
end
end
두 SDK가 spec/conformance/param-normalization.json을
동일하게 통과합니다.
| 입력 | 출력 |
|---|---|
true / false
|
1 / 0
|
NaN, Infinity
|
0 |
null, undefined
|
파라미터 자체를 버림 |
| 객체, 배열 | 버림 — 문자열로 바꾸지 않음 |
| 100자 초과 문자열 | 잘림 |
PII 키(email, phone 등, PII_KEYS) |
버림 |
| 파라미터 수 |
MAX_PARAMS 초과분 버림 |
platform.start(); // 자동 flush 시작
await platform.events.flush(); // 백그라운드 진입 시
await platform.shutdown(); // 종료 — 남은 것을 보내고 타이머 정지React Native:
AppState.addEventListener("change", (state) => {
if (state === "active") { platform.start(); platform.presence.resume(); return; }
platform.presence.stop();
void platform.events.flush();
});Web은 SDK가 document.visibilityState를 감지합니다.
const outcome = await platform.iap.verifyPurchase({
platform: "google_play", // "google_play" | "app_store" | "apps_in_toss"
productId: "gecko_galaxy",
token: purchase.purchaseToken,
});
outcome.status; // "verified" | "pending" | "revoked"
outcome.entitlements; // 현재 보유 전체
outcome.granted; // 이번 호출로 지급됐는지. alreadyGranted와 배타
outcome.completion; // { action, orderId }| 마켓 | token |
|---|---|
| Google Play | purchaseToken |
| App Store | transactionId |
| AppsInToss | orderId |
completion.action이 none이 아니면 앱이 마켓 쪽 마무리를 해야 합니다.
| action | 할 일 |
|---|---|
apps_in_toss_complete_product_grant |
AppsInToss SDK로 orderId의 지급 완료를 호출 |
retry_server_completion |
서버 완료 처리 대기 중. 잠시 후 verifyPurchase 재호출 |
app_store_sync_after_sandbox_reset |
sandbox 초기화 후 StoreKit 동기화 |
자동 재시도하지 않습니다. 호출 전에 session.token()이 만료 임박 세션을 선제
갱신하고, 응답이 실패하면 그대로 던집니다 — 서버가 멱등이어도 응답을 기다리는 사이
사용자에게는 두 번 결제한 것처럼 보입니다. 재시도는 사용자가 정합니다.
구매 전 계정 참조부터 완료 처리까지의 전체 흐름입니다.
sequenceDiagram
autonumber
participant App as 앱
participant SDK as Platform SDK
participant MK as 마켓 SDK
participant IAP as platform-iap
participant MAPI as 마켓 API
participant FS as 원장 Firestore
App->>SDK: iap.accountReferences
SDK->>IAP: GET /v1/iap/account-references
IAP-->>SDK: HMAC 계정 참조
App->>MK: 구매 요청 — obfuscatedAccountId · appAccountToken
MK-->>App: purchaseToken · transactionId · orderId
App->>SDK: iap.verifyPurchase platform · productId · token
SDK->>IAP: POST /v1/iap/verify — noRetry
IAP->>IAP: 마켓 검증기 선택 · SKU를 entitlement로 변환
IAP->>MAPI: 구매 확인
MAPI-->>IAP: 거래 상태
IAP->>IAP: 계정 바인딩 검증 — 불일치는 신호로 기록하고 지급은 막지 않는다
IAP->>FS: 트랜잭션 — 지급 · 보류 · 회수
IAP->>MAPI: 완료 처리 — Play consume 등 · 실패해도 지급 유지
IAP-->>SDK: status · entitlements · granted 또는 alreadyGranted · completion
SDK-->>App: VerifyOutcome
opt completion.action이 none이 아니면
App->>MK: 마켓 쪽 마무리 — AppsInToss 지급 완료 등
end
다른 사용자가 시작한 구매를 가로채지 못하게 마켓 결제 화면에 계정 참조를 넣습니다.
const refs = await platform.iap.accountReferences();
refs.googlePlayObfuscatedAccountId; // Play obfuscatedAccountId
refs.appStoreAppAccountToken; // StoreKit appAccountTokenconst owned = await platform.iap.listEntitlements(); // string[]환불은 마켓 웹훅으로 서버에 먼저 도착합니다. 앱은 다음 시작 때 listEntitlements()로
서버가 아는 최종 상태를 받아 반영합니다.
sequenceDiagram
autonumber
participant MAPI as App Store 또는 Google Play
participant IAP as platform-iap
participant FS as 원장 Firestore
participant App as 앱
participant SDK as Platform SDK
MAPI->>IAP: 환불 알림 — Apple JWS 또는 Play RTDN
IAP->>IAP: 서명 · OIDC 검증
IAP->>FS: 기존 소유자의 entitlement 회수 — 알림만으로 신규 지급은 없다
Note over App,SDK: 다음 앱 시작
App->>SDK: iap.listEntitlements
SDK->>IAP: GET /v1/iap/entitlements
IAP-->>SDK: 서버가 아는 최종 상태
SDK-->>App: entitlements — 환불분 제외
const config = await platform.config.fetch({
appVersion: "1.2.3",
platform: "android", // "android" | "ios" | "web"
locale: "ko-KR",
});
config.values; // Record<string, unknown>
config.features; // Record<string, boolean>
config.sdk.status; // "ok" | "deprecated" | "blocked"
config.sdk.updateUrl;
config.maintenance.active; // 점검 모드
config.maintenance.until;
config.minSupportedVersion;실패하면 마지막 캐시, 그것도 없으면 열린 기본값을 줍니다. 차단은 서버가
blocked나 maintenance.active를 명시할 때만 합니다. 네트워크 없이 마지막 값을
보려면 platform.config.current().
flowchart TD
A["config 조회"] --> B{"캐시가 TTL 안인가"}
B -->|예| C["캐시 반환"]
B -->|아니오| D["GET /v1/config — appVersion · platform · locale"]
D --> E{"성공"}
E -->|예| F["캐시 갱신 후 반환"]
E -->|아니오| G{"이전 캐시가 있는가"}
G -->|예| H["마지막 캐시 반환"]
G -->|아니오| I["열린 기본값 반환 — 차단 없음"]
F --> J{"sdk.status blocked<br/>또는 maintenance.active"}
H --> J
I --> J
J -->|예| K["앱이 차단 · 점검 화면 표시"]
J -->|아니오| L["정상 진행"]
레지스트리에 features.ads와 placement가 등록돼 있어야 합니다.
const policy = await platform.ads.policy();
if (!policy.adsEnabled) return; // disabledBy: "operator" | "ad_free"
const claim = await platform.ads.createClaim({
requestId: localId, // 앱이 만든 멱등 키
placement: "hint_reward",
provider: "admob", // "admob" | "apps_in_toss"
clientPlatform: "android",
reward: { key: "hint", amount: 3 },
});
claim.admobSsv; // { customData, userId } → 네이티브 SDK의 SSV 옵션에 그대로| 상태 | 뜻 |
|---|---|
accepted |
claim 생성. 광고 노출 전 |
confirmed |
보상 확인됨 (assurance: server_verified = SSV, client_confirmed = 클라이언트 보고) |
delivered |
앱이 ack — 로컬 지급 완료 |
expired |
만료 |
await platform.ads.confirm(claim.claimId, transactionId); // SSV가 없는 provider에서 클라이언트 확인
const latest = await platform.ads.claim(claim.claimId); // 상태 조회
await platform.ads.ack(claim.claimId); // 로컬 exactly-once 지급 후정책 조회 실패를 광고 허용으로 바꾸지 않습니다. 상태를 바꾸는 호출은 자동 반복하지 않습니다.
stateDiagram-v2
[*] --> accepted: claim 생성
accepted --> confirmed: AdMob SSV 콜백 — server_verified
accepted --> confirmed: confirm — client_confirmed · AppsInToss만
accepted --> expired: expiresAt 경과
confirmed --> delivered: ack — 로컬 지급 완료 후
delivered --> [*]
expired --> [*]
AdMob이 시청 완료를 서버에 직접 알립니다. 앱은 claim 상태를 조회해 server_verified를
확인한 뒤에만 로컬 지급합니다.
sequenceDiagram
autonumber
participant App as 앱
participant SDK as Platform SDK
participant ADS as platform-ads
participant GMA as AdMob SDK
participant SSV as AdMob SSV
App->>SDK: ads.policy
SDK->>ADS: GET /v1/ads/policy
ADS-->>SDK: adsEnabled · disabledBy
alt adsEnabled false 또는 조회 실패
SDK-->>App: 광고 경로 중단 — 실패를 허용으로 바꾸지 않는다
end
App->>SDK: ads.createClaim requestId · placement · reward
SDK->>ADS: POST /v1/ads/reward-claims
ADS->>ADS: placement · reward 범위 · 일일 한도 · cooldown 확인
ADS-->>SDK: claim — state accepted · admobSsv customData · userId
App->>GMA: 보상형 광고 표시 — SSV 옵션에 customData · userId
GMA->>SSV: 시청 완료
SSV->>ADS: GET /v1/ads/admob/ssv/appId — 서명 · key_id · custom_data · user_id
ADS->>ADS: AdMob 공개키로 서명 검증 · claim과 사용자 결합 확인
ADS->>ADS: 한도 원자적 재판정 → confirmed · server_verified
App->>SDK: ads.claim claimId
SDK->>ADS: GET /v1/ads/reward-claims/claimId
ADS-->>SDK: confirmed · server_verified
App->>App: 로컬 exactly-once 보상 지급
App->>SDK: ads.ack claimId
SDK->>ADS: POST /v1/ads/reward-claims/claimId/ack
ADS-->>SDK: state delivered
AppsInToss 광고는 SSV가 없어 앱이 confirm으로 transactionId를 보고합니다. 다른
provider에서 confirm을 부르면 ad_claim_assurance_invalid입니다.
sequenceDiagram
autonumber
participant App as 앱 — AppsInToss
participant SDK as Platform SDK
participant ADS as platform-ads
participant AIT as AppsInToss 광고 SDK
App->>SDK: ads.createClaim provider apps_in_toss
SDK->>ADS: POST /v1/ads/reward-claims
ADS-->>SDK: claim — state accepted
App->>AIT: 보상형 광고 표시
AIT-->>App: 보상 완료 · transactionId
App->>SDK: ads.confirm claimId · transactionId
SDK->>ADS: POST /v1/ads/reward-claims/claimId/confirm
ADS->>ADS: provider가 apps_in_toss인지 확인 · transactionId 해시 · 한도 재판정
ADS-->>SDK: confirmed · client_confirmed
App->>App: 로컬 exactly-once 보상 지급
App->>SDK: ads.ack claimId
SDK->>ADS: POST /v1/ads/reward-claims/claimId/ack
ADS-->>SDK: delivered
최근 활성 세션 heartbeat입니다. presenceEnabled: true이고 레지스트리에
features.presence가 켜진 앱만 동작합니다. fail-open — 실패를 던지지도, 저장해서
재전송하지도 않습니다. 앱 시작·로그인·결제를 기다리게 하면 안 됩니다.
platform.presence.start(); // platform.start()가 함께 부른다
platform.presence.stop();
platform.presence.resume(); // foreground 복귀heartbeat에는 익명 세션 ID, sequence, platform, appVersion만 실립니다.
sequenceDiagram
participant SDK as Platform SDK — Presence
participant ING as platform-ingest
participant EDGE as presence edge — RPI
SDK->>ING: POST /v1/presence/token — 익명 세션 ID
alt enabled false 또는 발급기 미준비
ING-->>SDK: enabled false — 5분 뒤 다시 bootstrap
else
ING-->>SDK: 단기 토큰 · edgeUrl
loop 60초 주기 · 실패 시 최대 5분 backoff
SDK->>EDGE: POST /v1/presence/heartbeat — sequence · platform · appVersion
EDGE-->>SDK: 200 또는 Retry-After
end
end
Note over SDK: 실패를 던지지도 outbox에 넣지도 않는다
platform.content.version() / resolve() / term()은 운글(ungeul) 전용
private 콘텐츠 계약(schemaVersion: 1)입니다. 일반 앱은 쓰지 않습니다.
모든 실패는 PlatformError로 던집니다.
import { PlatformError } from "@seorilabs/platform-sdk";
try { ... } catch (err) {
if (err instanceof PlatformError) {
err.code; // "session_expired" — 분기는 이 값으로만
err.status; // HTTP 상태
err.local; // true면 서버에 닿지 못했다 (network_error 등)
}
}전체 목록과 권장 대응은 Error Codes. SDK가 로컬에서 만드는 코드는
network_error, auth_required, purchase_proof_invalid, request_invalid,
iap_response_invalid, internal입니다.
import type { Session, SessionStore } from "@seorilabs/platform-sdk";
class KeychainSessionStore implements SessionStore {
async load(): Promise<Session | null> { ... }
async save(session: Session): Promise<void> { ... }
async clear(): Promise<void> { ... }
}refreshToken이 들어갑니다. 탈취되면 세션을 계속 갱신할 수 있으므로 Keychain·Keystore를
권장합니다. AsyncStorage 예시는
레퍼런스에 있습니다.
구현과 API 계약의 정본은 seorilabs/platform 저장소입니다.