@prsm/entitle 2.1.0 → 2.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/entitle",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Plan-based entitlements and feature gating, resolved at runtime, backed by postgres",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,8 +11,8 @@ import ms from "@prsm/ms"
11
11
  * A per-subject override layered on top of the subject's plan. Shallow-merges
12
12
  * over the plan, so you only specify what differs (the enterprise customer who
13
13
  * negotiated more seats, or got one feature switched on).
14
- * @property {Record<string, boolean>} [features]
15
- * @property {Record<string, number|null>} [limits]
14
+ * @property {Record<string, boolean>} [features] - feature flags to override for this subject; merged over the plan's features, so include only the ones that differ
15
+ * @property {Record<string, number|null>} [limits] - limit ceilings to override for this subject; merged over the plan's limits, so include only the ones that differ (`null` means unlimited)
16
16
  */
17
17
 
18
18
  /**
@@ -29,9 +29,9 @@ import ms from "@prsm/ms"
29
29
 
30
30
  /**
31
31
  * @typedef {Object} Effective
32
- * @property {string} plan - the effective plan name
33
- * @property {Record<string, boolean>} features
34
- * @property {Record<string, number|null>} limits
32
+ * @property {string} plan - the effective plan name (the default plan if the subject is unassigned)
33
+ * @property {Record<string, boolean>} features - the resolved feature flags, after applying the subject's override on top of the plan
34
+ * @property {Record<string, number|null>} limits - the resolved limit ceilings, after applying the subject's override on top of the plan (`null` means unlimited)
35
35
  */
36
36
 
37
37
  /**
@@ -49,9 +49,13 @@ function assertBoolean(scope, key, v) {
49
49
  }
50
50
 
51
51
  function assertLimitValue(scope, key, v) {
52
- if (v !== null && (typeof v !== "number" || !Number.isFinite(v))) {
52
+ if (v === null) return
53
+ if (typeof v !== "number" || !Number.isFinite(v)) {
53
54
  throw new Error(`${scope} limit "${key}" must be a finite number or null (null means unlimited)`)
54
55
  }
56
+ if (v < 0) {
57
+ throw new Error(`${scope} limit "${key}" must not be negative (use 0 to deny, null for unlimited)`)
58
+ }
55
59
  }
56
60
 
57
61
  /**
@@ -184,6 +188,12 @@ export function createEntitlements(options = {}) {
184
188
  return effective
185
189
  }
186
190
 
191
+ async function resolveLimit(subject, key) {
192
+ requireLimit(key)
193
+ const eff = await resolve(subject)
194
+ return key in eff.limits ? eff.limits[key] : 0
195
+ }
196
+
187
197
  return {
188
198
  /** Create the backing tables if they do not exist. Idempotent. */
