@copilotkit/shared 1.69.3 → 1.70.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.
Files changed (58) hide show
  1. package/dist/index.cjs +31 -13
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +19 -17
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +19 -17
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +29 -15
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.umd.js +159 -31
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/package.cjs +1 -1
  12. package/dist/package.mjs +1 -1
  13. package/dist/telemetry/index.d.mts +3 -2
  14. package/dist/telemetry/lambda-client.cjs +25 -4
  15. package/dist/telemetry/lambda-client.cjs.map +1 -1
  16. package/dist/telemetry/lambda-client.d.cts +14 -1
  17. package/dist/telemetry/lambda-client.d.cts.map +1 -1
  18. package/dist/telemetry/lambda-client.d.mts +14 -1
  19. package/dist/telemetry/lambda-client.d.mts.map +1 -1
  20. package/dist/telemetry/lambda-client.mjs +25 -5
  21. package/dist/telemetry/lambda-client.mjs.map +1 -1
  22. package/dist/telemetry/sampling.cjs +28 -0
  23. package/dist/telemetry/sampling.cjs.map +1 -0
  24. package/dist/telemetry/sampling.d.cts +37 -0
  25. package/dist/telemetry/sampling.d.cts.map +1 -0
  26. package/dist/telemetry/sampling.d.mts +37 -0
  27. package/dist/telemetry/sampling.d.mts.map +1 -0
  28. package/dist/telemetry/sampling.mjs +25 -0
  29. package/dist/telemetry/sampling.mjs.map +1 -0
  30. package/dist/telemetry/telemetry-client.cjs +72 -11
  31. package/dist/telemetry/telemetry-client.cjs.map +1 -1
  32. package/dist/telemetry/telemetry-client.d.cts +43 -1
  33. package/dist/telemetry/telemetry-client.d.cts.map +1 -1
  34. package/dist/telemetry/telemetry-client.d.mts +43 -1
  35. package/dist/telemetry/telemetry-client.d.mts.map +1 -1
  36. package/dist/telemetry/telemetry-client.mjs +73 -12
  37. package/dist/telemetry/telemetry-client.mjs.map +1 -1
  38. package/dist/utils/index.d.cts +1 -1
  39. package/dist/utils/index.d.mts +1 -1
  40. package/dist/utils/types.cjs.map +1 -1
  41. package/dist/utils/types.d.cts +46 -1
  42. package/dist/utils/types.d.cts.map +1 -1
  43. package/dist/utils/types.d.mts +46 -1
  44. package/dist/utils/types.d.mts.map +1 -1
  45. package/dist/utils/types.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/__tests__/license-context.test.ts +224 -25
  48. package/src/index.ts +72 -16
  49. package/src/telemetry/index.ts +2 -0
  50. package/src/telemetry/lambda-client.test.ts +336 -1
  51. package/src/telemetry/lambda-client.ts +56 -15
  52. package/src/telemetry/sampling.test.ts +65 -0
  53. package/src/telemetry/sampling.ts +70 -0
  54. package/src/telemetry/telemetry-blank-license-identity.test.ts +121 -0
  55. package/src/telemetry/telemetry-client.test.ts +438 -15
  56. package/src/telemetry/telemetry-client.ts +145 -30
  57. package/src/utils/__tests__/conditions.test.ts +161 -0
  58. package/src/utils/types.ts +52 -0
@@ -1,33 +1,232 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { expect, test } from "vitest";
2
2
  import { createLicenseContextValue } from "../index";
3
3
  import type { RuntimeLicenseStatus } from "../utils/types";
4
4
 
