@happyvertical/smrt-subscriptions 0.60.15 → 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.
Files changed (43) hide show
  1. package/AGENTS.md +78 -0
  2. package/README.md +195 -0
  3. package/dist/billing/apply.d.ts +52 -0
  4. package/dist/billing/apply.d.ts.map +1 -0
  5. package/dist/billing/checkout-sessions.d.ts +99 -0
  6. package/dist/billing/checkout-sessions.d.ts.map +1 -0
  7. package/dist/billing/config.d.ts +64 -0
  8. package/dist/billing/config.d.ts.map +1 -0
  9. package/dist/billing/events.d.ts +49 -0
  10. package/dist/billing/events.d.ts.map +1 -0
  11. package/dist/billing/host.d.ts +3 -0
  12. package/dist/billing/host.d.ts.map +1 -0
  13. package/dist/billing/index.d.ts +4 -0
  14. package/dist/billing/index.d.ts.map +1 -0
  15. package/dist/billing/plans.d.ts +45 -0
  16. package/dist/billing/plans.d.ts.map +1 -0
  17. package/dist/billing/signing.d.ts +24 -0
  18. package/dist/billing/signing.d.ts.map +1 -0
  19. package/dist/billing/types.d.ts +265 -0
  20. package/dist/billing/types.d.ts.map +1 -0
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1648 -26
  24. package/dist/index.js.map +1 -1
  25. package/dist/manifest.json +313 -4
  26. package/dist/models/BillingCheckoutSession.d.ts +39 -0
  27. package/dist/models/BillingCheckoutSession.d.ts.map +1 -0
  28. package/dist/models/index.d.ts +1 -0
  29. package/dist/models/index.d.ts.map +1 -1
  30. package/dist/recipes.d.ts +1 -1
  31. package/dist/smrt-knowledge.json +101 -8
  32. package/dist/smrt-knowledge.okf.md +5 -2
  33. package/dist/vault-account/actions.d.ts +24 -0
  34. package/dist/vault-account/actions.d.ts.map +1 -0
  35. package/dist/vault-account/errors.d.ts +20 -0
  36. package/dist/vault-account/errors.d.ts.map +1 -0
  37. package/dist/vault-account/index.d.ts +5 -0
  38. package/dist/vault-account/index.d.ts.map +1 -0
  39. package/dist/vault-account/relay-admin-client.d.ts +10 -0
  40. package/dist/vault-account/relay-admin-client.d.ts.map +1 -0
  41. package/dist/vault-account/types.d.ts +115 -0
  42. package/dist/vault-account/types.d.ts.map +1 -0
  43. package/package.json +9 -7
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-subscriptions",
5
- "packageVersion": "0.60.15",
5
+ "packageVersion": "0.60.16",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-subscriptions:SubscriptionPlanCollection": {
8
8
  "name": "subscriptionplancollection",
@@ -1132,6 +1132,311 @@
1132
1132
  },
1133
1133
  "importPath": "@happyvertical/smrt-subscriptions"
1134
1134
  },
