2.0.0: changelog and skill docs for slugs and environments
CI / check (push) Successful in 16s
CI / publish (push) Has been skipped

Claude-Session: https://claude.ai/code/session_01SMCvdwDmuxoaqGgvGBLk1V
This commit is contained in:
edmand46
2026-09-06 22:39:03 +03:00
parent a7401b534a
commit 7271874cac
9 changed files with 76 additions and 28 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ service, or standalone economy service in this SDK — wallets live on
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
| Inventory (catalog-merged) | `client.inventory` | observable `SyncedState<InventoryItem[]>` |
| Catalog | `client.catalog` | observable `SyncedState<Map<string, CatalogItem>>` |
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerId, opts?)` | observable `SyncedState<ShopHandle[]>` |
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerSlug, opts?)` | observable `SyncedState<ShopHandle[]>` |
| Remote config | `client.remoteConfig` + `.get(key, default?)` | observable, typed |
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations |
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation |
+13
View File
@@ -50,6 +50,19 @@ On success both tokens are saved to the client's `TokenStore`, the runtime
starts (domains warmed, scenario engine restored, `player_login` event fired),
and the state flips to `'signed-in'`.
## Environments
A project has two environments, `staging` and `prod`. The SDK key you pass as
`RudderClientOptions.projectKey` belongs to one of them, so the environment is
resolved at login and carried inside the access and refresh tokens; nothing in
the client API takes an environment argument, and a player created in one
environment is invisible in the other. Content released only to `staging` is
empty for a `prod` key and vice versa.
Tokens issued before SDK 2.0.0 carry no environment claim and are rejected with
401. The transport's refresh then fails, clears the token store and emits
`'signed-out'` — log the player in again.
## Auth state
```ts
@@ -2,7 +2,7 @@
`BattlePassService` (source: `src/battlepass/BattlePassService.ts`).
Call-and-response access to battle pass endpoints. Battle pass state is tied to
a **scenario battle pass node**, so calls carry `scenarioId` + `nodeId` (plus
a **scenario battle pass node**, so calls carry `scenarioSlug` + `nodeId` (plus
`runId` for mutating calls). NOT observable — re-fetch progress explicitly
after a mutation.
@@ -14,7 +14,7 @@ you already know the scenario/node/run identifiers.
## Methods
```ts
getProgress(scenarioId: string, nodeId: string): Promise<GetBattlePassProgressResponse>
getProgress(scenarioSlug: string, nodeId: string): Promise<GetBattlePassProgressResponse>
addXp(request: AddBattlePassXpRequest): Promise<AddBattlePassXpResponse>
claimReward(request: ClaimBattlePassRewardRequest): Promise<ClaimBattlePassRewardResponse>
purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise<PurchaseBattlePassPremiumResponse>
@@ -27,7 +27,7 @@ interface AddBattlePassXpRequest {
amount?: number;
nodeId?: string;
runId?: string;
scenarioId?: string;
scenarioSlug?: string;
source?: string; // configured XP source
}
interface AddBattlePassXpResponse {
@@ -41,7 +41,7 @@ interface ClaimBattlePassRewardRequest {
level?: number;
nodeId?: string;
runId?: string;
scenarioId?: string;
scenarioSlug?: string;
track?: 'free' | 'premium';
}
interface ClaimBattlePassRewardResponse {
@@ -62,7 +62,7 @@ interface PurchaseBattlePassPremiumRequest {
idempotencyKey?: string;
nodeId?: string;
runId?: string;
scenarioId?: string;
scenarioSlug?: string;
}
interface PurchaseBattlePassPremiumResponse {
error?: string;
@@ -80,5 +80,5 @@ interface PurchaseBattlePassPremiumResponse {
in the response and the typed error `code` (`RudderErrorCodes`).
- `purchasePremium` charges the player's wallet; idempotent; pass your own
`idempotencyKey` for safe retries.
- Prefer the `onBattlePass` effect session, which binds `scenarioId` /
- Prefer the `onBattlePass` effect session, which binds `scenarioSlug` /
`nodeId` / `runId` and posts scenario callbacks for you.
+6 -6
View File
@@ -11,15 +11,15 @@ Distinct from scenario quest nodes, which advance through
```ts
list(): Promise<Quest[]>
claim(questId: string): Promise<ClaimQuestResponse>
reportProgress(metric: string, amount: number): Promise<string[]> // ids of quests completed by this report
claim(questSlug: string): Promise<ClaimQuestResponse>
reportProgress(metric: string, amount: number): Promise<string[]> // slugs of quests completed by this report
```
## Types
```ts
interface Quest {
id?: string;
slug?: string; // stable across environments — what claim() takes
name?: string;
objectives?: QuestObjectiveProgress[];
rewards?: Reward[];
@@ -47,12 +47,12 @@ interface ClaimQuestResponse {
```ts
const quests = await client.quests.list();
for (const quest of quests) {
if (quest.status === 'completed' && quest.id) {
const res = await client.quests.claim(quest.id);
if (quest.status === 'completed' && quest.slug) {
const res = await client.quests.claim(quest.slug);
// res.success / res.alreadyClaimed / res.granted
}
}
const completedIds = await client.quests.reportProgress('kills', 1);
const completedSlugs = await client.quests.reportProgress('kills', 1);
```
- `claim` is idempotent server-side; check `success` / `alreadyClaimed` /
+4 -4
View File
@@ -122,7 +122,7 @@ counter response may carry the next `PendingEffect`.
}
```
These wrap `client.battlePass` with the run's `scenarioId`/`nodeId`/`runId`
These wrap `client.battlePass` with the run's `scenarioSlug`/`nodeId`/`runId`
already bound — prefer them over calling the service manually.
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
@@ -134,9 +134,9 @@ already bound — prefer them over calling the service manually.
### Run lifecycle effects
```ts
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioId: string }
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string }
interface ScenarioFailedEffect {
readonly runId: string; readonly scenarioId: string; readonly nodeId: string;
readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string;
readonly error: Error;
}
```
@@ -154,7 +154,7 @@ not delivered to the client.
## Reliability notes
- Completion methods POST `/sdk/v1/scenarios/callback` with
`{scenarioId, runId, nodeId, handle}` using the handles `output`,
`{scenarioSlug, runId, nodeId, handle}` using the handles `output`,
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
`onPremiumPurchase`.
- Transient callback failures (network error, 5xx) leave the effect active
+10 -8
View File
@@ -13,7 +13,7 @@ client.stores.onChange(cb);
await client.stores.load() / .reload();
// Direct purchase (bypassing handles):
await client.stores.purchase(storeSlug, offerId, options?);
await client.stores.purchase(storeSlug, offerSlug, options?);
```
## Handles
@@ -28,7 +28,8 @@ class ShopHandle {
}
class OfferHandle {
readonly id: string;
readonly id: string; // authoring id, differs between staging and prod
readonly slug: string; // stable across environments — what purchases use
readonly name?: string;
readonly price?: OfferPrice; // { amount?: number; currency?: string }
readonly contents: OfferContent[]; // { amount?: number; itemId?: string }[]
@@ -48,15 +49,15 @@ interface PurchaseOfferResponse {
## Purchase semantics
- `stores.purchase(storeSlug, offerId, options?)` and `offer.buy(options?)` are
the same executor; handles just bind the slug/id.
- `stores.purchase(storeSlug, offerSlug, options?)` and `offer.buy(options?)` are
the same executor; handles just bind the store slug and the offer slug.
- When `options.idempotencyKey` is omitted the SDK generates
`crypto.randomUUID()` per call — safe retries require passing your own key.
- On success the executor invalidates `player`, `inventory`, and `stores`, so
subscribers observe fresh wallet/inventory/store data immediately.
- Check `response.success` / `response.error` — a failed purchase is a resolved
response, not necessarily a thrown error.
- Purchase metrics (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) are
- Purchase metrics (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) are
reported to quests automatically server-side — do not report them manually
(see `reference/quests.md`).
@@ -71,10 +72,11 @@ interface Store {
}
interface Offer {
contents?: OfferContent[]; createdAt?: string; id?: string;
maxPurchases?: number; name?: string; price?: OfferPrice; updatedAt?: string;
maxPurchases?: number; name?: string; price?: OfferPrice; slug?: string;
updatedAt?: string;
}
```
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
without `slug` / an offer without `id` — in practice the domain only builds
handles from server data that has both.
without `slug` / an offer without `id` or `slug` — in practice the domain only
builds handles from server data that has all of them.