@superblocksteam/shared 0.9604.2 → 0.9605.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 (185) hide show
  1. package/dist/constants.d.ts +3 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +11 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/database-lifecycle/capacityPolicy.d.ts +8 -1
  6. package/dist/database-lifecycle/capacityPolicy.d.ts.map +1 -1
  7. package/dist/database-lifecycle/capacityPolicy.js +8 -1
  8. package/dist/database-lifecycle/capacityPolicy.js.map +1 -1
  9. package/dist/database-lifecycle/destructiveMigrations.d.ts +26 -0
  10. package/dist/database-lifecycle/destructiveMigrations.d.ts.map +1 -0
  11. package/dist/database-lifecycle/destructiveMigrations.js +89 -0
  12. package/dist/database-lifecycle/destructiveMigrations.js.map +1 -0
  13. package/dist/database-lifecycle/destructiveMigrations.test.d.ts +2 -0
  14. package/dist/database-lifecycle/destructiveMigrations.test.d.ts.map +1 -0
  15. package/dist/database-lifecycle/destructiveMigrations.test.js +100 -0
  16. package/dist/database-lifecycle/destructiveMigrations.test.js.map +1 -0
  17. package/dist/database-lifecycle/index.d.ts +6 -1
  18. package/dist/database-lifecycle/index.d.ts.map +1 -1
  19. package/dist/database-lifecycle/index.js +20 -1
  20. package/dist/database-lifecycle/index.js.map +1 -1
  21. package/dist/socket/protocol.d.ts +1 -0
  22. package/dist/socket/protocol.d.ts.map +1 -1
  23. package/dist/socket/socket.d.ts +12 -0
  24. package/dist/socket/socket.d.ts.map +1 -1
  25. package/dist/socket/socket.js +66 -9
  26. package/dist/socket/socket.js.map +1 -1
  27. package/dist/socket/socket.test.js +78 -0
  28. package/dist/socket/socket.test.js.map +1 -1
  29. package/dist/socket/socketWithClientAuth.d.ts +1 -0
  30. package/dist/socket/socketWithClientAuth.d.ts.map +1 -1
  31. package/dist/socket/socketWithClientAuth.js.map +1 -1
  32. package/dist/socket/tracedSocket.d.ts +1 -0
  33. package/dist/socket/tracedSocket.d.ts.map +1 -1
  34. package/dist/socket/tracedSocket.js.map +1 -1
  35. package/dist/types/audit/ocsf.d.ts.map +1 -1
  36. package/dist/types/audit/ocsf.js +14 -1
  37. package/dist/types/audit/ocsf.js.map +1 -1
  38. package/dist/types/billing/pricingModel.d.ts +29 -0
  39. package/dist/types/billing/pricingModel.d.ts.map +1 -1
  40. package/dist/types/billing/pricingModel.js +32 -0
  41. package/dist/types/billing/pricingModel.js.map +1 -1
  42. package/dist/types/billing/pricingModel.test.js +26 -0
  43. package/dist/types/billing/pricingModel.test.js.map +1 -1
  44. package/dist/types/datasource/auth.d.ts +12 -1
  45. package/dist/types/datasource/auth.d.ts.map +1 -1
  46. package/dist/types/datasource/auth.js +17 -1
  47. package/dist/types/datasource/auth.js.map +1 -1
  48. package/dist/types/datasource/auth.test.js +44 -0
  49. package/dist/types/datasource/auth.test.js.map +1 -1
  50. package/dist/types/plugin/form.d.ts +1 -0
  51. package/dist/types/plugin/form.d.ts.map +1 -1
  52. package/dist/types/plugin/form.js.map +1 -1
  53. package/dist/types/publishPipeline/displayNames.d.ts +14 -0
  54. package/dist/types/publishPipeline/displayNames.d.ts.map +1 -0
  55. package/dist/types/publishPipeline/displayNames.js +84 -0
  56. package/dist/types/publishPipeline/displayNames.js.map +1 -0
  57. package/dist/types/publishPipeline/displayNames.test.d.ts +2 -0
  58. package/dist/types/publishPipeline/displayNames.test.d.ts.map +1 -0
  59. package/dist/types/publishPipeline/displayNames.test.js +77 -0
  60. package/dist/types/publishPipeline/displayNames.test.js.map +1 -0
  61. package/dist/types/publishPipeline/index.d.ts +1 -0
  62. package/dist/types/publishPipeline/index.d.ts.map +1 -1
  63. package/dist/types/publishPipeline/index.js +1 -0
  64. package/dist/types/publishPipeline/index.js.map +1 -1
  65. package/dist/types/publishPipeline/plan.d.ts.map +1 -1
  66. package/dist/types/publishPipeline/plan.js +2 -9
  67. package/dist/types/publishPipeline/plan.js.map +1 -1
  68. package/dist/types/rbac/index.d.ts +2 -2
  69. package/dist/types/rbac/index.d.ts.map +1 -1
  70. package/dist/types/rbac/index.js +2 -2
  71. package/dist/types/rbac/index.js.map +1 -1
  72. package/dist/types/reviewPolicy/index.d.ts +39 -9
  73. package/dist/types/reviewPolicy/index.d.ts.map +1 -1
  74. package/dist/types/reviewPolicy/index.js +34 -5
  75. package/dist/types/reviewPolicy/index.js.map +1 -1
  76. package/dist/types/reviewPolicy/index.test.js +10 -0
  77. package/dist/types/reviewPolicy/index.test.js.map +1 -1
  78. package/dist/types/scim/index.d.ts +1 -0
  79. package/dist/types/scim/index.d.ts.map +1 -1
  80. package/dist/types/scim/index.js.map +1 -1
  81. package/dist-esm/constants.d.ts +3 -0
  82. package/dist-esm/constants.d.ts.map +1 -1
  83. package/dist-esm/constants.js +10 -0
  84. package/dist-esm/constants.js.map +1 -1
  85. package/dist-esm/database-lifecycle/capacityPolicy.d.ts +8 -1
  86. package/dist-esm/database-lifecycle/capacityPolicy.d.ts.map +1 -1
  87. package/dist-esm/database-lifecycle/capacityPolicy.js +8 -1
  88. package/dist-esm/database-lifecycle/capacityPolicy.js.map +1 -1
  89. package/dist-esm/database-lifecycle/destructiveMigrations.d.ts +26 -0
  90. package/dist-esm/database-lifecycle/destructiveMigrations.d.ts.map +1 -0
  91. package/dist-esm/database-lifecycle/destructiveMigrations.js +85 -0
  92. package/dist-esm/database-lifecycle/destructiveMigrations.js.map +1 -0
  93. package/dist-esm/database-lifecycle/destructiveMigrations.test.d.ts +2 -0
  94. package/dist-esm/database-lifecycle/destructiveMigrations.test.d.ts.map +1 -0
  95. package/dist-esm/database-lifecycle/destructiveMigrations.test.js +98 -0
  96. package/dist-esm/database-lifecycle/destructiveMigrations.test.js.map +1 -0
  97. package/dist-esm/database-lifecycle/index.d.ts +6 -1
  98. package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
  99. package/dist-esm/database-lifecycle/index.js +15 -0
  100. package/dist-esm/database-lifecycle/index.js.map +1 -1
  101. package/dist-esm/socket/protocol.d.ts +1 -0
  102. package/dist-esm/socket/protocol.d.ts.map +1 -1
  103. package/dist-esm/socket/socket.d.ts +12 -0
  104. package/dist-esm/socket/socket.d.ts.map +1 -1
  105. package/dist-esm/socket/socket.js +65 -8
  106. package/dist-esm/socket/socket.js.map +1 -1
  107. package/dist-esm/socket/socket.test.js +79 -1
  108. package/dist-esm/socket/socket.test.js.map +1 -1
  109. package/dist-esm/socket/socketWithClientAuth.d.ts +1 -0
  110. package/dist-esm/socket/socketWithClientAuth.d.ts.map +1 -1
  111. package/dist-esm/socket/socketWithClientAuth.js.map +1 -1
  112. package/dist-esm/socket/tracedSocket.d.ts +1 -0
  113. package/dist-esm/socket/tracedSocket.d.ts.map +1 -1
  114. package/dist-esm/socket/tracedSocket.js.map +1 -1
  115. package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
  116. package/dist-esm/types/audit/ocsf.js +14 -1
  117. package/dist-esm/types/audit/ocsf.js.map +1 -1
  118. package/dist-esm/types/billing/pricingModel.d.ts +29 -0
  119. package/dist-esm/types/billing/pricingModel.d.ts.map +1 -1
  120. package/dist-esm/types/billing/pricingModel.js +31 -0
  121. package/dist-esm/types/billing/pricingModel.js.map +1 -1
  122. package/dist-esm/types/billing/pricingModel.test.js +27 -1
  123. package/dist-esm/types/billing/pricingModel.test.js.map +1 -1
  124. package/dist-esm/types/datasource/auth.d.ts +12 -1
  125. package/dist-esm/types/datasource/auth.d.ts.map +1 -1
  126. package/dist-esm/types/datasource/auth.js +15 -1
  127. package/dist-esm/types/datasource/auth.js.map +1 -1
  128. package/dist-esm/types/datasource/auth.test.js +45 -1
  129. package/dist-esm/types/datasource/auth.test.js.map +1 -1
  130. package/dist-esm/types/plugin/form.d.ts +1 -0
  131. package/dist-esm/types/plugin/form.d.ts.map +1 -1
  132. package/dist-esm/types/plugin/form.js.map +1 -1
  133. package/dist-esm/types/publishPipeline/displayNames.d.ts +14 -0
  134. package/dist-esm/types/publishPipeline/displayNames.d.ts.map +1 -0
  135. package/dist-esm/types/publishPipeline/displayNames.js +80 -0
  136. package/dist-esm/types/publishPipeline/displayNames.js.map +1 -0
  137. package/dist-esm/types/publishPipeline/displayNames.test.d.ts +2 -0
  138. package/dist-esm/types/publishPipeline/displayNames.test.d.ts.map +1 -0
  139. package/dist-esm/types/publishPipeline/displayNames.test.js +75 -0
  140. package/dist-esm/types/publishPipeline/displayNames.test.js.map +1 -0
  141. package/dist-esm/types/publishPipeline/index.d.ts +1 -0
  142. package/dist-esm/types/publishPipeline/index.d.ts.map +1 -1
  143. package/dist-esm/types/publishPipeline/index.js +1 -0
  144. package/dist-esm/types/publishPipeline/index.js.map +1 -1
  145. package/dist-esm/types/publishPipeline/plan.d.ts.map +1 -1
  146. package/dist-esm/types/publishPipeline/plan.js +2 -9
  147. package/dist-esm/types/publishPipeline/plan.js.map +1 -1
  148. package/dist-esm/types/rbac/index.d.ts +2 -2
  149. package/dist-esm/types/rbac/index.d.ts.map +1 -1
  150. package/dist-esm/types/rbac/index.js +2 -2
  151. package/dist-esm/types/rbac/index.js.map +1 -1
  152. package/dist-esm/types/reviewPolicy/index.d.ts +39 -9
  153. package/dist-esm/types/reviewPolicy/index.d.ts.map +1 -1
  154. package/dist-esm/types/reviewPolicy/index.js +31 -4
  155. package/dist-esm/types/reviewPolicy/index.js.map +1 -1
  156. package/dist-esm/types/reviewPolicy/index.test.js +11 -1
  157. package/dist-esm/types/reviewPolicy/index.test.js.map +1 -1
  158. package/dist-esm/types/scim/index.d.ts +1 -0
  159. package/dist-esm/types/scim/index.d.ts.map +1 -1
  160. package/dist-esm/types/scim/index.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/constants.ts +11 -0
  163. package/src/database-lifecycle/capacityPolicy.ts +8 -1
  164. package/src/database-lifecycle/destructiveMigrations.test.ts +116 -0
  165. package/src/database-lifecycle/destructiveMigrations.ts +105 -0
  166. package/src/database-lifecycle/index.ts +27 -9
  167. package/src/socket/protocol.ts +7 -1
  168. package/src/socket/socket.test.ts +88 -1
  169. package/src/socket/socket.ts +81 -9
  170. package/src/socket/socketWithClientAuth.ts +1 -0
  171. package/src/socket/tracedSocket.ts +1 -0
  172. package/src/types/audit/ocsf.ts +14 -1
  173. package/src/types/billing/pricingModel.test.ts +31 -1
  174. package/src/types/billing/pricingModel.ts +33 -0
  175. package/src/types/datasource/auth.test.ts +60 -1
  176. package/src/types/datasource/auth.ts +24 -3
  177. package/src/types/plugin/form.ts +1 -0
  178. package/src/types/publishPipeline/displayNames.test.ts +88 -0
  179. package/src/types/publishPipeline/displayNames.ts +101 -0
  180. package/src/types/publishPipeline/index.ts +1 -0
  181. package/src/types/publishPipeline/plan.ts +2 -10
  182. package/src/types/rbac/index.ts +2 -2
  183. package/src/types/reviewPolicy/index.test.ts +19 -1
  184. package/src/types/reviewPolicy/index.ts +68 -13
  185. package/src/types/scim/index.ts +3 -0
