@superblocksteam/shared 0.9613.2 → 0.9614.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database-lifecycle/destructiveMigrations.d.ts +18 -0
- package/dist/database-lifecycle/destructiveMigrations.d.ts.map +1 -1
- package/dist/database-lifecycle/destructiveMigrations.js.map +1 -1
- package/dist/database-lifecycle/index.d.ts +8 -1
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +2 -1
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/dbfs/types.d.ts +1 -0
- package/dist/dbfs/types.d.ts.map +1 -1
- package/dist/dbfs/types.js +14 -3
- package/dist/dbfs/types.js.map +1 -1
- package/dist/errors/http.d.ts +5 -3
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +9 -5
- package/dist/errors/http.js.map +1 -1
- package/dist/types/ai/index.d.ts +1 -0
- package/dist/types/ai/index.d.ts.map +1 -1
- package/dist/types/ai/index.js +1 -0
- package/dist/types/ai/index.js.map +1 -1
- package/dist/types/audit/ocsf.d.ts +11 -0
- package/dist/types/audit/ocsf.d.ts.map +1 -1
- package/dist/types/billing/index.d.ts +1 -0
- package/dist/types/billing/index.d.ts.map +1 -1
- package/dist/types/billing/index.js +1 -0
- package/dist/types/billing/index.js.map +1 -1
- package/dist/types/billing/usageSource.d.ts +26 -0
- package/dist/types/billing/usageSource.d.ts.map +1 -0
- package/dist/types/billing/usageSource.js +100 -0
- package/dist/types/billing/usageSource.js.map +1 -0
- package/dist/types/policyGate/errors.d.ts +3 -0
- package/dist/types/policyGate/errors.d.ts.map +1 -1
- package/dist/types/policyGate/errors.js +1 -0
- package/dist/types/policyGate/errors.js.map +1 -1
- package/dist/types/policyGate/index.d.ts +5 -1
- package/dist/types/policyGate/index.d.ts.map +1 -1
- package/dist/types/policyGate/index.js +11 -0
- package/dist/types/policyGate/index.js.map +1 -1
- package/dist/types/policyGate/index.test.d.ts +2 -0
- package/dist/types/policyGate/index.test.d.ts.map +1 -0
- package/dist/types/policyGate/index.test.js +23 -0
- package/dist/types/policyGate/index.test.js.map +1 -0
- package/dist/types/publishPipeline/index.d.ts +27 -0
- package/dist/types/publishPipeline/index.d.ts.map +1 -1
- package/dist/types/publishPipeline/index.js +1 -0
- package/dist/types/publishPipeline/index.js.map +1 -1
- package/dist/types/publishPipeline/plan.d.ts +9 -4
- package/dist/types/publishPipeline/plan.d.ts.map +1 -1
- package/dist/types/publishPipeline/plan.js +37 -20
- package/dist/types/publishPipeline/plan.js.map +1 -1
- package/dist/types/publishPipeline/plan.test.js +28 -8
- package/dist/types/publishPipeline/plan.test.js.map +1 -1
- package/dist/types/publishPipeline/policyStatus.d.ts +7 -0
- package/dist/types/publishPipeline/policyStatus.d.ts.map +1 -0
- package/dist/types/publishPipeline/policyStatus.js +40 -0
- package/dist/types/publishPipeline/policyStatus.js.map +1 -0
- package/dist/types/reviewPolicy/index.d.ts +14 -4
- package/dist/types/reviewPolicy/index.d.ts.map +1 -1
- package/dist/types/reviewPolicy/index.js +11 -5
- package/dist/types/reviewPolicy/index.js.map +1 -1
- package/dist/types/scim/index.d.ts +21 -1
- package/dist/types/scim/index.d.ts.map +1 -1
- package/dist/types/scim/index.js +19 -1
- package/dist/types/scim/index.js.map +1 -1
- package/dist/utils/attachment-delivery.d.ts +7 -0
- package/dist/utils/attachment-delivery.d.ts.map +1 -1
- package/dist/utils/attachment-delivery.js +19 -2
- package/dist/utils/attachment-delivery.js.map +1 -1
- package/dist/utils/attachment-delivery.test.d.ts +2 -0
- package/dist/utils/attachment-delivery.test.d.ts.map +1 -0
- package/dist/utils/attachment-delivery.test.js +40 -0
- package/dist/utils/attachment-delivery.test.js.map +1 -0
- package/dist/utils/attachment-upload.d.ts +3 -2
- package/dist/utils/attachment-upload.d.ts.map +1 -1
- package/dist/utils/attachment-upload.js +11 -3
- package/dist/utils/attachment-upload.js.map +1 -1
- package/dist/utils/attachment-upload.test.js +40 -1
- package/dist/utils/attachment-upload.test.js.map +1 -1
- package/dist/utils/attachment-validation.d.ts +64 -0
- package/dist/utils/attachment-validation.d.ts.map +1 -0
- package/dist/utils/attachment-validation.js +269 -0
- package/dist/utils/attachment-validation.js.map +1 -0
- package/dist/utils/attachment-validation.test.d.ts +2 -0
- package/dist/utils/attachment-validation.test.d.ts.map +1 -0
- package/dist/utils/attachment-validation.test.js +171 -0
- package/dist/utils/attachment-validation.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist-esm/database-lifecycle/destructiveMigrations.d.ts +18 -0
- package/dist-esm/database-lifecycle/destructiveMigrations.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/destructiveMigrations.js.map +1 -1
- package/dist-esm/database-lifecycle/index.d.ts +8 -1
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +1 -0
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/dbfs/types.d.ts +1 -0
- package/dist-esm/dbfs/types.d.ts.map +1 -1
- package/dist-esm/dbfs/types.js +13 -2
- package/dist-esm/dbfs/types.js.map +1 -1
- package/dist-esm/errors/http.d.ts +5 -3
- package/dist-esm/errors/http.d.ts.map +1 -1
- package/dist-esm/errors/http.js +9 -5
- package/dist-esm/errors/http.js.map +1 -1
- package/dist-esm/types/ai/index.d.ts +1 -0
- package/dist-esm/types/ai/index.d.ts.map +1 -1
- package/dist-esm/types/ai/index.js +1 -0
- package/dist-esm/types/ai/index.js.map +1 -1
- package/dist-esm/types/audit/ocsf.d.ts +11 -0
- package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
- package/dist-esm/types/billing/index.d.ts +1 -0
- package/dist-esm/types/billing/index.d.ts.map +1 -1
- package/dist-esm/types/billing/index.js +1 -0
- package/dist-esm/types/billing/index.js.map +1 -1
- package/dist-esm/types/billing/usageSource.d.ts +26 -0
- package/dist-esm/types/billing/usageSource.d.ts.map +1 -0
- package/dist-esm/types/billing/usageSource.js +97 -0
- package/dist-esm/types/billing/usageSource.js.map +1 -0
- package/dist-esm/types/policyGate/errors.d.ts +3 -0
- package/dist-esm/types/policyGate/errors.d.ts.map +1 -1
- package/dist-esm/types/policyGate/errors.js +1 -0
- package/dist-esm/types/policyGate/errors.js.map +1 -1
- package/dist-esm/types/policyGate/index.d.ts +5 -1
- package/dist-esm/types/policyGate/index.d.ts.map +1 -1
- package/dist-esm/types/policyGate/index.js +10 -0
- package/dist-esm/types/policyGate/index.js.map +1 -1
- package/dist-esm/types/policyGate/index.test.d.ts +2 -0
- package/dist-esm/types/policyGate/index.test.d.ts.map +1 -0
- package/dist-esm/types/policyGate/index.test.js +21 -0
- package/dist-esm/types/policyGate/index.test.js.map +1 -0
- package/dist-esm/types/publishPipeline/index.d.ts +27 -0
- package/dist-esm/types/publishPipeline/index.d.ts.map +1 -1
- package/dist-esm/types/publishPipeline/index.js +1 -0
- package/dist-esm/types/publishPipeline/index.js.map +1 -1
- package/dist-esm/types/publishPipeline/plan.d.ts +9 -4
- package/dist-esm/types/publishPipeline/plan.d.ts.map +1 -1
- package/dist-esm/types/publishPipeline/plan.js +36 -19
- package/dist-esm/types/publishPipeline/plan.js.map +1 -1
- package/dist-esm/types/publishPipeline/plan.test.js +29 -9
- package/dist-esm/types/publishPipeline/plan.test.js.map +1 -1
- package/dist-esm/types/publishPipeline/policyStatus.d.ts +7 -0
- package/dist-esm/types/publishPipeline/policyStatus.d.ts.map +1 -0
- package/dist-esm/types/publishPipeline/policyStatus.js +37 -0
- package/dist-esm/types/publishPipeline/policyStatus.js.map +1 -0
- package/dist-esm/types/reviewPolicy/index.d.ts +14 -4
- package/dist-esm/types/reviewPolicy/index.d.ts.map +1 -1
- package/dist-esm/types/reviewPolicy/index.js +10 -4
- package/dist-esm/types/reviewPolicy/index.js.map +1 -1
- package/dist-esm/types/scim/index.d.ts +21 -1
- package/dist-esm/types/scim/index.d.ts.map +1 -1
- package/dist-esm/types/scim/index.js +18 -0
- package/dist-esm/types/scim/index.js.map +1 -1
- package/dist-esm/utils/attachment-delivery.d.ts +7 -0
- package/dist-esm/utils/attachment-delivery.d.ts.map +1 -1
- package/dist-esm/utils/attachment-delivery.js +17 -2
- package/dist-esm/utils/attachment-delivery.js.map +1 -1
- package/dist-esm/utils/attachment-delivery.test.d.ts +2 -0
- package/dist-esm/utils/attachment-delivery.test.d.ts.map +1 -0
- package/dist-esm/utils/attachment-delivery.test.js +38 -0
- package/dist-esm/utils/attachment-delivery.test.js.map +1 -0
- package/dist-esm/utils/attachment-upload.d.ts +3 -2
- package/dist-esm/utils/attachment-upload.d.ts.map +1 -1
- package/dist-esm/utils/attachment-upload.js +10 -2
- package/dist-esm/utils/attachment-upload.js.map +1 -1
- package/dist-esm/utils/attachment-upload.test.js +42 -3
- package/dist-esm/utils/attachment-upload.test.js.map +1 -1
- package/dist-esm/utils/attachment-validation.d.ts +64 -0
- package/dist-esm/utils/attachment-validation.d.ts.map +1 -0
- package/dist-esm/utils/attachment-validation.js +259 -0
- package/dist-esm/utils/attachment-validation.js.map +1 -0
- package/dist-esm/utils/attachment-validation.test.d.ts +2 -0
- package/dist-esm/utils/attachment-validation.test.d.ts.map +1 -0
- package/dist-esm/utils/attachment-validation.test.js +169 -0
- package/dist-esm/utils/attachment-validation.test.js.map +1 -0
- package/dist-esm/utils/index.d.ts +1 -0
- package/dist-esm/utils/index.d.ts.map +1 -1
- package/dist-esm/utils/index.js +1 -0
- package/dist-esm/utils/index.js.map +1 -1
- package/package.json +4 -3
- package/src/database-lifecycle/destructiveMigrations.ts +19 -0
- package/src/database-lifecycle/index.ts +13 -1
- package/src/dbfs/types.ts +15 -3
- package/src/errors/http.ts +11 -5
- package/src/types/ai/index.ts +1 -0
- package/src/types/audit/ocsf.ts +14 -0
- package/src/types/billing/index.ts +1 -0
- package/src/types/billing/usageSource.ts +114 -0
- package/src/types/policyGate/errors.ts +1 -0
- package/src/types/policyGate/index.test.ts +25 -0
- package/src/types/policyGate/index.ts +21 -1
- package/src/types/publishPipeline/index.ts +29 -0
- package/src/types/publishPipeline/plan.test.ts +34 -9
- package/src/types/publishPipeline/plan.ts +41 -19
- package/src/types/publishPipeline/policyStatus.ts +44 -0
- package/src/types/reviewPolicy/index.ts +18 -4
- package/src/types/scim/index.ts +28 -1
- package/src/utils/attachment-delivery.test.ts +52 -0
- package/src/utils/attachment-delivery.ts +20 -2
- package/src/utils/attachment-upload.test.ts +58 -4
- package/src/utils/attachment-upload.ts +13 -3
- package/src/utils/attachment-validation.test.ts +226 -0
- package/src/utils/attachment-validation.ts +269 -0
- package/src/utils/index.ts +1 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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,
|
|
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('
|
|
17
|
+
describe('rollUpPublishSteps', () => {
|
|
18
18
|
test('an empty plan is passed', () => {
|
|
19
|
-
expect(
|
|
19
|
+
expect(rollUpPublishSteps([])).toBe('passed');
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test('the worst status wins by precedence', () => {
|
|
23
23
|
expect(
|
|
24
|
-
|
|
24
|
+
rollUpPublishSteps([step({ id: 'a', status: 'passed' }), step({ id: 'b', status: 'advisory' }), step({ id: 'c', status: 'blocked' })])
|
|
25
25
|
).toBe('blocked');
|
|
26
|
-
expect(
|
|
27
|
-
expect(
|
|
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(
|
|
32
|
-
expect(
|
|
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(
|
|
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
|
-
/**
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
/**
|
|
44
|
-
|
|
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:
|
|
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
|
-
|
|
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 Vulnerability Detection';
|
|
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
|
-
|
|
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';
|
package/src/types/scim/index.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
*/
|