@ciromaciel/contract 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -94,7 +94,7 @@ the product-prefix check and the choice between `PLAN_LIMIT` and
94
94
 
95
95
  ## Links
96
96
 
97
- - [Documentation](https://myinfrastructure.click)
97
+ - [Website](https://ciromaciel.click)
98
98
  - [Package on npm](https://www.npmjs.com/package/@ciromaciel/contract)
99
99
 
100
100
  MIT © [Ciro Cesar Maciel](https://ciromaciel.click)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciromaciel/contract",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A single response envelope, a closed catalogue of error codes, and helpers that make answering outside it impossible.",
@@ -32,9 +32,9 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "homepage": "https://myinfrastructure.click",
35
+ "homepage": "https://ciromaciel.click",
36
36
  "bugs": {
37
- "url": "https://myinfrastructure.click"
37
+ "url": "https://ciromaciel.click"
38
38
  },
39
39
  "author": "Ciro Cesar Maciel <maciel.ciro@icloud.com> (https://ciromaciel.click)"
40
40
  }
@@ -22,66 +22,45 @@
22
22
  * @param {object} cfg Where the secret and the Payments URL live.
23
23
  * @param {string} ownerId
24
24
  * @param {object} [copy] Product wording. `product` is the name a person reads
25
- * ("RiLiGar Auth"), `keeps` is what goes on working,
25
+ * ("Infrastructure Auth"), `keeps` is what goes on working,
26
26
  * `frozen` is what does not.
27
- * @returns {Promise<{state: 'active'|'none'|'unknown', plan: string|null,
27
+ * @param {number} [current] How many resources the owner has now. Without it,
28
+ * an owner with no plan gets no notice.
29
+ * @returns {Promise<{state: 'active'|'free'|'none'|'unknown', plan: string|null,
28
30
  * limit: number|null, message: string|null}>}
29
31
  */
30
- export async function getPlanNotice(gate, cfg, ownerId, copy = {}) {
32
+ export async function getPlanNotice(gate, cfg, ownerId, copy = {}, current = null) {
31
33
  const ent = await gate.getEntitlements(cfg, ownerId)
32
-
33
- // `Infinity` does not survive JSON.stringify — it becomes `null` silently.
34
- // Converting on purpose is what keeps the wire honest.
35
34
  const limit = ent.limit === Infinity ? null : ent.limit
36
-
37
- // Could not ask: say nothing. Dunning somebody because our OWN billing call
38
- // timed out is worse than staying quiet for the length of an outage.
39
35
  if (ent.unknown) return { state: 'unknown', plan: null, limit: null, message: null }
40
-
41
36
  if (ent.subscribed) return { state: 'active', plan: ent.plan, limit, message: null }
42
-
43
37
  /*
44
- * ONE MESSAGE, WRITTEN ONCE, for both surfaces — a banner and an MCP
45
- * warning that disagree is how a customer ends up arguing with support
46
- * about which is true.
47
- *
48
- * IT SAYS WHAT STILL WORKS BEFORE WHAT DOES NOT. The first question of
49
- * anyone who sees this is "did my users just go down?", and an answer that
50
- * opens with the refusal makes them assume the worst.
51
- *
52
- * Portuguese, because this is the one string here a person reads.
38
+ * NO PLAN IS NOT A PROBLEM TO REPORT. Since the free floor became one
39
+ * resource (2026-09-17), an owner with no plan is simply on the free tier,
40
+ * and telling them "your subscription is not active" reads as a billing
41
+ * failure they never had. The notice speaks only when the free tier is
42
+ * actually full — and it can only know that from `current`, so without it
43
+ * there is no notice: the refusal on creation already says what to do.
53
44
  */
45
+ if (typeof current !== 'number' || limit === null || current < limit) {
46
+ return { state: 'free', plan: ent.plan, limit, message: null }
47
+ }
54
48
  const product = copy.product || 'este produto'
55
49
  const keeps = copy.keeps || 'O que você já criou continua funcionando'
56
50
  const frozen = copy.frozen || 'apenas a criação de novos recursos está bloqueada'
57
-
58
51
  return {
59
52
  state: 'none',
60
53
  plan: ent.plan,
61
54
  limit,
62
- message: `Sua assinatura do ${product} não está ativa. ${keeps} — ${frozen} até a regularização do pagamento.`,
55
+ message: `O plano gratuito do ${product} chegou ao limite. ${keeps} — ${frozen}. Assine para criar mais.`,
63
56
  }
64
57
  }
65
58
 
66
- /**
67
- * Attaches the notice to an MCP tool result, without ever refusing it.
68
- *
69
- * IT TRAVELS AS A FIELD, NOT AS PROSE. The stack's MCP rule is that
70
- * `content[0].text` is the `JSON.stringify` of the same object in
71
- * `structuredContent`. Appending a second text block would break that mirror,
72
- * and a client reading one field would disagree with one reading the other.
73
- *
74
- * ERRORS ARE LEFT ALONE: an `isError` result already carries the reason the
75
- * agent needs, and billing copy on top of a permission failure buries it.
76
- *
77
- * AND IT NEVER BREAKS A TOOL CALL — the notice is decoration, so an unreachable
78
- * Payments or an outright throw both leave the result untouched.
79
- */
80
- export async function withPlanNotice(result, gate, cfg, ownerId, copy) {
59
+ export async function withPlanNotice(result, gate, cfg, ownerId, copy, current = null) {
81
60
  if (!result || result.isError || !result.structuredContent) return result
82
61
 
83
62
  try {
84
- const notice = await getPlanNotice(gate, cfg, ownerId, copy)
63
+ const notice = await getPlanNotice(gate, cfg, ownerId, copy, current)
85
64
  if (notice.state !== 'none') return result
86
65
 
87
66
  const payload = { ...result.structuredContent, planNotice: notice }