npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ait-kit/api-core

v0.7.0

Published

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

Readme

@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. It tells you whether a key exists, not whether the caller owns it: never treat a successful verification as authentication. Authenticate anonymous users with exchangeAnonAuthCode instead. 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+)

Exchanging a fresh SDK-issued code for { ok: true, anonKey } is a first-class operation on every server surface:

| Surface | Call | |---|---| | createAppsInTossApi(options) | api.users.exchangeAnonAuthCode({ code }, { signal }) | | createAppsInTossApiRpc / createTossMtlsCore | rpc.exchangeAnonAuthCode({ code }) | | Standalone helper | exchangeAnonAuthCode({ code }, { mode: "forward", mtlsClient, signal }) | | @ait-kit/api-cloudflare-service | Service Binding RPC exchangeAnonAuthCode, or authenticated POST /internal/apps-in-toss/anonymous-key/exchange | | @ait-kit/api-client | client.anonAuthCodeExchange({ code }) (PROXY_ENDPOINTS.anonAuthCodeExchange) |

All of them call POST /api-partner/v1/apps-in-toss/users/anon-key/exchange with { code } and resolve to { ok: true, anonKey } or { ok: false, error } (ABORTED, INVALID_AUTH_CODE, FORWARD_MODE_REQUIRED, UPSTREAM_UNAVAILABLE, EXCHANGE_REJECTED, INVALID_RESPONSE). Stub mode returns FORWARD_MODE_REQUIRED; it never invents an identity.

Use an application-controlled abort signal for the request deadline. Exchange immediately; codes are single-use and expire after five minutes. Each call makes one request: nothing retries automatically or caches a result, so a failed or timed-out exchange needs a fresh code from the client, not a resend of the old one. 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