1135
+ "@happyvertical/smrt-subscriptions:BillingCheckoutSession": {
1136
+ "name": "billingcheckoutsession",
1137
+ "className": "BillingCheckoutSession",
1138
+ "qualifiedName": "@happyvertical/smrt-subscriptions:BillingCheckoutSession",
1139
+ "collection": "billingcheckoutsessions",
1140
+ "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…",
1141
+ "filePath": "/home/runner/work/smrt/smrt/packages/subscriptions/src/models/BillingCheckoutSession.ts",
1142
+ "packageName": "@happyvertical/smrt-subscriptions",
1143
+ "fields": {
1144
+ "sessionId": {
1145
+ "type": "text",
1146
+ "required": true,
1147
+ "default": "",
1148
+ "_meta": {
1149
+ "required": true
1150
+ }
1151
+ },
1152
+ "targetTenantId": {
1153
+ "type": "text",
1154
+ "required": false,
1155
+ "default": "",
1156
+ "description": "The tenant the host created the session for.",
1157
+ "_meta": {
1158
+ "description": "The tenant the host created the session for."
1159
+ }
1160
+ },
1161
+ "status": {
1162
+ "type": "text",
1163
+ "required": false,
1164
+ "default": "pending",
1165
+ "enum": [
1166
+ "pending",
1167
+ "completed",
1168
+ "needs_reconciliation",
1169
+ "dismissed"
1170
+ ]
1171
+ },
1172
+ "expiresAt": {
1173
+ "type": "datetime",
1174
+ "required": false
1175
+ },
1176
+ "completedAt": {
1177
+ "type": "datetime",
1178
+ "required": false
1179
+ },
1180
+ "reason": {
1181
+ "type": "text",
1182
+ "required": false,
1183
+ "default": ""
1184
+ },
1185
+ "detail": {
1186
+ "type": "text",
1187
+ "required": false,
1188
+ "default": "{}"
1189
+ }
1190
+ },
1191
+ "methods": {},
1192
+ "decoratorConfig": {
1193
+ "tableName": "_smrt_billing_checkout_sessions",
1194
+ "conflictColumns": [
1195
+ "session_id"
1196
+ ],
1197
+ "api": false,
1198
+ "cli": false,
1199
+ "mcp": false
1200
+ },
1201
+ "extends": "SmrtObject",
1202
+ "exportName": "BillingCheckoutSession",
1203
+ "collectionExportName": "BillingCheckoutSessionCollection",
1204
+ "validationRules": [
1205
+ {
1206
+ "field": "sessionId",
1207
+ "rule": "required",
1208
+ "fieldType": "text"
1209
+ }
1210
+ ],
1211
+ "schema": {
1212
+ "tableName": "_smrt_billing_checkout_sessions",
1213
+ "ddl": "CREATE TABLE IF NOT EXISTS \"_smrt_billing_checkout_sessions\" (\n \"_sync_origin_device_id\" TEXT,\n \"_sync_revision\" INTEGER,\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"session_id\" TEXT NOT NULL DEFAULT '',\n \"target_tenant_id\" TEXT DEFAULT '',\n \"status\" TEXT DEFAULT 'pending',\n \"expires_at\" TIMESTAMP,\n \"completed_at\" TIMESTAMP,\n \"reason\" TEXT DEFAULT '',\n \"detail\" TEXT DEFAULT '{}'\n);",
1214
+ "columns": {
1215
+ "_sync_origin_device_id": {
1216
+ "type": "TEXT"
1217
+ },
1218
+ "_sync_revision": {
1219
+ "type": "INTEGER"
1220
+ },
1221
+ "id": {
1222
+ "type": "UUID",
1223
+ "primaryKey": true,
1224
+ "referenceKind": "id",
1225
+ "notNull": true
1226
+ },
1227
+ "slug": {
1228
+ "type": "TEXT",
1229
+ "notNull": true
1230
+ },
1231
+ "context": {
1232
+ "type": "TEXT",
1233
+ "notNull": true,
1234
+ "default": ""
1235
+ },
1236
+ "created_at": {
1237
+ "type": "TIMESTAMP",
1238
+ "notNull": true,
1239
+ "default": "current_timestamp"
1240
+ },
1241
+ "updated_at": {
1242
+ "type": "TIMESTAMP",
1243
+ "notNull": true,
1244
+ "default": "current_timestamp"
1245
+ },
1246
+ "session_id": {
1247
+ "type": "TEXT",
1248
+ "notNull": true,
1249
+ "unique": false,
1250
+ "default": ""
1251
+ },
1252
+ "target_tenant_id": {
1253
+ "type": "TEXT",
1254
+ "notNull": false,
1255
+ "unique": false,
1256
+ "default": ""
1257
+ },
1258
+ "status": {
1259
+ "type": "TEXT",
1260
+ "notNull": false,
1261
+ "unique": false,
1262
+ "default": "pending"
1263
+ },
1264
+ "expires_at": {
1265
+ "type": "TIMESTAMP",
1266
+ "notNull": false,
1267
+ "unique": false
1268
+ },
1269
+ "completed_at": {
1270
+ "type": "TIMESTAMP",
1271
+ "notNull": false,
1272
+ "unique": false
1273
+ },
1274
+ "reason": {
1275
+ "type": "TEXT",
1276
+ "notNull": false,
1277
+ "unique": false,
1278
+ "default": ""
1279
+ },
1280
+ "detail": {
1281
+ "type": "TEXT",
1282
+ "notNull": false,
1283
+ "unique": false,
1284
+ "default": "{}"
1285
+ }
1286
+ },
1287
+ "indexes": [
1288
+ {
1289
+ "name": "_smrt_billing_checkout_sessions_session_id_idx",
1290
+ "columns": [
1291
+ "session_id"
1292
+ ],
1293
+ "unique": true
1294
+ },
1295
+ {
1296
+ "name": "_smrt_billing_checkout_sessions_slug_context_idx",
1297
+ "columns": [
1298
+ "slug",
1299
+ "context"
1300
+ ]
1301
+ },
1302
+ {
1303
+ "name": "_smrt_billing_checkout_sessions_created_at_idx",
1304
+ "columns": [
1305
+ "created_at"
1306
+ ]
1307
+ }
1308
+ ],
1309
+ "version": "c1924881"
1310
+ },
1311
+ "importPath": "@happyvertical/smrt-subscriptions"
1312
+ },
1313
+ "@happyvertical/smrt-subscriptions:BillingCheckoutSessionCollection": {
1314
+ "name": "billingcheckoutsessioncollection",
1315
+ "className": "BillingCheckoutSessionCollection",
1316
+ "qualifiedName": "@happyvertical/smrt-subscriptions:BillingCheckoutSessionCollection",
1317
+ "collection": "billingcheckoutsessions",
1318
+ "filePath": "/home/runner/work/smrt/smrt/packages/subscriptions/src/models/BillingCheckoutSession.ts",
1319
+ "packageName": "@happyvertical/smrt-subscriptions",
1320
+ "fields": {},
1321
+ "methods": {},
1322
+ "decoratorConfig": {
1323
+ "tableName": "_smrt_billing_checkout_sessions",
1324
+ "api": false,
1325
+ "mcp": false,
1326
+ "cli": false
1327
+ },
1328
+ "extends": "SmrtCollection",
1329
+ "extendsTypeArg": "BillingCheckoutSession",
1330
+ "exportName": "BillingCheckoutSessionCollection",
1331
+ "collectionExportName": "BillingCheckoutSessionCollectionCollection",
1332
+ "collectionOf": "@happyvertical/smrt-subscriptions:BillingCheckoutSession",
1333
+ "inheritedExposure": [
1334
+ "api",
1335
+ "mcp",
1336
+ "cli"
1337
+ ],
1338
+ "schema": {
1339
+ "tableName": "_smrt_billing_checkout_sessions",
1340
+ "ddl": "CREATE TABLE IF NOT EXISTS \"_smrt_billing_checkout_sessions\" (\n \"_sync_origin_device_id\" TEXT,\n \"_sync_revision\" INTEGER,\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"session_id\" TEXT NOT NULL DEFAULT '',\n \"target_tenant_id\" TEXT DEFAULT '',\n \"status\" TEXT DEFAULT 'pending',\n \"expires_at\" TIMESTAMP,\n \"completed_at\" TIMESTAMP,\n \"reason\" TEXT DEFAULT '',\n \"detail\" TEXT DEFAULT '{}'\n);",
1341
+ "columns": {
1342
+ "_sync_origin_device_id": {
1343
+ "type": "TEXT"
1344
+ },
1345
+ "_sync_revision": {
1346
+ "type": "INTEGER"
1347
+ },
1348
+ "id": {
1349
+ "type": "UUID",
1350
+ "primaryKey": true,
1351
+ "referenceKind": "id",
1352
+ "notNull": true
1353
+ },
1354
+ "slug": {
1355
+ "type": "TEXT",
1356
+ "notNull": true
1357
+ },
1358
+ "context": {
1359
+ "type": "TEXT",
1360
+ "notNull": true,
1361
+ "default": ""
1362
+ },
1363
+ "created_at": {
1364
+ "type": "TIMESTAMP",
1365
+ "notNull": true,
1366
+ "default": "current_timestamp"
1367
+ },
1368
+ "updated_at": {
1369
+ "type": "TIMESTAMP",
1370
+ "notNull": true,
1371
+ "default": "current_timestamp"
1372
+ },
1373
+ "session_id": {
1374
+ "type": "TEXT",
1375
+ "notNull": true,
1376
+ "unique": false,
1377
+ "default": ""
1378
+ },
1379
+ "target_tenant_id": {
1380
+ "type": "TEXT",
1381
+ "notNull": false,
1382
+ "unique": false,
1383
+ "default": ""
1384
+ },
1385
+ "status": {
1386
+ "type": "TEXT",
1387
+ "notNull": false,
1388
+ "unique": false,
1389
+ "default": "pending"
1390
+ },
1391
+ "expires_at": {
1392
+ "type": "TIMESTAMP",
1393
+ "notNull": false,
1394
+ "unique": false
1395
+ },
1396
+ "completed_at": {
1397
+ "type": "TIMESTAMP",
1398
+ "notNull": false,
1399
+ "unique": false
1400
+ },
1401
+ "reason": {
1402
+ "type": "TEXT",
1403
+ "notNull": false,
1404
+ "unique": false,
1405
+ "default": ""
1406
+ },
1407
+ "detail": {
1408
+ "type": "TEXT",
1409
+ "notNull": false,
1410
+ "unique": false,
1411
+ "default": "{}"
1412
+ }
1413
+ },
1414
+ "indexes": [
1415
+ {
1416
+ "name": "_smrt_billing_checkout_sessions_session_id_idx",
1417
+ "columns": [
1418
+ "session_id"
1419
+ ],
1420
+ "unique": true
1421
+ },
1422
+ {
1423
+ "name": "_smrt_billing_checkout_sessions_slug_context_idx",
1424
+ "columns": [
1425
+ "slug",
1426
+ "context"
1427
+ ]
1428
+ },
1429
+ {
1430
+ "name": "_smrt_billing_checkout_sessions_created_at_idx",
1431
+ "columns": [
1432
+ "created_at"
1433
+ ]
1434
+ }
1435
+ ],
1436
+ "version": "c1924881"
1437
+ },
1438
+ "importPath": "@happyvertical/smrt-subscriptions"
1439
+ },
1135
1440
  "@happyvertical/smrt-subscriptions:SubscriptionPlan": {
1136
1441
  "name": "subscriptionplan",
1137
1442
  "className": "SubscriptionPlan",
@@ -6894,7 +7199,7 @@
6894
7199
  "demo": {
6895
7200
  "mode": "server",
6896
7201
  "reasons": [
6897
- "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)."
7202
+ "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)."
6898
7203
  ]
6899
7204
  }
6900
7205
  }