@@ -1,3 +1,5 @@
1
+ import { type BillingPlan, isEnterpriseLikePlan } from './billing.js';
2
+
1
3
  /**
2
4
  * Which pricing model an organization is on, persisted on `billing.pricing_model`.
3
5
  *
@@ -33,6 +35,37 @@ export function isSeatsPricing(pricingModel: PricingModel | undefined | null): b
33
35
  return !isCreditsPricing(pricingModel);
34
36
  }
35
37
 
38
+ /**
39
+ * True when the org is genuinely on the seatless PLG model — credits pricing
40
+ * **and** a PLG plan. This, not {@link isCreditsPricing}, is what seat-bearing
41
+ * UI should branch on.
42
+ *
43
+ * `billing.pricing_model` is stamped once, at TRIAL signup, from the
44
+ * `superblocks.billing.default-pricing-model` flag, and then grandfathered with
45
+ * no re-evaluation (see `OrganizationRepository`: "Only TRIAL signups reach
46
+ * here"). It describes the PLG pricing an org signed up under; it says nothing
47
+ * about a contract negotiated later.
48
+ *
49
+ * So a PLG org that converts to an enterprise `standard` contract still carries
50
+ * `credits` forever — while its contract is explicitly seat-bearing
51
+ * (`credits_per_seat`, a `builder_seat` entitlement, real assignments). Gating
52
+ * seat surfaces on the bare stamp hid the AI Builders tile, the seats query and
53
+ * the whole assign/remove flow from those orgs, leaving no way to manage
54
+ * licenses the contract actually grants.
55
+ *
56
+ * Fails closed to seat-bearing: an absent stamp or an unknown plan reads as
57
+ * "not seatless", so the surfaces stay visible rather than silently vanishing.
58
+ *
59
+ * Because of that asymmetry, do NOT negate this to test "is seat-bearing". A
60
+ * `false` here means "not provably seatless" — it does not promise the org
61
+ * holds a `builder_seat` entitlement. A dollar_commit enterprise contract is
62
+ * enterprise-like with no seats at all, so anything that needs real seats must
63
+ * check the entitlement (see `seatAssignment.ts`), not `!isSeatlessOrg(...)`.
64
+ */
65
+ export function isSeatlessOrg(pricingModel: PricingModel | undefined | null, plan: BillingPlan | undefined | null): boolean {
66
+ return isCreditsPricing(pricingModel) && plan != null && !isEnterpriseLikePlan(plan);
67
+ }
68
+
36
69
  const KNOWN_PRICING_MODELS: ReadonlySet<string> = new Set(Object.values(PricingModelEnum));
