@stewardhq/sdk 0.1.0-rc.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.
Files changed (48) hide show
  1. package/README.md +437 -0
  2. package/dist/_chunks/errors.js +73 -0
  3. package/dist/_chunks/events.d.ts +537 -0
  4. package/dist/_chunks/events.js +350 -0
  5. package/dist/_chunks/index.d.ts +6005 -0
  6. package/dist/_chunks/locale.d.ts +17 -0
  7. package/dist/_chunks/locale.js +17 -0
  8. package/dist/_chunks/src.js +1202 -0
  9. package/dist/_chunks/steward.d.ts +2588 -0
  10. package/dist/_chunks/steward.js +578 -0
  11. package/dist/_chunks/validators.d.ts +23 -0
  12. package/dist/_chunks/validators.js +106 -0
  13. package/dist/_chunks/webhook-core.d.ts +32 -0
  14. package/dist/_chunks/webhook-core.js +64 -0
  15. package/dist/_chunks/webhook.d.ts +22 -0
  16. package/dist/_chunks/webhook.js +53 -0
  17. package/dist/contract.d.ts +4 -0
  18. package/dist/contract.js +4 -0
  19. package/dist/index.d.ts +268 -0
  20. package/dist/index.js +784 -0
  21. package/dist/server.d.ts +236 -0
  22. package/dist/server.js +384 -0
  23. package/dist/testing/fixtures/events/LOCK.json +27 -0
  24. package/dist/testing/fixtures/events/account.deleted.json +36 -0
  25. package/dist/testing/fixtures/events/account.state_changed.json +53 -0
  26. package/dist/testing/fixtures/events/account.updated.json +37 -0
  27. package/dist/testing/fixtures/events/checkout.completed.json +36 -0
  28. package/dist/testing/fixtures/events/checkout.expired.json +26 -0
  29. package/dist/testing/fixtures/events/checkout.failed.json +27 -0
  30. package/dist/testing/fixtures/events/invoice.created.json +37 -0
  31. package/dist/testing/fixtures/events/invoice.issued.json +38 -0
  32. package/dist/testing/fixtures/events/invoice.voided.json +39 -0
  33. package/dist/testing/fixtures/events/subscription.activated.json +40 -0
  34. package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +38 -0
  35. package/dist/testing/fixtures/events/subscription.canceled.json +29 -0
  36. package/dist/testing/fixtures/events/subscription.expired.json +27 -0
  37. package/dist/testing/fixtures/events/subscription.payment_failed.json +38 -0
  38. package/dist/testing/fixtures/events/subscription.reactivated.json +36 -0
  39. package/dist/testing/fixtures/events/subscription.renewed.json +39 -0
  40. package/dist/testing/fixtures/events/subscription.suspended.json +36 -0
  41. package/dist/testing/fixtures/events/subscription.terminated.json +28 -0
  42. package/dist/testing.d.ts +629 -0
  43. package/dist/testing.js +3759 -0
  44. package/dist/webhook/web.d.ts +17 -0
  45. package/dist/webhook/web.js +66 -0
  46. package/dist/webhook.d.ts +3 -0
  47. package/dist/webhook.js +3 -0
  48. package/package.json +70 -0
