@ait-kit/api-core

中文文档
0.6.0发布于 2026/10/14MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 3628

Runtime-neutral typed Apps in Toss server API adapters and mTLS transport ports.

原始使用文档
文档目录9 节
  1. IAP order verification contract
  2. Message recipients
  3. Smart message send results
  4. Anonymous keys and verification
  5. Recoverable promotion grants
  6. Outcome semantics
  7. Consumer responsibilities
  8. Recipient privacy and numeric IDs
  9. Anonymous user authentication (SDK 3.6+)

@ait-kit/api-core

Runtime-neutral typed adapters for Apps in Toss server APIs and mTLS transports.

npm install @ait-kit/api-core
import { createAppsInTossApiRpcFromOptions } from "@ait-kit/api-core";

const tossApi = createAppsInTossApiRpcFromOptions({
  mode: "forward",
  mtlsClient
});

IAP order verification contract

iapOrderStatus separates what the provider attested from what the caller asked. Grant decisions must gate on verified, never on ok:

  • ok: true — the get-order-status query succeeded and the provider returned a well-formed payload. A payload missing the spec-required orderId or status fails with error: "INVALID_RESPONSE" instead of being patched up from the request. Evidence fields must be real strings: an orderId, status, or present optional field (sku, statusDeterminedAt, reason) arriving as an array, object, number, or boolean is INVALID_RESPONSE too — coerced values are never used as verification evidence. Failure envelopes (FAIL, ERROR, NETWORK_ERROR, TIMEOUT) yield no evidence even when a contradictory success payload rides along, and unrecognized resultType values are INVALID_RESPONSE, never optimistic successes.
  • verified: true — the provider-attested status is PAYMENT_COMPLETED or PURCHASED and the provider-returned orderId matches the request. Response orderId/sku come exclusively from the provider payload; request values are expectations, never evidence.
  • verificationCode — required on every unverified success: PAYMENT_INCOMPLETE (retryable, the pending re-query flow is preserved), PAYMENT_FAILED, PAYMENT_REFUNDED, MINIAPP_MISMATCH, ORDER_NOT_FOUND (retryable), ORDER_ID_MISMATCH, UNKNOWN_STATUS, PROVIDER_STATUS_ERROR, or STUB_EVIDENCE.
  • skuCheck — present when the caller passes an expected sku: MATCHED, MISMATCHED, or NOT_PROVIDED. The provider sku is optional in the official API, so a missing SKU never flips verified; there is no supplementary lookup endpoint, so incomplete evidence on a payable status can only be re-queried through this same call.
  • stub: true — marks synthetic stub-mode output. Stub responses fabricate a PAYMENT_COMPLETED order for development but are never verified (verificationCode: "STUB_EVIDENCE"), so a deployment that accidentally runs in stub mode cannot grant purchases on fabricated data. Development flows that want to exercise grant logic must explicitly opt in by checking stub: true.

Grant gating example — verified alone confirms a payable order for the requested order ID; when your catalog expects a specific product, also require the SKU comparison before crediting:

const result = await tossApi.iapOrderStatus({ orderId, sku: "SKU_100_COINS" });
if (!result.ok) {
  // The lookup itself failed (transport, provider error, INVALID_RESPONSE):
  // no grant decision can be made from this response.
  throw new Error(`order status lookup failed: ${result.failureReason ?? result.error}`);
}

// verified: provider attests a payable status for THIS order.
// skuCheck: the product evidence matches what the caller expects.
const grantable =
  result.verified === true && !result.stub && result.skuCheck?.status === "MATCHED";
// NOT_PROVIDED means the provider omitted SKU evidence: the order is paid,
// but which product it bought is unconfirmed — decide explicitly.

Message recipients

normalizeMessageRecipient turns legacy { userKey, tossUserKey, anonKey } inputs into a single MessageRecipient ({ kind: "user", userKey } or { kind: "anonymous", anonKey }). Exactly one identifier must be supplied: requests with none, more than one, values of the wrong type, or empty strings are rejected (INVALID_MESSAGE_RECIPIENT for single sends, INVALID_CONTEXT_RECIPIENT for bulk contextList items) — identically in stub and forward mode. Each API converts the recipient itself:

  • Single (and test) sends carry the recipient in request headers: x-toss-user-key for user recipients, x-anon-key for anonymous ones. (Earlier releases emitted x-user-key, which the official API does not define; consumer proxy code that rewrote the header can now be removed.)
  • Bulk sends carry recipients in contextList body fields (userKey / anonKey), never in headers.