@@ -6902,17 +7207,21 @@
6902
7207
  "smrtDependencies": [
6903
7208
  "@happyvertical/smrt-core",
6904
7209
  "@happyvertical/smrt-features",
6905
- "@happyvertical/smrt-tenancy"
7210
+ "@happyvertical/smrt-tenancy",
7211
+ "@happyvertical/smrt-users"
6906
7212
  ],
6907
7213
  "browser": {
6908
7214
  "status": "server-only",
6909
7215
  "issues": [
7216
+ "#3617",
6910
7217
  "#3624",
6911
7218
  "#3626"
6912
7219
  ],
6913
7220
  "reason": "core migrations (node:crypto, node:os)",
6914
7221
  "via": [
6915
- "@happyvertical/smrt-tenancy"
7222
+ "@happyvertical/smrt-profiles",
7223
+ "@happyvertical/smrt-tenancy",
7224
+ "@happyvertical/smrt-users"
6916
7225
  ]
6917
7226
  },
6918
7227
  "stampsConstructors": true
@@ -0,0 +1,39 @@
1
+ import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';
2
+ /**
3
+ * Where a Checkout session the billing host created stands (#4814).
4
+ *
5
+ * - `pending`: created by the host, not yet completed.
6
+ * - `completed`: the paid session was linked to its tenant (single use).
7
+ * - `needs_reconciliation`: a paid session whose record was missing, expired
8
+ * or did not match. It is not linked; an operator links it on purpose with
9
+ * `BillingHost.reconcileCheckout()`.
10
+ */
11
+ export type BillingCheckoutSessionStatus = 'pending' | 'completed' | 'needs_reconciliation' | 'dismissed';
12
+ /**
13
+ * One Checkout session created by the billing host, recorded when the host
14
+ * creates it and required when the paid session completes, so signed
15
+ * metadata copied onto another integration's session is never trusted.
16
+ * `sessionId` is the provider's unguessable, unique id and doubles as the
17
+ * single-use nonce.
18
+ *
19
+ * A system table managed through `createBillingHost()`, like
20
+ * `_smrt_price_book_assignments`; it has no generated API surface. It is a
21
+ * root export because the published manifest advertises it. `tenantId` is the
22
+ * tenant the host created the session for (`''` on a row opened for a session
23
+ * the host never recorded).
24
+ */
25
+ export declare class BillingCheckoutSession extends SmrtObject {
26
+ sessionId: string;
27
+ targetTenantId: string;
28
+ status: BillingCheckoutSessionStatus;
29
+ expiresAt: Date;
30
+ completedAt: Date | null;
31
+ /** Why the row needs reconciliation (a code, never provider text). */
32
+ reason: string;
33
+ /** JSON ids needed to reconcile: event, subscription, customer, plan. */
34
+ detail: string;
35
+ }
36
+ export declare class BillingCheckoutSessionCollection extends SmrtCollection<BillingCheckoutSession> {
37
+ static readonly _itemClass: typeof BillingCheckoutSession;
38
+ }
39
+ //# sourceMappingURL=BillingCheckoutSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BillingCheckoutSession.d.ts","sourceRoot":"","sources":["../../src/models/BillingCheckoutSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;GAQG;AACH,MAAM,MAAM,4BAA4B,GACpC,SAAS,GACT,WAAW,GACX,sBAAsB,GACtB,WAAW,CAAC;AAEhB;;;;;;;;;;;;GAYG;AACH,qBAOa,sBAAuB,SAAQ,UAAU;IAEpD,SAAS,EAAE,MAAM,CAAM;IAEvB,cAAc,EAAE,MAAM,CAAM;IAC5B,MAAM,EAAE,4BAA4B,CAAa;IACjD,SAAS,EAAE,IAAI,CAAe;IAC9B,WAAW,EAAE,IAAI,GAAG,IAAI,CAAQ;IAChC,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAM;IACpB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAQ;CACvB;AAED,qBAAa,gCAAiC,SAAQ,cAAc,CAAC,sBAAsB,CAAC;IAC1F,MAAM,CAAC,QAAQ,CAAC,UAAU,gCAA0B;CACrD"}
@@ -1,3 +1,4 @@
1
+ export { BillingCheckoutSession } from './BillingCheckoutSession.js';
1
2
  export type { PricingStrategy, SpendingBasis, SpendingPeriod, SpendingPolicyBehavior, } from './commercial.js';
