@happyvertical/smrt-subscriptions 0.60.14 → 0.60.16
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/AGENTS.md +152 -0
- package/README.md +206 -0
- package/dist/billing/apply.d.ts +52 -0
- package/dist/billing/apply.d.ts.map +1 -0
- package/dist/billing/checkout-sessions.d.ts +99 -0
- package/dist/billing/checkout-sessions.d.ts.map +1 -0
- package/dist/billing/config.d.ts +64 -0
- package/dist/billing/config.d.ts.map +1 -0
- package/dist/billing/events.d.ts +49 -0
- package/dist/billing/events.d.ts.map +1 -0
- package/dist/billing/host.d.ts +3 -0
- package/dist/billing/host.d.ts.map +1 -0
- package/dist/billing/index.d.ts +4 -0
- package/dist/billing/index.d.ts.map +1 -0
- package/dist/billing/plans.d.ts +45 -0
- package/dist/billing/plans.d.ts.map +1 -0
- package/dist/billing/signing.d.ts +24 -0
- package/dist/billing/signing.d.ts.map +1 -0
- package/dist/billing/types.d.ts +265 -0
- package/dist/billing/types.d.ts.map +1 -0
- package/dist/collections/TenantUsageMetricCollection.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3230 -1123
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +314 -4
- package/dist/models/BillingCheckoutSession.d.ts +39 -0
- package/dist/models/BillingCheckoutSession.d.ts.map +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/recipes.d.ts +1 -1
- package/dist/services/entitlement-changes.d.ts +115 -0
- package/dist/services/entitlement-changes.d.ts.map +1 -0
- package/dist/services/hosted-ai-spend.d.ts +182 -0
- package/dist/services/hosted-ai-spend.d.ts.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/subscription-feature-source.d.ts +89 -0
- package/dist/services/subscription-feature-source.d.ts.map +1 -0
- package/dist/services/subscription-resolver.d.ts +34 -1
- package/dist/services/subscription-resolver.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +103 -8
- package/dist/smrt-knowledge.okf.md +6 -2
- package/dist/vault-account/actions.d.ts +24 -0
- package/dist/vault-account/actions.d.ts.map +1 -0
- package/dist/vault-account/errors.d.ts +20 -0
- package/dist/vault-account/errors.d.ts.map +1 -0
- package/dist/vault-account/index.d.ts +5 -0
- package/dist/vault-account/index.d.ts.map +1 -0
- package/dist/vault-account/relay-admin-client.d.ts +10 -0
- package/dist/vault-account/relay-admin-client.d.ts.map +1 -0
- package/dist/vault-account/types.d.ts +115 -0
- package/dist/vault-account/types.d.ts.map +1 -0
- package/package.json +9 -6
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted AI spend policy (#4760, epic #4667).
|
|
3
|
+
*
|
|
4
|
+
* A plan caps platform-paid (`source: hosted`) inference with ordinary
|
|
5
|
+
* `PlanThreshold`s on two reserved metric keys, so the window, amount and
|
|
6
|
+
* enforcement come from the plan (and its pinned version) like every other
|
|
7
|
+
* threshold:
|
|
8
|
+
*
|
|
9
|
+
* - {@link AI_HOSTED_SPEND_METRIC}: the tenant's hosted spend per window.
|
|
10
|
+
* - {@link AI_HOSTED_USER_SPEND_METRIC}: each user's hosted spend per window
|
|
11
|
+
* (the per-user cap within the tenant's plan).
|
|
12
|
+
*
|
|
13
|
+
* `limit` is **integer micro-dollars** (1 USD = 1,000,000): the repo's
|
|
14
|
+
* integer-minor-unit money rule at the precision per-call AI costs need.
|
|
15
|
+
* `enforcement: 'block'` blocks at or over the cap; `'warn'` only warns;
|
|
16
|
+
* `'observe'` never affects a call. `warningRatio` (default 0.8) warns
|
|
17
|
+
* before the cap. A cap of 0 blocks every hosted call.
|
|
18
|
+
*
|
|
19
|
+
* Spend is read from `_smrt_ai_usage.estimated_cost` (USD, summed and
|
|
20
|
+
* rounded to micro-dollars) for rows of the tenant in the threshold's
|
|
21
|
+
* window whose `tags` record `source: hosted` (and, for the per-user cap,
|
|
22
|
+
* the `userId` tag), matched as case-sensitive exact JSON fragments. Blocked rows (`operation: 'blocked'`) and
|
|
23
|
+
* external-agent measurements never count. Rows without a cost rate add
|
|
24
|
+
* nothing; give hosted models a cost rate.
|
|
25
|
+
*
|
|
26
|
+
* These keys are evaluated by {@link HostedAiSpendEvaluator} only:
|
|
27
|
+
* `resolveEntitlements()` reports them with quantity 0 (the tenant usage
|
|
28
|
+
* meter has no hosted or per-user meter).
|
|
29
|
+
*/
|
|
30
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
31
|
+
import type { PlanThreshold, SmrtClassOptions, SubscriptionResolverOptions, UsageWindow } from '../types.js';
|
|
32
|
+
import { SubscriptionResolver } from './subscription-resolver.js';
|
|
33
|
+
/** Plan threshold key: the tenant's hosted AI spend, micro-dollars. */
|
|
34
|
+
export declare const AI_HOSTED_SPEND_METRIC = "ai.hosted.spend_micros";
|
|
35
|
+
/** Plan threshold key: each user's hosted AI spend, micro-dollars. */
|
|
36
|
+
export declare const AI_HOSTED_USER_SPEND_METRIC = "ai.hosted.user_spend_micros";
|
|
37
|
+
/** Micro-dollars per US dollar. */
|
|
38
|
+
export declare const MICROS_PER_USD = 1000000;
|
|
39
|
+
/** The cap a decision names: tenant or per-user. */
|
|
40
|
+
export type HostedAiSpendPolicy = 'tenant-cap' | 'user-cap';
|
|
41
|
+
/** One evaluated hosted spend threshold. */
|
|
42
|
+
export interface HostedAiSpendEvaluation {
|
|
43
|
+
policy: HostedAiSpendPolicy;
|
|
44
|
+
threshold: PlanThreshold;
|
|
45
|
+
/** The cap, integer micro-dollars. */
|
|
46
|
+
cap: number;
|
|
47
|
+
/** Metered hosted spend in the window, integer micro-dollars. */
|
|
48
|
+
spend: number;
|
|
49
|
+
window: UsageWindow;
|
|
50
|
+
/**
|
|
51
|
+
* When the cap lifts: the calendar window's end; for a blocked `rolling`
|
|
52
|
+
* cap, the moment enough counted spend has left the 30-day window to bring
|
|
53
|
+
* the total below the cap (null when no expiry can, e.g. a 0 cap, or the
|
|
54
|
+
* cap is not blocked).
|
|
55
|
+
*/
|
|
56
|
+
resetsAt: Date | null;
|
|
57
|
+
state: 'ok' | 'warn' | 'blocked';
|
|
58
|
+
}
|
|
59
|
+
/** The most restrictive outcome over every hosted spend threshold. */
|
|
60
|
+
export interface HostedAiSpendDecision {
|
|
61
|
+
state: 'ok' | 'warn' | 'blocked';
|
|
62
|
+
/** False when the tenant has no current entitled subscription. */
|
|
63
|
+
entitled: boolean;
|
|
64
|
+
planId: string | null;
|
|
65
|
+
planKey: string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The deciding evaluation: for `blocked`, the blocking cap that lifts
|
|
68
|
+
* last (the call runs again only when every blocking cap has lifted);
|
|
69
|
+
* for `warn`, the warning cap closest to its limit.
|
|
70
|
+
*/
|
|
71
|
+
decidedBy: HostedAiSpendEvaluation | null;
|
|
72
|
+
evaluations: HostedAiSpendEvaluation[];
|
|
73
|
+
}
|
|
74
|
+
/** Hosted spend totals for one window. */
|
|
75
|
+
export interface HostedAiSpendSummary {
|
|
76
|
+
/** Integer micro-dollars. */
|
|
77
|
+
spend: number;
|
|
78
|
+
}
|
|
79
|
+
/** The scope of one spend read. */
|
|
80
|
+
export interface HostedAiSpendScope {
|
|
81
|
+
tenantId: string;
|
|
82
|
+
/** Only this user's rows (the `userId` tag); omitted: the whole tenant. */
|
|
83
|
+
userId?: string;
|
|
84
|
+
window: UsageWindow;
|
|
85
|
+
}
|
|
86
|
+
/** Reads hosted spend; the default reads `_smrt_ai_usage`. */
|
|
87
|
+
export interface HostedAiSpendReader {
|
|
88
|
+
summarize(options: HostedAiSpendScope): Promise<HostedAiSpendSummary>;
|
|
89
|
+
/**
|
|
90
|
+
* For a rolling window: the earliest moment the counted rows, expiring
|
|
91
|
+
* oldest first after `windowMs`, bring the scope's spend below `cap`;
|
|
92
|
+
* null when no expiry can. Optional: without it a blocked rolling cap
|
|
93
|
+
* reports no reset time.
|
|
94
|
+
*/
|
|
95
|
+
liftsAt?(options: HostedAiSpendScope & {
|
|
96
|
+
cap: number;
|
|
97
|
+
windowMs: number;
|
|
98
|
+
}): Promise<Date | null>;
|
|
99
|
+
}
|
|
100
|
+
/** A plan cap the gate cannot evaluate: the caller must fail closed. */
|
|
101
|
+
export declare class HostedAiSpendPolicyError extends Error {
|
|
102
|
+
constructor(message: string);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The exact fragment one tag leaves in `_smrt_ai_usage.tags` as
|
|
106
|
+
* `JSON.stringify` writes it (core's persistence handler): `"key":"value"`.
|
|
107
|
+
* The value is JSON-encoded, so a quote inside it cannot end the match
|
|
108
|
+
* early, and the closing quote anchors it, so `u1` never matches `u10`.
|
|
109
|
+
*/
|
|
110
|
+
export declare function hostedAiTagFragment(key: string, value: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* The default {@link HostedAiSpendReader}: one aggregate query over
|
|
113
|
+
* `_smrt_ai_usage` by `tenant_id` and `created_at` (served by
|
|
114
|
+
* `idx_smrt_ai_usage_tenant_created`), filtered on the `source` and
|
|
115
|
+
* `userId` tag fragments (`strpos` on PostgreSQL, `instr` on SQLite and
|
|
116
|
+
* DuckDB). A fragment can only over-count (a row whose tags contain it
|
|
117
|
+
* elsewhere), never miss a row core wrote, so the error is always toward
|
|
118
|
+
* the cap.
|
|
119
|
+
*/
|
|
120
|
+
export declare function createHostedAiSpendReader(db: DatabaseInterface): HostedAiSpendReader;
|
|
121
|
+
/**
|
|
122
|
+
* The hosted spend thresholds of a plan. A hosted key whose threshold is
|
|
123
|
+
* malformed (unknown window or enforcement, or a limit that is not a
|
|
124
|
+
* non-negative safe integer of micro-dollars) throws
|
|
125
|
+
* {@link HostedAiSpendPolicyError}: a broken cap must never read as "no cap".
|
|
126
|
+
*/
|
|
127
|
+
export declare function hostedAiSpendThresholds(thresholds: readonly PlanThreshold[]): Array<{
|
|
128
|
+
policy: HostedAiSpendPolicy;
|
|
129
|
+
threshold: PlanThreshold;
|
|
130
|
+
}>;
|
|
131
|
+
/** Evaluate one cap against its metered spend. */
|
|
132
|
+
export declare function evaluateHostedAiSpendThreshold(policy: HostedAiSpendPolicy, threshold: PlanThreshold, summary: HostedAiSpendSummary, window: UsageWindow): HostedAiSpendEvaluation;
|
|
133
|
+
/** Fold evaluations into the most restrictive decision. */
|
|
134
|
+
export declare function decideHostedAiSpend(evaluations: HostedAiSpendEvaluation[], plan: {
|
|
135
|
+
entitled: boolean;
|
|
136
|
+
planId: string | null;
|
|
137
|
+
planKey: string | null;
|
|
138
|
+
}): HostedAiSpendDecision;
|
|
139
|
+
/** Options for {@link HostedAiSpendEvaluator}. */
|
|
140
|
+
export interface HostedAiSpendEvaluatorOptions {
|
|
141
|
+
resolver: Pick<SubscriptionResolver, 'resolvePlanThresholds'>;
|
|
142
|
+
spend: HostedAiSpendReader;
|
|
143
|
+
/**
|
|
144
|
+
* How long a tenant's plan thresholds are reused, in ms (default 30 000):
|
|
145
|
+
* entitlements resolve once per request burst, while spend is always read
|
|
146
|
+
* fresh. 0 disables the memo.
|
|
147
|
+
*/
|
|
148
|
+
termsTtlMs?: number;
|
|
149
|
+
now?: () => Date;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Evaluates a tenant's (and one user's) hosted AI spend against the plan's
|
|
153
|
+
* caps. Plan thresholds come from `SubscriptionResolver` (memoized per
|
|
154
|
+
* tenant for `termsTtlMs`); spend is read on every call. Errors (a resolver
|
|
155
|
+
* or database outage, missing tables, a malformed cap) propagate: the gate
|
|
156
|
+
* turns them into a fail-closed block.
|
|
157
|
+
*/
|
|
158
|
+
export declare class HostedAiSpendEvaluator {
|
|
159
|
+
private readonly options;
|
|
160
|
+
private readonly terms;
|
|
161
|
+
private readonly termsTtlMs;
|
|
162
|
+
private readonly now;
|
|
163
|
+
constructor(options: HostedAiSpendEvaluatorOptions);
|
|
164
|
+
/** Build the default resolver and `_smrt_ai_usage` reader over one db. */
|
|
165
|
+
static create(classOptions: SmrtClassOptions & {
|
|
166
|
+
db: DatabaseInterface;
|
|
167
|
+
}, options?: Omit<HostedAiSpendEvaluatorOptions, 'resolver' | 'spend'>): Promise<HostedAiSpendEvaluator>;
|
|
168
|
+
/** Drop memoized plan thresholds (all tenants, or one). */
|
|
169
|
+
invalidate(tenantId?: string): void;
|
|
170
|
+
private planThresholds;
|
|
171
|
+
/**
|
|
172
|
+
* Evaluate the plan's hosted caps for `tenantId` and, when given, the
|
|
173
|
+
* per-user caps for `userId`. Without a `userId` the per-user caps are
|
|
174
|
+
* skipped (no user to meter: jobs, shared agent clients).
|
|
175
|
+
*/
|
|
176
|
+
evaluate(input: {
|
|
177
|
+
tenantId: string;
|
|
178
|
+
userId?: string;
|
|
179
|
+
resolverOptions?: SubscriptionResolverOptions;
|
|
180
|
+
}): Promise<HostedAiSpendDecision>;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=hosted-ai-spend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-ai-spend.d.ts","sourceRoot":"","sources":["../../src/services/hosted-ai-spend.ts"],"names":[],"mappings":"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"}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export type { EntitlementResolutionContext } from '../types.js';
|
|
2
2
|
export type { AutoTopUpGrant, AutoTopUpHook, AutoTopUpRequest, BillingRelationshipReader, CommercialBillingStorage, CommercialUsageServiceOptions, CustomPricingContext, CustomPricingStrategy, GrantCreditInput, PriceUsageOptions, RateUsageOptions, SpendingDecision, SpendingEvaluationInput, SpendingPolicyEvaluatorExtensions, SpendingPolicyEvaluatorOptions, UsageRating, } from './commercial.js';
|
|
3
3
|
export { assertCommercialBillingStorageSupported, CommercialBillingStorageConfigurationError, CommercialUsageService, SpendingPolicyEvaluator, UnsupportedCommercialBillingStorageError, } from './commercial.js';
|
|
4
|
+
export { diffEntitlements, ENTITLEMENT_CHANGE_LISTENERS_SLOT_KEY, type EntitlementChange, type EntitlementChangeFailure, type EntitlementChangeListener, type EntitlementChangeOutcome, type EntitlementSnapshot, emptyEntitlementSnapshot, getEntitlementChangeListeners, hasEntitlementChange, notifyEntitlementChange, registerEntitlementChangeListener, snapshotEntitlements, type TrackEntitlementChangeOptions, trackEntitlementChange, } from './entitlement-changes.js';
|
|
5
|
+
export { AI_HOSTED_SPEND_METRIC, AI_HOSTED_USER_SPEND_METRIC, createHostedAiSpendReader, decideHostedAiSpend, evaluateHostedAiSpendThreshold, type HostedAiSpendDecision, type HostedAiSpendEvaluation, HostedAiSpendEvaluator, type HostedAiSpendEvaluatorOptions, type HostedAiSpendPolicy, HostedAiSpendPolicyError, type HostedAiSpendReader, type HostedAiSpendScope, type HostedAiSpendSummary, hostedAiSpendThresholds, hostedAiTagFragment, MICROS_PER_USD, } from './hosted-ai-spend.js';
|
|
4
6
|
export { type AssignPriceBooksInput, type DefinePriceBookPriceInput, type DelegatedSpendingPolicyInput, type GrantChildCreditInput, type PriceBookAssignmentView, type PriceBookLeg, type ResellerBillingAction, ResellerBillingService, type ResellerBillingServiceOptions, } from './reseller.js';
|
|
5
7
|
export { ResellerBillingError, type ResellerBillingErrorCode, } from './reseller-errors.js';
|
|
8
|
+
export { FEATURE_GRANT_SOURCE_SLOT_KEY, type GrantingPlanReader, isMissingSubscriptionTableError, registerSubscriptionFeatureSource, type SubscriptionFeatureGrants, SubscriptionFeatureSource, type SubscriptionFeatureSourceOptions, type SubscriptionGrantingPlan, } from './subscription-feature-source.js';
|
|
6
9
|
export { type SubscriptionPlanReader, type SubscriptionPlanVersionReader, SubscriptionResolver, type SubscriptionResolverReaders, type TenantSubscriptionReader, type UsageSummaryReader, } from './subscription-resolver.js';
|
|
7
10
|
export { evaluateThreshold, evaluateThresholds, } from './threshold-evaluator.js';
|
|
8
11
|
export { TenantUsageMeter } from './usage-meter.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EACvB,iCAAiC,EACjC,8BAA8B,EAC9B,WAAW,GACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,uCAAuC,EACvC,0CAA0C,EAC1C,sBAAsB,EACtB,uBAAuB,EACvB,wCAAwC,GACzC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,4BAA4B,EACjC,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,oBAAoB,EACpB,KAAK,wBAAwB,GAC9B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EACvB,iCAAiC,EACjC,8BAA8B,EAC9B,WAAW,GACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,uCAAuC,EACvC,0CAA0C,EAC1C,sBAAsB,EACtB,uBAAuB,EACvB,wCAAwC,GACzC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,qCAAqC,EACrC,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,wBAAwB,EACxB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iCAAiC,EACjC,oBAAoB,EACpB,KAAK,6BAA6B,EAClC,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,yBAAyB,EACzB,mBAAmB,EACnB,8BAA8B,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,sBAAsB,EACtB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,uBAAuB,EACvB,mBAAmB,EACnB,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,4BAA4B,EACjC,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,oBAAoB,EACpB,KAAK,wBAAwB,GAC9B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,6BAA6B,EAC7B,KAAK,kBAAkB,EACvB,+BAA+B,EAC/B,iCAAiC,EACjC,KAAK,yBAAyB,EAC9B,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,wBAAwB,GAC9B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan feature grants as `smrt-features` flags (#4761).
|
|
3
|
+
*
|
|
4
|
+
* `SubscriptionFeatureSource` implements `smrt-features`' `FeatureGrantSource`
|
|
5
|
+
* structurally, so this package does not depend on `smrt-features` and
|
|
6
|
+
* `smrt-features` never depends on this one. Hosts either pass a per-request
|
|
7
|
+
* source as `grantSource` in the feature resolution context, or register an
|
|
8
|
+
* app-wide one with {@link registerSubscriptionFeatureSource}, which writes the
|
|
9
|
+
* `Symbol.for('@happyvertical/smrt-features:grant-source')` slot that
|
|
10
|
+
* `FeatureResolver` reads.
|
|
11
|
+
*
|
|
12
|
+
* Local installs: when the subscription tables do not exist, or the tenant has
|
|
13
|
+
* no entitled subscription, the source reports no grants and logs nothing, so
|
|
14
|
+
* paid keys stay at their (off) defaults. Every other failure propagates: a
|
|
15
|
+
* database outage must not silently look like "no plan".
|
|
16
|
+
*/
|
|
17
|
+
import { type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
18
|
+
import type { SubscriptionPlan } from '../models/SubscriptionPlan.js';
|
|
19
|
+
import { SubscriptionResolver } from './subscription-resolver.js';
|
|
20
|
+
/** Structural mirror of `smrt-features`' `FeatureGrantSnapshot`. */
|
|
21
|
+
export interface SubscriptionFeatureGrants {
|
|
22
|
+
featureKeys: readonly string[];
|
|
23
|
+
planId: string | null;
|
|
24
|
+
planKey: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** Structural mirror of `smrt-features`' `FeatureGrantingPlan`. */
|
|
27
|
+
export interface SubscriptionGrantingPlan {
|
|
28
|
+
planKey: string;
|
|
29
|
+
planId: string | null;
|
|
30
|
+
name: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** Reader for the plan catalog an upgrade response searches. */
|
|
33
|
+
export interface GrantingPlanReader {
|
|
34
|
+
findActive(): Promise<SubscriptionPlan[]>;
|
|
35
|
+
}
|
|
36
|
+
export interface SubscriptionFeatureSourceOptions {
|
|
37
|
+
/** One resolver for the request (or app lifetime); never built per flag. */
|
|
38
|
+
resolver: SubscriptionResolver;
|
|
39
|
+
/** Plan catalog for `findGrantingPlan()`; omitted means no plan is named. */
|
|
40
|
+
plans?: GrantingPlanReader;
|
|
41
|
+
/**
|
|
42
|
+
* Memoize grants per tenant for this instance's lifetime (default `true`).
|
|
43
|
+
* Keep it on for a request-scoped source; a process-wide source registered
|
|
44
|
+
* with {@link registerSubscriptionFeatureSource} turns it off so a plan
|
|
45
|
+
* change is visible on the next check.
|
|
46
|
+
*/
|
|
47
|
+
memoize?: boolean;
|
|
48
|
+
/** Clock for entitlement checks; defaults to the current time. */
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
}
|
|
51
|
+
/** `Symbol.for()` key `smrt-features` reads its global grant source from. */
|
|
52
|
+
export declare const FEATURE_GRANT_SOURCE_SLOT_KEY = "@happyvertical/smrt-features:grant-source";
|
|
53
|
+
export declare class SubscriptionFeatureSource {
|
|
54
|
+
private readonly resolver;
|
|
55
|
+
private readonly plans;
|
|
56
|
+
private readonly memoize;
|
|
57
|
+
private readonly now;
|
|
58
|
+
private readonly grants;
|
|
59
|
+
private catalog;
|
|
60
|
+
constructor(options: SubscriptionFeatureSourceOptions);
|
|
61
|
+
/**
|
|
62
|
+
* Build one source with its own resolver and plan catalog. Call once per
|
|
63
|
+
* request (or once at setup with `memoize: false` for a global source).
|
|
64
|
+
*/
|
|
65
|
+
static create(classOptions?: SmrtClassOptions, options?: Omit<SubscriptionFeatureSourceOptions, 'resolver' | 'plans'>): Promise<SubscriptionFeatureSource>;
|
|
66
|
+
/** Feature keys the tenant's entitled plan grants, or `null` for none. */
|
|
67
|
+
getGrants(tenantId: string): Promise<SubscriptionFeatureGrants | null>;
|
|
68
|
+
/**
|
|
69
|
+
* The first active plan (by `sortOrder`) visible in the current context
|
|
70
|
+
* that grants `featureKey`, for an upgrade response.
|
|
71
|
+
*/
|
|
72
|
+
findGrantingPlan(featureKey: string, _tenantId: string | null): Promise<SubscriptionGrantingPlan | null>;
|
|
73
|
+
private loadGrants;
|
|
74
|
+
private loadCatalog;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Register `source` as `smrt-features`' process-wide grant source, or clear it
|
|
78
|
+
* with `undefined`. Equivalent to `setFeatureGrantSource()` from
|
|
79
|
+
* `smrt-features`, without importing that package. Register a source built
|
|
80
|
+
* with `memoize: false`; pass request-scoped sources per call instead.
|
|
81
|
+
*/
|
|
82
|
+
export declare function registerSubscriptionFeatureSource(source: SubscriptionFeatureSource | undefined): void;
|
|
83
|
+
/**
|
|
84
|
+
* True when `error` reports that one of this package's tables does not exist
|
|
85
|
+
* — core's `DB_SCHEMA_MISSING` or a raw "no such table" / "relation does not
|
|
86
|
+
* exist" driver error naming a subscription table.
|
|
87
|
+
*/
|
|
88
|
+
export declare function isMissingSubscriptionTableError(error: unknown): boolean;
|
|
89
|
+
//# sourceMappingURL=subscription-feature-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-feature-source.d.ts","sourceRoot":"","sources":["../../src/services/subscription-feature-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE,oEAAoE;AACpE,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,mEAAmE;AACnE,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,UAAU,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,gCAAgC;IAC/C,4EAA4E;IAC5E,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,6EAA6E;AAC7E,eAAO,MAAM,6BAA6B,8CACG,CAAC;AAQ9C,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGnB;IACJ,OAAO,CAAC,OAAO,CAAmD;gBAEtD,OAAO,EAAE,gCAAgC;IAOrD;;;OAGG;WACU,MAAM,CACjB,YAAY,GAAE,gBAAqB,EACnC,OAAO,GAAE,IAAI,CAAC,gCAAgC,EAAE,UAAU,GAAG,OAAO,CAAM,GACzE,OAAO,CAAC,yBAAyB,CAAC;IAQrC,0EAA0E;IAC1E,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IActE;;;OAGG;IACG,gBAAgB,CACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;YAe7B,UAAU;IAuBxB,OAAO,CAAC,WAAW;CAyBpB;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,yBAAyB,GAAG,SAAS,GAC5C,IAAI,CAIN;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAiCvE"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SubscriptionPlan } from '../models/SubscriptionPlan.js';
|
|
2
2
|
import type { SubscriptionPlanVersion } from '../models/SubscriptionPlanVersion.js';
|
|
3
3
|
import type { TenantSubscription } from '../models/TenantSubscription.js';
|
|
4
|
-
import type { EntitlementResolution, EntitlementResolutionContext, PlanThreshold, SmrtClassOptions, Subscriber, SubscriptionResolverOptions, UsageSummary, UsageWindow } from '../types.js';
|
|
4
|
+
import type { EntitlementResolution, EntitlementResolutionContext, PlanFeatureGrant, PlanThreshold, SmrtClassOptions, Subscriber, SubscriptionResolverOptions, UsageSummary, UsageWindow } from '../types.js';
|
|
5
5
|
export interface SubscriptionPlanReader {
|
|
6
6
|
get(criteria: {
|
|
7
7
|
id: string;
|
|
@@ -82,6 +82,39 @@ export declare class SubscriptionResolver {
|
|
|
82
82
|
* this once, then pass it back via `options.context`.
|
|
83
83
|
*/
|
|
84
84
|
loadEntitlementContext(subscriberOrTenantId: Subscriber | string, options?: SubscriptionResolverOptions): Promise<EntitlementResolutionContext>;
|
|
85
|
+
/**
|
|
86
|
+
* Feature keys the subscriber's entitled terms grant, without evaluating
|
|
87
|
+
* usage thresholds (#4761). Same subscription, plan-version and entitlement
|
|
88
|
+
* rules as {@link resolveEntitlements}; used by `SubscriptionFeatureSource`
|
|
89
|
+
* so a flag check never reads usage meters.
|
|
90
|
+
*/
|
|
91
|
+
resolveFeatureGrants(subscriber: Subscriber, options?: SubscriptionResolverOptions): Promise<{
|
|
92
|
+
featureKeys: string[];
|
|
93
|
+
/** The entitled terms' grants with their `metadata` (#4759). */
|
|
94
|
+
grants: PlanFeatureGrant[];
|
|
95
|
+
planId: string | null;
|
|
96
|
+
planKey: string | null;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* The entitled plan terms' thresholds, without reading any usage meter
|
|
100
|
+
* (#4760). Same subscription, plan-version and entitlement rules as
|
|
101
|
+
* {@link resolveEntitlements}; `entitled` is false when there is no
|
|
102
|
+
* current subscription or it does not entitle now (thresholds are then
|
|
103
|
+
* empty). The hosted inference gate reads its spend caps from here and
|
|
104
|
+
* meters `_smrt_ai_usage` itself. Thresholds are returned as stored, valid
|
|
105
|
+
* or not (the models' `getThresholds()` drops invalid entries), so a caller
|
|
106
|
+
* can refuse a malformed cap instead of skipping it; thresholds JSON that
|
|
107
|
+
* is not an array throws.
|
|
108
|
+
*/
|
|
109
|
+
resolvePlanThresholds(subscriber: Subscriber, options?: SubscriptionResolverOptions): Promise<{
|
|
110
|
+
entitled: boolean;
|
|
111
|
+
thresholds: PlanThreshold[];
|
|
112
|
+
planId: string | null;
|
|
113
|
+
planKey: string | null;
|
|
114
|
+
planVersionId: string | null;
|
|
115
|
+
subscriptionId: string | null;
|
|
116
|
+
}>;
|
|
117
|
+
private resolveEntitledTerms;
|
|
85
118
|
/**
|
|
86
119
|
* Polymorphic entitlement resolution. Works for both `'tenant'`-kind and
|
|
87
120
|
* `'external'`-kind subscribers and is the preferred surface — the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription-resolver.d.ts","sourceRoot":"","sources":["../../src/services/subscription-resolver.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,KAAK,EACV,qBAAqB,EACrB,4BAA4B,EAC5B,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,2BAA2B,EAE3B,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AASrB,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACjE;AAED;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,GAAG,CAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IACvE,WAAW,CAAC,CACV,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,wBAAwB;IACvC,oBAAoB,CAClB,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACtC,wBAAwB,CAAC,CACvB,UAAU,EAAE,UAAU,EACtB,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CACvC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,OAAO,EAAE;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QACpC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,WAAW,CAAC;KACrB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC1B,cAAc,CAAC,CAAC,OAAO,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QACpC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,MAAM,EAAE,WAAW,CAAC;KACrB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,sBAAsB,CAAC;IAC9B,aAAa,EAAE,wBAAwB,CAAC;IACxC,KAAK,EAAE,kBAAkB,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,6BAA6B,CAAC;CAC9C;AAED,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,2BAA2B;IAEjE;;;OAGG;WACU,MAAM,CACjB,YAAY,GAAE,gBAAqB,GAClC,OAAO,CAAC,oBAAoB,CAAC;IAoBhC;;;;OAIG;IACG,sBAAsB,CAC1B,oBAAoB,EAAE,UAAU,GAAG,MAAM,EACzC,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,4BAA4B,CAAC;IAgBxC;;;;OAIG;IACG,mBAAmB,CACvB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"subscription-resolver.d.ts","sourceRoot":"","sources":["../../src/services/subscription-resolver.ts"],"names":[],"mappings":"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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-subscriptions",
|
|
6
|
-
"packageVersion": "0.60.
|
|
6
|
+
"packageVersion": "0.60.16",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "d0344a095641eda1066db194207f42badf47b28e0caf1fcc03afc73172b003eb",
|
|
11
|
+
"packageJson": "223e5c265728c912465daa447faf2c245561cdd69ba46abceca53596ec82a694",
|
|
12
|
+
"agents": "920c91705242b95baed544fa2b33223935353a247a9b9946c5776a315525e642"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
15
15
|
".",
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
"@happyvertical/smrt-core": "workspace:*",
|
|
22
22
|
"@happyvertical/smrt-tenancy": "workspace:*",
|
|
23
23
|
"@happyvertical/smrt-ui": "workspace:*",
|
|
24
|
+
"@happyvertical/smrt-users": "workspace:*",
|
|
24
25
|
"@happyvertical/sql": "catalog:",
|
|
26
|
+
"@happyvertical/accounting": "catalog:",
|
|
27
|
+
"@happyvertical/smrt-features": "workspace:*",
|
|
25
28
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
26
29
|
"@sveltejs/package": "^2.5.8",
|
|
27
30
|
"@types/node": "24.13.6",
|
|
@@ -33,8 +36,10 @@
|
|
|
33
36
|
},
|
|
34
37
|
"smrtDependencies": [
|
|
35
38
|
"@happyvertical/smrt-core",
|
|
39
|
+
"@happyvertical/smrt-features",
|
|
36
40
|
"@happyvertical/smrt-tenancy",
|
|
37
41
|
"@happyvertical/smrt-ui",
|
|
42
|
+
"@happyvertical/smrt-users",
|
|
38
43
|
"@happyvertical/smrt-vitest"
|
|
39
44
|
],
|
|
40
45
|
"sdkDependencies": [
|
|
@@ -467,6 +472,93 @@
|
|
|
467
472
|
"tags": [],
|
|
468
473
|
"risks": []
|
|
469
474
|
},
|
|
475
|
+
{
|
|
476
|
+
"name": "BillingCheckoutSession",
|
|
477
|
+
"qualifiedName": "@happyvertical/smrt-subscriptions:BillingCheckoutSession",
|
|
478
|
+
"collection": "billingcheckoutsessions",
|
|
479
|
+
"description": "One Checkout session created by the billing host, recorded when the host creates it and required when the paid session completes, so signed metadata copied onto another integration's session is never…",
|
|
480
|
+
"tableName": "_smrt_billing_checkout_sessions",
|
|
481
|
+
"packageName": "@happyvertical/smrt-subscriptions",
|
|
482
|
+
"extends": "SmrtObject",
|
|
483
|
+
"fields": [
|
|
484
|
+
{
|
|
485
|
+
"name": "sessionId",
|
|
486
|
+
"type": "text",
|
|
487
|
+
"required": true,
|
|
488
|
+
"columnType": "TEXT",
|
|
489
|
+
"default": ""
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"name": "targetTenantId",
|
|
493
|
+
"type": "text",
|
|
494
|
+
"required": false,
|
|
495
|
+
"columnType": "TEXT",
|
|
496
|
+
"default": "",
|
|
497
|
+
"description": "The tenant the host created the session for."
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"name": "status",
|
|
501
|
+
"type": "text",
|
|
502
|
+
"required": false,
|
|
503
|
+
"columnType": "TEXT",
|
|
504
|
+
"default": "pending"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"name": "expiresAt",
|
|
508
|
+
"type": "datetime",
|
|
509
|
+
"required": false,
|
|
510
|
+
"columnType": "TIMESTAMP"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"name": "completedAt",
|
|
514
|
+
"type": "datetime",
|
|
515
|
+
"required": false,
|
|
516
|
+
"columnType": "TIMESTAMP"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "reason",
|
|
520
|
+
"type": "text",
|
|
521
|
+
"required": false,
|
|
522
|
+
"columnType": "TEXT",
|
|
523
|
+
"default": ""
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
"name": "detail",
|
|
527
|
+
"type": "text",
|
|
528
|
+
"required": false,
|
|
529
|
+
"columnType": "TEXT",
|
|
530
|
+
"default": "{}"
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
"relationships": [],
|
|
534
|
+
"methods": [],
|
|
535
|
+
"conflictColumns": [
|
|
536
|
+
"session_id"
|
|
537
|
+
],
|
|
538
|
+
"surfaces": [],
|
|
539
|
+
"relationshipFeatures": [
|
|
540
|
+
"uuidColumns"
|
|
541
|
+
],
|
|
542
|
+
"tags": [],
|
|
543
|
+
"risks": []
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"name": "BillingCheckoutSessionCollection",
|
|
547
|
+
"qualifiedName": "@happyvertical/smrt-subscriptions:BillingCheckoutSessionCollection",
|
|
548
|
+
"collection": "billingcheckoutsessions",
|
|
549
|
+
"tableName": "_smrt_billing_checkout_sessions",
|
|
550
|
+
"packageName": "@happyvertical/smrt-subscriptions",
|
|
551
|
+
"extends": "SmrtCollection",
|
|
552
|
+
"fields": [],
|
|
553
|
+
"relationships": [],
|
|
554
|
+
"methods": [],
|
|
555
|
+
"surfaces": [],
|
|
556
|
+
"relationshipFeatures": [
|
|
557
|
+
"uuidColumns"
|
|
558
|
+
],
|
|
559
|
+
"tags": [],
|
|
560
|
+
"risks": []
|
|
561
|
+
},
|
|
470
562
|
{
|
|
471
563
|
"name": "SubscriptionPlan",
|
|
472
564
|
"qualifiedName": "@happyvertical/smrt-subscriptions:SubscriptionPlan",
|
|
@@ -4228,9 +4320,9 @@
|
|
|
4228
4320
|
"junctionCollections": 0,
|
|
4229
4321
|
"hierarchicalObjects": 0,
|
|
4230
4322
|
"polymorphicAssociations": 0,
|
|
4231
|
-
"uuidColumns":
|
|
4323
|
+
"uuidColumns": 90
|
|
4232
4324
|
},
|
|
4233
|
-
"agentDoc": "# @happyvertical/smrt-subscriptions\n\nTenant subscriptions, plan features, immutable/idempotent usage evidence,\neffective-dated client pricing, scoped spending policies, and entitlement\nresolution for SMRT applications.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-subscriptions test\npnpm --filter @happyvertical/smrt-subscriptions test:postgres\npnpm --filter @happyvertical/smrt-subscriptions typecheck\npnpm --filter @happyvertical/smrt-subscriptions build\n```\n\n`test:postgres` is the lane that holds the money-column line — SQLite's type\naffinity stores a REAL in an INTEGER column without complaint, so a money field\nthat reverted to DECIMAL passes every SQLite suite.\n\n## Money\n\n- **Money is integer minor units** (cents) — `$19.99` is `1999`.\n `SubscriptionPlan.priceAmount`, `ClientCharge.amount`, `RetailCharge.amount`,\n `CreditGrant.amount`,\n `BillingAdjustment.amount` and `SpendingPolicy.limitAmount` all initialize\n `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER\n columns (BIGINT on fresh PostgreSQL/DuckDB databases; #2401, #2373). `ClientCharge.quantity` and `TenantUsageMetric.quantity` are\n the opposite case — metered quantities are genuinely fractional and stay\n DECIMAL.\n- **`PricingRule.terms` are minor units *per unit of usage*, and stay\n fractional.** A per-token rate is routinely a fraction of a cent, so an\n integer `unitPrice` would truncate it to zero. `calculateAmount()` rounds the\n computed result to a whole minor unit — that is the single boundary in the\n package where a rate meets money, and it is what lets `evaluateSpending()`\n compare charge sums against `limitAmount` exactly.\n- **`adjust()` rejects a fractional amount.** Negative amounts are legitimate\n (a credit); fractional ones almost always mean the caller passed major units.\n- **Billing rejects immediate JSON write-back.** The JSON adapter's default\n `writeStrategy: 'immediate'` and DuckDB with that write-back strategy export\n files before transaction commit, while adapter rollback restores only the\n database transaction. `CommercialUsageService` therefore fails before setup\n on those configurations. PostgreSQL, SQLite, ordinary DuckDB, and explicit\n non-immediate write strategies remain supported. When supplying an already\n resolved database handle, also pass `billingStorage` so this capability\n decision is explicit rather than inferred from private adapter state.\n- **Embedded sourced adjustments serialize per database.** SQLite, DuckDB, and\n supported non-immediate JSON handles multiplex local writes; the complete\n deterministic adjustment flow, including recovery reads, runs under one\n database-scoped lock. Every supported embedded adjustment transaction,\n sourced or unsourced, also holds the core embedded write queue for its full\n lifetime; transaction-bound model saves re-enter that hold so adjustments\n cannot absorb, roll back, or deadlock with unrelated ordinary writes.\n- **UI formats by dividing, never by `toFixed`.** `PlanPicker` and\n `CommercialOverview` scale back to major units using the currency's own\n minor-unit exponent, so zero-decimal currencies (JPY, KRW) are not divided.\n- **Migrating an existing database**: `preflightSubscriptionsMoneyMinorUnits(db)`\n reports which columns still hold major units and which rows would be rounded\n or exceed JavaScript's safe-integer range; `migrateSubscriptionsMoneyToMinorUnits(db)` converts them\n (idempotent via `_smrt_backfills`). On SQLite the values are rescaled but the\n declared type needs the table-rebuild path (#2370). Existing\n `PricingRule.terms` are **not** migrated automatically — only the operator\n knows which keys in a given rule are prices rather than quantities or ratios.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns\n are BIGINT, and hydration rejects values outside JavaScript's safe-integer\n range. Existing PostgreSQL `int4` columns require the explicit widening in #2424.\n\n## Plan versions (#4032)\n\n- `SubscriptionPlan.save()` publishes the next immutable\n `SubscriptionPlanVersion` when its terms (`PLAN_TERM_FIELDS`) change; the\n logic lives in `src/plan-versioning.ts`. Versions are append-only,\n contiguous, and monotonic in `effectiveFrom`.\n- `TenantSubscription.validateBeforeSave()` maintains `planVersionId` via\n `pinSubscriptionPlanVersion()`: pin at period start, re-pin when\n `currentPeriodStart` moves, and refuse in-period changes that are not an\n `isEntitlementUpgrade()` unless `replacePlanVersion({ policy: 'authorized',\n reason })`. Never relax this to \"latest version wins\": a paid period must\n not lose entitlements.\n- The resolver reads the pinned version; a missing or inactive pinned version\n fails closed. Only unpinned rows on plans without versions fall back to the\n live plan. `migrateSubscriptionPlanVersions(db)` backfills both.\n- Renewal means the new `currentPeriodStart` is at or after the stored\n `currentPeriodEnd`; creation and renewal pin the boundary's version, and a\n caller-named other version needs an authorized replacement.\n- `canceled` subscriptions entitle until `currentPeriodEnd` only with\n `retainsPaidPeriod`, derived on save from an entitled pre-cancel row.\n- **Recipe `subscriptions.plans`** (`src/recipes.ts`, help in `src/plans.recipe.md`): lists plans, subscriptions, plan versions (read-only, written by `plan-versioning.ts`) and charges (read-only, written by the usage service) for the planner and other catalog hosts. Fields it shows carry `@field({ description })` in plain words, money fields saying \"in cents\"; keep them when editing the models. Reseller models and pricing rules stay out of it.\n\n## Reseller billing (#3059)\n\n- Billing ownership is consumed from `smrt-tenancy`'s\n `BillingRelationshipService` (`BillingRelationshipReader`); never re-derive\n parentage or owners here.\n- `ClientCharge` stays one row per usage event (`usage_event_id` unique) and\n `tenantId` is always the payer. That uniqueness is the double-billing guard\n across owner changes; do not widen its conflict key. The retail leg lives in\n `RetailCharge` (also one per event), written in the same transaction.\n- Price-book rules are `PricingRule` rows with `priceBookId`; `price()` must\n keep ignoring them, and `rateUsage()` only trusts rules whose tenant owns the\n book.\n- Delegated `SpendingPolicy` rows (`setByTenantId`) are guarded in\n `validateBeforeSave` and a `beforeDelete` hook, by id and by conflict key;\n `CreditGrant` applies the same parent-only rule to delegated balances.\n `PriceBookAssignmentCollection` is deliberately not a root export.\n- `ResellerBillingService` authorizes first, then writes in a system context\n (parent writes land on child-owned rows). The wholesale leg is authorized by\n the book's publisher and may never be published by the payer; whether a\n publisher is a legitimate provider for the reseller is a host obligation\n (the model records no upstream seller). Relationship reads precede\n authorization and stay under smrt-tenancy's own read rules. The only\n cross-tenant read in evaluation is the bounded `wholesale`-basis sum.\n- `balance` policies take their limit from `CreditGrant`s; `limitAmount` must\n be 0, and currency and period are fixed once saved (the ledger is scoped to\n them). Auto top-up is a host hook only — no payment provider calls. The\n evaluator re-reads credit after the hook returns (grant or not) and hands\n it `currentShortfall()`, so a hook that charges asynchronously can see a\n concurrent top-up (smrt-commerce's `autoTopUpHook()`, #3139).\n\n## Notes\n\n- Keep Stripe-specific fields as provider binding metadata. Runtime Stripe API\n calls belong in the HappyVertical SDK accounting provider.\n- Thresholds are evaluated from tenant usage metrics and optional AI usage\n summaries. Resolve multiple thresholds with the batch usage path where\n available; do not bypass tenant context in application code.\n- Use `SubscriptionResolver.create()` or an explicitly injected\n `SubscriptionResolver` once per request when resolving entitlements multiple\n times. Load and pass `EntitlementResolutionContext` to avoid re-reading the\n current subscription and plan.\n- Subscription UI components should stay provider-neutral and receive actions\n from the host app.\n",
|
|
4325
|
+
"agentDoc": "# @happyvertical/smrt-subscriptions\n\nTenant subscriptions, plan features, immutable/idempotent usage evidence,\neffective-dated client pricing, scoped spending policies, and entitlement\nresolution for SMRT applications.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-subscriptions test\npnpm --filter @happyvertical/smrt-subscriptions test:postgres\npnpm --filter @happyvertical/smrt-subscriptions typecheck\npnpm --filter @happyvertical/smrt-subscriptions build\n```\n\n`test:postgres` is the lane that holds the money-column line — SQLite's type\naffinity stores a REAL in an INTEGER column without complaint, so a money field\nthat reverted to DECIMAL passes every SQLite suite.\n\n## Money\n\n- **Money is integer minor units** (cents) — `$19.99` is `1999`.\n `SubscriptionPlan.priceAmount`, `ClientCharge.amount`, `RetailCharge.amount`,\n `CreditGrant.amount`,\n `BillingAdjustment.amount` and `SpendingPolicy.limitAmount` all initialize\n `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER\n columns (BIGINT on fresh PostgreSQL/DuckDB databases; #2401, #2373). `ClientCharge.quantity` and `TenantUsageMetric.quantity` are\n the opposite case — metered quantities are genuinely fractional and stay\n DECIMAL.\n- **`PricingRule.terms` are minor units *per unit of usage*, and stay\n fractional.** A per-token rate is routinely a fraction of a cent, so an\n integer `unitPrice` would truncate it to zero. `calculateAmount()` rounds the\n computed result to a whole minor unit — that is the single boundary in the\n package where a rate meets money, and it is what lets `evaluateSpending()`\n compare charge sums against `limitAmount` exactly.\n- **`adjust()` rejects a fractional amount.** Negative amounts are legitimate\n (a credit); fractional ones almost always mean the caller passed major units.\n- **Billing rejects immediate JSON write-back.** The JSON adapter's default\n `writeStrategy: 'immediate'` and DuckDB with that write-back strategy export\n files before transaction commit, while adapter rollback restores only the\n database transaction. `CommercialUsageService` therefore fails before setup\n on those configurations. PostgreSQL, SQLite, ordinary DuckDB, and explicit\n non-immediate write strategies remain supported. When supplying an already\n resolved database handle, also pass `billingStorage` so this capability\n decision is explicit rather than inferred from private adapter state.\n- **Embedded sourced adjustments serialize per database.** SQLite, DuckDB, and\n supported non-immediate JSON handles multiplex local writes; the complete\n deterministic adjustment flow, including recovery reads, runs under one\n database-scoped lock. Every supported embedded adjustment transaction,\n sourced or unsourced, also holds the core embedded write queue for its full\n lifetime; transaction-bound model saves re-enter that hold so adjustments\n cannot absorb, roll back, or deadlock with unrelated ordinary writes.\n- **UI formats by dividing, never by `toFixed`.** `PlanPicker` and\n `CommercialOverview` scale back to major units using the currency's own\n minor-unit exponent, so zero-decimal currencies (JPY, KRW) are not divided.\n- **Migrating an existing database**: `preflightSubscriptionsMoneyMinorUnits(db)`\n reports which columns still hold major units and which rows would be rounded\n or exceed JavaScript's safe-integer range; `migrateSubscriptionsMoneyToMinorUnits(db)` converts them\n (idempotent via `_smrt_backfills`). On SQLite the values are rescaled but the\n declared type needs the table-rebuild path (#2370). Existing\n `PricingRule.terms` are **not** migrated automatically — only the operator\n knows which keys in a given rule are prices rather than quantities or ratios.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns\n are BIGINT, and hydration rejects values outside JavaScript's safe-integer\n range. Existing PostgreSQL `int4` columns require the explicit widening in #2424.\n\n## Plan versions (#4032)\n\n- `SubscriptionPlan.save()` publishes the next immutable\n `SubscriptionPlanVersion` when its terms (`PLAN_TERM_FIELDS`) change; the\n logic lives in `src/plan-versioning.ts`. Versions are append-only,\n contiguous, and monotonic in `effectiveFrom`.\n- `TenantSubscription.validateBeforeSave()` maintains `planVersionId` via\n `pinSubscriptionPlanVersion()`: pin at period start, re-pin when\n `currentPeriodStart` moves, and refuse in-period changes that are not an\n `isEntitlementUpgrade()` unless `replacePlanVersion({ policy: 'authorized',\n reason })`. Never relax this to \"latest version wins\": a paid period must\n not lose entitlements.\n- The resolver reads the pinned version; a missing or inactive pinned version\n fails closed. Only unpinned rows on plans without versions fall back to the\n live plan. `migrateSubscriptionPlanVersions(db)` backfills both.\n- Renewal means the new `currentPeriodStart` is at or after the stored\n `currentPeriodEnd`; creation and renewal pin the boundary's version, and a\n caller-named other version needs an authorized replacement.\n- `canceled` subscriptions entitle until `currentPeriodEnd` only with\n `retainsPaidPeriod`, derived on save from an entitled pre-cancel row.\n- **Recipe `subscriptions.plans`** (`src/recipes.ts`, help in `src/plans.recipe.md`): lists plans, subscriptions, plan versions (read-only, written by `plan-versioning.ts`) and charges (read-only, written by the usage service) for the planner and other catalog hosts. Fields it shows carry `@field({ description })` in plain words, money fields saying \"in cents\"; keep them when editing the models. Reseller models and pricing rules stay out of it.\n\n## Hosted AI spend caps (#4760)\n\n`src/services/hosted-ai-spend.ts`. Plans cap platform-paid (`source:\nhosted`) inference with ordinary `PlanThreshold`s on two reserved keys, so\nwindow, amount and enforcement come from the plan (pinned version included):\n`AI_HOSTED_SPEND_METRIC` (`ai.hosted.spend_micros`, the tenant) and\n`AI_HOSTED_USER_SPEND_METRIC` (`ai.hosted.user_spend_micros`, each user).\n\n- `limit` is **integer micro-dollars** (1 USD = 1,000,000; the money rule at\n per-call precision). `block` blocks at or over the cap, `warn` warns,\n `observe` never affects a call; `warningRatio` (default 0.8) warns early;\n a 0 cap blocks every hosted call. A malformed hosted cap (fractional or\n negative limit, unknown window/enforcement) throws\n `HostedAiSpendPolicyError` (the gate fails closed); it never reads as \"no\n cap\". `resolvePlanThresholds()` returns thresholds unfiltered for this\n reason (the models' `getThresholds()` drop invalid entries) and throws on\n non-array JSON.\n- Spend: `createHostedAiSpendReader(db)` sums `_smrt_ai_usage.estimated_cost`\n (USD, rounded to micro-dollars) by `tenant_id` and `created_at` in the\n cap's window for rows whose `tags` hold `\"source\":\"hosted\"` (and\n `\"userId\":\"<id>\"` for the per-user cap), as exact, case-sensitive JSON\n fragments (`hostedAiTagFragment()`; `strpos` on the PostgreSQL dialect,\n `instr` elsewhere, never `LIKE`, which folds ASCII case on SQLite and\n would merge `Alice` and `alice`); errors only over-count.\n `operation: 'blocked'` rows and external-agent measurements never count;\n uncosted rows add nothing (give hosted models a cost rate). The `userId`\n tag exists only while the hosted gate's ambient-user resolver is\n registered.\n- `resetsAt`: the calendar window's end; for a blocked `rolling` cap (30\n days), `HostedAiSpendReader.liftsAt()`: one window-function query (newest\n first running `SUM(estimated_cost)`, rounded as `summarize()` rounds)\n returns the row whose expiry first brings the total below the cap, plus\n 30 days (null for a 0 cap). A blocked decision names the blocking cap that\n lifts last.\n- `HostedAiSpendEvaluator` (`create({ db })`): plan thresholds via\n `SubscriptionResolver.resolvePlanThresholds()` (entitled terms, no usage\n meters; memoized per tenant for `termsTtlMs`, 30 s, failures evicted,\n `invalidate()`), spend read fresh per call; per-user caps only with a\n user. Errors propagate. smrt-inference's `./gate` consumes it.\n- `resolveEntitlements()` / the tenant usage meter report these keys with\n quantity 0: only the evaluator meters them. The meter's AI summary (and so\n `ai.requests`) skips `operation: 'blocked'` rows. `SpendingPolicy` rows stay the\n billed-charge (cents) policy; hosted AI caps are plan terms because the\n issue's window and amount come from the plan.\n\n## Reseller billing (#3059)\n\n- Billing ownership is consumed from `smrt-tenancy`'s\n `BillingRelationshipService` (`BillingRelationshipReader`); never re-derive\n parentage or owners here.\n- `ClientCharge` stays one row per usage event (`usage_event_id` unique) and\n `tenantId` is always the payer. That uniqueness is the double-billing guard\n across owner changes; do not widen its conflict key. The retail leg lives in\n `RetailCharge` (also one per event), written in the same transaction.\n- Price-book rules are `PricingRule` rows with `priceBookId`; `price()` must\n keep ignoring them, and `rateUsage()` only trusts rules whose tenant owns the\n book.\n- Delegated `SpendingPolicy` rows (`setByTenantId`) are guarded in\n `validateBeforeSave` and a `beforeDelete` hook, by id and by conflict key;\n `CreditGrant` applies the same parent-only rule to delegated balances.\n `PriceBookAssignmentCollection` is deliberately not a root export.\n- `ResellerBillingService` authorizes first, then writes in a system context\n (parent writes land on child-owned rows). The wholesale leg is authorized by\n the book's publisher and may never be published by the payer; whether a\n publisher is a legitimate provider for the reseller is a host obligation\n (the model records no upstream seller). Relationship reads precede\n authorization and stay under smrt-tenancy's own read rules. The only\n cross-tenant read in evaluation is the bounded `wholesale`-basis sum.\n- `balance` policies take their limit from `CreditGrant`s; `limitAmount` must\n be 0, and currency and period are fixed once saved (the ledger is scoped to\n them). Auto top-up is a host hook only — no payment provider calls. The\n evaluator re-reads credit after the hook returns (grant or not) and hands\n it `currentShortfall()`, so a hook that charges asynchronously can see a\n concurrent top-up (smrt-commerce's `autoTopUpHook()`, #3139).\n\n## Billing host glue (#4786)\n\n`src/billing/` is the reusable Stripe checkout, signed idempotent webhook and\nportal wiring (`createBillingHost()`, `readBillingEnv()`); the recipe and\nenvironment names are in the README. Invariants:\n\n- **No direct Stripe calls.** The provider is the structural\n `BillingProviderPort`; `StripeAccountingProvider` from\n `@happyvertical/accounting` satisfies it (a test pins that). A missing SDK\n capability is fixed in the SDK first. `@happyvertical/accounting` is a\n devDependency only (tests run the real provider over a fake `fetch`).\n- **Webhook order is load-bearing:** size cap, signature header, own timestamp\n tolerance, SDK `verify` on the raw body, parse, handled-type filter,\n completed-claim pre-check, then (checkout only) the outside-transaction\n provider read-back, then `runOnce` claiming the event id *and* applying it.\n All DB access inside that transaction must use the `tx` handle; a read on the\n base handle deadlocks single-connection SQLite. Never claim before applying.\n- **Trust comes from our own state.** Checkout metadata is HMAC-signed\n (`smrt_sig`, `signing.ts`) and an unsigned or foreign-signed session never\n links a tenant or plan.\n- **Checkout is bound to a recorded session (#4814)**, `checkout-sessions.ts`\n and `models/BillingCheckoutSession.ts` (`_smrt_billing_checkout_sessions`,\n no API surface; a root export because the manifest advertises it).\n `createCheckout` records the session id\n before returning the URL and signs `smrt_exp`; the completion (inside the\n `runOnce` transaction, after the PostgreSQL `checkout:<id>` lock) checks, in\n order: record exists, status is not `completed` / `needs_reconciliation`\n (no-op `ignored`), record tenant equals the signed tenant, `smrt_exp` is\n well-formed, event time is not after the signed or recorded expiry. A\n missing, mismatched or expired record never links and never drops: the row\n becomes `needs_reconciliation` (outcome `needs_reconciliation`, error log).\n Only `applied` marks a record `completed`. `reconcileCheckout()` (link) and\n `dismissCheckout()` (terminal `dismissed`, for a payment that is not the\n tenant's) are the only ways out and are operator-only; reconcile never skips\n the apply-time conflict checks. The list shows `customerId`,\n `targetTenantId` and `claimedTenantHasOwnSession`, is paged, and has a count.\n The expiry is deterministic per idempotency window so a repeated click sends\n identical provider parameters. Change the table shape only with owner\n approval. A provider subscription or customer id links to one\n tenant; a conflicting event is ignored with a reason code, not merged.\n Verification accepts the signing secret plus `metadataVerificationSecrets`\n (rotation); a purpose-matching session that fails them logs at error.\n A price the host cannot map that differs from the recorded one is held\n (`incomplete`, plan and recorded price kept, `metadata.stripe.held` set so\n checkout still refuses a second subscription) with an error log, never\n carried over onto the previous plan.\n Ignore (2xx, logged) for security refusals; fail (5xx) for configuration or\n database faults so the provider retries.\n- **Only operator plans are sold by default** (`tenantPlans: 'deny'`); price\n resolution never lets a tenant-owned plan outrank a global one. Checkout\n metadata also signs the price and known customer, and the webhook refuses a\n session whose read-back subscription differs. PostgreSQL events lock the\n subscription and tenant (advisory, sorted) and re-read before deciding.\n- **Order state lives on the row** (`metadata.stripe.lastEventAt`, `terminal`);\n a checkout read-back stamps `max(eventCreated, readAt)` so later-delivered\n older events cannot rewind it. Cancellation caps `currentPeriodEnd` at now.\n- **Plan changes** from the provider use `replacePlanVersion({ policy:\n 'authorized', reason: 'stripe:<eventId>' })`; the provider billed the change,\n so a mid-period downgrade is honoured and audited. Do not relax the pinning\n rules for any other caller.\n- **No secrets out.** Responses use fixed messages; logs get ids and reason\n codes only, errors pass through `safeErrorMessage()`. `readBillingEnv` keeps\n secrets non-enumerable. Add a named test per new threat in\n `__tests__/billing-host.test.ts`.\n\n## Vault account restart and supersede (#4841)\n\n`src/vault-account/`. `supersedeVaultAccount`, `unsupersedeVaultAccount` and\n`provisionRestartAccount` take injected ports (`VaultRelayAdminPort`,\n`VaultAccountDirectory`, an audit sink); no HTTP server or relay is needed.\nOrder is load-bearing: owner of the tenant and of both accounts (`not_owner`),\nthen `requireRecentAuthentication` from `smrt-users/step-up`\n(`step_up_required`, default 300 s), then the `authorized` audit record, then\nexactly one relay call, then the outcome record. Refusals are audited and never\nreach the relay; a member never learns about the step-up. Audit entries carry\nids and codes only (no session id, key, token or message text). Import only the\n`@happyvertical/smrt-users/step-up` subpath, never the package root.\n\n## Notes\n\n- Keep Stripe-specific fields as provider binding metadata. Runtime Stripe API\n calls belong in the HappyVertical SDK accounting provider.\n- Thresholds are evaluated from tenant usage metrics and optional AI usage\n summaries. Resolve multiple thresholds with the batch usage path where\n available; do not bypass tenant context in application code.\n- Use `SubscriptionResolver.create()` or an explicitly injected\n `SubscriptionResolver` once per request when resolving entitlements multiple\n times. Load and pass `EntitlementResolutionContext` to avoid re-reading the\n current subscription and plan.\n- Subscription UI components should stay provider-neutral and receive actions\n from the host app.\n- **Feature grants as flags (#4761).** `SubscriptionFeatureSource` implements\n `smrt-features`' `FeatureGrantSource` structurally (no dependency either\n way). Build one per request (`SubscriptionFeatureSource.create(options)` or\n `new SubscriptionFeatureSource({ resolver })`; it memoizes grants per\n tenant) and pass it as `grantSource`, or register an app-wide one built with\n `memoize: false` through `registerSubscriptionFeatureSource()`, which writes\n the features slot key `FEATURE_GRANT_SOURCE_SLOT_KEY`. It reads\n `SubscriptionResolver.resolveFeatureGrants()`, which shares the\n version-pinning and entitlement rules of `resolveEntitlements()` but never\n evaluates thresholds, so a flag check reads no usage meters. Missing\n subscription tables (`isMissingSubscriptionTableError`) and no entitled\n subscription report no grants silently; every other error propagates.\n `findGrantingPlan()` names the first active plan by `sortOrder` visible in\n the current context.\n- **Entitlement change hook (#4759).** `src/services/entitlement-changes.ts`.\n The host wraps its billing-webhook write in\n `trackEntitlementChange({ tenantId, resolver }, apply)`: it snapshots\n `resolveFeatureGrants()` (keys plus grant `metadata`, no usage reads)\n before and after, diffs `gained` / `lost` / `changed` (metadata, e.g. a\n plan budget) and calls listeners registered with\n `registerEntitlementChangeListener()` (a `Symbol.for` slot) or passed in.\n Listeners run after `apply`; their failures are returned in `failures`,\n never thrown, and never undo the write, so listeners must be idempotent.\n An `apply` error propagates and notifies nobody. A redelivered webhook\n diffs to no change, so a lost reaction is repaired by the consumer's\n reconcile entry point (hosted inference: `createHostedInferenceReconciler()`\n with `snapshotEntitlements()`), run on redelivery or a schedule. Listeners satisfy the\n structural type (smrt-inference's hosted listener), so this package\n depends on none of them.\n",
|
|
4234
4326
|
"recipes": [
|
|
4235
4327
|
{
|
|
4236
4328
|
"id": "subscriptions.plans",
|
|
@@ -4599,7 +4691,7 @@
|
|
|
4599
4691
|
"demo": {
|
|
4600
4692
|
"mode": "server",
|
|
4601
4693
|
"reasons": [
|
|
4602
|
-
"Needs a server because its package does not build for a browser: core migrations (node:crypto, node:os); and its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624, #3626)."
|
|
4694
|
+
"Needs a server because its package does not build for a browser: core migrations (node:crypto, node:os); and its package depends on @happyvertical/smrt-profiles, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3617, #3624, #3626)."
|
|
4603
4695
|
]
|
|
4604
4696
|
}
|
|
4605
4697
|
}
|
|
@@ -4607,12 +4699,15 @@
|
|
|
4607
4699
|
"browser": {
|
|
4608
4700
|
"status": "server-only",
|
|
4609
4701
|
"issues": [
|
|
4702
|
+
"#3617",
|
|
4610
4703
|
"#3624",
|
|
4611
4704
|
"#3626"
|
|
4612
4705
|
],
|
|
4613
4706
|
"reason": "core migrations (node:crypto, node:os)",
|
|
4614
4707
|
"via": [
|
|
4615
|
-
"@happyvertical/smrt-
|
|
4708
|
+
"@happyvertical/smrt-profiles",
|
|
4709
|
+
"@happyvertical/smrt-tenancy",
|
|
4710
|
+
"@happyvertical/smrt-users"
|
|
4616
4711
|
]
|
|
4617
4712
|
}
|
|
4618
4713
|
}
|
|
@@ -5,10 +5,10 @@ description: "Tenant subscriptions, entitlement resolution, usage thresholds, an
|
|
|
5
5
|
status: "stable"
|
|
6
6
|
generated:
|
|
7
7
|
by: "smrt/scripts/generate-knowledge-okf.ts"
|
|
8
|
-
at: "2026-11-
|
|
8
|
+
at: "2026-11-09T23:09:38Z"
|
|
9
9
|
audience: "internal"
|
|
10
10
|
evidence_level: "source-inspected"
|
|
11
|
-
source_revision: "sha256:
|
|
11
|
+
source_revision: "sha256:48d3599db9578e435280b3dd42751090babf37fc1f9fc84da681314f14dc5579"
|
|
12
12
|
sources:
|
|
13
13
|
- id: "manifest"
|
|
14
14
|
resource: "https://github.com/happyvertical/smrt/blob/main/packages/subscriptions/package.json"
|
|
@@ -34,8 +34,10 @@ sources:
|
|
|
34
34
|
## Framework dependencies
|
|
35
35
|
|
|
36
36
|
- [smrt-core](/components/smrt-core.md)
|
|
37
|
+
- [smrt-features](/components/smrt-features.md)
|
|
37
38
|
- [smrt-tenancy](/components/smrt-tenancy.md)
|
|
38
39
|
- [smrt-ui](/components/smrt-ui.md)
|
|
40
|
+
- [smrt-users](/components/smrt-users.md)
|
|
39
41
|
- [smrt-vitest](/components/smrt-vitest.md)
|
|
40
42
|
|
|
41
43
|
## SDK dependencies
|
|
@@ -53,6 +55,8 @@ sources:
|
|
|
53
55
|
|
|
54
56
|
- `BillingAdjustment` (collection `billingadjustments`, extends `SmrtObject`)
|
|
55
57
|
- `BillingAdjustmentCollection` (collection `billingadjustments`, extends `SmrtCollection`)
|
|
58
|
+
- `BillingCheckoutSession` (collection `billingcheckoutsessions`, extends `SmrtObject`)
|
|
59
|
+
- `BillingCheckoutSessionCollection` (collection `billingcheckoutsessions`, extends `SmrtCollection`)
|
|
56
60
|
- `ClientCharge` (collection `clientcharges`, extends `SmrtObject`)
|
|
57
61
|
- `ClientChargeCollection` (collection `clientcharges`, extends `SmrtCollection`)
|
|
58
62
|
- `CreditGrant` (collection `creditgrants`, extends `SmrtObject`)
|