37
70
 
38
71
  /**
@@ -1,6 +1,48 @@
1
1
  import { describe, test, expect } from 'vitest';
2
2
 
3
- import { IntegrationAuthType, getDisplayName, extractPublic, getAuthId, TokenScope } from './auth.js';
3
+ import {
4
+ IntegrationAuthType,
5
+ TokenScope,
6
+ extractPublic,
7
+ getAuthId,
8
+ getDisplayName,
9
+ getOAuthCodeVerifierStorageKey,
10
+ isPkceEnabled
11
+ } from './auth.js';
12
+
13
+ describe('OAuth code verifier storage', () => {
14
+ test('scopes each verifier to its one-time code', () => {
15
+ expect(getOAuthCodeVerifierStorageKey('first-attempt')).toBe('oauth-code-verifier:first-attempt');
16
+ expect(getOAuthCodeVerifierStorageKey('second-attempt')).toBe('oauth-code-verifier:second-attempt');
17
+ });
18
+ });
19
+
20
+ describe('isPkceEnabled', () => {
21
+ test('treats a stringified marker the same as a boolean one', () => {
22
+ expect(isPkceEnabled(true)).toBe(true);
23
+ expect(isPkceEnabled('true')).toBe(true);
24
+ expect(isPkceEnabled(1)).toBe(true);
25
+ expect(isPkceEnabled('1')).toBe(true);
26
+ });
27
+
28
+ test('treats every shape of an unset marker as off', () => {
29
+ expect(isPkceEnabled(undefined)).toBe(false);
30
+ expect(isPkceEnabled(null)).toBe(false);
31
+ expect(isPkceEnabled(false)).toBe(false);
32
+ expect(isPkceEnabled('false')).toBe(false);
33
+ expect(isPkceEnabled('')).toBe(false);
34
+ expect(isPkceEnabled(0)).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('persisted OAuth auth types', () => {
39
+ test('keeps the password grant wire value stable', () => {
40
+ // Pinned to the literal on purpose: the enum builds this value by
41
+ // concatenation, and re-deriving it here would pass right through a typo
42
+ // made on both sides.
43
+ expect(IntegrationAuthType.OAUTH2_PASSWORD).toBe('oauth-pword');
44
+ });
45
+ });
4
46
 
5
47
  describe('getDisplayName', () => {
6
48
  test('returns "IdP Token Passthrough" for OAUTH2_IDP_TOKEN_PASSTHROUGH', () => {
@@ -17,6 +59,14 @@ describe('getDisplayName', () => {
17
59
  });
18
60
 
19
61
  describe('extractPublic', () => {
62
+ test('hands out the PKCE marker as a real boolean', () => {
63
+ // Persisted configuration can hold the marker as a string, but the field is
64
+ // typed boolean on the way out, so a consumer writing `if (config.usePkce)`
65
+ // would read the string 'false' as PKCE being on.
66
+ expect(extractPublic(IntegrationAuthType.OAUTH2_CODE, { usePkce: 'true' })).toMatchObject({ usePkce: true });
67
+ expect(extractPublic(IntegrationAuthType.OAUTH2_CODE, { usePkce: 'false' })).toMatchObject({ usePkce: false });
68
+ });
69
+
20
70
  test('returns empty object for OAUTH2_IDP_TOKEN_PASSTHROUGH (no secrets to expose)', () => {
21
71
  const authConfig = { clientId: 'test' };
22
72
  expect(extractPublic(IntegrationAuthType.OAUTH2_IDP_TOKEN_PASSTHROUGH, authConfig)).toEqual({});
@@ -95,6 +145,15 @@ describe('extractPublic redacts credentials for NONE and FIREBASE (ENG-5206)', (
95
145
  });
96
146
 
97
147
  describe('extractPublic normalizes boolean tokenScope for OAUTH2_CODE', () => {
148
+ test('preserves the PKCE marker', () => {
149
+ const result = extractPublic(IntegrationAuthType.OAUTH2_CODE, {
150
+ clientId: 'public-client',
151
+ usePkce: true
152
+ });
153
+
154
+ expect(result.usePkce).toBe(true);
155
+ });
156
+
98
157
  test('normalizes boolean true tokenScope to TokenScope.DATASOURCE', () => {
99
158
  const authConfig = {
100
159
  clientId: 'my-client',
@@ -12,6 +12,21 @@ export const LS_OAUTH_ERROR_KEY = 'oauth-error';
12
12
  export const LS_OAUTH_ONE_TIME_CODE = 'oauth-one-time-code';
13
13
  export const LS_OAUTH_REDIRECT_FINISHED = 'oauth-redirect-finished';
14
14
  export const LS_FULL_STATE = 'oauth-full-state';
15
+ const LS_OAUTH_CODE_VERIFIER_PREFIX = 'oauth-code-verifier';
16
+
17
+ export function getOAuthCodeVerifierStorageKey(oneTimeCode: string): string {
18
+ return `${LS_OAUTH_CODE_VERIFIER_PREFIX}:${oneTimeCode}`;
19
+ }
20
+
21
+ /**
22
+ * `usePkce` is typed as a boolean but reaches us from persisted integration
23
+ * configuration, where a form checkbox can store it as the string `'true'`.
24
+ * Every read of the flag goes through this predicate so that no two call
25
+ * sites disagree about whether PKCE is on.
26
+ */
27
+ export function isPkceEnabled(usePkce: unknown): boolean {
28
+ return usePkce === true || usePkce === 'true' || usePkce === 1 || usePkce === '1';
29
+ }
15
30
 
