@superblocksteam/shared 0.9608.3 → 0.9609.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 (68) hide show
  1. package/dist/types/application/index.d.ts +8 -0
  2. package/dist/types/application/index.d.ts.map +1 -1
  3. package/dist/types/application/index.js.map +1 -1
  4. package/dist/types/audit/ocsf.d.ts.map +1 -1
  5. package/dist/types/audit/ocsf.js +3 -1
  6. package/dist/types/audit/ocsf.js.map +1 -1
  7. package/dist/types/datasource/auth.d.ts +1 -0
  8. package/dist/types/datasource/auth.d.ts.map +1 -1
  9. package/dist/types/datasource/auth.js +26 -0
  10. package/dist/types/datasource/auth.js.map +1 -1
  11. package/dist/types/datasource/auth.test.js +57 -0
  12. package/dist/types/datasource/auth.test.js.map +1 -1
  13. package/dist/types/policyGate/index.d.ts +29 -0
  14. package/dist/types/policyGate/index.d.ts.map +1 -1
  15. package/dist/types/policyGate/index.js.map +1 -1
  16. package/dist/types/response/DeleteApplicationResponseBody.d.ts +2 -2
  17. package/dist/types/response/DeleteApplicationResponseBody.d.ts.map +1 -1
  18. package/dist/utils/index.d.ts +1 -0
  19. package/dist/utils/index.d.ts.map +1 -1
  20. package/dist/utils/index.js +1 -0
  21. package/dist/utils/index.js.map +1 -1
  22. package/dist/utils/launchDarkly.d.ts +18 -0
  23. package/dist/utils/launchDarkly.d.ts.map +1 -0
  24. package/dist/utils/launchDarkly.js +21 -0
  25. package/dist/utils/launchDarkly.js.map +1 -0
  26. package/dist/utils/launchDarkly.test.d.ts +2 -0
  27. package/dist/utils/launchDarkly.test.d.ts.map +1 -0
  28. package/dist/utils/launchDarkly.test.js +24 -0
  29. package/dist/utils/launchDarkly.test.js.map +1 -0
  30. package/dist-esm/types/application/index.d.ts +8 -0
  31. package/dist-esm/types/application/index.d.ts.map +1 -1
  32. package/dist-esm/types/application/index.js.map +1 -1
  33. package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
  34. package/dist-esm/types/audit/ocsf.js +3 -1
  35. package/dist-esm/types/audit/ocsf.js.map +1 -1
  36. package/dist-esm/types/datasource/auth.d.ts +1 -0
  37. package/dist-esm/types/datasource/auth.d.ts.map +1 -1
  38. package/dist-esm/types/datasource/auth.js +25 -0
  39. package/dist-esm/types/datasource/auth.js.map +1 -1
  40. package/dist-esm/types/datasource/auth.test.js +58 -1
  41. package/dist-esm/types/datasource/auth.test.js.map +1 -1
  42. package/dist-esm/types/policyGate/index.d.ts +29 -0
  43. package/dist-esm/types/policyGate/index.d.ts.map +1 -1
  44. package/dist-esm/types/policyGate/index.js.map +1 -1
  45. package/dist-esm/types/response/DeleteApplicationResponseBody.d.ts +2 -2
  46. package/dist-esm/types/response/DeleteApplicationResponseBody.d.ts.map +1 -1
  47. package/dist-esm/utils/index.d.ts +1 -0
  48. package/dist-esm/utils/index.d.ts.map +1 -1
  49. package/dist-esm/utils/index.js +1 -0
  50. package/dist-esm/utils/index.js.map +1 -1
  51. package/dist-esm/utils/launchDarkly.d.ts +18 -0
  52. package/dist-esm/utils/launchDarkly.d.ts.map +1 -0
  53. package/dist-esm/utils/launchDarkly.js +18 -0
  54. package/dist-esm/utils/launchDarkly.js.map +1 -0
  55. package/dist-esm/utils/launchDarkly.test.d.ts +2 -0
  56. package/dist-esm/utils/launchDarkly.test.d.ts.map +1 -0
  57. package/dist-esm/utils/launchDarkly.test.js +22 -0
  58. package/dist-esm/utils/launchDarkly.test.js.map +1 -0
  59. package/package.json +1 -1
  60. package/src/types/application/index.ts +11 -0
  61. package/src/types/audit/ocsf.ts +3 -1
  62. package/src/types/datasource/auth.test.ts +80 -0
  63. package/src/types/datasource/auth.ts +32 -0
  64. package/src/types/policyGate/index.ts +34 -0
  65. package/src/types/response/DeleteApplicationResponseBody.ts +2 -2
  66. package/src/utils/index.ts +1 -0
  67. package/src/utils/launchDarkly.test.ts +25 -0
  68. package/src/utils/launchDarkly.ts +23 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Analytics events drive LaunchDarkly's per-context billing: monthly active users are
