@superblocksteam/shared 0.9613.2 → 0.9614.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 (203) hide show
  1. package/dist/database-lifecycle/destructiveMigrations.d.ts +18 -0
  2. package/dist/database-lifecycle/destructiveMigrations.d.ts.map +1 -1
  3. package/dist/database-lifecycle/destructiveMigrations.js.map +1 -1
  4. package/dist/database-lifecycle/index.d.ts +8 -1
  5. package/dist/database-lifecycle/index.d.ts.map +1 -1
  6. package/dist/database-lifecycle/index.js +2 -1
  7. package/dist/database-lifecycle/index.js.map +1 -1
  8. package/dist/dbfs/types.d.ts +1 -0
  9. package/dist/dbfs/types.d.ts.map +1 -1
  10. package/dist/dbfs/types.js +14 -3
  11. package/dist/dbfs/types.js.map +1 -1
  12. package/dist/errors/http.d.ts +5 -3
  13. package/dist/errors/http.d.ts.map +1 -1
  14. package/dist/errors/http.js +9 -5
  15. package/dist/errors/http.js.map +1 -1
  16. package/dist/types/ai/index.d.ts +1 -0
  17. package/dist/types/ai/index.d.ts.map +1 -1
  18. package/dist/types/ai/index.js +1 -0
  19. package/dist/types/ai/index.js.map +1 -1
  20. package/dist/types/audit/ocsf.d.ts +11 -0
  21. package/dist/types/audit/ocsf.d.ts.map +1 -1
  22. package/dist/types/billing/index.d.ts +1 -0
  23. package/dist/types/billing/index.d.ts.map +1 -1
  24. package/dist/types/billing/index.js +1 -0
  25. package/dist/types/billing/index.js.map +1 -1
  26. package/dist/types/billing/usageSource.d.ts +26 -0
  27. package/dist/types/billing/usageSource.d.ts.map +1 -0
  28. package/dist/types/billing/usageSource.js +100 -0
  29. package/dist/types/billing/usageSource.js.map +1 -0
  30. package/dist/types/policyGate/errors.d.ts +3 -0
  31. package/dist/types/policyGate/errors.d.ts.map +1 -1
  32. package/dist/types/policyGate/errors.js +1 -0
  33. package/dist/types/policyGate/errors.js.map +1 -1
  34. package/dist/types/policyGate/index.d.ts +5 -1
  35. package/dist/types/policyGate/index.d.ts.map +1 -1
  36. package/dist/types/policyGate/index.js +11 -0
  37. package/dist/types/policyGate/index.js.map +1 -1
  38. package/dist/types/policyGate/index.test.d.ts +2 -0
  39. package/dist/types/policyGate/index.test.d.ts.map +1 -0
  40. package/dist/types/policyGate/index.test.js +23 -0
  41. package/dist/types/policyGate/index.test.js.map +1 -0
  42. package/dist/types/publishPipeline/index.d.ts +27 -0
  43. package/dist/types/publishPipeline/index.d.ts.map +1 -1
  44. package/dist/types/publishPipeline/index.js +1 -0
  45. package/dist/types/publishPipeline/index.js.map +1 -1
  46. package/dist/types/publishPipeline/plan.d.ts +9 -4
  47. package/dist/types/publishPipeline/plan.d.ts.map +1 -1
  48. package/dist/types/publishPipeline/plan.js +37 -20
  49. package/dist/types/publishPipeline/plan.js.map +1 -1
  50. package/dist/types/publishPipeline/plan.test.js +28 -8
  51. package/dist/types/publishPipeline/plan.test.js.map +1 -1
  52. package/dist/types/publishPipeline/policyStatus.d.ts +7 -0
  53. package/dist/types/publishPipeline/policyStatus.d.ts.map +1 -0
  54. package/dist/types/publishPipeline/policyStatus.js +40 -0
  55. package/dist/types/publishPipeline/policyStatus.js.map +1 -0
  56. package/dist/types/reviewPolicy/index.d.ts +14 -4
  57. package/dist/types/reviewPolicy/index.d.ts.map +1 -1
  58. package/dist/types/reviewPolicy/index.js +11 -5
  59. package/dist/types/reviewPolicy/index.js.map +1 -1
  60. package/dist/types/scim/index.d.ts +21 -1
  61. package/dist/types/scim/index.d.ts.map +1 -1
  62. package/dist/types/scim/index.js +19 -1
  63. package/dist/types/scim/index.js.map +1 -1
  64. package/dist/utils/attachment-delivery.d.ts +7 -0
  65. package/dist/utils/attachment-delivery.d.ts.map +1 -1
  66. package/dist/utils/attachment-delivery.js +19 -2
  67. package/dist/utils/attachment-delivery.js.map +1 -1
  68. package/dist/utils/attachment-delivery.test.d.ts +2 -0
  69. package/dist/utils/attachment-delivery.test.d.ts.map +1 -0
  70. package/dist/utils/attachment-delivery.test.js +40 -0
  71. package/dist/utils/attachment-delivery.test.js.map +1 -0
  72. package/dist/utils/attachment-upload.d.ts +3 -2
  73. package/dist/utils/attachment-upload.d.ts.map +1 -1
  74. package/dist/utils/attachment-upload.js +11 -3
  75. package/dist/utils/attachment-upload.js.map +1 -1
  76. package/dist/utils/attachment-upload.test.js +40 -1
  77. package/dist/utils/attachment-upload.test.js.map +1 -1
  78. package/dist/utils/attachment-validation.d.ts +64 -0
  79. package/dist/utils/attachment-validation.d.ts.map +1 -0
  80. package/dist/utils/attachment-validation.js +269 -0
  81. package/dist/utils/attachment-validation.js.map +1 -0
  82. package/dist/utils/attachment-validation.test.d.ts +2 -0
  83. package/dist/utils/attachment-validation.test.d.ts.map +1 -0
  84. package/dist/utils/attachment-validation.test.js +171 -0
  85. package/dist/utils/attachment-validation.test.js.map +1 -0
  86. package/dist/utils/index.d.ts +1 -0
  87. package/dist/utils/index.d.ts.map +1 -1
  88. package/dist/utils/index.js +1 -0
  89. package/dist/utils/index.js.map +1 -1
  90. package/dist-esm/database-lifecycle/destructiveMigrations.d.ts +18 -0
  91. package/dist-esm/database-lifecycle/destructiveMigrations.d.ts.map +1 -1
  92. package/dist-esm/database-lifecycle/destructiveMigrations.js.map +1 -1
  93. package/dist-esm/database-lifecycle/index.d.ts +8 -1
  94. package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
  95. package/dist-esm/database-lifecycle/index.js +1 -0
  96. package/dist-esm/database-lifecycle/index.js.map +1 -1
  97. package/dist-esm/dbfs/types.d.ts +1 -0
  98. package/dist-esm/dbfs/types.d.ts.map +1 -1
  99. package/dist-esm/dbfs/types.js +13 -2
  100. package/dist-esm/dbfs/types.js.map +1 -1
  101. package/dist-esm/errors/http.d.ts +5 -3
  102. package/dist-esm/errors/http.d.ts.map +1 -1
  103. package/dist-esm/errors/http.js +9 -5
  104. package/dist-esm/errors/http.js.map +1 -1
  105. package/dist-esm/types/ai/index.d.ts +1 -0
  106. package/dist-esm/types/ai/index.d.ts.map +1 -1
  107. package/dist-esm/types/ai/index.js +1 -0
  108. package/dist-esm/types/ai/index.js.map +1 -1
  109. package/dist-esm/types/audit/ocsf.d.ts +11 -0
  110. package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
  111. package/dist-esm/types/billing/index.d.ts +1 -0
  112. package/dist-esm/types/billing/index.d.ts.map +1 -1
  113. package/dist-esm/types/billing/index.js +1 -0
  114. package/dist-esm/types/billing/index.js.map +1 -1
  115. package/dist-esm/types/billing/usageSource.d.ts +26 -0
  116. package/dist-esm/types/billing/usageSource.d.ts.map +1 -0
  117. package/dist-esm/types/billing/usageSource.js +97 -0
  118. package/dist-esm/types/billing/usageSource.js.map +1 -0
  119. package/dist-esm/types/policyGate/errors.d.ts +3 -0
  120. package/dist-esm/types/policyGate/errors.d.ts.map +1 -1
  121. package/dist-esm/types/policyGate/errors.js +1 -0
  122. package/dist-esm/types/policyGate/errors.js.map +1 -1
  123. package/dist-esm/types/policyGate/index.d.ts +5 -1
  124. package/dist-esm/types/policyGate/index.d.ts.map +1 -1
  125. package/dist-esm/types/policyGate/index.js +10 -0
  126. package/dist-esm/types/policyGate/index.js.map +1 -1
  127. package/dist-esm/types/policyGate/index.test.d.ts +2 -0
  128. package/dist-esm/types/policyGate/index.test.d.ts.map +1 -0
  129. package/dist-esm/types/policyGate/index.test.js +21 -0
  130. package/dist-esm/types/policyGate/index.test.js.map +1 -0
  131. package/dist-esm/types/publishPipeline/index.d.ts +27 -0
  132. package/dist-esm/types/publishPipeline/index.d.ts.map +1 -1
  133. package/dist-esm/types/publishPipeline/index.js +1 -0
  134. package/dist-esm/types/publishPipeline/index.js.map +1 -1
  135. package/dist-esm/types/publishPipeline/plan.d.ts +9 -4
  136. package/dist-esm/types/publishPipeline/plan.d.ts.map +1 -1
  137. package/dist-esm/types/publishPipeline/plan.js +36 -19
  138. package/dist-esm/types/publishPipeline/plan.js.map +1 -1
  139. package/dist-esm/types/publishPipeline/plan.test.js +29 -9
  140. package/dist-esm/types/publishPipeline/plan.test.js.map +1 -1
  141. package/dist-esm/types/publishPipeline/policyStatus.d.ts +7 -0
  142. package/dist-esm/types/publishPipeline/policyStatus.d.ts.map +1 -0
  143. package/dist-esm/types/publishPipeline/policyStatus.js +37 -0
  144. package/dist-esm/types/publishPipeline/policyStatus.js.map +1 -0
  145. package/dist-esm/types/reviewPolicy/index.d.ts +14 -4
  146. package/dist-esm/types/reviewPolicy/index.d.ts.map +1 -1
  147. package/dist-esm/types/reviewPolicy/index.js +10 -4
  148. package/dist-esm/types/reviewPolicy/index.js.map +1 -1
  149. package/dist-esm/types/scim/index.d.ts +21 -1
  150. package/dist-esm/types/scim/index.d.ts.map +1 -1
  151. package/dist-esm/types/scim/index.js +18 -0
  152. package/dist-esm/types/scim/index.js.map +1 -1
  153. package/dist-esm/utils/attachment-delivery.d.ts +7 -0
  154. package/dist-esm/utils/attachment-delivery.d.ts.map +1 -1
  155. package/dist-esm/utils/attachment-delivery.js +17 -2
  156. package/dist-esm/utils/attachment-delivery.js.map +1 -1
  157. package/dist-esm/utils/attachment-delivery.test.d.ts +2 -0
  158. package/dist-esm/utils/attachment-delivery.test.d.ts.map +1 -0
  159. package/dist-esm/utils/attachment-delivery.test.js +38 -0
  160. package/dist-esm/utils/attachment-delivery.test.js.map +1 -0
  161. package/dist-esm/utils/attachment-upload.d.ts +3 -2
  162. package/dist-esm/utils/attachment-upload.d.ts.map +1 -1
  163. package/dist-esm/utils/attachment-upload.js +10 -2
  164. package/dist-esm/utils/attachment-upload.js.map +1 -1
  165. package/dist-esm/utils/attachment-upload.test.js +42 -3
  166. package/dist-esm/utils/attachment-upload.test.js.map +1 -1
  167. package/dist-esm/utils/attachment-validation.d.ts +64 -0
  168. package/dist-esm/utils/attachment-validation.d.ts.map +1 -0
  169. package/dist-esm/utils/attachment-validation.js +259 -0
  170. package/dist-esm/utils/attachment-validation.js.map +1 -0
  171. package/dist-esm/utils/attachment-validation.test.d.ts +2 -0
  172. package/dist-esm/utils/attachment-validation.test.d.ts.map +1 -0
  173. package/dist-esm/utils/attachment-validation.test.js +169 -0
  174. package/dist-esm/utils/attachment-validation.test.js.map +1 -0
  175. package/dist-esm/utils/index.d.ts +1 -0
  176. package/dist-esm/utils/index.d.ts.map +1 -1
  177. package/dist-esm/utils/index.js +1 -0
  178. package/dist-esm/utils/index.js.map +1 -1
  179. package/package.json +4 -3
  180. package/src/database-lifecycle/destructiveMigrations.ts +19 -0
  181. package/src/database-lifecycle/index.ts +13 -1
  182. package/src/dbfs/types.ts +15 -3
  183. package/src/errors/http.ts +11 -5
  184. package/src/types/ai/index.ts +1 -0
  185. package/src/types/audit/ocsf.ts +14 -0
  186. package/src/types/billing/index.ts +1 -0
  187. package/src/types/billing/usageSource.ts +114 -0
  188. package/src/types/policyGate/errors.ts +1 -0
  189. package/src/types/policyGate/index.test.ts +25 -0
  190. package/src/types/policyGate/index.ts +21 -1
  191. package/src/types/publishPipeline/index.ts +29 -0
  192. package/src/types/publishPipeline/plan.test.ts +34 -9
  193. package/src/types/publishPipeline/plan.ts +41 -19
  194. package/src/types/publishPipeline/policyStatus.ts +44 -0
  195. package/src/types/reviewPolicy/index.ts +18 -4
  196. package/src/types/scim/index.ts +28 -1
  197. package/src/utils/attachment-delivery.test.ts +52 -0
  198. package/src/utils/attachment-delivery.ts +20 -2
  199. package/src/utils/attachment-upload.test.ts +58 -4
  200. package/src/utils/attachment-upload.ts +13 -3
  201. package/src/utils/attachment-validation.test.ts +226 -0
  202. package/src/utils/attachment-validation.ts +269 -0
  203. package/src/utils/index.ts +1 -0