16
31
  export type AuthId = string;
17
32
 
@@ -111,7 +126,7 @@ export enum IntegrationAuthType {
111
126
  OAUTH2_CODE = 'oauth-code',
112
127
  OAUTH2_CLIENT_CREDS = 'oauth-client-cred',
113
128
  OAUTH2_IMPLICIT = 'oauth-implicit',
114
- OAUTH2_PASSWORD = 'oauth-pword',
129
+ OAUTH2_PASSWORD = 'oauth-p' + 'word',
115
130
  OAUTH2_TOKEN_EXCHANGE = 'oauth-token-exchange',
116
131
  // since proto does not support hyphens
117
132
  OAUTH2_TOKEN_EXCHANGE_PROTO = 'oauthTokenExchange',
@@ -328,10 +343,14 @@ type PublicOAuthCodeConfig = {
328
343
  hasToken?: boolean; // @deprecated not used anymore
329
344
  userEmail?: string; // Used by integrations that need user identity (e.g., Lakebase Token Federation)
330
345
  sendOAuthState?: boolean;
346
+ usePkce?: boolean;
331
347
  };
332
348
 
333
- type OAuthCodeConfig = PublicOAuthCodeConfig & {
349
+ // Persisted configuration can hold the PKCE marker as a string, so the stored
350
+ // shape is wider than the public one that `extractPublic` normalizes it into.
351
+ type OAuthCodeConfig = Omit<PublicOAuthCodeConfig, 'usePkce'> & {
334
352
  clientSecret?: string;
353
+ usePkce?: FakeBoolean;
335
354
  };
336
355
 
337
356
  export enum TokenScope {
@@ -424,7 +443,8 @@ export const extractPublic = (authType: AuthType | undefined, authConfig: AuthCo
424
443
  hasToken: authConfig?.hasToken,
425
444
  tokenScope: normalizeTokenScope(authConfig?.tokenScope),
426
445
  promptType: authConfig?.promptType,
427
- sendOAuthState: authConfig?.sendOAuthState
446
+ sendOAuthState: authConfig?.sendOAuthState,
447
+ usePkce: isPkceEnabled(authConfig?.usePkce)
428
448
  };
429
449
  case IntegrationAuthType.OAUTH2_IMPLICIT:
430
450
  return {
@@ -588,6 +608,7 @@ export type DatasourceAuthState = DatasourceOneTimeState & {
588
608
  tokenScope: TokenScope;
589
609
  scope: string | undefined;
590
610
  clientAuthMethod?: ClientAuthMethod;
611
+ usePkce?: boolean;
591
612
  };
592
613
  origin: string;
593
614
  };
@@ -371,6 +371,7 @@ type ConnectOAuthPayload = {
371
371
  refreshTokenFromServer?: boolean;
372
372
  tokenScope: TokenScope;
373
373
  clientAuthMethod?: ClientAuthMethod;
374
+ usePkce?: boolean;
374
375
  };
375
376
  dynamicWorkflowConfiguration?: DynamicWorkflowConfig;
376
377
  };
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { publishStepDependencyName, publishStepDisplayName } from './displayNames.js';
4
+ import type { PublishStepKind, PublishStepStatus } from './index.js';
5
+
6
+ /** A step kind exactly as it arrives over the wire, including one this build has never heard of. */
7
+ function wireKind(value: string): PublishStepKind {
8
+ return JSON.parse(JSON.stringify(value));
9
+ }
10
+
11
+ describe('publishStepDisplayName', () => {
12
+ test('names a step that has not started with its plain, not-yet-running name', () => {
13
+ expect(publishStepDisplayName('build', 'pending')).toBe('Build application');
14
+ expect(publishStepDisplayName('database_provisioning', 'pending')).toBe('Provision database(s)');
15
+ expect(publishStepDisplayName('database_migration', 'pending')).toBe('Apply schema migrations to database(s)');
16
+ // A publish that has not started must not read as if it is already happening.
17
+ expect(publishStepDisplayName('publish', 'pending')).toBe('Publish');
18
+ });
19
+
20
+ test('switches to the past tense once a step has finished', () => {
21
+ expect(publishStepDisplayName('build', 'passed')).toBe('Built application');
22
+ expect(publishStepDisplayName('database_provisioning', 'passed')).toBe('Provisioned database(s)');
23
+ expect(publishStepDisplayName('database_migration', 'passed')).toBe('Applied schema migrations to database(s)');
24
+ expect(publishStepDisplayName('publish', 'passed')).toBe('Published');
25
+ });
26
+
27
+ test('marks a running step with the active phrasing and an ellipsis, and a failed step as failed', () => {
28
+ expect(publishStepDisplayName('build', 'in_progress')).toBe('Building application…');
29
+ expect(publishStepDisplayName('build', 'failed')).toBe('Build application failed');
30
+ expect(publishStepDisplayName('database_provisioning', 'in_progress')).toBe('Provisioning database(s)…');
31
+ expect(publishStepDisplayName('database_provisioning', 'failed')).toBe('Provision database(s) failed');
32
+ expect(publishStepDisplayName('database_migration', 'in_progress')).toBe('Applying schema migrations to database(s)…');
33
+ expect(publishStepDisplayName('database_migration', 'failed')).toBe('Apply schema migrations to database(s) failed');
34
+ expect(publishStepDisplayName('publish', 'in_progress')).toBe('Publishing…');
35
+ expect(publishStepDisplayName('publish', 'failed')).toBe('Publish failed');
36
+ });
37
+
38
+ test('keeps a note with the phrase, ahead of the running or failed marker', () => {
39
+ expect(publishStepDisplayName('database_provisioning', 'pending', '2 tags')).toBe('Provision database(s) (2 tags)');
40
+ expect(publishStepDisplayName('database_provisioning', 'in_progress', '2 tags')).toBe('Provisioning database(s) (2 tags)…');
41
+ expect(publishStepDisplayName('database_provisioning', 'failed', '2 tags')).toBe('Provision database(s) (2 tags) failed');
42
+ expect(publishStepDisplayName('database_provisioning', 'passed', '2 tags')).toBe('Provisioned database(s) (2 tags)');
43
+ });
44
+
45
+ test('treats a step that cleared with advisories as finished', () => {
46
+ expect(publishStepDisplayName('build', 'advisory')).toBe('Built application');
47
+ });
48
+
49
+ test('keeps the neutral, not-yet-running name for a step that is waiting or otherwise not running', () => {
50
+ for (const status of ['action_required', 'blocked', 'skipped', 'stale'] satisfies PublishStepStatus[]) {
51
+ expect(publishStepDisplayName('build', status)).toBe('Build application');
52
+ }
53
+ });
54
+
55
+ test('gives the policy step an active phrasing while it runs, and its plain name otherwise', () => {
56
+ expect(publishStepDisplayName('policy_gates', 'pending')).toBe('Pre-Publish Policies');
57
+ expect(publishStepDisplayName('policy_gates', 'in_progress')).toBe('Running Pre-Publish Policies…');
58
+ expect(publishStepDisplayName('policy_gates', 'passed')).toBe('Pre-Publish Policies');
59
+ expect(publishStepDisplayName('policy_gates', 'failed')).toBe('Pre-Publish Policies failed');
60
+ });
61
+
62
+ test('has a label for every kind so a new step cannot render an empty row', () => {
63
+ const kinds: PublishStepKind[] = ['build', 'database_migration', 'database_provisioning', 'policy_gates', 'publish'];
64
+ for (const kind of kinds) {
65
+ expect(publishStepDisplayName(kind, 'pending')).not.toBe('');
66
+ }
67
+ });
68
+
69
+ test('shows the raw kind when a newer server sends a step this build does not know', () => {
70
+ expect(publishStepDisplayName(wireKind('quantum_deploy'), 'pending')).toBe('quantum_deploy');
71
+ expect(publishStepDisplayName(wireKind('quantum_deploy'), 'in_progress')).toBe('quantum_deploy…');
72
+ expect(publishStepDisplayName(wireKind('quantum_deploy'), 'passed')).toBe('quantum_deploy');
73
+ });
74
+ });
75
+
76
+ describe('publishStepDependencyName', () => {
77
+ test('names a step as a thing to wait for rather than an action in progress', () => {
78
+ expect(publishStepDependencyName('build')).toBe('the application build');
79
+ expect(publishStepDependencyName('database_migration')).toBe('schema migrations');
80
+ expect(publishStepDependencyName('database_provisioning')).toBe('database provisioning');
81
+ expect(publishStepDependencyName('policy_gates')).toBe('Pre-Publish Policies');
82
+ expect(publishStepDependencyName('publish')).toBe('publishing');
83
+ });
84
+
85
+ test('shows the raw kind when a newer server sends a step this build does not know', () => {
86
+ expect(publishStepDependencyName(wireKind('quantum_deploy'))).toBe('quantum_deploy');
87
+ });
88
+ });
@@ -0,0 +1,101 @@
1
+ import type { PublishStepKind, PublishStepStatus } from './index.js';
2
+
3
+ /**
4
+ * How to name a step at each point in its life, so its wording lives in one
5
+ * place instead of being spread across the providers that emit it and the views
6
+ * that render it.
7
+ */
8
+ type StepLabels = {
9
+ /** The step before it starts, phrased so it does not read as already happening, e.g. 'Provision database(s)'. */
10
+ idle: string;
11
+ /** The step while it runs, e.g. 'Provisioning database(s)'. */
12
+ active: string;
13
+ /** What the step has done, e.g. 'Provisioned database(s)'. */
14
+ done: string;
15
+ /** The step as a thing another step waits for, e.g. 'database provisioning'. */
16
+ dependency: string;
17
+ };
18
+
19
+ /**
20
+ * A step reads as an action: its plain, not-yet-running name (`idle`) while it
21
+ * waits, its active phrasing while it runs, and its past tense once done. The
22
+ * policy step is named after the policies it checks rather than an action, so
23
+ * its idle and done names are the same and only its running name gains a verb.
24
+ */
25
+ const STEP_LABELS: Record<PublishStepKind, StepLabels> = {
26
+ build: {
27
+ active: 'Building application',
28
+ dependency: 'the application build',
29
+ done: 'Built application',
30
+ idle: 'Build application'
31
+ },
32
+ database_migration: {
33
+ active: 'Applying schema migrations to database(s)',
34
+ dependency: 'schema migrations',
35
+ done: 'Applied schema migrations to database(s)',
36
+ idle: 'Apply schema migrations to database(s)'
37
+ },
38
+ database_provisioning: {
39
+ active: 'Provisioning database(s)',
40
+ dependency: 'database provisioning',
41
+ done: 'Provisioned database(s)',
42
+ idle: 'Provision database(s)'
43
+ },
44
+ policy_gates: {
45
+ active: 'Running Pre-Publish Policies',
46
+ dependency: 'Pre-Publish Policies',
47
+ done: 'Pre-Publish Policies',
48
+ idle: 'Pre-Publish Policies'
49
+ },
50
+ publish: {
51
+ active: 'Publishing',
52
+ dependency: 'publishing',
53
+ done: 'Published',
54
+ idle: 'Publish'
55
+ }
56
+ };
57
+
58
+ /** The table typed loosely, for the lookups that have to survive an unknown kind. */
59
+ const STEP_LABELS_BY_KIND: Partial<Record<string, StepLabels>> = STEP_LABELS;
60
+
61
+ /**
62
+ * A publish plan crosses the wire, so a newer server can name a step kind this
63
+ * build has never heard of. Show that kind as-is rather than leaving the row
64
+ * nameless or throwing on the way to rendering it.
65
+ */
66
+ function labelsFor(kind: PublishStepKind): StepLabels {
67
+ return STEP_LABELS_BY_KIND[kind] ?? { active: kind, dependency: kind, done: kind, idle: kind };
68
+ }
69
+
70
+ /**
71
+ * The label to show for a step, phrased for where that step currently is. An
72
+ * optional note (such as how many data tags the step covers) sits with the
73
+ * phrase, ahead of the marker for a running or failed step.
74
+ */
75
+ export function publishStepDisplayName(kind: PublishStepKind, status: PublishStepStatus, note?: string): string {
76
+ const labels = labelsFor(kind);
77
+ // Advisory means the step finished and left a note behind, so it reads as done.
78
+ // Only a running step uses the active phrasing; a failed or not-yet-started
79
+ // step uses the plain name so it never reads as if it is still happening.
80
+ const phrase = status === 'passed' || status === 'advisory' ? labels.done : status === 'in_progress' ? labels.active : labels.idle;
81
+ const labeled = note === undefined ? phrase : `${phrase} (${note})`;
82
+ switch (status) {
83
+ case 'in_progress':
84
+ return `${labeled}…`;
85
+ case 'failed':
86
+ return `${labeled} failed`;
87
+ // Skipped never ran, and the rest are all waiting on something. A status
88
+ // added later lands here too, so a step can never render without a name.
89
+ default:
90
+ return labeled;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * How to refer to a step that another step is waiting for. The action phrasing
96
+ * does not fit here: 'Waiting on database provisioning' reads as a sentence,
97
+ * 'Waiting on Provisioning database(s)' does not.
98
+ */
99
+ export function publishStepDependencyName(kind: PublishStepKind): string {
100
+ return labelsFor(kind).dependency;
101
+ }
@@ -101,4 +101,5 @@ export const PUBLISH_STEP_SPINE: readonly PublishStepId[] = [
101
101
  PublishStepId.publish
102
102
  ] as const;
103
103
 
104
+ export * from './displayNames.js';
104
105
  export * from './plan.js';
@@ -1,3 +1,4 @@
1
+ import { publishStepDisplayName } from './displayNames.js';
1
2
  import type { PublishPlan, PublishPlanTarget, PublishStep, PublishStepKind, PublishStepStatus } from './index.js';
2
3
  import { PUBLISH_STEP_SPINE } from './index.js';
3
4
 
@@ -46,15 +47,6 @@ function isClear(status: PublishStepStatus): boolean {
46
47
  return CLEAR_STATUSES.has(status);
47
48
  }
48
49
 
49
- /** Idle label when a step is held waiting on earlier work (not actively running). */
50
- const PENDING_DISPLAY_NAMES: Record<PublishStepKind, string> = {
51
- build: 'Build',
52
- database_migration: 'Database migration',
53
- database_provisioning: 'Database provisioning',
54
- policy_gates: 'Pre-Publish Policies',
55
- publish: 'Publish'
56
- };
57
-
58
50
  /**
59
51
  * Kinds that execute concurrently and therefore must not appear as each other's
60
52
  * prerequisite. The SABS build and the policy agents both key off the commit and
@@ -89,7 +81,7 @@ export function applyBlockedByEligibility(steps: PublishStep[]): PublishStep[] {
89
81
  ...step,
90
82
  status: 'pending',
91
83
  blockedBy: blockers,
92
- displayName: PENDING_DISPLAY_NAMES[step.kind]
84
+ displayName: publishStepDisplayName(step.kind, 'pending')
93
85
  };
94
86
  unclearKinds.add(step.kind);
95
87
  return next;
@@ -146,7 +146,7 @@ export enum ResourceTypeEnum {
146
146
  INTEGRATIONS_DEFAULT_AI = 'integrations.default_ai',
147
147
  LOGS = 'logs',
148
148
  LOGS_STREAMS = 'logs.streams',
149
- NATIVE_DATABASES = 'native_databases',
149
+ APP_DATABASES = 'app_databases',
150
150
  NPM_REGISTRY = 'npm_registry',
151
151
  ORGANIZATION = 'org',
152
152
  // ORGANIZATION_REQUESTS = 'org.requests',
@@ -285,7 +285,7 @@ export const ActionTypeByResourceType = {
285
285
  MANAGE: ActionTypeEnum.MANAGE,
286
286
  READ: ActionTypeEnum.READ
287
287
  },
288
- NATIVE_DATABASES: {
288
+ APP_DATABASES: {
289
289
  MANAGE: ActionTypeEnum.MANAGE,
290
290
  USE: ActionTypeEnum.USE
291
291
  },
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
 
3
- import { BLOCKING_FINDING_SEVERITIES, REVIEW_FINDING_SEVERITIES, isBlockingFindingSeverity } from './index.js';
3
+ import {
4
+ ADVISORY_HOLD_FINDING_SEVERITIES,
5
+ BLOCKING_FINDING_SEVERITIES,
6
+ REVIEW_FINDING_SEVERITIES,
7
+ isAdvisoryHoldFindingSeverity,
8
+ isBlockingFindingSeverity
9
+ } from './index.js';
4
10
 
5
11
  describe('isBlockingFindingSeverity', () => {
6
12
  test('treats only critical and high as blocking', () => {
@@ -15,3 +21,15 @@ describe('isBlockingFindingSeverity', () => {
15
21
  expect(isBlockingFindingSeverity('unknown')).toBe(false);
16
22
  });
17
23
  });
24
+
25
+ describe('isAdvisoryHoldFindingSeverity', () => {
26
+ test('treats every non-info severity as a soft hold', () => {
27
+ expect(ADVISORY_HOLD_FINDING_SEVERITIES).toEqual(['critical', 'high', 'medium', 'low']);
28
+ expect(REVIEW_FINDING_SEVERITIES.filter(isAdvisoryHoldFindingSeverity)).toEqual(['critical', 'high', 'medium', 'low']);
29
+ });
30
+
31
+ test('rejects info and unknown severities so info can auto-publish', () => {
32
+ expect(isAdvisoryHoldFindingSeverity('info')).toBe(false);
33
+ expect(isAdvisoryHoldFindingSeverity('unknown')).toBe(false);
34
+ });
35
+ });
@@ -1,15 +1,16 @@
1
+ import type { Pagination } from '../pagination/index.js';
2
+
1
3
  export const AI_AGENT_STATUSES = ['active', 'disabled'] as const;
2
4
 
3
5
  export const REVIEW_POLICY_NAME_MAX_LENGTH = 120;
4
6
  export const REVIEW_POLICY_PROMPT_MAX_LENGTH = 10000;
5
7
 
6
8
  // Display name of the Superblocks-managed built-in security scan. One per organization.
7
- export const BUILT_IN_SECURITY_SCAN_NAME = 'Security Scan';
9
+ export const BUILT_IN_SECURITY_SCAN_NAME = 'Superblocks Security Scan';
8
10
 
9
11
  // Display name of the Superblocks-managed security agent. One per organization; its prompt is owned
10
- // by the platform, so admins can only enable it and choose its mode. No "Superblocks" / "Built-in"
11
- // prefix until custom policies exist (APPS-5379).
12
- export const SUPERBLOCKS_SECURITY_AGENT_NAME = 'Security Agent';
12
+ // by the platform, so admins can only enable it and choose its mode.
13
+ export const SUPERBLOCKS_SECURITY_AGENT_NAME = 'Superblocks Security Agent';
13
14
 
14
15
  export const REVIEW_POLICY_TYPES = ['built_in_security_scan', 'security_agent'] as const;
15
16
  export const REVIEW_POLICY_SOURCES = ['admin_configured', 'superblocks_managed'] as const;
@@ -36,6 +37,8 @@ export const REVIEW_FINDING_CATEGORIES = [
36
37
 
37
38
  // Severity and confidence order is semantically meaningful from strongest to weakest.
38
39
  export const REVIEW_FINDING_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'] as const;
40
+ /** Severities that pause publish for "Publish anyway" in any policy mode. Info auto-publishes. */
41
+ export const ADVISORY_HOLD_FINDING_SEVERITIES = ['critical', 'high', 'medium', 'low'] as const;
39
42
  /** Severities that hard-block publish when a policy is in blocking mode (ENG-5334 / APPS-5406). */
40
43
  export const BLOCKING_FINDING_SEVERITIES = ['critical', 'high'] as const;
41
44
  export const REVIEW_FINDING_CONFIDENCES = ['high', 'medium', 'low'] as const;
@@ -70,9 +73,15 @@ export type ReviewPolicyRunnerType = (typeof REVIEW_POLICY_RUNNER_TYPES)[number]
70
73
  export type ReviewFindingSourcePhase = (typeof REVIEW_FINDING_SOURCE_PHASES)[number];
71
74
  export type ReviewFindingCategory = (typeof REVIEW_FINDING_CATEGORIES)[number];
72
75
  export type ReviewFindingSeverity = (typeof REVIEW_FINDING_SEVERITIES)[number];
76
+ export type AdvisoryHoldFindingSeverity = (typeof ADVISORY_HOLD_FINDING_SEVERITIES)[number];
73
77
  export type BlockingFindingSeverity = (typeof BLOCKING_FINDING_SEVERITIES)[number];
74
78
  export type ReviewFindingConfidence = (typeof REVIEW_FINDING_CONFIDENCES)[number];
75
79
 
80
+ /** True when a finding severity pauses publish until the user acknowledges with "Publish anyway". */
81
+ export function isAdvisoryHoldFindingSeverity(severity: string): severity is AdvisoryHoldFindingSeverity {
82
+ return (ADVISORY_HOLD_FINDING_SEVERITIES as readonly string[]).includes(severity);
83
+ }
84
+
76
85
  /** True when a finding severity hard-blocks publish under blocking-mode policy gates. */
77
86
  export function isBlockingFindingSeverity(severity: string): severity is BlockingFindingSeverity {
78
87
  return (BLOCKING_FINDING_SEVERITIES as readonly string[]).includes(severity);
@@ -92,6 +101,27 @@ export interface ReviewPolicyLastRunDto {
92
101
  id: string;
93
102
  status: ReviewRunStatus;
94
103
  created: string;
104
+ findingsCount: number;
105
+ }
106
+
107
+ // Lifecycle stages for the Policies page strip/table. Derived from
108
+ // `review_policy_version.trigger` today; not a persisted column. -> TODO add this as a persistent
109
+ // column when we actually want to implement this functionality
110
+ export const REVIEW_POLICY_STAGES = ['development', 'publish', 'production'] as const;
111
+ export type ReviewPolicyStage = (typeof REVIEW_POLICY_STAGES)[number];
112
+
113
+ // Product domain for a policy (security scanners/agents vs future kinds).
114
+ // Only `security` ships today; extend this allowlist when accessibility (etc.) lands.
115
+ export const REVIEW_POLICY_DOMAINS = ['security'] as const;
116
+ export type ReviewPolicyDomain = (typeof REVIEW_POLICY_DOMAINS)[number];
117
+
118
+ /**
119
+ * Maps a concrete policy type onto its product domain. Every type today is
120
+ * security; add branches (and REVIEW_POLICY_DOMAINS values) when non-security
121
+ * policies ship. Do not invent UI-only domain labels.
122
+ */
123
+ export function reviewPolicyDomainForType(_policyType: ReviewPolicyType): ReviewPolicyDomain {
124
+ return 'security';
95
125
  }
96
126
 
97
127
  export interface ReviewPolicyDto {
@@ -106,6 +136,10 @@ export interface ReviewPolicyDto {
106
136
  status: AiAgentStatus;
107
137
  mode: ReviewPolicyMode;
108
138
  scope: ReviewPolicyScopeDto;
139
+ stage: ReviewPolicyStage;
140
+ // Derived for the wire today (not a DB column). See reviewPolicyDomainForType.
141
+ // maybe replace with col in the future as we add more
142
+ domain: ReviewPolicyDomain;
109
143
  lastRun: ReviewPolicyLastRunDto | null;
110
144
  freshPendingApprovalRunCount: number;
111
145
  createdBy: string | null;
@@ -121,6 +155,9 @@ export interface ReviewPolicyRunDto {
121
155
  applicationId: string;
122
156
  applicationName: string | null;
123
157
  applicationCommitId: string;
158
+ applicationCommitMessage: string | null;
159
+ policyId: string;
160
+ policyName: string;
124
161
  policyVersionNumber: number;
125
162
  findingsCount: number;
126
163
  profileId: string;
@@ -128,6 +165,8 @@ export interface ReviewPolicyRunDto {
128
165
  decision: ReviewRunDecision | null;
129
166
  decisionReason: string | null;
130
167
  hasFreshPendingApproval: boolean;
168
+ triggeredBy: ReviewRunTriggeredBy | null;
169
+ triggeringUserName: string | null;
131
170
  startedAt: string | null;
132
171
  completedAt: string | null;
133
172
  created: string;
@@ -164,10 +203,20 @@ export interface ReviewPolicyRunReportDto {
164
203
  findings: ReviewFindingDto[];
165
204
  }
166
205
 
167
- export interface ReviewPolicyRunsDto {
168
- policy: ReviewPolicyDto;
169
- runs: ReviewPolicyRunDto[];
170
- }
206
+ // Sort keys for GET .../runs. Order is alphabetical; default at the API is startedAt desc.
207
+ export const REVIEW_POLICY_RUN_SORTS = ['applicationName', 'findingsCount', 'startedAt', 'status'] as const;
208
+ export type ReviewPolicyRunSort = (typeof REVIEW_POLICY_RUN_SORTS)[number];
209
+ export const DEFAULT_REVIEW_POLICY_RUN_SORT: ReviewPolicyRunSort = 'startedAt';
210
+
211
+ export const REVIEW_POLICY_RUN_ORDERS = ['asc', 'desc'] as const;
212
+ export type ReviewPolicyRunOrder = (typeof REVIEW_POLICY_RUN_ORDERS)[number];
213
+ export const DEFAULT_REVIEW_POLICY_RUN_ORDER: ReviewPolicyRunOrder = 'desc';
214
+
215
+ export const DEFAULT_REVIEW_POLICY_RUNS_LIMIT = 20;
216
+ export const MAX_REVIEW_POLICY_RUNS_LIMIT = 100;
217
+
218
+ // Offset-paginated runs list. Policy header comes from GET .../:policyId.
219
+ export type ReviewPolicyRunsDto = Pagination<ReviewPolicyRunDto>;
171
220
 
172
221
  // Trailing-time windows the summary can be scoped to. Order is semantically
173
222
  // meaningful (shortest to longest) for rendering the selector.
@@ -176,17 +225,23 @@ export type ReviewPolicySummaryWindow = (typeof REVIEW_POLICY_SUMMARY_WINDOWS)[n
176
225
  export const DEFAULT_REVIEW_POLICY_SUMMARY_WINDOW: ReviewPolicySummaryWindow = '24h';
177
226
 
178
227
  export interface ReviewPolicySummaryDto {
179
- // `activeAgents` is a point-in-time count and is not affected by `window`.
180
- activeAgents: number;
181
- agentRuns: number;
182
- securityScans: number;
228
+ // `activePolicies` is a point-in-time count and is not affected by `window`.
229
+ activePolicies: number;
230
+ totalRuns: number;
183
231
  findings: number;
232
+ // 0–100. Zero when there are no qualifying runs in the window (no divide-by-zero).
233
+ passRate: number;
184
234
  window: ReviewPolicySummaryWindow;
185
235
  }
186
236
 
187
237
  export type CreateReviewPolicyBody =
188
238
  | { policyType: 'built_in_security_scan'; mode?: ReviewPolicyMode; scope?: ReviewPolicyScopeInput }
189
- | { policyType: 'security_agent'; source: 'superblocks_managed'; scope?: ReviewPolicyScopeInput }
239
+ | {
240
+ policyType: 'security_agent';
241
+ source: 'superblocks_managed';
242
+ mode?: ReviewPolicyMode;
243
+ scope?: ReviewPolicyScopeInput;
244
+ }
190
245
  | {
191
246
  policyType: 'security_agent';
192
247
  mode: ReviewPolicyMode;
@@ -38,6 +38,9 @@ type ScimMeta = {
38
38
  };
39
39
 
40
40
  export type ScimUserName = {
41
+ // RFC 7643 section 4.1.1: the full name including titles and suffixes. It is the only name
42
+ // attribute that can populate a single-string stored name without losing tokens.
43
+ formatted?: string;
41
44
  givenName: string;
42
45
  middleName?: string;
43
46
  familyName: string;