@stigmer/server 3.16.0 → 3.17.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/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +6 -0
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +26 -6
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +44 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/plugin/materialize/agent.d.ts +10 -1
- package/dist/domain/plugin/materialize/agent.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/agent.js +35 -1
- package/dist/domain/plugin/materialize/agent.js.map +1 -1
- package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/plan.js +1 -1
- package/dist/domain/plugin/materialize/plan.js.map +1 -1
- package/dist/domain/project/reconcile.d.ts.map +1 -1
- package/dist/domain/project/reconcile.js +14 -10
- package/dist/domain/project/reconcile.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +4 -1
- package/dist/extensions/drivers.d.ts +11 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/license-status.d.ts +64 -0
- package/dist/extensions/license-status.d.ts.map +1 -0
- package/dist/extensions/license-status.js +9 -0
- package/dist/extensions/license-status.js.map +1 -0
- package/dist/extensions/registry.d.ts +6 -0
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +10 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/boot/compose.ts +6 -0
- package/src/domain/platform/__tests__/platform.test.ts +148 -1
- package/src/domain/platform/controller.ts +76 -6
- package/src/domain/plugin/__tests__/materialize.test.ts +60 -1
- package/src/domain/plugin/__tests__/plugin.test.ts +6 -0
- package/src/domain/plugin/materialize/agent.ts +38 -0
- package/src/domain/plugin/materialize/plan.ts +7 -1
- package/src/domain/project/__tests__/reconcile.test.ts +51 -5
- package/src/domain/project/reconcile.ts +20 -15
- package/src/domain/workflow/registry/data/task-kind-registry.json +4 -1
- package/src/extensions/__tests__/license-status-points.test.ts +50 -0
- package/src/extensions/drivers.ts +11 -0
- package/src/extensions/license-status.ts +71 -0
- package/src/extensions/registry.ts +19 -0
- package/src/index.ts +8 -0
- package/src/pipeline/__tests__/billing-contract-rules.test.ts +260 -0
package/src/index.ts
CHANGED
|
@@ -111,6 +111,14 @@ export type {
|
|
|
111
111
|
} from "./extensions/resource-authorization.js";
|
|
112
112
|
export type { OrganizationDirectory } from "./extensions/organization-directory.js";
|
|
113
113
|
export { ALL_ORGANIZATIONS } from "./extensions/organization-directory.js";
|
|
114
|
+
// The license-status seam (drivers.licenseStatus): the provider an
|
|
115
|
+
// Enterprise composition registers so getLicenseStatus answers from its
|
|
116
|
+
// configured ticket. The built-in `absent` default is the composition
|
|
117
|
+
// root's and is not exported.
|
|
118
|
+
export type {
|
|
119
|
+
LicenseStatusProvider,
|
|
120
|
+
LicenseStatusReport,
|
|
121
|
+
} from "./extensions/license-status.js";
|
|
114
122
|
// The 20260911.11 identity-account seams (Q-IA-9): the store PORT a
|
|
115
123
|
// composition drives the domain through (drivers.identityAccountStore;
|
|
116
124
|
// a driver throws DuplicateAccountError for a held id), the federation
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the message-level rules of the plan, subscription and license
|
|
3
|
+
* contract through the shared validator every edition runs
|
|
4
|
+
* (steps/validation.ts, the same instance the transport interceptor uses).
|
|
5
|
+
*
|
|
6
|
+
* Why here and not in a conformance suite: every billing kind is cloud_only,
|
|
7
|
+
* so the open-source server never serves a Plan or a License and the
|
|
8
|
+
* cross-edition suites, where field rules are normally proven against a
|
|
9
|
+
* running server, cannot reach them. The vocabulary those kinds share with
|
|
10
|
+
* the Enterprise edition (Entitlements, LicenseClaims) is verified offline
|
|
11
|
+
* by a server that holds no store for it at all. The one place in this
|
|
12
|
+
* repository where a rule on these messages can be proven red-then-green is
|
|
13
|
+
* the validator itself, so this file drives it directly, one row per rule,
|
|
14
|
+
* a passing shape beside every refused one.
|
|
15
|
+
*
|
|
16
|
+
* What the rows pin: the instrument decides which PlanTerms a plan may carry;
|
|
17
|
+
* a license's timestamps are ordered (issue, expiry, grace); a limit is
|
|
18
|
+
* present or absent, never zero; a feature is named once. A row that
|
|
19
|
+
* stops matching means the contract moved, and the proto is where it moved.
|
|
20
|
+
*/
|
|
21
|
+
import { create } from "@bufbuild/protobuf";
|
|
22
|
+
import type { DescMessage, MessageInitShape } from "@bufbuild/protobuf";
|
|
23
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
24
|
+
import {
|
|
25
|
+
PlanInstrument,
|
|
26
|
+
PlanSpecSchema,
|
|
27
|
+
} from "@stigmer/protos/ai/stigmer/billing/plan/v1/spec_pb";
|
|
28
|
+
import { LicenseSpecSchema } from "@stigmer/protos/ai/stigmer/billing/license/v1/spec_pb";
|
|
29
|
+
import {
|
|
30
|
+
EntitlementLimitsSchema,
|
|
31
|
+
EntitlementsSchema,
|
|
32
|
+
Feature,
|
|
33
|
+
} from "@stigmer/protos/ai/stigmer/platform/v1/entitlement_pb";
|
|
34
|
+
import {
|
|
35
|
+
LicenseClaimsSchema,
|
|
36
|
+
LicenseTerm,
|
|
37
|
+
} from "@stigmer/protos/ai/stigmer/platform/v1/license_pb";
|
|
38
|
+
import { describe, expect, it } from "vitest";
|
|
39
|
+
|
|
40
|
+
import { validator } from "../steps/validation.js";
|
|
41
|
+
|
|
42
|
+
const t = (iso: string) => timestampFromDate(new Date(iso));
|
|
43
|
+
|
|
44
|
+
const customer = {
|
|
45
|
+
id: "cus_acme",
|
|
46
|
+
displayName: "Acme Corp",
|
|
47
|
+
contactEmail: "billing@acme.example",
|
|
48
|
+
};
|
|
49
|
+
const entitlements = { features: [Feature.channels] };
|
|
50
|
+
const orderedTerm = {
|
|
51
|
+
issuedAt: t("2026-09-01T00:00:00Z"),
|
|
52
|
+
expiresAt: t("2027-09-01T00:00:00Z"),
|
|
53
|
+
graceUntil: t("2027-10-01T00:00:00Z"),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** One row: a message, its init, and the rule id it must trip, or none. */
|
|
57
|
+
interface Row<Desc extends DescMessage> {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly schema: Desc;
|
|
60
|
+
readonly init: MessageInitShape<Desc>;
|
|
61
|
+
readonly refusedBy?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function row<Desc extends DescMessage>(r: Row<Desc>): Row<Desc> {
|
|
65
|
+
return r;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rows: ReadonlyArray<Row<DescMessage>> = [
|
|
69
|
+
row({
|
|
70
|
+
name: "a subscription plan with monthly terms",
|
|
71
|
+
schema: PlanSpecSchema,
|
|
72
|
+
init: {
|
|
73
|
+
instrument: PlanInstrument.subscription,
|
|
74
|
+
entitlements,
|
|
75
|
+
terms: { monthlyMinimumMicros: 1n, usageShareBasisPoints: 100 },
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
row({
|
|
79
|
+
name: "a license plan with an annual price",
|
|
80
|
+
schema: PlanSpecSchema,
|
|
81
|
+
init: {
|
|
82
|
+
instrument: PlanInstrument.license,
|
|
83
|
+
entitlements,
|
|
84
|
+
terms: { annualPriceMicros: 1n },
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
row({
|
|
88
|
+
name: "a plan with no terms at all",
|
|
89
|
+
schema: PlanSpecSchema,
|
|
90
|
+
init: { instrument: PlanInstrument.subscription, entitlements },
|
|
91
|
+
}),
|
|
92
|
+
row({
|
|
93
|
+
name: "a license plan carrying a monthly minimum",
|
|
94
|
+
schema: PlanSpecSchema,
|
|
95
|
+
init: {
|
|
96
|
+
instrument: PlanInstrument.license,
|
|
97
|
+
entitlements,
|
|
98
|
+
terms: { monthlyMinimumMicros: 1n },
|
|
99
|
+
},
|
|
100
|
+
refusedBy: "plan_spec.license_has_no_monthly_terms",
|
|
101
|
+
}),
|
|
102
|
+
row({
|
|
103
|
+
name: "a license plan carrying a usage share",
|
|
104
|
+
schema: PlanSpecSchema,
|
|
105
|
+
init: {
|
|
106
|
+
instrument: PlanInstrument.license,
|
|
107
|
+
entitlements,
|
|
108
|
+
terms: { usageShareBasisPoints: 100 },
|
|
109
|
+
},
|
|
110
|
+
refusedBy: "plan_spec.license_has_no_monthly_terms",
|
|
111
|
+
}),
|
|
112
|
+
row({
|
|
113
|
+
name: "a subscription plan carrying an annual price",
|
|
114
|
+
schema: PlanSpecSchema,
|
|
115
|
+
init: {
|
|
116
|
+
instrument: PlanInstrument.subscription,
|
|
117
|
+
entitlements,
|
|
118
|
+
terms: { annualPriceMicros: 1n },
|
|
119
|
+
},
|
|
120
|
+
refusedBy: "plan_spec.subscription_has_no_annual_price",
|
|
121
|
+
}),
|
|
122
|
+
row({
|
|
123
|
+
name: "a usage share above one hundred percent",
|
|
124
|
+
schema: PlanSpecSchema,
|
|
125
|
+
init: {
|
|
126
|
+
instrument: PlanInstrument.subscription,
|
|
127
|
+
entitlements,
|
|
128
|
+
terms: { usageShareBasisPoints: 10001 },
|
|
129
|
+
},
|
|
130
|
+
refusedBy: "int32.gte_lte",
|
|
131
|
+
}),
|
|
132
|
+
row({
|
|
133
|
+
name: "ordered license claims",
|
|
134
|
+
schema: LicenseClaimsSchema,
|
|
135
|
+
init: {
|
|
136
|
+
licenseId: "lic_1",
|
|
137
|
+
customer,
|
|
138
|
+
term: LicenseTerm.paid,
|
|
139
|
+
entitlements,
|
|
140
|
+
...orderedTerm,
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
row({
|
|
144
|
+
name: "claims expiring before they were issued",
|
|
145
|
+
schema: LicenseClaimsSchema,
|
|
146
|
+
init: {
|
|
147
|
+
licenseId: "lic_1",
|
|
148
|
+
customer,
|
|
149
|
+
term: LicenseTerm.paid,
|
|
150
|
+
entitlements,
|
|
151
|
+
...orderedTerm,
|
|
152
|
+
expiresAt: t("2026-08-01T00:00:00Z"),
|
|
153
|
+
graceUntil: t("2026-08-02T00:00:00Z"),
|
|
154
|
+
},
|
|
155
|
+
refusedBy: "license_claims.expires_after_issue",
|
|
156
|
+
}),
|
|
157
|
+
row({
|
|
158
|
+
name: "claims whose grace ends before expiry",
|
|
159
|
+
schema: LicenseClaimsSchema,
|
|
160
|
+
init: {
|
|
161
|
+
licenseId: "lic_1",
|
|
162
|
+
customer,
|
|
163
|
+
term: LicenseTerm.paid,
|
|
164
|
+
entitlements,
|
|
165
|
+
...orderedTerm,
|
|
166
|
+
graceUntil: t("2027-08-01T00:00:00Z"),
|
|
167
|
+
},
|
|
168
|
+
refusedBy: "license_claims.grace_not_before_expiry",
|
|
169
|
+
}),
|
|
170
|
+
row({
|
|
171
|
+
name: "a customer with no contact address",
|
|
172
|
+
schema: LicenseClaimsSchema,
|
|
173
|
+
init: {
|
|
174
|
+
licenseId: "lic_1",
|
|
175
|
+
customer: { id: "cus_acme", displayName: "Acme Corp" },
|
|
176
|
+
term: LicenseTerm.paid,
|
|
177
|
+
entitlements,
|
|
178
|
+
...orderedTerm,
|
|
179
|
+
},
|
|
180
|
+
refusedBy: "required",
|
|
181
|
+
}),
|
|
182
|
+
row({
|
|
183
|
+
name: "a customer whose contact address is not an email",
|
|
184
|
+
schema: LicenseClaimsSchema,
|
|
185
|
+
init: {
|
|
186
|
+
licenseId: "lic_1",
|
|
187
|
+
customer: { ...customer, contactEmail: "acme" },
|
|
188
|
+
term: LicenseTerm.paid,
|
|
189
|
+
entitlements,
|
|
190
|
+
...orderedTerm,
|
|
191
|
+
},
|
|
192
|
+
refusedBy: "string.email",
|
|
193
|
+
}),
|
|
194
|
+
row({
|
|
195
|
+
name: "a license spec whose grace ends before expiry",
|
|
196
|
+
schema: LicenseSpecSchema,
|
|
197
|
+
init: {
|
|
198
|
+
customer,
|
|
199
|
+
entitlements,
|
|
200
|
+
term: LicenseTerm.trial,
|
|
201
|
+
expiresAt: orderedTerm.expiresAt,
|
|
202
|
+
graceUntil: t("2027-08-01T00:00:00Z"),
|
|
203
|
+
},
|
|
204
|
+
refusedBy: "license_spec.grace_not_before_expiry",
|
|
205
|
+
}),
|
|
206
|
+
row({
|
|
207
|
+
name: "a license spec with grace on the expiry instant",
|
|
208
|
+
schema: LicenseSpecSchema,
|
|
209
|
+
init: {
|
|
210
|
+
customer,
|
|
211
|
+
entitlements,
|
|
212
|
+
term: LicenseTerm.trial,
|
|
213
|
+
expiresAt: orderedTerm.expiresAt,
|
|
214
|
+
graceUntil: orderedTerm.expiresAt,
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
row({
|
|
218
|
+
name: "a limit of zero, which is never what an operator meant",
|
|
219
|
+
schema: EntitlementLimitsSchema,
|
|
220
|
+
init: { maxOrganizations: 0 },
|
|
221
|
+
refusedBy: "int32.gt",
|
|
222
|
+
}),
|
|
223
|
+
row({
|
|
224
|
+
name: "an absent limit, which is no limit",
|
|
225
|
+
schema: EntitlementLimitsSchema,
|
|
226
|
+
init: {},
|
|
227
|
+
}),
|
|
228
|
+
row({
|
|
229
|
+
name: "a feature named twice",
|
|
230
|
+
schema: EntitlementsSchema,
|
|
231
|
+
init: { features: [Feature.channels, Feature.channels] },
|
|
232
|
+
refusedBy: "repeated.unique",
|
|
233
|
+
}),
|
|
234
|
+
row({
|
|
235
|
+
name: "the unspecified feature",
|
|
236
|
+
schema: EntitlementsSchema,
|
|
237
|
+
init: { features: [Feature.feature_unspecified] },
|
|
238
|
+
refusedBy: "enum.not_in",
|
|
239
|
+
}),
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
describe("the billing contract's rules, through the shared validator", () => {
|
|
243
|
+
for (const r of rows) {
|
|
244
|
+
const verdict =
|
|
245
|
+
r.refusedBy === undefined ? "passes" : `is refused by ${r.refusedBy}`;
|
|
246
|
+
it(`${r.name} ${verdict}`, () => {
|
|
247
|
+
const result = validator().validate(r.schema, create(r.schema, r.init));
|
|
248
|
+
if (r.refusedBy === undefined) {
|
|
249
|
+
expect(result.kind).toBe("valid");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (result.kind !== "invalid") {
|
|
253
|
+
throw new Error(`expected a refusal, got ${result.kind}`);
|
|
254
|
+
}
|
|
255
|
+
expect(result.error.violations.map((v) => v.ruleId)).toContain(
|
|
256
|
+
r.refusedBy,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|