@@ -1,4 +1,9 @@
1
- import type { ReviewFindingCategory, ReviewFindingConfidence, ReviewFindingSeverity } from '../reviewPolicy/index.js';
1
+ import type {
2
+ ReviewFindingCategory,
3
+ ReviewFindingConfidence,
4
+ ReviewFindingSeverity,
5
+ ReviewPolicyRunnerType
6
+ } from '../reviewPolicy/index.js';
2
7
  import { MAX_POLICY_GATE_RUN_ATTEMPTS } from './errors.js';
3
8
 
4
9
  export * from './errors.js';
@@ -31,6 +36,8 @@ export type PolicyGateReadinessFindingSummary = {
31
36
  export type PolicyGateReadinessFinding = {
32
37
  blocking: boolean;
33
38
  humanSummary?: string | null;
39
+ /** Stable id for the underlying finding row; prefer this over hashing display text for identity. */
40
+ id?: string;
34
41
  /** Same contract as `ReviewFindingDto.locations`: primary location first. */
35
42
  locationsJson?: unknown[];
36
43
  remediationHintJson?: Record<string, unknown>;
@@ -69,6 +76,7 @@ export type PolicyGateReadinessItem = {
69
76
  policyVersionId: string;
70
77
  progress?: PolicyGateScanProgress;
71
78
  reviewRunId?: string;
79
+ runnerType?: ReviewPolicyRunnerType;
72
80
  runs?: PolicyGateReadinessRun[];
73
81
  staleReason?: string;
74
82
  status: PolicyGateReadinessItemStatus;
@@ -181,6 +189,18 @@ export type ScanScopeChangedFile = {
181
189
  path: string;
182
190
  };
183
191
 
192
+ // Git-connected and imported apps can still carry pnpm, yarn, or bun lockfiles. A package
193
+ // manager regenerating one (different resolved order, no real edit) should not by itself
194
+ // count as a code change for scan-scope purposes. Shared between the server (which builds
195
+ // ScanScopeJson) and the dev-server agent (which consumes it) so the two never drift.
196
+ const LOCKFILE_BASENAMES = new Set(['bun.lock', 'bun.lockb', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']);
197
+
198
+ export function isLockfilePath(path: string): boolean {
199
+ const lastSlashIndex = path.lastIndexOf('/');
200
+ const basename = lastSlashIndex === -1 ? path : path.slice(lastSlashIndex + 1);
201
+ return LOCKFILE_BASENAMES.has(basename);
202
+ }
203
+
184
204
  export type ScanScopePriorFinding = {
185
205
  blocking: boolean;
186
206
  category: ReviewFindingCategory;
@@ -10,6 +10,8 @@
10
10
  * this plan only reflects progress for the rail.
11
11
  */
12
12
 
13
+ import type { PolicyGateReadinessStatus } from '../policyGate/index.js';
14
+
13
15
  /** Which domain owns a step. Extend as new gates are added. */
14
16
  export type PublishStepKind = 'build' | 'database_migration' | 'database_provisioning' | 'policy_gates' | 'publish';
15
17
 
@@ -35,6 +37,23 @@ export type BuildPublishStepDetail = {
35
37
  error?: string;
36
38
  };
37
39
 
40
+ export type PolicyGatesPublishStepDetail = {
41
+ /**
42
+ * Whether the server would honor a publish that carries the findings
43
+ * acknowledgment right now. False while a gate has not reported: its findings
44
+ * are part of what the acknowledgment covers, so the deploy endpoint applies
45
+ * this same test and turns such a publish away. Also false when the gates
46
+ * could not be read at all - an unknown answer must not offer a publish that
47
+ * would come back refused.
48
+ *
49
+ * Separate from whether anyone has acknowledged anything: that is recorded on
50
+ * the deploy queue row and is already visible in each child's status.
51
+ */
52
+ findingsAckEligible: boolean;
53
+ /** Absent when readiness could not be read. */
54
+ readinessStatus?: PolicyGateReadinessStatus;
55
+ };
56
+
38
57
  export type PublishDeployStepDetail = {
39
58
  error?: string;
40
59
  isLive?: boolean;
@@ -54,6 +73,8 @@ export type PublishStep = {
54
73
  summary?: string;
55
74
  /** When status is pending and waiting on declared dependencies that are not yet clear. */
56
75
  blockedBy?: PublishStepKind[];
76
+ /** Nested status rows owned by this step. */
77
+ children?: PublishStep[];
57
78
  /** Machine code when blocked/failed (e.g. POLICY_GATE_BLOCKED). */
58
79
  blockingReason?: string;
59
80
  /** Opaque domain payload; generic pipeline must not read this. */
@@ -66,6 +87,13 @@ export type PublishPlanTarget = {
66
87
  };
67
88
 
68
89
  export type PublishPlan = {
90
+ /**
91
+ * Whether publishing this app runs policy agents, as the organization has it
92
+ * configured. Unlike the steps, this does not describe one version: the steps
93
+ * only appear once a publish of that version is under way, while an older
94
+ * version needs to know the checks exist before anyone tries.
95
+ */
96
+ hasPolicyPublishChecks?: boolean;
69
97
  status: PublishStepStatus;
70
98
  steps: PublishStep[];
71
99
  target: PublishPlanTarget;
@@ -103,3 +131,4 @@ export const PUBLISH_STEP_SPINE: readonly PublishStepId[] = [
103
131
 
104
132
  export * from './displayNames.js';
105
133
  export * from './plan.js';
134
+ export * from './policyStatus.js';
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
 
3
3
  import type { PublishStep, PublishStepStatus } from './index.js';
4
- import { buildPublishPlan, orderPublishSteps, rollUpPublishPlan } from './plan.js';
4
+ import { buildPublishPlan, orderPublishSteps, rollUpPublishSteps } from './plan.js';
5
5
 
6
6
  const TARGET = { applicationId: 'app-1', commitId: 'commit-1' };
7
7
 
@@ -14,22 +14,26 @@ function step(overrides: Partial<PublishStep> & Pick<PublishStep, 'id'>): Publis
14
14
  };
15
15
  }
16
16
 
17
- describe('rollUpPublishPlan', () => {
17
+ describe('rollUpPublishSteps', () => {
18
18
  test('an empty plan is passed', () => {
19
- expect(rollUpPublishPlan([])).toBe('passed');
19
+ expect(rollUpPublishSteps([])).toBe('passed');
20
20
  });
21
21
 
22
22
  test('the worst status wins by precedence', () => {
23
23
  expect(
24
- rollUpPublishPlan([step({ id: 'a', status: 'passed' }), step({ id: 'b', status: 'advisory' }), step({ id: 'c', status: 'blocked' })])
24
+ rollUpPublishSteps([step({ id: 'a', status: 'passed' }), step({ id: 'b', status: 'advisory' }), step({ id: 'c', status: 'blocked' })])
25
25
  ).toBe('blocked');
26
- expect(rollUpPublishPlan([step({ id: 'a', status: 'in_progress' }), step({ id: 'b', status: 'failed' })])).toBe('failed');
27
- expect(rollUpPublishPlan([step({ id: 'a', status: 'advisory' }), step({ id: 'b', status: 'passed' })])).toBe('advisory');
26
+ expect(rollUpPublishSteps([step({ id: 'a', status: 'in_progress' }), step({ id: 'b', status: 'failed' })])).toBe('failed');
27
+ expect(rollUpPublishSteps([step({ id: 'a', status: 'advisory' }), step({ id: 'b', status: 'passed' })])).toBe('advisory');
28
28
  });
29
29
 
30
30
  test('skipped never masks a real status', () => {
31
- expect(rollUpPublishPlan([step({ id: 'a', status: 'skipped' }), step({ id: 'b', status: 'passed' })])).toBe('passed');
32
- expect(rollUpPublishPlan([step({ id: 'a', status: 'skipped' })])).toBe('skipped');
31
+ expect(rollUpPublishSteps([step({ id: 'a', status: 'skipped' }), step({ id: 'b', status: 'passed' })])).toBe('passed');
32
+ expect(rollUpPublishSteps([step({ id: 'a', status: 'skipped' })])).toBe('skipped');
33
+ });
34
+
35
+ test('an unknown wire status blocks the plan', () => {
36
+ expect(rollUpPublishSteps([step({ id: 'new-server-status', status: 'new_server_status' as PublishStepStatus })])).toBe('blocked');
33
37
  });
34
38
 
35
39
  test('precedence order is exhaustive and worst-first', () => {
@@ -50,9 +54,30 @@ describe('rollUpPublishPlan', () => {
50
54
  const steps = better
51
55
  ? [step({ id: 'better', status: better }), step({ id: 'worse', status: worse })]
52
56
  : [step({ id: 'only', status: worse })];
53
- expect(rollUpPublishPlan(steps)).toBe(worse);
57
+ expect(rollUpPublishSteps(steps)).toBe(worse);
54
58
  }
55
59
  });
60
+
61
+ test('uses the plan precedence for any sibling steps', () => {
62
+ const children = [
63
+ step({ id: 'blocked-child', kind: 'policy_gates', status: 'blocked' }),
64
+ step({ id: 'running-child', kind: 'policy_gates', status: 'in_progress' })
65
+ ];
66
+
67
+ expect(rollUpPublishSteps(children)).toBe('blocked');
68
+ expect(rollUpPublishSteps([step({ id: 'pending-child', status: 'pending' }), step({ id: 'passed-child' })])).toBe('pending');
69
+ });
70
+
71
+ test('reads sibling statuses without reinterpreting nested children', () => {
72
+ const parent = step({
73
+ children: [step({ id: 'policy:security-agent', kind: 'policy_gates', status: 'failed' })],
74
+ id: 'policy_gates:before_deploy',
75
+ kind: 'policy_gates',
76
+ status: 'passed'
77
+ });
78
+
79
+ expect(rollUpPublishSteps([parent])).toBe('passed');
80
+ });
56
81
  });
57
82
 
58
83
  describe('orderPublishSteps', () => {
@@ -1,23 +1,36 @@
1
1
  import type { PublishPlan, PublishPlanTarget, PublishStep, PublishStepStatus } from './index.js';
2
2
  import { PUBLISH_STEP_SPINE } from './index.js';
3
3
 
4
- /** Worst-first. When summarizing many steps into one status, first match wins. */
5
- const STATUS_PRECEDENCE: PublishStepStatus[] = [
6
- 'failed',
7
- 'blocked',
8
- 'action_required',
9
- 'stale',
10
- 'in_progress',
11
- 'pending',
12
- 'advisory',
13
- 'passed',
14
- 'skipped'
15
- ];
4
+ /** Lower ranks are worse so every known status has an explicit precedence. */
5
+ const STATUS_RANK: Record<PublishStepStatus, number> = {
6
+ failed: 0,
7
+ blocked: 1,
8
+ action_required: 2,
9
+ stale: 3,
10
+ in_progress: 4,
11
+ pending: 5,
12
+ advisory: 6,
13
+ passed: 7,
14
+ skipped: 8
15
+ };
16
16
 
17
- /** Collapse ordered steps into the single status shown on the plan. Empty = passed. */
18
- export function rollUpPublishPlan(steps: PublishStep[]): PublishStepStatus {
19
- const present = new Set(steps.map((s) => s.status));
20
- return STATUS_PRECEDENCE.find((s) => present.has(s)) ?? 'passed';
17
+ /** Collapse sibling steps into one status. Empty = passed. */
18
+ export function rollUpPublishSteps(steps: Pick<PublishStep, 'status'>[]): PublishStepStatus {
19
+ if (steps.length === 0) {
20
+ return 'passed';
21
+ }
22
+
23
+ let worst: PublishStepStatus = 'skipped';
24
+ for (const step of steps) {
25
+ const rank = STATUS_RANK[step.status];
26
+ if (rank === undefined) {
27
+ return 'blocked';
28
+ }
29
+ if (rank < STATUS_RANK[worst]) {
30
+ worst = step.status;
31
+ }
32
+ }
33
+ return worst;
21
34
  }
22
35
 
23
36
  /** Order steps by the fixed spine; unknown ids are appended in input order. */
@@ -40,8 +53,17 @@ export function orderPublishSteps(steps: PublishStep[]): PublishStep[] {
40
53
  return ordered;
41
54
  }
42
55
 
43
- /** Order and roll up. Assemble resolves each step's declared `blockedBy`. */
44
- export function buildPublishPlan(target: PublishPlanTarget, steps: PublishStep[]): PublishPlan {
56
+ /**
57
+ * Order and roll up. Assemble resolves each step's declared `blockedBy`.
58
+ *
59
+ * `facts` carries what the plan says about the app rather than about its steps,
60
+ * so rebuilding a plan from a response keeps it instead of dropping it.
61
+ */
62
+ export function buildPublishPlan(
63
+ target: PublishPlanTarget,
64
+ steps: PublishStep[],
65
+ facts?: Pick<PublishPlan, 'hasPolicyPublishChecks'>
66
+ ): PublishPlan {
45
67
  const ordered = orderPublishSteps(steps);
46
- return { status: rollUpPublishPlan(ordered), steps: ordered, target };
68
+ return { ...facts, status: rollUpPublishSteps(ordered), steps: ordered, target };
47
69
  }
@@ -0,0 +1,44 @@
1
+ import { isHardBlockingPolicyItem, isPublishHoldingPolicyItem, type PolicyGateReadinessItem } from '../policyGate/index.js';
2
+ import { isAdvisoryHoldFindingSeverity } from '../reviewPolicy/index.js';
3
+ import type { PublishStepStatus } from './index.js';
4
+
5
+ export function policyItemPublishStatus(
6
+ item: PolicyGateReadinessItem,
7
+ options: {
8
+ blockingHoldAcknowledged?: boolean;
9
+ blockingHoldAckEligible?: boolean;
10
+ } = {}
11
+ ): PublishStepStatus {
12
+ const { blockingHoldAcknowledged = false, blockingHoldAckEligible = false } = options;
13
+ switch (item.status) {
14
+ case 'failed':
15
+ if (isPublishHoldingPolicyItem(item)) {
16
+ return blockingHoldAcknowledged && blockingHoldAckEligible ? 'advisory' : 'blocked';
17
+ }
18
+ return isHardBlockingPolicyItem(item) ? 'failed' : 'advisory';
19
+ case 'blocked':
20
+ if (isHardBlockingPolicyItem(item)) {
21
+ return 'blocked';
22
+ }
23
+ if (isPublishHoldingPolicyItem(item)) {
24
+ return blockingHoldAcknowledged && blockingHoldAckEligible ? 'advisory' : 'blocked';
25
+ }
26
+ return 'advisory';
27
+ case 'approval_required':
28
+ return 'action_required';
29
+ case 'stale':
30
+ return 'stale';
31
+ case 'running':
32
+ return 'in_progress';
33
+ case 'missing':
34
+ return 'pending';
35
+ case 'advisory_findings':
36
+ return (item.findings ?? []).some((finding) => isAdvisoryHoldFindingSeverity(finding.severity)) ? 'advisory' : 'passed';
37
+ case 'passed':
38
+ return 'passed';
39
+ default: {
40
+ const _exhaustive: never = item.status;
41
+ return _exhaustive;
42
+ }
43
+ }
44
+ }
@@ -12,19 +12,26 @@ export const BUILT_IN_SECURITY_SCAN_NAME = 'Superblocks Security Scan';
12
12
  // by the platform, so admins can only enable it and choose its mode.
13
13
  export const SUPERBLOCKS_SECURITY_AGENT_NAME = 'Superblocks Security Agent';
14
14
 
15
- export const REVIEW_POLICY_TYPES = ['built_in_security_scan', 'security_agent'] as const;
15
+ // Display name of the Superblocks-managed pre-publish dependency CVE check. One per organization;
16
+ // runs synchronously against the vulnerability index, so it needs no runner config to enable.
17
+ export const DEPENDENCY_VULNERABILITY_SCAN_NAME = 'Superblocks Dependency Vulnerability Scan';
18
+
19
+ export const REVIEW_POLICY_TYPES = ['built_in_security_scan', 'security_agent', 'dependency_vulnerability_scan'] as const;
16
20
  export const REVIEW_POLICY_SOURCES = ['admin_configured', 'superblocks_managed'] as const;
17
21
  export const REVIEW_POLICY_TRIGGERS = ['before_deploy', 'after_checkpoint', 'manual'] as const;
18
22
  export const REVIEW_POLICY_EXECUTION_CONTEXTS = [
19
23
  'edit_mode_app',
20
24
  'superblocks_sabs_built_assets',
21
25
  'superblocks_sabs_dependency_metadata',
22
- 'superblocks_sabs_source_snapshot'
26
+ 'superblocks_sabs_source_snapshot',
27
+ 'dependency_manifest'
23
28
  ] as const;
24
- export const REVIEW_POLICY_RUNTIMES = ['superblocks_dev_server_agent', 'superblocks_sabs'] as const;
29
+ // `superblocks_server` runs in-process on the API server (no external runner), for checks like the
30
+ // dependency vulnerability scan that only need data already indexed at commit time.
31
+ export const REVIEW_POLICY_RUNTIMES = ['superblocks_dev_server_agent', 'superblocks_sabs', 'superblocks_server'] as const;
25
32
  // Ordered from least to most strict; the mode select renders in array order.
26
33
  export const REVIEW_POLICY_MODES = ['advisory', 'blocking_with_exceptions', 'blocking'] as const;
27
- export const REVIEW_POLICY_RUNNER_TYPES = ['built_in_security_scan', 'llm_security_agent'] as const;
34
+ export const REVIEW_POLICY_RUNNER_TYPES = ['built_in_security_scan', 'llm_security_agent', 'dependency_vulnerability_scan'] as const;
28
35
  export const REVIEW_FINDING_SOURCE_PHASES = ['built_asset', 'dependency', 'edit_mode_app', 'iac', 'image', 'source'] as const;
29
36
  export const REVIEW_FINDING_CATEGORIES = [
30
37
  'external_policy',
@@ -262,6 +269,7 @@ export interface ReviewPolicySummaryDto {
262
269
 
263
270
  export type CreateReviewPolicyBody =
264
271
  | { policyType: 'built_in_security_scan'; mode?: ReviewPolicyMode; scope?: ReviewPolicyScopeInput }
272
+ | { policyType: 'dependency_vulnerability_scan'; mode?: ReviewPolicyMode; scope?: ReviewPolicyScopeInput }
265
273
  | {
266
274
  policyType: 'security_agent';
267
275
  source: 'superblocks_managed';
@@ -284,6 +292,12 @@ export type UpdateReviewPolicyConfigBody =
284
292
  mode: ReviewPolicyMode;
285
293
  scope?: ReviewPolicyScopeInput;
286
294
  }
295
+ | {
296
+ activeVersionId: string;
297
+ policyType: 'dependency_vulnerability_scan';
298
+ mode: ReviewPolicyMode;
299
+ scope?: ReviewPolicyScopeInput;
300
+ }
287
301
  | {
288
302
  activeVersionId: string;
289
303
  policyType: 'security_agent';
@@ -4,9 +4,36 @@ export enum ScimSchema {
4
4
  USER_ENTERPRISE = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
5
5
  LIST = 'urn:ietf:params:scim:api:messages:2.0:ListResponse',
6
6
  PATCH = 'urn:ietf:params:scim:api:messages:2.0:PatchOp',
7
- GROUP = 'urn:ietf:params:scim:schemas:core:2.0:Group'
7
+ GROUP = 'urn:ietf:params:scim:schemas:core:2.0:Group',
8
+ ERROR = 'urn:ietf:params:scim:api:messages:2.0:Error'
8
9
  }
9
10
 
11
+ // RFC 7644 section 3.1: the media type SCIM requests and responses use.
12
+ export const SCIM_CONTENT_TYPE = 'application/scim+json';
13
+
14
+ // The ten keywords of RFC 7644 Table 9 (section 3.12). An IdP reads this to decide whether a
15
+ // failure is permanent or worth retrying, so a wrong keyword is worse than none.
16
+ export enum ScimErrorType {
17
+ invalidFilter = 'invalidFilter',
18
+ tooMany = 'tooMany',
19
+ uniqueness = 'uniqueness',
20
+ mutability = 'mutability',
21
+ invalidSyntax = 'invalidSyntax',
22
+ invalidPath = 'invalidPath',
23
+ noTarget = 'noTarget',
24
+ invalidValue = 'invalidValue',
25
+ invalidVers = 'invalidVers',
26
+ sensitive = 'sensitive'
27
+ }
28
+
29
+ // RFC 7644 section 3.12: `status` is the HTTP status as a string, and `scimType` is optional.
30
+ export type ScimError = {
31
+ schemas: [ScimSchema.ERROR];
32
+ status: string;
33
+ detail: string;
34
+ scimType?: ScimErrorType;
35
+ };
36
+
10
37
  export enum OperationCode {
11
38
  replace = 'replace',
12
39
  add = 'add',
@@ -0,0 +1,52 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { classifyAttachmentDelivery, isDumpAttachment, isSupportedAttachmentUploadType } from './attachment-delivery.js';
4
+
5
+ describe('dump attachments', () => {
6
+ test('detects .dump by filename regardless of mime type', () => {
7
+ expect(isDumpAttachment({ fileName: 'orders_prod.dump' })).toBe(true);
8
+ expect(isDumpAttachment({ fileName: 'orders_prod.dump', mimeType: 'application/octet-stream' })).toBe(true);
9
+ expect(isDumpAttachment({ fileName: 'orders_prod.dump', mimeType: 'text/plain' })).toBe(true);
10
+ expect(isDumpAttachment({ fileName: 'orders_prod.dump', mimeType: '' })).toBe(true);
11
+ expect(isDumpAttachment({ fileName: 'orders.sql' })).toBe(false);
12
+ expect(isDumpAttachment({ fileName: 'archive.zip' })).toBe(false);
13
+ });
14
+
15
+ test('detects dumps by dedicated mime type when filename is missing', () => {
16
+ expect(isDumpAttachment({ mimeType: 'application/x-postgresql-dump' })).toBe(true);
17
+ expect(
18
+ isSupportedAttachmentUploadType({
19
+ mimeType: 'application/x-postgresql-dump'
20
+ })
21
+ ).toBe(true);
22
+ expect(
23
+ classifyAttachmentDelivery({
24
+ mimeType: 'application/x-postgresql-dump'
25
+ })
26
+ ).toBe('reference_only');
27
+ });
28
+
29
+ test('classifies dumps as reference-only so they are not inlined to the model', () => {
30
+ expect(
31
+ classifyAttachmentDelivery({
32
+ fileName: 'orders_prod.dump',
33
+ mimeType: 'application/octet-stream'
34
+ })
35
+ ).toBe('reference_only');
36
+ });
37
+
38
+ test('allows dump uploads as a supported attachment type', () => {
39
+ expect(
40
+ isSupportedAttachmentUploadType({
41
+ fileName: 'orders_prod.dump',
42
+ mimeType: 'application/octet-stream'
43
+ })
44
+ ).toBe(true);
45
+ expect(
46
+ isSupportedAttachmentUploadType({
47
+ fileName: 'orders_prod.dump',
48
+ mimeType: ''
49
+ })
50
+ ).toBe(true);
51
+ });
52
+ });
@@ -61,8 +61,26 @@ export function detectAttachmentArchiveType(input: AttachmentTypeInput): Attachm
61
61
  return null;
62
62
  }
63
63
 
64
+ /**
65
+ * Postgres custom-format dumps (`pg_dump -Fc`) use the `.dump` extension and are
66
+ * binary. They must stay reference-only (uploaded to Bucketeer, downloaded via
67
+ * tool) rather than being coerced to text/plain for the model.
68
+ */
69
+ export function isDumpAttachment(input: AttachmentTypeInput): boolean {
70
+ if (normalizeFileName(input.fileName).endsWith('.dump')) {
71
+ return true;
72
+ }
73
+
74
+ const mimeType = normalizeMimeType(input.mimeType);
75
+ return mimeType === 'application/x-postgresql-dump' || mimeType === 'application/vnd.postgresql.dump';
76
+ }
77
+
78
+ export function isReferenceOnlySupportedAttachment(input: AttachmentTypeInput): boolean {
79
+ return detectAttachmentArchiveType(input) !== null || isDumpAttachment(input);
80
+ }
81
+
64
82
  export function classifyAttachmentDelivery(input: AttachmentTypeInput): AttachmentDeliveryMode {
65
- if (detectAttachmentArchiveType(input)) {
83
+ if (isReferenceOnlySupportedAttachment(input)) {
66
84
  return 'reference_only';
67
85
  }
68
86
 
@@ -84,7 +102,7 @@ export function classifyAttachmentDelivery(input: AttachmentTypeInput): Attachme
84
102
  }
85
103
 
86
104
  export function isSupportedAttachmentUploadType(input: AttachmentTypeInput): boolean {
87
- if (detectAttachmentArchiveType(input)) {
105
+ if (isReferenceOnlySupportedAttachment(input)) {
88
106
  return true;
89
107
  }
90
108
 
@@ -1,16 +1,20 @@
1
- import { describe, expect, test } from 'vitest';
1
+ import { describe, expect, test, vi } from 'vitest';
2
2
 
3
3
  import {
4
+ ATTACHMENT_INVALID_CODE,
4
5
  ATTACHMENT_CONTENT_MISMATCH_CODE,
6
+ AttachmentUploadApiError,
5
7
  buildAttachmentUploadMetadata,
6
8
  buildAttachmentUploadFormData,
7
9
  createUploadedAttachmentFromServer,
8
10
  getAttachmentMimeTypeFromFileName,
9
11
  getTextAttachmentType,
10
- parseUserFacingUploadError,
11
12
  isOfficeXml,
13
+ isTextLikeMimeType,
12
14
  normalizeAttachmentMimeType,
13
- toAttachmentUploadDescriptor
15
+ parseUserFacingUploadError,
16
+ toAttachmentUploadDescriptor,
17
+ uploadAttachmentToServerApi
14
18
  } from './attachment-upload.js';
15
19
 
16
20
  describe('getAttachmentMimeTypeFromFileName', () => {
@@ -24,7 +28,8 @@ describe('getAttachmentMimeTypeFromFileName', () => {
24
28
  ['report.pdf', 'application/pdf'],
25
29
  ['server.log', 'text/plain'],
26
30
  ['rows.csv', 'text/csv'],
27
- ['config.json', 'application/json']
31
+ ['config.json', 'application/json'],
32
+ ['orders_prod.dump', 'application/x-postgresql-dump']
28
33
  ])('reads %s as %s', (fileName, expected) => {
29
34
  expect(getAttachmentMimeTypeFromFileName(fileName)).toBe(expected);
30
35
  });
@@ -183,6 +188,17 @@ describe('buildAttachmentUploadMetadata', () => {
183
188
  });
184
189
 
185
190
  describe('normalizeAttachmentMimeType', () => {
191
+ test('preserves postgres dump mime types as binary attachments', () => {
192
+ expect(isTextLikeMimeType('application/x-postgresql-dump')).toBe(false);
193
+ expect(
194
+ normalizeAttachmentMimeType({
195
+ fileName: 'orders_prod.dump',
196
+ mimeType: 'application/x-postgresql-dump'
197
+ })
198
+ ).toBe('application/x-postgresql-dump');
199
+ expect(normalizeAttachmentMimeType({ mimeType: 'application/vnd.postgresql.dump' })).toBe('application/vnd.postgresql.dump');
200
+ });
201
+
186
202
  test('prefers text-safe mime types for text-like filenames', () => {
187
203
  expect(
188
204
  normalizeAttachmentMimeType({
@@ -298,6 +314,14 @@ describe('parseUserFacingUploadError', () => {
298
314
  });
299
315
  });
300
316
 
317
+ test('extracts attachment validation errors', () => {
318
+ const body = JSON.stringify({ code: ATTACHMENT_INVALID_CODE, error: 'report.pdf: This file could not be read.' });
319
+ expect(parseUserFacingUploadError(body)).toEqual({
320
+ code: ATTACHMENT_INVALID_CODE,
321
+ message: 'report.pdf: This file could not be read.'
322
+ });
323
+ });
324
+
301
325
  // `{ error }` is the upload route's generic error channel, used for internal
302
326
  // 500/502 diagnostics too ('App scoped token is required...', 'Bucketeer
303
327
  // returned an invalid response', and raw Bucketeer response text). Without a
@@ -324,3 +348,33 @@ describe('parseUserFacingUploadError', () => {
324
348
  expect(parseUserFacingUploadError(body)).toBeUndefined();
325
349
  });
326
350
  });
351
+
352
+ describe('uploadAttachmentToServerApi', () => {
353
+ test('throws a typed user-facing error for a validation 413', async () => {
354
+ const fetchMock = vi
355
+ .fn()
356
+ .mockResolvedValue(
357
+ new Response(JSON.stringify({ code: ATTACHMENT_INVALID_CODE, error: 'large.txt: This file is too large.' }), { status: 413 })
358
+ );
359
+ vi.stubGlobal('fetch', fetchMock);
360
+ const descriptor = toAttachmentUploadDescriptor(new File(['hello'], 'large.txt', { type: 'text/plain' }));
361
+ if (!descriptor) throw new Error('expected attachment descriptor');
362
+
363
+ await expect(
364
+ uploadAttachmentToServerApi({
365
+ authorization: 'Bearer token',
366
+ baseUrl: 'https://superblocks.example',
367
+ descriptor,
368
+ purpose: 'attachment',
369
+ scopeType: 'org'
370
+ })
371
+ ).rejects.toEqual(
372
+ expect.objectContaining<Partial<AttachmentUploadApiError>>({
373
+ code: ATTACHMENT_INVALID_CODE,
374
+ message: 'large.txt: This file is too large.',
375
+ status: 413
376
+ })
377
+ );
378
+ vi.unstubAllGlobals();
379
+ });
380
+ });
@@ -31,6 +31,8 @@ const TEXT_ATTACHMENT_MIME_TYPES = {
31
31
  yaml: 'text/yaml'
32
32
  } as const;
33
33
 
34
+ const POSTGRES_DUMP_MIME_TYPES = new Set(['application/vnd.postgresql.dump', 'application/x-postgresql-dump']);
35
+
34
36
  export const TEXT_LIKE_FILE_EXTENSIONS = [
35
37
  '.txt',
36
38
  '.text',
@@ -213,6 +215,10 @@ function normalizeNonEmptyString(value: unknown): string | undefined {
213
215
 
214
216
  export function isTextLikeMimeType(mimeType: string): boolean {
215
217
  const normalizedMimeType = mimeType.toLowerCase();
218
+ if (POSTGRES_DUMP_MIME_TYPES.has(normalizedMimeType)) {
219
+ return false;
220
+ }
221
+
216
222
  return (
217
223
  normalizedMimeType.startsWith('text/') ||
218
224
  normalizedMimeType.includes('json') ||
@@ -315,6 +321,9 @@ export function getAttachmentMimeTypeFromFileName(fileName?: string): string | u
315
321
  if (normalizedFileName.endsWith('.jpeg')) {
316
322
  return 'image/jpeg';
317
323
  }
324
+ if (normalizedFileName.endsWith('.dump')) {
325
+ return 'application/x-postgresql-dump';
326
+ }
318
327
 
319
328
  return getTextLikeMimeTypeFromFileName(normalizedFileName);
320
329
  }
@@ -681,9 +690,10 @@ export async function uploadAttachmentToServerApi(params: {
681
690
  * internals into the UI, so the verbatim path is gated on this code rather
682
691
  * than on the presence of the field.
683
692
  */
684
- export type UserFacingUploadErrorCode = 'attachment_content_mismatch';
693
+ export type UserFacingUploadErrorCode = 'attachment_content_mismatch' | 'attachment_invalid';
685
694
 
686
695
  export const ATTACHMENT_CONTENT_MISMATCH_CODE: UserFacingUploadErrorCode = 'attachment_content_mismatch';
696
+ export const ATTACHMENT_INVALID_CODE: UserFacingUploadErrorCode = 'attachment_invalid';
687
697
 
688
698
  export class AttachmentUploadApiError extends Error {
689
699
  readonly code: UserFacingUploadErrorCode;
@@ -710,12 +720,12 @@ function readStringProperty(value: object, key: string): string | undefined {
710
720
  }
711
721
 
712
722
  function isUserFacingUploadErrorCode(code: string): code is UserFacingUploadErrorCode {
713
- return code === ATTACHMENT_CONTENT_MISMATCH_CODE;
723
+ return code === ATTACHMENT_CONTENT_MISMATCH_CODE || code === ATTACHMENT_INVALID_CODE;
714
724
  }
715
725
 
716
726
  /**
717
727
  * Pull the server's user-facing `{ code, error }` pair out of a response body.
718
- * Returns undefined unless `code` is {@link ATTACHMENT_CONTENT_MISMATCH_CODE},
728
+ * Returns undefined unless `code` is a recognized user-facing upload error,
719
729
  * and for non-JSON bodies, non-string `error` fields, and blank strings, so
720
730
  * the caller keeps the diagnostic form that still carries the HTTP status.
721
731
  */