@stewardhq/sdk 0.3.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/LICENSE +201 -0
- package/README.md +332 -273
- package/dist/_chunks/errors.js +1 -1
- package/dist/_chunks/events.d.ts +386 -78
- package/dist/_chunks/events.js +1035 -19
- package/dist/_chunks/index.d.ts +3752 -305
- package/dist/_chunks/locale.d.ts +270 -2
- package/dist/_chunks/src.js +925 -31
- package/dist/_chunks/validators.d.ts +46 -2
- package/dist/_chunks/validators.js +128 -27
- package/dist/_chunks/webhook-core.d.ts +2 -2
- package/dist/contract.d.ts +4 -4
- package/dist/contract.js +4 -4
- package/dist/index.d.ts +447 -33
- package/dist/index.js +1659 -26
- package/dist/server.d.ts +66 -16
- package/dist/server.js +35 -4
- package/package.json +14 -13
- package/dist/_chunks/steward.d.ts +0 -189
- package/dist/_chunks/steward.js +0 -564
- package/dist/testing/fixtures/events/LOCK.json +0 -27
- package/dist/testing/fixtures/events/account.deleted.json +0 -36
- package/dist/testing/fixtures/events/account.state_changed.json +0 -53
- package/dist/testing/fixtures/events/account.updated.json +0 -37
- package/dist/testing/fixtures/events/checkout.completed.json +0 -36
- package/dist/testing/fixtures/events/checkout.expired.json +0 -26
- package/dist/testing/fixtures/events/checkout.failed.json +0 -27
- package/dist/testing/fixtures/events/invoice.created.json +0 -37
- package/dist/testing/fixtures/events/invoice.issued.json +0 -38
- package/dist/testing/fixtures/events/invoice.voided.json +0 -39
- package/dist/testing/fixtures/events/subscription.activated.json +0 -40
- package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +0 -38
- package/dist/testing/fixtures/events/subscription.canceled.json +0 -29
- package/dist/testing/fixtures/events/subscription.expired.json +0 -27
- package/dist/testing/fixtures/events/subscription.payment_failed.json +0 -38
- package/dist/testing/fixtures/events/subscription.reactivated.json +0 -36
- package/dist/testing/fixtures/events/subscription.renewed.json +0 -39
- package/dist/testing/fixtures/events/subscription.suspended.json +0 -36
- package/dist/testing/fixtures/events/subscription.terminated.json +0 -28
- package/dist/testing.d.ts +0 -656
- package/dist/testing.js +0 -3684
package/dist/index.js
CHANGED
|
@@ -1,12 +1,640 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { $ as
|
|
3
|
-
import { a as
|
|
4
|
-
import { t as createSteward } from "./_chunks/steward.js";
|
|
1
|
+
import { $ as emailLocaleOf, $t as MeteredPriceSchema, A as SubscriptionStatusSchema, An as hasControlCharacters, At as METER_FILTER_MAX_CONDITIONS, B as EmailSenderSchema, Bn as EntitlementsSchema, Bt as MeterCreditEntryKindSchema, C as AccountSchema, Cn as EventNameSchema, Ct as DEFAULT_METER_LOW_AT, D as BillingProfileKindSchema, Dn as IngestEventSchema, Dt as METER_AGGREGATION_KINDS, E as AccountUpsertInputSchema, En as INGEST_REJECT_CODES, Et as MAX_METER_UNITS, F as EMAIL_LOCALES, Fn as CodeSchema, Ft as MeterAggregationSchema, G as InvoicePaymentDueEmailSchema, Gn as INVOICE_PAYMENT_STATUSES, Gt as MeterLabelSchema, H as EmailSettingsSchema, Hn as ErrorResponseSchema, Ht as MeterCreditPropertiesSchema, I as EMAIL_PAYLOAD_SCHEMAS, In as CurrencySchema, It as MeterChargeLineSchema, J as NotificationEmailsSchema, Jn as InvoiceBillingReasonSchema, Jt as MeterPeriodSchema, K as MAX_NOTIFICATION_EMAILS, Kn as IdSchema, Kt as MeterLowAtSchema, L as EMAIL_TEMPLATES, Ln as DateTimeSchema, Lt as MeterChargeListSchema, M as BILLING_PATH_PLACEHOLDERS, Mn as API_VERSION, Mt as METER_FILTER_OPERATORS, N as BankTransferInstructionsEmailSchema, Nn as ActorRefSchema, Nt as METER_PERIOD_STATUSES, O as BillingProfileSchema, On as IngestRejectCodeSchema, Ot as METER_CHARGE_STATUSES, P as BillingPathSchema, Pn as AmountMinorSchema, Pt as METER_STANDINGS, Q as billingPageUrl, Qt as MeteredPriceDefSchema, R as EMAIL_TEMPLATE_BY_EVENT, Rn as ERROR_CATEGORIES, Rt as MeterChargeSchema, S as AccountScheduledChangeSchema, Sn as EventMetadataValueSchema, St as CreditGrantInputSchema, T as AccountSubscriptionStateSchema, Tn as EventsIngestResultSchema, Tt as MAX_METERED_PRICE_PER_UNITS, U as EmailTemplateSchema, Un as ExternalRefSchema, Ut as MeterDefSchema, V as EmailSettingsPatchSchema, Vn as ErrorCategorySchema, Vt as MeterCreditEntrySchema, W as EmailTemplateSettingsSchema, Wn as INVOICE_BILLING_REASONS, Wt as MeterFilterSchema, X as PriceChangeNoticeMeteredRateSchema, Xn as MetadataSchema, Xt as MeterStandingSchema, Y as PriceChangeNoticeEmailSchema, Yn as InvoicePaymentStatusSchema, Yt as MeterPeriodStatusSchema, Z as applyEmailSettingsPatch, Zt as MeterUnitsSchema, _ as AccountMeterStateSchema, _n as EVENT_TIMESTAMP_MAX_AGE_DAYS, _t as isFeatureFlagBenefit, a as EVENT_TYPES, an as fromMicroUnits, at as uniqueEmails, b as AccountOpenInvoiceSchema, bn as EventMetadataKeySchema, bt as AccountMetersSchema, c as LEGACY_EVENT_TYPES, cn as parseMicroUnits, ct as BenefitDefSchema, d as isEventType, dn as EVENTS_INGEST_MAX_BYTES, dt as BenefitTypeSchema, en as canonicalMeterFilter, et as emailRecipients, f as ACCOUNT_INCLUDES, fn as EVENTS_INGEST_MAX_EVENTS, ft as CatalogBenefitSchema, g as AccountLocaleSchema, gn as EVENT_NAME_MAX_LENGTH, gt as featureValueMatchesType, h as AccountDunningStateSchema, hn as EVENT_METADATA_STRING_MAX_LENGTH, ht as MeterCreditBenefitDefSchema, i as EVENT_DETAIL_SCHEMAS, in as UNIT_DECIMALS, it as liveSubscriptionOf, j as SubscriptionSummarySchema, jn as isEventTimestampInRange, jt as METER_FILTER_MAX_DEPTH, k as EntitlementSnapshotSchema, kn as IngestRejectionSchema, kt as METER_CREDIT_ENTRY_KINDS, l as PING_EVENT_TYPE, ln as plainDecimalOf, lt as BenefitDescriptionSchema, m as AccountDeleteResponseSchema, mn as EVENT_METADATA_MAX_PAIRS, mt as FeatureFlagPropertiesSchema, n as ApiVersionSchema, nn as InvalidUnitValueError, nt as emailTemplateSettings, o as EventIdSchema, on as isMicroPrecise, ot as AccountBenefitSchema, p as AccessStateSchema, pn as EVENT_METADATA_KEY_MAX_LENGTH, pt as FeatureFlagBenefitDefSchema, q as NoticedMeteredRateSchema, qn as IntervalSchema, qt as MeterPeriodListSchema, r as BillingEventSchema, rn as MICRO_UNITS_PER_UNIT, rt as isEmailRelevant, s as FULL_STATE_EVENT_TYPES, sn as microToDecimalString, st as BENEFIT_TYPES, t as AnyBillingEventSchema, tn as meterFilterClauseOf, tt as emailTemplateOf, u as PingSchema, un as toMicroUnits, ut as BenefitSourceSchema, v as AccountMetersStateSchema, vn as EVENT_TIMESTAMP_MAX_SKEW_MINUTES, vt as isMeterCreditBenefit, w as AccountStateSchema, wn as EventsIngestInputSchema, wt as DecimalUnitsStringSchema, x as AccountProfileStateSchema, xn as EventMetadataSchema, xt as CatalogMeterSchema, y as AccountOpenCheckoutSchema, yn as EventExternalIdSchema, yt as AccountMeterSchema, z as EmailExtraTextSchema, zn as EntitlementValueSchema, zt as MeterChargeStatusSchema } from "./_chunks/events.js";
|
|
2
|
+
import { $ as rolloverOf, $n as MAX_CATALOG_BENEFITS, $t as CheckoutSessionCreatedSchema, A as WebhookEndpointRotateInputSchema, An as MeteringSettingsPatchSchema, At as InvoiceWaiveInputSchema, B as PlanSetResponseSchema, Bn as applySettingsPatch, Bt as PaymentSchema, C as hostedReadinessOf, Cn as DEFAULT_PORTAL_TTL_MINUTES, Ct as InvoiceIssueInputSchema, D as WEBHOOK_SECRET_PREFIX, Dn as LateGraceMinutesSchema, Dt as InvoiceSchema, E as WEBHOOK_ENDPOINT_STATUSES, En as EventsSettingsSchema, Et as InvoicePaymentSessionSchema, F as generateWebhookSecret, Fn as ReturnOriginSchema, Ft as splitInclusiveTax, G as contributionOf, Gn as returnOriginsOf, Gt as PortalSessionCreateInputSchema, H as ResyncResultSchema, Hn as eventsSettingsOf, Ht as SubscriptionChangeInputSchema, I as webhookEndpointUrlProblem, In as SETTINGS_LOCALES, It as CancelSubscriptionInputSchema, J as meterCreditsOf, Jn as CatalogSchema, Jt as BankTransferRecordedSchema, K as matchesFilter, Kn as AccountsRefreshResultSchema, Kt as PortalSessionSchema, L as PLAN_SET_MODES, Ln as SettingsLocaleSchema, Lt as GrantCreateInputSchema, M as WebhookEndpointStatusSchema, Mn as PaymentDueDaysSchema, Mt as ManualInvoiceInputSchema, N as WebhookLastDeliverySchema, Nn as ProductSettingsPatchSchema, Nt as ManualInvoiceLineInputSchema, O as WebhookEndpointCreateInputSchema, On as METER_CHARGE_MODES, Ot as InvoiceStatusSchema, P as WebhookSecretSchema, Pn as ProductSettingsSchema, Pt as manualInvoiceTotals, Q as rateOverage, Qn as FeatureTypeSchema, Qt as CheckoutSessionCreateInputSchema, R as PlanSetInputSchema, Rn as TtlMinutesSchema, Rt as GrantSchema, S as endpointDiagnosisOf, Sn as DEFAULT_PAYMENT_DUE_DAYS, St as UncancelSubscriptionResponseSchema, T as EventTypeSchema, Tn as EventsSettingsPatchSchema, Tt as InvoicePaymentSessionCreateInputSchema, U as addUtcMonths, Un as isReturnUrlAllowed, Ut as SubscriptionSchema, V as PlanSetWarningSchema, Vn as billingSettingsOf, Vt as StatsSchema, W as consumptionOf, Wn as meteringSettingsOf, Wt as PORTAL_SESSION_ERRORS, X as overageOf, Xn as CatalogSyncResultSchema, Xt as CheckoutConsentInputSchema, Y as overageCapReached, Yn as CatalogSyncInputSchema, Yt as CHECKOUT_ERRORS, Z as periodBoundsOf, Zn as FeatureDefSchema, Zt as CheckoutPrefillSchema, _ as DoctorSeveritySchema, _n as DEFAULT_CHECKOUT_TTL_MINUTES, _t as InvoiceVoidResponseSchema, a as DOCTOR_ROLLUP_LAG_WARN_SECONDS, an as PAYMENT_METHODS, ar as PriceDefSchema, at as AccountUpsertResponseSchema, b as buildDoctorReport, bn as DEFAULT_METER_CHARGE_MODE, bt as MeResponseSchema, c as DoctorCatalogSchema, cn as checkoutRedirectUrl, ct as CheckoutSessionResponseSchema, d as DoctorEventsSchema, dn as BankTransferSettingsSchema, dt as HEADER_ACTOR_REF, en as CheckoutSessionSchema, er as MAX_CATALOG_METERED_PRICES, et as standingOf, f as DoctorHostedSchema, fn as BillingSettingsPatchSchema, ft as HEADER_API_VERSION, g as DoctorSettingsSchema, gn as DEFAULT_BANK_TRANSFER_PAYMENT_DAYS, gt as InvoiceIssueResponseSchema, h as DoctorReportSchema, hn as BrandingSchema, ht as HEADER_SDK, i as DOCTOR_REQUIRED_LEGAL_LINKS, in as MAX_CHECKOUT_CONSENT_DOCS, ir as PlanDefSchema, it as AccountDetailSchema, j as WebhookEndpointSchema, jn as MeteringSettingsSchema, jt as MANUAL_INVOICE_MAX_AMOUNT_MINOR, k as WebhookEndpointListSchema, kn as MeterChargeModeSchema, kt as InvoiceVoidInputSchema, l as DoctorEmailsSchema, ln as BankTransferPaymentDaysSchema, lt as CreditGrantResponseSchema, m as DoctorProviderSchema, mn as BrandingPatchSchema, mt as HEADER_REQUEST_ID, n as DOCTOR_PROVIDER_ENVIRONMENTS, nn as ConsentSchema, nr as MAX_PLAN_BENEFITS, nt as errorCodeSpec, o as DOCTOR_SEVERITIES, on as PaymentMethodSchema, or as PriceSchema, ot as CancelSubscriptionResponseSchema, p as DoctorOverageSchema, pn as BillingSettingsSchema, pt as HEADER_IDEMPOTENCY_KEY, q as meterChargeTotals, qn as CatalogPlanSchema, qt as BankTransferRecordInputSchema, r as DOCTOR_REQUIRED_BRANDING, rn as INDIVIDUAL_IDENTITY_PLACEHOLDER, rr as METERED_PRICE_TAX_MISMATCH, rt as isKnownErrorCode, s as DoctorBillingSchema, sn as ProviderPayloadSchema, st as ChangeSubscriptionResponseSchema, t as DOCTOR_PENDING_BACKLOG_MS, tn as CheckoutStatusSchema, tr as MAX_CATALOG_METERS, tt as ERROR_CODES, u as DoctorEndpointSchema, un as BankTransferSettingsPatchSchema, ut as GrantCreateResponseSchema, v as DoctorUnsellablePriceSchema, vn as DEFAULT_EVENT_RETENTION_DAYS, vt as InvoiceWaiveResponseSchema, w as DEFAULT_ENDPOINT_EVENT_TYPES, wn as EventRetentionDaysSchema, wt as InvoiceLineSchema, x as emailReadinessOf, xn as DEFAULT_MIN_INVOICE_MINOR, xt as STEWARD_CAPABILITIES, y as DoctorWarningSchema, yn as DEFAULT_LATE_GRACE_MINUTES, yt as ManualInvoiceResponseSchema, z as PlanSetModeSchema, zn as WebUrlSchema, zt as MAX_GRANT_BENEFITS } from "./_chunks/src.js";
|
|
3
|
+
import { a as LIVE_SUBSCRIPTION_STATUSES, c as isLiveSubscriptionStatus, i as FEATURE_FLAG_BENEFIT_TYPE, l as resolveBenefits, n as isValidTckn, o as OVERRIDE_BENEFIT_SOURCE, r as isValidVkn, s as isGrantActive, t as isE164, u as resolveEntitlements } from "./_chunks/validators.js";
|
|
5
4
|
import { a as StewardNetworkError, i as StewardError, n as StewardConfigError, r as StewardContractError, t as StewardApiError } from "./_chunks/errors.js";
|
|
5
|
+
//#region ../contract/src/client.ts
|
|
6
|
+
/**
|
|
7
|
+
* `Retry-After` başlığını milisaniyeye çevirir: saniye (tam sayı) ya da HTTP
|
|
8
|
+
* tarihi. Geçmiş tarih 0; okunamayan değer null.
|
|
9
|
+
*/
|
|
10
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
11
|
+
if (value === null || value === void 0) return null;
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
14
|
+
if (!/^[A-Za-z]/.test(trimmed)) return null;
|
|
15
|
+
const at = Date.parse(trimmed);
|
|
16
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
17
|
+
}
|
|
18
|
+
const SDK_HEADER_VALUE = /^[\x21-\x7e][\x20-\x7e]{0,199}$/;
|
|
19
|
+
function createBillingClient(options) {
|
|
20
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
21
|
+
const doFetch = options.fetch ?? fetch;
|
|
22
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
23
|
+
if (options.sdk !== void 0 && !SDK_HEADER_VALUE.test(options.sdk)) throw new Error("sdk: yazdırılabilir ASCII, en çok 200 karakter");
|
|
24
|
+
function meta(res) {
|
|
25
|
+
const out = {};
|
|
26
|
+
const requestId = res.headers.get(HEADER_REQUEST_ID);
|
|
27
|
+
if (requestId) out.requestId = requestId;
|
|
28
|
+
if (res.status === 429 || res.status === 503) {
|
|
29
|
+
const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
|
|
30
|
+
if (retryAfterMs !== null) out.retryAfterMs = retryAfterMs;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
async function send(method, path, body, call, extraHeaders) {
|
|
35
|
+
const headers = {
|
|
36
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
37
|
+
accept: "application/json",
|
|
38
|
+
[HEADER_API_VERSION]: API_VERSION,
|
|
39
|
+
...extraHeaders
|
|
40
|
+
};
|
|
41
|
+
if (options.sdk !== void 0) headers[HEADER_SDK] = options.sdk;
|
|
42
|
+
if (method !== "GET") headers[HEADER_IDEMPOTENCY_KEY] = call?.idempotencyKey ?? globalThis.crypto.randomUUID();
|
|
43
|
+
if (call?.actorRef) headers[HEADER_ACTOR_REF] = call.actorRef;
|
|
44
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
45
|
+
try {
|
|
46
|
+
return { res: await doFetch(`${baseUrl}${path}`, {
|
|
47
|
+
method,
|
|
48
|
+
headers,
|
|
49
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
50
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
51
|
+
}) };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
status: 0,
|
|
56
|
+
error: error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") ? "timeout" : "network_error"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function failure(res) {
|
|
61
|
+
const json = await res.json().catch(() => null);
|
|
62
|
+
const parsed = ErrorResponseSchema.safeParse(json);
|
|
63
|
+
const { requestId, retryAfterMs } = meta(res);
|
|
64
|
+
const common = {
|
|
65
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
66
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {}
|
|
67
|
+
};
|
|
68
|
+
if (parsed.success) {
|
|
69
|
+
const { error, message, category, retryable } = parsed.data;
|
|
70
|
+
const bodyRequestId = requestId === void 0 ? parsed.data.requestId : void 0;
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
status: res.status,
|
|
74
|
+
error,
|
|
75
|
+
...message !== void 0 ? { message } : {},
|
|
76
|
+
...category !== void 0 ? { category } : {},
|
|
77
|
+
...retryable !== void 0 ? { retryable } : {},
|
|
78
|
+
...bodyRequestId !== void 0 ? { requestId: bodyRequestId } : {},
|
|
79
|
+
...common
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
status: res.status,
|
|
85
|
+
error: `http_${res.status}`,
|
|
86
|
+
...common
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function success(res, data) {
|
|
90
|
+
const { requestId } = meta(res);
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
status: res.status,
|
|
94
|
+
data,
|
|
95
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function invalid(res) {
|
|
99
|
+
const { requestId } = meta(res);
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
status: res.status,
|
|
103
|
+
error: "invalid_response",
|
|
104
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function call(schema, method, path, body, opts) {
|
|
108
|
+
const sent = await send(method, path, body, opts);
|
|
109
|
+
if (!("res" in sent)) return sent;
|
|
110
|
+
const { res } = sent;
|
|
111
|
+
if (!res.ok) return failure(res);
|
|
112
|
+
const json = await res.json().catch(() => void 0);
|
|
113
|
+
const parsed = schema.safeParse(json);
|
|
114
|
+
if (!parsed.success) return invalid(res);
|
|
115
|
+
return success(res, parsed.data);
|
|
116
|
+
}
|
|
117
|
+
async function empty(method, path, opts) {
|
|
118
|
+
const sent = await send(method, path, void 0, opts);
|
|
119
|
+
if (!("res" in sent)) return sent;
|
|
120
|
+
const { res } = sent;
|
|
121
|
+
if (!res.ok) return failure(res);
|
|
122
|
+
return success(res, null);
|
|
123
|
+
}
|
|
124
|
+
/** GET with `If-None-Match` (the previous response's `etag`): 304 → `{notModified: true}`. */
|
|
125
|
+
async function conditional(schema, path, etag) {
|
|
126
|
+
const sent = await send("GET", path, void 0, void 0, etag !== void 0 ? { "if-none-match": etag } : void 0);
|
|
127
|
+
if (!("res" in sent)) return sent;
|
|
128
|
+
const { res } = sent;
|
|
129
|
+
const responseEtag = res.headers.get("etag");
|
|
130
|
+
if (res.status === 304) {
|
|
131
|
+
const current = responseEtag ?? etag;
|
|
132
|
+
return current === void 0 ? invalid(res) : success(res, {
|
|
133
|
+
notModified: true,
|
|
134
|
+
etag: current
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (!res.ok) return failure(res);
|
|
138
|
+
const parsed = schema.safeParse(await res.json().catch(() => void 0));
|
|
139
|
+
if (!parsed.success) return invalid(res);
|
|
140
|
+
return success(res, {
|
|
141
|
+
notModified: false,
|
|
142
|
+
etag: responseEtag,
|
|
143
|
+
body: parsed.data
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const ref = (value) => encodeURIComponent(value);
|
|
147
|
+
return {
|
|
148
|
+
getCatalog: () => call(CatalogSchema, "GET", "/v1/catalog"),
|
|
149
|
+
syncCatalog: (input, opts) => call(CatalogSyncResultSchema, "PUT", "/v1/admin/catalog", input, opts),
|
|
150
|
+
upsertAccount: (accountRef, input, opts) => call(AccountUpsertResponseSchema, "PUT", `/v1/accounts/${ref(accountRef)}`, input, opts),
|
|
151
|
+
getAccount: (accountRef, include = []) => call(AccountDetailSchema, "GET", `/v1/accounts/${ref(accountRef)}${include.length ? `?include=${include.join(",")}` : ""}`),
|
|
152
|
+
/**
|
|
153
|
+
* Hesap durumu (`AccountState`). ETag = `"<version>"`; önceki yanıtın
|
|
154
|
+
* `etag`'i verilirse ve durum değişmediyse 304 → `{notModified: true}`.
|
|
155
|
+
*/
|
|
156
|
+
getAccountState: async (accountRef, opts = {}) => {
|
|
157
|
+
const result = await conditional(AccountStateSchema, `/v1/accounts/${ref(accountRef)}/state`, opts.etag);
|
|
158
|
+
if (!result.ok) return result;
|
|
159
|
+
const read = result.data;
|
|
160
|
+
return {
|
|
161
|
+
...result,
|
|
162
|
+
data: read.notModified ? read : {
|
|
163
|
+
notModified: false,
|
|
164
|
+
etag: read.etag,
|
|
165
|
+
state: read.body
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
/** Atomic plan (K6): active plan grants → one plan grant (`planCode: null` removes it); `mode` is accepted and ignored (M35: no rank), `warnings` is always empty. */
|
|
170
|
+
setPlan: (accountRef, input, opts) => call(PlanSetResponseSchema, "PUT", `/v1/accounts/${ref(accountRef)}/plan`, input, opts),
|
|
171
|
+
/** Saklı durumu `account.state_changed` (`cause: "resync"`) olarak yeniden yayınlar; version değişmez (202). */
|
|
172
|
+
resync: (accountRef, opts) => call(ResyncResultSchema, "POST", `/v1/accounts/${ref(accountRef)}/resync`, void 0, opts),
|
|
173
|
+
/** KVKK anonimleştirme (F4e): `{ref, deletedAt}`; silinmişse 410 `account_deleted`, canlı abonelikte 409 `subscription_active`. */
|
|
174
|
+
deleteAccount: (accountRef, opts) => call(AccountDeleteResponseSchema, "DELETE", `/v1/accounts/${ref(accountRef)}`, void 0, opts),
|
|
175
|
+
createCheckoutSession: (accountRef, input, opts) => call(CheckoutSessionCreatedSchema, "POST", `/v1/accounts/${ref(accountRef)}/checkout-sessions`, input, opts),
|
|
176
|
+
getCheckoutSession: (id) => call(CheckoutSessionResponseSchema, "GET", `/v1/checkout-sessions/${ref(id)}`),
|
|
177
|
+
createPortalSession: (accountRef, input = {}, opts) => call(PortalSessionSchema, "POST", `/v1/accounts/${ref(accountRef)}/portal-sessions`, input, opts),
|
|
178
|
+
cancelSubscription: (id, input, opts) => call(CancelSubscriptionResponseSchema, "POST", `/v1/subscriptions/${ref(id)}/cancel`, input, opts),
|
|
179
|
+
/**
|
|
180
|
+
* Billing core (MF9): schedules a plan/interval/price change for the next period (errors:
|
|
181
|
+
* `SubscriptionChangeInputSchema`: 409 `already_subscribed`, 422 `plan_not_found`,
|
|
182
|
+
* `plan_not_sellable`, `subscription_change_invalid`).
|
|
183
|
+
*/
|
|
184
|
+
changeSubscription: (id, input, opts) => call(ChangeSubscriptionResponseSchema, "POST", `/v1/subscriptions/${ref(id)}/change`, input, opts),
|
|
185
|
+
/**
|
|
186
|
+
* Billing core (MF9): takes back a cancellation scheduled for the period end; no body. 409
|
|
187
|
+
* `cancel_not_scheduled` (none scheduled) or `already_canceled` (ended, or its period is over).
|
|
188
|
+
*/
|
|
189
|
+
uncancelSubscription: (id, opts) => call(UncancelSubscriptionResponseSchema, "POST", `/v1/subscriptions/${ref(id)}/uncancel`, void 0, opts),
|
|
190
|
+
createGrant: (accountRef, input, opts) => call(GrantCreateResponseSchema, "POST", `/v1/accounts/${ref(accountRef)}/grants`, input, opts),
|
|
191
|
+
revokeGrant: (id, opts) => empty("DELETE", `/v1/grants/${ref(id)}`, opts),
|
|
192
|
+
getStats: () => call(StatsSchema, "GET", "/v1/stats"),
|
|
193
|
+
doctor: () => call(DoctorReportSchema, "GET", "/v1/doctor"),
|
|
194
|
+
issueInvoice: (id, input, opts) => call(InvoiceIssueResponseSchema, "POST", `/v1/invoices/${ref(id)}/issue`, input, opts),
|
|
195
|
+
/** Manuel taslak fatura (F4f): profil gerekir (422 `billing_profile_required`); `issueInvoice` ile kesilir. */
|
|
196
|
+
createManualInvoice: (accountRef, input, opts) => call(ManualInvoiceResponseSchema, "POST", `/v1/accounts/${ref(accountRef)}/invoices`, input, opts),
|
|
197
|
+
/** Yalnızca manuel fatura (409 `invoice_has_payment` / `invoice_not_voidable`); `invoice.voided`. */
|
|
198
|
+
voidInvoice: (id, input, opts) => call(InvoiceVoidResponseSchema, "POST", `/v1/invoices/${ref(id)}/void`, input, opts),
|
|
199
|
+
/**
|
|
200
|
+
* Billing core: opens steward's hosted pay page for an unpaid invoice → `{id, url, expiresAt}`;
|
|
201
|
+
* redirect the customer to `url` (409 `invoice_not_payable`). Opening changes no state.
|
|
202
|
+
*/
|
|
203
|
+
createInvoicePaymentSession: (id, input = {}, opts) => call(InvoicePaymentSessionSchema, "POST", `/v1/invoices/${ref(id)}/payment-sessions`, input, opts),
|
|
204
|
+
/** Billing core: forgives an unpaid invoice (409 `invoice_not_waivable`). */
|
|
205
|
+
waiveInvoice: (id, input, opts) => call(InvoiceWaiveResponseSchema, "POST", `/v1/invoices/${ref(id)}/waive`, input, opts),
|
|
206
|
+
/**
|
|
207
|
+
* `POST /v1/events/ingest`: 1–1 000 events → `{inserted, duplicates, rejected[]}`. Pass the SAME
|
|
208
|
+
* `idempotencyKey` on every retry of a batch (a new batch composition needs a new key);
|
|
209
|
+
* `externalId` dedups each event for good. 422 `batch_too_large`; 503 `events_backlog` (retry after `retryAfterMs`).
|
|
210
|
+
*/
|
|
211
|
+
ingestEvents: (input, opts) => call(EventsIngestResultSchema, "POST", "/v1/events/ingest", input, opts),
|
|
212
|
+
/**
|
|
213
|
+
* `GET /v1/accounts/:ref/meters` (open period per meter). ETag = the account's meter sequence;
|
|
214
|
+
* with the previous `etag` an unchanged read is 304 → `{notModified: true}`.
|
|
215
|
+
*/
|
|
216
|
+
getAccountMeters: async (accountRef, opts = {}) => {
|
|
217
|
+
const result = await conditional(AccountMetersSchema, `/v1/accounts/${ref(accountRef)}/meters`, opts.etag);
|
|
218
|
+
if (!result.ok) return result;
|
|
219
|
+
const read = result.data;
|
|
220
|
+
return {
|
|
221
|
+
...result,
|
|
222
|
+
data: read.notModified ? read : {
|
|
223
|
+
notModified: false,
|
|
224
|
+
etag: read.etag,
|
|
225
|
+
meters: read.body
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
/** `GET /v1/accounts/:ref/meters/periods?limit=`: closed meter periods, newest first. */
|
|
230
|
+
listMeterPeriods: (accountRef, opts = {}) => call(MeterPeriodListSchema, "GET", `/v1/accounts/${ref(accountRef)}/meters/periods${opts.limit !== void 0 ? `?limit=${encodeURIComponent(String(opts.limit))}` : ""}`),
|
|
231
|
+
/** `POST /v1/accounts/:ref/credits`: one-off credit for a meter's open period (422 `meter_not_found`). */
|
|
232
|
+
grantCredits: (accountRef, input, opts) => call(CreditGrantResponseSchema, "POST", `/v1/accounts/${ref(accountRef)}/credits`, input, opts),
|
|
233
|
+
/** `GET /v1/accounts/:ref/meter-charges`: overage pricing records (also `dry_run` ones), newest first. */
|
|
234
|
+
listMeterCharges: (accountRef) => call(MeterChargeListSchema, "GET", `/v1/accounts/${ref(accountRef)}/meter-charges`),
|
|
235
|
+
getInvoiceDocument: async (id) => {
|
|
236
|
+
const sent = await send("GET", `/v1/invoices/${ref(id)}/document`, void 0, void 0);
|
|
237
|
+
if (!("res" in sent)) return sent;
|
|
238
|
+
const { res } = sent;
|
|
239
|
+
if (!res.ok) return failure(res);
|
|
240
|
+
return success(res, {
|
|
241
|
+
contentType: res.headers.get("content-type") ?? "application/octet-stream",
|
|
242
|
+
bytes: new Uint8Array(await res.arrayBuffer())
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
getSettings: () => call(ProductSettingsSchema, "GET", "/v1/admin/settings"),
|
|
246
|
+
updateSettings: (patch, opts) => call(ProductSettingsSchema, "PATCH", "/v1/admin/settings", patch, opts),
|
|
247
|
+
listWebhookEndpoints: () => call(WebhookEndpointListSchema, "GET", "/v1/admin/webhook-endpoints"),
|
|
248
|
+
createWebhookEndpoint: (input, opts) => call(WebhookEndpointSchema, "POST", "/v1/admin/webhook-endpoints", input, opts),
|
|
249
|
+
rotateWebhookEndpointSecret: (id, input, opts) => call(WebhookEndpointSchema, "POST", `/v1/admin/webhook-endpoints/${ref(id)}/rotate`, input, opts),
|
|
250
|
+
deactivateWebhookEndpoint: (id, opts) => call(WebhookEndpointSchema, "POST", `/v1/admin/webhook-endpoints/${ref(id)}/deactivate`, void 0, opts)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/retry.ts
|
|
255
|
+
const RETRY_CLASSES = [
|
|
256
|
+
"network",
|
|
257
|
+
"5xx",
|
|
258
|
+
"429"
|
|
259
|
+
];
|
|
260
|
+
const BASE_DELAY_MS = 250;
|
|
261
|
+
function retryPolicy(options = {}) {
|
|
262
|
+
const attempts = options.attempts ?? 2;
|
|
263
|
+
const maxDelayMs = options.maxDelayMs ?? 5e3;
|
|
264
|
+
const on = options.on ?? RETRY_CLASSES;
|
|
265
|
+
const problems = [];
|
|
266
|
+
if (!Number.isInteger(attempts) || attempts < 0) problems.push(`retries.attempts negatif olmayan tam sayı olmalı: ${attempts}`);
|
|
267
|
+
if (!Number.isFinite(maxDelayMs) || maxDelayMs < 0) problems.push(`retries.maxDelayMs negatif olmayan sayı olmalı: ${maxDelayMs}`);
|
|
268
|
+
for (const value of on) if (!RETRY_CLASSES.includes(value)) problems.push(`retries.on bilinmeyen sınıf: ${String(value)} (${RETRY_CLASSES.join(", ")})`);
|
|
269
|
+
if (problems.length > 0) throw new StewardConfigError({ message: `createSteward: ${problems.join("; ")}` });
|
|
270
|
+
return {
|
|
271
|
+
attempts,
|
|
272
|
+
on: new Set(on),
|
|
273
|
+
maxDelayMs,
|
|
274
|
+
sleep: options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
275
|
+
random: options.random ?? Math.random
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** Başarısız sonucun tekrar sınıfı; hiçbirine girmiyorsa null (4xx, sözleşme hatası). */
|
|
279
|
+
function retryClassOf(failure) {
|
|
280
|
+
if (failure.status === 0) return "network";
|
|
281
|
+
if (failure.status === 429) return "429";
|
|
282
|
+
if (failure.status >= 500) return "5xx";
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
function shouldRetry(request, failure, policy) {
|
|
286
|
+
if (request.method !== "GET" && request.idempotencyKey === void 0) return false;
|
|
287
|
+
const kind = retryClassOf(failure);
|
|
288
|
+
if (kind === null || !policy.on.has(kind)) return false;
|
|
289
|
+
if (kind === "network") return true;
|
|
290
|
+
return (failure.retryable ?? errorCodeSpec(failure.error)?.retryable) !== false;
|
|
291
|
+
}
|
|
292
|
+
/** `retry`. tekrardan (1'den başlar) önceki bekleme, ms. */
|
|
293
|
+
function retryDelayMs(retry, failure, policy) {
|
|
294
|
+
if (failure.retryAfterMs !== void 0) return Math.min(failure.retryAfterMs, policy.maxDelayMs);
|
|
295
|
+
const ceiling = Math.min(policy.maxDelayMs, BASE_DELAY_MS * 2 ** (retry - 1));
|
|
296
|
+
return Math.round(ceiling / 2 + policy.random() * ceiling / 2);
|
|
297
|
+
}
|
|
298
|
+
/** `send`'i politika izin verdikçe tekrarlar; son sonucu (başarılı ya da değil) döner. */
|
|
299
|
+
async function withRetries(request, policy, send) {
|
|
300
|
+
let result = await send();
|
|
301
|
+
for (let retry = 1; retry <= policy.attempts && !result.ok && shouldRetry(request, result, policy); retry++) {
|
|
302
|
+
await policy.sleep(retryDelayMs(retry, result, policy));
|
|
303
|
+
result = await send();
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/steward.ts
|
|
309
|
+
const SDK_HEADER = `@stewardhq/sdk/0.6.0`;
|
|
310
|
+
const defaultEnv = () => globalThis.process?.env ?? {};
|
|
311
|
+
function isHttpUrl(value) {
|
|
312
|
+
try {
|
|
313
|
+
const { protocol } = new URL(value);
|
|
314
|
+
return protocol === "http:" || protocol === "https:";
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Nesne ağacındaki her metodu, istek göndermeden `error(yol)` ile reddeden eşiyle değiştirir. */
|
|
320
|
+
function rejectAll(api, error, prefix = "") {
|
|
321
|
+
return Object.fromEntries(Object.entries(api).map(([name, value]) => {
|
|
322
|
+
const path = `${prefix}${name}`;
|
|
323
|
+
if (typeof value === "function") return [name, () => Promise.reject(error(path))];
|
|
324
|
+
return [name, typeof value === "object" && value !== null ? rejectAll(value, error, `${path}.`) : value];
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
function createSteward(options = {}) {
|
|
328
|
+
const env = options.env ?? defaultEnv();
|
|
329
|
+
const setting = (value, name) => {
|
|
330
|
+
const text = (value ?? env[name])?.trim();
|
|
331
|
+
return text === void 0 || text === "" ? void 0 : text;
|
|
332
|
+
};
|
|
333
|
+
const baseUrl = setting(options.baseUrl, "STEWARD_URL");
|
|
334
|
+
const apiKey = setting(options.apiKey, "STEWARD_API_KEY");
|
|
335
|
+
const adminBaseUrl = setting(options.adminBaseUrl, "STEWARD_ADMIN_URL");
|
|
336
|
+
const adminApiKey = setting(options.adminApiKey, "STEWARD_ADMIN_API_KEY");
|
|
337
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
338
|
+
const adminOnly = baseUrl === void 0 && apiKey === void 0 && adminBaseUrl !== void 0 && adminApiKey !== void 0;
|
|
339
|
+
const problems = [];
|
|
340
|
+
if (!adminOnly) {
|
|
341
|
+
if (baseUrl === void 0) problems.push("STEWARD_URL (baseUrl) tanımlı değil");
|
|
342
|
+
else if (!isHttpUrl(baseUrl)) problems.push("STEWARD_URL (baseUrl) geçerli bir http(s) adresi değil");
|
|
343
|
+
if (apiKey === void 0) problems.push("STEWARD_API_KEY (apiKey) tanımlı değil");
|
|
344
|
+
}
|
|
345
|
+
if (adminBaseUrl !== void 0 && !isHttpUrl(adminBaseUrl)) problems.push("STEWARD_ADMIN_URL (adminBaseUrl) geçerli bir http(s) adresi değil");
|
|
346
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) problems.push(`timeoutMs pozitif sayı olmalı: ${timeoutMs}`);
|
|
347
|
+
if (options.actorRef !== void 0 && !ActorRefSchema.safeParse(options.actorRef).success) problems.push("actorRef biçimi geçersiz (tür[:kimlik], ör. system, user:42)");
|
|
348
|
+
if (problems.length > 0) {
|
|
349
|
+
const hint = baseUrl === void 0 && apiKey === void 0 ? " (yalnızca admin.* için STEWARD_ADMIN_URL + STEWARD_ADMIN_API_KEY yeter)" : "";
|
|
350
|
+
throw new StewardConfigError({ message: `createSteward: ${problems.join("; ")}${hint}` });
|
|
351
|
+
}
|
|
352
|
+
const policy = retryPolicy(options.retries);
|
|
353
|
+
const doFetch = options.fetch ?? fetch;
|
|
354
|
+
const clientOptions = (target) => ({
|
|
355
|
+
...target,
|
|
356
|
+
timeoutMs,
|
|
357
|
+
sdk: SDK_HEADER,
|
|
358
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
359
|
+
});
|
|
360
|
+
const adminTarget = adminBaseUrl !== void 0 && adminApiKey !== void 0 ? {
|
|
361
|
+
baseUrl: adminBaseUrl.replace(/\/$/, ""),
|
|
362
|
+
apiKey: adminApiKey
|
|
363
|
+
} : null;
|
|
364
|
+
const adminClient = adminTarget === null ? null : createBillingClient(clientOptions(adminTarget));
|
|
365
|
+
const productTarget = baseUrl !== void 0 && apiKey !== void 0 ? {
|
|
366
|
+
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
367
|
+
apiKey
|
|
368
|
+
} : null;
|
|
369
|
+
const missingProduct = (name) => new StewardConfigError({ message: `steward.${name}: STEWARD_URL (baseUrl) ve STEWARD_API_KEY (apiKey) tanımlı değil; istemci yalnızca admin.* için yapılandırıldı` });
|
|
370
|
+
const raw = createBillingClient(clientOptions(productTarget ?? {
|
|
371
|
+
baseUrl: "http://steward.invalid",
|
|
372
|
+
apiKey: "-"
|
|
373
|
+
}));
|
|
374
|
+
const eitherTarget = productTarget ?? adminTarget;
|
|
375
|
+
const eitherClient = productTarget !== null ? raw : adminClient;
|
|
376
|
+
function requireAdmin() {
|
|
377
|
+
if (adminClient !== null && adminTarget !== null) return {
|
|
378
|
+
client: adminClient,
|
|
379
|
+
target: adminTarget
|
|
380
|
+
};
|
|
381
|
+
const missing = [...adminBaseUrl === void 0 ? ["STEWARD_ADMIN_URL (adminBaseUrl)"] : [], ...adminApiKey === void 0 ? ["STEWARD_ADMIN_API_KEY (adminApiKey)"] : []];
|
|
382
|
+
throw new StewardConfigError({ message: `steward.admin: ${missing.join(" ve ")} tanımlı değil` });
|
|
383
|
+
}
|
|
384
|
+
async function send(target, method, path, call, extraHeaders = {}) {
|
|
385
|
+
const headers = {
|
|
386
|
+
authorization: `Bearer ${target.apiKey}`,
|
|
387
|
+
accept: "application/json",
|
|
388
|
+
[HEADER_API_VERSION]: API_VERSION,
|
|
389
|
+
[HEADER_SDK]: SDK_HEADER,
|
|
390
|
+
...extraHeaders
|
|
391
|
+
};
|
|
392
|
+
if (call.idempotencyKey !== void 0) headers[HEADER_IDEMPOTENCY_KEY] = call.idempotencyKey;
|
|
393
|
+
if (call.actorRef !== void 0) headers[HEADER_ACTOR_REF] = call.actorRef;
|
|
394
|
+
try {
|
|
395
|
+
return { res: await doFetch(`${target.baseUrl}${path}`, {
|
|
396
|
+
method,
|
|
397
|
+
headers,
|
|
398
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
399
|
+
}) };
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const name = typeof error === "object" && error !== null ? error.name : void 0;
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
status: 0,
|
|
405
|
+
error: name === "TimeoutError" || name === "AbortError" ? "timeout" : "network_error"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function meta(res) {
|
|
410
|
+
const requestId = res.headers.get(HEADER_REQUEST_ID);
|
|
411
|
+
const retryAfterMs = res.status === 429 || res.status === 503 ? parseRetryAfter(res.headers.get("retry-after")) : null;
|
|
412
|
+
return {
|
|
413
|
+
...requestId ? { requestId } : {},
|
|
414
|
+
...retryAfterMs !== null ? { retryAfterMs } : {}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function failureOf(res) {
|
|
418
|
+
const parsed = ErrorResponseSchema.safeParse(await res.json().catch(() => null));
|
|
419
|
+
const { requestId, retryAfterMs } = meta(res);
|
|
420
|
+
const common = { ...retryAfterMs !== void 0 ? { retryAfterMs } : {} };
|
|
421
|
+
if (!parsed.success) return {
|
|
422
|
+
ok: false,
|
|
423
|
+
status: res.status,
|
|
424
|
+
error: `http_${res.status}`,
|
|
425
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
426
|
+
...common
|
|
427
|
+
};
|
|
428
|
+
const { error, message, category, retryable } = parsed.data;
|
|
429
|
+
const id = requestId ?? parsed.data.requestId;
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
status: res.status,
|
|
433
|
+
error,
|
|
434
|
+
...message !== void 0 ? { message } : {},
|
|
435
|
+
...category !== void 0 ? { category } : {},
|
|
436
|
+
...retryable !== void 0 ? { retryable } : {},
|
|
437
|
+
...id !== void 0 ? { requestId: id } : {},
|
|
438
|
+
...common
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function success(res, data) {
|
|
442
|
+
const { requestId } = meta(res);
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
status: res.status,
|
|
446
|
+
data,
|
|
447
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function invalid(res) {
|
|
451
|
+
const { requestId } = meta(res);
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
status: res.status,
|
|
455
|
+
error: "invalid_response",
|
|
456
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function sendParsed(target, method, path, schema, call) {
|
|
460
|
+
const sent = await send(target, method, path, call);
|
|
461
|
+
if (!("res" in sent)) return sent;
|
|
462
|
+
if (!sent.res.ok) return failureOf(sent.res);
|
|
463
|
+
const parsed = schema.safeParse(await sent.res.json().catch(() => void 0));
|
|
464
|
+
return parsed.success ? success(sent.res, parsed.data) : invalid(sent.res);
|
|
465
|
+
}
|
|
466
|
+
async function readCatalog(etag) {
|
|
467
|
+
const sent = await send(productTarget, "GET", "/v1/catalog", {}, etag !== void 0 ? { "if-none-match": etag } : {});
|
|
468
|
+
if (!("res" in sent)) return sent;
|
|
469
|
+
const { res } = sent;
|
|
470
|
+
const responseEtag = res.headers.get("etag");
|
|
471
|
+
if (res.status === 304) {
|
|
472
|
+
const current = responseEtag ?? etag;
|
|
473
|
+
return current === void 0 ? invalid(res) : success(res, {
|
|
474
|
+
notModified: true,
|
|
475
|
+
etag: current
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (!res.ok) return failureOf(res);
|
|
479
|
+
const parsed = CatalogSchema.safeParse(await res.json().catch(() => void 0));
|
|
480
|
+
return parsed.success ? success(res, {
|
|
481
|
+
notModified: false,
|
|
482
|
+
etag: responseEtag,
|
|
483
|
+
catalog: parsed.data
|
|
484
|
+
}) : invalid(res);
|
|
485
|
+
}
|
|
486
|
+
function toError(failure) {
|
|
487
|
+
const context = {
|
|
488
|
+
...failure.requestId !== void 0 ? { requestId: failure.requestId } : {},
|
|
489
|
+
...failure.retryAfterMs !== void 0 ? { retryAfterMs: failure.retryAfterMs } : {}
|
|
490
|
+
};
|
|
491
|
+
if (failure.status === 0) return new StewardNetworkError({
|
|
492
|
+
code: failure.error === "timeout" ? "timeout" : "network_error",
|
|
493
|
+
...context
|
|
494
|
+
});
|
|
495
|
+
if (failure.error === "invalid_response") return new StewardContractError({
|
|
496
|
+
httpStatus: failure.status,
|
|
497
|
+
...context
|
|
498
|
+
});
|
|
499
|
+
return new StewardApiError({
|
|
500
|
+
code: failure.error,
|
|
501
|
+
httpStatus: failure.status,
|
|
502
|
+
...failure.message !== void 0 ? { message: failure.message } : {},
|
|
503
|
+
...failure.category !== void 0 ? { category: failure.category } : {},
|
|
504
|
+
...failure.retryable !== void 0 ? { retryable: failure.retryable } : {},
|
|
505
|
+
...context
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function run(method, call, attempt) {
|
|
509
|
+
let effective = {};
|
|
510
|
+
if (method !== "GET") {
|
|
511
|
+
const actorRef = call?.actorRef ?? options.actorRef;
|
|
512
|
+
if (actorRef === void 0) throw new StewardConfigError({
|
|
513
|
+
code: "actor_ref_required",
|
|
514
|
+
message: "actorRef yok: createSteward({ actorRef }) ya da çağrıda { actorRef } verin (tür[:kimlik], ör. user:42, system)"
|
|
515
|
+
});
|
|
516
|
+
if (!ActorRefSchema.safeParse(actorRef).success) throw new StewardConfigError({
|
|
517
|
+
code: "invalid_actor_ref",
|
|
518
|
+
message: "actorRef biçimi geçersiz (tür[:kimlik], ör. user:42, system)"
|
|
519
|
+
});
|
|
520
|
+
effective = {
|
|
521
|
+
idempotencyKey: call?.idempotencyKey ?? globalThis.crypto.randomUUID(),
|
|
522
|
+
actorRef
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const result = await withRetries({
|
|
526
|
+
method,
|
|
527
|
+
...effective.idempotencyKey !== void 0 ? { idempotencyKey: effective.idempotencyKey } : {}
|
|
528
|
+
}, policy, () => attempt(effective));
|
|
529
|
+
if (result.ok) return result.data;
|
|
530
|
+
throw toError(result);
|
|
531
|
+
}
|
|
532
|
+
const read = (attempt) => run("GET", void 0, attempt);
|
|
533
|
+
const productApi = {
|
|
534
|
+
accounts: {
|
|
535
|
+
upsert: (ref, input, call) => run("PUT", call, (c) => raw.upsertAccount(ref, input, c)),
|
|
536
|
+
get: (ref, opts = {}) => read(() => raw.getAccount(ref, opts.include)),
|
|
537
|
+
state: async (ref) => {
|
|
538
|
+
const result = await read(() => raw.getAccountState(ref));
|
|
539
|
+
if (result.notModified) throw new StewardContractError({ httpStatus: 304 });
|
|
540
|
+
return result.state;
|
|
541
|
+
},
|
|
542
|
+
readState: (ref, opts = {}) => read(() => raw.getAccountState(ref, opts.etag !== void 0 ? { etag: opts.etag } : {})),
|
|
543
|
+
setPlan: (ref, input, call) => run("PUT", call, (c) => raw.setPlan(ref, input, c)),
|
|
544
|
+
resync: (ref, call) => run("POST", call, (c) => raw.resync(ref, c)),
|
|
545
|
+
delete: (ref, call) => run("DELETE", call, (c) => raw.deleteAccount(ref, c))
|
|
546
|
+
},
|
|
547
|
+
checkoutSessions: {
|
|
548
|
+
create: (ref, input, call) => run("POST", call, (c) => raw.createCheckoutSession(ref, input, c)),
|
|
549
|
+
get: (id) => read(() => raw.getCheckoutSession(id))
|
|
550
|
+
},
|
|
551
|
+
portalSessions: { create: (ref, input = {}, call) => run("POST", call, (c) => raw.createPortalSession(ref, input, c)) },
|
|
552
|
+
subscriptions: {
|
|
553
|
+
cancel: (id, input, call) => run("POST", call, (c) => raw.cancelSubscription(id, input, c)),
|
|
554
|
+
change: (id, input, call) => run("POST", call, (c) => raw.changeSubscription(id, input, c)),
|
|
555
|
+
uncancel: (id, call) => run("POST", call, (c) => raw.uncancelSubscription(id, c))
|
|
556
|
+
},
|
|
557
|
+
grants: {
|
|
558
|
+
create: (ref, input, call) => run("POST", call, (c) => raw.createGrant(ref, input, c)),
|
|
559
|
+
revoke: async (id, call) => {
|
|
560
|
+
await run("DELETE", call, (c) => raw.revokeGrant(id, c));
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
invoices: {
|
|
564
|
+
issue: (id, input, call) => run("POST", call, (c) => raw.issueInvoice(id, input, c)),
|
|
565
|
+
document: (id) => read(() => raw.getInvoiceDocument(id)),
|
|
566
|
+
createManual: (ref, input, call) => run("POST", call, (c) => raw.createManualInvoice(ref, input, c)),
|
|
567
|
+
void: (id, input, call) => run("POST", call, (c) => raw.voidInvoice(id, input, c)),
|
|
568
|
+
createPaymentSession: (id, input = {}, call) => run("POST", call, (c) => raw.createInvoicePaymentSession(id, input, c)),
|
|
569
|
+
waive: (id, input, call) => run("POST", call, (c) => raw.waiveInvoice(id, input, c))
|
|
570
|
+
},
|
|
571
|
+
catalog: {
|
|
572
|
+
get: () => read(() => raw.getCatalog()),
|
|
573
|
+
read: (opts = {}) => read(() => readCatalog(opts.etag))
|
|
574
|
+
},
|
|
575
|
+
events: { ingest: (input, call) => run("POST", call, (c) => raw.ingestEvents(input, c)) },
|
|
576
|
+
meters: {
|
|
577
|
+
get: async (ref) => {
|
|
578
|
+
const result = await read(() => raw.getAccountMeters(ref));
|
|
579
|
+
if (result.notModified) throw new StewardContractError({ httpStatus: 304 });
|
|
580
|
+
return result.meters;
|
|
581
|
+
},
|
|
582
|
+
read: (ref, opts = {}) => read(() => raw.getAccountMeters(ref, opts.etag !== void 0 ? { etag: opts.etag } : {})),
|
|
583
|
+
periods: (ref, opts = {}) => read(() => raw.listMeterPeriods(ref, opts.limit !== void 0 ? { limit: opts.limit } : {}))
|
|
584
|
+
},
|
|
585
|
+
credits: { grant: (ref, input, call) => run("POST", call, (c) => raw.grantCredits(ref, input, c)) },
|
|
586
|
+
meterCharges: { list: (ref) => read(() => raw.listMeterCharges(ref)) },
|
|
587
|
+
stats: () => read(() => raw.getStats())
|
|
588
|
+
};
|
|
589
|
+
return {
|
|
590
|
+
...productTarget !== null ? productApi : rejectAll(productApi, missingProduct),
|
|
591
|
+
me: () => read(() => sendParsed(eitherTarget, "GET", "/v1/me", MeResponseSchema, {})),
|
|
592
|
+
doctor: () => read(() => eitherClient.doctor()),
|
|
593
|
+
admin: {
|
|
594
|
+
catalog: { sync: async (input, call) => {
|
|
595
|
+
const { client } = requireAdmin();
|
|
596
|
+
return run("PUT", call, (c) => client.syncCatalog(input, c));
|
|
597
|
+
} },
|
|
598
|
+
accounts: { refresh: async (call) => {
|
|
599
|
+
const { target } = requireAdmin();
|
|
600
|
+
return run("POST", call, (c) => sendParsed(target, "POST", "/v1/admin/accounts/refresh", AccountsRefreshResultSchema, c));
|
|
601
|
+
} },
|
|
602
|
+
settings: {
|
|
603
|
+
get: async () => {
|
|
604
|
+
const { client } = requireAdmin();
|
|
605
|
+
return read(() => client.getSettings());
|
|
606
|
+
},
|
|
607
|
+
update: async (patch, call) => {
|
|
608
|
+
const { client } = requireAdmin();
|
|
609
|
+
return run("PATCH", call, (c) => client.updateSettings(patch, c));
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
endpoints: {
|
|
613
|
+
list: async () => {
|
|
614
|
+
const { client } = requireAdmin();
|
|
615
|
+
return read(() => client.listWebhookEndpoints());
|
|
616
|
+
},
|
|
617
|
+
create: async (input, call) => {
|
|
618
|
+
const { client } = requireAdmin();
|
|
619
|
+
return run("POST", call, (c) => client.createWebhookEndpoint(input, c));
|
|
620
|
+
},
|
|
621
|
+
rotate: async (id, input, call) => {
|
|
622
|
+
const { client } = requireAdmin();
|
|
623
|
+
return run("POST", call, (c) => client.rotateWebhookEndpointSecret(id, input, c));
|
|
624
|
+
},
|
|
625
|
+
deactivate: async (id, call) => {
|
|
626
|
+
const { client } = requireAdmin();
|
|
627
|
+
return run("POST", call, (c) => client.deactivateWebhookEndpoint(id, c));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
6
634
|
//#region src/state-cache.ts
|
|
7
635
|
const MANY_CONCURRENCY = 8;
|
|
8
|
-
const FAILURE_BACKOFF_MS = 5e3;
|
|
9
|
-
function fieldsOf$
|
|
636
|
+
const FAILURE_BACKOFF_MS$1 = 5e3;
|
|
637
|
+
function fieldsOf$3(error) {
|
|
10
638
|
const { code, httpStatus, requestId } = typeof error === "object" && error !== null ? error : {};
|
|
11
639
|
return {
|
|
12
640
|
code: typeof code === "string" ? code : null,
|
|
@@ -14,7 +642,7 @@ function fieldsOf$1(error) {
|
|
|
14
642
|
requestId: typeof requestId === "string" ? requestId : null
|
|
15
643
|
};
|
|
16
644
|
}
|
|
17
|
-
function isOutage({ code, httpStatus }) {
|
|
645
|
+
function isOutage$1({ code, httpStatus }) {
|
|
18
646
|
if (httpStatus === 0) return code === "network_error" || code === "timeout";
|
|
19
647
|
return httpStatus !== null && (httpStatus >= 500 || httpStatus === 429);
|
|
20
648
|
}
|
|
@@ -34,7 +662,7 @@ function stateCache(steward, options = {}) {
|
|
|
34
662
|
const { catalog, fallback, onFallback } = options;
|
|
35
663
|
const logger = options.logger ?? console;
|
|
36
664
|
const now = options.now ?? Date.now;
|
|
37
|
-
const backoffMs = Math.min(ttlMs, FAILURE_BACKOFF_MS);
|
|
665
|
+
const backoffMs = Math.min(ttlMs, FAILURE_BACKOFF_MS$1);
|
|
38
666
|
const entries = /* @__PURE__ */ new Map();
|
|
39
667
|
const flights = /* @__PURE__ */ new Map();
|
|
40
668
|
let seq = 0;
|
|
@@ -67,7 +695,7 @@ function stateCache(steward, options = {}) {
|
|
|
67
695
|
/** Kesintide: bayat değer → fallback → asıl hata. WARN her başarısız istekte ve bayattan fallback'e geçişte bir kez. */
|
|
68
696
|
async function degrade(ref, failure) {
|
|
69
697
|
const { error } = failure;
|
|
70
|
-
const { code, requestId } = fieldsOf$
|
|
698
|
+
const { code, requestId } = fieldsOf$3(error);
|
|
71
699
|
const entry = entries.get(ref);
|
|
72
700
|
const t = now();
|
|
73
701
|
if (entry?.state && t < entry.fetchedAt + ttlMs + staleIfErrorMs) {
|
|
@@ -148,7 +776,7 @@ function stateCache(steward, options = {}) {
|
|
|
148
776
|
try {
|
|
149
777
|
read = await steward.accounts.readState(ref, known?.state && known.etag !== null ? { etag: known.etag } : {});
|
|
150
778
|
} catch (error) {
|
|
151
|
-
const fields = fieldsOf$
|
|
779
|
+
const fields = fieldsOf$3(error);
|
|
152
780
|
if (isNotFound(fields)) {
|
|
153
781
|
if (missing === "throw") {
|
|
154
782
|
entries.delete(ref);
|
|
@@ -170,7 +798,7 @@ function stateCache(steward, options = {}) {
|
|
|
170
798
|
});
|
|
171
799
|
return missingFallback(ref, failure);
|
|
172
800
|
}
|
|
173
|
-
if (!isOutage(fields)) throw error;
|
|
801
|
+
if (!isOutage$1(fields)) throw error;
|
|
174
802
|
const current = entries.get(ref);
|
|
175
803
|
const failure = {
|
|
176
804
|
error,
|
|
@@ -263,6 +891,10 @@ function stateCache(steward, options = {}) {
|
|
|
263
891
|
if (current?.state && account.version < current.state.version) return;
|
|
264
892
|
const etag = current?.state?.version === account.version ? current.etag : null;
|
|
265
893
|
put(account.ref, confirmed(account, etag, now()));
|
|
894
|
+
},
|
|
895
|
+
peek(ref) {
|
|
896
|
+
const entry = entries.get(ref);
|
|
897
|
+
return entry?.state && now() < entry.fetchedAt + ttlMs + staleIfErrorMs ? entry.state : null;
|
|
266
898
|
}
|
|
267
899
|
};
|
|
268
900
|
}
|
|
@@ -286,7 +918,7 @@ const BY_CATEGORY = {
|
|
|
286
918
|
unavailable: 503
|
|
287
919
|
};
|
|
288
920
|
const isCategory = (value) => typeof value === "string" && ERROR_CATEGORIES.includes(value);
|
|
289
|
-
function fieldsOf(err) {
|
|
921
|
+
function fieldsOf$2(err) {
|
|
290
922
|
if (typeof err !== "object" || err === null) return null;
|
|
291
923
|
const { code, category, retryable, requestId, retryAfterMs } = err;
|
|
292
924
|
if (typeof code !== "string") return null;
|
|
@@ -304,7 +936,7 @@ function fieldsOf(err) {
|
|
|
304
936
|
const retryAfter = (fields) => fields.retryAfterMs === void 0 ? {} : { "Retry-After": String(Math.max(1, Math.ceil(fields.retryAfterMs / 1e3))) };
|
|
305
937
|
function httpStatusFor(err, options = {}) {
|
|
306
938
|
const logger = options.logger ?? console;
|
|
307
|
-
const fields = fieldsOf(err);
|
|
939
|
+
const fields = fieldsOf$2(err);
|
|
308
940
|
if (!fields) return {
|
|
309
941
|
status: 500,
|
|
310
942
|
headers: {}
|
|
@@ -356,6 +988,46 @@ function httpStatusFor(err, options = {}) {
|
|
|
356
988
|
};
|
|
357
989
|
}
|
|
358
990
|
//#endregion
|
|
991
|
+
//#region src/meter-catalog.ts
|
|
992
|
+
function meterCatalogFacts(input) {
|
|
993
|
+
const meters = new Map(Object.entries(input.meters));
|
|
994
|
+
const credits = /* @__PURE__ */ new Map();
|
|
995
|
+
const creditsOf = (plan) => {
|
|
996
|
+
const known = credits.get(plan);
|
|
997
|
+
if (known !== void 0) return known;
|
|
998
|
+
const { benefits } = resolveBenefits({
|
|
999
|
+
catalog: input,
|
|
1000
|
+
subscriptions: plan === input.defaultPlanCode ? [] : [{
|
|
1001
|
+
planCode: plan,
|
|
1002
|
+
status: "active"
|
|
1003
|
+
}]
|
|
1004
|
+
});
|
|
1005
|
+
const granted = meterCreditsOf(benefits, { defaultPlanCode: input.defaultPlanCode });
|
|
1006
|
+
const out = new Map([...granted].map(([meter, credit]) => [meter, credit.unitsMicro]));
|
|
1007
|
+
credits.set(plan, out);
|
|
1008
|
+
return out;
|
|
1009
|
+
};
|
|
1010
|
+
const billableOf = (plan, meter) => plan !== input.defaultPlanCode && input.meteredPrices.some((price) => price.planCode === plan && price.meter === meter);
|
|
1011
|
+
const stateBlockOf = (plan) => {
|
|
1012
|
+
const planCredits = creditsOf(plan);
|
|
1013
|
+
return Object.fromEntries([...meters.keys()].map((code) => {
|
|
1014
|
+
const credit = planCredits.get(code);
|
|
1015
|
+
return [code, {
|
|
1016
|
+
includedUnits: credit === void 0 ? null : fromMicroUnits(credit),
|
|
1017
|
+
billable: billableOf(plan, code),
|
|
1018
|
+
standing: "ok"
|
|
1019
|
+
}];
|
|
1020
|
+
}));
|
|
1021
|
+
};
|
|
1022
|
+
return {
|
|
1023
|
+
defaultPlan: input.defaultPlanCode,
|
|
1024
|
+
meters,
|
|
1025
|
+
creditsOf,
|
|
1026
|
+
billableOf,
|
|
1027
|
+
stateBlockOf
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
//#endregion
|
|
359
1031
|
//#region src/catalog.ts
|
|
360
1032
|
const feature = (type, options) => options?.description === void 0 ? { type } : {
|
|
361
1033
|
type,
|
|
@@ -367,6 +1039,75 @@ const limit = (options) => feature("limit", options);
|
|
|
367
1039
|
const flag = (options) => feature("boolean", options);
|
|
368
1040
|
/** Serbest metin ayarı. */
|
|
369
1041
|
const text = (options) => feature("string", options);
|
|
1042
|
+
/**
|
|
1043
|
+
* A `feature_flag` benefit: feature key → value it grants, e.g. `featureFlag({ sso: true })`.
|
|
1044
|
+
* Keys are catalog features and values follow the feature's type (checked by `defineCatalog`).
|
|
1045
|
+
*/
|
|
1046
|
+
function featureFlag(properties, options) {
|
|
1047
|
+
const copy = { ...properties };
|
|
1048
|
+
const description = options?.description;
|
|
1049
|
+
return description === void 0 ? {
|
|
1050
|
+
type: "feature_flag",
|
|
1051
|
+
properties: copy
|
|
1052
|
+
} : {
|
|
1053
|
+
type: "feature_flag",
|
|
1054
|
+
properties: copy,
|
|
1055
|
+
description: { ...description }
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* A `meter_credit` benefit (metering phase 2, M5): `meterCredit("ai_tokens", { units: 50_000 })`,
|
|
1060
|
+
* optionally `rollover: { capUnits }`. The meter must be a catalog meter (checked by `defineCatalog`);
|
|
1061
|
+
* units may be decimals with at most 6 places.
|
|
1062
|
+
*/
|
|
1063
|
+
function meterCredit(meter, options) {
|
|
1064
|
+
const given = isRecord(options) ? options : {};
|
|
1065
|
+
const rollover = given.rollover;
|
|
1066
|
+
const properties = {
|
|
1067
|
+
meter,
|
|
1068
|
+
units: given.units,
|
|
1069
|
+
...rollover !== void 0 ? { rolloverCapUnits: isRecord(rollover) ? rollover.capUnits : null } : {}
|
|
1070
|
+
};
|
|
1071
|
+
const description = given.description;
|
|
1072
|
+
return description === void 0 ? {
|
|
1073
|
+
type: "meter_credit",
|
|
1074
|
+
properties
|
|
1075
|
+
} : {
|
|
1076
|
+
type: "meter_credit",
|
|
1077
|
+
properties,
|
|
1078
|
+
description: copyOf(description)
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Aggregations (M4): `count()` events, `sum(key)` / `max(key)` of the finite, non-negative number
|
|
1083
|
+
* at `metadata.<key>` (decimals allowed), `unique(key)` distinct values at `metadata.<key>`.
|
|
1084
|
+
*/
|
|
1085
|
+
const count = () => ({ kind: "count" });
|
|
1086
|
+
const sum = (key) => ({
|
|
1087
|
+
kind: "sum",
|
|
1088
|
+
key
|
|
1089
|
+
});
|
|
1090
|
+
const max = (key) => ({
|
|
1091
|
+
kind: "max",
|
|
1092
|
+
key
|
|
1093
|
+
});
|
|
1094
|
+
const unique = (key) => ({
|
|
1095
|
+
kind: "unique",
|
|
1096
|
+
key
|
|
1097
|
+
});
|
|
1098
|
+
function meter(options) {
|
|
1099
|
+
const given = isRecord(options) ? options : {};
|
|
1100
|
+
const spec = {
|
|
1101
|
+
aggregation: copyDeep(given.aggregate),
|
|
1102
|
+
label: copyDeep(given.label)
|
|
1103
|
+
};
|
|
1104
|
+
if (given.event !== void 0) spec.event = given.event;
|
|
1105
|
+
if (given.filter !== void 0) spec.filter = copyDeep(given.filter);
|
|
1106
|
+
if (given.unitLabel !== void 0) spec.unitLabel = copyDeep(given.unitLabel);
|
|
1107
|
+
if (given.displayPer !== void 0) spec.displayPer = given.displayPer;
|
|
1108
|
+
if (given.lowAt !== void 0) spec.lowAt = given.lowAt;
|
|
1109
|
+
return spec;
|
|
1110
|
+
}
|
|
370
1111
|
function matches(type, value) {
|
|
371
1112
|
switch (type) {
|
|
372
1113
|
case "limit": return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
@@ -379,9 +1120,35 @@ const EXPECTED = {
|
|
|
379
1120
|
boolean: "boolean",
|
|
380
1121
|
string: "string"
|
|
381
1122
|
};
|
|
1123
|
+
const EXPECTED_EN = {
|
|
1124
|
+
limit: "non-negative integer or null",
|
|
1125
|
+
boolean: "boolean",
|
|
1126
|
+
string: "string"
|
|
1127
|
+
};
|
|
382
1128
|
const show = (value) => value === void 0 ? "yok" : JSON.stringify(value);
|
|
1129
|
+
const showValue = (value) => value === void 0 ? "nothing" : JSON.stringify(value);
|
|
383
1130
|
const has = (object, key) => Object.hasOwn(object, key);
|
|
384
1131
|
const isObject = (value) => typeof value === "object" && value !== null;
|
|
1132
|
+
const isRecord = (value) => isObject(value) && !Array.isArray(value);
|
|
1133
|
+
const copyOf = (value) => Array.isArray(value) ? [...value] : isObject(value) ? { ...value } : value;
|
|
1134
|
+
/** Deep copy of plain JSON-like data (arrays and objects); other values as they are. */
|
|
1135
|
+
const copyDeep = (value) => Array.isArray(value) ? value.map(copyDeep) : isObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, copyDeep(v)])) : value;
|
|
1136
|
+
/** Deep freeze of data made by `copyDeep`. */
|
|
1137
|
+
const freezeDeep = (value) => {
|
|
1138
|
+
if (isObject(value)) {
|
|
1139
|
+
for (const inner of Object.values(value)) freezeDeep(inner);
|
|
1140
|
+
Object.freeze(value);
|
|
1141
|
+
}
|
|
1142
|
+
return value;
|
|
1143
|
+
};
|
|
1144
|
+
const benefitCopy = ({ type, properties, description }) => description === void 0 ? {
|
|
1145
|
+
type,
|
|
1146
|
+
properties: { ...properties }
|
|
1147
|
+
} : {
|
|
1148
|
+
type,
|
|
1149
|
+
properties: { ...properties },
|
|
1150
|
+
description: copyOf(description)
|
|
1151
|
+
};
|
|
385
1152
|
function invalid(problems) {
|
|
386
1153
|
return new StewardConfigError({
|
|
387
1154
|
code: "invalid_catalog",
|
|
@@ -402,7 +1169,39 @@ function defineCatalog(definition) {
|
|
|
402
1169
|
if (!isObject(input.features)) problems.push("features: nesne bekleniyor");
|
|
403
1170
|
if (!isObject(input.plans)) problems.push("plans: nesne bekleniyor");
|
|
404
1171
|
if (!Array.isArray(input.prices)) problems.push("prices: dizi bekleniyor");
|
|
1172
|
+
if (input.benefits !== void 0 && !isRecord(input.benefits)) problems.push("benefits: object expected");
|
|
1173
|
+
if (input.meters !== void 0 && !isRecord(input.meters)) problems.push("meters: object expected");
|
|
1174
|
+
if (input.meteredPrices !== void 0 && !Array.isArray(input.meteredPrices)) problems.push("meteredPrices: array expected");
|
|
405
1175
|
if (problems.length > 0) throw invalid(problems);
|
|
1176
|
+
const meters = /* @__PURE__ */ new Map();
|
|
1177
|
+
for (const [code, spec] of Object.entries(input.meters ?? {})) {
|
|
1178
|
+
if (!isRecord(spec) || !isRecord(spec.aggregation) || typeof spec.aggregation.kind !== "string") {
|
|
1179
|
+
problems.push(`meters.${code}: meter({ aggregate: count() | sum(key) | max(key) | unique(key), … }) expected`);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
meters.set(code, copyDeep(spec));
|
|
1183
|
+
}
|
|
1184
|
+
const benefitCodes = new Set(Object.keys(input.benefits ?? {}));
|
|
1185
|
+
const benefits = /* @__PURE__ */ new Map();
|
|
1186
|
+
for (const [code, spec] of Object.entries(input.benefits ?? {})) {
|
|
1187
|
+
if (!isRecord(spec) || spec.type !== "feature_flag" && spec.type !== "meter_credit" || !isRecord(spec.properties)) {
|
|
1188
|
+
problems.push(`benefits.${code}: featureFlag() or meterCredit() expected`);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (spec.type === "meter_credit") {
|
|
1192
|
+
const meterCode = spec.properties.meter;
|
|
1193
|
+
if (typeof meterCode !== "string" || !meters.has(meterCode)) problems.push(`benefits.${code}.properties.meter: undefined meter ${showValue(meterCode)}`);
|
|
1194
|
+
} else for (const [key, value] of Object.entries(spec.properties)) {
|
|
1195
|
+
const feature = features.get(key);
|
|
1196
|
+
if (feature === void 0) problems.push(`benefits.${code}.properties.${key}: undefined feature`);
|
|
1197
|
+
else if (!matches(feature.type, value)) problems.push(`benefits.${code}.properties.${key}: ${EXPECTED_EN[feature.type]} expected, got ${showValue(value)}`);
|
|
1198
|
+
}
|
|
1199
|
+
benefits.set(code, benefitCopy({
|
|
1200
|
+
type: spec.type,
|
|
1201
|
+
properties: spec.properties,
|
|
1202
|
+
...spec.description !== void 0 ? { description: spec.description } : {}
|
|
1203
|
+
}));
|
|
1204
|
+
}
|
|
406
1205
|
const plans = /* @__PURE__ */ new Map();
|
|
407
1206
|
for (const [code, plan] of Object.entries(input.plans)) {
|
|
408
1207
|
const given = isObject(plan) && isObject(plan.entitlements) ? plan.entitlements : {};
|
|
@@ -413,13 +1212,25 @@ function defineCatalog(definition) {
|
|
|
413
1212
|
entitlements[key] = given[key];
|
|
414
1213
|
}
|
|
415
1214
|
for (const key of Object.keys(given).filter((k) => !features.has(k))) problems.push(`plans.${code}.entitlements.${key}: tanımsız feature`);
|
|
416
|
-
const { name, rank, sellable } = isObject(plan) ? plan : {};
|
|
417
|
-
|
|
1215
|
+
const { name, rank, sellable, benefits: listed } = isObject(plan) ? plan : {};
|
|
1216
|
+
const runtime = {
|
|
418
1217
|
name,
|
|
419
|
-
rank,
|
|
420
1218
|
sellable,
|
|
421
1219
|
entitlements
|
|
422
|
-
}
|
|
1220
|
+
};
|
|
1221
|
+
if (rank !== void 0) runtime.rank = rank;
|
|
1222
|
+
if (listed !== void 0) {
|
|
1223
|
+
if (Array.isArray(listed)) {
|
|
1224
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1225
|
+
for (const [i, benefit] of listed.entries()) {
|
|
1226
|
+
if (typeof benefit !== "string" || !benefitCodes.has(benefit)) problems.push(`plans.${code}.benefits.${i}: undefined benefit ${showValue(benefit)}`);
|
|
1227
|
+
else if (seen.has(benefit)) problems.push(`plans.${code}.benefits.${i}: duplicate benefit ${benefit}`);
|
|
1228
|
+
seen.add(benefit);
|
|
1229
|
+
}
|
|
1230
|
+
runtime.benefits = [...listed];
|
|
1231
|
+
} else problems.push(`plans.${code}.benefits: array of benefit codes expected`);
|
|
1232
|
+
}
|
|
1233
|
+
plans.set(code, runtime);
|
|
423
1234
|
}
|
|
424
1235
|
if (typeof input.defaultPlan !== "string" || !plans.has(input.defaultPlan)) problems.push(`defaultPlan: ${show(input.defaultPlan)} planlarda yok`);
|
|
425
1236
|
const prices = input.prices.map((price, i) => {
|
|
@@ -427,6 +1238,12 @@ function defineCatalog(definition) {
|
|
|
427
1238
|
if (typeof copy.plan !== "string" || !plans.has(copy.plan)) problems.push(`prices.${i}.plan: ${show(copy.plan)} planlarda yok`);
|
|
428
1239
|
return copy;
|
|
429
1240
|
});
|
|
1241
|
+
const meteredPrices = (input.meteredPrices ?? []).map((price, i) => {
|
|
1242
|
+
const copy = { ...isObject(price) ? price : {} };
|
|
1243
|
+
if (typeof copy.plan !== "string" || !plans.has(copy.plan)) problems.push(`meteredPrices.${i}.plan: undefined plan ${showValue(copy.plan)}`);
|
|
1244
|
+
if (typeof copy.meter !== "string" || !meters.has(copy.meter)) problems.push(`meteredPrices.${i}.meter: undefined meter ${showValue(copy.meter)}`);
|
|
1245
|
+
return copy;
|
|
1246
|
+
});
|
|
430
1247
|
if (problems.length > 0) throw invalid(problems);
|
|
431
1248
|
const toSyncInput = () => ({
|
|
432
1249
|
defaultPlanCode: input.defaultPlan,
|
|
@@ -434,15 +1251,24 @@ function defineCatalog(definition) {
|
|
|
434
1251
|
key,
|
|
435
1252
|
...spec
|
|
436
1253
|
})),
|
|
437
|
-
plans: [...plans].map(([code,
|
|
1254
|
+
plans: [...plans].map(([code, { name, rank, sellable, entitlements, benefits: listed }]) => ({
|
|
438
1255
|
code,
|
|
439
|
-
|
|
440
|
-
|
|
1256
|
+
name,
|
|
1257
|
+
rank: rank === void 0 ? 0 : rank,
|
|
1258
|
+
sellable,
|
|
1259
|
+
entitlements: { ...entitlements },
|
|
1260
|
+
...listed !== void 0 && listed.length > 0 ? { benefits: [...listed] } : {}
|
|
441
1261
|
})),
|
|
442
1262
|
prices: prices.map(({ plan, ...rest }) => ({
|
|
443
1263
|
planCode: plan,
|
|
444
1264
|
...rest
|
|
445
|
-
}))
|
|
1265
|
+
})),
|
|
1266
|
+
...benefits.size > 0 ? { benefits: Object.fromEntries([...benefits].map(([code, benefit]) => [code, benefitCopy(benefit)])) } : {},
|
|
1267
|
+
...meters.size > 0 ? { meters: Object.fromEntries([...meters].map(([code, spec]) => [code, copyDeep(spec)])) } : {},
|
|
1268
|
+
...meteredPrices.length > 0 ? { meteredPrices: meteredPrices.map(({ plan, ...rest }) => ({
|
|
1269
|
+
planCode: plan,
|
|
1270
|
+
...rest
|
|
1271
|
+
})) } : {}
|
|
446
1272
|
});
|
|
447
1273
|
const parsed = CatalogSyncInputSchema.safeParse(toSyncInput());
|
|
448
1274
|
if (!parsed.success) {
|
|
@@ -461,7 +1287,14 @@ function defineCatalog(definition) {
|
|
|
461
1287
|
}
|
|
462
1288
|
const defaultPlan = input.defaultPlan;
|
|
463
1289
|
const isPlan = (code) => typeof code === "string" && plans.has(code);
|
|
464
|
-
const
|
|
1290
|
+
const resolved = new Map([...plans.keys()].map((code) => [code, Object.freeze(resolveEntitlements({
|
|
1291
|
+
catalog: parsed.data,
|
|
1292
|
+
subscription: code === defaultPlan ? null : {
|
|
1293
|
+
planCode: code,
|
|
1294
|
+
status: "active"
|
|
1295
|
+
}
|
|
1296
|
+
}).entitlements)]));
|
|
1297
|
+
const planEntitlements = (code) => resolved.get(code);
|
|
465
1298
|
const entitlementsOf = (plan) => {
|
|
466
1299
|
if (!isPlan(plan)) throw new StewardConfigError({
|
|
467
1300
|
code: "unknown_plan",
|
|
@@ -483,6 +1316,16 @@ function defineCatalog(definition) {
|
|
|
483
1316
|
if (fellBack.length > 0) options.onFallback?.(fellBack);
|
|
484
1317
|
return result;
|
|
485
1318
|
};
|
|
1319
|
+
const facts = meters.size > 0 ? meterCatalogFacts(parsed.data) : null;
|
|
1320
|
+
const metersBlocks = /* @__PURE__ */ new Map();
|
|
1321
|
+
const metersBlockOf = (plan) => {
|
|
1322
|
+
let block = metersBlocks.get(plan);
|
|
1323
|
+
if (block === void 0) {
|
|
1324
|
+
block = facts.stateBlockOf(plan);
|
|
1325
|
+
metersBlocks.set(plan, block);
|
|
1326
|
+
}
|
|
1327
|
+
return Object.fromEntries(Object.entries(block).map(([code, meterState]) => [code, { ...meterState }]));
|
|
1328
|
+
};
|
|
486
1329
|
const stateOf = (ref, plan = defaultPlan, options = {}) => ({
|
|
487
1330
|
ref,
|
|
488
1331
|
displayName: options.displayName ?? ref,
|
|
@@ -497,19 +1340,32 @@ function defineCatalog(definition) {
|
|
|
497
1340
|
profile: {
|
|
498
1341
|
present: false,
|
|
499
1342
|
kind: null
|
|
500
|
-
}
|
|
1343
|
+
},
|
|
1344
|
+
...facts !== null ? { meters: metersBlockOf(plan) } : {}
|
|
501
1345
|
});
|
|
502
1346
|
const frozen = (value) => Object.freeze(value);
|
|
1347
|
+
const frozenBenefit = ({ properties, description, ...rest }) => frozen({
|
|
1348
|
+
...rest,
|
|
1349
|
+
properties: frozen({ ...properties }),
|
|
1350
|
+
...isObject(description) ? { description: frozen({ ...description }) } : {}
|
|
1351
|
+
});
|
|
503
1352
|
return frozen({
|
|
504
1353
|
defaultPlan,
|
|
505
1354
|
features: frozen(Object.fromEntries([...features].map(([key, spec]) => [key, frozen({ ...spec })]))),
|
|
506
|
-
plans: frozen(Object.fromEntries([...plans].map(([code, plan]) => [code, frozen({
|
|
1355
|
+
plans: frozen(Object.fromEntries([...plans].map(([code, { benefits: listed, ...plan }]) => [code, frozen({
|
|
507
1356
|
...plan,
|
|
508
|
-
entitlements: frozen({ ...plan.entitlements })
|
|
1357
|
+
entitlements: frozen({ ...plan.entitlements }),
|
|
1358
|
+
...listed !== void 0 ? { benefits: frozen([...listed]) } : {}
|
|
509
1359
|
})]))),
|
|
510
1360
|
prices: frozen(prices.map((price) => frozen({ ...price }))),
|
|
1361
|
+
benefits: frozen(Object.fromEntries([...benefits].map(([code, benefit]) => [code, frozenBenefit(benefit)]))),
|
|
1362
|
+
meters: frozen(Object.fromEntries([...meters].map(([code, spec]) => [code, freezeDeep(copyDeep(spec))]))),
|
|
1363
|
+
meteredPrices: frozen(meteredPrices.map((price) => frozen({ ...price }))),
|
|
511
1364
|
$plan: void 0,
|
|
512
1365
|
$entitlements: void 0,
|
|
1366
|
+
$benefit: void 0,
|
|
1367
|
+
$meter: void 0,
|
|
1368
|
+
$event: void 0,
|
|
513
1369
|
toSyncInput,
|
|
514
1370
|
entitlementsOf,
|
|
515
1371
|
resolve,
|
|
@@ -518,4 +1374,781 @@ function defineCatalog(definition) {
|
|
|
518
1374
|
});
|
|
519
1375
|
}
|
|
520
1376
|
//#endregion
|
|
521
|
-
|
|
1377
|
+
//#region src/events.ts
|
|
1378
|
+
/** How long an acknowledged event stays pending: the meter rollup lags behind ingestion (≤ 10 s target). */
|
|
1379
|
+
const EVENTS_RECENT_MS = 6e4;
|
|
1380
|
+
const MAX_BACKOFF_MS = 3e4;
|
|
1381
|
+
const BASE_BACKOFF_MS = 1e3;
|
|
1382
|
+
const WARN_EVERY_MS = 1e4;
|
|
1383
|
+
/** Room for `{"events":[` … `]}` and separators within the ingest body limit. */
|
|
1384
|
+
const BATCH_BYTES = EVENTS_INGEST_MAX_BYTES - 1024;
|
|
1385
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1386
|
+
const describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
1387
|
+
function fieldsOf$1(error) {
|
|
1388
|
+
const { code, retryable, retryAfterMs } = isPlainObject(error) || error instanceof Error ? error : {};
|
|
1389
|
+
return {
|
|
1390
|
+
code: typeof code === "string" ? code : null,
|
|
1391
|
+
retryable: typeof retryable === "boolean" ? retryable : null,
|
|
1392
|
+
retryAfterMs: typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) ? retryAfterMs : null
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function isTransient(error) {
|
|
1396
|
+
const { code, retryable } = fieldsOf$1(error);
|
|
1397
|
+
return retryable === true || code === "invalid_response";
|
|
1398
|
+
}
|
|
1399
|
+
function utf8Length(text) {
|
|
1400
|
+
let bytes = 0;
|
|
1401
|
+
for (let i = 0; i < text.length; i++) {
|
|
1402
|
+
const c = text.charCodeAt(i);
|
|
1403
|
+
if (c < 128) bytes += 1;
|
|
1404
|
+
else if (c < 2048) bytes += 2;
|
|
1405
|
+
else if (c >= 55296 && c <= 56319) {
|
|
1406
|
+
bytes += 4;
|
|
1407
|
+
i++;
|
|
1408
|
+
} else bytes += 3;
|
|
1409
|
+
}
|
|
1410
|
+
return bytes;
|
|
1411
|
+
}
|
|
1412
|
+
function positive(name, value, problems, { integer = false, max = Number.POSITIVE_INFINITY } = {}) {
|
|
1413
|
+
if (!(Number.isFinite(value) && value > 0 && value <= max && (!integer || Number.isInteger(value)))) problems.push(`${name} must be a positive ${integer ? "integer" : "number"}${Number.isFinite(max) ? ` ≤ ${max}` : ""}: ${String(value)}`);
|
|
1414
|
+
}
|
|
1415
|
+
/** Events tracker (M16); see the comment above. */
|
|
1416
|
+
function Events(options) {
|
|
1417
|
+
const flushIntervalMs = options.flushIntervalMs ?? 2e3;
|
|
1418
|
+
const maxBatch = options.maxBatch ?? 500;
|
|
1419
|
+
const maxQueue = options.maxQueue ?? 1e4;
|
|
1420
|
+
const autoFlush = options.autoFlush ?? true;
|
|
1421
|
+
const problems = [];
|
|
1422
|
+
if (typeof options.steward?.events?.ingest !== "function") problems.push("steward.events.ingest is required (createSteward())");
|
|
1423
|
+
positive("flushIntervalMs", flushIntervalMs, problems);
|
|
1424
|
+
positive("maxBatch", maxBatch, problems, {
|
|
1425
|
+
integer: true,
|
|
1426
|
+
max: EVENTS_INGEST_MAX_EVENTS
|
|
1427
|
+
});
|
|
1428
|
+
positive("maxQueue", maxQueue, problems, { integer: true });
|
|
1429
|
+
if (typeof autoFlush !== "boolean") problems.push("autoFlush must be a boolean");
|
|
1430
|
+
if (options.actorRef !== void 0 && !ActorRefSchema.safeParse(options.actorRef).success) problems.push("actorRef format is invalid (kind[:id], e.g. service:api)");
|
|
1431
|
+
if (problems.length > 0) throw new StewardConfigError({ message: `Events: ${problems.join("; ")}` });
|
|
1432
|
+
const { steward, actorRef, onRejected, onDrop } = options;
|
|
1433
|
+
const logger = options.logger ?? console;
|
|
1434
|
+
const now = options.now ?? Date.now;
|
|
1435
|
+
const random = options.random ?? Math.random;
|
|
1436
|
+
/** Waiting events, oldest first. */
|
|
1437
|
+
let queue = [];
|
|
1438
|
+
/** The first `size` queued events were sent with `key` before (a failed batch): the next attempt sends exactly them with it. */
|
|
1439
|
+
let head = null;
|
|
1440
|
+
/** Acknowledged events kept pending for `EVENTS_RECENT_MS`, oldest first. */
|
|
1441
|
+
let recent = [];
|
|
1442
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
1443
|
+
let failures = 0;
|
|
1444
|
+
let retryAt = 0;
|
|
1445
|
+
let timer = null;
|
|
1446
|
+
let chain = Promise.resolve();
|
|
1447
|
+
let runs = 0;
|
|
1448
|
+
let dropWarnedAt = Number.NEGATIVE_INFINITY;
|
|
1449
|
+
let droppedSinceWarn = 0;
|
|
1450
|
+
const index = (entry) => {
|
|
1451
|
+
const ref = entry.event.accountRef;
|
|
1452
|
+
let set = byRef.get(ref);
|
|
1453
|
+
if (set === void 0) {
|
|
1454
|
+
set = /* @__PURE__ */ new Set();
|
|
1455
|
+
byRef.set(ref, set);
|
|
1456
|
+
}
|
|
1457
|
+
set.add(entry);
|
|
1458
|
+
};
|
|
1459
|
+
const unindex = (entry) => {
|
|
1460
|
+
const ref = entry.event.accountRef;
|
|
1461
|
+
const set = byRef.get(ref);
|
|
1462
|
+
if (set === void 0) return;
|
|
1463
|
+
set.delete(entry);
|
|
1464
|
+
if (set.size === 0) byRef.delete(ref);
|
|
1465
|
+
};
|
|
1466
|
+
function hook(name, run) {
|
|
1467
|
+
try {
|
|
1468
|
+
run();
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
logger.warn(`Events ${name} threw`, { error: describeError(error) });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function drop(events, info) {
|
|
1474
|
+
if (onDrop !== void 0) hook("onDrop", () => onDrop(events, info));
|
|
1475
|
+
}
|
|
1476
|
+
function pruneRecent() {
|
|
1477
|
+
const oldest = now() - EVENTS_RECENT_MS;
|
|
1478
|
+
let cut = 0;
|
|
1479
|
+
while (cut < recent.length && (recent[cut].ackedAt < oldest || recent.length - cut > maxQueue)) cut++;
|
|
1480
|
+
if (cut === 0) return;
|
|
1481
|
+
for (const entry of recent.slice(0, cut)) unindex(entry);
|
|
1482
|
+
recent = recent.slice(cut);
|
|
1483
|
+
}
|
|
1484
|
+
/** Queue over `maxQueue`: the oldest events go to `onDrop`. A shortened failed batch gets a new key (its composition changed). */
|
|
1485
|
+
function enforceLimit() {
|
|
1486
|
+
const excess = queue.length - maxQueue;
|
|
1487
|
+
if (excess <= 0) return;
|
|
1488
|
+
const dropped = queue.splice(0, excess);
|
|
1489
|
+
for (const entry of dropped) unindex(entry);
|
|
1490
|
+
if (head !== null) {
|
|
1491
|
+
const size = head.size - Math.min(excess, head.size);
|
|
1492
|
+
head = size > 0 ? {
|
|
1493
|
+
key: globalThis.crypto.randomUUID(),
|
|
1494
|
+
size
|
|
1495
|
+
} : null;
|
|
1496
|
+
}
|
|
1497
|
+
droppedSinceWarn += dropped.length;
|
|
1498
|
+
const t = now();
|
|
1499
|
+
if (t - dropWarnedAt >= WARN_EVERY_MS) {
|
|
1500
|
+
logger.warn("Events queue full; the oldest events were dropped (onDrop)", {
|
|
1501
|
+
dropped: droppedSinceWarn,
|
|
1502
|
+
maxQueue
|
|
1503
|
+
});
|
|
1504
|
+
dropWarnedAt = t;
|
|
1505
|
+
droppedSinceWarn = 0;
|
|
1506
|
+
}
|
|
1507
|
+
drop(dropped.map((entry) => entry.event), { reason: "queue_full" });
|
|
1508
|
+
}
|
|
1509
|
+
function takeBatch() {
|
|
1510
|
+
let size = 0;
|
|
1511
|
+
let key;
|
|
1512
|
+
if (head !== null) {
|
|
1513
|
+
size = Math.min(head.size, queue.length);
|
|
1514
|
+
key = head.key;
|
|
1515
|
+
} else {
|
|
1516
|
+
let bytes = 0;
|
|
1517
|
+
while (size < queue.length && size < maxBatch && (size === 0 || bytes + queue[size].bytes + 1 <= BATCH_BYTES)) {
|
|
1518
|
+
bytes += queue[size].bytes + 1;
|
|
1519
|
+
size++;
|
|
1520
|
+
}
|
|
1521
|
+
key = globalThis.crypto.randomUUID();
|
|
1522
|
+
}
|
|
1523
|
+
head = null;
|
|
1524
|
+
const entries = queue.splice(0, size);
|
|
1525
|
+
for (const entry of entries) entry.state = "sending";
|
|
1526
|
+
return {
|
|
1527
|
+
key,
|
|
1528
|
+
entries
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function acknowledge(batch, result) {
|
|
1532
|
+
const rejectedAt = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const rejection of result.rejected) if (rejection.index >= 0 && rejection.index < batch.entries.length) rejectedAt.set(rejection.index, rejection);
|
|
1534
|
+
const ackedAt = now();
|
|
1535
|
+
const rejected = [];
|
|
1536
|
+
batch.entries.forEach((entry, i) => {
|
|
1537
|
+
const rejection = rejectedAt.get(i);
|
|
1538
|
+
if (rejection !== void 0) {
|
|
1539
|
+
unindex(entry);
|
|
1540
|
+
rejected.push({
|
|
1541
|
+
...rejection,
|
|
1542
|
+
event: entry.event
|
|
1543
|
+
});
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
entry.state = "acked";
|
|
1547
|
+
entry.ackedAt = ackedAt;
|
|
1548
|
+
recent.push(entry);
|
|
1549
|
+
});
|
|
1550
|
+
pruneRecent();
|
|
1551
|
+
if (rejected.length > 0) {
|
|
1552
|
+
const codes = [...new Set(rejected.map((r) => r.code))];
|
|
1553
|
+
logger.warn("steward rejected events (not retried; onRejected)", {
|
|
1554
|
+
rejected: rejected.length,
|
|
1555
|
+
codes
|
|
1556
|
+
});
|
|
1557
|
+
if (onRejected !== void 0) hook("onRejected", () => onRejected(rejected));
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function backoffMs(retryAfterMs) {
|
|
1561
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.min(failures - 1, 16));
|
|
1562
|
+
const jittered = Math.round(ceiling / 2 + random() * ceiling / 2);
|
|
1563
|
+
return Math.min(MAX_BACKOFF_MS, Math.max(jittered, retryAfterMs ?? 0));
|
|
1564
|
+
}
|
|
1565
|
+
/** Sends one batch; `false` when sending should pause (transient failure). */
|
|
1566
|
+
async function send(batch) {
|
|
1567
|
+
try {
|
|
1568
|
+
const result = await steward.events.ingest({ events: batch.entries.map((entry) => entry.event) }, {
|
|
1569
|
+
idempotencyKey: batch.key,
|
|
1570
|
+
...actorRef !== void 0 ? { actorRef } : {}
|
|
1571
|
+
});
|
|
1572
|
+
failures = 0;
|
|
1573
|
+
retryAt = 0;
|
|
1574
|
+
acknowledge(batch, result);
|
|
1575
|
+
return true;
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
const { code, retryAfterMs } = fieldsOf$1(error);
|
|
1578
|
+
if (isTransient(error)) {
|
|
1579
|
+
for (const entry of batch.entries) entry.state = "queued";
|
|
1580
|
+
queue = [...batch.entries, ...queue];
|
|
1581
|
+
head = {
|
|
1582
|
+
key: batch.key,
|
|
1583
|
+
size: batch.entries.length
|
|
1584
|
+
};
|
|
1585
|
+
failures += 1;
|
|
1586
|
+
const delay = backoffMs(retryAfterMs);
|
|
1587
|
+
retryAt = now() + delay;
|
|
1588
|
+
logger.warn("steward event ingest failed; the batch stays queued", {
|
|
1589
|
+
code,
|
|
1590
|
+
events: batch.entries.length,
|
|
1591
|
+
queued: queue.length,
|
|
1592
|
+
retryInMs: delay
|
|
1593
|
+
});
|
|
1594
|
+
enforceLimit();
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
for (const entry of batch.entries) unindex(entry);
|
|
1598
|
+
logger.warn("steward event ingest failed permanently; the batch was dropped (onDrop)", {
|
|
1599
|
+
code,
|
|
1600
|
+
events: batch.entries.length
|
|
1601
|
+
});
|
|
1602
|
+
drop(batch.entries.map((entry) => entry.event), {
|
|
1603
|
+
reason: "request_failed",
|
|
1604
|
+
message: code ?? describeError(error),
|
|
1605
|
+
error
|
|
1606
|
+
});
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async function drain(force) {
|
|
1611
|
+
while (queue.length > 0) {
|
|
1612
|
+
if (!force && now() < retryAt) return;
|
|
1613
|
+
if (!await send(takeBatch())) return;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function arm(delay) {
|
|
1617
|
+
if (!autoFlush || timer !== null || queue.length === 0) return;
|
|
1618
|
+
timer = setTimeout(() => {
|
|
1619
|
+
timer = null;
|
|
1620
|
+
schedule(false);
|
|
1621
|
+
}, Math.max(0, delay));
|
|
1622
|
+
timer.unref?.();
|
|
1623
|
+
}
|
|
1624
|
+
function rearm() {
|
|
1625
|
+
if (queue.length === 0) return;
|
|
1626
|
+
const t = now();
|
|
1627
|
+
arm(retryAt > t ? retryAt - t : flushIntervalMs);
|
|
1628
|
+
}
|
|
1629
|
+
function schedule(force) {
|
|
1630
|
+
runs += 1;
|
|
1631
|
+
const run = chain.then(() => drain(force)).catch((error) => logger.warn("Events flush failed", { error: describeError(error) })).finally(() => {
|
|
1632
|
+
runs -= 1;
|
|
1633
|
+
rearm();
|
|
1634
|
+
});
|
|
1635
|
+
chain = run;
|
|
1636
|
+
return run;
|
|
1637
|
+
}
|
|
1638
|
+
function normalizedTimestamp(timestamp) {
|
|
1639
|
+
if (timestamp === void 0) return new Date(now()).toISOString();
|
|
1640
|
+
if (!(timestamp instanceof Date)) return timestamp;
|
|
1641
|
+
return Number.isNaN(timestamp.getTime()) ? "Invalid Date" : timestamp.toISOString();
|
|
1642
|
+
}
|
|
1643
|
+
function eventOf(ref, name, metadata, externalId, timestamp) {
|
|
1644
|
+
return {
|
|
1645
|
+
accountRef: ref,
|
|
1646
|
+
name,
|
|
1647
|
+
externalId: externalId === void 0 ? globalThis.crypto.randomUUID() : externalId,
|
|
1648
|
+
timestamp: normalizedTimestamp(timestamp),
|
|
1649
|
+
...metadata !== void 0 ? { metadata: isPlainObject(metadata) ? { ...metadata } : metadata } : {}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function issuesOf(error) {
|
|
1653
|
+
return error.issues.map((issue) => `${issue.path.map(String).join(".") || "(event)"}: ${issue.message}`).join("; ");
|
|
1654
|
+
}
|
|
1655
|
+
return {
|
|
1656
|
+
track(ref, name, metadata, trackOptions) {
|
|
1657
|
+
let event = null;
|
|
1658
|
+
try {
|
|
1659
|
+
const given = isPlainObject(trackOptions) ? trackOptions : {};
|
|
1660
|
+
event = eventOf(ref, name, metadata, given.externalId, given.timestamp);
|
|
1661
|
+
const parsed = IngestEventSchema.safeParse(event);
|
|
1662
|
+
if (!parsed.success) {
|
|
1663
|
+
const message = issuesOf(parsed.error);
|
|
1664
|
+
logger.warn("Events.track: invalid event dropped (onDrop)", {
|
|
1665
|
+
name: typeof name === "string" ? name.slice(0, 128) : null,
|
|
1666
|
+
problems: message
|
|
1667
|
+
});
|
|
1668
|
+
drop([event], {
|
|
1669
|
+
reason: "invalid_event",
|
|
1670
|
+
message
|
|
1671
|
+
});
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const entry = {
|
|
1675
|
+
event: parsed.data,
|
|
1676
|
+
bytes: utf8Length(JSON.stringify(parsed.data)),
|
|
1677
|
+
state: "queued",
|
|
1678
|
+
ackedAt: null
|
|
1679
|
+
};
|
|
1680
|
+
queue.push(entry);
|
|
1681
|
+
index(entry);
|
|
1682
|
+
enforceLimit();
|
|
1683
|
+
if (!autoFlush) return;
|
|
1684
|
+
if (queue.length >= maxBatch && runs === 0 && now() >= retryAt) schedule(false);
|
|
1685
|
+
else arm(flushIntervalMs);
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
logger.warn("Events.track: event dropped", { error: describeError(error) });
|
|
1688
|
+
if (event !== null) drop([event], {
|
|
1689
|
+
reason: "invalid_event",
|
|
1690
|
+
message: describeError(error)
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
flush() {
|
|
1695
|
+
if (timer !== null) {
|
|
1696
|
+
clearTimeout(timer);
|
|
1697
|
+
timer = null;
|
|
1698
|
+
}
|
|
1699
|
+
return schedule(true);
|
|
1700
|
+
},
|
|
1701
|
+
async ingest(events, ingestOptions = {}) {
|
|
1702
|
+
let input;
|
|
1703
|
+
try {
|
|
1704
|
+
input = { events: (Array.isArray(events) ? events : []).map((e) => eventOf(e.accountRef, e.name, e.metadata, e.externalId, e.timestamp)) };
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
throw new StewardConfigError({
|
|
1707
|
+
code: "invalid_events",
|
|
1708
|
+
message: `Events.ingest: ${describeError(error)}`,
|
|
1709
|
+
cause: error
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
const parsed = EventsIngestInputSchema.safeParse(input);
|
|
1713
|
+
if (!parsed.success) throw new StewardConfigError({
|
|
1714
|
+
code: "invalid_events",
|
|
1715
|
+
message: `Events.ingest: ${issuesOf(parsed.error)}`
|
|
1716
|
+
});
|
|
1717
|
+
const entries = parsed.data.events.map((event) => ({
|
|
1718
|
+
event,
|
|
1719
|
+
bytes: 0,
|
|
1720
|
+
state: "sending",
|
|
1721
|
+
ackedAt: null
|
|
1722
|
+
}));
|
|
1723
|
+
for (const entry of entries) index(entry);
|
|
1724
|
+
try {
|
|
1725
|
+
const result = await steward.events.ingest(parsed.data, {
|
|
1726
|
+
idempotencyKey: ingestOptions.idempotencyKey ?? globalThis.crypto.randomUUID(),
|
|
1727
|
+
...actorRef !== void 0 ? { actorRef } : {}
|
|
1728
|
+
});
|
|
1729
|
+
const rejected = new Set(result.rejected.map((r) => r.index));
|
|
1730
|
+
const ackedAt = now();
|
|
1731
|
+
entries.forEach((entry, i) => {
|
|
1732
|
+
if (rejected.has(i)) {
|
|
1733
|
+
unindex(entry);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
entry.state = "acked";
|
|
1737
|
+
entry.ackedAt = ackedAt;
|
|
1738
|
+
recent.push(entry);
|
|
1739
|
+
});
|
|
1740
|
+
pruneRecent();
|
|
1741
|
+
return result;
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
for (const entry of entries) unindex(entry);
|
|
1744
|
+
throw error;
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
pending(ref) {
|
|
1748
|
+
pruneRecent();
|
|
1749
|
+
const set = byRef.get(ref);
|
|
1750
|
+
if (set === void 0) return [];
|
|
1751
|
+
return [...set].map((entry) => ({
|
|
1752
|
+
event: entry.event,
|
|
1753
|
+
ackedAt: entry.state === "acked" ? entry.ackedAt : null
|
|
1754
|
+
}));
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
//#endregion
|
|
1759
|
+
//#region src/meters.ts
|
|
1760
|
+
const FAILURE_BACKOFF_MS = 5e3;
|
|
1761
|
+
const STANDING_ORDER = {
|
|
1762
|
+
ok: 0,
|
|
1763
|
+
low: 1,
|
|
1764
|
+
exhausted: 2
|
|
1765
|
+
};
|
|
1766
|
+
const worse = (a, b) => STANDING_ORDER[b] > STANDING_ORDER[a] ? b : a;
|
|
1767
|
+
const micro = (value) => typeof value === "number" ? parseMicroUnits(value) ?? 0n : 0n;
|
|
1768
|
+
const isStanding = (value) => typeof value === "string" && Object.hasOwn(STANDING_ORDER, value);
|
|
1769
|
+
function fieldsOf(error) {
|
|
1770
|
+
const { code, httpStatus } = typeof error === "object" && error !== null ? error : {};
|
|
1771
|
+
return {
|
|
1772
|
+
code: typeof code === "string" ? code : null,
|
|
1773
|
+
httpStatus: typeof httpStatus === "number" ? httpStatus : null
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function isOutage(error) {
|
|
1777
|
+
const { code, httpStatus } = fieldsOf(error);
|
|
1778
|
+
if (httpStatus === 0) return code === "network_error" || code === "timeout";
|
|
1779
|
+
return httpStatus !== null && (httpStatus >= 500 || httpStatus === 429);
|
|
1780
|
+
}
|
|
1781
|
+
/** Rollup lag of a read (`asOf − rolledUpThrough`, steward's clock), ms; unknown → the whole recent window. */
|
|
1782
|
+
function rollupLagMs(body) {
|
|
1783
|
+
const lag = Date.parse(body.asOf) - Date.parse(body.rolledUpThrough);
|
|
1784
|
+
return Number.isFinite(lag) ? Math.min(Math.max(lag, 0), EVENTS_RECENT_MS) : EVENTS_RECENT_MS;
|
|
1785
|
+
}
|
|
1786
|
+
/** Units the pending events add to a meter whose period consumption is `consumed` (micro units). */
|
|
1787
|
+
function pendingMicroOf(def, events, cutoff, consumed) {
|
|
1788
|
+
let total = 0n;
|
|
1789
|
+
let largest = 0n;
|
|
1790
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
1791
|
+
for (const { event, ackedAt } of events) {
|
|
1792
|
+
if (ackedAt !== null && ackedAt < cutoff) continue;
|
|
1793
|
+
if (!matchesFilter(event, def.filter)) continue;
|
|
1794
|
+
const c = contributionOf(event, def.aggregation);
|
|
1795
|
+
switch (c.kind) {
|
|
1796
|
+
case "count":
|
|
1797
|
+
total += MICRO_UNITS_PER_UNIT;
|
|
1798
|
+
break;
|
|
1799
|
+
case "sum":
|
|
1800
|
+
total += c.micro;
|
|
1801
|
+
break;
|
|
1802
|
+
case "max":
|
|
1803
|
+
if (c.micro > largest) largest = c.micro;
|
|
1804
|
+
break;
|
|
1805
|
+
case "unique": distinct.add(c.value);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
switch (def.aggregation.kind) {
|
|
1809
|
+
case "max": return largest > consumed ? largest - consumed : 0n;
|
|
1810
|
+
case "unique": return BigInt(distinct.size) * MICRO_UNITS_PER_UNIT;
|
|
1811
|
+
default: return total;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/** Meter reader and gate (M13, M15, M16); see the comment above. */
|
|
1815
|
+
function Meters(options) {
|
|
1816
|
+
const ttlMs = options.ttlMs ?? 1e4;
|
|
1817
|
+
const staleIfErrorMs = options.staleIfErrorMs ?? 6e5;
|
|
1818
|
+
const maxEntries = options.maxEntries ?? 1e4;
|
|
1819
|
+
const policy = options.onUnavailable ?? "allow";
|
|
1820
|
+
const problems = [];
|
|
1821
|
+
if (typeof options.steward?.meters?.read !== "function") problems.push("steward.meters.read is required (createSteward())");
|
|
1822
|
+
if (typeof options.catalog?.toSyncInput !== "function") problems.push("catalog is required (defineCatalog())");
|
|
1823
|
+
if (!(Number.isFinite(ttlMs) && ttlMs >= 0)) problems.push(`ttlMs must be a non-negative number: ${ttlMs}`);
|
|
1824
|
+
if (!(Number.isFinite(staleIfErrorMs) && staleIfErrorMs >= 0)) problems.push(`staleIfErrorMs must be a non-negative number: ${staleIfErrorMs}`);
|
|
1825
|
+
if (!(Number.isInteger(maxEntries) && maxEntries >= 1)) problems.push(`maxEntries must be a positive integer: ${maxEntries}`);
|
|
1826
|
+
if (policy !== "allow" && policy !== "deny" && typeof policy !== "function") problems.push(`onUnavailable must be "allow", "deny" or a function: ${String(policy)}`);
|
|
1827
|
+
if (options.events !== void 0 && typeof options.events.pending !== "function") problems.push("events must be an Events() (pending)");
|
|
1828
|
+
if (options.cache !== void 0 && typeof options.cache.peek !== "function") problems.push("cache must be a stateCache() (peek)");
|
|
1829
|
+
if (problems.length > 0) throw new StewardConfigError({ message: `Meters: ${problems.join("; ")}` });
|
|
1830
|
+
const parsed = CatalogSyncInputSchema.safeParse(options.catalog.toSyncInput());
|
|
1831
|
+
if (!parsed.success) throw new StewardConfigError({
|
|
1832
|
+
code: "invalid_catalog",
|
|
1833
|
+
message: "Meters: the catalog is not valid (defineCatalog)"
|
|
1834
|
+
});
|
|
1835
|
+
const facts = meterCatalogFacts(parsed.data);
|
|
1836
|
+
if (facts.meters.size === 0) throw new StewardConfigError({ message: "Meters: the catalog defines no meters" });
|
|
1837
|
+
const defaultCredits = facts.creditsOf(facts.defaultPlan);
|
|
1838
|
+
const { steward, cache, events, onFallback } = options;
|
|
1839
|
+
const logger = options.logger ?? console;
|
|
1840
|
+
const now = options.now ?? Date.now;
|
|
1841
|
+
const backoffMs = Math.min(ttlMs, FAILURE_BACKOFF_MS);
|
|
1842
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1843
|
+
const flights = /* @__PURE__ */ new Map();
|
|
1844
|
+
const warnedMissing = /* @__PURE__ */ new Set();
|
|
1845
|
+
function put(ref, entry) {
|
|
1846
|
+
entries.delete(ref);
|
|
1847
|
+
entries.set(ref, entry);
|
|
1848
|
+
for (const oldest of entries.keys()) {
|
|
1849
|
+
if (entries.size <= maxEntries) break;
|
|
1850
|
+
entries.delete(oldest);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
function degrade(ref, entry, failure) {
|
|
1854
|
+
const t = now();
|
|
1855
|
+
if (entry.body !== null && t < entry.confirmedAt + ttlMs + staleIfErrorMs) {
|
|
1856
|
+
if (failure.warned !== "stale") {
|
|
1857
|
+
const { code } = fieldsOf(failure.error);
|
|
1858
|
+
logger.warn("steward meters unreadable; using the stale value", {
|
|
1859
|
+
ref,
|
|
1860
|
+
code,
|
|
1861
|
+
ageMs: t - entry.confirmedAt
|
|
1862
|
+
});
|
|
1863
|
+
failure.warned = "stale";
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
kind: "value",
|
|
1867
|
+
body: entry.body,
|
|
1868
|
+
bodyAt: entry.bodyAt,
|
|
1869
|
+
source: "stale"
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
kind: "fallback",
|
|
1874
|
+
error: failure.error
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
async function load(ref, flight) {
|
|
1878
|
+
const known = entries.get(ref);
|
|
1879
|
+
let read;
|
|
1880
|
+
try {
|
|
1881
|
+
read = await steward.meters.read(ref, known?.body && known.etag !== null ? { etag: known.etag } : {});
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
if (!isOutage(error)) throw error;
|
|
1884
|
+
const failure = {
|
|
1885
|
+
error,
|
|
1886
|
+
until: now() + backoffMs,
|
|
1887
|
+
warned: null
|
|
1888
|
+
};
|
|
1889
|
+
const current = entries.get(ref) ?? {
|
|
1890
|
+
body: null,
|
|
1891
|
+
etag: null,
|
|
1892
|
+
bodyAt: Number.NEGATIVE_INFINITY,
|
|
1893
|
+
confirmedAt: Number.NEGATIVE_INFINITY,
|
|
1894
|
+
expiresAt: Number.NEGATIVE_INFINITY,
|
|
1895
|
+
failure: null
|
|
1896
|
+
};
|
|
1897
|
+
current.failure = failure;
|
|
1898
|
+
put(ref, current);
|
|
1899
|
+
return degrade(ref, current, failure);
|
|
1900
|
+
}
|
|
1901
|
+
const t = now();
|
|
1902
|
+
let body;
|
|
1903
|
+
let bodyAt;
|
|
1904
|
+
if (!read.notModified) {
|
|
1905
|
+
body = read.meters;
|
|
1906
|
+
bodyAt = t;
|
|
1907
|
+
} else if (known?.body) {
|
|
1908
|
+
body = known.body;
|
|
1909
|
+
bodyAt = known.bodyAt;
|
|
1910
|
+
} else throw new StewardContractError({ httpStatus: 304 });
|
|
1911
|
+
put(ref, {
|
|
1912
|
+
body,
|
|
1913
|
+
etag: read.etag,
|
|
1914
|
+
bodyAt,
|
|
1915
|
+
confirmedAt: t,
|
|
1916
|
+
expiresAt: flight.invalidated ? Number.NEGATIVE_INFINITY : t + ttlMs,
|
|
1917
|
+
failure: null
|
|
1918
|
+
});
|
|
1919
|
+
return {
|
|
1920
|
+
kind: "value",
|
|
1921
|
+
body,
|
|
1922
|
+
bodyAt,
|
|
1923
|
+
source: "live"
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function join(ref) {
|
|
1927
|
+
const running = flights.get(ref);
|
|
1928
|
+
if (running) return running.promise;
|
|
1929
|
+
const control = { invalidated: false };
|
|
1930
|
+
const promise = load(ref, control).finally(() => {
|
|
1931
|
+
if (flights.get(ref)?.promise === promise) flights.delete(ref);
|
|
1932
|
+
});
|
|
1933
|
+
flights.set(ref, Object.assign(control, { promise }));
|
|
1934
|
+
return promise;
|
|
1935
|
+
}
|
|
1936
|
+
async function snapshot(ref, fresh) {
|
|
1937
|
+
const entry = entries.get(ref);
|
|
1938
|
+
if (entry) put(ref, entry);
|
|
1939
|
+
if (entry && !fresh) {
|
|
1940
|
+
const t = now();
|
|
1941
|
+
if (entry.body !== null && t < entry.expiresAt) return {
|
|
1942
|
+
kind: "value",
|
|
1943
|
+
body: entry.body,
|
|
1944
|
+
bodyAt: entry.bodyAt,
|
|
1945
|
+
source: "cached"
|
|
1946
|
+
};
|
|
1947
|
+
if (entry.failure && t < entry.failure.until) return degrade(ref, entry, entry.failure);
|
|
1948
|
+
}
|
|
1949
|
+
return join(ref);
|
|
1950
|
+
}
|
|
1951
|
+
/** Steward's meter plus the pending units. `def` absent: a meter this catalog does not define (no pending units). */
|
|
1952
|
+
function computed(meter, def, pending, cutoff, source) {
|
|
1953
|
+
const credited = micro(meter.creditedUnits);
|
|
1954
|
+
const consumedBefore = micro(meter.consumedUnits);
|
|
1955
|
+
const pendingMicro = def === void 0 ? 0n : pendingMicroOf(def, pending, cutoff, consumedBefore);
|
|
1956
|
+
const consumed = consumedBefore + pendingMicro;
|
|
1957
|
+
const balance = credited - consumed;
|
|
1958
|
+
const standing = def === void 0 || pendingMicro === 0n ? meter.standing : worse(meter.standing, standingOf({
|
|
1959
|
+
creditedMicro: credited,
|
|
1960
|
+
consumedMicro: consumed,
|
|
1961
|
+
lowAt: def.lowAt
|
|
1962
|
+
}));
|
|
1963
|
+
return {
|
|
1964
|
+
balanceMicro: balance,
|
|
1965
|
+
value: {
|
|
1966
|
+
periodStart: meter.periodStart,
|
|
1967
|
+
periodEnd: meter.periodEnd,
|
|
1968
|
+
includedUnits: meter.includedUnits,
|
|
1969
|
+
consumedUnits: fromMicroUnits(consumed),
|
|
1970
|
+
creditedUnits: fromMicroUnits(credited),
|
|
1971
|
+
balance: fromMicroUnits(balance),
|
|
1972
|
+
overageUnits: fromMicroUnits(balance < 0n ? -balance : 0n),
|
|
1973
|
+
pendingUnits: fromMicroUnits(pendingMicro),
|
|
1974
|
+
standing,
|
|
1975
|
+
billable: meter.billable,
|
|
1976
|
+
amountDueMinor: meter.amountDueMinor,
|
|
1977
|
+
currency: meter.currency,
|
|
1978
|
+
capReached: meter.billable && meter.capReached === true,
|
|
1979
|
+
source
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
/** Fallback value: the account's coarse facts from `cache` if it holds them, else the default plan's credit; zero consumption. */
|
|
1984
|
+
function fallbackOf(ref, code, pending) {
|
|
1985
|
+
const def = facts.meters.get(code);
|
|
1986
|
+
const coarse = cache?.peek(ref)?.meters?.[code];
|
|
1987
|
+
const includedMicro = coarse !== void 0 ? coarse.includedUnits === null ? null : micro(coarse.includedUnits) : defaultCredits.get(code) ?? null;
|
|
1988
|
+
const credited = includedMicro ?? 0n;
|
|
1989
|
+
const consumed = pendingMicroOf(def, pending, Number.NEGATIVE_INFINITY, 0n);
|
|
1990
|
+
const balance = credited - consumed;
|
|
1991
|
+
const standing = worse(isStanding(coarse?.standing) ? coarse.standing : "ok", standingOf({
|
|
1992
|
+
creditedMicro: credited,
|
|
1993
|
+
consumedMicro: consumed,
|
|
1994
|
+
lowAt: def.lowAt
|
|
1995
|
+
}));
|
|
1996
|
+
return {
|
|
1997
|
+
balanceMicro: balance,
|
|
1998
|
+
value: {
|
|
1999
|
+
periodStart: null,
|
|
2000
|
+
periodEnd: null,
|
|
2001
|
+
includedUnits: includedMicro === null ? null : fromMicroUnits(includedMicro),
|
|
2002
|
+
consumedUnits: fromMicroUnits(consumed),
|
|
2003
|
+
creditedUnits: fromMicroUnits(credited),
|
|
2004
|
+
balance: fromMicroUnits(balance),
|
|
2005
|
+
overageUnits: fromMicroUnits(balance < 0n ? -balance : 0n),
|
|
2006
|
+
pendingUnits: fromMicroUnits(consumed),
|
|
2007
|
+
standing,
|
|
2008
|
+
billable: coarse?.billable === true,
|
|
2009
|
+
amountDueMinor: null,
|
|
2010
|
+
currency: null,
|
|
2011
|
+
capReached: false,
|
|
2012
|
+
source: "fallback"
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
function fellBack(event) {
|
|
2017
|
+
if (onFallback === void 0) return;
|
|
2018
|
+
try {
|
|
2019
|
+
onFallback(event);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
logger.warn("Meters onFallback threw", {
|
|
2022
|
+
ref: event.ref,
|
|
2023
|
+
error: String(error)
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
function warnUnavailable(ref, snap) {
|
|
2028
|
+
const failure = entries.get(ref)?.failure;
|
|
2029
|
+
if (failure && failure.error === snap.error) {
|
|
2030
|
+
if (failure.warned === "fallback") return;
|
|
2031
|
+
failure.warned = "fallback";
|
|
2032
|
+
}
|
|
2033
|
+
const { code } = fieldsOf(snap.error);
|
|
2034
|
+
logger.warn("steward meters unreadable, no stale value; using the catalog fallback", {
|
|
2035
|
+
ref,
|
|
2036
|
+
code
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
/** One meter of a snapshot; a catalog meter steward does not report gets the catalog value. */
|
|
2040
|
+
function meterOf(ref, code, snap, pending) {
|
|
2041
|
+
const def = facts.meters.get(code);
|
|
2042
|
+
const reported = Object.hasOwn(snap.body.meters, code) ? snap.body.meters[code] : void 0;
|
|
2043
|
+
if (reported !== void 0) return computed(reported, def, pending, snap.bodyAt - rollupLagMs(snap.body), snap.source);
|
|
2044
|
+
if (!warnedMissing.has(code)) {
|
|
2045
|
+
warnedMissing.add(code);
|
|
2046
|
+
logger.warn("steward does not report a catalog meter (catalog not pushed?); using the catalog value", { meter: code });
|
|
2047
|
+
}
|
|
2048
|
+
fellBack({
|
|
2049
|
+
ref,
|
|
2050
|
+
meter: code,
|
|
2051
|
+
reason: "meter_not_found"
|
|
2052
|
+
});
|
|
2053
|
+
return fallbackOf(ref, code, pending);
|
|
2054
|
+
}
|
|
2055
|
+
function requireMeter(name, meter) {
|
|
2056
|
+
if (typeof meter !== "string" || !facts.meters.has(meter)) throw new StewardConfigError({
|
|
2057
|
+
code: "unknown_meter",
|
|
2058
|
+
message: `Meters.${name}: not a catalog meter: ${JSON.stringify(meter)}`
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
const pendingOf = (ref) => events?.pending(ref) ?? [];
|
|
2062
|
+
return {
|
|
2063
|
+
async get(ref, readOptions = {}) {
|
|
2064
|
+
const snap = await snapshot(ref, readOptions.fresh === true);
|
|
2065
|
+
const pending = pendingOf(ref);
|
|
2066
|
+
const out = {};
|
|
2067
|
+
if (snap.kind === "fallback") {
|
|
2068
|
+
warnUnavailable(ref, snap);
|
|
2069
|
+
fellBack({
|
|
2070
|
+
ref,
|
|
2071
|
+
meter: null,
|
|
2072
|
+
reason: fieldsOf(snap.error).code ?? "unknown"
|
|
2073
|
+
});
|
|
2074
|
+
for (const code of facts.meters.keys()) out[code] = fallbackOf(ref, code, pending).value;
|
|
2075
|
+
return out;
|
|
2076
|
+
}
|
|
2077
|
+
for (const code of facts.meters.keys()) out[code] = meterOf(ref, code, snap, pending).value;
|
|
2078
|
+
for (const [code, meter] of Object.entries(snap.body.meters)) if (!Object.hasOwn(out, code)) out[code] = computed(meter, void 0, pending, 0, snap.source).value;
|
|
2079
|
+
return out;
|
|
2080
|
+
},
|
|
2081
|
+
async balance(ref, meter, readOptions = {}) {
|
|
2082
|
+
requireMeter("balance", meter);
|
|
2083
|
+
const snap = await snapshot(ref, readOptions.fresh === true);
|
|
2084
|
+
const pending = pendingOf(ref);
|
|
2085
|
+
if (snap.kind === "value") return meterOf(ref, meter, snap, pending).value;
|
|
2086
|
+
warnUnavailable(ref, snap);
|
|
2087
|
+
fellBack({
|
|
2088
|
+
ref,
|
|
2089
|
+
meter,
|
|
2090
|
+
reason: fieldsOf(snap.error).code ?? "unknown"
|
|
2091
|
+
});
|
|
2092
|
+
return fallbackOf(ref, meter, pending).value;
|
|
2093
|
+
},
|
|
2094
|
+
async check(ref, meter, checkOptions = {}) {
|
|
2095
|
+
requireMeter("check", meter);
|
|
2096
|
+
const units = checkOptions.units ?? 1;
|
|
2097
|
+
const unitsMicro = typeof units === "number" ? parseMicroUnits(units) : null;
|
|
2098
|
+
if (unitsMicro === null) throw new StewardConfigError({
|
|
2099
|
+
code: "invalid_units",
|
|
2100
|
+
message: `Meters.check: units must be a finite, non-negative number: ${String(units)}`
|
|
2101
|
+
});
|
|
2102
|
+
const snap = await snapshot(ref, checkOptions.fresh === true);
|
|
2103
|
+
const pending = pendingOf(ref);
|
|
2104
|
+
const result = ({ value, balanceMicro }, allowed) => ({
|
|
2105
|
+
allowed: allowed ?? (value.billable ? !value.capReached : value.includedUnits === null || balanceMicro >= unitsMicro),
|
|
2106
|
+
balance: value.balance,
|
|
2107
|
+
standing: value.standing,
|
|
2108
|
+
billable: value.billable,
|
|
2109
|
+
source: value.source
|
|
2110
|
+
});
|
|
2111
|
+
if (snap.kind === "value") return result(meterOf(ref, meter, snap, pending));
|
|
2112
|
+
warnUnavailable(ref, snap);
|
|
2113
|
+
const fallback = fallbackOf(ref, meter, pending);
|
|
2114
|
+
let allowed;
|
|
2115
|
+
if (typeof policy === "function") try {
|
|
2116
|
+
allowed = policy({
|
|
2117
|
+
ref,
|
|
2118
|
+
meter,
|
|
2119
|
+
units,
|
|
2120
|
+
balance: fallback.value,
|
|
2121
|
+
error: snap.error
|
|
2122
|
+
}) === true;
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
logger.warn("Meters onUnavailable threw; the steward error is thrown", {
|
|
2125
|
+
ref,
|
|
2126
|
+
meter,
|
|
2127
|
+
error: String(error)
|
|
2128
|
+
});
|
|
2129
|
+
throw snap.error;
|
|
2130
|
+
}
|
|
2131
|
+
else allowed = policy === "allow";
|
|
2132
|
+
fellBack({
|
|
2133
|
+
ref,
|
|
2134
|
+
meter,
|
|
2135
|
+
reason: fieldsOf(snap.error).code ?? "unknown"
|
|
2136
|
+
});
|
|
2137
|
+
return result(fallback, allowed);
|
|
2138
|
+
},
|
|
2139
|
+
invalidate(ref) {
|
|
2140
|
+
const flight = flights.get(ref);
|
|
2141
|
+
if (flight) {
|
|
2142
|
+
flight.invalidated = true;
|
|
2143
|
+
flights.delete(ref);
|
|
2144
|
+
}
|
|
2145
|
+
const entry = entries.get(ref);
|
|
2146
|
+
if (entry) Object.assign(entry, {
|
|
2147
|
+
expiresAt: Number.NEGATIVE_INFINITY,
|
|
2148
|
+
failure: null
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
//#endregion
|
|
2154
|
+
export { ACCOUNT_INCLUDES, API_VERSION, AccessStateSchema, AccountBenefitSchema, AccountDeleteResponseSchema, AccountDetailSchema, AccountDunningStateSchema, AccountLocaleSchema, AccountMeterSchema, AccountMeterStateSchema, AccountMetersSchema, AccountMetersStateSchema, AccountOpenCheckoutSchema, AccountOpenInvoiceSchema, AccountProfileStateSchema, AccountScheduledChangeSchema, AccountSchema, AccountStateSchema, AccountSubscriptionStateSchema, AccountUpsertInputSchema, AccountUpsertResponseSchema, AccountsRefreshResultSchema, ActorRefSchema, AmountMinorSchema, AnyBillingEventSchema, ApiVersionSchema, BENEFIT_TYPES, BILLING_PATH_PLACEHOLDERS, BankTransferInstructionsEmailSchema, BankTransferPaymentDaysSchema, BankTransferRecordInputSchema, BankTransferRecordedSchema, BankTransferSettingsPatchSchema, BankTransferSettingsSchema, BenefitDefSchema, BenefitDescriptionSchema, BenefitSourceSchema, BenefitTypeSchema, BillingEventSchema, BillingPathSchema, BillingProfileKindSchema, BillingProfileSchema, BillingSettingsPatchSchema, BillingSettingsSchema, BrandingPatchSchema, BrandingSchema, CHECKOUT_ERRORS, CancelSubscriptionInputSchema, CancelSubscriptionResponseSchema, CatalogBenefitSchema, CatalogMeterSchema, CatalogPlanSchema, CatalogSchema, CatalogSyncInputSchema, CatalogSyncResultSchema, ChangeSubscriptionResponseSchema, CheckoutConsentInputSchema, CheckoutPrefillSchema, CheckoutSessionCreateInputSchema, CheckoutSessionCreatedSchema, CheckoutSessionResponseSchema, CheckoutSessionSchema, CheckoutStatusSchema, CodeSchema, ConsentSchema, CreditGrantInputSchema, CreditGrantResponseSchema, CurrencySchema, DEFAULT_BANK_TRANSFER_PAYMENT_DAYS, DEFAULT_CHECKOUT_TTL_MINUTES, DEFAULT_ENDPOINT_EVENT_TYPES, DEFAULT_EVENT_RETENTION_DAYS, DEFAULT_LATE_GRACE_MINUTES, DEFAULT_METER_CHARGE_MODE, DEFAULT_METER_LOW_AT, DEFAULT_MIN_INVOICE_MINOR, DEFAULT_PAYMENT_DUE_DAYS, DEFAULT_PORTAL_TTL_MINUTES, DOCTOR_PENDING_BACKLOG_MS, DOCTOR_PROVIDER_ENVIRONMENTS, DOCTOR_REQUIRED_BRANDING, DOCTOR_REQUIRED_LEGAL_LINKS, DOCTOR_ROLLUP_LAG_WARN_SECONDS, DOCTOR_SEVERITIES, DateTimeSchema, DecimalUnitsStringSchema, DoctorBillingSchema, DoctorCatalogSchema, DoctorEmailsSchema, DoctorEndpointSchema, DoctorEventsSchema, DoctorHostedSchema, DoctorOverageSchema, DoctorProviderSchema, DoctorReportSchema, DoctorSettingsSchema, DoctorSeveritySchema, DoctorUnsellablePriceSchema, DoctorWarningSchema, EMAIL_LOCALES, EMAIL_PAYLOAD_SCHEMAS, EMAIL_TEMPLATES, EMAIL_TEMPLATE_BY_EVENT, ERROR_CATEGORIES, ERROR_CODES, EVENTS_INGEST_MAX_BYTES, EVENTS_INGEST_MAX_EVENTS, EVENTS_RECENT_MS, EVENT_DETAIL_SCHEMAS, EVENT_METADATA_KEY_MAX_LENGTH, EVENT_METADATA_MAX_PAIRS, EVENT_METADATA_STRING_MAX_LENGTH, EVENT_NAME_MAX_LENGTH, EVENT_TIMESTAMP_MAX_AGE_DAYS, EVENT_TIMESTAMP_MAX_SKEW_MINUTES, EVENT_TYPES, EmailExtraTextSchema, EmailSenderSchema, EmailSettingsPatchSchema, EmailSettingsSchema, EmailTemplateSchema, EmailTemplateSettingsSchema, EntitlementSnapshotSchema, EntitlementValueSchema, EntitlementsSchema, ErrorCategorySchema, ErrorResponseSchema, EventExternalIdSchema, EventIdSchema, EventMetadataKeySchema, EventMetadataSchema, EventMetadataValueSchema, EventNameSchema, EventRetentionDaysSchema, EventTypeSchema, Events, EventsIngestInputSchema, EventsIngestResultSchema, EventsSettingsPatchSchema, EventsSettingsSchema, ExternalRefSchema, FEATURE_FLAG_BENEFIT_TYPE, FULL_STATE_EVENT_TYPES, FeatureDefSchema, FeatureFlagBenefitDefSchema, FeatureFlagPropertiesSchema, FeatureTypeSchema, GrantCreateInputSchema, GrantCreateResponseSchema, GrantSchema, HEADER_ACTOR_REF, HEADER_API_VERSION, HEADER_IDEMPOTENCY_KEY, HEADER_REQUEST_ID, HEADER_SDK, INDIVIDUAL_IDENTITY_PLACEHOLDER, INGEST_REJECT_CODES, INVOICE_BILLING_REASONS, INVOICE_PAYMENT_STATUSES, IdSchema, IngestEventSchema, IngestRejectCodeSchema, IngestRejectionSchema, IntervalSchema, InvalidUnitValueError, InvoiceBillingReasonSchema, InvoiceIssueInputSchema, InvoiceIssueResponseSchema, InvoiceLineSchema, InvoicePaymentDueEmailSchema, InvoicePaymentSessionCreateInputSchema, InvoicePaymentSessionSchema, InvoicePaymentStatusSchema, InvoiceSchema, InvoiceStatusSchema, InvoiceVoidInputSchema, InvoiceVoidResponseSchema, InvoiceWaiveInputSchema, InvoiceWaiveResponseSchema, LEGACY_EVENT_TYPES, LIVE_SUBSCRIPTION_STATUSES, LateGraceMinutesSchema, MANUAL_INVOICE_MAX_AMOUNT_MINOR, MAX_CATALOG_BENEFITS, MAX_CATALOG_METERED_PRICES, MAX_CATALOG_METERS, MAX_CHECKOUT_CONSENT_DOCS, MAX_GRANT_BENEFITS, MAX_METERED_PRICE_PER_UNITS, MAX_METER_UNITS, MAX_NOTIFICATION_EMAILS, MAX_PLAN_BENEFITS, METERED_PRICE_TAX_MISMATCH, METER_AGGREGATION_KINDS, METER_CHARGE_MODES, METER_CHARGE_STATUSES, METER_CREDIT_ENTRY_KINDS, METER_FILTER_MAX_CONDITIONS, METER_FILTER_MAX_DEPTH, METER_FILTER_OPERATORS, METER_PERIOD_STATUSES, METER_STANDINGS, MICRO_UNITS_PER_UNIT, ManualInvoiceInputSchema, ManualInvoiceLineInputSchema, ManualInvoiceResponseSchema, MeResponseSchema, MetadataSchema, MeterAggregationSchema, MeterChargeLineSchema, MeterChargeListSchema, MeterChargeModeSchema, MeterChargeSchema, MeterChargeStatusSchema, MeterCreditBenefitDefSchema, MeterCreditEntryKindSchema, MeterCreditEntrySchema, MeterCreditPropertiesSchema, MeterDefSchema, MeterFilterSchema, MeterLabelSchema, MeterLowAtSchema, MeterPeriodListSchema, MeterPeriodSchema, MeterPeriodStatusSchema, MeterStandingSchema, MeterUnitsSchema, MeteredPriceDefSchema, MeteredPriceSchema, MeteringSettingsPatchSchema, MeteringSettingsSchema, Meters, NoticedMeteredRateSchema, NotificationEmailsSchema, OVERRIDE_BENEFIT_SOURCE, PAYMENT_METHODS, PING_EVENT_TYPE, PLAN_SET_MODES, PORTAL_SESSION_ERRORS, PaymentDueDaysSchema, PaymentMethodSchema, PaymentSchema, PingSchema, PlanDefSchema, PlanSetInputSchema, PlanSetModeSchema, PlanSetResponseSchema, PlanSetWarningSchema, PortalSessionCreateInputSchema, PortalSessionSchema, PriceChangeNoticeEmailSchema, PriceChangeNoticeMeteredRateSchema, PriceDefSchema, PriceSchema, ProductSettingsPatchSchema, ProductSettingsSchema, ProviderPayloadSchema, ResyncResultSchema, ReturnOriginSchema, SETTINGS_LOCALES, STEWARD_CAPABILITIES, SettingsLocaleSchema, StatsSchema, StewardApiError, StewardConfigError, StewardContractError, StewardError, StewardNetworkError, SubscriptionChangeInputSchema, SubscriptionSchema, SubscriptionStatusSchema, SubscriptionSummarySchema, TtlMinutesSchema, UNIT_DECIMALS, UncancelSubscriptionResponseSchema, WEBHOOK_ENDPOINT_STATUSES, WEBHOOK_SECRET_PREFIX, WebUrlSchema, WebhookEndpointCreateInputSchema, WebhookEndpointListSchema, WebhookEndpointRotateInputSchema, WebhookEndpointSchema, WebhookEndpointStatusSchema, WebhookLastDeliverySchema, WebhookSecretSchema, addUtcMonths, applyEmailSettingsPatch, applySettingsPatch, billingPageUrl, billingSettingsOf, buildDoctorReport, canonicalMeterFilter, checkoutRedirectUrl, consumptionOf, contributionOf, count, createSteward, defineCatalog, emailLocaleOf, emailReadinessOf, emailRecipients, emailTemplateOf, emailTemplateSettings, endpointDiagnosisOf, errorCodeSpec, eventsSettingsOf, featureFlag, featureValueMatchesType, flag, fromMicroUnits, generateWebhookSecret, hasControlCharacters, hostedReadinessOf, httpStatusFor, isE164, isEmailRelevant, isEventTimestampInRange, isEventType, isFeatureFlagBenefit, isGrantActive, isKnownErrorCode, isLiveSubscriptionStatus, isMeterCreditBenefit, isMicroPrecise, isReturnUrlAllowed, isValidTckn, isValidVkn, limit, liveSubscriptionOf, manualInvoiceTotals, matchesFilter, max, meter, meterChargeTotals, meterCredit, meterCreditsOf, meterFilterClauseOf, meteringSettingsOf, microToDecimalString, overageCapReached, overageOf, parseMicroUnits, periodBoundsOf, plainDecimalOf, rateOverage, resolveBenefits, resolveEntitlements, returnOriginsOf, rolloverOf, splitInclusiveTax, standingOf, stateCache, sum, text, toMicroUnits, unique, uniqueEmails, webhookEndpointUrlProblem };
|