Smart message send results

smartMessageSend / smartMessageBulkSend only report success from send-result evidence the provider actually returned. providerStatus carries three meanings:

  • "SENT" — the send result contained real counts (an explicit msgCount of 0 included) or channel counts confirming delivery.
  • "FAILED" — a definite failure: explicit failure entries on a zero-send result, a FAIL/ERROR envelope, or a 4xx rejection. providerErrorCode mirrors a code the provider returned; channel results, failure reasons, and contentIds are preserved for partial successes.
  • "UNKNOWN" — the outcome could not be determined: an evidence-free body (empty object, HTML error page, SUCCESS envelope without a send result), wrong-typed counts, an unrecognized or outcome-ambiguous resultType (NETWORK_ERROR, TIMEOUT), or a 5xx response. These carry the internal error: "INVALID_RESPONSE" marker (parse/validation) and keep providerRequestId correlation, but never a fabricated sentAt. The message may or may not have been delivered: resolve the state out of band — do not assert non-delivery and do not auto-resend on this signal alone.

The same evidence rule applies to single and bulk sends, and to already-normalized responses (only this module's own SENT/FAILED vocabulary round-trips; anything else is UNKNOWN).

Anonymous keys and verification

verifyAnonKey calls POST /api-partner/v1/apps-in-toss/users/anon-key/verify with the key in the x-anon-key header. Results are tri-state:

  • { ok: true, valid: true } / { ok: true, valid: false } — definitive provider verdicts.
  • { ok: false, ... } (ProviderFailure) — no verdict was obtained (transport failure, non-2xx, FAIL envelope such as errorCode 4010, malformed response). Never translate this into valid: false.

Prefixes: the SDK's getAnonymousKey() resolves to { type: "HASH", hash } and the hash value is the key. Server APIs expect exactly that value. Applications that decorate stored identifiers with their own prefix (for example anon: or anonymous-) must strip their own prefix before calling; the kit transmits keys byte-for-byte and never adds or removes prefixes in either direction.

Recoverable promotion grants

promotionRewardGrant (legacy) chains key issuance, execution, and a result lookup in one call. The explicit three-step flow decouples them so your database sits between the phases:

// The recipient identity the grant is for — exactly one of userKey /
// tossUserKey / anonKey, the same value you will execute with. Strip your
// own storage prefix first; the kit transmits identifiers byte-for-byte.
const recipientKey = "anon:stored-hash"; // your app-prefixed storage form
const recipientAnonKey = recipientKey.replace(/^anon:/, "");

// 1. Issue a transaction key for that recipient. Prepare carries the
//    recipient as the single identity header and sends no request body;
//    a call with a missing, duplicated, or malformed recipient is rejected
//    before anything is dispatched.
const prepared = await tossApi.promotionPrepareReward({ anonKey: recipientAnonKey });
if (!prepared.ok) throw new Error(prepared.failureReason);

// 2. Persist the key BEFORE executing. Bind it to the owner, the recipient
//    identifier you prepared with, the promotion code, the amount, and
//    your own request id — see the storage guidance below.
await db.promotionGrants.insert({
  providerTransactionKey: prepared.providerTransactionKey,
  ownerId: session.userId,          // who initiated the grant
  recipientKey,                     // exactly what you prepared with
  promotionCode: "WELCOME_EVENT",
  amount: 1000,
  requestId: request.id,            // your idempotency/tracing id
  stage: "PREPARED"
});

// 3. Execute the grant with the stored key and the SAME recipient.
const executed = await tossApi.promotionExecuteReward({
  providerTransactionKey: prepared.providerTransactionKey,
  promotionCode: "WELCOME_EVENT",
  amount: 1000,
  anonKey: recipientAnonKey // or userKey / tossUserKey
});

// 4. Confirm the outcome. If step 3 crashed or returned UNKNOWN, re-run
//    only this step — status issues no keys and executes no grants.
const status = await tossApi.promotionRewardStatus({
  providerTransactionKey: prepared.providerTransactionKey,
  promotionCode: "WELCOME_EVENT",
  anonKey: recipientAnonKey
});
// status.status: "GRANTED" | "PENDING" | "FAILED" | "NOT_FOUND" | "UNKNOWN"

Outcome semantics

  • SUBMITTED (execute) means the provider accepted the request — confirm with the status step; it is not the final grant state.
  • UNKNOWN means no verdict was obtained: transport failures, 5xx, or unparseable responses. The transaction key is always preserved so you can resolve the outcome with a status query. Never treat UNKNOWN as a definite failure, and do not automatically re-execute: the provider rejects same-key re-execution with error 4113 ("already granted/ retracted"), but the official contract does not document idempotency for repeated execution.
  • Explicit failures (execute ok: false, status "FAILED") come only from a provider verdict — FAIL envelopes with error codes, 4xx, or the documented FAILED status (the provider rolls back the used budget).
  • "NOT_FOUND" is the documented error 4111: no grant record exists for the key (never executed, or the record is gone).
  • The official API supplies no grant timestamp and no documented key expiry. Status responses report checkedAt (when you observed the status) and never fabricate a grantedAt from your request time.
  • Passing a request identifier of your own does not make an external grant idempotent — treat it as tracing metadata only.

Consumer responsibilities

The provider only deduplicates by transaction key. Your application owns:

  • Storage: save the key bound to the owner, recipient identifier, promotion code, amount, and your request id before executing, so a lost execute response can always be resolved against what you intended to do.
  • Ownership checks: verify the caller owns the stored grant before executing or polling it.
  • Concurrency control: prevent two executes for the same key from racing (the legacy 4113 rejection is the provider's backstop, not a contract).
  • Ledger updates: credit balances only on GRANTED, and design UNKNOWN handling (retry the status query with backoff) rather than guessing.

The legacy promotionRewardGrant keeps its original behavior, including treating a caller-supplied providerTransactionKey as a result lookup only — passing an existing key never triggers an execute through that API.

See the AIT Kit repository for supported APIs, Cloudflare bindings, examples, and release notes.

Recipient privacy and numeric IDs

Promotion prepare/execute/status suppress failureReason text when it contains the submitted recipient (including short IDs and trimmed header spellings). Provider error codes, transaction keys and outcome semantics are preserved. This protects returned failure text; raw transport logs and the generic mTLS relay remain the caller's responsibility.

Numeric userKey and tossUserKey must be safe integers. Fractional and unsafe numbers are rejected before dispatch, including in single/bulk Smart Message requests. Use an exact string for large IDs; converting an already-rounded Number to a string cannot recover the original identifier.

Anonymous user authentication (SDK 3.6+)

exchangeAnonAuthCode({ code }, { mode: "forward", mtlsClient, signal }) exchanges a fresh SDK-issued code for { ok: true, anonKey }. Use an application-controlled abort signal for the request deadline. Exchange immediately; codes are single-use and expire after five minutes. The helper makes one request, never retries automatically, and never returns fabricated identities in stub mode. Failures contain fixed error categories rather than provider payloads or secrets. Cancellation returns ABORTED, distinct from UPSTREAM_UNAVAILABLE; neither result includes a transport exception or the caller-supplied abort reason.

verifyAnonKey checks that a key exists; it does not authenticate the caller. Authenticate with code exchange before issuing your own session. Order ownership, per-account grant routing, replay prevention, and a durable purchase ledger remain the application's responsibility. This helper does not itself authorize purchases.

Official contract: https://developers-apps-in-toss.toss.im/api/user-key.md

版本与发布记录

@ait-kit/api-core 共发布 11 个版本,首次发布于 2026/06/30;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.0latest2026/10/1424 天前
0.5.12026/09/211 个月前
0.5.02026/09/171 个月前
0.4.22026/09/171 个月前
0.4.12026/09/161 个月前
0.4.02026/09/161 个月前

关于 @ait-kit/api-core 的常见问题

@ait-kit/api-core 是什么?

Runtime-neutral typed Apps in Toss server API adapters and mTLS transport ports.

如何安装 @ait-kit/api-core?

在项目目录运行 npm install @ait-kit/api-core,或使用 pnpm add / yarn add / bun add。

@ait-kit/api-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ait-kit/api-core 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ait-kit/api-core 可以商用吗?

@ait-kit/api-core 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ait-kit/api-core 的下载量有多少?

根据 npm 官方下载统计,@ait-kit/api-core 在 2026/10/05 – 2026/11/03 这 30 天内被下载 3,628 次,日均约 121 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ait-kit/api-core 还在维护吗?

最新版本 0.6.0 发布于 2026/10/14(24 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/06/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。