3
+ * counted from the unique context keys LaunchDarkly receives events for. Throwaway
4
+ * contexts, such as the per-run organizations created by end-to-end tests, each bill as
5
+ * an additional monthly active user, so events should be off outside real deployments.
6
+ *
7
+ * Events are on by default and only an explicit 'false' turns them off, matching
8
+ * SUPERBLOCKS_SERVER_LAUNCH_DARKLY_SEND_EVENTS_ENABLED on the server-side SDK. Any
9
+ * unrecognized value keeps events on so a malformed setting cannot silently disable
10
+ * reporting for a real deployment.
11
+ */
12
+ export function shouldSendLaunchDarklyEvents(value) {
13
+ if (typeof value === 'boolean') {
14
+ return value;
15
+ }
16
+ return value?.trim().toLowerCase() !== 'false';
17
+ }
18
+ //# sourceMappingURL=launchDarkly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launchDarkly.js","sourceRoot":"","sources":["../../src/utils/launchDarkly.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAoC;IAC/E,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC;AACjD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=launchDarkly.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launchDarkly.test.d.ts","sourceRoot":"","sources":["../../src/utils/launchDarkly.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { shouldSendLaunchDarklyEvents } from './launchDarkly.js';
3
+ describe('shouldSendLaunchDarklyEvents', () => {
4
+ test('sends events by default when unset', () => {
5
+ expect(shouldSendLaunchDarklyEvents(undefined)).toBe(true);
6
+ expect(shouldSendLaunchDarklyEvents(null)).toBe(true);
7
+ expect(shouldSendLaunchDarklyEvents('')).toBe(true);
8
+ });
9
+ test('only an explicit false disables events', () => {
10
+ expect(shouldSendLaunchDarklyEvents('false')).toBe(false);
11
+ expect(shouldSendLaunchDarklyEvents('FALSE')).toBe(false);
12
+ expect(shouldSendLaunchDarklyEvents(' false ')).toBe(false);
13
+ expect(shouldSendLaunchDarklyEvents(false)).toBe(false);
14
+ });
15
+ test('sends events for truthy and unrecognized values', () => {
16
+ expect(shouldSendLaunchDarklyEvents('true')).toBe(true);
17
+ expect(shouldSendLaunchDarklyEvents(true)).toBe(true);
18
+ expect(shouldSendLaunchDarklyEvents('0')).toBe(true);
19
+ expect(shouldSendLaunchDarklyEvents('no')).toBe(true);
20
+ });
21
+ });
22
+ //# sourceMappingURL=launchDarkly.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launchDarkly.test.js","sourceRoot":"","sources":["../../src/utils/launchDarkly.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAEjE,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,4BAA4B,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,CAAC,4BAA4B,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1D,MAAM,CAAC,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1D,MAAM,CAAC,4BAA4B,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9D,MAAM,CAAC,4BAA4B,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,iDAAiD,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,4BAA4B,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,CAAC,4BAA4B,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superblocksteam/shared",
3
- "version": "0.9608.3",
3
+ "version": "0.9609.0",
4
4
  "description": "Superblocks Shared Resources",