@@ -0,0 +1,236 @@
1
+ import { Qn as CheckoutPrefill, Un as PortalSession, Wn as PortalSessionCreateInput, ei as Interval, lr as CheckoutStatus, or as CheckoutSessionCreated, qt as CheckoutSessionResponse, rr as CheckoutSessionCreateInput } from "./_chunks/index.js";
2
+ import { D as AccountInclude, F as AccountState, V as BillingProfile, a as BillingEvent, f as EventType, n as AnyBillingEvent, o as BillingEventOf } from "./_chunks/events.js";
3
+ import { i as MutationOptions } from "./_chunks/steward.js";
4
+ import { r as StewardLogger, t as StewardLocale } from "./_chunks/locale.js";
5
+ import { a as HeaderSource, s as VerifyRawFailure } from "./_chunks/webhook-core.js";
6
+ import "./_chunks/webhook.js";
7
+ //#region src/server/checkout.d.ts
8
+ /** Hosted sayfada onaylatılan belge (ürünün yasal metni). */
9
+ type ConsentDoc = {
10
+ document: string;
11
+ version: string;
12
+ url: string;
13
+ };
14
+ /** `createSteward()` sonucunun `Checkout`'un kullandığı kısmı. */
15
+ type CheckoutSteward = {
16
+ checkoutSessions: {
17
+ create(ref: string, input: CheckoutSessionCreateInput, options?: {
18
+ actorRef?: string;
19
+ idempotencyKey?: string;
20
+ }): Promise<CheckoutSessionCreated>;
21
+ get(id: string): Promise<CheckoutSessionResponse>;
22
+ };
23
+ };
24
+ /** Tamamlanan oturumun durumunu alan önbellek (`stateCache()` karşılar). */
25
+ type CheckoutCache = {
26
+ prime(account: AccountState): unknown;
27
+ };
28
+ type CheckoutOptions = {
29
+ steward: CheckoutSteward;
30
+ cache?: CheckoutCache;
31
+ /**
32
+ * Mutlak dönüş adresi; origin'i steward ürün ayarında izinli olmalı (`appUrl` +
33
+ * `extraReturnOrigins`). Yer tutucular `{CHECKOUT_ID}` `{STATUS}` `{ACCOUNT_REF}` `{LOCALE}`
34
+ * varsa steward yalnızca onları doldurur; yoksa `?session=&status=` ekler.
35
+ */
36
+ successUrl: string;
37
+ /** Sayfadaki "Vazgeç" hedefi (mutlak); verilmezse steward ayarı `appUrl`. */
38
+ cancelUrl?: string;
39
+ /** Onaylatılacak belgeler; dile göre değişiyorsa işlev. */
40
+ consents?: readonly ConsentDoc[] | ((locale: StewardLocale | undefined) => readonly ConsentDoc[]);
41
+ /** Varsayılan sayfa dili (`tr`, `en`; `tr-TR` → `tr`); verilmezse steward ayarı `defaultLocale`. */
42
+ locale?: string;
43
+ /** Verilmezse steward ayarı `defaultCurrency`. */
44
+ currency?: string;
45
+ };
46
+ type CheckoutCreateInput = {
47
+ /** Hesap ref'i (ürünün kimliği; ör. organizasyon id'si). */
48
+ ref: string;
49
+ planCode: string;
50
+ interval: Interval;
51
+ /** Sayfa dili, BCP-47 benzeri (`tr`, `tr-TR`, `en-US`); desteklenmeyen dil yok sayılır (seçenekteki dil ya da steward varsayılanı). */
52
+ locale?: string;
53
+ /** `X-Actor-Ref` (ör. `user:42`): satın almayı başlatan. */
54
+ actorRef: string;
55
+ /** Hesap steward'da yoksa bu adla açılır (varsa adı değişmez); verilmez ve hesap yoksa `account_not_found`. */
56
+ displayName?: string;
57
+ /** Sayfanın ön doldurması (ad, e-posta, tür); kimlik verisi değil (K21). */
58
+ prefill?: CheckoutPrefill;
59
+ };
60
+ type CheckoutCreated = {
61
+ id: string;
62
+ url: string;
63
+ expiresAt: string;
64
+ };
65
+ type CheckoutResult = {
66
+ status: CheckoutStatus;
67
+ planCode?: string;
68
+ /** Yalnızca `completed`: işlem sonrası hesap durumu (önbelleğe yazıldı). */
69
+ account?: AccountState;
70
+ };
71
+ type CheckoutHandler = {
72
+ create(input: CheckoutCreateInput): Promise<CheckoutCreated>;
73
+ result(id: string, options: {
74
+ ref: string;
75
+ }): Promise<CheckoutResult>;
76
+ };
77
+ export declare function Checkout(options: CheckoutOptions): CheckoutHandler;
78
+ //#endregion
79
+ //#region src/server/portal.d.ts
80
+ /** `createSteward()` sonucunun `CustomerPortal`'ın kullandığı kısmı. */
81
+ type CustomerPortalSteward = {
82
+ portalSessions: {
83
+ create(ref: string, input?: PortalSessionCreateInput, options?: MutationOptions): Promise<PortalSession>;
84
+ };
85
+ };
86
+ type CustomerPortalOptions = {
87
+ steward: CustomerPortalSteward;
88
+ /** Varsayılan "Ürüne dön" adresi (mutlak http(s)); verilmezse steward ürün ayarı `appUrl`. */
89
+ returnUrl?: string;
90
+ };
91
+ type CustomerPortalCreateArgs = {
92
+ /** Hesabın ürün tarafındaki kimliği (`accounts.external_ref`). */
93
+ ref: string;
94
+ /** Sayfa dili; `tr`/`en` dışı değer steward varsayılanına bırakılır. */
95
+ locale?: string;
96
+ returnUrl?: string;
97
+ /** Portalı açan kullanıcı, ör. `user:42`. */
98
+ actorRef: string;
99
+ idempotencyKey?: string;
100
+ };
101
+ type CustomerPortalClient = {
102
+ /** `POST /v1/accounts/{ref}/portal-sessions` → `{id, url, expiresAt}`; tarayıcı `url`e yönlendirilir. */
103
+ create(args: CustomerPortalCreateArgs): Promise<PortalSession>;
104
+ };
105
+ export declare function CustomerPortal(options: CustomerPortalOptions): CustomerPortalClient;
106
+ //#endregion
107
+ //#region src/server/webhooks.d.ts
108
+ /** Neden hook'u olan tipler: `account.state_changed` dışındaki her tip (neden olarak da gelirler). */
109
+ type CauseEventType = Exclude<EventType, "account.state_changed">;
110
+ type Pascal<S extends string> = S extends `${infer Head}_${infer Tail}` ? `${Capitalize<Head>}${Pascal<Tail>}` : Capitalize<S>;
111
+ /** `subscription.payment_failed` → `onSubscriptionPaymentFailed`. */
112
+ type EventHookName<T extends string> = T extends `${infer Scope}.${infer Name}` ? `on${Pascal<Scope>}${Pascal<Name>}` : `on${Pascal<T>}`;
113
+ type WebhookHandled = "received" | "no_state" | "ignored" | "deleted" | "ping";
114
+ type WebhookErrorCode = "not_configured" | VerifyRawFailure | "invalid_payload" | "id_mismatch" | "state_unavailable" | "profile_unavailable" | "hook_failed";
115
+ type WebhookResult = {
116
+ status: 200;
117
+ body: {
118
+ ok: true;
119
+ handled: WebhookHandled;
120
+ };
121
+ } | {
122
+ status: 400 | 401 | 503;
123
+ body: {
124
+ error: WebhookErrorCode;
125
+ };
126
+ };
127
+ type StateChangedArgs = {
128
+ ref: string;
129
+ state: AccountState;
130
+ /** `detail.cause` (state_changed) ya da eski/tanınmayan tipin adı; küme açık. */
131
+ cause: string;
132
+ event: BillingEvent | AnyBillingEvent;
133
+ };
134
+ /**
135
+ * Hook'lara giden durumun tipi: `AccountState` (refetch açık: durum her teslimatta canlı okunur)
136
+ * ya da `AccountState | null` (varsayılan; `refetch: false`, yalnızca snapshot taşıyan eski gövde).
137
+ */
138
+ type HookState = AccountState | null;
139
+ type CauseHookArgs<T extends CauseEventType, S extends HookState = HookState> = {
140
+ ref: string;
141
+ cause: T;
142
+ /** Filtresiz (eski) endpoint'te o tipin event'i, yeni endpoint'te `account.state_changed`. */
143
+ event: BillingEventOf<T> | BillingEventOf<"account.state_changed">;
144
+ /** Teslimatın tam durumu (refetch'te canlı); yalnızca `refetch: false` + snapshot taşıyan eski gövdede `null`. */
145
+ state: S;
146
+ };
147
+ type NotificationHookArgs<T extends CauseEventType, S extends HookState = HookState> = CauseHookArgs<T, S> & {
148
+ /** `includeProfile: true` ise steward'dan okunan fatura profili (e-posta; K15). */
149
+ profile?: BillingProfile | null;
150
+ };
151
+ type UnknownEventArgs<S extends HookState = HookState> = {
152
+ ref: string;
153
+ cause: string;
154
+ event: AnyBillingEvent;
155
+ /** Tam durum (refetch'te canlı; değilse `data.account` tam `AccountState` ise — o zaman `onStateChanged` de koştu). */
156
+ state: S;
157
+ };
158
+ type AfterArgs<S extends HookState = HookState> = {
159
+ ref: string;
160
+ cause: string;
161
+ event: BillingEvent | AnyBillingEvent;
162
+ state: S;
163
+ /** Refetch açıkken `received` ya da (hesap silme teslimatı, F4e) `deleted`; `no_state` yalnızca `state: null` ile. */
164
+ handled: null extends S ? Exclude<WebhookHandled, "ping" | "ignored"> : "received" | "deleted";
165
+ };
166
+ /** Dönüş değeri beklenir (`await`) ama kullanılmaz: sorgu/kuyruk promise'ı doğrudan dönebilir. */
167
+ type Hook<A> = (args: A) => unknown;
168
+ /**
169
+ * Steward istemcisinin `Webhooks`'un kullandığı kısmı (`createSteward()` karşılar).
170
+ * Her metot yalnızca onu gerektiren seçenekte istenir: `accounts.state` →
171
+ * `refetch`, `accounts.get` → `includeProfile`.
172
+ */
173
+ type WebhooksSteward = {
174
+ accounts: {
175
+ state?(ref: string): Promise<AccountState>;
176
+ get?(ref: string, options: {
177
+ include: readonly AccountInclude[];
178
+ }): Promise<{
179
+ profile?: BillingProfile | null | undefined;
180
+ }>;
181
+ };
182
+ };
183
+ /** Teslimatın durumunu alan önbellek (`stateCache()` karşılar); version'da geri gitmemek önbelleğin işidir. */
184
+ type WebhooksCache = {
185
+ prime(account: AccountState): unknown;
186
+ /** Hesap silindiğinde (F4e) çağrılır; verilmezse önbellek TTL'le düşer. */
187
+ invalidate?(ref: string): unknown;
188
+ };
189
+ type CauseHooks<S extends HookState> = { [T in CauseEventType as EventHookName<T>]?: Hook<CauseHookArgs<T, S>>; };
190
+ type WebhooksOptions<S extends HookState = HookState> = CauseHooks<S> & {
191
+ /** İmza sırları; metinse virgülle "yeni,eski". Verilmezse her teslimatta `STEWARD_WEBHOOK_SECRET`. */
192
+ secrets?: string | readonly string[];
193
+ /** Zaman damgası penceresi, saniye (varsayılan 300). */
194
+ toleranceSeconds?: number;
195
+ steward?: WebhooksSteward;
196
+ /**
197
+ * Durum her teslimatta steward'dan canlı okunur (`steward.accounts.state`); gövde
198
+ * yalnızca tetikleyici. Varsayılan: `steward` verildiyse `true`.
199
+ */
200
+ refetch?: boolean;
201
+ /** Teslimatın tam durumu hook'lardan önce buraya yazılır (`stateCache()`). */
202
+ cache?: WebhooksCache;
203
+ /** `onPaymentFailed`/`onSuspended`/`onTerminated` koşacaksa profil steward'dan okunur (`steward.accounts.get` gerekir). */
204
+ includeProfile?: boolean;
205
+ /** Tam durumlu her teslimatta (tekrar dahil). `"ignore"` dönerse hesap bilinmiyor: 200, diğer hook'lar ve `after` koşmaz. */
206
+ onStateChanged?: Hook<StateChangedArgs>;
207
+ onPaymentFailed?: Hook<NotificationHookArgs<"subscription.payment_failed", S>>;
208
+ onSuspended?: Hook<NotificationHookArgs<"subscription.suspended", S>>;
209
+ onTerminated?: Hook<NotificationHookArgs<"subscription.terminated", S>>;
210
+ onUnknownEvent?: Hook<UnknownEventArgs<S>>;
211
+ /** Diğer hook'lardan sonra, her teslimatta (tekrar dahil; `"ignore"` hariç); ör. ağlara yayılma. */
212
+ after?: Hook<AfterArgs<S>>;
213
+ logger?: StewardLogger;
214
+ };
215
+ /**
216
+ * `refetch` ve `steward` alanlarının tiplerinden hook durumu: `refetch: true` ya da (`refetch`
217
+ * verilmemiş ve `steward` verilmiş) → `AccountState`; aksi (ya da tip belirsizse) `AccountState | null`.
218
+ */
219
+ type WebhooksHookState<R, St> = [R] extends [true] ? AccountState : [R] extends [undefined] ? ([undefined] extends [St] ? HookState : AccountState) : HookState;
220
+ /** `Request`'in kullanılan kısmı (Hono `c.req.raw`, Next `req`, undici `Request`). */
221
+ type WebhookRequest = {
222
+ readonly headers: Headers;
223
+ text(): Promise<string>;
224
+ };
225
+ type WebhooksHandler = {
226
+ /** Fetch-standart handler: `app.post("/billing/events", (c) => webhooks.handle(c.req.raw))`. */
227
+ handle(req: WebhookRequest): Promise<Response>;
228
+ /** Düz fonksiyon: ham gövde + başlıklar → `{status, body}` (ürün kendi yanıtını kurar). */
229
+ receive(rawBody: string, headers: HeaderSource): Promise<WebhookResult>;
230
+ };
231
+ export declare function Webhooks<const R extends boolean | undefined = undefined, St extends WebhooksSteward | undefined = undefined>(options: Omit<WebhooksOptions<WebhooksHookState<R, St>>, "refetch" | "steward"> & {
232
+ refetch?: R;
233
+ steward?: St;
234
+ }): WebhooksHandler;
235
+ //#endregion
236
+ export type { AfterArgs, CauseEventType, CauseHookArgs, CheckoutCache, CheckoutCreateInput, CheckoutCreated, CheckoutHandler, CheckoutOptions, CheckoutResult, CheckoutSteward, ConsentDoc, CustomerPortalClient, CustomerPortalCreateArgs, CustomerPortalOptions, CustomerPortalSteward, EventHookName, HookState, NotificationHookArgs, StateChangedArgs, UnknownEventArgs, WebhookErrorCode, WebhookHandled, WebhookRequest, WebhookResult, WebhooksCache, WebhooksHandler, WebhooksHookState, WebhooksOptions, WebhooksSteward };
package/dist/server.js ADDED
@@ -0,0 +1,384 @@
1
+ import { O as ActorRefSchema, R as ExternalRefSchema, a as EVENT_TYPES, c as PING_EVENT_TYPE, l as PingSchema, r as BillingEventSchema, t as AnyBillingEventSchema, y as AccountStateSchema, z as IdSchema } from "./_chunks/events.js";
2
+ import "./_chunks/src.js";
3
+ import { n as StewardConfigError, r as StewardContractError, t as StewardApiError } from "./_chunks/errors.js";
4
+ import { t as hostedLocaleOf } from "./_chunks/locale.js";
5
+ import { i as verifyWebhookRaw } from "./_chunks/webhook.js";
6
+ //#region src/server/checkout.ts
7
+ function isAbsoluteHttpUrl(value) {
8
+ if (typeof value !== "string") return false;
9
+ try {
10
+ const { protocol } = new URL(value);
11
+ return protocol === "https:" || protocol === "http:";
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ const notFound = () => new StewardApiError({
17
+ code: "checkout_session_not_found",
18
+ httpStatus: 404,
19
+ category: "not_found",
20
+ retryable: false
21
+ });
22
+ function Checkout(options) {
23
+ const { steward, cache } = options;
24
+ const problems = [];
25
+ if (typeof steward?.checkoutSessions?.create !== "function" || typeof steward.checkoutSessions.get !== "function") problems.push("steward.checkoutSessions.create/get gerekli (createSteward())");
26
+ if (!isAbsoluteHttpUrl(options.successUrl)) problems.push("successUrl mutlak bir http(s) adresi olmalı");
27
+ if (options.cancelUrl !== void 0 && !isAbsoluteHttpUrl(options.cancelUrl)) problems.push("cancelUrl mutlak bir http(s) adresi olmalı");
28
+ const defaultLocale = hostedLocaleOf(options.locale);
29
+ if (options.locale !== void 0 && defaultLocale === void 0) problems.push("locale tr | en olmalı (ör. tr-TR)");
30
+ if (Array.isArray(options.consents) && options.consents.some((doc) => !isAbsoluteHttpUrl(doc?.url))) problems.push("consents[].url mutlak bir http(s) adresi olmalı");
31
+ if (options.consents !== void 0 && !Array.isArray(options.consents) && typeof options.consents !== "function") problems.push("consents dizi ya da (locale) => dizi olmalı");
32
+ if (cache !== void 0 && typeof cache?.prime !== "function") problems.push("cache.prime gerekli (stateCache())");
33
+ if (problems.length > 0) throw new StewardConfigError({ message: `Checkout: ${problems.join("; ")}` });
34
+ return {
35
+ async create(input) {
36
+ const locale = hostedLocaleOf(input.locale) ?? defaultLocale;
37
+ const docs = typeof options.consents === "function" ? options.consents(locale) : options.consents;
38
+ const body = {
39
+ planCode: input.planCode,
40
+ interval: input.interval,
41
+ successUrl: options.successUrl,
42
+ ...options.cancelUrl !== void 0 ? { cancelUrl: options.cancelUrl } : {},
43
+ ...options.currency !== void 0 ? { currency: options.currency } : {},
44
+ ...locale !== void 0 ? { locale } : {},
45
+ ...docs !== void 0 ? { consents: docs.map(({ document, version, url }) => ({
46
+ document,
47
+ version,
48
+ url
49
+ })) } : {},
50
+ ...input.displayName !== void 0 ? { displayName: input.displayName } : {},
51
+ ...input.prefill !== void 0 ? { prefill: input.prefill } : {}
52
+ };
53
+ const session = await steward.checkoutSessions.create(input.ref, body, { actorRef: input.actorRef });
54
+ if (typeof session.url !== "string" || session.url === "") throw new StewardContractError({ message: "Checkout: steward hosted checkout adresi (url) döndürmedi; steward sürümü F2b öncesi mi?" });
55
+ return {
56
+ id: session.id,
57
+ url: session.url,
58
+ expiresAt: session.expiresAt
59
+ };
60
+ },
61
+ async result(id, { ref }) {
62
+ if (!IdSchema.safeParse(id).success) throw notFound();
63
+ const session = await steward.checkoutSessions.get(id);
64
+ if (session.accountRef !== ref) throw notFound();
65
+ if (session.status === "completed" && session.account) {
66
+ await cache?.prime(session.account);
67
+ return {
68
+ status: session.status,
69
+ planCode: session.planCode,
70
+ account: session.account
71
+ };
72
+ }
73
+ return {
74
+ status: session.status,
75
+ planCode: session.planCode
76
+ };
77
+ }
78
+ };
79
+ }
80
+ //#endregion
81
+ //#region src/server/portal.ts
82
+ function isHttpUrl(value) {
83
+ try {
84
+ const { protocol } = new URL(value);
85
+ return protocol === "http:" || protocol === "https:";
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ function CustomerPortal(options) {
91
+ const steward = options?.steward;
92
+ if (typeof steward?.portalSessions?.create !== "function") throw new StewardConfigError({ message: "CustomerPortal: steward.portalSessions.create gerekli (createSteward())" });
93
+ if (options.returnUrl !== void 0 && !isHttpUrl(options.returnUrl)) throw new StewardConfigError({ message: "CustomerPortal: returnUrl mutlak bir http(s) adresi olmalı" });
94
+ const create = steward.portalSessions.create.bind(steward.portalSessions);
95
+ return { async create(args) {
96
+ if (typeof args?.ref !== "string" || !ExternalRefSchema.safeParse(args.ref).success) throw new StewardConfigError({ message: "CustomerPortal.create: ref geçersiz" });
97
+ if (typeof args.actorRef !== "string" || !ActorRefSchema.safeParse(args.actorRef).success) throw new StewardConfigError({ message: "CustomerPortal.create: actorRef zorunlu (tür[:kimlik], ör. user:42)" });
98
+ const returnUrl = args.returnUrl ?? options.returnUrl;
99
+ if (returnUrl !== void 0 && !isHttpUrl(returnUrl)) throw new StewardConfigError({ message: "CustomerPortal.create: returnUrl mutlak bir http(s) adresi olmalı" });
100
+ const locale = hostedLocaleOf(args.locale);
101
+ const input = {
102
+ ...locale !== void 0 ? { locale } : {},
103
+ ...returnUrl !== void 0 ? { returnUrl } : {}
104
+ };
105
+ return create(args.ref, input, {
106
+ actorRef: args.actorRef,
107
+ ...args.idempotencyKey !== void 0 ? { idempotencyKey: args.idempotencyKey } : {}
108
+ });
109
+ } };
110
+ }
111
+ //#endregion
112
+ //#region src/server/webhooks.ts
113
+ const NOTIFICATION_HOOKS = {
114
+ "subscription.payment_failed": "onPaymentFailed",
115
+ "subscription.suspended": "onSuspended",
116
+ "subscription.terminated": "onTerminated"
117
+ };
118
+ const CAUSE_TYPES = new Set(EVENT_TYPES.filter((t) => t !== "account.state_changed"));
119
+ const KNOWN_TYPES = new Set(EVENT_TYPES);
120
+ const isCauseType = (value) => CAUSE_TYPES.has(value);
121
+ const eventHookName = (type) => `on${type.split(/[._]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
122
+ function parseSecrets(value) {
123
+ return (typeof value === "string" ? value.split(",") : value ?? []).map((s) => s.trim()).filter(Boolean);
124
+ }
125
+ const isObject = (value) => typeof value === "object" && value !== null;
126
+ function fullState(account) {
127
+ const parsed = AccountStateSchema.safeParse(account);
128
+ return parsed.success ? parsed.data : null;
129
+ }
130
+ /** İmzası doğrulanmış gövde → teslimat. `eventId` başlıktaki id. */
131
+ function parseDelivery(eventId, rawBody) {
132
+ let json;
133
+ try {
134
+ json = JSON.parse(rawBody);
135
+ } catch {
136
+ return {
137
+ kind: "invalid",
138
+ reason: "invalid_payload",
139
+ detail: "gövde JSON değil"
140
+ };
141
+ }
142
+ const type = isObject(json) ? json.type : void 0;
143
+ const idMismatch = {
144
+ kind: "invalid",
145
+ reason: "id_mismatch",
146
+ detail: "gövdedeki id başlıktakiyle aynı değil"
147
+ };
148
+ const refMismatch = {
149
+ kind: "invalid",
150
+ reason: "invalid_payload",
151
+ detail: "data.account.ref accountRef ile aynı değil"
152
+ };
153
+ if (type === "endpoint.ping") {
154
+ const ping = PingSchema.safeParse(json);
155
+ if (!ping.success) return {
156
+ kind: "invalid",
157
+ reason: "invalid_payload",
158
+ detail: `${PING_EVENT_TYPE} şemaya uymuyor`
159
+ };
160
+ return ping.data.id === eventId ? { kind: "ping" } : idMismatch;
161
+ }
162
+ const known = BillingEventSchema.safeParse(json);
163
+ if (known.success) {
164
+ const event = known.data;
165
+ if (event.id !== eventId) return idMismatch;
166
+ const state = event.type === "account.state_changed" ? event.data.account : fullState(json.data.account);
167
+ if (state && state.ref !== event.accountRef) return refMismatch;
168
+ const cause = event.type === "account.state_changed" ? event.data.detail.cause : event.type;
169
+ return {
170
+ kind: "event",
171
+ ref: event.accountRef,
172
+ event,
173
+ cause,
174
+ causeType: isCauseType(cause) ? cause : null,
175
+ state
176
+ };
177
+ }
178
+ if (typeof type === "string" && KNOWN_TYPES.has(type)) return {
179
+ kind: "invalid",
180
+ reason: "invalid_payload",
181
+ detail: `${type} şemaya uymuyor`
182
+ };
183
+ const loose = AnyBillingEventSchema.safeParse(json);
184
+ if (!loose.success) return {
185
+ kind: "invalid",
186
+ reason: "invalid_payload",
187
+ detail: "event zarfı şemaya uymuyor"
188
+ };
189
+ const event = loose.data;
190
+ if (event.id !== eventId) return idMismatch;
191
+ const state = fullState(event.data.account);
192
+ if (state && state.ref !== event.accountRef) return refMismatch;
193
+ return {
194
+ kind: "unknown",
195
+ ref: event.accountRef,
196
+ event,
197
+ cause: event.type,
198
+ state
199
+ };
200
+ }
201
+ const describeError = (error) => error instanceof Error ? error.message : String(error);
202
+ /** Silme teslimatı (F4e): `account.deleted` ya da onun nedeni olduğu `account.state_changed`. */
203
+ const DELETED = "account.deleted";
204
+ const isDeletion = (d) => d.kind === "event" && (d.event.type === DELETED || d.event.type === "account.state_changed" && d.cause === DELETED);
205
+ function Webhooks(input) {
206
+ const options = input;
207
+ const logger = options.logger ?? console;
208
+ const configuredSecrets = options.secrets === void 0 ? null : parseSecrets(options.secrets);
209
+ const tolerance = options.toleranceSeconds;
210
+ const hooks = options;
211
+ const getAccount = options.steward?.accounts.get?.bind(options.steward.accounts);
212
+ const readState = options.steward?.accounts.state?.bind(options.steward.accounts);
213
+ const refetch = options.refetch ?? options.steward !== void 0;
214
+ if (options.includeProfile && !getAccount) throw new StewardConfigError({ message: "Webhooks: includeProfile için steward.accounts.get gerekli" });
215
+ if (refetch && !readState) throw new StewardConfigError({ message: "Webhooks: refetch için steward.accounts.state gerekli (ya da refetch: false)" });
216
+ if (tolerance !== void 0 && !(Number.isFinite(tolerance) && tolerance >= 0)) throw new StewardConfigError({ message: "Webhooks: toleranceSeconds negatif olmayan bir sayı olmalı" });
217
+ const ok = (handled) => ({
218
+ status: 200,
219
+ body: {
220
+ ok: true,
221
+ handled
222
+ }
223
+ });
224
+ const fail = (status, error) => ({
225
+ status,
226
+ body: { error }
227
+ });
228
+ async function fetchProfile(d) {
229
+ if (!options.includeProfile || !getAccount || d.kind !== "event" || d.causeType === null) return void 0;
230
+ const hook = NOTIFICATION_HOOKS[d.causeType];
231
+ if (!hook || !options[hook]) return void 0;
232
+ return (await getAccount(d.ref, { include: ["profile"] })).profile ?? null;
233
+ }
234
+ async function runHooks(d, profile) {
235
+ if (isDeletion(d)) {
236
+ await options.cache?.invalidate?.(d.ref);
237
+ await options.onAccountDeleted?.({
238
+ ref: d.ref,
239
+ cause: DELETED,
240
+ event: d.event,
241
+ state: d.state
242
+ });
243
+ await options.after?.({
244
+ ref: d.ref,
245
+ cause: d.cause,
246
+ event: d.event,
247
+ state: d.state,
248
+ handled: "deleted"
249
+ });
250
+ return "deleted";
251
+ }
252
+ if (d.state) {
253
+ await options.cache?.prime(d.state);
254
+ if (await options.onStateChanged?.({
255
+ ref: d.ref,
256
+ state: d.state,
257
+ cause: d.cause,
258
+ event: d.event
259
+ }) === "ignore") return "ignored";
260
+ }
261
+ if (d.kind === "unknown") await options.onUnknownEvent?.({
262
+ ref: d.ref,
263
+ cause: d.cause,
264
+ event: d.event,
265
+ state: d.state
266
+ });
267
+ else if (d.causeType !== null) {
268
+ const args = {
269
+ ref: d.ref,
270
+ cause: d.causeType,
271
+ event: d.event,
272
+ state: d.state
273
+ };
274
+ await hooks[eventHookName(d.causeType)]?.(args);
275
+ const notification = NOTIFICATION_HOOKS[d.causeType];
276
+ if (notification) await hooks[notification]?.(profile === void 0 ? args : {
277
+ ...args,
278
+ profile
279
+ });
280
+ }
281
+ const handled = d.state ? "received" : "no_state";
282
+ await options.after?.({
283
+ ref: d.ref,
284
+ cause: d.cause,
285
+ event: d.event,
286
+ state: d.state,
287
+ handled
288
+ });
289
+ return handled;
290
+ }
291
+ async function receive(rawBody, headers) {
292
+ const secrets = configuredSecrets ?? parseSecrets(process.env.STEWARD_WEBHOOK_SECRET);
293
+ if (secrets.length === 0) {
294
+ logger.error("ALARM steward webhook: imza sırrı tanımlı değil (STEWARD_WEBHOOK_SECRET); steward yeniden deneyecek", {});
295
+ return fail(503, "not_configured");
296
+ }
297
+ const verified = verifyWebhookRaw(secrets, headers, rawBody, tolerance === void 0 ? {} : { toleranceSeconds: tolerance });
298
+ if (!verified.ok) {
299
+ logger.warn("steward webhook reddedildi", { reason: verified.reason });
300
+ return fail(401, verified.reason);
301
+ }
302
+ const parsed = parseDelivery(verified.eventId, rawBody);
303
+ if (parsed.kind === "invalid") {
304
+ logger.error(`ALARM steward webhook: imzalı gövde sözleşmeye uymuyor (${parsed.detail}); sözleşme sürümü?`, { eventId: verified.eventId });
305
+ return fail(400, parsed.reason);
306
+ }
307
+ if (parsed.kind === "ping") return ok("ping");
308
+ let d = parsed;
309
+ const context = {
310
+ eventId: d.event.id,
311
+ type: d.event.type,
312
+ ref: d.ref,
313
+ cause: d.cause
314
+ };
315
+ if (d.kind === "unknown") logger.warn("steward webhook: tanınmayan event tipi (SDK sürümü eski?)", context);
316
+ if (refetch && readState && !isDeletion(d)) try {
317
+ d = {
318
+ ...d,
319
+ state: await readState(d.ref)
320
+ };
321
+ } catch (error) {
322
+ const { code, requestId } = typeof error === "object" && error !== null ? error : {};
323
+ if (code === "account_deleted") {
324
+ logger.warn("steward webhook: hesap silinmiş; silmeden önceki teslimat hook'suz geçildi", context);
325
+ try {
326
+ await options.cache?.invalidate?.(d.ref);
327
+ } catch (cacheError) {
328
+ logger.warn("steward webhook: cache.invalidate fırlattı", {
329
+ ...context,
330
+ error: describeError(cacheError)
331
+ });
332
+ }
333
+ return ok("deleted");
334
+ }
335
+ logger.error("steward webhook: güncel durum okunamadı; steward yeniden deneyecek", {
336
+ ...context,
337
+ error: describeError(error),
338
+ code: typeof code === "string" ? code : null,
339
+ requestId: typeof requestId === "string" ? requestId : null
340
+ });
341
+ return fail(503, "state_unavailable");
342
+ }
343
+ else if (d.state === null && d.kind === "event") logger.warn("steward webhook: gövde tam AccountState taşımıyor (yükseltme öncesi kuyruk); onStateChanged atlandı, neden hook'ları koşuyor", context);
344
+ let profile;
345
+ try {
346
+ profile = await fetchProfile(d);
347
+ } catch (error) {
348
+ logger.error("steward webhook: fatura profili okunamadı; steward yeniden deneyecek", {
349
+ ...context,
350
+ error: describeError(error)
351
+ });
352
+ return fail(503, "profile_unavailable");
353
+ }
354
+ try {
355
+ const handled = await runHooks(d, profile);
356
+ if (handled === "ignored") logger.warn("steward webhook: bilinmeyen hesap (onStateChanged \"ignore\"); diğer hook'lar ve after atlandı", context);
357
+ return ok(handled);
358
+ } catch (error) {
359
+ logger.error("steward webhook: hook başarısız; steward yeniden deneyecek", {
360
+ ...context,
361
+ error: describeError(error)
362
+ });
363
+ return fail(503, "hook_failed");
364
+ }
365
+ }
366
+ return {
367
+ receive,
368
+ async handle(req) {
369
+ let rawBody = null;
370
+ try {
371
+ rawBody = await req.text();
372
+ } catch (error) {
373
+ logger.error("steward webhook: istek gövdesi okunamadı", { error: describeError(error) });
374
+ }
375
+ const result = rawBody === null ? fail(400, "invalid_payload") : await receive(rawBody, req.headers);
376
+ return new Response(JSON.stringify(result.body), {
377
+ status: result.status,
378
+ headers: { "content-type": "application/json" }
379
+ });
380
+ }
381
+ };
382
+ }
383
+ //#endregion
384
+ export { Checkout, CustomerPortal, Webhooks };
@@ -0,0 +1,27 @@
1
+ {
2
+ "apiVersion": "2026-10-01",
3
+ "sha256": "00b36567f8972e90997f6436efcaab7e75e1104301ace40941ba3eaaea908582",
4
+ "additions": [
5
+ {
6
+ "since": "0.2.0",
7
+ "files": [
8
+ "account.state_changed.json"
9
+ ],
10
+ "sha256": "af2584923b587bd1d50f8aeceaca4617e76a328d092a0f08cf5b8306fe0bd2d4"
11
+ },
12
+ {
13
+ "since": "F4f",
14
+ "files": [
15
+ "invoice.voided.json"
16
+ ],
17
+ "sha256": "26bde921ce073e6f1634612b0b8d122835972647c8feb4608f54e776559cdeaf"
18
+ },
19
+ {
20
+ "since": "F4e",
21
+ "files": [
22
+ "account.deleted.json"
23
+ ],
24
+ "sha256": "68c3507812304c790d73ccaffa82389c7fd25c9be85a1ca405722597921c8b32"
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "id": "evt_0192f1a0-0000-7000-8000-000000000030",
3
+ "type": "account.deleted",
4
+ "apiVersion": "2026-10-01",
5
+ "createdAt": "2026-10-05T09:00:00Z",
6
+ "product": "example",
7
+ "accountRef": "acct_42",
8
+ "data": {
9
+ "account": {
10
+ "ref": "acct_42",
11
+ "displayName": "",
12
+ "version": 9,
13
+ "updatedAt": "2026-10-05T09:00:00Z",
14
+ "planCode": "free",
15
+ "accessState": "active",
16
+ "entitlements": {
17
+ "max_users": 3,
18
+ "max_devices": 15,
19
+ "max_networks_per_account": 1,
20
+ "max_approved_routes": 0,
21
+ "max_exit_nodes": 0
22
+ },
23
+ "catalogVersion": "3f2a9c0e7b41d8a65c09e1f4b7d2a8c3",
24
+ "subscription": null,
25
+ "openCheckout": null,
26
+ "profile": {
27
+ "present": false,
28
+ "kind": null
29
+ }
30
+ },
31
+ "detail": {
32
+ "deletedAt": "2026-10-05T09:00:00Z",
33
+ "actorRef": "user:7"
34
+ }
35
+ }
36
+ }