@viceme-ai/sdk
v0.8.0
Published
ViceMe hosted engagement and Website Work access SDK for static HTML, ESM, React, and Agent-generated sites.
Readme
@viceme-ai/sdk
ViceMe browser SDK for Shop-hosted engagement and Website Work access.
The release PR atomically owns the package version, runtime manifest, and
changelog update. During the current preview stage, publication does not
require a repository license file. The root LICENSE-PENDING.md records the
deferred decision without blocking release; licensing will be finalized
separately.
Install
pnpm add @viceme-ai/sdkStatic HTML
<div id="viceme-engagement"></div>
<script
defer
src="https://s3.viceme.cn/viceme-sdk/0.5.0/viceme.min.js"
data-viceme-work="wrk_live_demo"
data-viceme-region="cn"
data-viceme-features="danmaku,tip"
data-viceme-target="#viceme-engagement"
data-viceme-theme="auto"
></script>The feature declaration accepts danmaku, tip, or both without whitespace or
duplicates.
Static CDN entry points always use an immutable exact-version directory. The CN
host is s3.viceme.cn; GLOBAL uses s3.viceme.ai. The 0.5.0 URL above is the
current source target and is unavailable until that exact release is published
and verified.
With CSP, allow the exact regional S3 origin in script-src and connect-src,
the exact regional Shop origin in frame-src, and keep object-src 'none'. A
nonce with 'strict-dynamic' may authorize dynamic scripts, but the other
directives still need exact origins. Do not use * or a ViceMe subdomain
wildcard.
ESM
import { createViceMe } from '@viceme-ai/sdk';
import { mountDanmaku } from '@viceme-ai/sdk/danmaku';
import { mountTip } from '@viceme-ai/sdk/tip';
const client = createViceMe({ workKey: 'wrk_live_demo', region: 'cn' });
await client.ready();
const target = document.querySelector('#viceme-engagement');
if (!target) throw new Error('ViceMe target missing');
const results = await Promise.allSettled([
mountDanmaku(client, { target, theme: 'auto' }),
mountTip(client, { target, theme: 'auto', presentation: 'integrated' }),
]);
const mounted = results.flatMap((result) => (result.status === 'fulfilled' ? [result.value] : []));
mounted.forEach((handle) => handle.destroy());
client.destroy();Run those cleanup calls from the owning component's unmount path or another
explicit lifecycle boundary, not from pagehide (which also covers bfcache).
The integrated presentation uses the danmaku bar as its only visible launcher
and opens the official Tip dialog. Standalone mountTip() remains inline by
default.
createViceMe and ready() are purely local and never contact Shop. A live client reports
build support for danmaku and tip; Shop remains authoritative for whether a
Work enables either capability. The hosted /embed/danmaku iframe uses Shop's
internal SDK to read and create anonymous messages through
/v1/danmaku/messages. The /widget/tip/<workKey> iframe owns confirmation,
payment, risk, and result authority. Visitors do not sign in to ViceMe and are
anonymous to the creator; the observed parent Origin is attribution rather than
an authorization gate.
Pass a selected public pair value: keys.test is wrk_test_... and keys.live
is wrk_live_.... Other Work key shapes are rejected locally.
Access operations establish a short-lived, memory-only Work session on first
use. They expose client.auth, client.access, and client.checkout; login,
buyer recovery, and hosted checkout remain ViceMe-owned UI. The host never
receives a general ViceMe session or payment credential. Tests can inject a
deterministic transport and presenter through @viceme-ai/sdk/testing.
Calling client.destroy() cancels in-flight access requests, closes the active
SDK-owned sign-in or checkout layer, and rejects the interrupted call with
CLIENT_DESTROYED; late responses cannot restore the in-memory Work token.
Request cancellation and timeouts remain effective until the parsed response is
delivered. Cancelling the client's optional signal preserves the caller's
Error reason for requests and interactive presentations; a request timeout
rejects with retryable NETWORK_TIMEOUT.
A cancelled response cannot establish a Work session even if its body has
already finished parsing.
Website Access v3 is negotiated during the first Access session request via
supportedAccessProtocolVersions: [3]. An unversioned server response retains
the legacy login/follow flow and CNY amountCents representation. V3 returns
CNY or USD amountMinor prices and feature status; PENDING_CHANNEL may have
no sale price and a non-null pricingIntent. The server decides which channels
are ready. GLOBAL supports access integration and login; missing payment
channels never fall back to CN or create an order.
V3 does not require, bind, or compare host Origin, including null/missing origins. Official HTTPS Shop URLs and server-authoritative identity, Work, expiry, and entitlement validation remain mandatory. Browser restrictions on Web Crypto, frames, navigation, and storage can still require a first-party recovery step.
access.require(featureKey) handles buyer identification, checkout, and a fresh
access check. It never grants access from a payment or window message. V3 login
opens directly from the initiating action; FOLLOW executes in that same user
interaction without another confirmation. Shop owns automatic follow after an
authenticated login/payment; anonymous purchase skips both login and follow.
Optional recovery controls use the same protocol:
await client.access.restorePurchase('download'); // Remains anonymous.
await client.access.claimPurchase('download'); // User explicitly chooses login and claim.The official page owns purchase receipts, account selection, and claim consent. The SDK obtains only a short-lived Work-scoped buyer or user credential through PKCE challenge/result/exchange calls. These calls omit cookies. It does not accept credentials from postMessage, query strings, or fragments. Explicit claim replaces the current anonymous context only after a verified user exchange; failed/cancelled claim keeps existing purchase authorization.
If an iframe cannot access first-party storage, “在当前页面继续” navigates to the
official page. Only the challenge, state, verifier, and selected feature/purpose
are saved in sessionStorage for at most five minutes. After returning, invoking
the same action or require() for the same feature resumes the authorized
recovery/claim handshake. No buyer/user token, recovery secret,
or payment result is persisted by the SDK. Unavailable navigation storage is
reported instead of pretending that a purchase was recovered.
Work-session refresh is single-flight and server expiry retries at most once.
Refreshing a Work session drops user/buyer context; the next authoritative
check can request recovery again. Logout, identity replacement, and destruction
invalidate late identity results. Authorization reads that race a session
change are retried against the current identity; successful mutations are not
replayed merely because another request refreshed a session. Both explicit
destroy() and caller cancellation stop bridge polling and close the layer;
the caller's Error reason remains intact.
const decisions = await client.access.checkMany(['members', 'pro-tools']);
if (!decisions['pro-tools']?.allowed) {
await client.access.require('pro-tools');
}The Tip subpath exports TipPaidDetail and TipWidgetCloseDetail for the
sanitized viceme:tip-paid and viceme:widget-close CustomEvent details.
TipPaidDetail contains only status, trusted work.id/title, amount, and CNY;
it contains no key, provider, order number, token, or transaction identifier.
The danmaku mount hashes the canonical page URL locally, combines it with a 10% scroll bucket, and sends only the opaque anchor to the hosted iframe. Destroying the mount removes its nodes, listeners, debounce timer, and location poll.
The Tip mount sends no amount, provider, token, or application ID. It enables interaction only after a trusted resize handshake. In integrated mode it also accepts open requests only from the matching trusted danmaku controls, then restores focus there after close. Shop resets its hosted payment surface on Escape before sending close; the SDK forwards sanitized close and paid notifications, and removes its iframe, timer, media listener, and message listener on destroy.
Headless Tip
This additive API targets 0.5.0. Do not expect createTip or
@viceme-ai/sdk/tip/testing from the immutable npm 0.4.0 package.
import { createViceMe } from '@viceme-ai/sdk';
import { createTip } from '@viceme-ai/sdk/tip';
const client = createViceMe({ workKey: 'wrk_live_demo', region: 'cn' });
const tip = createTip(client);
const config = await tip.getConfig();
button.addEventListener('click', async () => {
const result = await tip.open({
amountCents: config.amount.minCents,
provider: config.providers[0],
locale: 'zh-CN',
appearance: 'auto',
});
console.log(result.status);
});
function destroyTip() {
tip.destroy();
client.destroy();
}Call destroyTip() from the owning component or route cleanup. Do not bind it
to pagehide, because that event also fires when a page enters the bfcache.
getConfig() strictly parses the credential-free regional
GET /v1/work-sdk/<encoded-workKey>/tip-config response. Call open() directly
from user activation so it can create its secure full-screen frame in the same
call stack. Only PAID, CANCELLED, and UNKNOWN cross back; no order number,
token, payment action, or transaction ID is exposed. scene, metadata, and
testMode are rejected or absent from the API.
The config environment remains server-authoritative. The SDK checks only that
wrk_test_... is paired with SANDBOX and wrk_live_... with PRODUCTION, and
rejects inconsistent responses.
The first Tip release is CN/CNY only. region: 'global' fails locally with
CAPABILITY_DISABLED without a config request or iframe. Anonymous Tip does not
use the legacy WeChat JSAPI user/OpenID path.
TIP_CONFIG_INVALID is a non-retryable Shop/SDK contract mismatch and should be
reported rather than retried. TIP_OPEN_IN_PROGRESS means the existing call
must settle first. TIP_READY_TIMEOUT is retryable after the failed call has
cleaned itself up. On every component or route unmount, call tip.destroy()
before client.destroy() so an in-flight flow settles as UNKNOWN and leaves
no portal or message listener behind.
Immutable exact-version CDN ESM exports the same implementation from
<origin>/viceme-sdk/<version>/index.js and tip.js without adding a window
global.
Use the scoped fake in components and Storybook:
import { createTestTip } from '@viceme-ai/sdk/tip/testing';
const paid = createTestTip({ config, outcome: 'PAID' });
const failed = createTestTip({ config, outcome: new Error('fixture failure') });The config may also be an Error. SANDBOX is always supplied by server config
for a test Work, never by a production SDK switch.
