@stewardhq/sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,188 +1,195 @@
1
1
  # @stewardhq/sdk
2
2
 
3
- Steward ↔ ürün entegrasyonu: sözleşme şemaları (zod), tipli API istemcisi ve
4
- webhook imzası. Kaynak: [z9cloud/steward](https://github.com/z9cloud/steward/tree/main/sdk).
3
+ Steward ↔ product integration: contract schemas (zod), a typed API client and webhook signing.
4
+ Source: [z9cloud/steward](https://github.com/z9cloud/steward/tree/main/sdk).
5
5
 
6
- ## Kurulum
6
+ ## Installation
7
7
 
8
- npmjs'te public; token gerekmez.
8
+ Public on npmjs; no token needed.
9
9
 
10
10
  ```bash
11
11
  pnpm add @stewardhq/sdk
12
12
  ```
13
13
 
14
- ESM, Node ≥ 22. Tek çalışma zamanı bağımlılığı zod.
14
+ ESM, Node ≥ 22. Its only runtime dependency is zod.
15
15
 
16
- ## Alt yollar
16
+ ## Subpaths
17
17
 
18
- | Import | İçerik |
18
+ | Import | Contents |
19
19
  | --- | --- |
20
- | `@stewardhq/sdk` | Tüm şema/tipler + fırlatan istemci `createSteward`, hesap durumu önbelleği `stateCache`, `defineCatalog`, hata sınıfları + `httpStatusFor`, `isValidTckn`/`isValidVkn`/`isE164`; metering: `Events()`, `Meters()` |
21
- | `@stewardhq/sdk/contract` | Yalnızca şemalar ve tipler |
22
- | `@stewardhq/sdk/webhook` | `signWebhook` / `verifyWebhook` / `verifyWebhookRaw` (Node HMAC, senkron) |
23
- | `@stewardhq/sdk/webhook/web` | Aynı API WebCrypto ile (async; edge/workerd/tarayıcı) |
24
- | `@stewardhq/sdk/server` | `Webhooks()`: imzalı event alıcısı, canlı durum + önbellek, tipli hook'lar (Node); `Checkout()`: hosted checkout oturumu (`create` → `url`) ve dönüş sonucu (`result`); `CustomerPortal()`: hosted müşteri portalı oturumu (`create` → `url`) |
20
+ | `@stewardhq/sdk` | All schemas and types + the throwing client `createSteward`, the account state cache `stateCache`, `defineCatalog`, the error classes + `httpStatusFor`, `isValidTckn`/`isValidVkn`/`isE164`; metering: `Events()`, `Meters()` |
21
+ | `@stewardhq/sdk/contract` | Schemas and types only |
22
+ | `@stewardhq/sdk/webhook` | `signWebhook` / `verifyWebhook` / `verifyWebhookRaw` (Node HMAC, synchronous) |
23
+ | `@stewardhq/sdk/webhook/web` | The same API on WebCrypto (async; edge/workerd/browser) |
24
+ | `@stewardhq/sdk/server` | `Webhooks()`: a signed event receiver, live state + cache, typed hooks (Node); `Checkout()`: a hosted checkout session (`create` → `url`) and its return result (`result`); `CustomerPortal()`: a hosted customer portal session (`create` → `url`) |
25
25
 
26
- Kök giriş, `./contract` ve `./webhook/web` `node:*` kullanmaz (edge/tarayıcı);
27
- `./webhook` senkron Node HMAC'idir.
26
+ The root entry, `./contract` and `./webhook/web` do not use `node:*` (edge/browser);
27
+ `./webhook` is the synchronous Node HMAC.
28
28
 
29
- ## Kullanım
29
+ ## Usage
30
30
 
31
- Hesap durumu (okuma modeli) — ürün DB'sinde hak projeksiyonu tutulmaz; kapılar steward'ı
32
- pod başına bir önbellek üzerinden canlı okur:
31
+ Account state (the read model) — no entitlement projection is kept in the product database;
32
+ gates read steward live through a per-pod cache:
33
33
 
34
34
  ```ts
35
35
  import { createSteward, stateCache } from "@stewardhq/sdk";
36
36
 
37
37
  export const steward = createSteward();
38
38
  export const state = stateCache(steward, {
39
- catalog, // entitlements() tipli olur
40
- // steward kapalı ve bayat değer yoksa (AccountState | null); catalog.stateOf: katalogdan tam durum (version 0)
39
+ catalog, // makes entitlements() typed
40
+ // used when steward is down and there is no stale value (AccountState | null); catalog.stateOf: full state from the catalog (version 0)
41
41
  fallback: async (ref) => {
42
42
  const org = await findOrg(ref);
43
43
  return org ? catalog.stateOf(ref, org.plan, { displayName: org.name }) : null;
44
44
  },
45
45
  onFallback: ({ ref, reason }) => metrics.billingFallback.inc({ reason }),
46
- // missing: "fallback" → steward'da hesabı olmayan kayıt da fallback'e (hesap ilk checkout'ta açılıyorsa)
46
+ // missing: "fallback" → a record with no account in steward also goes to the fallback (if the account is only created at the first checkout)
47
47
  // ttlMs: 30_000, staleIfErrorMs: 600_000, maxEntries: 10_000, logger
48
48
  });
49
49
 
50
50
  const e = await state.entitlements(orgId); // { max_projects: number | null; sso: boolean }
51
- await state.state(orgId, { fresh: true }); // TTL'i beklemeden
52
- state.prime((await steward.grants.create(orgId, input)).account); // mutasyon yanıtı önbelleğe
53
- const byRef = await state.many(orgIds); // Map<ref, AccountState>, en çok 8 paralel
51
+ await state.state(orgId, { fresh: true }); // without waiting for the TTL
52
+ state.prime((await steward.grants.create(orgId, input)).account); // a mutation response into the cache
53
+ const byRef = await state.many(orgIds); // Map<ref, AccountState>, at most 8 in parallel
54
54
  ```
55
55
 
56
- | Durum | Davranış |
56
+ | Situation | Behaviour |
57
57
  | --- | --- |
58
- | TTL içinde | Bellekten, istek yok; aynı hesabın eşzamanlı okumaları tek istek |
59
- | TTL doldu | `If-None-Match`: 304 süreyi yeniler, 200 yeni durumu yazar |
60
- | Ağ hatası, zaman aşımı, 5xx, 429 | Süre dolduktan sonra `staleIfErrorMs` boyunca bayat değer (WARN), sonra `fallback` (WARN + `onFallback`), o da yoksa (ya da `null`) asıl `StewardNetworkError`/`StewardApiError`. Başarısız istekten sonra o hesap için `min(ttlMs, 5 sn)` steward'a gidilmez |
61
- | 404 `account_not_found` (varsayılan `missing: "throw"`) | Kesinti değil: fallback yok, önbelleğe girmez, `StewardApiError` fırlar (eksik `upsert` görünür kalır) |
62
- | 404 `account_not_found`, `missing: "fallback"` | `fallback(ref)` (zorunlu seçenek); WARN yok, `onFallback` nedeni `account_not_found` (kesinti metriğinden ayrılır); bayat değer kullanılmaz. Sonuç önbelleğe girmez, "yok" bilgisi `min(ttlMs, 5 sn)` tutulur; `prime`/`invalidate`/`fresh` bitirir (checkout sonucu ve webhook `prime` eder). Fallback `null` → 404 fırlar |
63
- | `prime(account)` | Yalnızca `version >= önbellekteki` ise yazar (geri gitmez); `null`/`undefined` yok sayılır |
58
+ | Within the TTL | From memory, no request; concurrent reads of the same account share one request |
59
+ | TTL expired | `If-None-Match`: a 304 renews the window, a 200 stores the new state |
60
+ | Network error, timeout, 5xx, 429 | After expiry, the stale value for `staleIfErrorMs` (WARN), then `fallback` (WARN + `onFallback`), and if there is none (or it returns `null`) the original `StewardNetworkError`/`StewardApiError`. After a failed request, steward is not called again for that account for `min(ttlMs, 5 s)` |
61
+ | 404 `account_not_found` (default `missing: "throw"`) | Not an outage: no fallback, nothing cached, `StewardApiError` is thrown (a missing `upsert` stays visible) |
62
+ | 404 `account_not_found`, `missing: "fallback"` | `fallback(ref)` (a required option); no WARN, and `onFallback`'s reason is `account_not_found` (kept apart from the outage metric); the stale value is not used. The result is not cached, and the "absent" fact is held for `min(ttlMs, 5 s)`; `prime`/`invalidate`/`fresh` end it (a checkout result and a webhook both `prime`). A `null` fallback → the 404 is thrown |
63
+ | `prime(account)` | Writes only when `version >= the cached one` (never goes backwards); `null`/`undefined` is ignored |
64
64
 
65
- Önbellek pod başına bellek içidir (LRU); çok pod'da tutarlılık TTL kadardır.
66
- `invalidate(ref)` sonraki okumanın hemen (ETag'le) sormasını sağlar.
65
+ The cache is in-memory per pod (LRU); across many pods, consistency is bounded by the TTL.
66
+ `invalidate(ref)` makes the next read ask immediately (with the ETag).
67
67
 
68
- Webhook alıcısı (`/server`, Node) — imza ham gövdede, sonra sözleşme; durum steward'dan
69
- canlı okunur, önbelleğe yazılır; hook'lar tipli:
68
+ Webhook receiver (`/server`, Node) — the signature is checked on the raw body, then the
69
+ contract; state is read live from steward and written into the cache; the hooks are typed:
70
70
 
71
71
  ```ts
72
72
  import { Webhooks } from "@stewardhq/sdk/server";
73
73
 
74
74
  const webhooks = Webhooks({
75
- steward, cache: state, // refetch (steward verilince varsayılan): payload yalnız tetikleyici
76
- // secrets verilmezse her teslimatta STEWARD_WEBHOOK_SECRET ("yeni,eski" rotasyonu); toleranceSeconds: 300
75
+ steward, cache: state, // refetch (the default once steward is given): the payload is only a trigger
76
+ // without secrets, STEWARD_WEBHOOK_SECRET is read on every delivery ("new,old" rotation); toleranceSeconds: 300
77
77
  onStateChanged: async ({ ref, state, cause }) => {
78
78
  const org = await findOrg(ref);
79
- if (!org) return "ignore"; // bilinmeyen hesap: 200, diğer hook'lar ve after koşmaz
79
+ if (!org) return "ignore"; // unknown account: 200, and neither the other hooks nor after run
80
80
  await applyOrgPlan(org, state.planCode, state.accessState);
81
81
  },
82
- onSubscriptionTerminated: ({ ref }) => flagOps(ref), // on<Tip>: eski tipten ya da state_changed cause'undan
82
+ onSubscriptionTerminated: ({ ref }) => flagOps(ref), // on<Type>: from the old type or from state_changed's cause
83
83
  onPaymentFailed: ({ ref, state, profile }) => mailOwner(ref, profile), // + onSuspended, onTerminated
84
- after: ({ ref, state }) => propagate(ref, state.accessState), // diğer hook'lardan sonra, her teslimatta
85
- // includeProfile: true → bildirim hook'ları fatura profilini (e-posta) alır; refetch: false → gövdedeki durum
84
+ after: ({ ref, state }) => propagate(ref, state.accessState), // after the other hooks, on every delivery
85
+ // includeProfile: true → the notification hooks get the billing profile (email); refetch: false → the state from the body
86
86
  });
87
87
 
88
- app.post("/v1/billing/events", (c) => webhooks.handle(c.req.raw)); // ya da: const { status, body } = await webhooks.receive(rawBody, headers)
88
+ app.post("/v1/billing/events", (c) => webhooks.handle(c.req.raw)); // or: const { status, body } = await webhooks.receive(rawBody, headers)
89
89
  ```
90
90
 
91
- | Durum | Yanıt |
91
+ | Situation | Response |
92
92
  | --- | --- |
93
- | Hook'lar koştu | 200 `{ok: true, handled: "received"}` |
94
- | `onStateChanged` `"ignore"` döndü | 200 `ignored` + WARN; neden hook'ları ve `after` koşmaz |
95
- | Hesap silindi: `account.deleted` ya da `cause: "account.deleted"` | 200 `deleted`; canlı okuma yok, `onAccountDeleted` + `after` gövdedeki anonim son durumla (`onStateChanged` koşmaz, `cache.invalidate` varsa çağrılır). Silmeden önce kuyruğa girmiş teslimatın canlı okuması `account_deleted` (410) alırsa hook'suz 200 `deleted` + WARN |
96
- | `endpoint.ping` | 200 `ping`, hook yok |
97
- | `refetch: false` ve gövdede yalnızca snapshot (yükseltme öncesi kuyruk) | 200 `no_state` + WARN: `onStateChanged` koşmaz, neden hook'ları ve `after` `state: null` ile koşar |
98
- | Tanınmayan tip | 200; `onUnknownEvent` (durum varsa önce `onStateChanged`) |
99
- | İmza/başlık/zaman penceresi | 401 |
100
- | İmzalı ama sözleşmeye uymayan gövde, id uyuşmazlığı | 400 + ALARM (steward yeniden dener) |
101
- | Sır yok, güncel durum okunamadı (`state_unavailable`), profil okunamadı, bir hook fırlattı | 503 (steward yeniden dener, hook'ların hepsi yeniden koşar) |
102
-
103
- Tipler: `steward` verilmiş ve `refetch: false` değilse her hook'un (`onStateChanged`, `on<Tip>`,
104
- `onPaymentFailed`…, `onUnknownEvent`, `after`) `state`'i `AccountState`'tir — durum okunamazsa 503 döner,
105
- hook koşmaz; `after`'ın `handled`'ı `"received"`. `refetch: false` ya da `steward`'sız alıcıda neden hook'ları,
106
- `onUnknownEvent` ve `after` `AccountState | null` alır (yükseltme öncesi kuyruk; `onStateChanged` yine tam
107
- durumla koşar). Ayrım `refetch` ve `steward` alanlarının tiplerinden çıkarılır (tip belirsizse null'lı).
108
-
109
- **SDK tekrar ayıklamaz, version kapısı ve kilit tutmaz.** `refetch` ile her teslimat
110
- durumu steward'dan canlı okur: sırasız ya da tekrar teslimat en güncel durumu görür,
111
- yalnızca snapshot taşıyan eski gövde de tam kullanılır (`event.data.account` bayat
112
- olabilir; `state`'i kullan). Aynı event yine birden çok kez ve aynı hesabın teslimatları
113
- eşzamanlı gelebilir: hook'lar idempotent yazmalı (plan alanını durumdan yaz, "zaten
114
- askıdaysa dokunma", sonlandırma işareti `coalesce`). Yeniden denenmesi istenen durumda hook
115
- fırlatır → 503. `refetch: false` gövdedeki durumu kullanır (sıra garantisi yok; eski
116
- kuyruğun boşalmasını bekle). Aynı olay için hem eski tipi hem `account.state_changed`'i
117
- isteyen endpoint neden hook'larını iki kez çalıştırır: endpoint filtresinde birini seç.
118
-
119
- Hosted checkout (`/server`) — profil, TCKN/VKN, onaylar ve ödeme formu steward'ın sayfasında;
120
- ürün yalnızca oturumu açar, kullanıcıyı `url`'e yönlendirir ve dönüşte sonucu sorar:
93
+ | The hooks ran | 200 `{ok: true, handled: "received"}` |
94
+ | `onStateChanged` returned `"ignore"` | 200 `ignored` + WARN; the cause hooks and `after` do not run |
95
+ | Account deleted: `account.deleted` or `cause: "account.deleted"` | 200 `deleted`; no live read, and `onAccountDeleted` + `after` run with the anonymised final state from the body (`onStateChanged` does not run; `cache.invalidate` is called if present). If a delivery queued before the deletion gets `account_deleted` (410) on its live read, it returns 200 `deleted` + WARN with no hooks |
96
+ | `endpoint.ping` | 200 `ping`, no hooks |
97
+ | `refetch: false` and the body carries only a snapshot (a queue from before the upgrade) | 200 `no_state` + WARN: `onStateChanged` does not run, and the cause hooks and `after` run with `state: null` |
98
+ | Unrecognised type | 200; `onUnknownEvent` (preceded by `onStateChanged` if there is state) |
99
+ | Signature / header / time window | 401 |
100
+ | Signed but not contract-conformant body, or an id mismatch | 400 + ALARM (steward retries) |
101
+ | No secret, current state unreadable (`state_unavailable`), profile unreadable, or a hook threw | 503 (steward retries, and every hook runs again) |
102
+
103
+ Types: when `steward` is given and `refetch` is not `false`, the `state` of every hook
104
+ (`onStateChanged`, `on<Type>`, `onPaymentFailed`…, `onUnknownEvent`, `after`) is an
105
+ `AccountState` — if the state cannot be read, 503 is returned and no hook runs; `after`'s
106
+ `handled` is `"received"`. With `refetch: false`, or in a receiver without `steward`, the cause
107
+ hooks, `onUnknownEvent` and `after` get `AccountState | null` (a queue from before the upgrade;
108
+ `onStateChanged` still runs with full state). The distinction is inferred from the types of the
109
+ `refetch` and `steward` fields (when the type is ambiguous, the nullable one).
110
+
111
+ **The SDK does not deduplicate, and holds no version gate or lock.** With `refetch`, every
112
+ delivery reads the state live from steward: an out-of-order or repeated delivery sees the most
113
+ current state, and even an old body carrying only a snapshot is fully usable
114
+ (`event.data.account` may be stale; use `state`). The same event can still arrive several
115
+ times, and deliveries for the same account can arrive concurrently: hooks must write
116
+ idempotently (write the plan field from the state, "if already suspended, don't touch",
117
+ `coalesce` the termination flag). To have a delivery retried, throw from a hook → 503.
118
+ `refetch: false` uses the state in the body (no ordering guarantee; wait for the old queue to
119
+ drain). An endpoint that asks for both the old type and `account.state_changed` for the same
120
+ occurrence runs the cause hooks twice: pick one in the endpoint filter.
121
+
122
+ Hosted checkout (`/server`) — the profile, tax identity, consents and the payment form all live
123
+ on steward's page; the product only opens the session, redirects the user to `url`, and asks
124
+ for the result on return:
121
125
 
122
126
  ```ts
123
127
  import { Checkout } from "@stewardhq/sdk/server";
124
128
 
125
129
  const checkout = Checkout({
126
130
  steward, cache: state,
127
- // mutlak; origin steward ayarında izinli (appUrl + extraReturnOrigins). Yer tutucular varsa
128
- // steward yalnızca onları doldurur; yoksa adres aynen kullanılır (parametre eklenmez).
131
+ // absolute; the origin must be allowed in steward's settings (appUrl + extraReturnOrigins). If
132
+ // placeholders are present steward fills only those; otherwise the address is used as-is (no parameters added).
129
133
  successUrl: `${process.env.APP_URL}/{LOCALE}/app/orgs/{ACCOUNT_REF}/billing?checkout={CHECKOUT_ID}`,
130
- consents: (locale) => legalDocs(locale), // [{document, version, url}] ya da dizi; sayfada onaylatılır
131
- // cancelUrl (yoksa ayar appUrl), locale ve currency (yoksa ürün ayarı)
134
+ consents: (locale) => legalDocs(locale), // [{document, version, url}] or an array; consented to on the page
135
+ // cancelUrl (defaults to the appUrl setting), locale and currency (default to the product setting)
132
136
  });
133
137
 
134
138
  app.post("/v1/orgs/:orgId/billing/checkout", async (c) => {
135
- if (!(await isOwner(c))) return c.json({ error: "forbidden" }, 403); // yetki ÇAĞRIDAN ÖNCE ürünün işi
139
+ if (!(await isOwner(c))) return c.json({ error: "forbidden" }, 403); // authorization BEFORE the call is the product's job
136
140
  const { url } = await checkout.create({ ref: c.req.param("orgId"), planCode: "team", interval: "month",
137
141
  locale: c.get("locale"), actorRef: `user:${c.get("user").id}` }); // displayName?, prefill?: {email, name, kind}
138
- return c.json({ url }); // tarayıcı 303 url
142
+ return c.json({ url }); // the browser 303s to url
139
143
  });
140
144
 
141
- app.get("/v1/orgs/:orgId/billing/checkout/:id", async (c) => // dönüş sayfası ?checkout=<id>
145
+ app.get("/v1/orgs/:orgId/billing/checkout/:id", async (c) => // the return page ?checkout=<id>
142
146
  c.json(await checkout.result(c.req.param("id"), { ref: c.req.param("orgId") })));
143
147
  // { status: "open" | "completed" | "failed" | "expired" | "canceled", planCode?, account? }
144
148
  ```
145
149
 
146
- | Durum | Davranış |
150
+ | Situation | Behaviour |
147
151
  | --- | --- |
148
- | `create` | `POST …/checkout-sessions` hosted gövdeyle (profil/kimlik yok); sağlayıcıya çağrı yok; `{id, url, expiresAt}`. Hesap yoksa `displayName` ile açılır, o da yoksa `account_not_found`. Steward hataları aynen (`already_subscribed` 409, `plan_not_sellable` 422, `return_url_not_allowed` → `httpStatusFor` 500 + ALARM) |
149
- | `result`, oturum başka hesabın / id geçersiz / oturum yok | Aynı hata: `StewardApiError` `checkout_session_not_found` (404) — `?checkout=` kullanıcı girdisidir, sahiplik ele verilmez |
150
- | `result`, `completed` | Yanıttaki `account` `cache.prime` edilir (dönüş sayfası webhook'u beklemeden yeni planı gösterir; webhook yine kaynak) |
151
- | `locale` | `CustomerPortal` ile aynı kural: BCP-47 benzeri etiketin birincil alt etiketi (`tr-TR` → `tr`, `en-US` → `en`). Çağrıdaki desteklenmeyen dil yok sayılır: seçenekteki dil, o da yoksa gönderilmez (steward `defaultLocale`); `consents` işlevi bu dille çağrılır |
152
- | Göreli/http(s) olmayan `successUrl`, `cancelUrl`, belge adresi; seçenekte desteklenmeyen `locale` | Oluşturmada `StewardConfigError` |
153
- | Yanıtta `url` yok (F2b öncesi steward) | `StewardContractError` |
154
-
155
- Kullanıcı sayfayı kapatırsa oturum süresi dolunca (`checkoutTtlMinutes`) steward kapatır;
156
- ürün sonucu `account.state_changed` ile öğrenir. `steward.checkoutSessions.create` de yalnızca
157
- hosted gövdeyi kabul eder; fatura profili yalnızca hosted checkout ve portal sayfalarında yazılır.
158
-
159
- Hosted müşteri portalı (`/server`) — abonelik durumu, dönem sonunda iptal, ödemeyi yeniden dene,
160
- fatura listesi/PDF ve kurumsal fatura profili steward'ın sayfasında; ürün yalnızca yetkiyi
161
- denetler, oturumu açar ve yönlendirir. Ürün tarafında iptal/yeniden dene/fatura ucu yoktur:
162
- sonuçlar `account.state_changed` ile `Webhooks()`'a gelir:
152
+ | `create` | `POST …/checkout-sessions` with the hosted body (no profile or identity); no call to the provider; `{id, url, expiresAt}`. If the account does not exist it is opened with `displayName`, and without that, `account_not_found`. Steward's errors pass through (`already_subscribed` 409, `plan_not_sellable` 422, `return_url_not_allowed` → `httpStatusFor` 500 + ALARM) |
153
+ | `result`, session belongs to another account / invalid id / no session | The same error: `StewardApiError` `checkout_session_not_found` (404) — `?checkout=` is user input, so ownership is not leaked |
154
+ | `result`, `completed` | The `account` in the response is `cache.prime`d (the return page shows the new plan without waiting for the webhook; the webhook is still the source of truth) |
155
+ | `locale` | Same rule as `CustomerPortal`: the primary subtag of a BCP-47-like tag (`tr-TR` → `tr`, `en-US` → `en`). An unsupported language in the call is ignored: the one from the options is used, and failing that nothing is sent (steward's `defaultLocale`); the `consents` function is called with that language |
156
+ | A relative or non-http(s) `successUrl`, `cancelUrl` or document address; an unsupported `locale` in the options | `StewardConfigError` at construction |
157
+ | No `url` in the response (a steward from before F2b) | `StewardContractError` |
158
+
159
+ If the user closes the page, steward closes the session once it expires
160
+ (`checkoutTtlMinutes`), and the product learns the result from `account.state_changed`.
161
+ `steward.checkoutSessions.create` also only accepts the hosted body; the billing profile is
162
+ written only on the hosted checkout and portal pages.
163
+
164
+ Hosted customer portal (`/server`) — subscription status, cancel at period end, retry payment,
165
+ the invoice list/PDF and the corporate billing profile all live on steward's page; the product
166
+ only checks authorization, opens the session and redirects. There is no cancel, retry or
167
+ invoice endpoint on the product side: the results arrive at `Webhooks()` as
168
+ `account.state_changed`:
163
169
 
164
170
  ```ts
165
171
  import { CustomerPortal } from "@stewardhq/sdk/server";
166
172
 
167
- const portal = CustomerPortal({ steward, returnUrl: `${process.env.APP_URL}/billing` }); // returnUrl yoksa ayar appUrl
173
+ const portal = CustomerPortal({ steward, returnUrl: `${process.env.APP_URL}/billing` }); // without returnUrl, the appUrl setting
168
174
 
169
175
  app.post("/v1/orgs/:orgId/billing/portal", async (c) => {
170
- if (!(await isOwner(c))) return c.json({ error: "forbidden" }, 403); // yetki ÇAĞRIDAN ÖNCE ürünün işi
176
+ if (!(await isOwner(c))) return c.json({ error: "forbidden" }, 403); // authorization BEFORE the call is the product's job
171
177
  const { url } = await portal.create({ ref: c.req.param("orgId"), locale: c.get("locale"), actorRef: `user:${c.get("user").id}` });
172
- return c.json({ url }); // tarayıcı 303 url
178
+ return c.json({ url }); // the browser 303s to url
173
179
  });
174
180
  ```
175
181
 
176
- | Durum | Davranış |
182
+ | Situation | Behaviour |
177
183
  | --- | --- |
178
- | `create` | `POST …/portal-sessions {locale?, returnUrl?}` → `{id, url, expiresAt}`; durum değişmez, event yok. Süre ürün ayarı `portalTtlMinutes` (varsayılan 30 dk); süresi geçen ya da anahtarı yanlış bağlantı 404 |
179
- | Hesap yok | `StewardApiError` `account_not_found` (404) — checkout'tan farklı olarak hesap açılmaz |
180
- | `returnUrl` origin'i izinli değil | `return_url_not_allowed` (422; `httpStatusFor` 500 + ALARM) |
181
- | `locale` | `tr`/`en` (ör. `tr-TR` → `tr`; `Checkout` ile aynı kural); başka dil gönderilmez, steward `defaultLocale`'i kullanır |
182
- | `actorRef` eksik/biçimsiz, göreli ya da http(s) olmayan `returnUrl` | `StewardConfigError` (istek gitmez) |
183
- | Portal aksiyonları | Aktör steward denetim izinde `customer:<ref>`; iptal `subscription.cancel_scheduled` (neden `customer_portal`), profil `billing_profile.updated`, yeniden dene durum değiştirmez (tahsilat sonucu `subscription.reactivated`) |
184
+ | `create` | `POST …/portal-sessions {locale?, returnUrl?}` → `{id, url, expiresAt}`; state does not change and no event is emitted. The lifetime is the product setting `portalTtlMinutes` (30 minutes by default); an expired link, or one with the wrong key, gives 404 |
185
+ | No account | `StewardApiError` `account_not_found` (404) — unlike checkout, no account is opened |
186
+ | The `returnUrl` origin is not allowed | `return_url_not_allowed` (422; `httpStatusFor` 500 + ALARM) |
187
+ | `locale` | `tr`/`en` (e.g. `tr-TR` → `tr`; the same rule as `Checkout`); any other language is not sent and steward uses its `defaultLocale` |
188
+ | Missing or malformed `actorRef`, or a relative / non-http(s) `returnUrl` | `StewardConfigError` (no request is sent) |
189
+ | Portal actions | The actor in steward's audit trail is `customer:<ref>`; a cancellation is `subscription.cancel_scheduled` (reason `customer_portal`), a profile change is `billing_profile.updated`, and a retry does not change state (the collection result is `subscription.reactivated`) |
184
190
 
185
- Düşük seviyede imza ham gövde üzerinde doğrulanır, sonra sözleşmeyle parse edilir:
191
+ At the low level the signature is verified over the raw body and only then parsed with the
192
+ contract:
186
193
 
187
194
  ```ts
188
195
  import { verifyWebhook } from "@stewardhq/sdk/webhook";
@@ -190,93 +197,100 @@ import { verifyWebhook } from "@stewardhq/sdk/webhook";
190
197
  const rawBody = await request.text();
191
198
  const result = verifyWebhook({ secrets: [process.env.STEWARD_WEBHOOK_SECRET!], headers: request.headers, rawBody });
192
199
  if (!result.ok) return new Response(null, { status: 400 });
193
- // result.event: BillingEvent — tekrarı `id` ile ayıkla, eski `version`'ı uygulama.
200
+ // result.event: BillingEvent — deduplicate on `id`, and do not apply an older `version`.
194
201
  ```
195
202
 
196
- API istemcisi — `createSteward()` ayarları seçeneklerden, yoksa ortamdan okur
197
- (`STEWARD_URL`, `STEWARD_API_KEY`; edge'de `env` verilir) ve eksikse **açılışta**
198
- `StewardConfigError` fırlatır (yalnızca admin ayarlı istemci hariç, aşağıda). Başarısız çağrı fırlatır:
203
+ API client — `createSteward()` reads its settings from the options and otherwise from the
204
+ environment (`STEWARD_URL`, `STEWARD_API_KEY`; on the edge, pass `env`), and throws
205
+ `StewardConfigError` **at construction** if anything is missing (except for the admin-only
206
+ client, below). A failed call throws:
199
207
 
200
208
  ```ts
201
209
  import { createSteward } from "@stewardhq/sdk";
202
210
 
203
- export const steward = createSteward({ actorRef: "system" }); // ya da createSteward({ env, fetch, timeoutMs: 10_000 })
211
+ export const steward = createSteward({ actorRef: "system" }); // or createSteward({ env, fetch, timeoutMs: 10_000 })
204
212
 
205
213
  const state = await steward.accounts.state(accountRef); // AccountState
206
- await steward.grants.create(accountRef, { planCode: "team", reason: "kurumsal sözleşme" }, { actorRef: `staff:${staffId}` });
214
+ await steward.grants.create(accountRef, { planCode: "team", reason: "enterprise agreement" }, { actorRef: `staff:${staffId}` });
207
215
  ```
208
216
 
209
- | Kaynak | Metotlar |
217
+ | Resource | Methods |
210
218
  | --- | --- |
211
- | `accounts` | `upsert(ref, input)`, `get(ref, {include?})`, `state(ref)`, `readState(ref, {etag?})` (304 → `{notModified: true}`), `setPlan(ref, {planCode \| null, reason, mode?, endsAt?, overrides?})` → `{grant, account, warnings}` (canlı abonelik planı hedefi geçiyorsa `mode: "ensure"` uyarı, `"strict"` `subscription_plan_higher`), `resync(ref)` → `{queued, eventId}` (saklı durum `account.state_changed`, `cause: "resync"`), `delete(ref)` → `{ref, deletedAt}` (KVKK anonimleştirme; faturalar kalır; önce canlı abonelik `immediate` iptal — yoksa `subscription_active`, formu açık checkout'ta `checkout_in_progress`; sonra hesabın her ucu `account_deleted` 410) |
219
+ | `accounts` | `upsert(ref, input)`, `get(ref, {include?})`, `state(ref)`, `readState(ref, {etag?})` (304 → `{notModified: true}`), `setPlan(ref, {planCode \| null, reason, mode?, endsAt?, overrides?})` → `{grant, account, warnings}` (entitlements are a union of layers — there is no `rank`; M35: `mode` is accepted and **ignored**, and `warnings` is always empty), `resync(ref)` → `{queued, eventId}` (the stored state as `account.state_changed`, `cause: "resync"`), `delete(ref)` → `{ref, deletedAt}` (KVKK anonymisation; invoices remain; any live subscription must be cancelled `immediate` first — otherwise `subscription_active`, or `checkout_in_progress` while a checkout form is open; afterwards every endpoint for the account returns `account_deleted` 410) |
212
220
  | `checkoutSessions` | `create(ref, input)`, `get(id)` |
213
- | `portalSessions` | `create(ref, {locale?, returnUrl?})` → `{id, url, expiresAt}` (hosted müşteri portalı; `CustomerPortal()` bunu sarar) |
221
+ | `portalSessions` | `create(ref, {locale?, returnUrl?})` → `{id, url, expiresAt}` (the hosted customer portal; `CustomerPortal()` wraps this) |
214
222
  | `subscriptions` | `cancel(id, input)` |
215
223
  | `grants` | `create(ref, input)`, `revoke(id)` |
216
- | `invoices` | `issue(id, input)`, `document(id)` → `{contentType, bytes}`, `createManual(ref, {lines, currency, taxRateBps, taxInclusive?, dueAt?, note?})` (manuel taslak; profil gerekir, `issue` ile kesilir), `void(id, {reason})` (yalnızca manuel fatura; `invoice.voided` yalnızca filtresinde isteyen endpoint'e) |
224
+ | `invoices` | `issue(id, input)`, `document(id)` → `{contentType, bytes}`, `createManual(ref, {lines, currency, taxRateBps, taxInclusive?, dueAt?, note?})` (a manual draft; needs a profile, and is issued with `issue`), `void(id, {reason})` (manual invoices only; `invoice.voided` goes only to an endpoint that asks for it in its filter) |
217
225
  | `catalog` | `get()`, `read({etag?})` |
218
- | — | `stats()`, `me()`, `doctor()` → kurulum teşhisi (`GET /v1/doctor`; aşağıda) |
219
- | `admin` | `catalog.sync(input)`, `accounts.refresh()`, `settings.{get(), update(patch)}`, `endpoints.{list(), create(input), rotate(id, {secret}), deactivate(id)}` — `STEWARD_ADMIN_URL` + `STEWARD_ADMIN_API_KEY` (ya da `adminBaseUrl`/`adminApiKey`) ister; yoksa çağrıda `StewardConfigError` |
220
-
221
- - **Mutasyonlar** son argümanda `{ idempotencyKey?, actorRef? }` alır. Anahtar
222
- verilmezse çağrı başına üretilir; `actorRef` verilmezse `createSteward`'daki varsayılan gider.
223
- İkisi de yoksa ya da biçimi geçersizse (`tür[:kimlik]`) istek **gitmez**: `StewardConfigError`
224
- (`code` servisinkiyle aynı: `actor_ref_required` / `invalid_actor_ref`).
225
- - **Yalnızca yönetim:** `STEWARD_URL` ve `STEWARD_API_KEY` ikisi de yoksa ve `STEWARD_ADMIN_URL` +
226
- `STEWARD_ADMIN_API_KEY` (ya da `adminBaseUrl`/`adminApiKey`) verilmişse istemci kurulur (CLI, katalog
227
- senkronu): `admin.*` çalışır, iki kapsama açık `doctor()` ve `me()` admin anahtarıyla gider, ürün uçları
228
- ve `raw.*` çağrıda `StewardConfigError` ile reddeder (istek gitmez). Ürün ayarının yalnızca yarısı
229
- verilmişse yine açılışta hata.
230
- - **Hatalar:** yanıtlı hata `StewardApiError` (`code`, `httpStatus`, `category`,
231
- `retryable`, `requestId`, `retryAfterMs`), yanıt yok `StewardNetworkError`
232
- (`network_error`/`timeout`), şemaya uymayan yanıt `StewardContractError`.
233
- - **Tekrar deneme** (`retries: { attempts: 2, on: ["network", "5xx", "429"], maxDelayMs: 5000 }`):
234
- ağ hatası/zaman aşımı, 5xx ve 429'da GET'ler ve `Idempotency-Key` taşıyan mutasyonlar
235
- en çok `attempts` kez daha (varsayılan en çok 3 istek) denenir; anahtar bütün denemelerde
236
- aynıdır. Bekleme `Retry-After` kadar, yoksa üstel (250 ms, 500 ms…) + jitter; ikisi de
237
- `maxDelayMs` ile sınırlı. 429 dışındaki 4xx ve `retryable: false` yanıtlar
238
- (`provider_error`, `provider_not_configured`) denenmez. `timeoutMs` deneme başınadır.
239
- - İstekler `Steward-Api-Version` ve `X-Steward-Sdk: @stewardhq/sdk/<sürüm>` taşır (yalnızca gözlem).
240
- - Yalnızca web standartları (`fetch`, `crypto.randomUUID`, `AbortSignal.timeout`): Node 22,
241
- edge ve workerd'de aynı kod. Kök giriş ve `./webhook/web` her paket kontrolünde
242
- workerd'de `nodejs_compat` kapalı koşturulur (`scripts/check-workerd.mjs`).
243
-
244
- Yönetim (admin anahtarı; cluster içi, port-forward) — ürün ayarları ve webhook endpoint'leri.
245
- Webhook sırrını **çağıran üretir** ve kendi Secret'ına yazar; steward sırrı hiçbir yanıtta
246
- geri vermez:
226
+ | — | `stats()`, `me()`, `doctor()` → setup diagnostics (`GET /v1/doctor`; below) |
227
+ | `admin` | `catalog.sync(input)`, `accounts.refresh()`, `settings.{get(), update(patch)}`, `endpoints.{list(), create(input), rotate(id, {secret}), deactivate(id)}` — requires `STEWARD_ADMIN_URL` + `STEWARD_ADMIN_API_KEY` (or `adminBaseUrl`/`adminApiKey`); without them, `StewardConfigError` at the call |
228
+
229
+ - **Mutations** take `{ idempotencyKey?, actorRef? }` as their last argument. Without a key,
230
+ one is generated per call; without `actorRef`, the default from `createSteward` is used. If
231
+ neither exists, or the format is invalid (`type[:id]`), the request is **not sent**:
232
+ `StewardConfigError` (with the same `code` as the service's: `actor_ref_required` /
233
+ `invalid_actor_ref`).
234
+ - **Admin only:** when neither `STEWARD_URL` nor `STEWARD_API_KEY` is set but
235
+ `STEWARD_ADMIN_URL` + `STEWARD_ADMIN_API_KEY` (or `adminBaseUrl`/`adminApiKey`) are, the
236
+ client is constructed (for the CLI and catalog sync): `admin.*` works, `doctor()` and `me()`
237
+ — open to both scopes — go out with the admin key, and the product endpoints and `raw.*`
238
+ reject at the call with `StewardConfigError` (no request is sent). If only half of the
239
+ product setting is given, it still fails at construction.
240
+ - **Errors:** an error with a response is `StewardApiError` (`code`, `httpStatus`, `category`,
241
+ `retryable`, `requestId`, `retryAfterMs`), no response is `StewardNetworkError`
242
+ (`network_error`/`timeout`), and a response that does not match the schema is
243
+ `StewardContractError`.
244
+ - **Retries** (`retries: { attempts: 2, on: ["network", "5xx", "429"], maxDelayMs: 5000 }`): on
245
+ a network error/timeout, a 5xx or a 429, GETs and mutations carrying an `Idempotency-Key` are
246
+ retried at most `attempts` more times (by default at most 3 requests), with the same key on
247
+ every attempt. The wait is `Retry-After` when given, otherwise exponential (250 ms,
248
+ 500 ms…) + jitter; both are capped by `maxDelayMs`. 4xx other than 429, and responses with
249
+ `retryable: false` (`provider_error`, `provider_not_configured`), are not retried.
250
+ `timeoutMs` is per attempt.
251
+ - Requests carry `Steward-Api-Version` and `X-Steward-Sdk: @stewardhq/sdk/<version>`
252
+ (observability only).
253
+ - Web standards only (`fetch`, `crypto.randomUUID`, `AbortSignal.timeout`): the same code on
254
+ Node 22, on the edge and on workerd. The root entry and `./webhook/web` are run on workerd
255
+ with `nodejs_compat` disabled on every pack check (`scripts/check-workerd.mjs`).
256
+
257
+ Administration (admin key; in-cluster, port-forward) — product settings and webhook endpoints.
258
+ **The caller generates** the webhook secret and writes it into their own Secret; steward never
259
+ returns the secret in any response:
247
260
 
248
261
  ```ts
249
262
  import { generateWebhookSecret } from "@stewardhq/sdk";
250
263
 
251
264
  await steward.admin.settings.update({
252
- appUrl: "https://app.example.com", // origin'i checkout dönüşüne izinli; + extraReturnOrigins
265
+ appUrl: "https://app.example.com", // its origin is allowed for the checkout return; + extraReturnOrigins
253
266
  privacyUrl: "https://app.example.com/kvkk",
254
- branding: { name: "Acme", accentColor: "#0f766e" }, // sığ birleşir; alanı null temizler
267
+ branding: { name: "Acme", accentColor: "#0f766e" }, // shallow merge; null clears a field
255
268
  checkoutTtlMinutes: 30,
256
269
  });
257
270
 
258
- const secret = generateWebhookSecret(); // whsec_ + 32 rastgele bayt
271
+ const secret = generateWebhookSecret(); // whsec_ + 32 random bytes
259
272
  const endpoint = await steward.admin.endpoints.create({ url: "http://api.acme.svc.cluster.local/billing/events", secret });
260
- // eventTypes verilmezse ["account.state_changed"]; adres https:// ya da http://*.svc.cluster.local
261
- await steward.admin.endpoints.rotate(endpoint.id, { secret: generateWebhookSecret() }); // eski sır geçiş için ikinci imza
273
+ // without eventTypes, ["account.state_changed"]; the address must be https:// or http://*.svc.cluster.local
274
+ await steward.admin.endpoints.rotate(endpoint.id, { secret: generateWebhookSecret() }); // the old secret stays as a second signature during the transition
262
275
  ```
263
276
 
264
- Kurulum teşhisi — ürün anahtarı yeter; ilk checkout'tan önce `ok: true` beklenir:
277
+ Setup diagnostics — the product key is enough; expect `ok: true` before the first checkout:
265
278
 
266
279
  ```ts
267
280
  const report = await steward.doctor();
268
- // report.ok: `error` önemde uyarı yok
281
+ // report.ok: no warning at `error` severity
269
282
  // report.warnings: [{ code: "endpoint_wrong_secret", severity: "error", message, endpointId }, ...]
270
- // report.endpoints[].diagnosis: ok | wrong_secret (son teslimat 401) | old_contract (400) | unreachable | failing | no_deliveries
283
+ // report.endpoints[].diagnosis: ok | wrong_secret (last delivery 401) | old_contract (400) | unreachable | failing | no_deliveries
271
284
  // report.catalog, report.provider (environment: sandbox | live | fake), report.hosted.ready, report.dunning.dryRun
272
285
  ```
273
286
 
274
- Kodlar ve teşhisler açık kümedir (bilinmeyeni yalnızca gösterin). `provider.healthy` ve
275
- `lastError` yanıtı veren steward pod'unun gözlemidir.
287
+ The codes and diagnoses are an open set (display an unknown one, nothing more).
288
+ `provider.healthy` and `lastError` are the observation of the steward pod that answered.
276
289
 
277
- Katalog — kodda tipli tanım; eksik/fazla hak, yanlış tip (`limit` → tam sayı ya da
278
- `null` = sınırsız, `flag` → boolean, `text` → string) ve tanımsız plana fiyat derleme
279
- hatasıdır, tipler atlatılırsa tanım anında `StewardConfigError` fırlatır (TypeScript ≥ 5.4):
290
+ Catalog — a typed definition in code; a missing or extra entitlement, a wrong type (`limit` →
291
+ an integer or `null` = unlimited, `flag` → boolean, `text` → string) and a price for an
292
+ undefined plan are compile errors, and if the types are bypassed the definition throws
293
+ `StewardConfigError` immediately (TypeScript ≥ 5.4):
280
294
 
281
295
  ```ts
282
296
  import { defineCatalog, flag, limit } from "@stewardhq/sdk";
@@ -291,16 +305,17 @@ export const catalog = defineCatalog({
291
305
  prices: [{ plan: "team", interval: "month", currency: "TRY", amountMinor: 125_000, taxInclusive: true, taxRateBps: 2000 }],
292
306
  });
293
307
 
294
- await steward.admin.catalog.sync(catalog.toSyncInput()); // admin anahtarıyla (CLI/deploy adımı)
308
+ await steward.admin.catalog.sync(catalog.toSyncInput()); // with the admin key (a CLI/deploy step)
295
309
  const e = catalog.resolve(snapshot); // { max_projects: number | null; sso: boolean }
296
310
  const placeholder = catalog.stateOf("org_1", "team", { accessState: "active", displayName: "Acme" });
297
- // tam AccountState: version 0, updatedAt 1970, catalogVersion null, haklar planınki, abonelik/açık checkout yok,
298
- // profil { present: false }; plan verilmezse varsayılan plan (stateCache fallback'i için)
311
+ // a full AccountState: version 0, updatedAt 1970, catalogVersion null, the plan's entitlements, no subscription or open checkout,
312
+ // profile { present: false }; without a plan, the default plan (for a stateCache fallback)
299
313
  ```
300
314
 
301
- `resolve` eksik ya da tipi uymayan anahtarda değeri snapshot'ın planından (plan
302
- katalogda yoksa varsayılan plandan) alır, asla "sınırsız" üretmez; `onFallback`
303
- ile alarm verilebilir. Snapshot yoksa varsayılan planın hakları.
315
+ For a missing key or one whose type does not match, `resolve` takes the value from the
316
+ snapshot's plan (or, if that plan is not in the catalog, from the default plan) and never
317
+ produces "unlimited"; `onFallback` can raise an alarm. Without a snapshot, the default plan's
318
+ entitlements.
304
319
 
305
320
  Benefits (billing core) — reusable definitions plans list by code; `rank` is optional (the
306
321
  benefits resolution does not read it). A `featureFlag()` sets catalog features with their
@@ -397,7 +412,7 @@ process.once("SIGTERM", () => void events.flush()); // the SDK never hooks pro
397
412
  `steward.credits.grant`, `steward.meterCharges.list` are the raw calls. Push a catalog with
398
413
  meters only to a steward whose `me().capabilities` lists `"meters"` (an older one drops them).
399
414
 
400
- Hatalar — ürün API'sinin durum kodu:
415
+ Errors — the product API's status code:
401
416
 
402
417
  ```ts
403
418
  import { httpStatusFor, StewardError } from "@stewardhq/sdk";
@@ -408,34 +423,42 @@ if (error instanceof StewardError) {
408
423
  }
409
424
  ```
410
425
 
411
- Tabloda olmayan kod `category`'ye göre eşlenir (`not_found` 404, `conflict` 409,
412
- `provider` 502, `unavailable` 503; `validation`/`config` 500 + ERROR log).
413
- Kullanıcıya gösterilen metin ürünündür (`error.code` → kendi i18n'iniz); `error.message`
414
- log içindir (steward'ın mesajı, yoksa kod). Fiyat gösterimi hosted checkout/portal
415
- sayfalarındadır; SDK para biçimlendirme yardımcısı taşımaz.
426
+ A code that is not in the table is mapped by `category` (`not_found` 404, `conflict` 409,
427
+ `provider` 502, `unavailable` 503; `validation`/`config` 500 + an ERROR log). The text shown to
428
+ the user is the product's (`error.code` → your own i18n); `error.message` is for logs
429
+ (steward's message, or the code if there is none). Price display lives on the hosted checkout
430
+ and portal pages; the SDK carries no currency formatting helper.
416
431
 
417
- Doğrulayıcılar: `isValidTckn`, `isValidVkn` (sağlama hanesi), `isE164(value, {country: "TR"}?)`
418
- değeri olduğu gibi denetler (boşluk kırpmaz).
432
+ Validators: `isValidTckn`, `isValidVkn` (checksum digit) and `isE164(value, {country: "TR"}?)`
433
+ check the value as-is (no whitespace trimming).
419
434
 
420
- ## `@steward/contract`'tan geçiş
435
+ ## Removed APIs
421
436
 
422
- | Eski | Yeni |
437
+ | Old | New |
423
438
  | --- | --- |
424
- | `@z9cloud/steward-contract` | `@stewardhq/sdk` (ya da yalnız şema için `@stewardhq/sdk/contract`) |
425
- | `@z9cloud/steward-contract/client` | `@stewardhq/sdk` `createSteward` (fırlatan; fırlatmayan `createBillingClient` dışa açılmaz) |
426
- | `@z9cloud/steward-contract/webhook` | `@stewardhq/sdk/webhook` |
427
- | `@z9cloud/steward-contract/fixtures/events/*.json` | Yok (paketle yayınlanmaz) |
428
- | `@stewardhq/sdk/testing` (`signedEvent`, `createFakeSteward`) | Yok (0.4.0): servise karşı entegrasyon testi ya da `fetch` saplaması |
429
- | `getEntitlements(ref)` / `accounts.entitlements(ref)` | `accounts.state(ref)` ya da `stateCache` (tek okuma modeli `AccountState`; uç 2026-09-16'da kaldırıldı) |
430
- | `listEvents` / `redeliverEvent` / `events.*` | Yok (2026-09-16): kaçırılan durum için `accounts.resync(ref)`; dead teslimatı operatör pod CLI `redeliver-event` ile yeniden gönderir |
431
-
432
- GitHub Packages `.npmrc` satırı ve Docker `--secret` bağlaması artık gerekmez.
433
-
434
- ## Sürümleme
435
-
436
- Semver. Alan kaldırma/yeniden adlandırma kırıcıdır (major; 1.0 öncesi minor).
437
- Yayın: `sdk/package.json` sürümü artırılır, `sdk-v<sürüm>` etiketi push edilir
438
- (`.github/workflows/sdk-publish.yml`).
439
+ | `@stewardhq/sdk/testing` (`signedEvent`, `createFakeSteward`) | Gone (0.4.0): write integration tests against the service, or stub `fetch` |
440
+ | `getEntitlements(ref)` / `accounts.entitlements(ref)` | `accounts.state(ref)` or `stateCache` (the single read model `AccountState`; the endpoint was removed on 2026-09-16) |
441
+ | `listEvents` / `redeliverEvent` / `events.*` | Gone (2026-09-16): for missed state use `accounts.resync(ref)`; an operator redelivers a dead delivery with the pod CLI's `redeliver-event` |
442
+
443
+ ## Versioning
444
+
445
+ Semver. Removing or renaming a field is breaking (major; before 1.0, minor). Release: bump the
446
+ version in `sdk/package.json` and push the `sdk-v<version>` tag
447
+ (`.github/workflows/sdk-publish.yml`). **The first version on npmjs is 0.5.0** (2026-09-21);
448
+ 0.4.0 stayed inside the repository and was never published.
449
+
450
+ **0.6.0** (contract 0.5.0, bank transfer F1; additive; `API_VERSION` `2026-10-01` unchanged):
451
+
452
+ - `Checkout().create({ replacesSubscriptionId })`: an upgrade that replaces the live subscription only
453
+ when the payment is recorded (do not cancel it before the checkout); `result()` adds `paymentMethod`.
454
+ - State: `openCheckout.paymentMethod` (`bank_transfer` while the order waits for a transfer) and
455
+ `bankTransferCode`; `subscription.activated` detail `replacedSubscriptionId`; `CheckoutSession`
456
+ `paymentMethod` / `replacesSubscriptionId`.
457
+ - Settings: `billing.bankTransfer` (`enabled`, `paymentDays`); email template `bank_transfer_instructions`.
458
+ - Admin: `BankTransferRecordInputSchema` / `BankTransferRecordedSchema` (`POST /v1/admin/bank-transfers`;
459
+ a transfer completes a checkout or pays an unpaid invoice, `checkoutSessionId` / `invoiceId`);
460
+ error codes `replaces_subscription_invalid`, `bank_transfer_*`.
461
+ - State change cause `bank_transfer.code_assigned` (the account chose bank transfer on the invoice pay page).
439
462
 
440
463
  **0.5.0** (contract 0.4.0, metering phase 2; additive; `API_VERSION` `2026-10-01` unchanged):
441
464
 
@@ -446,16 +469,22 @@ Yayın: `sdk/package.json` sürümü artırılır, `sdk-v<sürüm>` etiketi push
446
469
  - Client: `events.ingest`, `meters.get/read/periods`, `credits.grant`, `meterCharges.list`.
447
470
  - Hooks: `onMeterThreshold`, `onMeterPeriodClosed` (+ `period`), `onBenefitCycled`.
448
471
 
449
- **0.4.0** (sözleşme 0.3.0, faturalama çekirdeği Faz 1; `API_VERSION` `2026-10-01` aynı):
450
-
451
- - Kırıcı (0.x minor): `@stewardhq/sdk/testing` alt yolu (`signedEvent`, fixture'lar, sahte steward)
452
- kaldırıldı; testler gerçek servise ya da `fetch` saplamasına karşı yazılır.
453
- - Katalog: `benefits` + `featureFlag()`, `plans[].benefits`, `typeof catalog.$benefit`; `PlanSpec.rank`
454
- isteğe bağlı ve hak çözümünde okunmaz (katman birleşimi, sözleşmenin `resolveBenefits`'i).
455
- - Durum: `AccountState.benefits`, `subscriptions[]` (tekil `subscription` birincil abonelik),
456
- `openInvoices[]`; abonelikteki `amountMinor` aboneliğin kilitli tutarıdır.
457
- - Olaylar (endpoint'in `eventTypes`'ında istenirse): `invoice.paid`, `benefit_grant.created/updated/revoked`;
458
- hook'lar `onInvoicePaid`, `onBenefitGranted`, `onBenefitRevoked` (ve tipten türeyen
459
- `onBenefitGrantCreated/Updated/Revoked`).
460
- - İstemci: `invoices.createPaymentSession`, `invoices.waive`, `subscriptions.change`,
461
- `subscriptions.uncancel`; `me()` yanıtında `capabilities`.
472
+ **0.4.0** (contract 0.3.0, billing core phase 1; `API_VERSION` `2026-10-01` unchanged):
473
+
474
+ - Breaking (0.x minor): the `@stewardhq/sdk/testing` subpath (`signedEvent`, the fixtures, the
475
+ fake steward) was removed; tests are written against the real service or a `fetch` stub.
476
+ - Catalog: `benefits` + `featureFlag()`, `plans[].benefits`, `typeof catalog.$benefit`;
477
+ `PlanSpec.rank` is optional and is not read during entitlement resolution (a union of layers,
478
+ the contract's `resolveBenefits`).
479
+ - State: `AccountState.benefits`, `subscriptions[]` (the singular `subscription` is the primary
480
+ subscription), `openInvoices[]`; `amountMinor` on a subscription is the amount that
481
+ subscription locked.
482
+ - Events (when requested in the endpoint's `eventTypes`): `invoice.paid`,
483
+ `benefit_grant.created/updated/revoked`; the hooks `onInvoicePaid`, `onBenefitGranted`,
484
+ `onBenefitRevoked` (and `onBenefitGrantCreated/Updated/Revoked` derived from the type).
485
+ - Client: `invoices.createPaymentSession`, `invoices.waive`, `subscriptions.change`,
486
+ `subscriptions.uncancel`; `capabilities` in the `me()` response.
487
+
488
+ ## License
489
+
490
+ Apache License 2.0 — see [LICENSE](LICENSE).