5
5
  "license": "Superblocks Community Software License",
6
6
  "repository": "https://github.com/superblocksteam/shared.git",
@@ -472,6 +472,17 @@ export interface IApplicationV2Dto {
472
472
  removedProfiles?: Record<string, Array<{ id: string; key: string; displayName: string }>>;
473
473
  }
474
474
 
475
+ export type DatabaseDisposition = 'delete' | 'retain';
476
+
477
+ export type DatabaseRetirementEnqueueFailure = {
478
+ applicationId: string;
479
+ bindingKey: string;
480
+ };
481
+
482
+ export type DatabaseRetirementEnqueueResult = {
483
+ databaseRetirementEnqueueFailures: DatabaseRetirementEnqueueFailure[];
484
+ };
485
+
475
486
  export interface IHomepageApplicationV2Dto extends IResourceMetadata {
476
487
  deployedAt?: Date | null;
477
488
  creator?: {
@@ -289,7 +289,9 @@ export const AUDIT_EVENT_TYPE_CATALOG: AuditEventTypeEntry[] = [
289
289
  'reviewpolicy.allowlist.applications.added',
290
290
  'reviewpolicy.allowlist.applications.removed',
291
291
  'reviewpolicy.create',
292
- 'reviewpolicy.delete'
292
+ 'reviewpolicy.delete',
293
+ 'reviewpolicy.disable',
294
+ 'reviewpolicy.enable'
293
295
  ]
294
296
  },
295
297
  {
@@ -5,6 +5,7 @@ import {
5
5
  TokenScope,
6
6
  extractPublic,
7
7
  getAuthId,
8
+ getAuthIdCandidates,
8
9
  getDisplayName,
9
10
  getOAuthCodeVerifierStorageKey,
10
11
  isPkceEnabled
@@ -229,6 +230,85 @@ describe('getAuthId normalizes boolean tokenScope in getClientId', () => {
229
230
  });
230
231
  });
231
232
 
233
+ describe('getAuthIdCandidates', () => {
234
+ test('user-scoped config also probes the org-unique configId keying (recognizes a datasource->user flip)', () => {
235
+ const authConfig = { clientId: 'my-client', tokenScope: TokenScope.USER, scope: 'read' };
236
+ const candidates = getAuthIdCandidates(IntegrationAuthType.OAUTH2_CODE, authConfig, 'int-1', 'cfg-1');
237
+ // The config's own current keying (user -> client id) PLUS the widened datasource keying (config id).
238
+ expect(candidates).toContain(getAuthId(IntegrationAuthType.OAUTH2_CODE, authConfig, 'int-1', 'cfg-1'));
239
+ expect(candidates).toContain(
240
+ getAuthId(
241
+ IntegrationAuthType.OAUTH2_CODE,
242
+ { clientId: 'my-client', tokenScope: TokenScope.DATASOURCE, scope: 'read' },
243
+ 'int-1',
244
+ 'cfg-1'
245
+ )
246
+ );
247
+ expect(candidates).toHaveLength(2);
248
+ expect(candidates.some((c) => c.startsWith('oauth-code.cfg-1'))).toBe(true); // org-unique config id
249
+ expect(candidates.some((c) => c.startsWith('oauth-code.my-client'))).toBe(true); // its own current client-id key
250
+ });
251
+
252
+ test('datasource-scoped config widens ONLY to the configId keying, never synthesizes the client-id keying (SEC-73)', () => {
253
+ // The client-id keying is shared across tenants (many orgs can share a clientId + scope); synthesizing
254
+ // it for a datasource-scoped config would let one org witness ownership of another org's client-id-keyed
255
+ // shared token. It must never appear as a candidate for a datasource-scoped config.
256
+ const authConfig = { clientId: 'shared-client', tokenScope: TokenScope.DATASOURCE, scope: 'read' };
257
+ const candidates = getAuthIdCandidates(IntegrationAuthType.OAUTH2_CODE, authConfig, 'int-1', 'cfg-1');
258
+ expect(candidates).toEqual([getAuthId(IntegrationAuthType.OAUTH2_CODE, authConfig, 'int-1', 'cfg-1')]);
259
+ expect(candidates).toHaveLength(1);
260
+ expect(candidates[0]).toMatch(/^oauth-code\.cfg-1/); // org-unique config id
261
+ expect(candidates.some((c) => c.startsWith('oauth-code.shared-client'))).toBe(false); // no cross-tenant client-id key
262
+ });
263
+
264
+ test('collapses to one candidate when config id equals client id', () => {
265
+ const authConfig = { clientId: 'same', tokenScope: TokenScope.USER, scope: 'read' };
266
+ const candidates = getAuthIdCandidates(IntegrationAuthType.OAUTH2_CODE, authConfig, 'int-1', 'same');
267
+ expect(candidates).toHaveLength(1);
268
+ });
269
+
270
+ test.each([IntegrationAuthType.OAUTH2_CLIENT_CREDS, IntegrationAuthType.OAUTH2_IMPLICIT])(
271
+ 'OAuth-split type %s (user-scoped) also probes the configId keying',
272
+ (authType) => {
273
+ const authConfig = { clientId: 'my-client', tokenScope: TokenScope.USER, scope: 'read' };
274
+ const candidates = getAuthIdCandidates(authType, authConfig, 'int-1', 'cfg-1');
275
+ expect(candidates).toHaveLength(2);
276
+ expect(candidates).toContain(
277
+ getAuthId(authType, { clientId: 'my-client', tokenScope: TokenScope.DATASOURCE, scope: 'read' }, 'int-1', 'cfg-1')
278
+ );
279
+ }
280
+ );
281
+
282
+ test.each([IntegrationAuthType.BASIC, IntegrationAuthType.FIREBASE, IntegrationAuthType.OAUTH2_PASSWORD])(
283
+ 'scope-invariant type %s returns a single candidate (tokenScope does not affect keying)',
284
+ (authType) => {
285
+ const authConfig = { clientId: 'my-client', apiKey: '{"projectId":"p"}', scope: 'read' };
286
+ const candidates = getAuthIdCandidates(authType, authConfig, 'int-1', 'cfg-1');
287
+ expect(candidates).toHaveLength(1);
288
+ expect(candidates[0]).toBe(getAuthId(authType, authConfig, 'int-1', 'cfg-1'));
289
+ }
290
+ );
291
+
292
+ test('scope-invariant auth type returns a single candidate (tokenScope does not affect keying)', () => {
293
+ const authConfig = {
294
+ subjectTokenSource: 'SUBJECT_TOKEN_SOURCE_STATIC_TOKEN',
295
+ subjectTokenSourceStaticToken: 't',
296
+ tokenUrl: 'https://idp.example.com/token',
297
+ scope: 'read',
298
+ audience: 'aud',
299
+ clientId: 'c'
300
+ };
301
+ const candidates = getAuthIdCandidates(IntegrationAuthType.OAUTH2_TOKEN_EXCHANGE, authConfig, 'int-1', 'cfg-1');
302
+ expect(candidates).toHaveLength(1);
303
+ expect(candidates[0]).toBe(getAuthId(IntegrationAuthType.OAUTH2_TOKEN_EXCHANGE, authConfig, 'int-1', 'cfg-1'));
304
+ });
305
+
306
+ test('undefined authConfig yields a single candidate', () => {
307
+ const candidates = getAuthIdCandidates(IntegrationAuthType.OAUTH2_CODE, undefined, 'int-1', 'cfg-1');
308
+ expect(candidates).toHaveLength(1);
309
+ });
310
+ });
311
+
232
312
  describe('getAuthId', () => {
233
313
  test('uses unknown-client for OAUTH2_IDP_TOKEN_PASSTHROUGH (no client identity)', () => {
234
314
  const authId = getAuthId(IntegrationAuthType.OAUTH2_IDP_TOKEN_PASSTHROUGH, {}, 'int-1', 'cfg-1');
@@ -60,6 +60,38 @@ export function getAuthId(
60
60
  return `${authType}.${getClientId(authType, authConfig, integrationId, integrationConfigurationId)}`;
61
61
  }
62
62
 
63
+ // A token minted while a config's tokenScope was 'datasource' keys on the org-unique configuration id;
64
+ // after the config is flipped to 'user' a fresh recompute keys on the client id instead and no longer
65
+ // matches, false-denying the legitimate owner. getAuthIdCandidates returns the config's current-scope
66
+ // authId PLUS the datasource (configuration-id) keying, so an ownership recompute recognizes a token
67
+ // across a post-mint datasource->user flip.
68
+ //
69
+ // It widens ONLY toward the datasource keying, which is org-unique (the configuration id is a per-config
70
+ // UUID). It deliberately does NOT synthesize the user (client-id) keying for a datasource-scoped config:
71
+ // the client-id key is shared across tenants (multiple orgs can share a clientId + scope), so adding it
72
+ // would let one org's config witness ownership of another org's client-id-keyed shared token. Recognizing
73
+ // the reverse user->datasource flip would require matching that shared key, so it is intentionally not
74
+ // done here; closing it safely needs org-scoped token storage, tracked separately. See SEC-73.
75
+ export function getAuthIdCandidates(
76
+ authType: AuthType | undefined,
77
+ authConfig: AuthConfig | undefined,
78
+ integrationId?: string,
79
+ integrationConfigurationId?: string
80
+ ): AuthId[] {
81
+ const current = getAuthId(authType, authConfig, integrationId, integrationConfigurationId);
82
+ const asDatasource = getAuthId(authType, withTokenScope(authConfig, TokenScope.DATASOURCE), integrationId, integrationConfigurationId);
83
+ return current === asDatasource ? [current] : [current, asDatasource];
84
+ }
85
+
86
+ // Return a shallow copy of authConfig with tokenScope overridden, without mutating the caller's object.
87
+ // undefined passes through so getAuthId falls back to its unknown-client key.
88
+ function withTokenScope(authConfig: AuthConfig | undefined, tokenScope: TokenScope): AuthConfig | undefined {
89
+ if (!authConfig) {
90
+ return authConfig;
91
+ }
92
+ return { ...authConfig, tokenScope } as AuthConfig;
93
+ }
94
+
63
95
  function getClientId(
64
96
  authType: AuthType | undefined,
65
97
  config: AuthConfig | undefined,
@@ -1,3 +1,5 @@
1
+ import type { ReviewFindingCategory, ReviewFindingConfidence, ReviewFindingSeverity } from '../reviewPolicy/index.js';
2
+
1
3
  /**
2
4
  * @deprecated Use the `superblocks.policy-gates.security-scans.enabled` LaunchDarkly flag instead.
3
5
  * Kept as `false` to match the fail-closed LD default; create and enable the
@@ -141,3 +143,35 @@ export const POLICY_GATE_BLOCKED_ERROR = 'POLICY_GATE_BLOCKED';
141
143
  */
142
144
  export const POLICY_GATE_STALE_LIVE_EDIT_ERROR =
143
145
  'Policy gates can only run for the current app state. Deploy the current changes or restore to this commit and then deploy.';
146
+
147
+ export type ScanScopeChangeType = 'added' | 'deleted' | 'modified';
148
+
149
+ export type ScanScopeChangedFile = {
150
+ changeType: ScanScopeChangeType;
151
+ path: string;
152
+ };
153
+
154
+ export type ScanScopePriorFinding = {
155
+ blocking: boolean;
156
+ category: ReviewFindingCategory;
157
+ clarkRemediable?: boolean;
158
+ confidence?: ReviewFindingConfidence | null;
159
+ evidenceJson?: Record<string, unknown>;
160
+ fingerprint: string;
161
+ humanSummary?: string | null;
162
+ locations: unknown[];
163
+ remediationHintJson?: Record<string, unknown>;
164
+ severity: ReviewFindingSeverity;
165
+ technicalSummary?: string | null;
166
+ title: string;
167
+ };
168
+
169
+ export type ScanScopeJson = {
170
+ baseApplicationCommitId: string;
171
+ changedFiles: ScanScopeChangedFile[];
172
+ diffTruncated: boolean;
173
+ priorAiAgentRunId?: string;
174
+ priorFindings: ScanScopePriorFinding[];
175
+ priorReviewRunId?: string;
176
+ unifiedDiff: string;
177
+ };
@@ -1,3 +1,3 @@
1
- import { IApplicationV2Dto } from '../application/index.js';
1
+ import type { DatabaseRetirementEnqueueResult, IApplicationV2Dto } from '../application/index.js';
2
2
 
3
- export type DeleteApplicationResponseBody = IApplicationV2Dto;
3
+ export type DeleteApplicationResponseBody = IApplicationV2Dto & Partial<DatabaseRetirementEnqueueResult>;
@@ -6,6 +6,7 @@ export * from './configuration.js';
6
6
  export * from './env.js';
7
7
  export * from './environment.js';
8
8
  export * from './git-url.js';
9
+ export * from './launchDarkly.js';
9
10
  export * from './maskCredential.js';
10
11
  export * from './normalize.js';
11
12
  export * from './normalizeRoleName.js';
@@ -0,0 +1,25 @@
1
+ import { describe, test, expect } from 'vitest';
2
+
3
+ import { shouldSendLaunchDarklyEvents } from './launchDarkly.js';
4
+
5
+ describe('shouldSendLaunchDarklyEvents', () => {
6
+ test('sends events by default when unset', () => {
7
+ expect(shouldSendLaunchDarklyEvents(undefined)).toBe(true);
8
+ expect(shouldSendLaunchDarklyEvents(null)).toBe(true);
9
+ expect(shouldSendLaunchDarklyEvents('')).toBe(true);
10
+ });
11
+
12
+ test('only an explicit false disables events', () => {
13
+ expect(shouldSendLaunchDarklyEvents('false')).toBe(false);
14
+ expect(shouldSendLaunchDarklyEvents('FALSE')).toBe(false);
15
+ expect(shouldSendLaunchDarklyEvents(' false ')).toBe(false);
16
+ expect(shouldSendLaunchDarklyEvents(false)).toBe(false);
17
+ });
18
+
19
+ test('sends events for truthy and unrecognized values', () => {
20
+ expect(shouldSendLaunchDarklyEvents('true')).toBe(true);
21
+ expect(shouldSendLaunchDarklyEvents(true)).toBe(true);
22
+ expect(shouldSendLaunchDarklyEvents('0')).toBe(true);
23
+ expect(shouldSendLaunchDarklyEvents('no')).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A LaunchDarkly send-events setting as it arrives from configuration: either an
3
+ * already-parsed boolean or the raw string form an env var carries.
4
+ */
5
+ export type LaunchDarklySendEventsSetting = boolean | string | undefined | null;
6
+
7
+ /**
8
+ * Analytics events drive LaunchDarkly's per-context billing: monthly active users are
9
+ * counted from the unique context keys LaunchDarkly receives events for. Throwaway
10
+ * contexts, such as the per-run organizations created by end-to-end tests, each bill as
11
+ * an additional monthly active user, so events should be off outside real deployments.
12
+ *
13
+ * Events are on by default and only an explicit 'false' turns them off, matching
14
+ * SUPERBLOCKS_SERVER_LAUNCH_DARKLY_SEND_EVENTS_ENABLED on the server-side SDK. Any
15
+ * unrecognized value keeps events on so a malformed setting cannot silently disable
16
+ * reporting for a real deployment.
17
+ */
18
+ export function shouldSendLaunchDarklyEvents(value: LaunchDarklySendEventsSetting): boolean {
19
+ if (typeof value === 'boolean') {
20
+ return value;
21
+ }
22
+ return value?.trim().toLowerCase() !== 'false';
23
+ }