189
199
  setup() {
@@ -192,7 +202,7 @@ export function createEntitlements(options = {}) {
192
202
 
193
203
  /**
194
204
  * Assign a subject to a plan. Takes effect immediately.
195
- * @param {string} subject
205
+ * @param {string} subject - the subject identifier (whatever id you key entitlements by, such as an account or user id)
196
206
  * @param {string} plan - a key of the plan catalog
197
207
  */
198
208
  async assign(subject, plan) {
@@ -206,7 +216,7 @@ export function createEntitlements(options = {}) {
206
216
  * Remove a subject's plan assignment, reverting them to the default plan.
207
217
  * Distinct from assigning the default plan explicitly: an unassigned subject
208
218
  * follows `defaultPlan` if it later changes.
209
- * @param {string} subject
219
+ * @param {string} subject - the subject identifier whose assignment is removed
210
220
  */
211
221
  async unassign(subject) {
212
222
  if (!subject) throw new Error("unassign requires a `subject`")
@@ -220,8 +230,8 @@ export function createEntitlements(options = {}) {
220
230
  * accumulate: overriding `seats`, then later overriding `sso`, leaves both in
221
231
  * place, and overriding a key again updates just that key. Use clearOverride
222
232
  * to remove overrides. Takes effect immediately.
223
- * @param {string} subject
224
- * @param {Override} data
233
+ * @param {string} subject - the subject identifier to override
234
+ * @param {Override} data - the features and/or limits to override; merged into any existing override
225
235
  */
226
236
  async override(subject, data) {
227
237
  if (!subject) throw new Error("override requires a `subject`")
@@ -234,8 +244,8 @@ export function createEntitlements(options = {}) {
234
244
  * Remove overrides for a subject. With no `keys`, removes the entire override
235
245
  * and the subject falls back to plain plan entitlements. With `keys`, removes
236
246
  * only those override entries (reverting them to the plan) and keeps the rest.
237
- * @param {string} subject
238
- * @param {{ features?: string[], limits?: string[] }} [keys]
247
+ * @param {string} subject - the subject identifier whose override is cleared
248
+ * @param {{ features?: string[], limits?: string[] }} [keys] - the specific feature and limit keys to revert; omit to clear the entire override
239
249
  */
240
250
  async clearOverride(subject, keys) {
241
251
  if (!subject) throw new Error("clearOverride requires a `subject`")
@@ -249,7 +259,7 @@ export function createEntitlements(options = {}) {
249
259
 
250
260
  /**
251
261
  * The subject's effective plan name (the default plan if unassigned).
252
- * @param {string} subject
262
+ * @param {string} subject - the subject identifier to resolve
253
263
  * @returns {Promise<string>}
254
264
  */
255
265
  async plan(subject) {
@@ -260,8 +270,8 @@ export function createEntitlements(options = {}) {
260
270
  * Whether a capability flag is granted to the subject. Throws on a feature
261
271
  * key outside the declared/derived universe, so typos surface instead of
262
272
  * silently returning false.
263
- * @param {string} subject
264
- * @param {string} feature
273
+ * @param {string} subject - the subject identifier to resolve
274
+ * @param {string} feature - the feature key to check; must be in the declared/derived feature universe
265
275
  * @returns {Promise<boolean>}
266
276
  */
267
277
  async can(subject, feature) {
@@ -278,14 +288,12 @@ export function createEntitlements(options = {}) {
278
288
  * subject's plan does not grant - never silently unlimited. Throws on a key
279
289
  * outside the declared/derived universe. This is the static ceiling; it does
280
290
  * not read usage.
281
- * @param {string} subject
282
- * @param {string} key
291
+ * @param {string} subject - the subject identifier to resolve
292
+ * @param {string} key - the limit key to resolve; must be in the declared/derived limit universe
283
293
  * @returns {Promise<number|null>}
284
294
  */
285
- async limit(subject, key) {
286
- requireLimit(key)
287
- const eff = await resolve(subject)
288
- return key in eff.limits ? eff.limits[key] : 0
295
+ limit(subject, key) {
296
+ return resolveLimit(subject, key)
289
297
  },
290
298
 
291
299
  /**
@@ -293,7 +301,7 @@ export function createEntitlements(options = {}) {
293
301
  * reads current usage from the meter for the metric of the same name. This is
294
302
  * the composition seam: entitle supplies the limit, meter supplies the usage.
295
303
  * Requires a `meter` to have been passed to `createEntitlements`.
296
- * @param {string} subject
304
+ * @param {string} subject - the subject identifier; must match the id usage is recorded under in the meter
297
305
  * @param {string} key - a limit key that is also a meter metric
298
306
  * @param {{ period?: any, range?: any }} [usageQuery] - forwarded to `meter.usage`
299
307
  * @returns {Promise<CheckResult>}
@@ -303,7 +311,7 @@ export function createEntitlements(options = {}) {
303
311
  throw new Error("check requires a `meter`; pass it to createEntitlements, or use limit() for the static ceiling")
304
312
  }
305
313
  return traced(tracer, "entitle.check", { "entitle.subject": subject, "entitle.feature": key }, async () => {
306
- const limit = await this.limit(subject, key)
314
+ const limit = await resolveLimit(subject, key)
307
315
  const usage = await meter.usage({ ...usageQuery, subject, metric: key })
308
316
  const used = usage.quantity
309
317
  const allowed = limit === null || used < limit
@@ -320,7 +328,7 @@ export function createEntitlements(options = {}) {
320
328
 
321
329
  /**
322
330
  * The subject's full effective entitlements, for a settings or billing page.
323
- * @param {string} subject
331
+ * @param {string} subject - the subject identifier to resolve
324
332
  * @returns {Promise<Effective>}
325
333
  */
326
334
  async describe(subject) {
@@ -11,7 +11,7 @@ export function createEntitlements(options?: EntitlementsOptions): {
11
11
  setup(): any;
12
12
  /**
13
13
  * Assign a subject to a plan. Takes effect immediately.
14
- * @param {string} subject
14
+ * @param {string} subject - the subject identifier (whatever id you key entitlements by, such as an account or user id)
15
15
  * @param {string} plan - a key of the plan catalog
16
16
  */
17
17
  assign(subject: string, plan: string): Promise<void>;
@@ -19,7 +19,7 @@ export function createEntitlements(options?: EntitlementsOptions): {
19
19
  * Remove a subject's plan assignment, reverting them to the default plan.
20
20
  * Distinct from assigning the default plan explicitly: an unassigned subject
21
21
  * follows `defaultPlan` if it later changes.
22
- * @param {string} subject
22
+ * @param {string} subject - the subject identifier whose assignment is removed
23
23
  */
24
24
  unassign(subject: string): Promise<void>;
25
25
  /**
@@ -28,16 +28,16 @@ export function createEntitlements(options?: EntitlementsOptions): {
28
28
  * accumulate: overriding `seats`, then later overriding `sso`, leaves both in
29
29
  * place, and overriding a key again updates just that key. Use clearOverride
30
30
  * to remove overrides. Takes effect immediately.
31
- * @param {string} subject
32
- * @param {Override} data
31
+ * @param {string} subject - the subject identifier to override
32
+ * @param {Override} data - the features and/or limits to override; merged into any existing override
33
33
  */
34
34
  override(subject: string, data: Override): Promise<void>;
35
35
  /**
36
36
  * Remove overrides for a subject. With no `keys`, removes the entire override
37
37
  * and the subject falls back to plain plan entitlements. With `keys`, removes
38
38
  * only those override entries (reverting them to the plan) and keeps the rest.
39
- * @param {string} subject
40
- * @param {{ features?: string[], limits?: string[] }} [keys]
39
+ * @param {string} subject - the subject identifier whose override is cleared
40
+ * @param {{ features?: string[], limits?: string[] }} [keys] - the specific feature and limit keys to revert; omit to clear the entire override
41
41
  */
42
42
  clearOverride(subject: string, keys?: {
43
43
  features?: string[];
@@ -45,7 +45,7 @@ export function createEntitlements(options?: EntitlementsOptions): {
45
45
  }): Promise<void>;
46
46
  /**
47
47
  * The subject's effective plan name (the default plan if unassigned).
48
- * @param {string} subject
48
+ * @param {string} subject - the subject identifier to resolve
49
49
  * @returns {Promise<string>}
50
50
  */
51
51
  plan(subject: string): Promise<string>;
@@ -53,8 +53,8 @@ export function createEntitlements(options?: EntitlementsOptions): {
53
53
  * Whether a capability flag is granted to the subject. Throws on a feature
54
54
  * key outside the declared/derived universe, so typos surface instead of
55
55
  * silently returning false.
56
- * @param {string} subject
57
- * @param {string} feature
56
+ * @param {string} subject - the subject identifier to resolve
57
+ * @param {string} feature - the feature key to check; must be in the declared/derived feature universe
58
58
  * @returns {Promise<boolean>}
59
59
  */
60
60
  can(subject: string, feature: string): Promise<boolean>;
@@ -64,8 +64,8 @@ export function createEntitlements(options?: EntitlementsOptions): {
64
64
  * subject's plan does not grant - never silently unlimited. Throws on a key
65
65
  * outside the declared/derived universe. This is the static ceiling; it does
66
66
  * not read usage.
67
- * @param {string} subject
68
- * @param {string} key
67
+ * @param {string} subject - the subject identifier to resolve
68
+ * @param {string} key - the limit key to resolve; must be in the declared/derived limit universe
69
69
  * @returns {Promise<number|null>}
70
70
  */
71
71
  limit(subject: string, key: string): Promise<number | null>;
@@ -74,7 +74,7 @@ export function createEntitlements(options?: EntitlementsOptions): {
74
74
  * reads current usage from the meter for the metric of the same name. This is
75
75
  * the composition seam: entitle supplies the limit, meter supplies the usage.
76
76
  * Requires a `meter` to have been passed to `createEntitlements`.
77
- * @param {string} subject
77
+ * @param {string} subject - the subject identifier; must match the id usage is recorded under in the meter
78
78
  * @param {string} key - a limit key that is also a meter metric
79
79
  * @param {{ period?: any, range?: any }} [usageQuery] - forwarded to `meter.usage`
80
80
  * @returns {Promise<CheckResult>}
@@ -85,7 +85,7 @@ export function createEntitlements(options?: EntitlementsOptions): {
85
85
  }): Promise<CheckResult>;
86
86
  /**
87
87
  * The subject's full effective entitlements, for a settings or billing page.
88
- * @param {string} subject
88
+ * @param {string} subject - the subject identifier to resolve
89
89
  * @returns {Promise<Effective>}
90
90
  */
91
91
  describe(subject: string): Promise<Effective>;
@@ -108,7 +108,13 @@ export type Plan = {
108
108
  * negotiated more seats, or got one feature switched on).
109
109
  */
110
110
  export type Override = {
111
+ /**
112
+ * - feature flags to override for this subject; merged over the plan's features, so include only the ones that differ
113
+ */
111
114
  features?: Record<string, boolean>;
115
+ /**
116
+ * - limit ceilings to override for this subject; merged over the plan's limits, so include only the ones that differ (`null` means unlimited)
117
+ */
112
118
  limits?: Record<string, number | null>;
113
119
  };
114
120
  export type EntitlementsOptions = {
@@ -151,10 +157,16 @@ export type EntitlementsOptions = {
151
157
  };
152
158
  export type Effective = {
153
159
  /**
154
- * - the effective plan name
160
+ * - the effective plan name (the default plan if the subject is unassigned)
155
161
  */
156
162
  plan: string;
163
+ /**
164
+ * - the resolved feature flags, after applying the subject's override on top of the plan
165
+ */
157
166
  features: Record<string, boolean>;
167
+ /**
168
+ * - the resolved limit ceilings, after applying the subject's override on top of the plan (`null` means unlimited)
169
+ */
158
170
  limits: Record<string, number | null>;
159
171
  };
160
172
  export type CheckResult = {