5
- describe("createLicenseContextValue", () => {
6
- it("fails open when no status is known", () => {
7
- for (const status of [null, undefined] as const) {
8
- const ctx = createLicenseContextValue(status);
9
- expect(ctx.status).toBeNull();
10
- expect(ctx.license).toBeNull();
11
- expect(ctx.checkFeature("chat")).toBe(true);
12
- expect(ctx.getLimit("chat")).toBeNull();
13
- }
5
+ test("license context fails open when no status is known", () => {
6
+ for (const status of [null, undefined] as const) {
7
+ const ctx = createLicenseContextValue(status);
8
+ expect(ctx.status).toBeNull();
9
+ expect(ctx.license).toBeNull();
10
+ expect(ctx.checkFeature("chat")).toBe(true);
11
+ expect(ctx.getLimit("chat")).toBeNull();
12
+ }
13
+ });
14
+
15
+ test.each(["valid", "none", "expiring", "unknown"] as RuntimeLicenseStatus[])(
16
+ "license context enables features for %s status",
17
+ (status) => {
18
+ const ctx = createLicenseContextValue(status);
19
+ expect(ctx.status).toBe(status);
20
+ expect(ctx.checkFeature("chat")).toBe(true);
21
+ },
22
+ );
23
+
24
+ test.each(["expired", "invalid"] as RuntimeLicenseStatus[])(
25
+ "license context disables features for %s status",
26
+ (status) => {
27
+ const ctx = createLicenseContextValue(status);
28
+ expect(ctx.status).toBe(status);
29
+ expect(ctx.checkFeature("chat")).toBe(false);
30
+ },
31
+ );
32
+
33
+ test("license context uses ready active feature grants and numeric limits", () => {
34
+ const ctx = createLicenseContextValue("valid", {
35
+ status: "ready",
36
+ entitlement: {
37
+ active: true,
38
+ source: "managedOrgSubscription",
39
+ features: {
40
+ chat: true,
41
+ threads: false,
42
+ },
43
+ limits: {
44
+ threads: 25,
45
+ },
46
+ },
14
47
  });
15
48
 
16
- it.each(["valid", "none", "expiring", "unknown"] as RuntimeLicenseStatus[])(
17
- "enables features for %s status",
18
- (status) => {
19
- const ctx = createLicenseContextValue(status);
20
- expect(ctx.status).toBe(status);
21
- expect(ctx.checkFeature("chat")).toBe(true);
49
+ expect(ctx.checkFeature("chat")).toBe(true);
50
+ expect(ctx.checkFeature("threads")).toBe(false);
51
+ expect(ctx.checkFeature("unknown")).toBe(false);
52
+ expect(ctx.getLimit("threads")).toBe(25);
53
+ expect(ctx.getLimit("unknown")).toBeNull();
54
+ });
55
+
56
+ test.each([0, 25])(
57
+ "license context treats an own threads.max_count limit of %s as a thread grant",
58
+ (threadLimit) => {
59
+ const ctx = createLicenseContextValue("valid", {
60
+ status: "ready",
61
+ entitlement: {
62
+ active: true,
63
+ source: "managedOrgSubscription",
64
+ features: {},
65
+ limits: { "threads.max_count": threadLimit },
66
+ },
67
+ });
68
+
69
+ expect(ctx.checkFeature("threads")).toBe(true);
70
+ expect(ctx.getLimit("threads.max_count")).toBe(threadLimit);
71
+ },
72
+ );
73
+
74
+ test.each([
75
+ {
76
+ source: "managedOrgSubscription" as const,
77
+ features: {},
78
+ limits: {
79
+ "threads.retention_hours": 72,
80
+ "threads.max_count": 200,
81
+ },
82
+ },
83
+ {
84
+ source: "selfHostedDeploymentLicense" as const,
85
+ features: {
86
+ deployment_via_helm_chart: true,
87
+ msteams: true,
88
+ },
89
+ limits: {
90
+ "threads.retention_hours": 336,
91
+ "threads.max_count": 25000,
92
+ },
93
+ },
94
+ ])(
95
+ "license context preserves legacy UI features for an active $source payload",
96
+ (entitlement) => {
97
+ const ctx = createLicenseContextValue("valid", {
98
+ status: "ready",
99
+ entitlement: {
100
+ active: true,
101
+ ...entitlement,
102
+ },
103
+ });
104
+
105
+ for (const feature of ["chat", "popup", "sidebar", "threads"]) {
106
+ expect(ctx.checkFeature(feature)).toBe(true);
107
+ }
108
+ expect(ctx.checkFeature("unknown")).toBe(false);
109
+ },
110
+ );
111
+
112
+ test.each(["chat", "popup", "sidebar"])(
113
+ "license context preserves an explicit %s denial",
114
+ (feature) => {
115
+ const ctx = createLicenseContextValue("valid", {
116
+ status: "ready",
117
+ entitlement: {
118
+ active: true,
119
+ source: "managedOrgSubscription",
120
+ features: { [feature]: false },
121
+ limits: {},
122
+ },
123
+ });
124
+
125
+ expect(ctx.checkFeature(feature)).toBe(false);
126
+ },
127
+ );
128
+
129
+ test.each([true, false])(
130
+ "license context preserves a legacy threads feature value of %s",
131
+ (threads) => {
132
+ const ctx = createLicenseContextValue("valid", {
133
+ status: "ready",
134
+ entitlement: {
135
+ active: true,
136
+ source: "selfHostedDeploymentLicense",
137
+ features: { threads },
138
+ limits: {},
139
+ },
140
+ });
141
+
142
+ expect(ctx.checkFeature("threads")).toBe(threads);
143
+ },
144
+ );
145
+
146
+ test("license context denies features and limits for a ready inactive entitlement", () => {
147
+ const ctx = createLicenseContextValue("none", {
148
+ status: "ready",
149
+ entitlement: {
150
+ active: false,
151
+ source: "managedOrgSubscription",
152
+ features: {
153
+ chat: true,
154
+ },
155
+ limits: {
156
+ threads: 25,
157
+ },
22
158
  },
23
- );
24
-
25
- it.each(["expired", "invalid"] as RuntimeLicenseStatus[])(
26
- "disables features for %s status",
27
- (status) => {
28
- const ctx = createLicenseContextValue(status);
29
- expect(ctx.status).toBe(status);
30
- expect(ctx.checkFeature("chat")).toBe(false);
159
+ });
160
+
161
+ expect(ctx.checkFeature("chat")).toBe(false);
162
+ expect(ctx.getLimit("threads")).toBeNull();
163
+ });
164
+
165
+ test.each(["valid", "expiring"] as RuntimeLicenseStatus[])(
166
+ "license context preserves a %s legacy fallback for an inactive self-hosted entitlement",
167
+ (status) => {
168
+ const ctx = createLicenseContextValue(status, {
169
+ status: "ready",
170
+ entitlement: {
171
+ active: false,
172
+ source: "selfHostedDeploymentLicense",
173
+ features: {
174
+ threads: false,
175
+ },
176
+ limits: {
177
+ threads: 0,
178
+ },
179
+ },
180
+ });
181
+
182
+ expect(ctx.status).toBe(status);
183
+ expect(ctx.checkFeature("threads")).toBe(true);
184
+ expect(ctx.getLimit("threads")).toBeNull();
185
+ },
186
+ );
187
+
188
+ test.each([
189
+ { status: undefined, expectedStatus: "none" },
190
+ { status: "none", expectedStatus: "none" },
191
+ { status: "expired", expectedStatus: "expired" },
192
+ { status: "invalid", expectedStatus: "invalid" },
193
+ { status: "unknown", expectedStatus: "unknown" },
194
+ ] as const)(
195
+ "license context denies an inactive self-hosted entitlement with $status legacy status",
196
+ ({ status, expectedStatus }) => {
197
+ const ctx = createLicenseContextValue(status, {
198
+ status: "ready",
199
+ entitlement: {
200
+ active: false,
201
+ source: "selfHostedDeploymentLicense",
202
+ features: {
203
+ threads: true,
204
+ },
205
+ limits: {
206
+ threads: 25,
207
+ },
208
+ },
209
+ });
210
+
211
+ expect(ctx.status).toBe(expectedStatus);
212
+ expect(ctx.checkFeature("threads")).toBe(false);
213
+ expect(ctx.getLimit("threads")).toBeNull();
214
+ },
215
+ );
216
+
217
+ test("license context ignores inherited feature and limit keys", () => {
218
+ const ctx = createLicenseContextValue("valid", {
219
+ status: "ready",
220
+ entitlement: {
221
+ active: true,
222
+ source: "managedOrgSubscription",
223
+ features: {},
224
+ limits: {},
31
225
  },
32
- );
226
+ });
227
+
228
+ for (const feature of ["toString", "constructor"]) {
229
+ expect(ctx.checkFeature(feature)).toBe(false);
230
+ expect(ctx.getLimit(feature)).toBeNull();
231
+ }
33
232
  });
package/src/index.ts CHANGED
@@ -33,7 +33,10 @@ export type {
33
33
  } from "@copilotkit/license-verifier";
34
34
 
35
35
  import type { LicensePayload } from "@copilotkit/license-verifier";
36
- import type { RuntimeLicenseStatus } from "./utils/types";
36
+ import type {
37
+ RuntimeEntitlementResponse,
38
+ RuntimeLicenseStatus,
39
+ } from "./utils/types";
37
40
 
38
41
  // LicenseContextValue was dropped from license-verifier's public API in
39
42
  // 0.3.0, so it is defined here. The context shape is owned by this package
@@ -44,40 +47,93 @@ import type { RuntimeLicenseStatus } from "./utils/types";
44
47
  * Frontend providers create their own context using this shape.
45
48
  */
46
49
  export interface LicenseContextValue {
47
- /** Server-reported license status from the runtime's /info endpoint. Null until known. */
50
+ /** Effective license status after structured entitlement precedence. Null until known. */
48
51
  status: RuntimeLicenseStatus | null;
49
52
  /** The license payload if available. Always null on the client; the payload stays server-side. */
50
53
  license: LicensePayload | null;
51
- /** Whether a specific feature is licensed. Returns true if no licensing is active (no token). */
54
+ /** Whether a feature is licensed. Ready entitlements override legacy status behavior. */
52
55
  checkFeature: (feature: string) => boolean;
53
- /** Get a numeric feature limit. Returns null if not applicable. */
56
+ /** Get a numeric feature limit. Zero means unlimited; null means not applicable. */
54
57
  getLimit: (feature: string) => number | null;
55
58
  }
56
59
 
60
+ /** Read a record value without traversing its prototype chain. */
61
+ function getOwnRecordValue<Value>(
62
+ record: Readonly<Record<string, Value>>,
63
+ key: string,
64
+ ): Value | undefined {
65
+ return Object.prototype.hasOwnProperty.call(record, key)
66
+ ? record[key]
67
+ : undefined;
68
+ }
69
+
70
+ /** Legacy UI surfaces that remain available for every active entitlement. */
71
+ function isLegacyUiFeature(feature: string): boolean {
72
+ return feature === "chat" || feature === "popup" || feature === "sidebar";
73
+ }
74
+
57
75
  /**
58
- * Client-safe license context factory, driven by the license status the
76
+ * Client-safe license context factory, driven by the license authority the
59
77
  * runtime reports via /info.
60
78
  *
61
- * Features are enabled unless the runtime definitively reports the license
62
- * as "expired" or "invalid". A null/"none"/"unknown" status fails open
63
- * (unlicensed = unrestricted, with branding), and "expiring" keeps features
64
- * on while the provider surfaces a warning banner. Per-feature data is not
65
- * in /info yet, so checkFeature is uniform across features and getLimit has
66
- * no limits to report. This is inlined here to avoid importing the full
67
- * license-verifier bundle (which depends on Node's `crypto`) into browser
68
- * bundles.
79
+ * A ready managed entitlement is authoritative in both directions. A ready
80
+ * active self-hosted entitlement is also authoritative, while an inactive
81
+ * self-hosted response preserves the legacy signed-license fallback. Active
82
+ * entitlements supply feature grants and limits; authoritative inactive
83
+ * entitlements deny every feature and limit. Older runtimes that report only a
84
+ * status retain the legacy behavior: features are enabled unless the status is
85
+ * "expired" or "invalid", and no limits are reported. This is inlined here to
86
+ * avoid importing the full license-verifier bundle (which depends on Node's
87
+ * `crypto`) into browser bundles.
69
88
  */
70
89
  export function createLicenseContextValue(
71
90
  status: RuntimeLicenseStatus | null | undefined,
91
+ runtimeEntitlements?: RuntimeEntitlementResponse,
72
92
  ): LicenseContextValue {
73
- const resolvedStatus = status ?? null;
93
+ const readyEntitlement =
94
+ runtimeEntitlements?.status === "ready"
95
+ ? runtimeEntitlements.entitlement
96
+ : null;
97
+ const hasUsableSelfHostedLegacyFallback =
98
+ readyEntitlement &&
99
+ !readyEntitlement.active &&
100
+ readyEntitlement.source === "selfHostedDeploymentLicense" &&
101
+ (status === "valid" || status === "expiring");
102
+ const featureAuthority =
103
+ readyEntitlement && !hasUsableSelfHostedLegacyFallback
104
+ ? readyEntitlement
105
+ : null;
106
+ const activeEntitlement = featureAuthority?.active ? featureAuthority : null;
107
+ const resolvedStatus = activeEntitlement
108
+ ? "valid"
109
+ : readyEntitlement?.source === "managedOrgSubscription"
110
+ ? "none"
111
+ : featureAuthority
112
+ ? (status ?? "none")
113
+ : (status ?? null);
74
114
  const featuresEnabled =
75
115
  resolvedStatus !== "expired" && resolvedStatus !== "invalid";
116
+
76
117
  return {
77
118
  status: resolvedStatus,
78
119
  license: null,
79
- checkFeature: () => featuresEnabled,
80
- getLimit: () => null,
120
+ checkFeature: (feature) =>
121
+ featureAuthority
122
+ ? activeEntitlement
123
+ ? feature === "threads" &&
124
+ Object.prototype.hasOwnProperty.call(
125
+ activeEntitlement.limits,
126
+ "threads.max_count",
127
+ )
128
+ ? true
129
+ : (getOwnRecordValue(activeEntitlement.features, feature) ??
130
+ isLegacyUiFeature(feature))
131
+ : false
132
+ : featuresEnabled,
133
+ getLimit: (feature) =>
134
+ activeEntitlement
135
+ ? (getOwnRecordValue(activeEntitlement.limits, feature) ?? null)
136
+ : null,
81
137
  };
82
138
  }
83
139
 
@@ -1,5 +1,7 @@
1
1
  export * from "./telemetry-client";
2
+ export * from "./sampling";
2
3
  export {
4
+ firstNonBlankTelemetryId,
3
5
  lambdaClient,
4
6
  parseTelemetryIdFromLicense,
5
7
  parseAndWarnTelemetryId,