2
3
  export { BillingAdjustment, BillingAdjustmentCollection, ClientCharge, ClientChargeCollection, PricingRule, PricingRuleCollection, SpendingPolicy, SpendingPolicyCollection, } from './commercial.js';
3
4
  export type { PriceBookKind } from './reseller.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/models/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,aAAa,EACb,cAAc,EACd,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,EACZ,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/models/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACV,eAAe,EACf,aAAa,EACb,cAAc,EACd,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,EACZ,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/recipes.d.ts CHANGED
@@ -29,7 +29,7 @@ export declare class SubscriptionPlansRecipe extends SmrtRecipe {
29
29
  icon: string;
30
30
  description: string;
31
31
  };
32
- static models: (typeof SubscriptionPlanVersion | typeof TenantSubscription | typeof SubscriptionPlan | typeof ClientCharge)[];
32
+ static models: (typeof SubscriptionPlan | typeof SubscriptionPlanVersion | typeof TenantSubscription | typeof ClientCharge)[];
33
33
  static nav: ({
34
34
  label: string;
35
35
  model: typeof SubscriptionPlan;
@@ -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.15",
6
+ "packageVersion": "0.60.16",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "f4816979dc2afcb8355251a0fc02a249c9098e0a8b5224d53066244daabfb538",
11
- "packageJson": "59ca645f3cb564affced4b5417316c97ffb8cae0c561edf79239c7d11fafef44",
12
- "agents": "51c6ab853fadd46068e2146e60313a0818cb1e10fdc28abb97fc00a5173e1bbb"
10
+ "manifest": "d0344a095641eda1066db194207f42badf47b28e0caf1fcc03afc73172b003eb",
11
+ "packageJson": "223e5c265728c912465daa447faf2c245561cdd69ba46abceca53596ec82a694",
12
+ "agents": "920c91705242b95baed544fa2b33223935353a247a9b9946c5776a315525e642"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -21,7 +21,9 @@
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:",
25
27
  "@happyvertical/smrt-features": "workspace:*",
26
28
  "@happyvertical/smrt-vitest": "workspace:*",
27
29
  "@sveltejs/package": "^2.5.8",
@@ -37,6 +39,7 @@
37
39
  "@happyvertical/smrt-features",
38
40
  "@happyvertical/smrt-tenancy",
39
41
  "@happyvertical/smrt-ui",
42
+ "@happyvertical/smrt-users",
40
43
  "@happyvertical/smrt-vitest"
41
44
  ],
42
45
  "sdkDependencies": [
@@ -469,6 +472,93 @@
469
472
  "tags": [],
470
473
  "risks": []
471
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
+ },
472
562
  {
473
563
  "name": "SubscriptionPlan",
474
564
  "qualifiedName": "@happyvertical/smrt-subscriptions:SubscriptionPlan",
@@ -4230,9 +4320,9 @@
4230
4320
  "junctionCollections": 0,
4231
4321
  "hierarchicalObjects": 0,
4232
4322
  "polymorphicAssociations": 0,
4233
- "uuidColumns": 88
4323
+ "uuidColumns": 90
4234
4324
  },
4235
- "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## 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",
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",
4236
4326
  "recipes": [
4237
4327
  {
4238
4328
  "id": "subscriptions.plans",
@@ -4601,7 +4691,7 @@
4601
4691
  "demo": {
4602
4692
  "mode": "server",
4603
4693
  "reasons": [
4604
- "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)."
4605
4695
  ]
4606
4696
  }
4607
4697
  }
@@ -4609,12 +4699,15 @@
4609
4699
  "browser": {
4610
4700
  "status": "server-only",
4611
4701
  "issues": [
4702
+ "#3617",
4612
4703
  "#3624",
4613
4704
  "#3626"
4614
4705
  ],
4615
4706
  "reason": "core migrations (node:crypto, node:os)",
4616
4707
  "via": [
4617
- "@happyvertical/smrt-tenancy"
4708
+ "@happyvertical/smrt-profiles",
4709
+ "@happyvertical/smrt-tenancy",
4710
+ "@happyvertical/smrt-users"
4618
4711
  ]
4619
4712
  }
4620
4713
  }